diff --git a/README.md b/README.md index 9f15c510..17936bf0 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,33 @@ ## *PATHFINDER* Mapping tool for [*EVE ONLINE*](https://www.eveonline.com) -- Project [https://www.pathfinder-w.space](https://www.pathfinder-w.space) +- Project URL [https://www.pathfinder-w.space](https://www.pathfinder-w.space) +- **NEW** Features: [checkout](https://www.pathfinder-w.space#pf-landing-gallery) - Official Forum post [https://forums.eveonline.com](https://forums.eveonline.com/default.aspx?g=posts&m=6021776#post6021776) - Screenshots [imgur.com](http://imgur.com/a/k2aVa) - Video [youtube.com](https://www.youtube.com/channel/UC7HU7XEoMbqRwqxDTbMjSPg) -- Community [google +](https://plus.google.com/u/0/b/110257318165279088853/110257318165279088853) +- Community [google +](https://plus.google.com/+Pathfinder-wSpace) - Licence [MIT](http://opensource.org/licenses/MIT) -##### IMPORTANT Information: -**The setup and installation process in ``1.0.0RC1`` and is not backwards compatible with previous beta releases (check wiki)!** +#### Development +- **NEW** Test server + - URL: http://www.dev.pathfinder-w.space + - Running current `develop` branch + - Using Singularity (SISI) CREST (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 chat [Slack](https://slack.com) : + - https://pathfinder-eve-online.slack.com + - Please send me a mail for invite: 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.** -If you are looking for installation help, please check the [wiki](https://github.com/exodus4d/pathfinder/wiki). -More information will be added once the beta is over and the first stable build is released. +*** -## Project structure +### Project structure ``` |-- (0755) app --> backend [*.php] @@ -34,8 +44,8 @@ More information will be added once the beta is over and the first stable build |-- sql --> static DB data for import (pathfinder.sql) |-- (0755) favicon --> Favicons |-- (0755) js --> JS source files (raw) - |-- app --> "PASTHFINDER" core files (not used for production ) - |-- lib --> 3rd partie extension/library (not used for production ) + |-- 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 |-- ... @@ -47,7 +57,7 @@ More information will be added once the beta is over and the first stable build |-- img --> images |-- js --> JS dist/build folder and source maps (minified, uglified) |-- templates --> templates - |-- sass --> SCSS source (not used for production ) + |-- sass --> SCSS source (not used for production) |-- ... |-- (0777) tmp --> cache folder |-- ... @@ -57,15 +67,16 @@ More information will be added once the beta is over and the first stable build -------------------------- CI/CD config files: -------------------------- - |-- build.js --> "RequireJs Optimizer" config (not used for production ) - |-- config.rb --> "Compass" config (not used for production ) + |-- build.js --> "RequireJs Optimizer" config (not used for production) + |-- config.rb --> "Compass" config (not used for production) |-- 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 ) + |-- package.json --> "Node.js" dependency config (not used for production) + |-- README.md --> This file :) (not used for production) ``` -## Thanks! -I´m very proud that **you** are using *PATHFINDER*! +*** + +### Thanks! It took me month of time in development until this project got into the first *BETA*. If you like it, please help to improve it. (report bugs, find security issues,...) diff --git a/app/config.ini b/app/config.ini index 5873cdc9..ea8ca2ee 100644 --- a/app/config.ini +++ b/app/config.ini @@ -39,9 +39,9 @@ AUTOLOAD = app/main/ FAVICON = /favicon ; load additional config files +; DO NOT load environment.ini, it is loaded automatically [configs] app/routes.ini = true -app/environment.ini = true app/pathfinder.ini = true app/requirements.ini = true app/cron.ini = true \ No newline at end of file diff --git a/app/cron.ini b/app/cron.ini index af484c23..7bb7cb96 100644 --- a/app/cron.ini +++ b/app/cron.ini @@ -17,8 +17,11 @@ importSystemData = Cron\CcpSystemsUpdate->importSystemData, @hourly ; disable outdated maps deactivateMapData = Cron\MapUpdate->deactivateMapData, @hourly +; delete character log data +deleteLogData = Cron\CharacterUpdate->deleteLogData, @hourly + ; delete disabled maps deleteMapData = Cron\MapUpdate->deleteMapData, @downtime -; delete character log data -deleteLogData = Cron\CharacterUpdate->deleteLogData, @hourly \ No newline at end of file +; delete expired character cookie authentication data +deleteAuthenticationData = Cron\CharacterUpdate->deleteAuthenticationData, @downtime \ No newline at end of file diff --git a/app/environment.ini b/app/environment.ini index fdc37c0c..eea23f0c 100644 --- a/app/environment.ini +++ b/app/environment.ini @@ -2,16 +2,17 @@ [ENVIRONMENT] ; project environment (DEVELOP, PRODUCTION). -; This effects: DB connection, Mail-Server connection +; This effects: DB connection, Mail-Server, SSO, CREST configurations in this file ; configuration below SERVER = DEVELOP [ENVIRONMENT.DEVELOP] -; base dir (Default: "auto-detect" +; 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 +; deployment URL (e.g. http://localhost) URL = http://pathfinder.local -; Verbosity level of the stack trace +; level of debug/error stack trace DEBUG = 3 ; main db DB_DNS = mysql:host=localhost;port=3306;dbname= @@ -21,18 +22,23 @@ DB_PASS = ; EVE-Online CCP Database export DB_CCP_DNS = mysql:host=localhost;port=3306;dbname= -DB_CCP_NAME = eve_parallax_min +DB_CCP_NAME = eve_citadel_min DB_CCP_USER = root DB_CCP_PASS = -; CCP SSO settings +; CCP SSO settings (OAuth2) - visit: https://developers.eveonline.com/applications +CCP_CREST_URL = https://api-sisi.testeveonline.com +SSO_CCP_URL = https://sisilogin.testeveonline.com SSO_CCP_CLIENT_ID = SSO_CCP_SECRET_KEY = -; SMTP settings. see: https://developers.eveonline.com/applications +; CCP XML APIv2 +CCP_XML = https://api.testeveonline.com + +; SMTP settings (optional) SMTP_HOST = localhost SMTP_PORT = 25 -SMTP_SCHEME = "" +SMTP_SCHEME = TLS SMTP_USER = pathfinder SMTP_PASS = root @@ -40,10 +46,12 @@ SMTP_FROM = pathfinder@localhost.com SMTP_ERROR = pathfinder@localhost.com [ENVIRONMENT.PRODUCTION] -BASE = /www/htdocs/www.pathfinder-w.space -; deployment URL +; 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 = https://www.pathfinder-w.space -; Verbosity level of the stack trace +; level of debug/error stack trace DEBUG = 0 ; main db DB_DNS = mysql:host=localhost;port=3306;dbname= @@ -58,10 +66,15 @@ DB_CCP_USER = DB_CCP_PASS = ; CCP SSO settings +CCP_CREST_URL = https://crest-tq.eveonline.com +SSO_CCP_URL = https://login.eveonline.com SSO_CCP_CLIENT_ID = SSO_CCP_SECRET_KEY = -; SMTP settings +; CCP XML APIv2 +CCP_XML = https://api.eveonline.com + +; SMTP settings (optional) SMTP_HOST = localhost SMTP_PORT = 25 SMTP_SCHEME = TLS diff --git a/app/lib/CHANGELOG b/app/lib/CHANGELOG index d7179d10..cf2dab51 100644 --- a/app/lib/CHANGELOG +++ b/app/lib/CHANGELOG @@ -1,5 +1,52 @@ CHANGELOG +3.5.1 (31 December 2015) +* NEW: ttl attribute in template tag +* NEW: allow anonymous function for template filter +* NEW: format modifier for international and custom currency symbol +* NEW: Image->data() returns image resource +* NEW: extract() get prefixed array keys from an assoc array +* NEW: Optimized and faster Template parser with full support for HTML5 empty tags +* NEW: Added support for {@token} encapsulation syntax in routes definition +* NEW: DB\SQL->exec(), automatically shift to 1-based query arguments +* NEW: abort() flush output +* Added referenced value to devoid() +* Template token filters are now resolved within Preview->token() +* Web->_curl: restrict redirections to HTTP +* Web->minify(), skip importing of external files +* Improved session and error handling in until() +* Get the error trace array with the new $format parameter +* Better support for unicode URLs +* Optimized TZ detection with date_default_timezone_get() +* format() Provide default decimal places +* Optimize code: remove redundant TTL checks +* Optimized timeout handling in Web->request() +* Improved PHPDoc hints +* Added missing russian DIACRITICS letters +* DB\Cursor: allow child implementation of reset() +* DB\Cursor: Copyfrom now does an internal call to set() +* DB\SQL: Provide the ability to disable SQL logging +* DB\SQL: improved query analysis to trigger fetchAll +* DB\SQL\Mapper: added support for binary table columns +* SQL,JIG,MONGO,CACHE Session handlers refactored and optimized +* SMTP Refactoring and optimization +* Bug fix: SMTP, Align quoted_printable_encode() with SMTP specs (dot-stuffing) +* Bug fix: SMTP, Send buffered optional headers to output +* Bug fix: SMTP, Content-Transfer-Encoding for non-TLS connections +* Bug fix: SMTP, Single attachment error +* Bug fix: Cursor->load not always mapping to first record +* Bug fix: dry SQL mapper should not trigger 'load' +* Bug fix: Code highlighting on empty text +* Bug fix: Image->resize, round dimensions instead of cast +* Bug fix: whitespace handling in $f3->compile() +* Bug fix: TTL of `View` and `Preview` (`Template`) +* Bug fix: token filter regex +* Bug fix: Template, empty attributes +* Bug fix: Preview->build() greedy regex +* Bug fix: Web->minify() single-line comment on last line +* Bug fix: Web->request(), follow_location with cURL and open_basedir +* Bug fix: Web->send() Single quotes around filename not interpreted correctly by some browsers + 3.5.0 (2 June 2015) * NEW: until() method for long polling * NEW: abort() to disconnect HTTP client (and continue execution) diff --git a/app/lib/base.php b/app/lib/base.php index 0855f9d1..ea1a1f09 100644 --- a/app/lib/base.php +++ b/app/lib/base.php @@ -45,7 +45,7 @@ final class Base extends Prefab implements ArrayAccess { //@{ Framework details const PACKAGE='Fat-Free Framework', - VERSION='3.5.0-Release'; + VERSION='3.5.1-Release'; //@} //@{ HTTP status codes (RFC 2616) @@ -179,8 +179,8 @@ final class Base extends Prefab implements ArrayAccess { } /** - * assemble url from alias name - * @return NULL + * Assemble url from alias name + * @return string * @param $name string * @param $params array|string **/ @@ -215,7 +215,7 @@ final class Base extends Prefab implements ArrayAccess { function compile($str) { $fw=$this; return preg_replace_callback( - '/(?|::)*)/', + '/(?|::)*)/', function($var) use($fw) { return '$'.preg_replace_callback( '/\.(\w+)\(|\.(\w+)|\[((?:[^\[\]]*|(?R))*)\]/', @@ -226,7 +226,7 @@ final class Base extends Prefab implements ArrayAccess { ('['.var_export($expr[1],TRUE).']')).'('): ('['.var_export( isset($expr[3])? - $fw->compile($expr[3]): + trim($fw->compile($expr[3])): (ctype_digit($expr[2])? (int)$expr[2]: $expr[2]),TRUE).']'); @@ -305,10 +305,11 @@ final class Base extends Prefab implements ArrayAccess { /** * Return TRUE if hive key is empty and not cached - * @return bool * @param $key string + * @param $val mixed + * @return bool **/ - function devoid($key) { + function devoid($key,&$val=NULL) { $val=$this->ref($key,FALSE); return empty($val) && (!Cache::instance()->exists($this->hash($key).'.var',$val) || @@ -422,8 +423,7 @@ final class Base extends Prefab implements ArrayAccess { // End session session_unset(); session_destroy(); - unset($_COOKIE[session_name()]); - header_remove('Set-Cookie'); + $this->clear('COOKIE.'.session_name()); } $this->sync('SESSION'); } @@ -679,6 +679,19 @@ final class Base extends Prefab implements ArrayAccess { return $num?($num/abs($num)):0; } + /** + * Extract values of an associative array whose keys start with the given prefix + * @return array + * @param $arr array + * @param $prefix string + **/ + function extract($arr,$prefix) { + $out=array(); + foreach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr)) as $key) + $out[substr($key,strlen($prefix))]=$arr[$key]; + return $out; + } + /** * Convert class constants to array * @return array @@ -687,14 +700,7 @@ final class Base extends Prefab implements ArrayAccess { **/ 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; + return $this->extract($ref->getconstants(),$prefix); } /** @@ -843,9 +849,12 @@ final class Base extends Prefab implements ArrayAccess { return number_format( $args[$pos],0,'',$thousands_sep); case 'currency': - if (function_exists('money_format')) + $int=$cstm=false; + if (isset($prop) && $cstm=!$int=($prop=='int')) + $currency_symbol=$prop; + if (!$cstm && function_exists('money_format')) return money_format( - '%n',$args[$pos]); + '%'.($int?'i':'n'),$args[$pos]); $fmt=array( 0=>'(nc)',1=>'(n c)', 2=>'(nc)',10=>'+nc', @@ -878,7 +887,8 @@ final class Base extends Prefab implements ArrayAccess { $frac_digits, $decimal_point, $thousands_sep), - $currency_symbol), + $int?$int_curr_symbol + :$currency_symbol), $fmt[(int)( (${$pre.'_cs_precedes'}%2). (${$pre.'_sign_posn'}%5). @@ -891,8 +901,8 @@ final class Base extends Prefab implements ArrayAccess { $thousands_sep).'%'; case 'decimal': return number_format( - $args[$pos],$prop,$decimal_point, - $thousands_sep); + $args[$pos],isset($prop)?$prop:2, + $decimal_point,$thousands_sep); } break; case 'date': @@ -1022,7 +1032,7 @@ final class Base extends Prefab implements ArrayAccess { **/ function status($code) { $reason=@constant('self::HTTP_'.$code); - if (PHP_SAPI!='cli') + if (PHP_SAPI!='cli' && !headers_sent()) header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason); return $reason; } @@ -1089,11 +1099,12 @@ final class Base extends Prefab implements ArrayAccess { } /** - * Return formatted stack trace - * @return string + * Return filtered, formatted stack trace + * @return string|array * @param $trace array|NULL + * @param $format bool **/ - function trace(array $trace=NULL) { + function trace(array $trace=NULL, $format=TRUE) { if (!$trace) { $trace=debug_backtrace(FALSE); $frame=$trace[0]; @@ -1111,6 +1122,8 @@ final class Base extends Prefab implements ArrayAccess { '__call|call_user_func)/',$frame['function'])); } ); + if (!$format) + return $trace; $out=''; $eol="\n"; // Analyze stack trace @@ -1369,7 +1382,7 @@ final class Base extends Prefab implements ArrayAccess { $url=$this->rel($this->hive['URI']); $case=$this->hive['CASELESS']?'i':''; preg_match('/^'. - preg_replace('/@(\w+\b)/','(?P<\1>[^\/\?]+)', + preg_replace('/((\\\{)?@(\w+\b)(?(2)\\\}))/','(?P<\3>[^\/\?]+)', str_replace('\*','([^\?]+)',preg_quote($pattern,'/'))). '\/?(?:\?.*)?$/'.$case.'um',$url,$args); return $args; @@ -1394,7 +1407,7 @@ final class Base extends Prefab implements ArrayAccess { 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']); + $req=$this->rel(urldecode($this->hive['URI'])); if ($cors=(isset($this->hive['HEADERS']['Origin']) && $this->hive['CORS']['origin'])) { $cors=$this->hive['CORS']; @@ -1428,7 +1441,7 @@ final class Base extends Prefab implements ArrayAccess { if (is_numeric($key) && $key) unset($args[$key]); // Capture values of route pattern tokens - $this->hive['PARAMS']=$args=array_map('urldecode',$args); + $this->hive['PARAMS']=$args; // Save matching route $this->hive['ALIAS']=$alias; $this->hive['PATTERN']=$pattern; @@ -1437,9 +1450,10 @@ final class Base extends Prefab implements ArrayAccess { implode(',',$cors['expose']):$cors['expose'])); if (is_string($handler)) { // Replace route pattern tokens in handler if any - $handler=preg_replace_callback('/@(\w+\b)/', + $handler=preg_replace_callback('/({)?@(\w+\b)(?(1)})/', function($id) use($args) { - return isset($args[$id[1]])?$args[$id[1]]:$id[0]; + $pid=count($id)>2?2:1; + return isset($args[$id[$pid]])?$args[$id[$pid]]:$id[0]; }, $handler ); @@ -1458,7 +1472,7 @@ final class Base extends Prefab implements ArrayAccess { $cached=$cache->exists( $hash=$this->hash($this->hive['VERB'].' '. $this->hive['URI']).'.url',$data); - if ($cached && $cached[0]+$ttl>$now) { + if ($cached) { if (isset($headers['If-Modified-Since']) && strtotime($headers['If-Modified-Since'])+ $ttl>$now) { @@ -1520,10 +1534,13 @@ final class Base extends Prefab implements ArrayAccess { // Unhandled HTTP method header('Allow: '.implode(',',array_unique($allowed))); if ($cors) { - header('Access-Control-Allow-Methods: OPTIONS,'.implode(',',$allowed)); + 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'])); + 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']); } @@ -1546,28 +1563,26 @@ final class Base extends Prefab implements ArrayAccess { $time=time(); $limit=max(0,min($timeout,$max=ini_get('max_execution_time')-1)); $out=''; - $flag=FALSE; + // Turn output buffering on + ob_start(); // Not for the weak of heart while ( + // No error occurred + !$this->hive['ERROR'] && // Still alive? !connection_aborted() && // Got time left? (time()-$time+1<$limit) && // Restart session - $flag=@session_start() && + @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(); - } + ob_flush(); + flush(); return $out; } @@ -1577,9 +1592,11 @@ final class Base extends Prefab implements ArrayAccess { function abort() { @session_start(); session_commit(); - header('Content-Length: 0'); + $out=''; while (ob_get_level()) - ob_end_clean(); + $out=ob_get_clean().$out; + header('Content-Length: '.strlen($out)); + echo $out; flush(); if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); @@ -1826,7 +1843,7 @@ final class Base extends Prefab implements ArrayAccess { $out=''; $pre=FALSE; $text=trim($text); - if (!preg_match('/^<\?php/',$text)) { + if ($text && !preg_match('/^<\?php/',$text)) { $text='NULL, 'CORS'=>array( 'headers'=>'', - 'origin'=>false, - 'credentials'=>false, - 'expose'=>false, + 'origin'=>FALSE, + 'credentials'=>FALSE, + 'expose'=>FALSE, 'ttl'=>0), 'DEBUG'=>0, 'DIACRITICS'=>array(), @@ -2131,7 +2148,7 @@ final class Base extends Prefab implements ArrayAccess { 'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php', 'TEMP'=>'tmp/', 'TIME'=>microtime(TRUE), - 'TZ'=>(@ini_get('date.timezone'))?:'UTC', + 'TZ'=>@date_default_timezone_get(), 'UI'=>'./', 'UNLOAD'=>NULL, 'UPLOADS'=>'./', @@ -2500,8 +2517,7 @@ class View extends Prefab { 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)) + if ($cache->exists($hash=$fw->hash($file),$data)) return $data; foreach ($fw->split($fw->get('UI').';./') as $dir) if (is_file($this->view=$fw->fixslashes($dir.$file))) { @@ -2516,7 +2532,7 @@ class View extends Prefab { foreach($this->trigger['afterrender'] as $func) $data=$fw->call($func,$data); if ($ttl) - $cache->set($hash,$data); + $cache->set($hash,$data,$ttl); return $data; } user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); @@ -2539,7 +2555,7 @@ class Preview extends View { //! MIME type $mime, //! token filter - $filter = array( + $filter=array( 'esc'=>'$this->esc', 'raw'=>'$this->raw', 'alias'=>'\Base::instance()->alias', @@ -2552,15 +2568,24 @@ class Preview extends View { * @param $str string **/ function token($str) { - return trim(preg_replace('/\{\{(.+?)\}\}/s',trim('\1'), + $str=trim(preg_replace('/\{\{(.+?)\}\}/s',trim('\1'), Base::instance()->compile($str))); + if (preg_match('/^(.+)(?split($parts[2]) as $func) + $str=is_string($cmd=$this->filter($func))?$cmd.'('.$str.')': + '\Base::instance()->call('. + '$this->filter(\''.$func.'\'),array('.$str.'))'; + } + return $str; } /** - * register token filter + * Register or get (a specific one or all) token filters * @param string $key - * @param string $func - * @return array + * @param string|closure $func + * @return array|closure|string */ function filter($key=NULL,$func=NULL) { if (!$key) @@ -2578,19 +2603,15 @@ class Preview extends View { protected function build($node) { $self=$this; return preg_replace_callback( - '/\{\-(.+?)\-\}|\{\{(.+?)\}\}(\n+)?/s', + '/\{\-(.+?)\-\}|\{\{(.+?)\}\}(\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":''); + return empty($expr[4])? + (''. + (isset($expr[3])?$expr[3]."\n":'')): + ''; }, preg_replace_callback( '/\{~(.+?)~\}/s', @@ -2631,8 +2652,7 @@ class Preview extends View { if (!is_dir($tmp=$fw->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)) + if ($cache->exists($hash=$fw->hash($dir.$file),$data)) return $data; if (is_file($view=$fw->fixslashes($dir.$file))) { if (!is_file($this->view=($tmp. @@ -2659,7 +2679,7 @@ class Preview extends View { foreach ($this->trigger['afterrender'] as $func) $data = $fw->call($func, $data); if ($ttl) - $cache->set($hash,$data); + $cache->set($hash,$data,$ttl); return $data; } } diff --git a/app/lib/basket.php b/app/lib/basket.php index 7445e143..94e03063 100644 --- a/app/lib/basket.php +++ b/app/lib/basket.php @@ -195,7 +195,7 @@ class Basket extends Magic { if (is_string($var)) $var=\Base::instance()->get($var); foreach ($var as $key=>$val) - $this->item[$key]=$val; + $this->set($key,$val); } /** diff --git a/app/lib/db/cortex.php b/app/lib/db/cortex.php index 9f0616bb..cfc9a138 100644 --- a/app/lib/db/cortex.php +++ b/app/lib/db/cortex.php @@ -13,14 +13,14 @@ * | | < | <| -__|-- __| * |__|__|__||__|__|_____|_____| * - * Copyright (c) 2014 by ikkez - * Christian Knuth + * Copyright (c) 2016 by ikkez + * Christian Knuth * https://github.com/ikkez/F3-Sugar/ * * @package DB - * @version 1.4.1-dev + * @version 1.4.1 + * @date 29.01.2016 * @since 24.04.2012 - * @date 04.06.2015 */ namespace DB; @@ -173,7 +173,9 @@ class Cortex extends Cursor { list($key, $relField) = explode('.',$val,2); $this->relWhitelist[$key][(int)$exclude][] = $relField; unset($fields[$i]); + $fields[] = $key; } + $fields = array_unique($fields); $schema = $this->whitelist ?: $this->mapper->fields(); if (!$schema && !$this->dbsType != 'sql' && $this->dry()) { $schema = $this->load()->mapper->fields(); diff --git a/app/lib/db/cursor.php b/app/lib/db/cursor.php index c218fa47..baef9531 100644 --- a/app/lib/db/cursor.php +++ b/app/lib/db/cursor.php @@ -103,7 +103,7 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { /** * Get cursor's equivalent external iterator - * Causes a fatal error in PHP 5.3.5if uncommented + * Causes a fatal error in PHP 5.3.5 if uncommented * return ArrayIterator **/ abstract function getiterator(); @@ -119,7 +119,7 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { /** * Return first record (mapper object) that matches criteria - * @return \DB\Cursor|FALSE + * @return static|FALSE * @param $filter string|array * @param $options array * @param $ttl int @@ -171,8 +171,9 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { * @param $ttl int **/ function load($filter=NULL,array $options=NULL,$ttl=0) { + $this->reset(); return ($this->query=$this->find($filter,$options,$ttl)) && - $this->skip(0)?$this->query[$this->ptr=0]:FALSE; + $this->skip(0)?$this->query[$this->ptr]:FALSE; } /** diff --git a/app/lib/db/jig/mapper.php b/app/lib/db/jig/mapper.php index e5b007c6..f0f3953c 100644 --- a/app/lib/db/jig/mapper.php +++ b/app/lib/db/jig/mapper.php @@ -149,7 +149,7 @@ class Mapper extends \DB\Cursor { /** * Return records that match criteria - * @return \DB\JIG\Mapper[]|FALSE + * @return static[]|FALSE * @param $filter array * @param $options array * @param $ttl int @@ -431,7 +431,7 @@ class Mapper extends \DB\Cursor { if ($func) $var=call_user_func($func,$var); foreach ($var as $key=>$val) - $this->document[$key]=$val; + $this->set($key,$val); } /** diff --git a/app/lib/db/jig/session.php b/app/lib/db/jig/session.php index 7f9a6ffa..e4dca2fe 100644 --- a/app/lib/db/jig/session.php +++ b/app/lib/db/jig/session.php @@ -27,7 +27,15 @@ class Session extends Mapper { protected //! Session ID - $sid; + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect; /** * Open session @@ -44,6 +52,8 @@ class Session extends Mapper { * @return TRUE **/ function close() { + $this->reset(); + $this->sid=NULL; return TRUE; } @@ -53,9 +63,20 @@ class Session extends Mapper { * @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'); + $this->load(array('@session_id=?',$this->sid=$id)); + if ($this->dry()) + return FALSE; + if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { + $fw=\Base::instance(); + if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + //NB: `session_destroy` can't be called at that stage (`session_start` not completed) + $this->destroy($id); + $this->close(); + $fw->clear('COOKIE.'.session_name()); + $fw->error(403); + } + } + return $this->get('data'); } /** @@ -65,19 +86,10 @@ class Session extends Mapper { * @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('ip',$this->_ip); + $this->set('agent',$this->_agent); $this->set('stamp',time()); $this->save(); return TRUE; @@ -90,9 +102,6 @@ class Session extends Mapper { **/ 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; } @@ -107,19 +116,27 @@ class Session extends Mapper { } /** - * Return anti-CSRF token - * @return string|FALSE - **/ - function csrf() { - return $this->dry()?FALSE:$this->get('csrf'); + * Return session id (if session has started) + * @return string|NULL + **/ + function sid() { + return $this->sid; } /** - * Return IP address - * @return string|FALSE - **/ + * Return anti-CSRF token + * @return string + **/ + function csrf() { + return $this->_csrf; + } + + /** + * Return IP address + * @return string + **/ function ip() { - return $this->dry()?FALSE:$this->get('ip'); + return $this->_ip; } /** @@ -127,6 +144,8 @@ class Session extends Mapper { * @return string|FALSE **/ function stamp() { + if (!$this->sid) + session_start(); return $this->dry()?FALSE:$this->get('stamp'); } @@ -135,17 +154,19 @@ class Session extends Mapper { * @return string|FALSE **/ function agent() { - return $this->dry()?FALSE:$this->get('agent'); + return $this->_agent; } /** * Instantiate class - * @param $db object + * @param $db \DB\Jig * @param $file string * @param $onsuspect callback + * @param $key string **/ - function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL) { + function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL,$key=NULL) { parent::__construct($db,$file); + $this->onsuspect=$onsuspect; session_set_save_handler( array($this,'open'), array($this,'close'), @@ -155,26 +176,14 @@ class Session extends Mapper { 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')).'.'. + $this->_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(); - } + if ($key) + $fw->set($key,$this->_csrf); + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + $this->_ip=$fw->get('IP'); } } diff --git a/app/lib/db/mongo/mapper.php b/app/lib/db/mongo/mapper.php index f4ef5170..e814b293 100644 --- a/app/lib/db/mongo/mapper.php +++ b/app/lib/db/mongo/mapper.php @@ -84,7 +84,7 @@ class Mapper extends \DB\Cursor { /** * Convert array to mapper object - * @return \DB\Mongo\Mapper + * @return static * @param $row array **/ protected function factory($row) { @@ -111,7 +111,7 @@ class Mapper extends \DB\Cursor { /** * Build query and execute - * @return \DB\Mongo\Mapper[] + * @return static[] * @param $fields string * @param $filter array * @param $options array @@ -177,7 +177,7 @@ class Mapper extends \DB\Cursor { /** * Return records that match criteria - * @return \DB\Mongo\Mapper[] + * @return static[] * @param $filter array * @param $options array * @param $ttl int @@ -308,7 +308,7 @@ class Mapper extends \DB\Cursor { if ($func) $var=call_user_func($func,$var); foreach ($var as $key=>$val) - $this->document[$key]=$val; + $this->set($key,$val); } /** diff --git a/app/lib/db/mongo/session.php b/app/lib/db/mongo/session.php index 3d7e1d26..0b510f13 100644 --- a/app/lib/db/mongo/session.php +++ b/app/lib/db/mongo/session.php @@ -27,7 +27,15 @@ class Session extends Mapper { protected //! Session ID - $sid; + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect; /** * Open session @@ -44,6 +52,8 @@ class Session extends Mapper { * @return TRUE **/ function close() { + $this->reset(); + $this->sid=NULL; return TRUE; } @@ -53,9 +63,20 @@ class Session extends Mapper { * @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'); + $this->load(array('session_id'=>$this->sid=$id)); + if ($this->dry()) + return FALSE; + if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { + $fw=\Base::instance(); + if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + //NB: `session_destroy` can't be called at that stage (`session_start` not completed) + $this->destroy($id); + $this->close(); + $fw->clear('COOKIE.'.session_name()); + $fw->error(403); + } + } + return $this->get('data'); } /** @@ -65,19 +86,10 @@ class Session extends Mapper { * @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('ip',$this->_ip); + $this->set('agent',$this->_agent); $this->set('stamp',time()); $this->save(); return TRUE; @@ -90,9 +102,6 @@ class Session extends Mapper { **/ 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; } @@ -107,45 +116,57 @@ class Session extends Mapper { } /** - * Return anti-CSRF token - * @return string|FALSE - **/ + * Return session id (if session has started) + * @return string|NULL + **/ + function sid() { + return $this->sid; + } + + /** + * Return anti-CSRF token + * @return string + **/ function csrf() { - return $this->dry()?FALSE:$this->get('csrf'); + return $this->_csrf; } /** - * Return IP address - * @return string|FALSE - **/ + * Return IP address + * @return string + **/ function ip() { - return $this->dry()?FALSE:$this->get('ip'); + return $this->_ip; } /** - * Return Unix timestamp - * @return string|FALSE - **/ + * Return Unix timestamp + * @return string|FALSE + **/ function stamp() { + if (!$this->sid) + session_start(); return $this->dry()?FALSE:$this->get('stamp'); } /** - * Return HTTP user agent - * @return string|FALSE - **/ + * Return HTTP user agent + * @return string + **/ function agent() { - return $this->dry()?FALSE:$this->get('agent'); + return $this->_agent; } /** * Instantiate class - * @param $db object + * @param $db \DB\Mongo * @param $table string * @param $onsuspect callback + * @param $key string **/ - function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL) { + function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL,$key=NULL) { parent::__construct($db,$table); + $this->onsuspect=$onsuspect; session_set_save_handler( array($this,'open'), array($this,'close'), @@ -155,26 +176,14 @@ class Session extends Mapper { 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')).'.'. + $this->_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(); - } + if ($key) + $fw->set($key,$this->_csrf); + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + $this->_ip=$fw->get('IP'); } } diff --git a/app/lib/db/sql.php b/app/lib/db/sql.php index f4734c3c..60089446 100644 --- a/app/lib/db/sql.php +++ b/app/lib/db/sql.php @@ -91,6 +91,8 @@ class SQL { return \PDO::PARAM_BOOL; case 'integer': return \PDO::PARAM_INT; + case 'resource': + return \PDO::PARAM_LOB; default: return \PDO::PARAM_STR; } @@ -112,6 +114,8 @@ class SQL { return (bool)$val; case \PDO::PARAM_STR: return (string)$val; + case \PDO::PARAM_LOB: + return (binary)$val; } } @@ -149,6 +153,11 @@ class SQL { for ($i=0;$i<$count;$i++) { $cmd=$cmds[$i]; $arg=$args[$i]; + // ensure 1-based arguments + if (array_key_exists(0,$arg)) { + array_unshift($arg,''); + unset($arg[0]); + } if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd)) continue; $now=microtime(TRUE); @@ -198,8 +207,8 @@ class SQL { $this->rollback(); user_error('PDOStatement: '.$error[2],E_USER_ERROR); } - if (preg_match('/^\s*'. - '(?:EXPLAIN|SELECT|PRAGMA|SHOW|RETURNING)\b/is',$cmd) || + 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); @@ -245,11 +254,14 @@ class SQL { } /** - * Return SQL profiler results + * Return SQL profiler results (or disable logging) + * @param $flag bool * @return string **/ - function log() { - return $this->log; + function log($flag=TRUE) { + if ($flag) + return $this->log; + $this->log=FALSE; } /** @@ -333,7 +345,10 @@ class SQL { \PDO::PARAM_INT: (preg_match('/bool/i',$row[$val[2]])? \PDO::PARAM_BOOL: - \PDO::PARAM_STR), + (preg_match('/blob|bytea|image|binary/i', + $row[$val[2]])? + \PDO::PARAM_LOB: + \PDO::PARAM_STR)), 'default'=>is_string($row[$val[3]])? preg_replace('/^\s*([\'"])(.*)\1\s*/','\2', $row[$val[3]]):$row[$val[3]], @@ -422,7 +437,7 @@ class SQL { } /** - * Redirect call to MongoDB object + * Redirect call to PDO object * @return mixed * @param $func string * @param $args array diff --git a/app/lib/db/sql/mapper.php b/app/lib/db/sql/mapper.php index 3a13c65b..ca6f8493 100644 --- a/app/lib/db/sql/mapper.php +++ b/app/lib/db/sql/mapper.php @@ -190,7 +190,7 @@ class Mapper extends \DB\Cursor { /** * Build query string and execute - * @return \DB\SQL\Mapper[] + * @return static[] * @param $fields string * @param $filter string|array * @param $options array @@ -294,7 +294,7 @@ class Mapper extends \DB\Cursor { /** * Return records that match criteria - * @return \DB\SQL\Mapper[] + * @return static[] * @param $filter string|array * @param $options array * @param $ttl int @@ -362,7 +362,7 @@ class Mapper extends \DB\Cursor { $field['value']=$dry?NULL:$out->adhoc[$key]['value']; unset($field); } - if (isset($this->trigger['load'])) + if (!$dry && isset($this->trigger['load'])) \Base::instance()->call($this->trigger['load'],$this); return $out; } @@ -559,14 +559,8 @@ class Mapper extends \DB\Cursor { 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); - } + if (in_array($key,array_keys($this->fields))) + $this->set($key,$val); } /** diff --git a/app/lib/db/sql/session.php b/app/lib/db/sql/session.php index 12c27f42..60702050 100644 --- a/app/lib/db/sql/session.php +++ b/app/lib/db/sql/session.php @@ -27,7 +27,15 @@ class Session extends Mapper { protected //! Session ID - $sid; + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect; /** * Open session @@ -44,6 +52,8 @@ class Session extends Mapper { * @return TRUE **/ function close() { + $this->reset(); + $this->sid=NULL; return TRUE; } @@ -53,9 +63,20 @@ class Session extends Mapper { * @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'); + $this->load(array('session_id=?',$this->sid=$id)); + if ($this->dry()) + return FALSE; + if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { + $fw=\Base::instance(); + if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + //NB: `session_destroy` can't be called at that stage (`session_start` not completed) + $this->destroy($id); + $this->close(); + $fw->clear('COOKIE.'.session_name()); + $fw->error(403); + } + } + return $this->get('data'); } /** @@ -65,19 +86,10 @@ class Session extends Mapper { * @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('ip',$this->_ip); + $this->set('agent',$this->_agent); $this->set('stamp',time()); $this->save(); return TRUE; @@ -90,9 +102,6 @@ class Session extends Mapper { **/ 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; } @@ -106,20 +115,28 @@ class Session extends Mapper { return TRUE; } + /** + * Return session id (if session has started) + * @return string|NULL + **/ + function sid() { + return $this->sid; + } + /** * Return anti-CSRF token - * @return string|FALSE + * @return string **/ function csrf() { - return $this->dry()?FALSE:$this->get('csrf'); + return $this->_csrf; } /** * Return IP address - * @return string|FALSE + * @return string **/ function ip() { - return $this->dry()?FALSE:$this->get('ip'); + return $this->_ip; } /** @@ -127,25 +144,28 @@ class Session extends Mapper { * @return string|FALSE **/ function stamp() { + if (!$this->sid) + session_start(); return $this->dry()?FALSE:$this->get('stamp'); } /** * Return HTTP user agent - * @return string|FALSE + * @return string **/ function agent() { - return $this->dry()?FALSE:$this->get('agent'); + return $this->_agent; } /** * Instantiate class - * @param $db object + * @param $db \DB\SQL * @param $table string * @param $force bool * @param $onsuspect callback + * @param $key string **/ - function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL) { + function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL,$key=NULL) { if ($force) { $eol="\n"; $tab="\t"; @@ -160,7 +180,6 @@ class Session extends Mapper { $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. @@ -169,6 +188,7 @@ class Session extends Mapper { ); } parent::__construct($db,$table); + $this->onsuspect=$onsuspect; session_set_save_handler( array($this,'open'), array($this,'close'), @@ -178,26 +198,14 @@ class Session extends Mapper { 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')).'.'. + $this->_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(); - } + if ($key) + $fw->set($key,$this->_csrf); + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + $this->_ip=$fw->get('IP'); } } diff --git a/app/lib/image.php b/app/lib/image.php index 55a29ba9..e1acd483 100644 --- a/app/lib/image.php +++ b/app/lib/image.php @@ -230,9 +230,9 @@ class Image { $ratio=($origw=imagesx($this->data))/($origh=imagesy($this->data)); if (!$crop) { if ($width/$ratio<=$height) - $height=$width/$ratio; + $height=round($width/$ratio); else - $width=$height*$ratio; + $width=round($height*$ratio); } if (!$enlarge) { $width=min($origw,$width); @@ -245,12 +245,12 @@ class Image { // Resize if ($crop) { if ($width/$ratio<=$height) { - $cropw=$origh*$width/$height; + $cropw=round($origh*$width/$height); imagecopyresampled($tmp,$this->data, 0,0,($origw-$cropw)/2,0,$width,$height,$cropw,$origh); } else { - $croph=$origw*$height/$width; + $croph=round($origw*$height/$width); imagecopyresampled($tmp,$this->data, 0,0,0,($origh-$croph)/2,$width,$height,$origw,$croph); } @@ -489,6 +489,14 @@ class Image { return ob_get_clean(); } + /** + * Return image resource + * @return resource + **/ + function data() { + return $this->data; + } + /** * Save current state * @return object diff --git a/app/lib/session.php b/app/lib/session.php index 8723c69f..8b28c936 100644 --- a/app/lib/session.php +++ b/app/lib/session.php @@ -25,7 +25,15 @@ class Session { protected //! Session ID - $sid; + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect; /** * Open session @@ -42,6 +50,7 @@ class Session { * @return TRUE **/ function close() { + $this->sid=NULL; return TRUE; } @@ -51,9 +60,20 @@ class Session { * @param $id string **/ function read($id) { - if ($id!=$this->sid) - $this->sid=$id; - return Cache::instance()->exists($id.'.@',$data)?$data['data']:FALSE; + $this->sid=$id; + if (!$data=Cache::instance()->get($id.'.@')) + return FALSE; + if ($data['ip']!=$this->_ip || $data['agent']!=$this->_agent) { + $fw=Base::instance(); + if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + //NB: `session_destroy` can't be called at that stage (`session_start` not completed) + $this->destroy($id); + $this->close(); + $fw->clear('COOKIE.'.session_name()); + $fw->error(403); + } + } + return $data['data']; } /** @@ -64,20 +84,12 @@ class Session { **/ 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']:'', + 'ip'=>$this->_ip, + 'agent'=>$this->_agent, 'stamp'=>time() ), $jar['expire']?($jar['expire']-time()):0 @@ -92,9 +104,6 @@ class Session { **/ function destroy($id) { Cache::instance()->clear($id.'.@'); - setcookie(session_name(),'',strtotime('-1 year')); - unset($_COOKIE[session_name()]); - header_remove('Set-Cookie'); return TRUE; } @@ -109,50 +118,55 @@ class Session { } /** - * Return anti-CSRF token - * @return string|FALSE - **/ + * Return session id (if session has started) + * @return string|NULL + **/ + function sid() { + return $this->sid; + } + + /** + * Return anti-CSRF token + * @return string + **/ function csrf() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['csrf']:FALSE; + return $this->_csrf; } /** - * Return IP address - * @return string|FALSE - **/ + * Return IP address + * @return string + **/ function ip() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['ip']:FALSE; + return $this->_ip; } /** - * Return Unix timestamp - * @return string|FALSE - **/ + * Return Unix timestamp + * @return string|FALSE + **/ function stamp() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['stamp']:FALSE; + if (!$this->sid) + session_start(); + return Cache::instance()->exists($this->sid.'.@',$data)? + $data['stamp']:FALSE; } /** - * Return HTTP user agent - * @return string|FALSE - **/ + * Return HTTP user agent + * @return string + **/ function agent() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['agent']:FALSE; + return $this->_agent; } /** * Instantiate class * @param $onsuspect callback + * @param $key string **/ - function __construct($onsuspect=NULL) { + function __construct($onsuspect=NULL,$key=NULL) { + $this->onsuspect=$onsuspect; session_set_save_handler( array($this,'open'), array($this,'close'), @@ -162,30 +176,14 @@ class Session { 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')).'.'. + $this->_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 - ); - } + if ($key) + $fw->set($key,$this->_csrf); + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + $this->_ip=$fw->get('IP'); } } diff --git a/app/lib/smtp.php b/app/lib/smtp.php index f0351ae5..3c9e964f 100644 --- a/app/lib/smtp.php +++ b/app/lib/smtp.php @@ -182,12 +182,13 @@ class SMTP extends Magic { 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 (preg_match('/8BITMIME/',$reply)) + $headers['Content-Transfer-Encoding']='8bit'; + else { + $headers['Content-Transfer-Encoding']='quoted-printable'; + $message=preg_replace('/^\.(.+)/m', + '..$1',quoted_printable_encode($message)); } if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) { // Authenticate @@ -204,9 +205,9 @@ class SMTP extends Magic { $str=''; // Stringify headers foreach ($headers as $key=>&$val) { - if (!in_array($key,$reqd)) { + if (!in_array($key,$reqd) && (!$this->attachments || + $key!='Content-Type' && $key!='Content-Transfer-Encoding')) $str.=$key.': '.$val.$eol; - } if (in_array($key,array('From','To','Cc','Bcc')) && !preg_match('/[<>]/',$val)) $val='<'.$val.'>'; @@ -221,12 +222,13 @@ class SMTP extends Magic { $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.'"'; + unset($headers['Content-Type']); + $enc=$headers['Content-Transfer-Encoding']; + unset($headers['Content-Transfer-Encoding']); + $hash=uniqid(NULL,TRUE); // Send mail headers - $out=''; + $out='Content-Type: multipart/mixed; boundary="'.$hash.'"'.$eol; foreach ($headers as $key=>$val) if ($key!='Bcc') $out.=$key.': '.$val.$eol; @@ -235,16 +237,17 @@ class SMTP extends Magic { $out.=$eol; $out.='--'.$hash.$eol; $out.='Content-Type: '.$type.$eol; - $out.=$eol; + $out.='Content-Transfer-Encoding: '.$enc.$eol; + $out.=$str.$eol; $out.=$message.$eol; foreach ($this->attachments as $attachment) { if (is_array($attachment['filename'])) { - list($alias,$file)=each($attachment); + list($alias,$file)=each($attachment['filename']); $filename=$alias; $attachment['filename']=$file; } else - $filename=basename($attachment); + $filename=basename($attachment['filename']); $out.='--'.$hash.$eol; $out.='Content-Type: application/octet-stream'.$eol; $out.='Content-Transfer-Encoding: base64'.$eol; @@ -253,8 +256,8 @@ class SMTP extends Magic { $out.='Content-Disposition: attachment; '. 'filename="'.$filename.'"'.$eol; $out.=$eol; - $out.=chunk_split( - base64_encode(file_get_contents($attachment))).$eol; + $out.=chunk_split(base64_encode( + file_get_contents($attachment['filename']))).$eol; } $out.=$eol; $out.='--'.$hash.'--'.$eol; @@ -287,7 +290,7 @@ class SMTP extends Magic { * @param $user string * @param $pw string **/ - function __construct($host,$port,$scheme,$user,$pw) { + function __construct($host='localhost',$port=25,$scheme=null,$user=null,$pw=null) { $this->headers=array( 'MIME-Version'=>'1.0', 'Content-Type'=>'text/plain; '. diff --git a/app/lib/template.php b/app/lib/template.php index 8c1c0e11..8b985cde 100644 --- a/app/lib/template.php +++ b/app/lib/template.php @@ -69,6 +69,7 @@ class Template extends Preview { \Base::instance()->stringify($pair[2])); },$pairs)).')+get_defined_vars()': 'get_defined_vars()'; + $ttl=isset($attrib['ttl'])?(int)$attrib['ttl']:0; return 'token($attrib['if']).') '):''). @@ -76,7 +77,7 @@ class Template extends Preview { (preg_match('/^\{\{(.+?)\}\}$/',$attrib['href'])? $this->token($attrib['href']): Base::instance()->stringify($attrib['href'])).','. - '$this->mime,'.$hive.'); ?>'); + '$this->mime,'.$hive.','.$ttl.'); ?>'); } /** @@ -269,45 +270,40 @@ class Template extends Preview { **/ 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:)?'. + for ($ptr=0,$w=5,$len=strlen($text),$tree=array(),$tmp='';$ptr<$len;) + if (preg_match('/^(.{0,'.$w.'}?)<(\/?)(?:F3:)?'. '('.$this->tags.')\b((?:\h+[\w-]+'. - '(?:\h*=\h*(?:"(?:.+?)"|\'(?:.+?)\'))?|'. + '(?:\h*=\h*(?:"(?:.*?)"|\'(?:.*?)\'))?|'. '\h*\{\{.+?\}\})*)\h*(\/?)>/is', substr($text,$ptr),$match)) { - if (strlen($tmp)) - $node[]=$tmp; + if (strlen($tmp)||$match[1]) + $tree[]=$tmp.$match[1]; // Element node - if ($match[1]) { + if ($match[2]) { // 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; - } + $stack=array(); + for($i=count($tree)-1;$i>=0;$i--) { + $item = $tree[$i]; + if (is_array($item) && array_key_exists($match[3],$item) + && !isset($item[$match[3]][0])) { + // Start tag found + $tree[$i][$match[3]]+=array_reverse($stack); + $tree=array_slice($tree,0,$i+1); + break; + } else $stack[]=$item; } - if (!$found) - // Unbalanced tag - $depth=$save; - $node=&$stack[$depth]; } else { // Start tag - $stack[$depth]=&$node; - $node=&$node[][$match[2]]; - if ($match[3]) { + $node=&$tree[][$match[3]]; + $node=array(); + if ($match[4]) { // Process attributes preg_match_all( '/(?:\b([\w-]+)\h*'. '(?:=\h*(?:"(.*?)"|\'(.*?)\'))?|'. '(\{\{.+?\}\}))/s', - $match[3],$attr,PREG_SET_ORDER); + $match[4],$attr,PREG_SET_ORDER); foreach ($attr as $kv) if (isset($kv[4])) $node['@attrib'][]=$kv[4]; @@ -318,26 +314,23 @@ class Template extends Preview { (isset($kv[3]) && $kv[3]!==''? $kv[3]:NULL)); } - if ($match[4]) - // Empty tag - $node=&$stack[$depth]; - else - $depth++; } $tmp=''; $ptr+=strlen($match[0]); + $w=5; } else { // Text node - $tmp.=substr($text,$ptr,1); - $ptr++; + $tmp.=substr($text,$ptr,$w); + $ptr+=$w; + if ($w<50) + $w++; } if (strlen($tmp)) // Append trailing text - $node[]=$tmp; + $tree[]=$tmp; // Break references unset($node); - unset($stack); return $tree; } diff --git a/app/lib/web.php b/app/lib/web.php index ab3389ec..57fbf937 100644 --- a/app/lib/web.php +++ b/app/lib/web.php @@ -133,7 +133,7 @@ class Web extends Prefab { header('Content-Type: '.($mime?:$this->mime($file))); if ($force) header('Content-Disposition: attachment; '. - 'filename='.var_export(basename($file),TRUE)); + 'filename="'.basename($file).'"'); header('Accept-Ranges: bytes'); header('Content-Length: '.$size); header('X-Powered-By: '.Base::instance()->get('PACKAGE')); @@ -259,10 +259,13 @@ class Web extends Prefab { **/ protected function _curl($url,$options) { $curl=curl_init($url); - curl_setopt($curl,CURLOPT_FOLLOWLOCATION, - $options['follow_location']); + if (!ini_get('open_basedir')) + curl_setopt($curl,CURLOPT_FOLLOWLOCATION, + $options['follow_location']); curl_setopt($curl,CURLOPT_MAXREDIRS, $options['max_redirects']); + curl_setopt($curl,CURLOPT_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS); + curl_setopt($curl,CURLOPT_REDIR_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS); curl_setopt($curl,CURLOPT_CUSTOMREQUEST,$options['method']); if (isset($options['header'])) curl_setopt($curl,CURLOPT_HTTPHEADER,$options['header']); @@ -288,6 +291,11 @@ class Web extends Prefab { curl_exec($curl); curl_close($curl); $body=ob_get_clean(); + if ($options['follow_location'] && + preg_match('/^Location: (.+)$/m',implode(PHP_EOL,$headers),$loc)) { + $options['max_redirects']--; + return $this->request($loc[1],$options); + } return array( 'body'=>$body, 'headers'=>$headers, @@ -357,7 +365,8 @@ class Web extends Prefab { if (!$socket) return FALSE; stream_set_blocking($socket,TRUE); - stream_set_timeout($socket,$options['timeout']); + stream_set_timeout($socket,isset($options['timeout'])? + $options['timeout']:ini_get('default_socket_timeout')); fputs($socket,$options['method'].' '.$parts['path']. ($parts['query']?('?'.$parts['query']):'').' HTTP/1.0'.$eol ); @@ -565,7 +574,7 @@ class Web extends Prefab { $src=$fw->read($save); for ($ptr=0,$len=strlen($src);$ptr<$len;) { if (preg_match('/^@import\h+url'. - '\(\h*([\'"])(.+?)\1\h*\)[^;]*;/', + '\(\h*([\'"])((?!(?:https?:)?\/\/).+?)\1\h*\)[^;]*;/', substr($src,$ptr),$parts)) { $path=dirname($file); $data.=$this->minify( @@ -586,7 +595,8 @@ class Web extends Prefab { // Single-line comment $str=strstr( substr($src,$ptr+2),"\n",TRUE); - $ptr+=strlen($str)+2; + $ptr+=(empty($str))? + strlen(substr($src,$ptr)):strlen($str)+2; } else { // Presume it's a regex pattern @@ -785,7 +795,7 @@ class Web extends Prefab { 'ù'=>'u','ű'=>'u','ů'=>'u','ư'=>'u','ū'=>'u','ǚ'=>'u', 'ǜ'=>'u','ǔ'=>'u','ǖ'=>'u','ũ'=>'u','ü'=>'ue','в'=>'v', 'ŵ'=>'w','ы'=>'y','ÿ'=>'y','ý'=>'y','ŷ'=>'y','ź'=>'z', - 'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh' + 'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh','ь'=>'','ъ'=>'' )+Base::instance()->get('DIACRITICS'))))),'-'); } diff --git a/app/main/controller/accesscontroller.php b/app/main/controller/accesscontroller.php index d3c76219..73fc2426 100644 --- a/app/main/controller/accesscontroller.php +++ b/app/main/controller/accesscontroller.php @@ -7,53 +7,26 @@ */ namespace Controller; +use Controller\Api as Api; use Model; class AccessController extends Controller { /** * event handler - * @param $f3 + * @param \Base $f3 */ - function beforeroute($f3) { + function beforeroute(\Base $f3) { parent::beforeroute($f3); - // Any CMS route of a child class of this one, requires a - // valid logged in user! - $loginCheck = $this->_checkLogIn(); + // Any route/endpoint of a child class of this one, + // requires a valid logged in user! + $loginCheck = $this->checkLogTimer($f3); if( !$loginCheck ){ // no user found or LogIn timer expired - $this->logOut($f3); + $this->logout($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 index e21ff90d..02e45f1a 100644 --- a/app/main/controller/api/access.php +++ b/app/main/controller/api/access.php @@ -7,25 +7,24 @@ */ namespace controller\api; +use Controller; use Model; -class Access extends \Controller\AccessController { +class Access extends Controller\AccessController { /** * event handler - * @param $f3 + * @param \Base $f3 */ - function beforeroute($f3) { - - parent::beforeroute($f3); - + function beforeroute(\Base $f3) { // set header for all routes header('Content-type: application/json'); + parent::beforeroute($f3); } /** - * search user/corporation or alliance by name - * @param $f3 + * search character/corporation or alliance by name + * @param \Base $f3 * @param $params */ public function search($f3, $params){ @@ -41,8 +40,8 @@ class Access extends \Controller\AccessController { $accessModel = null; switch($searchType){ - case 'user': - $accessModel = Model\BasicModel::getNew('UserModel'); + case 'character': + $accessModel = Model\BasicModel::getNew('CharacterModel'); break; case 'corporation': $accessModel = Model\BasicModel::getNew('CorporationModel'); @@ -55,12 +54,12 @@ class Access extends \Controller\AccessController { if( is_object($accessModel) ){ // find "active" entries that have their "sharing" option activated - $accessList = $accessModel->find( array( + $accessList = $accessModel->find( [ "LOWER(name) LIKE :token AND " . "active = 1 AND " . "shared = 1 ", ':token' => '%' . $searchToken . '%' - )); + ]); if($accessList){ foreach($accessList as $accessObject){ diff --git a/app/main/controller/api/connection.php b/app/main/controller/api/connection.php index aefbca82..45f296a7 100644 --- a/app/main/controller/api/connection.php +++ b/app/main/controller/api/connection.php @@ -7,27 +7,26 @@ */ namespace Controller\Api; +use Controller; use Model; -class Connection extends \Controller\AccessController{ +class Connection extends Controller\AccessController{ /** - * @param $f3 + * @param \Base $f3 */ - function beforeroute($f3) { - - parent::beforeroute($f3); - + function beforeroute(\Base $f3) { // set header for all routes header('Content-type: application/json'); + parent::beforeroute($f3); } /** * 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 $f3 + * @param \Base $f3 */ - public function save($f3){ + public function save(\Base $f3){ $postData = (array)$f3->get('POST'); $newConnectionData = []; @@ -38,26 +37,34 @@ class Connection extends \Controller\AccessController{ $mapData = (array)$postData['mapData']; $connectionData = (array)$postData['connectionData']; - $user = $this->_getUser(); + $activeCharacter = $this->getCharacter(); + + if($activeCharacter){ - if($user){ // get map model and check map access + /** + * @var Model\MapModel $map + */ $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( $map->hasAccess($activeCharacter) ){ + $source = $map->getSystemById( $connectionData['source'] ); + $target = $map->getSystemById( $connectionData['target'] ); if( !is_null($source) && !is_null($target) ){ + /** + * @var $connection Model\ConnectionModel + */ $connection = Model\BasicModel::getNew('ConnectionModel'); $connection->getById( (int)$connectionData['id'] ); // search if systems are neighbors $routeController = new Route(); + $routeController->initJumpData(); $route = $routeController->findRoute($connectionData['sourceName'], $connectionData['targetName'], 1); if($route['routePossible'] == true){ @@ -90,18 +97,25 @@ class Connection extends \Controller\AccessController{ echo json_encode($newConnectionData); } - public function delete($f3){ + /** + * delete connection + * @param \Base $f3 + * @throws \Exception + */ + public function delete(\Base $f3){ $connectionIds = $f3->get('POST.connectionIds'); - $user = $this->_getUser(); - $connection = Model\BasicModel::getNew('ConnectionModel'); + if($activeCharacter = $this->getCharacter()){ + /** + * @var Model\ConnectionModel $connection + */ + $connection = Model\BasicModel::getNew('ConnectionModel'); + foreach($connectionIds as $connectionId){ + $connection->getById($connectionId); + $connection->delete( $activeCharacter ); - foreach($connectionIds as $connectionId){ - - $connection->getById($connectionId); - $connection->delete($user); - - $connection->reset(); + $connection->reset(); + } } echo json_encode([]); diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php index fc18a886..be714422 100644 --- a/app/main/controller/api/map.php +++ b/app/main/controller/api/map.php @@ -7,6 +7,7 @@ */ namespace Controller\Api; +use Controller; use Model; /** @@ -14,25 +15,23 @@ use Model; * Class Map * @package Controller\Api */ -class Map extends \Controller\AccessController { +class Map extends Controller\AccessController { /** * event handler - * @param $f3 + * @param \Base $f3 */ - function beforeroute($f3) { - + function beforeroute(\Base $f3) { // set header for all routes header('Content-type: application/json'); - parent::beforeroute($f3); } /** * Get all required static config data for program initialization - * @param $f3 + * @param \Base $f3 */ - public function init($f3){ + public function init(\Base $f3){ // expire time in seconds $expireTimeHead = 60 * 60 * 12; @@ -40,10 +39,11 @@ class Map extends \Controller\AccessController { $f3->expire($expireTimeHead); - $initData = []; + $return = (object) []; + $return->error = []; // static program data ------------------------------------------------ - $initData['timer'] = $f3->get('PATHFINDER.TIMER'); + $return->timer = $f3->get('PATHFINDER.TIMER'); // get all available map types ---------------------------------------- $mapType = Model\BasicModel::getNew('MapTypeModel'); @@ -60,7 +60,7 @@ class Map extends \Controller\AccessController { $mapTypeData[$rowData->name] = $data; } - $initData['mapTypes'] = $mapTypeData; + $return->mapTypes = $mapTypeData; // get all available map scopes --------------------------------------- $mapScope = Model\BasicModel::getNew('MapScopeModel'); @@ -73,7 +73,7 @@ class Map extends \Controller\AccessController { ]; $mapScopeData[$rowData->name] = $data; } - $initData['mapScopes'] = $mapScopeData; + $return->mapScopes = $mapScopeData; // get all available system status ------------------------------------ $systemStatus = Model\BasicModel::getNew('SystemStatusModel'); @@ -87,7 +87,7 @@ class Map extends \Controller\AccessController { ]; $systemScopeData[$rowData->name] = $data; } - $initData['systemStatus'] = $systemScopeData; + $return->systemStatus = $systemScopeData; // get all available system types ------------------------------------- $systemType = Model\BasicModel::getNew('SystemTypeModel'); @@ -100,7 +100,7 @@ class Map extends \Controller\AccessController { ]; $systemTypeData[$rowData->name] = $data; } - $initData['systemType'] = $systemTypeData; + $return->systemType = $systemTypeData; // get available connection scopes ------------------------------------ $connectionScope = Model\BasicModel::getNew('ConnectionScopeModel'); @@ -114,7 +114,7 @@ class Map extends \Controller\AccessController { ]; $connectionScopeData[$rowData->name] = $data; } - $initData['connectionScopes'] = $connectionScopeData; + $return->connectionScopes = $connectionScopeData; // get available character status ------------------------------------- $characterStatus = Model\BasicModel::getNew('CharacterStatusModel'); @@ -128,24 +128,40 @@ class Map extends \Controller\AccessController { ]; $characterStatusData[$rowData->name] = $data; } - $initData['characterStatus'] = $characterStatusData; + $return->characterStatus = $characterStatusData; // get max number of shared entities per map -------------------------- $maxSharedCount = [ - 'user' => $f3->get('PATHFINDER.MAX_SHARED_USER'), + 'character' => $f3->get('PATHFINDER.MAX_SHARED_CHARACTER'), 'corporation' => $f3->get('PATHFINDER.MAX_SHARED_CORPORATION'), 'alliance' => $f3->get('PATHFINDER.MAX_SHARED_ALLIANCE'), ]; - $initData['maxSharedCount'] = $maxSharedCount; + $return->maxSharedCount = $maxSharedCount; - echo json_encode($initData); + // get program routes ------------------------------------------------- + $return->routes = [ + 'ssoLogin' => $this->getF3()->alias( 'sso', ['action' => 'requestAuthorization'] ) + ]; + + // 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) ){ + $ssoError = (object) []; + $ssoError->type = 'error'; + $ssoError->title = 'Login failed'; + $ssoError->message = $f3->get(Controller\Ccp\Sso::SESSION_KEY_SSO_ERROR); + $return->error[] = $ssoError; + $f3->clear(Controller\Ccp\Sso::SESSION_KEY_SSO_ERROR); + } + + echo json_encode($return); } /** * import new map data - * @param $f3 + * @param \Base $f3 */ - public function import($f3){ + public function import(\Base $f3){ $importData = (array)$f3->get('POST'); $return = (object) []; @@ -155,13 +171,23 @@ class Map extends \Controller\AccessController { isset($importData['typeId']) && count($importData['mapData']) > 0 ){ - $user = $this->_getUser(); + $activeCharacter = $this->getCharacter(); - if($user){ - $activeCharacter = $user->getActiveUserCharacter(); + if($activeCharacter){ + /** + * @var $map Model\MapModel + */ $map = Model\BasicModel::getNew('MapModel'); + + /** + * @var $system Model\SystemModel + */ $system = Model\BasicModel::getNew('SystemModel'); + + /** + * @var $connection Model\ConnectionModel + */ $connection = Model\BasicModel::getNew('ConnectionModel'); foreach($importData['mapData'] as $mapData){ @@ -194,8 +220,8 @@ class Map extends \Controller\AccessController { $system->setData($systemData); $system->mapId = $map; - $system->createdCharacterId = $activeCharacter->characterId; - $system->updatedCharacterId = $activeCharacter->characterId; + $system->createdCharacterId = $activeCharacter; + $system->updatedCharacterId = $activeCharacter; $system->save(); $tempSystemIdMapping[$oldId] = $system->id; @@ -203,7 +229,6 @@ class Map extends \Controller\AccessController { } } - foreach($mapData['data']['connections'] as $connectionData){ // check if source and target IDs match with new system ID if( @@ -226,15 +251,13 @@ class Map extends \Controller\AccessController { // map access info should not automatically imported if($map->isPrivate()){ - $map->setAccess($user); + $map->setAccess($activeCharacter); }elseif($map->isCorporation()){ - $corporation = $activeCharacter->getCharacter()->getCorporation(); - if($corporation){ + if($corporation = $activeCharacter->getCorporation()){ $map->setAccess($corporation); } }elseif($map->isAlliance()){ - $alliance = $activeCharacter->getCharacter()->getAlliance(); - if($alliance){ + if($alliance = $activeCharacter->getAlliance()){ $map->setAccess($alliance); } } @@ -260,7 +283,7 @@ class Map extends \Controller\AccessController { } }else{ // user not found - $return->error[] = $this->getUserLoggedOffError(); + $return->error[] = $this->getLogoutError(); } }else{ // map data missing @@ -276,25 +299,28 @@ class Map extends \Controller\AccessController { /** * save a new map or update an existing map - * @param $f3 + * @param \Base $f3 */ - public function save($f3){ + public function save(\Base $f3){ $formData = (array)$f3->get('POST.formData'); $return = (object) []; $return->error = []; if( isset($formData['id']) ){ + $activeCharacter = $this->getCharacter(0); - $user = $this->_getUser(0); + if($activeCharacter){ - if($user){ + /** + * @var $map Model\MapModel + */ $map = Model\BasicModel::getNew('MapModel'); $map->getById( (int)$formData['id'] ); if( $map->dry() || - $map->hasAccess($user) + $map->hasAccess($activeCharacter) ){ // new map $map->setData($formData); @@ -303,118 +329,117 @@ class Map extends \Controller\AccessController { // save global map access. Depends on map "type" if($map->isPrivate()){ - // share map between users -> set access - if(isset($formData['mapUsers'])){ + // share map between characters -> set access + if(isset($formData['mapCharacters'])){ // avoid abuse -> respect share limits - $accessUsers = array_slice( $formData['mapUsers'], 0, $f3->get('PATHFINDER.MAX_SHARED_USER') ); + $accessCharacters = array_slice( $formData['mapCharacters'], 0, $f3->get('PATHFINDER.MAX_SHARED_CHARACTER') ); // clear map access. In case something has removed from access list $map->clearAccess(); - $tempUser = Model\BasicModel::getNew('UserModel'); + /** + * @var $tempCharacter Model\CharacterModel + */ + $tempCharacter = Model\BasicModel::getNew('CharacterModel'); - foreach($accessUsers as $userId){ - $tempUser->getById( (int)$userId ); + foreach($accessCharacters as $characterId){ + $tempCharacter->getById( (int)$characterId ); if( - !$tempUser->dry() && - $tempUser->shared == 1 // check if map shared is enabled + !$tempCharacter->dry() && + $tempCharacter->shared == 1 // check if map shared is enabled ){ - $map->setAccess($tempUser); + $map->setAccess($tempCharacter); } - $tempUser->reset(); + $tempCharacter->reset(); } } - // the current user itself should always have access + // the current character itself should always have access // just in case he removed himself :) - $map->setAccess($user); + $map->setAccess($activeCharacter); }elseif($map->isCorporation()){ - $activeCharacter = $user->getActiveUserCharacter(); + $corporation = $activeCharacter->getCorporation(); - if($activeCharacter){ - $corporation = $activeCharacter->getCharacter()->getCorporation(); + if($corporation){ + // the current user has to have a corporation when + // working on corporation maps! - 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') ); - // 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(); - // clear map access. In case something has removed from access list - $map->clearAccess(); + /** + * @var $tempCorporation Model\CorporationModel + */ + $tempCorporation = Model\BasicModel::getNew('CorporationModel'); - $tempCorporation = Model\BasicModel::getNew('CorporationModel'); + foreach($accessCorporations as $corporationId){ + $tempCorporation->getById( (int)$corporationId ); - foreach($accessCorporations as $corporationId){ - $tempCorporation->getById( (int)$corporationId ); - - if( - !$tempCorporation->dry() && - $tempCorporation->shared == 1 // check if map shared is enabled - ){ - $map->setAccess($tempCorporation); - } - - $tempCorporation->reset(); + if( + !$tempCorporation->dry() && + $tempCorporation->shared == 1 // check if map shared is enabled + ){ + $map->setAccess($tempCorporation); } - } - // the corporation of the current user should always have access - $map->setAccess($corporation); + $tempCorporation->reset(); + } } + + // the corporation of the current user should always have access + $map->setAccess($corporation); } }elseif($map->isAlliance()){ - $activeCharacter = $user->getActiveUserCharacter(); + $alliance = $activeCharacter->getAlliance(); - if($activeCharacter){ - $alliance = $activeCharacter->getCharacter()->getAlliance(); + if($alliance){ + // the current user has to have a alliance when + // working on alliance maps! - 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') ); - // 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(); - // clear map access. In case something has removed from access list - $map->clearAccess(); + /** + * @var $tempAlliance Model\AllianceModel + */ + $tempAlliance = Model\BasicModel::getNew('AllianceModel'); - $tempAlliance = Model\BasicModel::getNew('AllianceModel'); + foreach($accessAlliances as $allianceId){ + $tempAlliance->getById( (int)$allianceId ); - foreach($accessAlliances as $allianceId){ - $tempAlliance->getById( (int)$allianceId ); - - if( - !$tempAlliance->dry() && - $tempAlliance->shared == 1 // check if map shared is enabled - ){ - $map->setAccess($tempAlliance); - } - - $tempAlliance->reset(); + if( + !$tempAlliance->dry() && + $tempAlliance->shared == 1 // check if map shared is enabled + ){ + $map->setAccess($tempAlliance); } + $tempAlliance->reset(); } - // the alliance of the current user should always have access - $map->setAccess($alliance); } + + // 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) []; @@ -423,7 +448,6 @@ class Map extends \Controller\AccessController { $return->error[] = $captchaError; } } - }else{ // map id field missing $idError = (object) []; @@ -437,17 +461,19 @@ class Map extends \Controller\AccessController { /** * delete a map and all dependencies - * @param $f3 + * @param \Base $f3 */ - public function delete($f3){ + public function delete(\Base $f3){ $mapData = (array)$f3->get('POST.mapData'); + $activeCharacter = $this->getCharacter(); - $user = $this->_getUser(); - - if($user){ + if($activeCharacter){ + /** + * @var $map Model\MapModel + */ $map = Model\BasicModel::getNew('MapModel'); $map->getById($mapData['id']); - $map->delete($user); + $map->delete( $activeCharacter ); } echo json_encode([]); @@ -455,34 +481,28 @@ class Map extends \Controller\AccessController { /** * update map data - * function is called continuously - * @param $f3 + * -> function is called continuously (trigger) by any active client + * @param \Base $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; + public function updateData(\Base $f3){ $mapData = (array)$f3->get('POST.mapData'); - $user = $this->_getUser(); + $activeCharacter = $this->getCharacter(); + $return = (object) []; $return->error = []; - if($user){ - // -> get active character - $activeCharacter = $user->getActiveUserCharacter(); + if($activeCharacter){ - $cacheKey = 'user_map_data_' . $activeCharacter->id; + $cacheKey = 'user_map_data_' . $activeCharacter->_id; // if there is any system/connection change data submitted -> save new data if( - $f3->exists($cacheKey) === false || - !empty($mapData) + !empty($mapData) || + !$f3->exists($cacheKey) ){ - // get current map data ======================================================== - $maps = $user->getMaps(); + $maps = $activeCharacter->getMaps(); // loop all submitted map data that should be saved // -> currently there will only be ONE map data change submitted -> single loop @@ -532,7 +552,7 @@ class Map extends \Controller\AccessController { unset($systemData['updated']); $system = $filteredMap->systems->current(); $system->setData($systemData); - $system->updatedCharacterId = $activeCharacter->characterId; + $system->updatedCharacterId = $activeCharacter; $system->save(); // a system belongs to ONE map -> speed up for multiple maps @@ -561,7 +581,7 @@ class Map extends \Controller\AccessController { unset($connectionData['updated']); $connection = $filteredMap->connections->current(); $connection->setData($connectionData); - $connection->save($user); + $connection->save(); // a connection belongs to ONE map -> speed up for multiple maps unset($connectionData[$i]); @@ -574,6 +594,11 @@ class Map extends \Controller\AccessController { // format map Data for return $return->mapData = self::getFormattedMapData($maps); + + // cache time(s) per user should be equal or less than this function is called + // prevent request flooding + $responseTTL = (int)$f3->get('PATHFINDER.TIMER.UPDATE_SERVER_MAP.DELAY') / 1000; + $f3->set($cacheKey, $return, $responseTTL); }else{ // get from cache @@ -582,23 +607,24 @@ class Map extends \Controller\AccessController { }else{ // user logged off - $return->error[] = $this->getUserLoggedOffError(); + $return->error[] = $this->getLogoutError(); } echo json_encode( $return ); } /** + * get formatted map data * @param $mapModels - * @return Model\MapModel[] + * @return array */ public static function getFormattedMapData($mapModels){ - $mapData = []; - foreach($mapModels as $mapModel){ - + foreach($mapModels as &$mapModel){ + /** + * @var $mapModel Model\MapModel + */ $allMapData = $mapModel->getData(); - $mapData[] = [ 'config' => $allMapData->mapData, 'data' => [ @@ -613,33 +639,23 @@ class Map extends \Controller\AccessController { /** * update map data api - * function is called continuously - * @param $f3 + * -> function is called continuously by any active client + * @param \Base $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; - + public function updateUserData(\Base $f3){ $return = (object) []; $return->error = []; + $activeCharacter = $this->getCharacter(0); - $user = $this->_getUser(); - - if($user){ + if($activeCharacter){ 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'); - - - // update current location (IGB data) - $user->updateCharacterLog(60 * 5); + // update current location + // -> suppress temporary timeout errors + $activeCharacter = $activeCharacter->updateLog(['suppressTimeoutErrors' => true]); // if data is requested extend the cache key in order to get new data $requestSystemData = (object) []; @@ -649,24 +665,23 @@ class Map extends \Controller\AccessController { // 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 + // the userMapData 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 ){ + if( !$f3->exists($cacheKey) ){ foreach($mapIds as $mapId){ - $map = $user->getMap($mapId); + $map = $activeCharacter->getMap( (int)$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 ); + $system = $map->getSystemById( $requestSystemData->systemId ); if( !is_null($system) ){ - // data for the current selected system + // data for currently selected system $return->system = $system->getData(); $return->system->signatures = $system->getSignaturesData(); } @@ -674,6 +689,10 @@ class Map extends \Controller\AccessController { } } + // cache time (seconds) should be equal or less than request trigger time + // prevent request flooding + $responseTTL = (int)$f3->get('PATHFINDER.TIMER.UPDATE_SERVER_USER_DATA.DELAY') / 1000; + // cache response $f3->set($cacheKey, $return, $responseTTL); }else{ @@ -686,14 +705,12 @@ class Map extends \Controller\AccessController { // 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(); - + $return->userData = $activeCharacter->getUser()->getData(); }else{ // user logged off - $return->error[] = $this->getUserLoggedOffError(); + $return->error[] = $this->getLogoutError(); } - echo json_encode( $return ); } diff --git a/app/main/controller/api/route.php b/app/main/controller/api/route.php index 8a75d85c..018ba2c1 100644 --- a/app/main/controller/api/route.php +++ b/app/main/controller/api/route.php @@ -18,10 +18,16 @@ use Model; class Route extends \Controller\AccessController { /** - * cache time for static jump data + * cache time for static jump data (e.g. K-Space stargates) * @var int */ - private $jumpDataCacheTime = 86400; + private $staticJumpDataCacheTime = 86400; + + /** + * cache time for dynamic jump data (e.g. W-Space systems, Jumpbridges. ...) + * @var int + */ + private $dynamicJumpDataCacheTime = 10; /** * array system information grouped by systemId @@ -41,11 +47,27 @@ class Route extends \Controller\AccessController { */ private $idArray = []; + /** + * set jump data for route search + * -> this function is required for route search! (Don´t forget) + * @param array $mapIds + * @param array $filterData + */ + public function initJumpData($mapIds = [], $filterData = []){ + // add static data (e.g. K-Space stargates,..) + $this->setStaticJumpData(); + + // add map specific data + $this->setDynamicJumpData($mapIds, $filterData); + } + /** * 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 setSystemJumpData(){ + private function setStaticJumpData(){ $cacheKey = 'staticJumpData'; $f3 = $this->getF3(); @@ -59,42 +81,167 @@ class Route extends \Controller\AccessController { $f3->exists($cacheKeyJumpArray) && $f3->exists($cacheKeyIdArray) ){ - // get cached values + // get cached values $this->nameArray = $f3->get($cacheKeyNamedArray); $this->jumpArray = $f3->get($cacheKeyJumpArray); $this->idArray = $f3->get($cacheKeyIdArray); }else{ // nothing cached - - $pfDB = $this->getDB('PF'); - $query = "SELECT * FROM system_neighbour"; - - $rows = $pfDB->exec($query, null, $this->jumpDataCacheTime); + $rows = $this->getDB()->exec($query, null, $this->staticJumpDataCacheTime); if(count($rows) > 0){ - foreach($rows as $row){ - $regionId = $row['regionId']; - $constId = $row['constellationId']; - $systemName = strtoupper($row['systemName']); - $systemId = $row['systemId']; - $secStatus = $row['trueSec']; + $this->updateJumpData($rows); - $this->nameArray[$systemId][0] = $systemName; - $this->nameArray[$systemId][1] = $regionId; - $this->nameArray[$systemId][2] = $constId; - $this->nameArray[$systemId][3] = $secStatus; + // static data should be cached + $f3->set($cacheKeyNamedArray, $this->nameArray, $this->staticJumpDataCacheTime); + $f3->set($cacheKeyJumpArray, $this->jumpArray, $this->staticJumpDataCacheTime); + $f3->set($cacheKeyIdArray, $this->idArray, $this->staticJumpDataCacheTime); + } + } + } - $this->idArray[strtoupper($systemName)] = $systemId; + /** + * 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 + */ + private function setDynamicJumpData($mapIds = [], $filterData = []){ - $this->jumpArray[$systemName]= explode(":", strtoupper($row['jumpNodes'])); - array_push($this->jumpArray[$systemName],$systemId); + if( !empty($mapIds) ){ + // make sure, mapIds are integers (protect against SQL injections) + $mapIds = array_map('intval', $mapIds); + + // connection filter -------------------------------------------------------- + $whereQuery = ""; + $includeScopes = []; + $includeTypes = []; + + 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'; } - $f3->set($cacheKeyNamedArray, $this->nameArray, $this->jumpDataCacheTime); - $f3->set($cacheKeyJumpArray, $this->jumpArray, $this->jumpDataCacheTime); - $f3->set($cacheKeyIdArray, $this->idArray, $this->jumpDataCacheTime); + if( $filterData['wormholesCritical'] === true ){ + $includeTypes[] = 'wh_critical'; + } + } + + // search connections ------------------------------------------------------- + + if( !empty($includeScopes) ){ + $whereQuery .= " connection.scope IN ('" . implode("', '", $includeScopes) . "') AND "; + + if( !empty($includeTypes) ){ + $whereQuery .= " connection.type REGEXP '" . implode("|", $includeTypes) . "' AND "; + } + + + $query = "SELECT + system_src.regionId regionId, + system_src.constellationId constellationId, + system_src.name systemName, + system_src.systemId systemId, + ( + SELECT + GROUP_CONCAT( NULLIF(system_tar.name, NULL) SEPARATOR ':') + FROM + connection INNER JOIN + system system_tar ON + system_tar.id = connection.source OR + system_tar.id = connection.target + WHERE + ( + connection.source = system_src.id OR + connection.target = system_src.id + ) AND + " . $whereQuery . " + connection.active = 1 AND + system_tar.id != system_src.id AND + system_tar.active = 1 + ) jumpNodes, + system_src.trueSec trueSec + FROM + system system_src INNER JOIN + map ON + map.id = system_src.mapId + WHERE + system_src.mapId IN (" . implode(', ', $mapIds) . ") AND + system_src.active = 1 AND + map.active = 1 + HAVING + -- skip systems without neighbors (e.g. WHs) + jumpNodes IS NOT NULL + "; + + $rows = $this->getDB()->exec($query, null, $this->dynamicJumpDataCacheTime); + + if(count($rows) > 0){ + // update jump data for this instance + $this->updateJumpData($rows); + } + + } + } + } + + /** + * update jump data for this instance + * -> data is either coming from CCPs static export 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 = strtoupper($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[$systemName]) ){ + $this->idArray[$systemName] = $systemId; + } + + // fill "jumpArray" data ---------------------------------------------------- + if( !is_array($this->jumpArray[$systemName]) ){ + $this->jumpArray[$systemName] = []; + } + $this->jumpArray[$systemName] = array_merge( explode(':', strtoupper($row['jumpNodes'])), $this->jumpArray[$systemName] ); + + // add systemId to end (if not already there) + if(end($this->jumpArray[$systemName]) != $systemId){ + array_push($this->jumpArray[$systemName],$systemId); } } } @@ -284,8 +431,6 @@ class Route extends \Controller\AccessController { !empty($systemTo) ){ - $this->setSystemJumpData(); - $from = strtoupper( $systemFrom ); $to = strtoupper( $systemTo ); @@ -294,7 +439,6 @@ class Route extends \Controller\AccessController { if( isset($this->jumpArray[$from]) ){ - // check if the system we are looking for is a direct neighbour foreach( $this->jumpArray[$from] as $n ) { @@ -352,45 +496,145 @@ class Route extends \Controller\AccessController { 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; + $key = 'route_' . hash('md5', implode('_', $keyParts)); + + return $key; + } + /** * search multiple route between two systems - * @param $f3 + * @param \Base $f3 */ public function search($f3){ - $routesData = $data = (array)$f3->get('POST.routeData'); + $requestData = (array)$f3->get('POST'); + + $activeCharacter = $this->getCharacter(); $return = (object) []; $return->error = []; $return->routesData = []; - foreach($routesData as $routeData){ - $cacheKey = self::formatHiveKey($routeData['systemFrom']) . '_' . self::formatHiveKey($routeData['systemTo']); + if( + $activeCharacter && + !empty($requestData['routeData']) + ){ - 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']); + $routesData = (array)$requestData['routeData']; - // cache if route was found - if( - isset($foundRoutData['routePossible']) && - $foundRoutData['routePossible'] === true - ){ - $f3->set($cacheKey, $foundRoutData, $this->jumpDataCacheTime); + // map data where access was already checked -> cached data + $validMaps = []; + + /** + * @var $map Model\MapModel + */ + $map = Model\BasicModel::getNew('MapModel'); + + 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){ + + if( isset($data[1][$key]) ){ + // character has mas 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'] + ]; + + $returnRoutData = [ + 'systemFrom' => $routeData['systemFrom'], + 'systemTo' => $routeData['systemTo'], + 'maps' => $mapData, + 'mapIds' => $mapIds + ]; + + // add filter options for each route as well + $returnRoutData += $filterData; + + if(count($mapIds) > 0){ + $cacheKey = $this->getRouteCacheKey( + $mapIds, + $routeData['systemFrom'], + $routeData['systemTo'], + $filterData + ); + + if($f3->exists($cacheKey)){ + // get data from cache + $returnRoutData = $f3->get($cacheKey); + }else{ + // set jump data for following route search + $this->initJumpData($mapIds, $filterData); + + // no cached route data found + $foundRoutData = $this->findRoute($routeData['systemFrom'], $routeData['systemTo']); + $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[] = $foundRoutData; + $return->routesData[] = $returnRoutData; } - } echo json_encode($return); } - } diff --git a/app/main/controller/api/signature.php b/app/main/controller/api/signature.php index bd43f344..343b6522 100644 --- a/app/main/controller/api/signature.php +++ b/app/main/controller/api/signature.php @@ -7,43 +7,49 @@ */ namespace Controller\Api; +use Controller; use Model; -class Signature extends \Controller\AccessController{ +class Signature extends Controller\AccessController{ /** * event handler - * @param $f3 + * @param \Base $f3 */ - function beforeroute($f3) { - - parent::beforeroute($f3); - + function beforeroute(\Base $f3) { // set header for all routes header('Content-type: application/json'); + parent::beforeroute($f3); } /** * get signature data for systems - * @param $f3 + * -> return value of this is limited to a "SINGLE" system + * @param \Base $f3 */ public function getAll($f3){ $signatureData = []; - $systemIds = $f3->get('POST.systemIds'); + $systemIds = (array)$f3->get('POST.systemIds'); - $user = $this->_getUser(); + if( !empty($systemIds) ){ + $activeCharacter = $this->getCharacter(); - $system = Model\BasicModel::getNew('SystemModel'); + if($activeCharacter){ + /** + * @var Model\SystemModel $system + */ + $system = Model\BasicModel::getNew('SystemModel'); + foreach($systemIds as $systemId){ + $system->getById($systemId); + if( + !$system->dry() && + $system->hasAccess($activeCharacter) + ){ + $signatureData = $system->getSignaturesData(); + } - foreach($systemIds as $systemId){ - $system->getById($systemId); - - if(!$system->dry()){ - - // check access - if($system->hasAccess($user)){ - $signatureData = $system->getSignaturesData(); + $system->reset(); } } } @@ -74,11 +80,13 @@ class Signature extends \Controller\AccessController{ } if( !is_null($signatureData) ){ - $user = $this->_getUser(); + $activeCharacter = $this->getCharacter(); - if($user){ - $activeUserCharacter = $user->getActiveUserCharacter(); - $activeCharacter = $activeUserCharacter->getCharacter(); + if($activeCharacter){ + + /** + * @var Model\SystemModel $system + */ $system = Model\BasicModel::getNew('SystemModel'); // update/add all submitted signatures @@ -91,12 +99,15 @@ class Signature extends \Controller\AccessController{ if(!$system->dry()){ // update/save signature + /** + * @var $signature Model\SystemSignatureModel + */ $signature = null; if( isset($data['pk']) ){ // try to get system by "primary key" - $signature = $system->getSignatureById($user, (int)$data['pk']); + $signature = $system->getSignatureById($activeCharacter, (int)$data['pk']); }elseif( isset($data['name']) ){ - $signature = $system->getSignatureByName($user, $data['name']); + $signature = $system->getSignatureByName($activeCharacter, $data['name']); } if( is_null($signature) ){ @@ -135,8 +146,13 @@ class Signature extends \Controller\AccessController{ // description should not be updated unset( $data['description'] ); + // prevent some data from overwrite manually changes // wormhole typeID can not figured out/saved by the sig reader dialog - if($data['groupId'] == 5){ + // -> type could not be identified -> do not overwrite them (e.g. sig update) + if( + $data['groupId'] == 5 || + $data['typeId'] == 0 + ){ unset( $data['typeId'] ); } @@ -155,6 +171,9 @@ class Signature extends \Controller\AccessController{ // 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 + /** + * @var $newSignature Model\SystemSignatureModel + */ $newSignature = Model\BasicModel::getNew('SystemSignatureModel'); $newSignature->getById( $signature->id, 0); @@ -173,23 +192,23 @@ class Signature extends \Controller\AccessController{ /** * delete signatures - * @param $f3 + * @param \Base $f3 */ public function delete($f3){ $signatureIds = $f3->get('POST.signatureIds'); + $activeCharacter = $this->getCharacter(); - $user = $this->_getUser(); + /** + * @var Model\SystemSignatureModel $signature + */ $signature = Model\BasicModel::getNew('SystemSignatureModel'); - foreach($signatureIds as $signatureId){ $signature->getById($signatureId); - - $signature->delete($user); + $signature->delete( $activeCharacter ); $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 index 3b2a78f4..0f14b896 100644 --- a/app/main/controller/api/system.php +++ b/app/main/controller/api/system.php @@ -7,20 +7,22 @@ */ namespace Controller\Api; +use Controller\Ccp\Sso; use Data\Mapper as Mapper; use Model; class System extends \Controller\AccessController { private $mainQuery = "SELECT - map_sys.constellationID connstallation_id, - map_sys.solarSystemID system_id, - map_sys.solarSystemName system_name, - ROUND( map_sys.security, 2) system_security, - map_con.constellationName constallation_name, - map_reg.regionID region_id, - map_reg.regionName region_name, - '' type, + map_sys.constellationID `connstallation_id`, + map_sys.solarSystemID `system_id`, + map_sys.solarSystemName `system_name`, + map_sys.security `system_security`, + map_con.constellationName `constallation_name`, + map_reg.regionID `region_id`, + map_reg.regionName `region_name`, + '0' `trueSec`, + '' `type`, IFNULL( ( SELECT @@ -33,7 +35,7 @@ class System extends \Controller\AccessController { system_effect.groupID = 995 AND map_norm.solarSystemID = map_sys.solarSystemID LIMIT 1 - ), '') effect, + ), '') `effect`, IFNULL( ( SELECT @@ -43,7 +45,7 @@ class System extends \Controller\AccessController { WHERE map_worm_class.locationID = map_sys.regionID LIMIT 1 - ), 7) security + ), 7) `security` FROM mapSolarSystems map_sys INNER JOIN mapConstellations map_con ON @@ -63,9 +65,9 @@ class System extends \Controller\AccessController { private $limitQuery = ""; /** - * @param $f3 + * @param \Base $f3 */ - function beforeroute($f3) { + function beforeroute(\Base $f3) { parent::beforeroute($f3); @@ -92,7 +94,8 @@ class System extends \Controller\AccessController { * get static system Data from CCPs Static DB export * search column for IDs can be (solarSystemID, regionID, constellationID) * @param array $columnIDs - * @return null + * @param string $column + * @return Model\SystemModel[] * @throws \Exception */ protected function _getSystemModelByIds($columnIDs = [], $column = 'solarSystemID'){ @@ -110,10 +113,12 @@ class System extends \Controller\AccessController { // format result $mapper = new Mapper\CcpSystemsMapper($rows); - $ccpSystemsData = $mapper->getData(); foreach($ccpSystemsData as $ccpSystemData){ + /** + * @var Model\SystemModel $system + */ $system = Model\BasicModel::getNew('SystemModel'); $system->setData($ccpSystemData); $systemModels[] = $system; @@ -142,10 +147,10 @@ class System extends \Controller\AccessController { /** * search systems by name - * @param $f3 - * @param $params + * @param \Base $f3 + * @param array $params */ - public function search($f3, $params){ + public function search(\Base $f3, $params){ $ccpDB = $this->getDB('CCP'); @@ -155,6 +160,15 @@ class System extends \Controller\AccessController { $searchToken = $params['arg1']; } + // some "edge cases for testing if rounding works correct + //$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) + $this->whereQuery = "WHERE map_sys.solarSystemName LIKE '%" . $searchToken . "%'"; @@ -172,10 +186,9 @@ class System extends \Controller\AccessController { /** * save a new system to a a map - * @param $f3 + * @param \Base $f3 */ - public function save($f3){ - + public function save(\Base $f3){ $newSystemData = []; $postData = (array)$f3->get('POST'); @@ -187,92 +200,70 @@ class System extends \Controller\AccessController { isset($postData['systemData']) && isset($postData['mapData']) ){ - $user = $this->_getUser(); + $activeCharacter = $this->getCharacter(); - if($user){ + if($activeCharacter){ $systemData = (array)$postData['systemData']; $mapData = (array)$postData['mapData']; - $activeCharacter = $user->getActiveUserCharacter(); - if( isset($systemData['id']) ){ - // update existing system + // update existing system (e.g. changed system description) ------------------- + /** + * @var $system Model\SystemModel + */ $system = Model\BasicModel::getNew('SystemModel'); $system->getById($systemData['id']); - if( !$system->dry() ){ - if( $system->hasAccess($user) ){ + if( $system->hasAccess($activeCharacter) ){ // system model found $systemModel = $system; } } }elseif( isset($mapData['id']) ){ - // save NEW system + // save NEW system ------------------------------------------------------------ + /** + * @var $map Model\MapModel + */ $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) + if( + !$map->dry() && + $map->hasAccess($activeCharacter) + ){ + // make sure system is not already on map + // --> (e.g. multiple simultaneously save() calls for the same system) + if( is_null( $systemModel = $map->getSystemByCCPId($systemData['systemId']) ) ){ + // system not found on map -> get static system data (CCP DB) $systemModel = array_values( $this->_getSystemModelByIds([$systemData['systemId']]) )[0]; - - $systemModel->createdCharacterId = $activeCharacter->characterId; - + $systemModel->createdCharacterId = $activeCharacter; } + + // map is not changeable for a system! (security) + $systemData['mapId'] = $map; } } } } - if( !is_null($systemModel) ){ // set/update system - $systemModel->setData($systemData); - $systemModel->updatedCharacterId = $activeCharacter->characterId; + $systemModel->updatedCharacterId = $activeCharacter; $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 + * @param \Base $f3 */ - public function graphData($f3){ + public function graphData(\Base $f3){ $graphData = []; $systemIds = $f3->get('POST.systemIds'); @@ -288,14 +279,13 @@ class System extends \Controller\AccessController { ]; 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); + $systemLogModel->getByForeignKey('systemId', $systemId, [], 60 * 10); - if(!$systemLogModel->dry()){ + if( !$systemLogModel->dry() ){ $counter = 0; for( $i = $logEntryCount; $i >= 1; $i--){ $column = 'value' . $i; @@ -313,7 +303,6 @@ class System extends \Controller\AccessController { $counter++; } } - } } @@ -322,25 +311,21 @@ class System extends \Controller\AccessController { /** * get system data for all systems within a constellation - * @param $f3 - * @param $params + * @param \Base $f3 + * @param array $params */ - public function constellationData($f3, $params){ - + public function constellationData(\Base $f3, $params){ $return = (object) []; $return->error = []; $return->systemData = []; - $constellationId = 0; + if( $activeCharacter = $this->getCharacter() ){ + $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)){ @@ -361,7 +346,66 @@ class System extends \Controller\AccessController { echo json_encode($return); } + /** + * set destination for specific systemIds + * @param \Base $f3 + */ + public function setDestination(\Base $f3){ + $postData = (array)$f3->get('POST'); + $return = (object) []; + $return->error = []; + $return->systemData = []; + + if( + ($activeCharacter = $this->getCharacter()) && + !empty($postData['systemData']) + ){ + $return->clearOtherWaypoints = (bool)$postData['clearOtherWaypoints']; + $return->first = (bool)$postData['first']; + + /** + * @var Sso $ssoController + */ + $ssoController = self::getController('Sso'); + foreach($postData['systemData'] as $systemData){ + $waypointData = $ssoController->setWaypoint($activeCharacter, $systemData['systemId'], [ + 'clearOtherWaypoints' => $return->clearOtherWaypoints, + 'first' => $return->first, + ]); + if($waypointData['systemId']){ + $return->systemData[] = $systemData; + }elseif( isset($waypointData['error']) ){ + $return->error[] = $waypointData['error']; + } + } + } + + echo json_encode($return); + } + + + /** + * delete systems and all its connections + * @param \Base $f3 + */ + public function delete(\Base $f3){ + $systemIds = (array)$f3->get('POST.systemIds'); + + if($activeCharacter = $this->getCharacter()){ + /** + * @var Model\SystemModel $system + */ + $system = Model\BasicModel::getNew('SystemModel'); + foreach((array)$systemIds as $systemId){ + $system->getById($systemId); + $system->delete($activeCharacter); + $system->reset(); + } + } + + echo json_encode([]); + } } diff --git a/app/main/controller/api/user.php b/app/main/controller/api/user.php index ffd1445e..be9c5c35 100644 --- a/app/main/controller/api/user.php +++ b/app/main/controller/api/user.php @@ -15,87 +15,109 @@ use DB; class User extends Controller\Controller{ + // captcha specific session keys + const SESSION_CAPTCHA_ACCOUNT_UPDATE = 'SESSION.CAPTCHA.ACCOUNT.UPDATE'; + const SESSION_CAPTCHA_ACCOUNT_DELETE = 'SESSION.CAPTCHA.ACCOUNT.DELETE'; + + // user specific session keys + const SESSION_KEY_USER = 'SESSION.USER'; + const SESSION_KEY_USER_ID = 'SESSION.USER.ID'; + const SESSION_KEY_USER_NAME = 'SESSION.USER.NAME'; + + // character specific session keys + const SESSION_KEY_CHARACTER = 'SESSION.CHARACTER'; + const SESSION_KEY_CHARACTER_ID = 'SESSION.CHARACTER.ID'; + const SESSION_KEY_CHARACTER_NAME = 'SESSION.CHARACTER.NAME'; + const SESSION_KEY_CHARACTER_TIME = 'SESSION.CHARACTER.TIME'; + + const SESSION_KEY_CHARACTER_ACCESS_TOKEN = 'SESSION.CHARACTER.ACCESS_TOKEN'; + const SESSION_KEY_CHARACTER_REFRESH_TOKEN = 'SESSION.CHARACTER.REFRESH_TOKEN'; + + // log text + const LOG_LOGGED_IN = 'userId: %s, userName: %s, charId: %s, charName: %s'; + /** * valid reasons for captcha images - * @var array + * @var string array */ - private static $captchaReason = ['createAccount', 'deleteAccount']; + private static $captchaReason = [self::SESSION_CAPTCHA_ACCOUNT_UPDATE, self::SESSION_CAPTCHA_ACCOUNT_DELETE]; /** - * login function - * @param $f3 + * login a valid character + * @param Model\CharacterModel $characterModel + * @return bool */ - public function logIn($f3){ - $data = $data = $f3->get('POST'); + protected function loginByCharacter(Model\CharacterModel &$characterModel){ + $login = false; - $return = (object) []; + if($user = $characterModel->getUser()){ + // set user/character data to session ------------------- + $this->f3->set(self::SESSION_KEY_USER, [ + 'ID' => $user->_id, + 'NAME' => $user->name + ]); - $user = null; + $dateTime = new \DateTime(); + $this->f3->set(self::SESSION_KEY_CHARACTER, [ + 'ID' => $characterModel->_id, + 'NAME' => $characterModel->name, + 'TIME' => $dateTime->getTimestamp() + ]); - if($data['loginData']){ - $loginData = $data['loginData']; - $user = $this->logUserIn( $loginData['userName'], $loginData['userPassword'] ); + // save user login information --------------------------- + $characterModel->touch('lastLogin'); + $characterModel->save(); + + // write login log -------------------------------------- + self::getLogger( $this->f3->get('PATHFINDER.LOGFILES.LOGIN') )->write( + sprintf(self::LOG_LOGGED_IN, + $user->_id, + $user->name, + $characterModel->_id, + $characterModel->name + ) + ); + + $login = true; } - // set "vague" error - if(is_null($user)){ - $return->error = []; - $loginError = (object) []; - $loginError->type = 'login'; - $return->error[] = $loginError; - }else{ - // update/check api data - $user->updateApiData(); + return $login; + } - // route user to map app - $return->reroute = rtrim(self::getEnvironmentData('URL'), '/') . $f3->alias('map'); + /** + * validate cookie character information + * -> return character data (if valid) + * @param \Base $f3 + */ + public function getCookieCharacter($f3){ + $data = $f3->get('POST'); + + $return = (object) []; + $return->error = []; + + if( !empty($data['cookie']) ){ + if( !empty($cookieData = $this->getCookieByName($data['cookie']) )){ + // 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 + $return->character = reset($characters)->getData(); + }else{ + $characterError = (object) []; + $characterError->type = 'warning'; + $characterError->message = 'This can happen through "invalid cookie data", "login restrictions", "CREST problems".'; + $return->error[] = $characterError; + } + } } 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(), - 'name' => $user->name, - '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 + * @param \Base $f3 */ - public function getCaptcha($f3){ + public function getCaptcha(\Base $f3){ $data = $f3->get('POST'); $return = (object) []; @@ -117,7 +139,7 @@ class User extends Controller\Controller{ 'fonts/oxygen-bold-webfont.ttf', 14, 6, - 'SESSION.' . $reason, + $reason, '', $colorText, $colorBG @@ -136,337 +158,132 @@ class User extends Controller\Controller{ /** * delete the character log entry for the current active (main) character - * @param $f3 + * @param \Base $f3 */ - public function deleteLog($f3){ - - $user = $this->_getUser(); - if($user){ - $activeUserCharacter = $user->getActiveUserCharacter(); - - if($activeUserCharacter){ - $character = $activeUserCharacter->getCharacter(); - - if($characterLog = $character->getLog()){ - $characterLog->erase(); - } + public function deleteLog(\Base $f3){ + $activeCharacter = $this->getCharacter(); + if($activeCharacter){ + if($characterLog = $activeCharacter->getLog()){ + $characterLog->erase(); } } } /** * log the current user out + clear character system log data - * @param $f3 + * @param \Base $f3 */ - public function logOut($f3){ + public function logout(\Base $f3){ $this->deleteLog($f3); - parent::logOut($f3); + parent::logout($f3); } /** - * save/update "map sharing" configurations for all map types - * the user has access to - * @param $f3 + * update user account data + * -> a fresh user automatically generated on first login with a new character + * -> see CREST SSO login + * @param \Base $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->shared = $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->shared = $corporationSharing; - $corporation->save(); - } - - if(is_object($alliance)){ - $alliance->shared = $allianceSharing; - $alliance->save(); - } - } - - $return->userData = $user->getData(); - } - - echo json_encode($return); - } - - /** - * search for a registration key model - * e.g. for new user registration with "invite" feature enabled - * @param $email - * @param $registrationKey - * @return bool|Model\RegistrationKeyModel - * @throws Exception - */ - protected function getRegistrationKey($email, $registrationKey){ - $registrationKeyModel = Model\BasicModel::getNew('RegistrationKeyModel'); - $registrationKeyModel->load([ - 'registrationKey = :registrationKey AND - email = :email AND - used = 0 AND - active = 1', - ':registrationKey' => $registrationKey, - ':email' => $email - ]); - - if( $registrationKeyModel->dry() ){ - return false; - }else{ - return $registrationKeyModel; - } - } - - /** - * check if there is already an active Key for a mail - * @param $email - * @param bool|false $used - * @return bool|null - * @throws Exception - */ - protected function findRegistrationKey($email, $used = false){ - - $queryPart = 'email = :email AND active = 1'; - - if(is_int($used)){ - $queryPart .= ' AND used = ' . $used; - } - - $registrationKeyModel = Model\BasicModel::getNew('RegistrationKeyModel'); - $registrationKeyModels = $registrationKeyModel->find([ - $queryPart, - ':email' => $email - ]); - - if( is_object($registrationKeyModels) ){ - return $registrationKeyModels; - }else{ - return false; - } - } - - /** - * save/update user account data - * @param $f3 - */ - public function saveAccount($f3){ + public function saveAccount(\Base $f3){ $data = $f3->get('POST'); $return = (object) []; $return->error = []; - $captcha = $f3->get('SESSION.createAccount'); + $captcha = $f3->get(self::SESSION_CAPTCHA_ACCOUNT_UPDATE); // reset captcha -> forces user to enter new one - $f3->clear('SESSION.createAccount'); + $f3->clear(self::SESSION_CAPTCHA_ACCOUNT_UPDATE); $newUserData = null; - // check for new user - $loginAfterSave = false; - - // valid registration key Model is required for new registration - // if "invite" feature is enabled - $registrationKeyModel = false; - - if( isset($data['settingsData']) ){ - $settingsData = $data['settingsData']; + if( isset($data['formData']) ){ + $formData = $data['formData']; try{ - $user = $this->_getUser(0); + if($activeCharacter = $this->getCharacter(0)){ + $user = $activeCharacter->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){ - - // check if registration key invite function is enabled - if($f3->get('PATHFINDER.REGISTRATION.INVITE') === 1 ){ - $registrationKeyModel = $this->getRegistrationKey( $settingsData['email'], $settingsData['registrationKey'] ); - - if($registrationKeyModel === false){ - throw new Exception\RegistrationException('Registration key invalid', 'registrationKey'); - } - } - - // new user registration - $user = $mapType = Model\BasicModel::getNew('UserModel'); - $loginAfterSave = true; + // 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($settingsData['name']) && - !empty($settingsData['name']) + isset($formData['name']) && + !empty($formData['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; - } + $user->name = $formData['name']; } - if(is_null($api)){ - // new API Key - $api = Model\BasicModel::getNew('UserApiModel'); - $api->userId = $user; + // 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']; } - $api->keyId = $keyId; - $api->vCode = $settingsData['vCode'][$i]; - $api->save(); + // save/update user model + // this will fail if model validation fails! + $user->save(); - $characterCount = $api->updateCharacters(); + }else{ + // captcha was send but not valid -> return error + $captchaError = (object) []; + $captchaError->type = 'error'; + $captchaError->message = 'Captcha does not match'; + $return->error[] = $captchaError; + } + } - 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; - } + // sharing config --------------------------------------------------- + if(isset($formData['share'])){ + $privateSharing = 0; + $corporationSharing = 0; + $allianceSharing = 0; + + if(isset($formData['privateSharing'])){ + $privateSharing = 1; } - // delete API models that no longer exists - foreach($apiModels as $apiModel){ - $apiModel->delete(); + if(isset($formData['corporationSharing'])){ + $corporationSharing = 1; } - // get fresh updated user object (API info may have has changed) - $user = $this->_getUser(0); - } + if(isset($formData['allianceSharing'])){ + $allianceSharing = 1; + } - // set main character - if( isset($settingsData['mainCharacterId']) ){ - $user->setMainCharacterId((int)$settingsData['mainCharacterId']); - } + // update private/corp/ally + $corporation = $activeCharacter->getCorporation(); + $alliance = $activeCharacter->getAlliance(); - // check if the user already has a main character - // if not -> save the next best character as main - $mainUserCharacter = $user->getMainUserCharacter(); + if(is_object($corporation)){ + $corporation->shared = $corporationSharing; + $corporation->save(); + } - // set main character if no main character exists - if(is_null($mainUserCharacter)){ - $user->setMainCharacterId(); - } + if(is_object($alliance)){ + $alliance->shared = $allianceSharing; + $alliance->save(); + } - // save/update user model - // this will fail if model validation fails! - $user->save(); - - if(is_object($registrationKeyModel)){ - $registrationKeyModel->used = 1; - $registrationKeyModel->save(); - } - - // log user in (in case he is new - if($loginAfterSave){ - $this->logUserIn( $user->name, $settingsData['password'] ); - - // return reroute path - $return->reroute = rtrim(self::getEnvironmentData('URL'), '/') . $this->f3->alias('map'); + $activeCharacter->shared = $privateSharing; + $activeCharacter->save(); } // get fresh updated user object - $user = $this->_getUser(0); $newUserData = $user->getData(); } + }catch(Exception\ValidationException $e){ $validationError = (object) []; $validationError->type = 'error'; @@ -483,109 +300,6 @@ class User extends Controller\Controller{ // return new/updated user data $return->userData = $newUserData; - - } - echo json_encode($return); - } - - /** - * send mail with registration key - * -> check INVITE in pathfinder.ini - * @param $f3 - * @throws Exception - */ - public function sendInvite($f3){ - $data = $f3->get('POST.settingsData'); - $return = (object) []; - - // check invite limit - // get handed out key count - $tempRegistrationKeyModel = Model\BasicModel::getNew('RegistrationKeyModel'); - $tempRegistrationKeyModels = $tempRegistrationKeyModel->find([ ' - email != "" AND - active = 1' - ]); - - $totalKeys = 0; - if(is_object($tempRegistrationKeyModels)){ - $totalKeys = $tempRegistrationKeyModels->count(); - } - - if( - $f3->get('PATHFINDER.REGISTRATION.INVITE') == 1 && - $totalKeys < $f3->get('PATHFINDER.REGISTRATION.INVITE_LIMIT') - ){ - // key limit not reached - - if( - isset($data['email']) && - !empty($data['email']) - ){ - $email = trim($data['email']); - - // check if mail is valid - if( \Audit::instance()->email($email) ){ - - // new key for this mail is allowed - $registrationKeyModel = $this->findRegistrationKey($email, 0); - - if($registrationKeyModel === false){ - - // check for total number of invites (active and inactive) -> prevent spamming - $allRegistrationKeysByMail = $this->findRegistrationKey($email); - - if( - $allRegistrationKeysByMail == false || - $allRegistrationKeysByMail->count() < 3 - ){ - - // get a fresh key - $registrationKeyModel = Model\BasicModel::getNew('RegistrationKeyModel'); - $registrationKeyModel->load([' - used = 0 AND - active = 1 AND - email = "" ', - ':email' => $email - ], ['limit' => 1]); - - }else{ - $validationError = (object) []; - $validationError->type = 'warning'; - $validationError->message = 'The number of keys is limited by Email. You can not get more keys'; - $return->error[] = $validationError; - } - - }else{ - $registrationKeyModel = $registrationKeyModel[0]; - } - - // send "old" key again or send a new key - if( is_object($registrationKeyModel) ){ - $msg = 'Your personal Registration Key: ' . $registrationKeyModel->registrationKey; - - $mailController = new MailController(); - $status = $mailController->sendInviteKey($email, $msg); - - if( $status ){ - $registrationKeyModel->email = $email; - $registrationKeyModel->ip = $this->f3->get('IP'); - $registrationKeyModel->save(); - } - } - - }else{ - $validationError = (object) []; - $validationError->type = 'error'; - $validationError->field = 'email'; - $validationError->message = 'Email is not valid'; - $return->error[] = $validationError; - } - } - }else{ - $validationError = (object) []; - $validationError->type = 'warning'; - $validationError->message = 'The pool of beta keys has been exhausted, please try again in a few days/weeks'; - $return->error[] = $validationError; } echo json_encode($return); @@ -593,31 +307,26 @@ class User extends Controller\Controller{ /** * delete current user account from DB - * @param $f3 + * @param \Base $f3 */ - public function deleteAccount($f3){ + public function deleteAccount(\Base $f3){ $data = $f3->get('POST.formData'); $return = (object) []; - $captcha = $f3->get('SESSION.deleteAccount'); + $captcha = $f3->get(self::SESSION_CAPTCHA_ACCOUNT_DELETE); // reset captcha -> forces user to enter new one - $f3->clear('SESSION.deleteAccount'); + $f3->clear(self::SESSION_CAPTCHA_ACCOUNT_DELETE); if( isset($data['captcha']) && !empty($data['captcha']) && $data['captcha'] === $captcha ){ - $user = $this->_getUser(0); + $activeCharacter = $this->getCharacter(0); + $user = $activeCharacter->getUser(); - $validUser = $this->_verifyUser( $user->name, $data['password']); - - if( - is_object($validUser) && - is_object($user) && - $user->id === $validUser->id - ){ + if($user){ // send delete account mail $msg = 'Hello ' . $user->name . ',

'; $msg .= 'your account data has been successfully deleted.'; @@ -635,15 +344,9 @@ class User extends Controller\Controller{ // remove user $user->erase(); - $this->logOut($f3); + $this->logout($f3); die(); } - }else{ - // password does not match current user pw - $passwordError = (object) []; - $passwordError->type = 'error'; - $passwordError->message = 'Invalid password'; - $return->error[] = $passwordError; } }else{ // captcha not valid -> return error diff --git a/app/main/controller/appcontroller.php b/app/main/controller/appcontroller.php index 62aec0c8..2bec783c 100644 --- a/app/main/controller/appcontroller.php +++ b/app/main/controller/appcontroller.php @@ -7,15 +7,29 @@ */ namespace Controller; - +use Controller\Ccp as Ccp; +use Model; class AppController extends Controller { /** - * show main login (index) page - * @param $f3 + * event handler after routing + * @param \Base $f3 */ - public function init($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) { // page title $f3->set('pageTitle', 'Login'); @@ -30,6 +44,12 @@ class AppController extends Controller { // JS main file $f3->set('jsView', 'login'); + + // 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)) ); + }); } } \ No newline at end of file diff --git a/app/main/controller/ccp/sso.php b/app/main/controller/ccp/sso.php new file mode 100644 index 00000000..b0051766 --- /dev/null +++ b/app/main/controller/ccp/sso.php @@ -0,0 +1,894 @@ +request automatically caches responses by their response "Cache-Control" header! + */ + +namespace Controller\Ccp; +use Controller; +use Controller\Api as Api; +use Data\Mapper as Mapper; +use Model; +use Lib; + +class Sso extends Api\User{ + + /** + * @var int timeout (seconds) for API calls + */ + const CREST_TIMEOUT = 4; + + /** + * @var int expire time (seconds) for an valid "accessToken" + */ + const ACCESS_KEY_EXPIRE_TIME = 20 * 60; + + // SSO specific session keys + const SESSION_KEY_SSO = 'SESSION.SSO'; + const SESSION_KEY_SSO_ERROR = 'SESSION.SSO.ERROR'; + const SESSION_KEY_SSO_STATE = 'SESSION.SSO.STATE'; + const SESSION_KEY_SSO_FROM_MAP = 'SESSION.SSO.FROM_MAP'; + + // cache keys + const CACHE_KEY_LOCATION_DATA = 'CACHED.LOCATION.%s'; + + // error messages + const ERROR_CCP_SSO_URL = 'Invalid "ENVIRONMENT.[ENVIRONMENT].SSO_CCP_URL" url. %s'; + const ERROR_CCP_CREST_URL = 'Invalid "ENVIRONMENT.[ENVIRONMENT].CCP_CREST_URL" url. %s'; + const ERROR_CCP_CLIENT_ID = 'Missing "ENVIRONMENT.[ENVIRONMENT].SSO_CCP_CLIENT_ID".'; + const ERROR_RESOURCE_DEPRECATED = 'Resource: %s has been marked as deprecated. %s'; + const ERROR_ACCESS_TOKEN = 'Unable to get a valid "access_token. %s'; + const ERROR_VERIFY_CHARACTER = 'Unable to verify character data. %s'; + const ERROR_GET_ENDPOINT = 'Unable to get endpoint data. $s'; + const ERROR_FIND_ENDPOINT = 'Unable to find endpoint: %s'; + const ERROR_LOGIN_FAILED = 'Failed authentication due to technical problems: %s'; + const ERROR_CHARACTER_VERIFICATION = 'Character verification failed from CREST'; + const ERROR_CHARACTER_FORBIDDEN = 'Character "%s" is not authorized to log in'; + const ERROR_SERVICE_TIMEOUT = 'CCP SSO service timeout (%ss). Try again later'; + const ERROR_COOKIE_LOGIN = 'Login from Cookie failed. Please retry by CCP SSO'; + + /** + * CREST "Scopes" are used by pathfinder + * -> Enable scopes: https://developers.eveonline.com + * @var array + */ + private $requestScopes = [ + // 'characterFittingsRead', + // 'characterFittingsWrite', + 'characterLocationRead', + 'characterNavigationWrite' + ]; + + /** + * redirect user to CCP SSO page and request authorization + * -> cf. Controller->getCookieCharacters() ( equivalent cookie based login) + * @param \Base $f3 + */ + public function requestAuthorization($f3){ + + if( !empty($ssoCcpClientId = Controller\Controller::getEnvironmentData('SSO_CCP_CLIENT_ID')) ){ + $params = $f3->get('GET'); + + if( + isset($params['characterId']) && + ( $activeCharacter = $this->getCharacter(0) ) + ){ + // authentication restricted to a characterId ----------------------------------------------- + // restrict login to this characterId e.g. for character switch on map page + $characterId = (int)trim($params['characterId']); + + /** + * @var Model\CharacterModel $character + */ + $character = Model\BasicModel::getNew('CharacterModel'); + $character->getById($characterId, 0); + + // check if character is valid and exists + if( + !$character->dry() && + $character->hasUserCharacter() && + ($activeCharacter->getUser()->_id === $character->getUser()->_id) + ){ + // requested character belongs to current user + // -> update character vom CREST (e.g. corp changed,..) + $updateStatus = $character->updateFromCrest(); + + 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() + ){ + $loginCheck = $this->loginByCharacter($character); + + if($loginCheck){ + // 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, true); + } + + // redirect to CCP SSO ---------------------------------------------------------------------- + + // used for "state" check between request and callback + $state = bin2hex(mcrypt_create_iv(12, MCRYPT_DEV_URANDOM)); + $f3->set(self::SESSION_KEY_SSO_STATE, $state); + + $urlParams = [ + 'response_type' => 'code', + 'redirect_uri' => Controller\Controller::getEnvironmentData('URL') . $f3->build('/sso/callbackAuthorization'), + 'client_id' => Controller\Controller::getEnvironmentData('SSO_CCP_CLIENT_ID'), + 'scope' => implode(' ', $this->requestScopes), + 'state' => $state + ]; + + $ssoAuthUrl = self::getAuthorizationEndpoint() . '?' . 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::getCrestLogger()->write(self::ERROR_CCP_CLIENT_ID); + $f3->reroute('@login'); + } + } + + /** + * callback handler for CCP SSO user Auth + * -> see requestAuthorization() + * @param \Base $f3 + */ + public function callbackAuthorization($f3){ + $getParams = (array)$f3->get('GET'); + + // users can log in either from @login (new user) or @map (existing user) root alias + // -> in case login fails, users should be redirected differently + $authFromMapAlias = false; + + 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'] + ){ + // check if user came from map (for redirect) + if( $f3->get(self::SESSION_KEY_SSO_FROM_MAP) ){ + $authFromMapAlias = true; + } + + // clear 'state' for new next login request + $f3->clear(self::SESSION_KEY_SSO_STATE); + $f3->clear(self::SESSION_KEY_SSO_FROM_MAP); + + $accessData = $this->getCrestAccessData($getParams['code']); + + if( + isset($accessData->accessToken) && + isset($accessData->refreshToken) + ){ + // login succeeded -> get basic character data for current login + $verificationCharacterData = $this->verifyCharacterData($accessData->accessToken); + + if( !is_null($verificationCharacterData)){ + + // check if login is restricted to a characterID + + // verification available data. Data is needed for "ownerHash" check + + // get character data from CREST + $characterData = $this->getCharacterData($accessData->accessToken); + + if( isset($characterData->character) ){ + // add "ownerHash" and CREST tokens + $characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash; + $characterData->character['crestAccessToken'] = $accessData->accessToken; + $characterData->character['crestRefreshToken'] = $accessData->refreshToken; + + // add/update static character data + $characterModel = $this->updateCharacter($characterData); + + if( !is_null($characterModel) ){ + // check if character is authorized to log in + if($characterModel->isAuthorized()){ + + // character is authorized to log in + // -> update character log (current location,...) + $characterModel = $characterModel->updateLog(); + + // check if there is already an active user logged in + if($activeCharacter = $this->getCharacter()){ + // connect character with current user + $user = $activeCharacter->getUser(); + }elseif( is_null( $user = $characterModel->getUser()) ){ + // no user found (new character) -> create new user and connect to character + $user = Model\BasicModel::getNew('UserModel'); + $user->name = $characterModel->name; + $user->save(); + } + + /** + * @var $userCharactersModel Model\UserCharacterModel + */ + if( is_null($userCharactersModel = $characterModel->userCharacter) ){ + $userCharactersModel = Model\BasicModel::getNew('UserCharacterModel'); + $userCharactersModel->characterId = $characterModel; + } + + // user might have changed + $userCharactersModel->userId = $user; + $userCharactersModel->save(); + + // get updated character model + $characterModel = $userCharactersModel->getCharacter(); + + // login by character + $loginCheck = $this->loginByCharacter($characterModel); + + if($loginCheck){ + // set "login" cookie + $this->setLoginCookie($characterModel); + + // route to "map" + $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)); + } + } + } + }else{ + // failed to verify character by CREST + $f3->set(self::SESSION_KEY_SSO_ERROR, self::ERROR_CHARACTER_VERIFICATION); + } + }else{ + // CREST "accessData" missing (e.g. timeout) + $f3->set(self::SESSION_KEY_SSO_ERROR, sprintf(self::ERROR_SERVICE_TIMEOUT, self::CREST_TIMEOUT)); + } + }else{ + // invalid CREST response + $f3->set(self::SESSION_KEY_SSO_ERROR, sprintf(self::ERROR_LOGIN_FAILED, 'Invalid response')); + } + } + + if($authFromMapAlias){ + // on error -> route back to map + $f3->reroute('@map'); + }else{ + // on error -> route back to login form + $f3->reroute('@login'); + } + } + + /** + * login by cookie name + * @param \Base $f3 + */ + public function login(\Base $f3){ + $data = (array)$f3->get('GET'); + $cookieName = empty($data['cookie']) ? '' : $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 + $loginCheck = $this->loginByCharacter($character); + if($loginCheck){ + // route to "map" + $f3->reroute('@map'); + } + } + + // on error -> route back to login form + $f3->set(self::SESSION_KEY_SSO_ERROR, self::ERROR_COOKIE_LOGIN); + $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 + */ + public function getCrestAccessData($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::getCrestLogger()->write(sprintf(self::ERROR_ACCESS_TOKEN, $authCode)); + } + + return $accessData; + } + + /** + * verify authorization code, and get an "access_token" data + * @param $authCode + * @return \stdClass + */ + protected function verifyAuthorizationCode($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 $refreshToken + * @return \stdClass + */ + public function refreshAccessToken($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 $requestParams + * @return \stdClass + */ + protected function requestAccessData($requestParams){ + $verifyAuthCodeUrl = self::getVerifyAuthorizationCodeEndpoint(); + $verifyAuthCodeUrlParts = parse_url($verifyAuthCodeUrl); + + $accessData = (object) []; + $accessData->accessToken = null; + $accessData->refreshToken = null; + + if($verifyAuthCodeUrlParts){ + $contentType = 'application/x-www-form-urlencoded'; + $requestOptions = [ + 'timeout' => self::CREST_TIMEOUT, + 'method' => 'POST', + 'user_agent' => $this->getUserAgent(), + 'header' => [ + 'Authorization: Basic ' . $this->getAuthorizationHeader(), + 'Content-Type: ' . $contentType, + 'Host: ' . $verifyAuthCodeUrlParts['host'] + ] + ]; + + // content (parameters to send with) + $requestOptions['content'] = http_build_query($requestParams); + + $apiResponse = Lib\Web::instance()->request($verifyAuthCodeUrl, $requestOptions); + + if($apiResponse['body']){ + $authCodeRequestData = json_decode($apiResponse['body'], true); + + if( !empty($authCodeRequestData) ){ + if( isset($authCodeRequestData['access_token']) ){ + // this token is required for endpoints that require Auth + $accessData->accessToken = $authCodeRequestData['access_token']; + } + + if(isset($authCodeRequestData['refresh_token'])){ + // this token is used to refresh/get a new access_token when expires + $accessData->refreshToken = $authCodeRequestData['refresh_token']; + } + } + }else{ + self::getCrestLogger()->write( + sprintf( + self::ERROR_ACCESS_TOKEN, + print_r($requestParams, true) + ) + ); + } + }else{ + self::getCrestLogger()->write( + sprintf(self::ERROR_CCP_SSO_URL, __METHOD__) + ); + } + + return $accessData; + } + + /** + * verify character data by "access_token" + * -> get some basic information (like character id) + * -> if more character information is required, use CREST endpoints request instead + * @param $accessToken + * @return mixed|null + */ + public function verifyCharacterData($accessToken){ + $verifyUserUrl = self::getVerifyUserEndpoint(); + $verifyUrlParts = parse_url($verifyUserUrl); + $characterData = null; + + if($verifyUrlParts){ + $requestOptions = [ + 'timeout' => self::CREST_TIMEOUT, + 'method' => 'GET', + 'user_agent' => $this->getUserAgent(), + 'header' => [ + 'Authorization: Bearer ' . $accessToken, + 'Host: ' . $verifyUrlParts['host'] + ] + ]; + + $apiResponse = Lib\Web::instance()->request($verifyUserUrl, $requestOptions); + + if($apiResponse['body']){ + $characterData = json_decode($apiResponse['body']); + }else{ + self::getCrestLogger()->write(sprintf(self::ERROR_VERIFY_CHARACTER, __METHOD__)); + } + }else{ + self::getCrestLogger()->write(sprintf(self::ERROR_CCP_SSO_URL, __METHOD__)); + } + + return $characterData; + } + + /** + * get all available Endpoints + * @param $accessToken + * @param array $additionalOptions + * @return mixed|null + */ + protected function getEndpoints($accessToken = '', $additionalOptions = []){ + $crestUrl = self::getCrestEndpoint(); + $additionalOptions['accept'] = 'application/vnd.ccp.eve.Api-v3+json'; + $endpoint = $this->getEndpoint($crestUrl, $accessToken, $additionalOptions); + + return $endpoint; + } + + /** + * get a specific endpoint by its $resourceUrl + * @param string $resourceUrl endpoint API url + * @param string $accessToken CREST access token + * @param array $additionalOptions optional request options (pathfinder specific) + * @return mixed|null + */ + protected function getEndpoint($resourceUrl, $accessToken = '', $additionalOptions = []){ + $resourceUrlParts = parse_url($resourceUrl); + $endpoint = null; + + if($resourceUrlParts){ + $requestOptions = [ + 'timeout' => self::CREST_TIMEOUT, + 'method' => 'GET', + 'user_agent' => $this->getUserAgent(), + 'header' => [ + 'Host: login.eveonline.com', + 'Host: ' . $resourceUrlParts['host'] + ] + ]; + + // some endpoints don´t require an "access_token" (e.g. public crest data) + if( !empty($accessToken) ){ + $requestOptions['header'][] = 'Authorization: Bearer ' . $accessToken; + } + + // if specific contentType is required -> add it to request header + // CREST versioning can be done by calling different "Accept:" Headers + if( isset($additionalOptions['accept']) ){ + $requestOptions['header'][] = 'Accept: ' . $additionalOptions['accept']; + } + + $apiResponse = Lib\Web::instance()->request($resourceUrl, $requestOptions, $additionalOptions); + + if( + $apiResponse['timeout'] === false && + $apiResponse['headers'] + ){ + // check headers for error + $this->checkResponseHeaders($apiResponse['headers'], $requestOptions); + + if($apiResponse['body']){ + $endpoint = json_decode($apiResponse['body'], true); + }else{ + self::getCrestLogger()->write(sprintf(self::ERROR_GET_ENDPOINT, __METHOD__)); + } + } + }else{ + self::getCrestLogger()->write(sprintf(self::ERROR_CCP_CREST_URL, __METHOD__)); + } + + return $endpoint; + } + + /** + * recursively walk down the CREST API tree by a given $path array + * -> return "leaf" endpoint + * @param $endpoint + * @param $accessToken + * @param array $path + * @param array $additionalOptions + * @return null + */ + protected function walkEndpoint($endpoint, $accessToken, $path = [], $additionalOptions = []){ + $targetEndpoint = null; + + if( !empty($path) ){ + $newNode = array_shift($path); + if(isset($endpoint[$newNode])){ + $currentEndpoint = $endpoint[$newNode]; + if(isset($currentEndpoint['href'])){ + $newEndpoint = $this->getEndpoint($currentEndpoint['href'], $accessToken, $additionalOptions); + $targetEndpoint = $this->walkEndpoint($newEndpoint, $accessToken, $path, $additionalOptions); + }else{ + // leaf found + $targetEndpoint = $currentEndpoint; + } + }else{ + // endpoint not found + self::getCrestLogger()->write(sprintf(self::ERROR_FIND_ENDPOINT, $newNode)); + } + }else{ + $targetEndpoint = $endpoint; + } + + return $targetEndpoint; + } + + /** + * get character data + * @param $accessToken + * @param array $additionalOptions + * @return object + */ + public function getCharacterData($accessToken, $additionalOptions = []){ + $endpoints = $this->getEndpoints($accessToken, $additionalOptions); + $characterData = (object) []; + + $endpoint = $this->walkEndpoint($endpoints, $accessToken, [ + 'decode', + 'character' + ], $additionalOptions); + + if( !empty($endpoint) ){ + $crestCharacterData = (new Mapper\CrestCharacter($endpoint))->getData(); + $characterData->character = $crestCharacterData + ; + if(isset($endpoint['corporation'])){ + $characterData->corporation = (new Mapper\CrestCorporation($endpoint['corporation']))->getData(); + } + + // IMPORTANT: alliance data is not yet available over CREST! + // -> we need to request them over the XML api + /* + if(isset($endpoint['alliance'])){ + $characterData->alliance = (new Mapper\CrestAlliance($endpoint['alliance']))->getData(); + } + */ + + $xmlCharacterData = (new Xml())->getPublicCharacterData( (int)$crestCharacterData['id'] ); + if(isset($xmlCharacterData['alli'])){ + $characterData->alliance = $xmlCharacterData['alli']; + } + } + + return $characterData; + } + + /** + * get current character location data (result is cached!) + * -> solarSystem data where character is currently active + * @param $accessToken + * @param int $ttl + * @param array $additionalOptions + * @return array|mixed + */ + public function getCharacterLocationData($accessToken, $ttl = 10, $additionalOptions = []){ + // null == CREST call failed (e.g. timeout) + $locationData = [ + 'timeout' => false + ]; + + // in addition to the cURL caching (based on cache-control headers, + // the final location data is cached additionally -> speed up + $cacheKey = sprintf(self::CACHE_KEY_LOCATION_DATA, 'TOKEN_' . hash('md5', $accessToken)); + + if( !$this->getF3()->exists($cacheKey) ){ + $endpoints = $this->getEndpoints($accessToken, $additionalOptions); + + $additionalOptions['accept'] = 'application/vnd.ccp.eve.CharacterLocation-v1+json'; + $endpoint = $this->walkEndpoint($endpoints, $accessToken, [ + 'decode', + 'character', + 'location' + ], $additionalOptions); + + if( !is_null($endpoint) ){ + // request succeeded (e.g. no timeout) + + if(isset($endpoint['solarSystem'])){ + $locationData['system'] = (new Mapper\CrestSystem($endpoint['solarSystem']))->getData(); + } + + if(isset($endpoint['station'])){ + $locationData['station'] = (new Mapper\CrestStation($endpoint['station']))->getData(); + } + + $this->getF3()->set($cacheKey, $locationData, $ttl); + }else{ + // timeout + $locationData['timeout'] = true; + } + + }else{ + $locationData = $this->getF3()->get($cacheKey); + } + + return $locationData; + } + + /** + * set new ingame waypoint + * @param Model\CharacterModel $character + * @param int $systemId + * @param array $options + * @return array + */ + public function setWaypoint( Model\CharacterModel $character, $systemId, $options = []){ + $crestUrlParts = parse_url( self::getCrestEndpoint() ); + $waypointData = []; + + if( $crestUrlParts ){ + $endpointUrl = self::getCrestEndpoint() . '/characters/' . $character->_id . '/navigation/waypoints/'; + $systemEndpoint = self::getCrestEndpoint() . '/solarsystems/' . $systemId . '/'; + + // request body + $content = [ + 'clearOtherWaypoints' => (bool)$options['clearOtherWaypoints'], + 'first' => (bool)$options['first'], + 'solarSystem' => [ + 'href' => $systemEndpoint, + 'id' => (int)$systemId + ] + ]; + + $requestOptions = [ + 'timeout' => self::CREST_TIMEOUT, + 'method' => 'POST', + 'user_agent' => $this->getUserAgent(), + 'header' => [ + 'Scope: characterNavigationWrite', + 'Authorization: Bearer ' . $character->getAccessToken(), + 'Host: ' . $crestUrlParts['host'], + 'Content-Type: application/vnd.ccp.eve.PostWaypoint-v1+json;charset=utf-8', + ], + 'content' => json_encode($content, JSON_UNESCAPED_SLASHES) + ]; + + $apiResponse = Lib\Web::instance()->request($endpointUrl, $requestOptions); + + if( isset($apiResponse['body']) ){ + $responseData = json_decode($apiResponse['body']); + + if( empty($responseData) ){ + $waypointData['systemId'] = (int)$systemId; + }elseif( + isset($responseData->message) && + isset($responseData->key) + ){ + // waypoint could not be set... + $error = (object) []; + $error->type = 'error'; + $error->message = $responseData->key; + $waypointData['error'] = $error; + } + } + } + + return $waypointData; + } + + /** + * update character + * @param $characterData + * @return \Model\CharacterModel + * @throws \Exception + */ + protected function updateCharacter($characterData){ + + $characterModel = null; + $corporationModel = null; + $allianceModel = null; + + if( isset($characterData->corporation) ){ + /** + * @var Model\CorporationModel $corporationModel + */ + $corporationModel = Model\BasicModel::getNew('CorporationModel'); + $corporationModel->getById($characterData->corporation['id'], 0); + $corporationModel->copyfrom($characterData->corporation); + $corporationModel->save(); + } + + if( isset($characterData->alliance) ){ + /** + * @var Model\AllianceModel $allianceModel + */ + $allianceModel = Model\BasicModel::getNew('AllianceModel'); + $allianceModel->getById($characterData->alliance['id'], 0); + $allianceModel->copyfrom($characterData->alliance); + $allianceModel->save(); + } + + if( isset($characterData->character) ){ + /** + * @var Model\CharacterModel $characterModel + */ + $characterModel = Model\BasicModel::getNew('CharacterModel'); + $characterModel->getById($characterData->character['id'], 0); + $characterModel->copyfrom($characterData->character); + $characterModel->corporationId = $corporationModel; + $characterModel->allianceId = $allianceModel; + $characterModel = $characterModel->save(); + } + + return $characterModel; + } + + /** + * get CREST server status (online/offline) + * @return \stdClass object + */ + public function getCrestServerStatus(){ + $endpoints = $this->getEndpoints(); + + // set default status e.g. Endpoints don´t work + $data = (object) []; + $data->crestOffline = true; + $data->serverName = 'EVE ONLINE'; + $data->serviceStatus = [ + 'eve' => 'offline', + 'server' => 'offline', + ]; + $data->userCounts = [ + 'eve' => 0 + ]; + + $endpoint = $this->walkEndpoint($endpoints, '', ['serverName']); + if( !empty($endpoint) ){ + $data->crestOffline = false; + $data->serverName = (string) $endpoint; + } + $endpoint = $this->walkEndpoint($endpoints, '', ['serviceStatus']); + if( !empty($endpoint) ){ + $data->crestOffline = false; + $data->serviceStatus = (new Mapper\CrestServiceStatus($endpoint))->getData(); + } + + $endpoint = $this->walkEndpoint($endpoints, '', ['userCounts']); + if( !empty($endpoint) ){ + $data->crestOffline = false; + $data->userCounts = (new Mapper\CrestUserCounts($endpoint))->getData(); + } + return $data; + } + + /** + * check response "Header" data for errors + * @param $headers + * @param string $requestUrl + * @param string $contentType + */ + protected function checkResponseHeaders($headers, $requestUrl = '', $contentType = ''){ + $headers = (array)$headers; + if( preg_grep('/^X-Deprecated/i', $headers) ){ + self::getCrestLogger()->write(sprintf(self::ERROR_RESOURCE_DEPRECATED, $requestUrl, $contentType)); + } + } + + /** + * get "Authorization:" Header data + * -> This header is required for any Auth-required endpoints! + * @return string + */ + protected function getAuthorizationHeader(){ + return base64_encode( + Controller\Controller::getEnvironmentData('SSO_CCP_CLIENT_ID') . ':' + . Controller\Controller::getEnvironmentData('SSO_CCP_SECRET_KEY') + ); + } + + /** + * get CCP CREST url from configuration file + * -> throw error if url is broken/missing + * @return string + */ + static function getCrestEndpoint(){ + $url = ''; + if( \Audit::instance()->url(self::getEnvironmentData('CCP_CREST_URL')) ){ + $url = self::getEnvironmentData('CCP_CREST_URL'); + }else{ + $error = sprintf(self::ERROR_CCP_CREST_URL, __METHOD__); + self::getCrestLogger()->write($error); + \Base::instance()->error(502, $error); + } + + return $url; + } + + /** + * get CCP SSO url from configuration file + * -> throw error if url is broken/missing + * @return string + */ + static function getSsoUrlRoot(){ + $url = ''; + if( \Audit::instance()->url(self::getEnvironmentData('SSO_CCP_URL')) ){ + $url = self::getEnvironmentData('SSO_CCP_URL'); + }else{ + $error = sprintf(self::ERROR_CCP_SSO_URL, __METHOD__); + self::getCrestLogger()->write($error); + \Base::instance()->error(502, $error); + } + + return $url; + } + + static function getAuthorizationEndpoint(){ + return self::getSsoUrlRoot() . '/oauth/authorize'; + } + + static function getVerifyAuthorizationCodeEndpoint(){ + return self::getSsoUrlRoot() . '/oauth/token'; + } + + static function getVerifyUserEndpoint(){ + return self::getSsoUrlRoot() . '/oauth/verify'; + } + + /** + * get logger for CREST logging + * @return \Log + */ + static function getCrestLogger(){ + return parent::getLogger('crest'); + } +} \ No newline at end of file diff --git a/app/main/controller/ccp/xml.php b/app/main/controller/ccp/xml.php new file mode 100644 index 00000000..5fcce469 --- /dev/null +++ b/app/main/controller/ccp/xml.php @@ -0,0 +1,74 @@ + 4, + 'user_agent' => $this->getUserAgent(), + 'follow_location' => false // otherwise CURLOPT_FOLLOWLOCATION will fail + ]; + + return $requestOptions; + } + + /** + * request character data from CCP API + * @param int $characterId + * @return array + */ + public function getPublicCharacterData($characterId){ + $characterData = []; + $apiPath = self::getEnvironmentData('CCP_XML') . '/eve/CharacterInfo.xml.aspx'; + + $baseOptions = $this->getRequestOptions(); + $requestOptions = [ + 'method' => 'GET', + 'content' => [ + 'characterID' => (int)$characterId + ] + + ]; + + $requestOptions = array_merge($baseOptions, $requestOptions); + $apiResponse = Lib\Web::instance()->request($apiPath, $requestOptions ); + if( + $apiResponse['body'] && + ($xml = simplexml_load_string($apiResponse['body'])) + ){ + + if( + isset($xml->result) && + is_object($rowApiData = $xml->result->children()) + ){ + foreach($rowApiData as $item){ + // map attributes to array + if(count($item->children()) == 0){ + $characterData[$item->getName()] = strval($item); + } + } + } + } + + $data = (new Mapper\CcpCharacterMapper($characterData))->getData(); + + return $data; + } + +} \ 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 1e372940..00000000 --- a/app/main/controller/ccpapicontroller.php +++ /dev/null @@ -1,184 +0,0 @@ - 8, - 'method' => 'POST', - 'user_agent' => $this->getUserAgent(), - 'follow_location' => false // otherwise CURLOPT_FOLLOWLOCATION will fail - ]; - - return $requestOptions; - } - - /** - * request character information from CCP API - * @param $keyID - * @param $vCode - * @return bool|\SimpleXMLElement - */ - public function requestCharacters($keyID, $vCode){ - - $apiPath = $this->getF3()->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( - is_object($rowApiData) && - $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, 0); - - $corporationModelTemp = null; - $allianceModelTemp = null; - - // check if corporation already exists - if($attributeData['corporationID'] > 0){ - $corporationModel->getById($attributeData['corporationID'], 0); - 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'], 0); - 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/ccpssocontroller.php b/app/main/controller/ccpssocontroller.php deleted file mode 100644 index d3d48ea2..00000000 --- a/app/main/controller/ccpssocontroller.php +++ /dev/null @@ -1,500 +0,0 @@ -request automatically caches responses by their response "Cache-Control" header! - */ - -namespace Controller; - -use Data\Mapper as Mapper; -use Model; - -class CcpSsoController extends Controller { - - const SESSION_KEY_ACCESS_TOKEN = 'SESSION.sso.access_token'; - const SESSION_KEY_REFRESH_TOKEN = 'SESSION.sso.refresh_token'; - - const ERROR_CCP_SSO_URL = 'Invalid "PATHFINDER.API.CCP_SSO" url. %s'; - const ERROR_CCP_CREST_URL = 'Invalid "PATHFINDER.API.CCP_CREST" url. %s'; - const ERROR_RESOURCE_DEPRECATED = 'Resource: %s has been marked deprecated. %s'; - const ERROR_ACCESS_TOKEN = 'Unable to get a valid "access_token. %s'; - const ERROR_VERIFY_CHARACTER = 'Unable to verify character data. %s'; - const ERROR_GET_ENDPOINTS = 'Unable to get endpoints data. $s'; - const ERROR_GET_ENDPOINT = 'Unable to get endpoint data. $s'; - const ERROR_FIND_ENDPOINT = 'Unable to find endpoint: %s'; - - /** - * "Scopes" that are used by pathfinder - * -> Enable scopes: https://developers.eveonline.com - * @var array - */ - private $requestScopes = [ - 'characterLocationRead', - 'characterNavigationWrite' - ]; - - /** - * timeout for API calls - * @var int - */ - private $apiTimeout = 3; - - /** - * redirect user to CCP SSO page and request authorization - * @param $f3 - */ - public function requestAuthorization($f3){ - // used for state check between request and callback - $state = bin2hex(mcrypt_create_iv(12, MCRYPT_DEV_URANDOM)); - $f3->set('SESSION.sso.state', $state); - - $urlParams = [ - 'response_type' => 'code', - 'redirect_uri' => Controller::getEnvironmentData('URL') . $f3->build('/sso/callbackAuthorization'), - 'client_id' => Controller::getEnvironmentData('SSO_CCP_CLIENT_ID'), - 'scope' => implode(' ', $this->requestScopes), - 'state' => $state - ]; - - $ssoAuthUrl = self::getAuthorizationEndpoint() . '?' . http_build_query($urlParams, '', '&', PHP_QUERY_RFC3986 ); - - $f3->status(302); - $f3->reroute($ssoAuthUrl); - } - - /** - * callback handler for CCP SSO user Auth - * -> see requestAuthorization() - * @param $f3 - */ - public function callbackAuthorization($f3){ - $getParams = (array)$f3->get('GET'); - - if($f3->exists('SESSION.sso.state')){ - // check response and validate 'state' - if( - isset($getParams['code']) && - isset($getParams['state']) && - !empty($getParams['code']) && - !empty($getParams['state']) && - $f3->get('SESSION.sso.state') === $getParams['state'] - ){ - - // clear 'state' for new next request - $f3->clear('SESSION.sso.state'); - - $accessToken = $this->getAccessToken($getParams['code']); - if($accessToken){ - $data = $this->verifyCharacterData($accessToken); - - $characterData = $this->getCharacterData($accessToken); - $characterModel = $this->updateCharacter($characterData); - - if( !is_null($characterModel) ){ - // everything OK -> login succeeded - } - } - } - } - - // on error -> route back to login form - $this->getF3()->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 bool|mixed - */ - private function getAccessToken($authCode = false){ - $accessToken = false; - - if( !empty($authCode) ){ - // Authentication Code is set -> request new Access Token ------------------------------------------------- - - // clear "old" token (if exist and still valid) - $this->getF3()->clear(self::SESSION_KEY_ACCESS_TOKEN); - - $accessToken = $this->verifyAuthorizationCode($authCode); - }elseif($this->getF3()->exists(self::SESSION_KEY_ACCESS_TOKEN)){ - // Access Token exists and not expired -------------------------------------------------------------------- - $accessToken = $this->getF3()->get(self::SESSION_KEY_ACCESS_TOKEN); - }elseif($this->getF3()->exists(self::SESSION_KEY_REFRESH_TOKEN)){ - // Refresh Token exists -> refresh Access Token ----------------------------------------------------------- - $accessToken = $this->refreshAccessToken($this->getF3()->get(self::SESSION_KEY_REFRESH_TOKEN)); - }else{ - // Unable to get Token -> trigger error ------------------------------------------------------------------- - $this->getLogger('error')->write(sprintf(self::ERROR_ACCESS_TOKEN, $authCode)); - } - - return $accessToken; - } - - /** - * verify authorization code, and get an "access_token" data - * @param $authCode - * @return bool|mixed - */ - private function verifyAuthorizationCode($authCode){ - $requestParams = [ - 'grant_type' => 'authorization_code', - 'code' => $authCode - ]; - - return $this->requestAccessToken($requestParams); - } - - /** - * get new "access_token" by an existing "refresh_token" - * -> if "access_token" is expired, this function gets a fresh one - * @param $refreshToken - * @return bool|mixed - */ - private function refreshAccessToken($refreshToken){ - $requestParams = [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $refreshToken - ]; - - return $this->requestAccessToken($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 $requestParams - * @return bool|mixed - */ - private function requestAccessToken($requestParams){ - $verifyAuthCodeUrl = self::getVerifyAuthorizationCodeEndpoint(); - $verifyAuthCodeUrlParts = parse_url($verifyAuthCodeUrl); - $accessToken = false; - - if($verifyAuthCodeUrlParts){ - $contentType = 'application/x-www-form-urlencoded'; - $requestOptions = [ - 'timeout' => $this->apiTimeout, - 'method' => 'POST', - 'user_agent' => $this->getUserAgent(), - 'header' => [ - 'Authorization: Basic ' . $this->getAuthorizationHeader(), - 'Content-Type: ' . $contentType, - 'Host: ' . $verifyAuthCodeUrlParts['host'] - ] - ]; - - // content (parameters to send with) - $requestOptions['content'] = http_build_query($requestParams); - - $apiResponse = \Web::instance()->request($verifyAuthCodeUrl, $requestOptions); - - if($apiResponse['body']){ - $authCodeRequestData = json_decode($apiResponse['body']); - - if(property_exists($authCodeRequestData, 'refresh_token')){ - // this token is used to refresh/get a new access_token when expires - $this->getF3()->set(self::SESSION_KEY_REFRESH_TOKEN, $authCodeRequestData->refresh_token); - } - - if(property_exists($authCodeRequestData, 'access_token')){ - // this token is required for endpoints that require Auth - $accessToken = $this->getF3()->set(self::SESSION_KEY_ACCESS_TOKEN, $authCodeRequestData->access_token); - } - }else{ - $this->getLogger('error')->write(sprintf(self::ERROR_ACCESS_TOKEN, print_r($requestParams, true))); - } - }else{ - $this->getLogger('error')->write(sprintf(self::ERROR_CCP_SSO_URL, __METHOD__)); - } - - return $accessToken; - } - - - - /** - * verify character data by "access_token" - * -> get some basic information (like character id) - * -> if more character information is required, use CREST endpoints request instead - * @param $accessToken - * @return bool|mixed - */ - private function verifyCharacterData($accessToken){ - $verifyUserUrl = self::getVerifyUserEndpoint(); - $verifyUrlParts = parse_url($verifyUserUrl); - $characterData = false; - - if($verifyUrlParts){ - $requestOptions = [ - 'timeout' => $this->apiTimeout, - 'method' => 'GET', - 'user_agent' => $this->getUserAgent(), - 'header' => [ - 'Authorization: Bearer ' . $accessToken, - 'Host: ' . $verifyUrlParts['host'] - ] - ]; - - $apiResponse = \Web::instance()->request($verifyUserUrl, $requestOptions); - - if($apiResponse['body']){ - $characterData = json_decode($apiResponse['body']); - }else{ - $this->getLogger('error')->write(sprintf(self::ERROR_VERIFY_CHARACTER, __METHOD__)); - } - }else{ - $this->getLogger('error')->write(sprintf(self::ERROR_CCP_SSO_URL, __METHOD__)); - } - - return $characterData; - } - - /** - * get all available Endpoints - * @param $accessToken - * @return bool|mixed - */ - private function getEndpoints($accessToken){ - $crestUrl = self::getCrestEndpoint(); - $endpointsData = false; - $crestUrlParts = parse_url($crestUrl); - - if($crestUrlParts){ - // represents API version - $contentType = 'application/vnd.ccp.eve.Api-v3+json'; - $requestOptions = [ - 'timeout' => $this->apiTimeout, - 'method' => 'GET', - 'user_agent' => $this->getUserAgent(), - 'header' => [ - 'Authorization: Bearer ' . $accessToken, - 'Accept: ' . $contentType, - 'Host: ' . $crestUrlParts['host'] - ] - ]; - - $apiResponse = \Web::instance()->request($crestUrl, $requestOptions); - - if($apiResponse['headers']){ - // check headers for error - $this->checkResponseHeaders($apiResponse['headers'], $crestUrl, $contentType); - - if($apiResponse['body']){ - $endpointsData = json_decode($apiResponse['body'], true); - }else{ - $this->getLogger('error')->write(sprintf(self::ERROR_GET_ENDPOINTS, __METHOD__)); - } - } - }else{ - $this->getLogger('error')->write(sprintf(self::ERROR_CCP_CREST_URL, __METHOD__)); - } - - return $endpointsData; - } - - private function walkEndpoint($accessToken, $endpoint, $path = []){ - $targetEndpoint = null; - - - if( !empty($path) ){ - $newNode = array_shift($path); - - if(isset($endpoint[$newNode])){ - $currentEndpoint = $endpoint[$newNode]; - if(isset($currentEndpoint['href'])){ - $newEndpoint = $this->getEndpoint($accessToken, $currentEndpoint['href']); - $targetEndpoint = $this->walkEndpoint($accessToken, $newEndpoint, $path); - - }else{ - // TODO leaf - $targetEndpoint = ' target:) '; - } - }else{ - // endpoint not found - $this->getLogger('error')->write(sprintf(self::ERROR_FIND_ENDPOINT, $newNode)); - } - }else{ - $targetEndpoint = $endpoint; - } - - - - return $targetEndpoint; - } - - - /** - * get a specific endpoint by its $resourceUrl - * @param $accessToken - * @param $resourceUrl - * @return mixed|null - */ - private function getEndpoint($accessToken, $resourceUrl){ - $resourceUrlParts = parse_url($resourceUrl); - $endpoint = null; - - if($resourceUrlParts){ - $requestOptions = [ - 'timeout' => $this->apiTimeout, - 'method' => 'GET', - 'user_agent' => $this->getUserAgent(), - 'header' => [ - 'Authorization: Bearer ' . $accessToken, - 'Host: login.eveonline.com', - 'Host: ' . $resourceUrlParts['host'] - ] - ]; - - $apiResponse = \Web::instance()->request($resourceUrl, $requestOptions); - - if($apiResponse['headers']){ - // check headers for error - $this->checkResponseHeaders($apiResponse['headers'], $requestOptions); - - if($apiResponse['body']){ - $endpoint = json_decode($apiResponse['body'], true); - }else{ - $this->getLogger('error')->write(sprintf(self::ERROR_GET_ENDPOINT, __METHOD__)); - } - } - }else{ - $this->getLogger('error')->write(sprintf(self::ERROR_CCP_CREST_URL, __METHOD__)); - } - - return $endpoint; - } - - /** - * get character data - * @param $accessToken - * @return array - */ - private function getCharacterData($accessToken){ - $endpoints = $this->getEndpoints($accessToken); - $characterData = []; - - $endpoint = $this->walkEndpoint($accessToken, $endpoints, [ - 'decode', - 'character' - ]); - - if( !empty($endpoint) ){ - $characterData['character'] = (new Mapper\CrestCharacter($endpoint))->getData(); - - if(isset($endpoint['corporation'])){ - $characterData['corporation'] = (new Mapper\CrestCorporation($endpoint['corporation']))->getData(); - } - } - - return $characterData; - } - - /* - private function getCharacterLocation($accessToken){ - $endpoints = $this->getEndpoints($accessToken); - $endpoint = $this->walkEndpoint($accessToken, $endpoints, [ - 'decode', - 'character', - 'location' - ]); - - var_dump($endpoint); - - die(' END getCharacterLocation() '); - - - $characterData = []; - return $characterData; - } */ - - /** - * update character - * @param $characterData - * @return \Model\CharacterModel - * @throws \Exception - */ - private function updateCharacter($characterData){ - - $characterModel = null; - $corporationModel = null; - $allianceModel = null; - - if( !empty($characterData['corporation']) ){ - $corporationModel = Model\BasicModel::getNew('CorporationModel'); - $corporationModel->getById($characterData['corporation']['id'], 0); - $corporationModel->copyfrom($characterData['corporation']); - $corporationModel->save(); - } - - if( !empty($characterData['alliance']) ){ - $allianceModel = Model\BasicModel::getNew('AllianceModel'); - $allianceModel->getById($characterData['alliance']['id'], 0); - $allianceModel->copyfrom($characterData['alliance']); - $allianceModel->save(); - } - - if( !empty($characterData['character']) ){ - $characterModel = Model\BasicModel::getNew('CharacterModel'); - $characterModel->getById($characterData['character']['id'], 0); - $characterModel->copyfrom($characterData['character']); - $characterModel->corporationId = $corporationModel; - $characterModel->allianceId = $allianceModel; - $characterModel->save(); - } - - return $characterModel; - } - - /** - * check response "Header" data for errors - * @param $headers - * @param string $requestUrl - * @param string $contentType - */ - private function checkResponseHeaders($headers, $requestUrl = '', $contentType = ''){ - $headers = (array)$headers; - if(preg_grep ('/^X-Deprecated/i', $headers)){ - $this->getLogger('error')->write(sprintf(self::ERROR_RESOURCE_DEPRECATED, $requestUrl, $contentType)); - } - } - - /** - * get "Authorization:" Header data - * -> This header is required for any Auth-required endpoints! - * @return string - */ - private function getAuthorizationHeader(){ - return base64_encode( - Controller::getEnvironmentData('SSO_CCP_CLIENT_ID') . ':' - . Controller::getEnvironmentData('SSO_CCP_SECRET_KEY') - ); - } - - - static function getAuthorizationEndpoint(){ - return \Base::instance()->get('PATHFINDER.API.CCP_SSO') . '/oauth/authorize'; - } - - static function getVerifyAuthorizationCodeEndpoint(){ - return \Base::instance()->get('PATHFINDER.API.CCP_SSO') . '/oauth/token'; - } - - static function getVerifyUserEndpoint(){ - return \Base::instance()->get('PATHFINDER.API.CCP_SSO') . '/oauth/verify'; - } - - static function getCrestEndpoint(){ - return \Base::instance()->get('PATHFINDER.API.CCP_CREST'); - } -} \ No newline at end of file diff --git a/app/main/controller/controller.php b/app/main/controller/controller.php index 10c11dd4..b4b415a5 100644 --- a/app/main/controller/controller.php +++ b/app/main/controller/controller.php @@ -7,50 +7,69 @@ */ namespace Controller; +use Controller\Api as Api; +use Controller\Ccp\Sso as Sso; +use lib\Config; use Model; use DB; class Controller { + // cookie specific keys (names) + const COOKIE_NAME_STATE = 'cookie'; + const COOKIE_PREFIX_CHARACTER = 'char'; + + const ERROR_SESSION_SUSPECT = 'Suspect id: [%30s], ip: [%40s], new ip: [%40s], User-Agent: %s '; + /** + * @var \Base + */ protected $f3; - private $template; /** - * @param mixed $template + * @var string template for render */ - public function setTemplate($template){ + protected $template; + + /** + * @param string $template + */ + protected function setTemplate($template){ $this->template = $template; } /** - * @return mixed + * @return string */ - public function getTemplate(){ + protected function getTemplate(){ return $this->template; } /** - * set global f3 instance - * @param null $f3 - * @return null|static + * set $f3 base object + * @param \Base $f3 */ - protected function getF3($f3 = null){ - if(is_object($f3)){ - $this->f3 = $f3; - }else{ - $this->f3 = \Base::instance(); - } + protected function setF3(\Base $f3){ + $this->f3 = $f3; + } + /** + * get $f3 base object + * @return \Base + */ + protected function getF3(){ + if( !($this->f3 instanceof \Base) ){ + $this->setF3( \Base::instance() ); + } return $this->f3; } /** * event handler for all "views" * some global template variables are set in here - * @param $f3 + * @param \Base $f3 */ - function beforeroute($f3) { - $this->getF3($f3); + function beforeroute(\Base $f3) { + $this->setF3($f3); // initiate DB connection DB\Database::instance('PF'); @@ -73,8 +92,9 @@ class Controller { /** * event handler after routing * -> render view + * @param \Base $f3 */ - public function afterroute($f3){ + public function afterroute(\Base $f3){ if($this->getTemplate()){ // Ajax calls don´t need a page render.. // this happens on client side @@ -85,7 +105,7 @@ class Controller { /** * set change the DB connection * @param string $database - * @return mixed|void + * @return DB\SQL */ protected function getDB($database = 'PF'){ return DB\Database::instance()->getDB($database); @@ -95,51 +115,294 @@ class Controller { * init new Session handler */ protected function initSession(){ - // init DB Session (not file based) - if( $this->getDB('PF') instanceof \DB\SQL){ - new \DB\SQL\Session($this->getDB('PF')); + + // init DB based Session (not file based) + if( $this->getDB('PF') instanceof DB\SQL){ + // init session with custom "onsuspect()" handler + new DB\SQL\Session($this->getDB('PF'), 'sessions', true, function($session, $sid){ + $f3 = $this->getF3(); + if( ($ip = $session->ip() )!= $f3->get('IP') ){ + // IP address changed -> not critical + $sessionSuspectLogFile = 'PATHFINDER.LOGFILES.SESSION_SUSPECT'; + if( !$f3->devoid($sessionSuspectLogFile) ){ + $this->getLogger( + $f3->get($sessionSuspectLogFile) + )->write( sprintf( + self::ERROR_SESSION_SUSPECT, + $sid, + $session->ip(), + $f3->get('IP'), + $f3->get('AGENT') + )); + } + // no more error handling here + return true; + }elseif($session->agent() != $f3->get('AGENT') ){ + // The default behaviour destroys the suspicious session. + return false; + } + + return true; + }); } } /** - * get current user model - * @param int $ttl - * @return bool|null + * get cookies "state" information + * -> whether user accepts cookies + * @return bool + */ + protected function getCookieState(){ + 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){ + $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 Model\CharacterModel $character + */ + protected function setLoginCookie(Model\CharacterModel $character){ + + if( $this->getCookieState() ){ + $expireSeconds = (int) $this->getF3()->get('PATHFINDER.LOGIN.COOKIE_EXPIRE'); + $expireSeconds *= 24 * 60 * 60; + + $timezone = new \DateTimeZone( $this->getF3()->get('TZ') ); + $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(mcrypt_create_iv(12, MCRYPT_DEV_URANDOM)); + + // generate unique "validator" (strong encryption) + // -> plaintext set to user (cookie), hashed version of this in DB + $size = mcrypt_get_iv_size(MCRYPT_CAST_256, MCRYPT_MODE_CFB); + $validator = bin2hex(mcrypt_create_iv($size, MCRYPT_DEV_URANDOM)); + + // 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 + * @return array * @throws \Exception */ - protected function _getUser($ttl = 5){ - $user = false; + protected function getCookieCharacters($cookieData = []){ + $characters = []; - if( $this->f3->exists('SESSION.user.id') ){ - $userId = (int)$this->f3->get('SESSION.user.id'); + if( + $this->getCookieState() && + !empty($cookieData) + ){ + /** + * @var $characterAuth Model\CharacterAuthenticationModel + */ + $characterAuth = Model\BasicModel::getNew('CharacterAuthenticationModel'); - if($userId > 0){ - $userModel = Model\BasicModel::getNew('UserModel', $ttl); - $userModel->getById($userId, $ttl); + $timezone = new \DateTimeZone( $this->getF3()->get('TZ') ); + $currentTime = new \DateTime('now', $timezone); - if( !$userModel->dry() ){ - $user = $userModel; + 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 expire data + // 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 CREST + // e.g. Corp has changed, this also ensures valid "access_token" + /** + * @var $character Model\CharacterModel + */ + $updateStatus = $characterAuth->characterId->updateFromCrest(); + + 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->characterId->_id); + + // 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. CREST is down,..) + if( + $character->hasUserCharacter() && + $character->isAuthorized() + ){ + $characters[$name] = $character; + } + } + }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 $user; + return $characters; } /** - * log the current user out - * @param $f3 + * checks whether a user is currently logged in + * @param \Base $f3 + * @return bool */ - public function logOut($f3){ + protected function checkLogTimer($f3){ + $loginCheck = false; - // destroy session + if($f3->get(Api\User::SESSION_KEY_CHARACTER_TIME) > 0){ + // check logIn time + $logInTime = new \DateTime(); + $logInTime->setTimestamp( $f3->get(Api\User::SESSION_KEY_CHARACTER_TIME) ); + $now = new \DateTime(); + + $timeDiff = $now->diff($logInTime); + + $minutes = $timeDiff->days * 60 * 24 * 60; + $minutes += $timeDiff->h * 60; + $minutes += $timeDiff->i; + + if($minutes <= $f3->get('PATHFINDER.TIMER.LOGGED')){ + $loginCheck = true; + } + } + + return $loginCheck; + } + + /** + * get current character model + * @param int $ttl + * @return Model\CharacterModel|null + * @throws \Exception + */ + public function getCharacter($ttl = 0){ + $character = null; + + if( $this->getF3()->exists(Api\User::SESSION_KEY_CHARACTER_ID) ){ + $characterId = (int)$this->getF3()->get(Api\User::SESSION_KEY_CHARACTER_ID); + if($characterId){ + /** + * @var $characterModel Model\CharacterModel + */ + $characterModel = Model\BasicModel::getNew('CharacterModel'); + $characterModel->getById($characterId, $ttl); + + if( + !$characterModel->dry() && + $characterModel->hasUserCharacter() + ){ + $character = &$characterModel; + } + } + } + + return $character; + } + + /** + * log out current character + * @param \Base $f3 + */ + public function logout(\Base $f3){ + $params = (array)$f3->get('POST'); + + // ---------------------------------------------------------- + // delete server side cookie validation data + // for the current character as well + if( + $params['clearCookies'] === '1' && + ( $activeCharacter = $this->getCharacter()) + ){ + $activeCharacter->logout(); + } + + // destroy session login data ------------------------------- $f3->clear('SESSION'); - if( !$f3->get('AJAX') ){ - // redirect to landing page - $f3->reroute('@login'); - }else{ - $params = $f3->get('POST'); + if( $f3->get('AJAX') ){ $return = (object) []; if( isset($params['reroute']) && @@ -148,272 +411,59 @@ class Controller { $return->reroute = rtrim(self::getEnvironmentData('URL'), '/') . $f3->alias('login'); }else{ // no reroute -> errors can be shown - $return->error[] = $this->getUserLoggedOffError(); + $return->error[] = $this->getLogoutError(); } echo json_encode($return); - die(); - } - } - - /** - * 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; - } - - /** - * check weather the page is IGB trusted or not - * @return mixed - */ - static function isIGBTrusted(){ - - $igbHeaderData = self::getIGBHeaderData(); - - return $igbHeaderData->trusted; - } - - /** - * get all eve IGB specific header data - * @return object - */ - static function getIGBHeaderData(){ - $data = (object) []; - $data->trusted = false; - $data->values = []; - $headerData = self::getRequestHeaders(); - - foreach($headerData as $key => $value){ - $key = strtolower($key); - $key = str_replace('eve_', 'eve-', $key); - - - if (strpos($key, 'eve-') === 0) { - $key = str_replace('eve-', '', $key); - - if ( - $key === 'trusted' && - $value === 'Yes' - ) { - $data->trusted = true; - } - - $data->values[$key] = $value; - } - } - - return $data; - } - - /** - * Helper function to return all headers because - * getallheaders() is not available under nginx - * - * @return array (string $key -> string $value) - */ - static function getRequestHeaders(){ - $headers = []; - - $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){ - if(substr($name, 0, 5) == 'HTTP_'){ - $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; - } - } + // redirect to landing page + $f3->reroute('@login'); } - - return $headers; } /** - * get some server information - * @param int $ttl cache time (default: 1h) - * @return object + * get EVE server status from CREST + * @param \Base $f3 */ - static function getServerData($ttl = 3600){ - $f3 = \Base::instance(); - $cacheKey = 'PF_SERVER_INFO'; + public function getEveServerStatus(\Base $f3){ + $return = (object) []; + $return->error = []; + // server status can be cached for some seconds + $cacheKey = 'eve_server_status'; if( !$f3->exists($cacheKey) ){ - $serverData = (object) []; - $serverData->type = 'unknown'; - $serverData->version = 'unknown'; - $serverData->requiredVersion = 'unknown'; - $serverData->phpInterfaceType = php_sapi_name(); + $sso = new Sso(); + $return->status = $sso->getCrestServerStatus(); - 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]; - } - } + if( !$return->status->crestOffline ){ + $f3->set($cacheKey, $return, 60); } - - // cache data for one day - $f3->set($cacheKey, $serverData, $ttl); + }else{ + // get from cache + $return = $f3->get($cacheKey); } - return $f3->get($cacheKey); + echo json_encode($return); } - /** - * 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; - } /** * get error object is a user is not found/logged of - * @return object + * @return \stdClass */ - protected function getUserLoggedOffError(){ + protected function getLogoutError(){ $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 strtolower( str_replace($illegalCharacters, '', $key) ); - } - - /** - * get environment specific configuration data - * @param $key - * @return mixed|null - */ - static function getEnvironmentData($key){ - $f3 = \Base::instance(); - $environment = self::getEnvironment(); - $environmentKey = 'ENVIRONMENT[' . $environment . '][' . $key . ']'; - $data = null; - - if( $f3->exists($environmentKey) ){ - $data = $f3->get($environmentKey); - } - return $data; - } - - /** - * get current server environment status - * -> "DEVELOP" or "PRODUCTION" - * @return mixed - */ - static function getEnvironment(){ - $f3 = \Base::instance(); - return $f3->get('ENVIRONMENT.SERVER'); - } - - /** - * check if current server is "PRODUCTION" - * @return bool - */ - static function isProduction(){ - return self::getEnvironment() == 'PRODUCTION'; - } - - /** - * get required MySQL variable value - * @param $key - * @return mixed|null - */ - static function getRequiredMySqlVariables($key){ - $f3 = \Base::instance(); - $requiredMySqlVarKey = 'REQUIREMENTS[MYSQL][VARS][' . $key . ']'; - $data = null; - - if( $f3->exists($requiredMySqlVarKey) ){ - $data = $f3->get($requiredMySqlVarKey); - } - return $data; - } - /** * get a program URL by alias * -> if no $alias given -> get "default" route (index.php) * @param null $alias - * @return bool + * @return bool|string */ protected function getRouteUrl($alias = null){ $url = false; @@ -452,9 +502,9 @@ class Controller { * onError() callback function * -> on AJAX request -> return JSON with error information * -> on HTTP request -> render error page - * @param $f3 + * @param \Base $f3 */ - public function showError($f3){ + public function showError(\Base $f3){ // set HTTP status $errorCode = $f3->get('ERROR.code'); if(!empty($errorCode)){ @@ -510,9 +560,224 @@ class Controller { /** * Callback for framework "unload" * check -> config.ini + * @param \Base $f3 + * @return bool */ - public function unload($f3){ + public function unload(\Base $f3){ return true; } + /** + * 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; + } + + /** + * check weather the page is IGB trusted or not + * @return boolean + */ + static function isIGBTrusted(){ + $igbHeaderData = self::getIGBHeaderData(); + return $igbHeaderData->trusted; + } + + /** + * get all eve IGB specific header data + * @return \stdClass + */ + static function getIGBHeaderData(){ + $data = (object) []; + $data->trusted = false; + $data->values = []; + $headerData = self::getRequestHeaders(); + + foreach($headerData as $key => $value){ + $key = strtolower($key); + $key = str_replace('eve_', 'eve-', $key); + + + if (strpos($key, 'eve-') === 0) { + $key = str_replace('eve-', '', $key); + + if ( + $key === 'trusted' && + $value === 'Yes' + ) { + $data->trusted = true; + } + + $data->values[$key] = $value; + } + } + + return $data; + } + + /** + * Helper function to return all headers because + * getallheaders() is not available under nginx + * @return array (string $key -> string $value) + */ + static function getRequestHeaders(){ + $headers = []; + + $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){ + if(substr($name, 0, 5) == 'HTTP_'){ + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $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); + } + + /** + * 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; + } + + /** + * 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 string $loggerType + * @return \Log + */ + static function getLogger($loggerType){ + return LogController::getLogger($loggerType); + } + + /** + * removes illegal characters from a Hive-key that are not allowed + * @param $key + * @return string + */ + static function formatHiveKey($key){ + $illegalCharacters = ['-', ' ']; + return strtolower( str_replace($illegalCharacters, '', $key) ); + } + + /** + * get environment specific configuration data + * @param string $key + * @return string|null + */ + static function getEnvironmentData($key){ + return Config::getEnvironmentData($key); + } + + /** + * get required MySQL variable value + * @param $key + * @return string|null + */ + static function getRequiredMySqlVariables($key){ + $f3 = \Base::instance(); + $requiredMySqlVarKey = 'REQUIREMENTS[MYSQL][VARS][' . $key . ']'; + $data = null; + + if( $f3->exists($requiredMySqlVarKey) ){ + $data = $f3->get($requiredMySqlVarKey); + } + return $data; + } + } \ No newline at end of file diff --git a/app/main/controller/logcontroller.php b/app/main/controller/logcontroller.php index 59589033..daade6cc 100644 --- a/app/main/controller/logcontroller.php +++ b/app/main/controller/logcontroller.php @@ -31,7 +31,6 @@ class LogController extends Controller { $f3->set($hiveKey, new \Log($logFile)); } - return $f3->get($hiveKey); } diff --git a/app/main/controller/mapcontroller.php b/app/main/controller/mapcontroller.php index f86e9676..e05bbd9f 100644 --- a/app/main/controller/mapcontroller.php +++ b/app/main/controller/mapcontroller.php @@ -8,8 +8,11 @@ namespace Controller; -class MapController extends \Controller\AccessController { +class MapController extends AccessController { + /** + * @param \Base $f3 + */ public function init($f3) { // page title diff --git a/app/main/controller/setup.php b/app/main/controller/setup.php index 2b254d33..a0fbe519 100644 --- a/app/main/controller/setup.php +++ b/app/main/controller/setup.php @@ -11,11 +11,46 @@ namespace Controller; use DB; use DB\SQL; use DB\SQL\MySQL as MySQL; - +use lib\Config; use Model; class Setup extends Controller { + /** + * required environment variables + * @var array + */ + protected $environmentVars = [ + 'TYPE', + 'BASE', + 'URL', + 'DEBUG', + 'DB_DNS', + 'DB_NAME', + 'DB_USER', + 'DB_PASS', + 'DB_CCP_DNS', + 'DB_CCP_NAME', + 'DB_CCP_USER', + 'DB_CCP_PASS', + 'CCP_CREST_URL', + 'SSO_CCP_URL', + 'SSO_CCP_CLIENT_ID', + 'SSO_CCP_SECRET_KEY', + 'CCP_XML', + 'SMTP_HOST', + 'SMTP_PORT', + 'SMTP_SCHEME', + 'SMTP_USER', + 'SMTP_PASS', + 'SMTP_FROM', + 'SMTP_ERROR' + ]; + + /** + * required database setup + * @var array + */ protected $databases = [ 'PF' => [ 'info' => [], @@ -30,18 +65,17 @@ class Setup extends Controller { 'Model\SystemStatusModel', 'Model\SystemNeighbourModel', 'Model\WormholeModel', - 'Model\RegistrationKeyModel', 'Model\CharacterStatusModel', 'Model\ConnectionScopeModel', - 'Model\UserMapModel', + 'Model\CharacterMapModel', 'Model\AllianceMapModel', 'Model\CorporationMapModel', - 'Model\UserApiModel', 'Model\UserCharacterModel', 'Model\CharacterModel', + 'Model\CharacterAuthenticationModel', 'Model\CharacterLogModel', 'Model\SystemModel', @@ -76,9 +110,9 @@ class Setup extends Controller { /** * event handler for all "views" * some global template variables are set in here - * @param $f3 + * @param \Base $f3 */ - function beforeroute($f3) { + function beforeroute(\Base $f3) { // page title $f3->set('pageTitle', 'Setup'); @@ -92,7 +126,7 @@ class Setup extends Controller { $f3->set('pathJs', 'public/js/' . $f3->get('PATHFINDER.VERSION') ); } - public function afterroute($f3) { + public function afterroute(\Base $f3) { // js view (file) $f3->set('jsView', 'setup'); @@ -103,9 +137,9 @@ class Setup extends Controller { /** * main setup route handler * works as dispatcher for setup functions - * @param $f3 + * @param \Base $f3 */ - public function init($f3){ + public function init(\Base $f3){ $params = $f3->get('GET'); // enables automatic column fix @@ -129,22 +163,69 @@ class Setup extends Controller { $fixColumns = true; } - // set server information for page render + // set template data ---------------------------------------------------------------- + // set environment information + $f3->set('environmentInformation', $this->getEnvironmentInformation($f3)); + + // set server information $f3->set('serverInformation', $this->getServerInformation($f3)); - // set requirement check information for page render + // set requirement check information $f3->set('checkRequirements', $this->checkRequirements($f3)); - // set database connection information for page render + // set database connection information $f3->set('checkDatabase', $this->checkDatabase($f3, $fixColumns)); } /** - * get server information - * @param $f3 + * set environment information + * @param \Base $f3 * @return array */ - protected function getServerInformation($f3){ + protected function getEnvironmentInformation(\Base $f3){ + $environmentData = []; + // exclude some sensitive data (e.g. database, passwords) + $excludeVars = ['DB_DNS', 'DB_NAME', 'DB_USER', + 'DB_PASS', 'DB_CCP_DNS', 'DB_CCP_NAME', + 'DB_CCP_USER', 'DB_CCP_PASS' + ]; + + // obscure some values + $obscureVars = ['SSO_CCP_CLIENT_ID', 'SSO_CCP_SECRET_KEY', 'SMTP_PASS']; + + foreach($this->environmentVars as $var){ + if( !in_array($var, $excludeVars) ){ + $value = Config::getEnvironmentData($var); + $check = true; + + if(is_null($value)){ + // variable missing + $check = false; + $value = '[missing]'; + }elseif( in_array($var, $obscureVars)){ + $length = strlen($value); + $hideChars = ($length < 10) ? $length : 10; + $value = substr_replace($value, str_repeat('.', 3), -$hideChars); + $value .= ' [' . $length . ']'; + } + + $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){ $serverInfo = [ 'time' => [ 'label' => 'Time', @@ -178,10 +259,10 @@ class Setup extends Controller { /** * check all required backend requirements * (Fat Free Framework) - * @param $f3 + * @param \Base $f3 * @return array */ - protected function checkRequirements($f3){ + protected function checkRequirements(\Base $f3){ // server type ------------------------------------------------------------------ @@ -288,11 +369,11 @@ class Setup extends Controller { /** * get database connection information - * @param $f3 + * @param \Base $f3 * @param bool|false $exec * @return array */ - protected function checkDatabase($f3, $exec = false){ + protected function checkDatabase(\Base $f3, $exec = false){ foreach($this->databases as $dbKey => $dbData){ @@ -413,6 +494,7 @@ class Setup extends Controller { $changedType = false; $changedUnique = false; $changedIndex = false; + $addConstraints = []; // set (new) column information ------------------------------------------------------- $requiredTables[$requiredTableName]['fieldConf'][$columnName]['exists'] = true; @@ -427,17 +509,22 @@ class Setup extends Controller { $constraint = $col->newConstraint($constraintData); $foreignKeyExists = $col->constraintExists($constraint); + + // constraint information -> show in template $requiredTables[$requiredTableName]['foreignKeys'][] = [ 'exists' => $foreignKeyExists, 'keyName' => $constraint->getConstraintName() ]; - $col->addConstraint($constraint); - - if(!$foreignKeyExists){ + 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; } } @@ -452,11 +539,21 @@ class Setup extends Controller { $tableStatusCheckCount++; } - // check if column unique changed ----------------------------------------------------- + // 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; @@ -466,15 +563,6 @@ class Setup extends Controller { $indexUnique =(bool)$fieldConf['unique']; } - // check if column index changed ------------------------------------------------------ - if($currentColIndex != $fieldConf['index']){ - $changedIndex = true; - $columnStatusCheck = false; - $tableStatusCheckCount++; - - $indexUpdate = true; - $indexKey = (bool) $fieldConf['index']; - } // build table with changed columns --------------------------------------------------- if(!$columnStatusCheck || !$foreignKeyStatusCheck){ @@ -495,6 +583,12 @@ class Setup extends Controller { $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( @@ -559,11 +653,11 @@ class Setup extends Controller { } /** check MySQL params - * @param $f3 + * @param \Base $f3 * @param $db * @return array */ - protected function checkDBConfig($f3, $db){ + protected function checkDBConfig(\Base $f3, $db){ // some db like "Maria DB" have some strange version strings.... $dbVersionString = $db->version(); diff --git a/app/main/cron/ccpsystemsupdate.php b/app/main/cron/ccpsystemsupdate.php index cf24b59e..29051583 100644 --- a/app/main/cron/ccpsystemsupdate.php +++ b/app/main/cron/ccpsystemsupdate.php @@ -9,6 +9,7 @@ namespace Cron; use Controller; use DB; +use lib\Config; class CcpSystemsUpdate { @@ -69,7 +70,7 @@ class CcpSystemsUpdate { /** * imports all relevant map stats from CCPs API * >> php index.php "/cron/importSystemData" - * @param $f3 + * @param \Base $f3 */ function importSystemData($f3){ @@ -83,7 +84,7 @@ class CcpSystemsUpdate { // get current jump Data ------------------------------------------------------- $time_start = microtime(true); - $apiPath = $f3->get('PATHFINDER.API.CCP_XML') . '/map/Jumps.xml.aspx'; + $apiPath = Config::getEnvironmentData('CCP_XML') . '/map/Jumps.xml.aspx'; $apiResponse = \Web::instance()->request($apiPath, $this->apiRequestOptions ); @@ -108,7 +109,7 @@ class CcpSystemsUpdate { // get current kill Data ------------------------------------------------------- $time_start = microtime(true); - $apiPath = $f3->get('PATHFINDER.API.CCP_XML') . '/map/Kills.xml.aspx'; + $apiPath = Config::getEnvironmentData('CCP_XML') . '/map/Kills.xml.aspx'; $apiResponse = \Web::instance()->request($apiPath, $this->apiRequestOptions ); $killData = []; diff --git a/app/main/cron/characterupdate.php b/app/main/cron/characterupdate.php index 4c0fe49e..a40b7068 100644 --- a/app/main/cron/characterupdate.php +++ b/app/main/cron/characterupdate.php @@ -17,15 +17,17 @@ class CharacterUpdate { /** * delete all character log data * >> php index.php "/cron/deleteLogData" - * @param $f3 + * @param \Base $f3 */ function deleteLogData($f3){ - DB\Database::instance()->getDB('PF'); + /** + * @var $characterLogModel Model\CharacterLogModel + */ $characterLogModel = Model\BasicModel::getNew('CharacterLogModel', 0); - // find "old" character logs + // find expired character logs $characterLogs = $characterLogModel->find([ 'TIMESTAMPDIFF(SECOND, updated, NOW() ) > :lifetime', ':lifetime' => (int)$f3->get('PATHFINDER.CACHE.CHARACTER_LOG') @@ -33,10 +35,35 @@ class CharacterUpdate { if(is_object($characterLogs)){ foreach($characterLogs as $characterLog){ - // delete log and all cached values $characterLog->erase(); } } } + /** + * delete expired character authentication data + * authentication data is used for cookie based login + * >> php index.php "/cron/deleteAuthenticationData" + * @param $f3 + */ + function deleteAuthenticationData($f3){ + DB\Database::instance()->getDB('PF'); + + /** + * @var $authenticationModel Model\CharacterAuthenticationModel + */ + $authenticationModel = Model\BasicModel::getNew('CharacterAuthenticationModel', 0); + + // find expired authentication data + $authentications = $authenticationModel->find([ + '(expires - NOW()) <= 0' + ]); + + if(is_object($authentications)){ + foreach($authentications as $authentication){ + $authentication->erase(); + } + } + } + } \ No newline at end of file diff --git a/app/main/cron/mapupdate.php b/app/main/cron/mapupdate.php index f92fd2d1..8104ee45 100644 --- a/app/main/cron/mapupdate.php +++ b/app/main/cron/mapupdate.php @@ -20,7 +20,7 @@ class MapUpdate { /** * deactivate all "private" maps whose lifetime is over * >> php index.php "/cron/deactivateMapData" - * @param $f3 + * @param \Base $f3 */ function deactivateMapData($f3){ @@ -46,7 +46,7 @@ class MapUpdate { /** * delete all deactivated maps * >> php index.php "/cron/deleteMapData" - * @param $f3 + * @param \Base $f3 */ function deleteMapData($f3){ diff --git a/app/main/data/mapper/abstractiterator.php b/app/main/data/mapper/abstractiterator.php index cd8892e9..4ce14274 100644 --- a/app/main/data/mapper/abstractiterator.php +++ b/app/main/data/mapper/abstractiterator.php @@ -47,43 +47,48 @@ class AbstractIterator extends \RecursiveArrayIterator { */ static function recursiveIterator($iterator){ + $keyWhitelist = array_keys(static::$map); + while($iterator->valid()){ - if(array_key_exists($iterator->key(), static::$map)){ + if( isset(static::$map[$iterator->key()]) ){ + $mapValue = static::$map[$iterator->key()]; + // check for mapping key if($iterator->hasChildren()){ // recursive call for child elements $iterator->offsetSet($iterator->key(), forward_static_call(array('self', __METHOD__), $iterator->getChildren())->getArrayCopy()); $iterator->next(); - }elseif(is_array(static::$map[$iterator->key()])){ + }elseif(is_array($mapValue)){ // a -> array mapping - $parentKey = array_keys(static::$map[$iterator->key()])[0]; - $entryKey = array_values(static::$map[$iterator->key()])[0]; + $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; } - - }elseif(is_object(static::$map[$iterator->key()])){ + $iterator->offsetUnset($iterator->key()); + }elseif(is_object($mapValue)){ // a -> a (format by function) - $formatFunction = static::$map[$iterator->key()]; + $formatFunction = $mapValue; $iterator->offsetSet($iterator->key(), call_user_func($formatFunction, $iterator)); // just value change no key change $iterator->next(); - }elseif(static::$map[$iterator->key()] !== $iterator->key()){ + }elseif($mapValue !== $iterator->key()){ // a -> b mapping (key changed) - $iterator->offsetSet(static::$map[$iterator->key()], $iterator->current()); + $iterator->offsetSet($mapValue, $iterator->current()); $iterator->offsetUnset($iterator->key()); + $keyWhitelist[] = $mapValue; }else{ // a -> a (no changes) $iterator->next(); @@ -91,13 +96,13 @@ class AbstractIterator extends \RecursiveArrayIterator { }elseif( static::$removeUnmapped && - !in_array($iterator->key(), static::$map) - + !in_array($iterator->key(), $keyWhitelist) ){ $iterator->offsetUnset($iterator->key()); }else{ $iterator->next(); } + } return $iterator; diff --git a/app/main/data/mapper/ccpcharactermapper.php b/app/main/data/mapper/ccpcharactermapper.php new file mode 100644 index 00000000..c3c59a53 --- /dev/null +++ b/app/main/data/mapper/ccpcharactermapper.php @@ -0,0 +1,37 @@ + ['character' => 'id'], + 'characterName' => ['character' => 'name'], + + 'race' => 'race', + + 'bloodlineID' => ['blood' => 'id'], + 'bloodline' => ['blood' => 'name'], + + 'ancestryID' => ['origin' => 'id'], + 'ancestry' => ['origin' => 'name'], + + 'corporationID' => ['corp' => 'id'], + 'corporation' => ['corp' => 'name'], + 'corporationDate' => ['corp' => 'date'], + + 'allianceID' => ['alli' => 'id'], + 'alliance' => ['alli' => 'name'], + 'allianceDate' => ['alli' => 'date'], + + 'securityStatus' => 'security' + ]; +} \ No newline at end of file diff --git a/app/main/data/mapper/ccpsystemsmapper.php b/app/main/data/mapper/ccpsystemsmapper.php index 1eac1157..dc2b859d 100644 --- a/app/main/data/mapper/ccpsystemsmapper.php +++ b/app/main/data/mapper/ccpsystemsmapper.php @@ -13,7 +13,6 @@ class CcpSystemsMapper extends AbstractIterator { protected static $map = [ 'system_id' => 'systemId', 'system_name' => 'name', - 'system_security' => 'trueSec', 'connstallation_id' => ['constellation' => 'id'], 'constallation_name' => ['constellation' => 'name'], 'region_id' => ['region' => 'id'], @@ -21,14 +20,19 @@ class CcpSystemsMapper extends AbstractIterator { ]; /** - * get formatted data + * map iterator * @return array */ public function getData(){ - // format functions - self::$map['effect'] = function($iterator){ + // "system trueSec" mapping ------------------------------------------- + self::$map['trueSec'] = function($iterator){ + $trueSec = round((float)$iterator['system_security'], 1); + return $trueSec; + }; + // "system effect" mapping -------------------------------------------- + self::$map['effect'] = function($iterator){ $effect = $iterator['effect']; switch($iterator['effect']){ @@ -55,8 +59,9 @@ class CcpSystemsMapper extends AbstractIterator { return $effect; }; - // format functions + // "system security" mapping ------------------------------------------ self::$map['security'] = function($iterator){ + $security = ''; if( $iterator['security'] == 7 || @@ -64,7 +69,7 @@ class CcpSystemsMapper extends AbstractIterator { $iterator['security'] == 9 ){ // k-space system - $trueSec = round($iterator['trueSec'], 3); + $trueSec = round($iterator['system_security'], 3); if($trueSec <= 0){ $security = '0.0'; @@ -98,7 +103,7 @@ class CcpSystemsMapper extends AbstractIterator { return $security; }; - // format functions + // "system type" mapping ---------------------------------------------- self::$map['type'] = function($iterator){ // TODO refactor @@ -122,7 +127,6 @@ class CcpSystemsMapper extends AbstractIterator { iterator_apply($this, 'self::recursiveIterator', [$this]); - return iterator_to_array($this, false); } @@ -166,7 +170,7 @@ class CcpSystemsMapper extends AbstractIterator { // just value change no key change $removeOldEntry = false; - $iterator -> next(); + $iterator->next(); }else{ // a -> b mapping $iterator->offsetSet( self::$map[$iterator->key()], $iterator->current() ); diff --git a/app/main/data/mapper/crestalliance.php b/app/main/data/mapper/crestalliance.php new file mode 100644 index 00000000..5d2bceea --- /dev/null +++ b/app/main/data/mapper/crestalliance.php @@ -0,0 +1,18 @@ + 'id', + 'name' => 'name' + ]; +} \ No newline at end of file diff --git a/app/main/data/mapper/crestservicestatus.php b/app/main/data/mapper/crestservicestatus.php new file mode 100644 index 00000000..07b39a76 --- /dev/null +++ b/app/main/data/mapper/crestservicestatus.php @@ -0,0 +1,19 @@ + 'dust', + 'eve' => 'eve', + 'server' => 'server' + ]; +} \ No newline at end of file diff --git a/app/main/data/mapper/creststation.php b/app/main/data/mapper/creststation.php new file mode 100644 index 00000000..c4e60ba5 --- /dev/null +++ b/app/main/data/mapper/creststation.php @@ -0,0 +1,18 @@ + 'id', + 'name' => 'name' + ]; +} \ No newline at end of file diff --git a/app/main/data/mapper/crestsystem.php b/app/main/data/mapper/crestsystem.php new file mode 100644 index 00000000..bbdeb619 --- /dev/null +++ b/app/main/data/mapper/crestsystem.php @@ -0,0 +1,18 @@ + 'id', + 'name' => 'name' + ]; +} \ No newline at end of file diff --git a/app/main/data/mapper/crestusercounts.php b/app/main/data/mapper/crestusercounts.php new file mode 100644 index 00000000..7ed58b7b --- /dev/null +++ b/app/main/data/mapper/crestusercounts.php @@ -0,0 +1,18 @@ + 'dust', + 'eve' => 'eve' + ]; +} \ No newline at end of file diff --git a/app/main/data/mapper/igbheader.php b/app/main/data/mapper/igbheader.php new file mode 100644 index 00000000..ab46894f --- /dev/null +++ b/app/main/data/mapper/igbheader.php @@ -0,0 +1,47 @@ + ['character' => 'id'], + 'charname' => ['character' => 'name'], + + // -------------------------------------------------------------------- + + 'solarsystemid' => ['system' => 'id'], + 'solarsystemname' => ['system' => 'name'], + + 'constellationid' => ['constellation' => 'id'], + 'constellationname' => ['constellation' => 'name'], + + 'regionid' => ['region' => 'id'], + 'regionname' => ['region' => 'name'], + + // -------------------------------------------------------------------- + + 'shiptypeid' => ['ship' => 'typeId'], + 'shiptypename' => ['ship' => 'typeName'], + 'shipid' => ['ship' => 'id'], + 'shipname' => ['ship' => 'name'], + + 'stationid' => ['station' => 'id'], + 'stationname' => ['station' => 'name'], + + // -------------------------------------------------------------------- + + 'corpid' => ['corporation' => 'id'], + 'corpname' => ['corporation' => 'name'], + + 'allianceid' => ['alliance' => 'id'], + 'alliancename' => ['alliance' => 'name'] + ]; +} \ No newline at end of file diff --git a/app/main/db/sql/mysql/tablemodifier.php b/app/main/db/sql/mysql/tablemodifier.php index 6f5992b6..c620e521 100644 --- a/app/main/db/sql/mysql/tablemodifier.php +++ b/app/main/db/sql/mysql/tablemodifier.php @@ -80,7 +80,7 @@ class TableModifier extends SQL\TableModifier { public function dropConstraint($constraint){ if($constraint->isValid()){ $this->queries[] = "ALTER TABLE " . $this->db->quotekey($this->name) . " - DROP FOREIGN KEY " . $this->db->quotekey($constraint->getConstraintName()); + DROP FOREIGN KEY " . $this->db->quotekey($constraint->getConstraintName()) . ";"; }else{ trigger_error(sprintf(self::TEXT_ConstraintNotValid, 'table: ' . $this->name . ' constraintName: ' . $constraint->getConstraintName())); } @@ -93,18 +93,13 @@ class TableModifier extends SQL\TableModifier { public function addConstraint($constraint){ if($constraint->isValid()){ - if($this->constraintExists($constraint)){ - // drop constraint and re-add in case something has changed - $this->dropConstraint($constraint); - } - $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(); + ON UPDATE " . $constraint->getOnUpdate() . ";"; }else{ trigger_error(sprintf(self::TEXT_ConstraintNotValid, 'table: ' . $this->name . ' constraintName: ' . $constraint->getConstraintName())); } @@ -120,7 +115,15 @@ class Column extends SQL\Column { const TEXT_TableNameMissing = 'Table name missing for FOREIGN KEY in `%s`'; /** - * ass constraint to this column + * 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){ diff --git a/app/main/lib/config.php b/app/main/lib/config.php new file mode 100644 index 00000000..49a7840e --- /dev/null +++ b/app/main/lib/config.php @@ -0,0 +1,150 @@ + CGI params (Nginx) + // -> .htaccess (Apache) + $this->setServerData(); + // set environment data + $this->setAllEnvironmentData(); + // set hive configuration variables + // -> overwrites default configuration + $this->setHiveVariables(); + } + + /** + * get environment configuration data + * @return array|null + */ + protected function getAllEnvironmentData(){ + $f3 = \Base::instance(); + $environmentData = null; + + if( $f3->exists(self::HIVE_KEY_ENVIRONMENT) ){ + $environmentData = $f3->get(self::HIVE_KEY_ENVIRONMENT); + }else{ + $environmentData = $this->setAllEnvironmentData(); + } + return $environmentData; + } + + /** + * set some global framework variables + * that depend on environment settings + */ + protected function setHiveVariables(){ + $f3 = \Base::instance(); + + // hive keys that should be overwritten by environment config + $hiveKeys = ['BASE', 'URL', 'DEBUG']; + foreach($hiveKeys as $key){ + $f3->set($key, self::getEnvironmentData($key)); + } + } + + /** + * set all environment configuration data + * @return array|null + */ + protected function setAllEnvironmentData(){ + $environmentData = null; + $f3 = \Base::instance(); + + if( !empty($this->serverConfigData['ENV']) ){ + // get environment config from $_SERVER data + $environmentData = (array)$this->serverConfigData['ENV']; + $environmentData['TYPE'] = 'PHP: environment variables'; + }else{ + // get environment data from *.ini file config + $f3->config('app/environment.ini'); + + if( + $f3->exists(self::HIVE_KEY_ENVIRONMENT) && + ($environment = $f3->get(self::HIVE_KEY_ENVIRONMENT . '.SERVER')) && + ($environmentData = $f3->get(self::HIVE_KEY_ENVIRONMENT . '.' . $environment)) + ){ + $environmentData['TYPE'] = 'Config: environment.ini'; + } + } + + 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; + * + * @return array + */ + 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){ + $f3 = \Base::instance(); + $hiveKey = self::HIVE_KEY_ENVIRONMENT . '.' . $key; + $data = null; + if( $f3->exists($hiveKey) ){ + $data = $f3->get($hiveKey); + } + + return $data; + } + +} \ No newline at end of file diff --git a/app/main/lib/web.php b/app/main/lib/web.php new file mode 100644 index 00000000..99871ac8 --- /dev/null +++ b/app/main/lib/web.php @@ -0,0 +1,233 @@ +eol, $headers), + $matches + ) + ){ + $statusCode = (int)$matches[1]; + } + return $statusCode; + } + + /** + * get cache time in seconds from Header data array + * @param array $headers + * @return int + */ + protected function getCacheTimeFromHeaders($headers = []){ + $cacheTime = 0; + + if( + preg_match( + '/Cache-Control:(.*?)max-age=([0-9]+)/', + implode($this->eol, $headers), + $matches + ) + ){ + $cacheTime = (int)$matches[2]; + }elseif( + preg_match( + '/Access-Control-Max-Age: ([0-9]+)/', + implode($this->eol, $headers), + $matches + ) + ){ + $cacheTime = (int)$matches[1]; + } + return $cacheTime; + } + + /** + * get a unique cache kay for a request + * @param $url + * @param null $options + * @return string + */ + protected function getCacheKey($url, $options = null){ + $f3 = \Base::instance(); + + $headers = isset($options['header']) ? implode($this->eol, (array) $options['header']) : ''; + + return $f3->hash( + $options['method'] . ' ' + . $url . ' ' + . $headers + ) . 'url'; + } + + /** + * perform curl() request + * -> caches response by returned HTTP Cache header data + * @param string $url + * @param array|null $options + * @param array $additionalOptions + * @param int $retryCount request counter for failed crest call + * @return array|FALSE|mixed + */ + public function request($url,array $options = null, $additionalOptions = [], $retryCount = 0 ) { + $f3 = \Base::instance(); + + if( !$f3->exists( $hash = $this->getCacheKey($url, $options) ) ){ + // retry same request until request limit is reached + $retry = false; + + $result = parent::request($url, $options); + $result['timeout'] = false; + $statusCode = $this->getStatusCodeFromHeaders( $result['headers'] ); + + switch($statusCode){ + case 100: + case 200: + // request succeeded -> check if response should be cached + $ttl = $this->getCacheTimeFromHeaders( $result['headers'] ); + + if( + $ttl > 0 && + !empty( json_decode( $result['body'], true ) ) + ){ + $f3->set($hash, $result, $ttl); + } + break; + case 401: + case 415: + // unauthorized + $errorMsg = $this->getErrorMessageFromJsonResponse( + $statusCode, + $options['method'], + $url, + json_decode($result['body']) + ); + LogController::getLogger('error')->write($errorMsg); + break; + case 500: + case 501: + case 502: + case 503: + case 505: + $retry = true; + + if( $retryCount == self::RETRY_COUNT_MAX ){ + $errorMsg = $this->getErrorMessageFromJsonResponse( + $statusCode, + $options['method'], + $url, + json_decode($result['body']) + ); + LogController::getLogger('error')->write($errorMsg); + + // trigger error + $f3->error($statusCode, $errorMsg); + } + break; + case 504: + case 0: + $retry = true; + + if( $retryCount == self::RETRY_COUNT_MAX ){ + // timeout -> response should not be cached + $result['timeout'] = true; + + $errorMsg = $this->getErrorMessageFromJsonResponse( + 504, + $options['method'], + $url, + json_decode($result['body']) + ); + + // log error + LogController::getLogger('error')->write($errorMsg); + + if($additionalOptions['suppressTimeoutErrors'] !== true){ + // trigger error + $f3->error(504, $errorMsg); + } + } + break; + default: + // unknown status + $errorMsg = $this->getErrorMessageFromJsonResponse( + $statusCode, + $options['method'], + $url + ); + + LogController::getLogger('error')->write($errorMsg); + break; + } + + if( + $retry && + $retryCount < self::RETRY_COUNT_MAX + ){ + $retryCount++; + $this->request($url, $options, $additionalOptions, $retryCount); + } + + }else{ + $result = $f3->get($hash); + } + + return $result; + } + + /** + * get error message from response object + * @param int $code + * @param string $method + * @param string $url + * @param null|\stdClass $responseBody + * @return string + */ + protected function getErrorMessageFromJsonResponse($code, $method, $url, $responseBody = null){ + if( empty($responseBody->message) ){ + $message = @constant('Base::HTTP_' . $code); + }else{ + $message = $responseBody->message; + } + + $body = ''; + if( !is_null($responseBody) ){ + $body = ' | body: ' . print_r($responseBody, true); + } + + return sprintf(self::ERROR_STATUS_LOG, $code, $message, $method, $url, $body); + } + +} \ No newline at end of file diff --git a/app/main/model/basicmodel.php b/app/main/model/basicmodel.php index c70d0d21..1c776425 100644 --- a/app/main/model/basicmodel.php +++ b/app/main/model/basicmodel.php @@ -13,7 +13,7 @@ use Exception; use Controller; use DB; -class BasicModel extends \DB\Cortex { +abstract class BasicModel extends \DB\Cortex { /** * Hive key with DB object @@ -81,15 +81,21 @@ class BasicModel extends \DB\Cortex { $self->clearCacheData(); }); - // model updated $this->afterupdate( function($self){ $self->clearCacheData(); }); - // model updated $this->beforeinsert( function($self){ $self->beforeInsertEvent($self); }); + + $this->beforeerase( function($self){ + $self->beforeeraseEvent($self); + }); + + $this->aftererase( function($self){ + $self->aftereraseEvent($self); + }); } @@ -106,18 +112,27 @@ class BasicModel extends \DB\Cortex { return; } - if($key != 'updated'){ + if( + !$this->dry() && + $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 - ){ + if(is_object($currentVal)){ + if( + is_numeric($val) && + is_subclass_of($currentVal, 'Model\BasicModel') && + $currentVal->_id !== (int)$val + ){ + $this->touch('updated'); + } + }elseif($currentVal != $val){ $this->touch('updated'); } + } } @@ -226,7 +241,7 @@ class BasicModel extends \DB\Cortex { $cacheKey = null; // set a model unique cache key if the model is saved - if( $this->_id > 0){ + 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 @@ -245,6 +260,67 @@ class BasicModel extends \DB\Cortex { return $cacheKey; } + /** + * get cached data from this model + * @param string $dataCacheKeyPrefix - optional key prefix + * @return \stdClass|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){ + + $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 + */ + public function clearCacheData(){ + $cacheKey = $this->getCacheKey(); + + if( !is_null($cacheKey) ){ + $f3 = self::getF3(); + + if( $f3->exists($cacheKey) ){ + $f3->clear($cacheKey); + } + + } + } + /** * Throws a validation error for a giben column * @param $col @@ -303,18 +379,18 @@ class BasicModel extends \DB\Cortex { /** * get dataSet by foreign column (single result) * @param $key - * @param $id + * @param $value * @param array $options * @param int $ttl * @return \DB\Cortex */ - public function getByForeignKey($key, $id, $options = [], $ttl = 60){ + public function getByForeignKey($key, $value, $options = [], $ttl = 60){ $querySet = []; $query = []; if($this->exists($key)){ $query[] = $key . " = :" . $key; - $querySet[':' . $key] = $id; + $querySet[':' . $key] = $value; } // check active column @@ -338,11 +414,29 @@ class BasicModel extends \DB\Cortex { } /** - * function should be overwritten in child classes with access restriction - * @param $accessObject + * Event "Hook" function + * can be overwritten * @return bool */ - public function hasAccess($accessObject){ + public function beforeeraseEvent($self){ + return true; + } + + /** + * Event "Hook" function + * can be overwritten + * @return bool + */ + public function aftereraseEvent($self){ + return true; + } + + /** + * function should be overwritten in child classes with access restriction + * @param CharacterModel $characterModel + * @return bool + */ + public function hasAccess(CharacterModel $characterModel){ return true; } @@ -354,113 +448,6 @@ class BasicModel extends \DB\Cortex { 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){ - - $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 - */ - public function clearCacheData(){ - $cacheKey = $this->getCacheKey(); - - if( !is_null($cacheKey) ){ - $f3 = self::getF3(); - - if( $f3->exists($cacheKey) ){ - $f3->clear($cacheKey); - } - - } - } - - /** - * get the current class name - * -> namespace not included - * @return string - */ - public static function getClassName(){ - $parts = explode('\\', static::class); - return end($parts); - } - - /** - * 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( null, 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); - } - /** * export and download table data as *.csv * this is primarily used for static tables @@ -573,6 +560,54 @@ class BasicModel extends \DB\Cortex { return ['added' => $addedCount, 'updated' => $updatedCount, 'deleted' => $deletedCount]; } + /** + * get the current class name + * -> namespace not included + * @return string + */ + public static function getClassName(){ + $parts = explode('\\', static::class); + return end($parts); + } + + /** + * factory for all Models + * @param string $model + * @param int $ttl + * @return BasicModel + * @throws \Exception + */ + public static function getNew($model, $ttl = 86400){ + $class = null; + + $model = '\\' . __NAMESPACE__ . '\\' . $model; + if(class_exists($model)){ + $class = new $model( null, null, null, $ttl ); + }else{ + throw new \Exception('No model class found'); + } + + return $class; + } + + /** + * get the framework instance (singleton) + * @return \Base + */ + public static function getF3(){ + return \Base::instance(); + } + + /** + * debug log function + * @param $text + * @param string $logFile + */ + public static function log($text, $logFile = null){ + $logFile = isset($logFile) ? $logFile : self::getF3()->get('PATHFINDER.LOGFILES.DEBUG'); + Controller\LogController::getLogger($logFile)->write($text); + } + /** * get tableModifier class for this table * @return bool|DB\SQL\TableModifier diff --git a/app/main/model/characterauthenticationmodel.php b/app/main/model/characterauthenticationmodel.php new file mode 100644 index 00000000..c5c4b82f --- /dev/null +++ b/app/main/model/characterauthenticationmodel.php @@ -0,0 +1,74 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'characterId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\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 $self CharacterAuthenticationModel + * @return bool + */ + public function beforeeraseEvent($self){ + // 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/main/model/characterlogmodel.php b/app/main/model/characterlogmodel.php index 2b9a7acf..ee7634c1 100644 --- a/app/main/model/characterlogmodel.php +++ b/app/main/model/characterlogmodel.php @@ -14,6 +14,14 @@ class CharacterLogModel extends BasicModel { protected $table = 'character_log'; + /** + * caching for relational data + * -> 10s matches REST API - Expire: Header-Data + * for "Location" calls + * @var int + */ + protected $rel_ttl = 10; + protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, @@ -33,6 +41,9 @@ class CharacterLogModel extends BasicModel { ] ] ], + + // -------------------------------------------------------------------- + 'systemId' => [ 'type' => Schema::DT_INT, 'index' => true @@ -42,30 +53,107 @@ class CharacterLogModel extends BasicModel { 'nullable' => false, 'default' => '' ], - 'shipId' => [ + 'constellationId' => [ 'type' => Schema::DT_INT, 'index' => true ], + 'constellationName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'regionId' => [ + 'type' => Schema::DT_INT, + 'index' => true + ], + 'regionName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + + // -------------------------------------------------------------------- + 'shipTypeId' => [ + 'type' => Schema::DT_INT, + 'index' => true + ], + 'shipTypeName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'shipId' => [ + 'type' => Schema::DT_BIGINT, + 'index' => true + ], 'shipName' => [ 'type' => Schema::DT_VARCHAR128, 'nullable' => false, 'default' => '' ], - 'shipTypeName' => [ + 'stationId' => [ + 'type' => Schema::DT_INT, + 'index' => true + ], + 'stationName' => [ 'type' => Schema::DT_VARCHAR128, 'nullable' => false, 'default' => '' ] ]; - public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = 0){ + /** + * set log data from array + * @param array $logData + */ + public function setData($logData){ - parent::__construct($db, $table, $fluid, $ttl); + if( isset($logData['system']) ){ + $this->systemId = (int)$logData['system']['id']; + $this->systemName = $logData['system']['name']; + }else{ + $this->systemId = null; + $this->systemName = ''; + } + + if( isset($logData['constellation']) ){ + $this->constellationId = (int)$logData['constellation']['id']; + $this->constellationName = $logData['constellation']['name']; + }else{ + $this->constellationId = null; + $this->constellationName = ''; + } + + if( isset($logData['region']) ){ + $this->regionId = (int)$logData['region']['id']; + $this->regionName = $logData['region']['name']; + }else{ + $this->regionId = null; + $this->regionName = ''; + } + + // -------------------------------------------------------------------- + + 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']; + }else{ + $this->shipTypeId = null; + $this->shipTypeName = ''; + $this->shipId = null; + $this->shipName = ''; + } + + if( isset($logData['station']) ){ + $this->stationId = (int)$logData['station']['id']; + $this->stationName = $logData['station']['name']; + }else{ + $this->stationId = null; + $this->stationName = ''; + } - // events ----------------------------------------- - $this->beforeerase(function($self){ - $self->clearCacheData(); - }); } /** @@ -76,30 +164,30 @@ class CharacterLogModel extends BasicModel { $logData = (object) []; $logData->system = (object) []; - $logData->system->id = $this->systemId; + $logData->system->id = (int)$this->systemId; $logData->system->name = $this->systemName; + $logData->constellation = (object) []; + $logData->constellation->id = (int)$this->constellationId; + $logData->constellation->name = $this->constellationName; + + $logData->region = (object) []; + $logData->region->id = (int)$this->regionId; + $logData->region->name = $this->regionName; + + // -------------------------------------------------------------------- + $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->typeName = $this->shipTypeName; + + $logData->station = (object) []; + $logData->station->id = (int)$this->stationId; + $logData->station->name = $this->stationName; return $logData; } - /** - * see parent - */ - public function clearCacheData(){ - parent::clearCacheData(); - - // delete log cache key as well - $f3 = self::getF3(); - $character = $this->characterId; - - $character->clearCacheData(); - $f3->clear('LOGGED.user.character.id_' . $character->id); - - return true; - } } \ No newline at end of file diff --git a/app/main/model/usermapmodel.php b/app/main/model/charactermapmodel.php similarity index 79% rename from app/main/model/usermapmodel.php rename to app/main/model/charactermapmodel.php index bd39f5cb..fadb8793 100644 --- a/app/main/model/usermapmodel.php +++ b/app/main/model/charactermapmodel.php @@ -1,18 +1,18 @@ [ @@ -21,13 +21,13 @@ class UserMapModel extends BasicModel { 'default' => 1, 'index' => true ], - 'userId' => [ + 'characterId' => [ 'type' => Schema::DT_INT, 'index' => true, - 'belongs-to-one' => 'Model\UserModel', + 'belongs-to-one' => 'Model\CharacterModel', 'constraint' => [ [ - 'table' => 'user', + 'table' => 'character', 'on-delete' => 'CASCADE' ] ] @@ -66,10 +66,10 @@ class UserMapModel extends BasicModel { $status = parent::setup($db,$table,$fields); if($status === true){ - $status = parent::setMultiColumnIndex(['userId', 'mapId'], true); + $status = parent::setMultiColumnIndex(['characterId', 'mapId'], true); } return $status; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/main/model/charactermodel.php b/app/main/model/charactermodel.php index fd58eda7..e1b1e5e5 100644 --- a/app/main/model/charactermodel.php +++ b/app/main/model/charactermodel.php @@ -8,13 +8,20 @@ namespace Model; +use Controller; +use Controller\Ccp\Sso as Sso; use DB\SQL\Schema; +use Data\Mapper as Mapper; class CharacterModel extends BasicModel { protected $table = 'character'; protected $fieldConf = [ + 'lastLogin' => [ + 'type' => Schema::DT_TIMESTAMP, + 'index' => true + ], 'active' => [ 'type' => Schema::DT_BOOL, 'nullable' => false, @@ -26,6 +33,22 @@ class CharacterModel extends BasicModel { 'nullable' => false, 'default' => '' ], + 'ownerHash' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'crestAccessToken' => [ + 'type' => Schema::DT_VARCHAR256 + ], + 'crestAccessTokenUpdated' => [ + 'type' => Schema::DT_TIMESTAMP, + 'default' => Schema::DF_CURRENT_TIMESTAMP, + 'index' => true + ], + 'crestRefreshToken' => [ + 'type' => Schema::DT_VARCHAR256 + ], 'corporationId' => [ 'type' => Schema::DT_INT, 'index' => true, @@ -57,29 +80,47 @@ class CharacterModel extends BasicModel { 'nullable' => false, 'default' => '' ], + 'shared' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0 + ], + 'userCharacter' => [ + 'has-one' => ['Model\UserCharacterModel', 'characterId'] + ], 'characterLog' => [ 'has-one' => ['Model\CharacterLogModel', 'characterId'] + ], + 'characterMaps' => [ + 'has-many' => ['Model\CharacterMapModel', 'characterId'] + ], + 'characterAuthentications' => [ + 'has-many' => ['Model\CharacterAuthenticationModel', 'characterId'] ] ]; /** * get character data * @param bool|false $addCharacterLogData - * @return object + * @return \stdClass */ public function getData($addCharacterLogData = false){ + $cacheKeyModifier = ''; // check if there is cached data - // temporary disabled (performance test) - $characterData = null; //$this->getCacheData(); + if($addCharacterLogData){ + $cacheKeyModifier = strtoupper($this->table) . '_LOG'; + } + + $characterData = $this->getCacheData($cacheKeyModifier); if(is_null($characterData)){ + // no cached character data found - $characterData = (object) []; - $characterData->id = $this->id; $characterData->name = $this->name; + $characterData->shared = $this->shared; if($addCharacterLogData){ if($logModel = $this->getLog()){ @@ -100,24 +141,60 @@ class CharacterModel extends BasicModel { // 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); + $this->updateCacheData($characterData, $cacheKeyModifier, 10); } return $characterData; } + /** + * 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() ){ + $this->userCharacter->erase(); + } + + // delete all existing login-cookie data + $this->logout(); + } + + return $ownerHash; + } + + /** + * set CREST accessToken for current session + * -> update "tokenUpdated" column on change + * -> this is required for expire checking! + * @param string $accessToken + * @return string + */ + public function set_crestAccessToken($accessToken){ + if($this->crestAccessToken !== $accessToken){ + $this->touch('crestAccessTokenUpdated'); + } + return $accessToken; + } + + /** + * check whether this character has already a user assigned to it + * @return bool + */ + public function hasUserCharacter(){ + return is_object($this->userCharacter); + } + /** * check whether this character has a corporation * @return bool */ public function hasCorporation(){ - $hasCorporation = false; - - if($this->corporationId){ - $hasCorporation = true; - } - - return $hasCorporation; + return is_object($this->corporationId); } /** @@ -125,46 +202,280 @@ class CharacterModel extends BasicModel { * @return bool */ public function hasAlliance(){ - $hasAlliance = false; + return is_object($this->allianceId); + } - if($this->allianceId){ - $hasAlliance = true; + /** + * @return UserModel|null + */ + public function getUser(){ + $user = null; + if($this->hasUserCharacter()){ + /** + * @var $user UserModel + */ + $user = $this->userCharacter->userId; } - - return $hasAlliance; + return $user; } /** * get the corporation for this user - * @return mixed|null + * @return \Model\CorporationModel|null */ public function getCorporation(){ - $corporation = null; - - if($this->hasCorporation()){ - $corporation = $this->corporationId; - } - - return $corporation; + return $this->corporationId; } /** * get the alliance of this user - * @return mixed|null + * @return \Model\AllianceModel|null */ public function getAlliance(){ - $alliance = null; + return $this->allianceId; + } - if($this->hasAlliance()){ - $alliance = $this->allianceId; + /** + * get CREST API "access_token" from OAuth + * @return bool|string + */ + public function getAccessToken(){ + $accessToken = false; + + // check if there is already an "accessToken" for this user + // check expire timer for stored "accessToken" + if( + !empty($this->crestAccessToken) && + !empty($this->crestAccessTokenUpdated) + ){ + $timezone = new \DateTimeZone( $this->getF3()->get('TZ') ); + $tokenTime = \DateTime::createFromFormat( + 'Y-m-d H:i:s', + $this->crestAccessTokenUpdated, + $timezone + ); + // add expire time buffer for this "accessToken" + // token should be marked as "deprecated" BEFORE it actually expires. + $timeBuffer = 2 * 60; + $tokenTime->add(new \DateInterval('PT' . (Sso::ACCESS_KEY_EXPIRE_TIME - $timeBuffer) . 'S')); + + $now = new \DateTime('now', $timezone); + if($tokenTime->getTimestamp() > $now->getTimestamp()){ + $accessToken = $this->crestAccessToken; + } } - return $alliance; + // if no "accessToken" was found -> get a fresh one by an existing "refreshToken" + if( + !$accessToken && + !empty($this->crestRefreshToken) + ){ + // no accessToken found OR token is deprecated + $ssoController = new Sso(); + $accessData = $ssoController->refreshAccessToken($this->crestRefreshToken); + + if( + isset($accessData->accessToken) && + isset($accessData->refreshToken) + ){ + $this->crestAccessToken = $accessData->accessToken; + $this->save(); + + $accessToken = $this->crestAccessToken; + } + } + + return $accessToken; + } + + /** + * checks whether this character is authorized to log in + * -> check corp/ally whitelist config (pathfinder.ini) + * @return bool + */ + public function isAuthorized(){ + $isAuthorized = false; + $f3 = self::getF3(); + + $whitelistCorporations = $whitelistAlliance = []; + if( !empty($f3->get('PATHFINDER.LOGIN.CORPORATION')) ){ + $whitelistCorporations = array_map('trim',(array) $f3->get('PATHFINDER.LOGIN.CORPORATION') ); + } + if( !empty($f3->get('PATHFINDER.LOGIN.ALLIANCE')) ){ + $whitelistAlliance = array_map('trim',(array) $f3->get('PATHFINDER.LOGIN.ALLIANCE') ); + } + + if( + empty($whitelistCorporations) && + empty($whitelistAlliance) + ){ + // no corp/ally restrictions set -> any character is allowed to login + $isAuthorized = true; + }else{ + // check if character corporation is set in whitelist + if( + !empty($whitelistCorporations) && + $this->hasCorporation() && + in_array($this->getCorporation()->_id, $whitelistCorporations) + ){ + $isAuthorized = true; + } + + // check if character alliance is set in whitelist + if( + !$isAuthorized && + !empty($whitelistAlliance) && + $this->hasAlliance() && + in_array($this->getAlliance()->_id, $whitelistAlliance) + ){ + $isAuthorized = true; + } + } + + return $isAuthorized; + } + + /** + * update character log (active system, ...) + * -> HTTP Header Data (if IGB) + * -> CREST API request for character log data (if not IGB) + * @param array $additionalOptions (optional) request options for cURL request + * @return $this + */ + public function updateLog($additionalOptions = []){ + // check if everything is OK + // -> do not update log in case of temporary CREST timeouts + $updateLogData = false; + $logData = []; + $headerData = Controller\Controller::getIGBHeaderData(); + + // check if IGB Data is available + if( + $headerData->trusted === true && + !empty($headerData->values) + ){ + // format header data + $formattedHeaderData = (new Mapper\IgbHeader($headerData->values))->getData(); + + // just for security -> check if Header Data matches THIS character + // in case current IGB-Character is NOT the one logged on -> don´t update log + if( + isset($formattedHeaderData['character']) && + $formattedHeaderData['character']['id'] == $this->_id + ){ + $updateLogData = true; + $logData = $formattedHeaderData; + } + } + + if($updateLogData == false){ + // ... IGB Header data not found OR character does not match current active character + // -> try to pull data from CREST + + $ssoController = new Sso(); + $logData = $ssoController->getCharacterLocationData($this->getAccessToken(), 10, $additionalOptions); + + if($logData['timeout'] === false){ + $updateLogData = true; + } + } + + if($updateLogData == true){ + if( empty($logData['system']) ){ + // character is not in-game + if(is_object($this->characterLog)){ + // delete existing log + $this->characterLog->erase(); + $this->save(); + } + }else{ + // character is currently in-game + if( !$characterLog = $this->getLog() ){ + // create new log + $characterLog = $this->rel('characterLog'); + $characterLog->characterId = $this->_id; + } + $characterLog->setData($logData); + $characterLog->save(); + + $this->characterLog = $characterLog; + } + } + + return $this; + } + + /** + * update character data from CCPs CREST API + * @return array (some status messages) + */ + public function updateFromCrest(){ + $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( + !is_null( $verificationCharacterData = $ssoController->verifyCharacterData($accessToken) ) && + $verificationCharacterData->CharacterID === $this->_id + ){ + // get character data from CREST + $characterData = $ssoController->getCharacterData($accessToken); + if( isset($characterData->character) ){ + $characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash; + + $corporation = null; + $alliance = null; + if( isset($characterData->corporation) ){ + /** + * @var $corporation CorporationModel + */ + $corporation = $this->rel('corporationId'); + $corporation->getById($characterData->corporation['id'], 0); + $corporation->copyfrom($characterData->corporation, ['id', 'name', 'isNPC']); + $corporation->save(); + } + + if( isset($characterData->alliance) ){ + /** + * @var $alliance AllianceModel + */ + $alliance = $this->rel('allianceId'); + $alliance->getById($characterData->alliance['id'], 0); + $alliance->copyfrom($characterData->alliance, ['id', 'name']); + $alliance->save(); + } + + $this->copyfrom($characterData->character, ['name', 'ownerHash']); + $this->set('corporationId', is_object($corporation) ? $corporation->get('id') : null); + $this->set('allianceId', is_object($alliance) ? $alliance->get('id') : null); + $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(){ + return md5($this->name); } /** * get the character log entry for this character - * @return bool|null + * @return bool|CharacterLogModel */ public function getLog(){ @@ -173,10 +484,78 @@ class CharacterModel extends BasicModel { is_object($this->characterLog) && !$this->characterLog->dry() ){ - $characterLog = $this->characterLog; + $characterLog = &$this->characterLog; } return $characterLog; } + /** + * get mapModel by id and check if user has access + * @param $mapId + * @return MapModel|null + */ + public function getMap($mapId){ + /** + * @var $map MapModel + */ + $map = self::getNew('MapModel'); + $map->getById( (int)$mapId ); + + $returnMap = null; + if($map->hasAccess($this)){ + $returnMap = $map; + } + + return $returnMap; + } + + /** + * get all accessible map models for this character + * @return MapModel[] + */ + public function getMaps(){ + $this->filter( + 'characterMaps', + ['active = ?', 1], + ['order' => 'created'] + ); + + $maps = []; + + if($alliance = $this->getAlliance()){ + $maps = array_merge($maps, $alliance->getMaps()); + } + + if($corporation = $this->getCorporation()){ + $maps = array_merge($maps, $corporation->getMaps()); + } + + if($this->characterMaps){ + $mapCountPrivate = 0; + foreach($this->characterMaps as &$characterMap){ + if( + $mapCountPrivate < self::getF3()->get('PATHFINDER.MAX_MAPS_PRIVATE') && + $characterMap->mapId->isActive() + ){ + $maps[] = &$characterMap->mapId; + $mapCountPrivate++; + } + } + } + + return $maps; + } + + public function logout(){ + if($this->characterAuthentications){ + foreach($this->characterAuthentications as $characterAuthentication){ + /** + * @var $characterAuthentication CharacterAuthenticationModel + */ + $characterAuthentication->erase(); + } + } + } + } \ No newline at end of file diff --git a/app/main/model/connectionmodel.php b/app/main/model/connectionmodel.php index 51d53a84..c5dc133d 100644 --- a/app/main/model/connectionmodel.php +++ b/app/main/model/connectionmodel.php @@ -103,11 +103,11 @@ class ConnectionModel extends BasicModel{ /** * check object for model access - * @param $accessObject - * @return bool + * @param CharacterModel $characterModel + * @return mixed */ - public function hasAccess($accessObject){ - return $this->mapId->hasAccess($accessObject); + public function hasAccess(CharacterModel $characterModel){ + return $this->mapId->hasAccess($characterModel); } /** @@ -117,8 +117,12 @@ class ConnectionModel extends BasicModel{ public function isValid(){ $isValid = true; + // check if source/target system are not equal // check if source/target belong to same map - if( $this->source->mapId->id !== $this->target->mapId->id ){ + if( + $this->source->_id === $this->target->_id || + $this->source->mapId->_id !== $this->target->mapId->_id + ){ $isValid = false; } @@ -127,13 +131,12 @@ class ConnectionModel extends BasicModel{ /** * delete a connection - * @param $accessObject + * @param CharacterModel $characterModel */ - public function delete($accessObject){ - - if(!$this->dry()){ - // check if editor has access - if($this->hasAccess($accessObject)){ + public function delete(CharacterModel $characterModel){ + if( !$this->dry() ){ + // check if character has access + if($this->hasAccess($characterModel)){ $this->erase(); } } diff --git a/app/main/model/mapmodel.php b/app/main/model/mapmodel.php index 9dc4890d..b66a6547 100644 --- a/app/main/model/mapmodel.php +++ b/app/main/model/mapmodel.php @@ -8,6 +8,7 @@ namespace Model; +use Controller\Api\User; use DB\SQL\Schema; class MapModel extends BasicModel { @@ -60,8 +61,8 @@ class MapModel extends BasicModel { 'connections' => [ 'has-many' => ['Model\ConnectionModel', 'mapId'] ], - 'mapUsers' => [ - 'has-many' => ['Model\UserMapModel', 'mapId'] + 'mapCharacters' => [ + 'has-many' => ['Model\CharacterMapModel', 'mapId'] ], 'mapCorporations' => [ 'has-many' => ['Model\CorporationMapModel', 'mapId'] @@ -117,16 +118,14 @@ class MapModel extends BasicModel { } } - /** * get map data * -> this includes system and connection data as well! - * @return array + * @return \stdClass */ public function getData(){ - - // check if there is cached data - $mapDataAll = $this->getCacheData(); + // check if there is cached data + $mapDataAll = $this->getCacheData(); if(is_null($mapDataAll)){ // no cached map data found @@ -158,17 +157,23 @@ class MapModel extends BasicModel { // get access object data ------------------------------------- if($this->isPrivate()){ - $users = $this->getUsers(); - $userData = []; - foreach($users as $user){ - $userData[] = $user->getSimpleData(); + $characters = $this->getCharacters(); + $characterData = []; + foreach($characters as $character){ + /** + * @var $character CharacterModel + */ + $characterData[] = $character->getData(); } - $mapData->access->user = $userData; + $mapData->access->character = $characterData; } elseif($this->isCorporation()){ $corporations = $this->getCorporations(); $corporationData = []; foreach($corporations as $corporation){ + /** + * @var $corporation CorporationModel + */ $corporationData[] = $corporation->getData(); } $mapData->access->corporation = $corporationData; @@ -177,6 +182,9 @@ class MapModel extends BasicModel { $allianceData = []; foreach($alliances as $alliance){ + /** + * @var $alliance AllianceModel + */ $allianceData[] = $alliance->getData(); } $mapData->access->alliance = $allianceData; @@ -202,39 +210,61 @@ class MapModel extends BasicModel { } /** - * search for a system by id - * @param $systemId - * @return null + * search for a system by CCPs systemId + * @param int $systemId + * @return null|SystemModel */ - public function getSystem($systemId){ - $searchSystem = null; - - if($systemId > 0){ - $systems = $this->getSystems(); - foreach($systems as $system){ - if($system->id == $systemId){ - $searchSystem = $system; - break; - } - } + public function getSystemByCCPId($systemId){ + $system = null; + if( !empty($systems = $this->getSystems('systemId', (int)$systemId) ) ){ + $system = $systems[0]; } - return $searchSystem; + return $system; } /** - * get all system models in this map + * search for a system by id + * @param int $systemId + * @return null|SystemModel + */ + public function getSystemById($systemId){ + $system = null; + if( !empty($systems = $this->getSystems('id', (int)$systemId) ) ){ + $system = $systems[0]; + } + + return $system; + } + + /** + * get either all system models in this map + * -> or get a specific system by column filter + * @param string $column + * @param string $value * @return array|mixed */ - public function getSystems(){ - // orderBy x-Coordinate for cleaner frontend animation (left to right) - $this->filter('systems', - ['active = :active AND id > 0', - ':active' => 1 - ], - ['order' => 'posX']); - + public function getSystems($column = '', $value = ''){ $systems = []; + + $filterQuery = ['active = :active AND id > 0', + ':active' => 1 + ]; + + // add more filter options.... + if( + !empty($column) && + !empty($value) + ){ + $filterQuery[0] .= ' AND ' . $column . ' = :value'; + $filterQuery[':value'] = $value; + } + + // orderBy x-Coordinate for smoother frontend animation (left to right) + $this->filter('systems', $filterQuery, + ['order' => 'posX'] + ); + if($this->systems){ $systems = $this->systems; } @@ -252,6 +282,9 @@ class MapModel extends BasicModel { $systemData = []; foreach($systems as $system){ + /** + * @var $system SystemModel + */ $systemData[] = $system->getData(); } @@ -285,6 +318,9 @@ class MapModel extends BasicModel { $connectionData = []; foreach($connections as $connection){ + /** + * @var $connection ConnectionModel + */ $connectionData[] = $connection->getData(); } @@ -292,26 +328,26 @@ class MapModel extends BasicModel { } /** - * set map access for an object (user, corporation or alliance) + * set map access for an object (character, corporation or alliance) * @param $obj */ public function setAccess($obj){ $newAccessGranted = false; - if($obj instanceof UserModel){ + if($obj instanceof CharacterModel){ // private map // check whether the user has already map access - $this->has('mapUsers', ['active = 1 AND userId = :userId', ':userId' => $obj->id]); + $this->has('mapCharacters', ['active = 1 AND characterId = :characterId', ':characterId' => $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(); + // grant access for the character + $characterMap = self::getNew('CharacterMapModel'); + $characterMap-> characterId = $obj; + $characterMap->mapId = $this; + $characterMap->save(); $newAccessGranted = true; } @@ -355,25 +391,34 @@ class MapModel extends BasicModel { /** * clear access for a given type of objects - * @param $clearKeys + * @param array $clearKeys */ - public function clearAccess($clearKeys = ['user', 'corporation', 'alliance']){ + public function clearAccess($clearKeys = ['character', 'corporation', 'alliance']){ foreach($clearKeys as $key){ switch($key){ - case 'user': - foreach((array)$this->mapUsers as $obj){ - $obj->erase(); + case 'character': + foreach((array)$this->mapCharacters as $characterMapModel){ + /** + * @var CharacterMapModel $characterMapModel + */ + $characterMapModel->erase(); }; break; case 'corporation': - foreach((array)$this->mapCorporations as $obj){ - $obj->erase(); + foreach((array)$this->mapCorporations as $corporationMapModel){ + /** + * @var CorporationMapModel $corporationMapModel + */ + $corporationMapModel->erase(); }; break; case 'alliance': - foreach((array)$this->mapAlliances as $obj){ - $obj->erase(); + foreach((array)$this->mapAlliances as $allianceMapModel){ + /** + * @var AllianceMapModel $allianceMapModel + */ + $allianceMapModel->erase(); }; break; } @@ -381,22 +426,17 @@ class MapModel extends BasicModel { } /** - * checks weather a user has access to this map or not - * @param $user + * checks weather a character has access to this map or not + * @param CharacterModel $characterModel * @return bool */ - public function hasAccess($user){ + public function hasAccess(CharacterModel $characterModel){ $hasAccess = false; - if( - !$this->dry() && - $user instanceof UserModel - ){ - + if( !$this->dry() ){ // get all maps the user has access to // this includes corporation and alliance maps - $maps = $user->getMaps(); - + $maps = $characterModel->getMaps(); foreach($maps as $map){ if($map->id === $this->id){ $hasAccess = true; @@ -409,54 +449,56 @@ class MapModel extends BasicModel { } /** - * 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 + * get all (private) characters for this map + * @return CharacterModel array */ private function getCharacters(){ $characters = []; + $this->filter('mapCharacters', ['active = ?', 1]); + + if($this->mapCharacters){ + foreach($this->mapCharacters as $characterMapModel){ + $characters[] = $characterMapModel->characterId; + } + } + + return $characters; + } + + /** + * get all character models that are currently online "viewing" this map + * @return CharacterModel[] + */ + private function getActiveCharacters(){ + $characters = []; + if($this->isPrivate()){ - $users = $this->getUsers(); + $activeCharacters = $this->getCharacters(); - foreach($users as $user){ - // get all active character logs for a user - $tempActiveUserCharacters = $user->getActiveUserCharacters(); - - foreach($tempActiveUserCharacters as $tempActiveUserCharacter){ - $characters[] = $tempActiveUserCharacter; - } + // add active character for each user + foreach($activeCharacters as $activeCharacter){ + /** + * @var UserModel $user + */ + $characters[] = $activeCharacter; } }elseif($this->isCorporation()){ $corporations = $this->getCorporations(); foreach($corporations as $corporation){ + /** + * @var CorporationModel $corporation + */ $characters = array_merge($characters, $corporation->getCharacters()); } }elseif($this->isAlliance()){ $alliances = $this->getAlliances(); foreach($alliances as $alliance){ + /** + * @var AllianceModel $alliance + */ $characters = array_merge($characters, $alliance->getCharacters()); } } @@ -476,7 +518,7 @@ class MapModel extends BasicModel { if(is_null($charactersData)){ $charactersData = []; - $characters = $this->getCharacters(); + $characters = $this->getActiveCharacters(); foreach($characters as $character){ $charactersData[] = $character->getData(true); @@ -484,7 +526,7 @@ class MapModel extends BasicModel { // cache active characters (if found) if(!empty($charactersData)){ - $this->updateCacheData($charactersData, 'CHARACTERS', 10); + $this->updateCacheData($charactersData, 'CHARACTERS', 5); } } @@ -531,19 +573,15 @@ class MapModel extends BasicModel { return $alliances; } - /** * delete this map and all dependencies - * @param $accessObject + * @param CharacterModel $characterModel */ - public function delete($accessObject){ - - if(!$this->dry()){ - // check if editor has access - if($this->hasAccess($accessObject)){ + public function delete(CharacterModel $characterModel){ + if( !$this->dry() ){ + // check if character has access + if($this->hasAccess($characterModel)){ // all map related tables will be deleted on cascade - - // delete map $this->erase(); } } @@ -656,9 +694,9 @@ class MapModel extends BasicModel { if( $mapModel->isPrivate() ){ $mapModel->clearAccess(['corporation', 'alliance']); }elseif( $mapModel->isCorporation() ){ - $mapModel->clearAccess(['user', 'alliance']); + $mapModel->clearAccess(['character', 'alliance']); }elseif( $mapModel->isAlliance() ){ - $mapModel->clearAccess(['user', 'corporation']); + $mapModel->clearAccess(['character', 'corporation']); } } diff --git a/app/main/model/registrationkeymodel.php b/app/main/model/registrationkeymodel.php deleted file mode 100644 index 0b3ddc7b..00000000 --- a/app/main/model/registrationkeymodel.php +++ /dev/null @@ -1,49 +0,0 @@ - [ - 'type' => Schema::DT_BOOL, - 'nullable' => false, - 'default' => 1, - 'index' => true - ], - 'ip' => [ - 'type' => Schema::DT_VARCHAR128, - 'nullable' => false, - 'default' => '' - ], - 'used' => [ - 'type' => Schema::DT_BOOL, - 'nullable' => false, - 'default' => 0 - ], - 'email' => [ - 'type' => Schema::DT_VARCHAR128, - 'nullable' => false, - 'default' => '', - 'index' => true - ], - 'registrationKey' => [ - 'type' => Schema::DT_VARCHAR128, - 'nullable' => false, - 'default' => '', - 'index' => true, - 'unique' => true - ] - ]; - -} \ No newline at end of file diff --git a/app/main/model/systemmodel.php b/app/main/model/systemmodel.php index d338451d..b33096c1 100644 --- a/app/main/model/systemmodel.php +++ b/app/main/model/systemmodel.php @@ -177,18 +177,18 @@ class SystemModel extends BasicModel { }else{ // special array data if($key == 'constellation'){ - $this->constellationId = $value['id']; + $this->constellationId = (int)$value['id']; $this->constellation = $value['name']; }elseif($key == 'region'){ - $this->regionId = $value['id']; + $this->regionId = (int)$value['id']; $this->region = $value['name']; }elseif($key == 'type'){ - $this->typeId = $value['id']; + $this->typeId = (int)$value['id']; }elseif($key == 'status'){ - $this->statusId = $value['id']; + $this->statusId = (int)$value['id']; }elseif($key == 'position'){ - $this->posX = $value['x']; - $this->posY = $value['y']; + $this->posX = (int)$value['x']; + $this->posY = (int)$value['y']; } } } @@ -242,16 +242,17 @@ class SystemModel extends BasicModel { $systemData->position->x = $this->posX; $systemData->position->y = $this->posY; - if($this->createdCharacterId){ - $systemData->created = (object) []; + + $systemData->created = (object) []; + $systemData->created->created = strtotime($this->created); + if( is_object($this->createdCharacterId) ){ $systemData->created->character = $this->createdCharacterId->getData(); - $systemData->created->created = strtotime($this->created); } - if($this->updatedCharacterId){ - $systemData->updated = (object) []; + $systemData->updated = (object) []; + $systemData->updated->updated = strtotime($this->updated); + if( is_object($this->updatedCharacterId) ){ $systemData->updated->character = $this->updatedCharacterId->getData(); - $systemData->updated->updated = strtotime($this->updated); } // max caching time for a system @@ -313,23 +314,22 @@ class SystemModel extends BasicModel { /** * check object for model access - * @param $accessObject - * @return bool + * @param CharacterModel $characterModel + * @return mixed */ - public function hasAccess($accessObject){ - return $this->mapId->hasAccess($accessObject); + public function hasAccess(CharacterModel $characterModel){ + return $this->mapId->hasAccess($characterModel); } /** * delete a system from a map * hint: signatures and connections will be deleted on cascade - * @param $accessObject + * @param CharacterModel $characterModel */ - public function delete($accessObject){ - - if(! $this->dry()){ - // check if user has access - if($this->hasAccess($accessObject)){ + public function delete(CharacterModel $characterModel){ + if( !$this->dry() ){ + // check if character has access + if($this->hasAccess($characterModel)){ $this->erase(); } } @@ -367,14 +367,14 @@ class SystemModel extends BasicModel { /** * get Signature by id and check for access - * @param $accessObject + * @param CharacterModel $characterModel * @param $id * @return bool|null */ - public function getSignatureById($accessObject, $id){ + public function getSignatureById(CharacterModel $characterModel, $id){ $signature = null; - if($this->hasAccess($accessObject)){ + if($this->hasAccess($characterModel)){ $this->filter('signatures', ['active = ? AND id = ?', 1, $id]); if($this->signatures){ $signature = reset( $this->signatures ); @@ -386,14 +386,14 @@ class SystemModel extends BasicModel { /** * get a signature by its "unique" 3-digit name - * @param $accessObject + * @param CharacterModel $characterModel * @param $name * @return mixed|null */ - public function getSignatureByName($accessObject, $name){ + public function getSignatureByName(CharacterModel $characterModel, $name){ $signature = null; - if($this->hasAccess($accessObject)){ + if($this->hasAccess($characterModel)){ $this->filter('signatures', ['active = ? AND name = ?', 1, $name]); if($this->signatures){ $signature = reset( $this->signatures ); diff --git a/app/main/model/systemsignaturemodel.php b/app/main/model/systemsignaturemodel.php index 445635cf..41df0cbf 100644 --- a/app/main/model/systemsignaturemodel.php +++ b/app/main/model/systemsignaturemodel.php @@ -35,13 +35,13 @@ class SystemSignatureModel extends BasicModel { 'groupId' => [ 'type' => Schema::DT_INT, 'nullable' => false, - 'default' => 1, + 'default' => 0, 'index' => true, ], 'typeId' => [ 'type' => Schema::DT_INT, 'nullable' => false, - 'default' => 1, + 'default' => 0, 'index' => true, ], 'name' => [ @@ -103,26 +103,29 @@ class SystemSignatureModel extends BasicModel { } /** - * get signature data as array - * @return array + * get signature data + * @return \stdClass */ 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) - ] - ]; + $signatureData = (object) []; + $signatureData->id = $this->id; + $signatureData->groupId = $this->groupId; + $signatureData->typeId = $this->typeId; + $signatureData->name = $this->name; + $signatureData->description = $this->description; + + $signatureData->created = (object) []; + $signatureData->created->created = strtotime($this->created); + if( is_object($this->createdCharacterId) ){ + $signatureData->created->character = $this->createdCharacterId->getData(); + } + + $signatureData->updated = (object) []; + $signatureData->updated->updated = strtotime($this->updated); + if( is_object($this->updatedCharacterId) ){ + $signatureData->updated->character = $this->updatedCharacterId->getData(); + } return $signatureData; } @@ -151,18 +154,21 @@ class SystemSignatureModel extends BasicModel { /** * check object for model access - * @param $accessObject + * @param CharacterModel $characterModel * @return bool */ - public function hasAccess($accessObject){ - return $this->systemId->hasAccess($accessObject); + public function hasAccess(CharacterModel $characterModel){ + return $this->systemId->hasAccess($characterModel); } - public function delete($accessObject){ - - if(!$this->dry()){ - // check if editor has access - if($this->hasAccess($accessObject)){ + /** + * delete signature + * @param CharacterModel $characterModel + */ + public function delete(CharacterModel $characterModel){ + if( !$this->dry() ){ + // check if character has access + if($this->hasAccess($characterModel)){ $this->erase(); } } diff --git a/app/main/model/userapimodel.php b/app/main/model/userapimodel.php deleted file mode 100644 index 9ebb7c46..00000000 --- a/app/main/model/userapimodel.php +++ /dev/null @@ -1,152 +0,0 @@ - [ - 'type' => Schema::DT_BOOL, - 'nullable' => false, - 'default' => 1, - 'index' => true - ], - 'userId' => [ - 'type' => Schema::DT_INT, - 'index' => true, - 'belongs-to-one' => 'Model\UserModel', - 'constraint' => [ - [ - 'table' => 'user', - 'on-delete' => 'CASCADE' - ] - ] - ], - 'keyId' => [ - 'type' => Schema::DT_INT, - 'index' => true, - 'unique' => true - ], - 'vCode' => [ - 'type' => Schema::DT_VARCHAR128, - 'nullable' => false, - 'default' => '', - ], - '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->getUserCharacters(); - 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 index 9463a425..5d661286 100644 --- a/app/main/model/usercharactermodel.php +++ b/app/main/model/usercharactermodel.php @@ -32,20 +32,10 @@ class UserCharacterModel extends BasicModel { ] ] ], - 'apiId' => [ - 'type' => Schema::DT_INT, - 'index' => true, - 'belongs-to-one' => 'Model\UserApiModel', - 'constraint' => [ - [ - 'table' => 'user_api', - 'on-delete' => 'CASCADE' - ] - ] - ], 'characterId' => [ 'type' => Schema::DT_INT, 'index' => true, + 'unique' => true, 'belongs-to-one' => 'Model\CharacterModel', 'constraint' => [ [ @@ -53,12 +43,6 @@ class UserCharacterModel extends BasicModel { 'on-delete' => 'CASCADE' ] ] - ], - 'isMain' => [ - 'type' => Schema::DT_BOOLEAN, - 'nullable' => false, - 'default' => 0, - 'index' => true ] ]; @@ -67,9 +51,7 @@ class UserCharacterModel extends BasicModel { * @param $characterData */ public function setData($characterData){ - foreach((array)$characterData as $key => $value){ - if(!is_array($value)){ if($this->exists($key)){ $this->$key = $value; @@ -79,52 +61,19 @@ class UserCharacterModel extends BasicModel { } /** - * 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 + * event "Hook" + * -> remove user if there are no other characters bound to this user + * @param $self * @return bool */ - public function isMain(){ - $isMain = false; - if($this->isMain == 1){ - $isMain = true; + public function aftereraseEvent($self){ + if( + is_object($self->userId) && + is_null($self->userId->userCharacters) + ){ + $self->userId->erase(); } - - return $isMain; - } - - /** - * set this character as main character - */ - public function setMain($value = 0){ - $this->isMain = $value; + return true; } /** @@ -146,10 +95,7 @@ class UserCharacterModel extends BasicModel { $status = parent::setup($db,$table,$fields); if($status === true){ - $status = parent::setMultiColumnIndex(['userId', 'apiId', 'characterId'], true); - if($status === true){ - $status = parent::setMultiColumnIndex(['userId', 'apiId']); - } + $status = parent::setMultiColumnIndex(['userId', 'characterId'], true); } return $status; diff --git a/app/main/model/usermodel.php b/app/main/model/usermodel.php index 42c4ab8e..4ce5fe5d 100644 --- a/app/main/model/usermodel.php +++ b/app/main/model/usermodel.php @@ -10,6 +10,7 @@ namespace Model; use DB\SQL\Schema; use Controller; +use Controller\Api; use Exception; class UserModel extends BasicModel { @@ -17,10 +18,6 @@ class UserModel extends BasicModel { protected $table = 'user'; protected $fieldConf = [ - 'lastLogin' => [ - 'type' => Schema::DT_TIMESTAMP, - 'index' => true - ], 'active' => [ 'type' => Schema::DT_BOOL, 'nullable' => false, @@ -31,61 +28,32 @@ class UserModel extends BasicModel { 'type' => Schema::DT_VARCHAR128, 'nullable' => false, 'default' => '', - 'index' => true, - 'unique' => true + 'index' => true ], 'email' => [ - 'type' => Schema::DT_VARCHAR128, - 'nullable' => false, - 'default' => '', - 'index' => true, - 'unique' => true - ], - 'password' => [ 'type' => Schema::DT_VARCHAR128, 'nullable' => false, 'default' => '' ], - 'shared' => [ - 'type' => Schema::DT_BOOL, - 'nullable' => false, - 'default' => 0 - ], - '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' => 25 - ] - ], - 'email' => [ - 'length' => [ - 'min' => 5 - ] - ], - 'password' => [ - 'length' => [ - 'min' => 6 + 'min' => 3, + 'max' => 50 ] ] ]; /** * get all data for this user - * ! caution ! this function returns sensitive data! + * -> ! caution ! this function returns sensitive data! (e.g. email,..) * -> user getSimpleData() for faster performance and public user data - * @return object + * @return \stdClass */ public function getData(){ @@ -95,27 +63,19 @@ class UserModel extends BasicModel { // add sensitive user data $userData->email = $this->email; - // user shared info - $userData->shared = $this->shared; - - // 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(); + $characters = $this->getCharacters(); + foreach($characters as $character){ + /** + * @var $character CharacterModel + */ + $userData->characters[] = $character->getData(); } - // set active character with log data - $activeUserCharacter = $this->getActiveUserCharacter(); - if($activeUserCharacter){ - $userData->character = $activeUserCharacter->getData(true); - } + // get active character with log data + $activeCharacter = $this->getActiveCharacter(); + $userData->character = $activeCharacter->getData(true); return $userData; } @@ -123,7 +83,7 @@ class UserModel extends BasicModel { /** * get public user data * - check out getData() for all user data - * @return object + * @return \stdClass */ public function getSimpleData(){ $userData = (object) []; @@ -135,31 +95,21 @@ class UserModel extends BasicModel { /** * validate and set a email address for this user - * @param $email - * @return mixed + * -> empty email is allowed! + * @param string $email + * @return string */ public function set_email($email){ - if (\Audit::instance()->email($email) == false) { + if ( + !empty($email) && + \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 @@ -191,366 +141,89 @@ class UserModel extends BasicModel { return $this->getByForeignKey('name', $name, [], 0); } - /** - * 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], - ['order' => 'created'] - ); - - $maps = []; - if($this->userMaps){ - $mapCountPrivate = 0; - foreach($this->userMaps as $userMap){ - if( - $userMap->mapId->isActive() && - $mapCountPrivate < $f3->get('PATHFINDER.MAX_MAPS_PRIVATE') - ){ - $maps[] = $userMap->mapId; - $mapCountPrivate++; - } - } - } - - $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 first character as "main" - if( !$mainSet ){ - $userCharacter = reset($userCharacters); - $userCharacter->setMain(1); - $userCharacter->save(); - } - } - } - } - /** * get all userCharacters models for a user * characters will be checked/updated on login by CCP API call - * @return array|mixed + * @return UserCharacterModel[] */ public function getUserCharacters(){ + $this->filter('userCharacters', ['active = ?', 1]); - $this->filter('apis', ['active = ?', 1]); - - // if a user has multiple API keys saved for ONE character, - // skip double characters! $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()){ - - $tempCharacterId = $this->apis->current()->userCharacters->current()->characterId->get('id'); - if( !isset($userCharacters[ $tempCharacterId ]) ){ - $userCharacters[ $tempCharacterId ] = $this->apis->current()->userCharacters->current(); - } - $this->apis->current()->userCharacters->next(); - } - } - $this->apis->next(); - } + if($this->userCharacters){ + $userCharacters = $this->userCharacters; } return $userCharacters; } /** - * Get the main user character for this user - * @return null + * 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 */ - public function getMainUserCharacter(){ - $mainUserCharacter = null; + public function getActiveCharacter(){ + $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(){ $userCharacters = $this->getUserCharacters(); - + $characters = []; foreach($userCharacters as $userCharacter){ - if($userCharacter->isMain()){ - $mainUserCharacter = $userCharacter; - break; + /** + * @var $userCharacter UserCharacterModel + */ + if( $currentCharacter = $userCharacter->getCharacter() ){ + // check if userCharacter has a valid character + // -> this should never fail! + $characters[] = $currentCharacter; } } - return $mainUserCharacter; + return $characters; } /** - * 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; - - $headerData = Controller\CcpApiController::getIGBHeaderData(); - - // check if IGB Data is available - if( !empty($headerData->values) ){ - // search for the active character by IGB Header Data - - $this->filter('userCharacters', - [ - 'active = :active AND characterId = :characterId', - ':active' => 1, - ':characterId' => intval($headerData->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) + * get all active characters (with log entry) * hint: a user can have multiple active characters - * @return array + * @return CharacterModel[] */ - public function getActiveUserCharacters(){ + public function getActiveCharacters(){ $userCharacters = $this->getUserCharacters(); - $activeUserCharacters = []; + $activeCharacters = []; foreach($userCharacters as $userCharacter){ - $characterLog = $userCharacter->getCharacter()->getLog(); - - if($characterLog){ - $activeUserCharacters[] = $userCharacter; + /** + * @var $userCharacter UserCharacterModel + */ + $characterModel = $userCharacter->getCharacter(); + if($characterLog = $characterModel->getLog()){ + $activeCharacters[] = $characterModel; } } - return $activeUserCharacters; + return $activeCharacters; } - /** - * 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){ - $headerData = Controller\CcpApiController::getIGBHeaderData(); - - // check if IGB Data is available - if( !empty($headerData->values) ){ - $f3 = self::getF3(); - - // check if system has changed since the last call - // current location is stored (global) to avoid unnecessary DB calls - $sessionCharacterKey = 'LOGGED.user.character.id_' . $headerData->values['charid']; - - if($f3->exists($sessionCharacterKey)){ - // cache data exists - $cacheData = $f3->get($sessionCharacterKey); - }else{ - // new cache data - $cacheData = [ - 'systemId' => 0, - 'shipId' => 0 - ]; - } - - if( - $cacheData['systemId'] != $headerData->values['solarsystemid'] || - $cacheData['shipId'] != $headerData->values['shiptypeid'] - ){ - $cacheData['systemId'] = (int)$headerData->values['solarsystemid']; - $cacheData['shipId'] = (int)$headerData->values['shiptypeid']; - - // character has changed system, or character just logged on - $character = self::getNew('CharacterModel'); - $character->getById( (int)$headerData->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 - $corporationId = array_key_exists('corpid', $headerData->values) ? $headerData->values['corpid'] : null; - $allianceId = array_key_exists('allianceid', $headerData->values) ? $headerData->values['allianceid'] : null; - - // check if corp exists - if( !is_null($corporationId) ){ - $corporation = self::getNew('CorporationModel'); - $corporation->getById( (int)$corporationId ); - if( $corporation->dry() ){ - $corporation->id = $corporationId; - $corporation->name = $headerData->values['corpname']; - $corporation->save(); - } - } - - // check if ally exists - if( !is_null($allianceId) ){ - $alliance = self::getNew('AllianceModel'); - $alliance->getById( (int)$allianceId ); - if( $alliance->dry() ){ - $alliance->id = $allianceId; - $alliance->name = $headerData->values['alliancename']; - $alliance->save(); - } - } - - $character->id = (int) $headerData->values['charid']; - $character->name = $headerData->values['charname']; - $character->corporationId = $corporationId; - $character->allianceId = $allianceId; - $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 = (int)$headerData->values['solarsystemid']; - $characterLog->systemName = $headerData->values['solarsystemname']; - $characterLog->shipId = (int)$headerData->values['shiptypeid']; - $characterLog->shipName = $headerData->values['shipname']; - $characterLog->shipTypeName = $headerData->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 cf243f15..2702bb04 100644 --- a/app/pathfinder.ini +++ b/app/pathfinder.ini @@ -3,7 +3,7 @@ [PATHFINDER] NAME = PATHFINDER ; installed version (used for CSS/JS cache busting) -VERSION = v1.0.0RC2 +VERSION = v1.0.0 ; contact information (DO NOT CHANGE) CONTACT = https://github.com/exodus4d ; source code (DO NOT CHANGE) @@ -15,7 +15,7 @@ MAX_MAPS_CORPORATION = 3 MAX_MAPS_ALLIANCE = 3 ; Max number of shared entities per map -MAX_SHARED_USER = 10 +MAX_SHARED_CHARACTER = 10 MAX_SHARED_CORPORATION = 3 MAX_SHARED_ALLIANCE = 2 @@ -34,6 +34,14 @@ INVITE = 0 ; the limit of registration keys. Increase it to hand out more keys INVITE_LIMIT = 50 +[PATHFINDER.LOGIN] +; expire time (in days) for login cookies +COOKIE_EXPIRE = 30 + +; restrict login to specific corporations/alliances by id (e.g. 1000166,1000080) +CORPORATION = +ALLIANCE = + ; View ============================================================================================ [PATHFINDER.VIEW] ; static page templates @@ -78,7 +86,7 @@ EXECUTION_LIMIT = 50 ; map user update ping (ajax) (ms) [PATHFINDER.TIMER.UPDATE_SERVER_USER_DATA] DELAY = 5000 -EXECUTION_LIMIT = 200 +EXECUTION_LIMIT = 300 ; update client user data (ms) [PATHFINDER.TIMER.UPDATE_CLIENT_USER_DATA] @@ -95,19 +103,14 @@ CONSTELLATION_SYSTEMS = 2592000 [PATHFINDER.LOGFILES] ; just for manuel debug during development DEBUG = debug -; user login information +; login information LOGIN = login +; session warnings (suspect) +SESSION_SUSPECT = session_suspect ; account deleted -DELETE_ACCOUNT = delete_account +DELETE_ACCOUNT = account_delete ; API ============================================================================================= [PATHFINDER.API] -; CCP SSO OAuth 2.0 -CCP_SSO = https://login.eveonline.com -; CCP CREST API -CCP_CREST = https://crest-tq.eveonline.com -; CCP_CREST = https://api-sisi.testeveonline.com -; CCP XML APIv2 -CCP_XML = https://api.eveonline.com ; GitHub Developer API GIT_HUB = https://api.github.com diff --git a/app/routes.ini b/app/routes.ini index e319b891..20318742 100644 --- a/app/routes.ini +++ b/app/routes.ini @@ -3,16 +3,15 @@ [routes] ; DB setup setup ; IMPORTANT: remove/comment this line after setup/update is finished! -GET @setup: /setup = Controller\Setup->init, 0 +GET @setup: /setup [sync] = Controller\Setup->init, 0 ; login (index) page -GET @login: / = Controller\AppController->init, 0 +GET @login: / [sync] = Controller\AppController->init, 0 ; CCP SSO redirect -GET @sso: /sso/@action = Controller\CcpSsoController->@action, 0 - +GET @sso: /sso/@action [sync] = Controller\Ccp\Sso->@action, 0 ; map page -GET @map: /map = Controller\MapController->init, 0 +GET @map: /map [sync] = Controller\MapController->init, 0 ; 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 +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 diff --git a/export/sql/eve_citadel_min.sql.zip b/export/sql/eve_citadel_min.sql.zip new file mode 100644 index 00000000..b6dd80da Binary files /dev/null and b/export/sql/eve_citadel_min.sql.zip differ diff --git a/gulpfile.js b/gulpfile.js index 662fa753..8dbdfc7c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,7 +6,6 @@ var plumber = require('gulp-plumber'); var gzip = require('gulp-gzip'); var gulpif = require('gulp-if'); var clean = require('gulp-clean'); -var critical = require('critical'); var runSequence = require('run-sequence'); var exec = require('child_process').exec; @@ -91,13 +90,7 @@ gulp.task('jshint', function(){ errorHandler: onError })) .pipe(jshint()) - .pipe(jshint.reporter(stylish)) - .pipe(notify({ - icon: path.resolve(__dirname, _src.ICON), - title: 'JSHint', - message: 'Task complete', - onLast: true - })); + .pipe(jshint.reporter(stylish)); // .pipe(jshint.reporter('fail')); // uncomment this line to stop build on error }); @@ -247,25 +240,6 @@ gulp.task('default', function(tag) { ); }); -/* -// This removes all CSS styles "above the fold" from *.css and inlines them -// -> to improve pagespeed. The remaining (main) css file will be lazy loaded afterwards... -// https://github.com/addyosmani/critical -gulp.task('critical', function (cb) { - critical.generate({ - inline: true, - base: './', - src: './public/templates/view/index.html', - dest: './public/templates/view/index-critical.html', - extract: true, - minify: true, - width: 2560, - height: 1440 - }); -}); -*/ - - /** * clear all backend (fat free framework) cache files */ diff --git a/index.php b/index.php index b373f027..8bcde159 100644 --- a/index.php +++ b/index.php @@ -4,14 +4,8 @@ $f3 = require('app/lib/base.php'); // load main config $f3->config('app/config.ini'); -// set base dir -$f3->set('BASE', \Controller\Controller::getEnvironmentData('BASE')); - -// set debug level (stacktrace) -$f3->set('DEBUG', \Controller\Controller::getEnvironmentData('DEBUG')); - -// set debug level (stacktrace) -$f3->set('URL', \Controller\Controller::getEnvironmentData('URL')); +// load environment dependent config +lib\Config::instance(); // initiate cron-jobs Cron::instance(); diff --git a/js/app.js b/js/app.js index 529205b1..e66abc0a 100644 --- a/js/app.js +++ b/js/app.js @@ -39,13 +39,13 @@ requirejs.config({ 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 + bootbox: 'lib/bootbox.min', // v4.4.0 Bootbox.js - custom dialogs - http://bootboxjs.com/ 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 + validator: 'lib/validator.min', // v0.10.1 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://github.com/blueimp/Gallery/ blueImpGalleryHelper: 'lib/blueimp-helper', // helper function for Blue Imp Gallery diff --git a/js/app/config/signature_type.js b/js/app/config/signature_type.js index 8078a9cc..7b0150b3 100644 --- a/js/app/config/signature_type.js +++ b/js/app/config/signature_type.js @@ -6,11 +6,70 @@ * proofed, signature names (copy & paste from scanning window) */ -define([], function() { +define(['jquery'], function($) { 'use strict'; - // system effects + // signature sources + // http://de.sistersprobe.wikia.com/wiki/EVE_Sister_Core_Scanner_Probe_Wiki + + + // NullSec Relic sites, which can also spawn in C1, C2, C3 wormholes + var nullSecRelicSites = { + 10: 'Ruined Angel Crystal Quarry', + 11: 'Ruined Angel Monument Site', + 12: 'Ruined Angel Science Outpost', + 13: 'Ruined Angel Temple Site', + 14: 'Ruined Blood Raider Crystal Quarry', + 15: 'Ruined Blood Raider Monument Site', + 16: 'Ruined Blood Raider Science Outpost', + 17: 'Ruined Blood Raider Temple Site', + 18: 'Ruined Guristas Crystal Quarry', + 19: 'Ruined Guristas Monument Site', + 20: 'Ruined Guristas Science Outpost', + 21: 'Ruined Guristas Temple Site', + 22: 'Ruined Sansha Crystal Quarry', + 23: 'Ruined Sansha Monument Site', + 24: 'Ruined Sansha Science Outpost', + 25: 'Ruined Sansha Temple Site', + 26: 'Ruined Serpentis Crystal Quarry', + 27: 'Ruined Serpentis Monument Site', + 28: 'Ruined Serpentis Science Outpost', + 29: 'Ruined Serpentis Temple Site' + }; + + // NulSec Data sites, which can also spawn in C1, C2, C3 wormholes + var nullSecDataSites = { + 10: 'Abandoned Research Complex DA005', + 11: 'Abandoned Research Complex DA015', + 12: 'Abandoned Research Complex DC007', + 13: 'Abandoned Research Complex DC021', + 14: 'Abandoned Research Complex DC035', + 15: 'Abandoned Research Complex DG003', + 16: 'Central Angel Command Center', + 17: 'Central Angel Data Mining Site', + 18: 'Central Angel Sparking Transmitter', + 19: 'Central Angel Survey Site', + 20: 'Central Blood Raider Command Center', + 21: 'Central Blood Raider Data Mining Site', + 22: 'Central Blood Raider Sparking Transmitter', + 23: 'Central Blood Raider Survey Site', + 24: 'Central Guristas Command Center', + 25: 'Central Guristas Data Mining Center', + 26: 'Central Guristas Sparking Transmitter', + 27: 'Central Guristas Survey Site', + 28: 'Central Sansha Command Center', + 29: 'Central Sansha Data Mining Site', + 30: 'Central Sansha Sparking Transmitter', + 31: 'Central Sansha Survey Site', + 32: 'Central Serpentis Command Center', + 33: 'Central Serpentis Data Mining Site', + 34: 'Central Serpentis Sparking Transmitter', + 35: 'Central Serpentis Survey Site' + }; + + + // signature types var signatureTypes = { 1: { // system type (wh) 1: { // C1 (area id) @@ -20,14 +79,14 @@ define([], function() { 3: 'Phase Catalyst Node', 4: 'The Line' }, - 2: { // Relic - 1: 'Forgotten Perimeter Coronation Platform', - 2: 'Forgotten Perimeter Power Array' - }, - 3: { // Data - 1: 'Unsecured Perimeter Amplifier', - 2: 'Unsecured Perimeter Information Center ' - }, + 2: $.extend({}, nullSecRelicSites, { // Relic + 1: 'Forgotten Perimeter Coronation Platform', //* + 2: 'Forgotten Perimeter Power Array' //* + }), + 3: $.extend({}, nullSecDataSites, { // Data + 1: 'Unsecured Perimeter Amplifier', //* + 2: 'Unsecured Perimeter Information Center' //* + }), 4: { // Gas 1: 'Barren Perimeter Reservoir', //* 2: 'Token Perimeter Reservoir', //* @@ -62,14 +121,14 @@ define([], function() { 3: 'The Ruins of Enclave Cohort 27', 4: 'Sleeper Data Sanctuary' }, - 2: { // Relic - 1: 'Forgotten Perimeter Gateway', - 2: 'Forgotten Perimeter Habitation Coils' - }, - 3: { // Data - 1: 'Unsecured Perimeter Comms Relay', - 2: 'Unsecured Perimeter Transponder Farm ' - }, + 2: $.extend({}, nullSecRelicSites, { // Relic + 1: 'Forgotten Perimeter Gateway', //* + 2: 'Forgotten Perimeter Habitation Coils' //* + }), + 3: $.extend({}, nullSecDataSites, { // Data + 1: 'Unsecured Perimeter Comms Relay', //* + 2: 'Unsecured Perimeter Transponder Farm' //* + }), 4: { // Gas 1: 'Barren Perimeter Reservoir', //* 2: 'Token Perimeter Reservoir', //* @@ -100,14 +159,14 @@ define([], function() { 3: 'Solar Cell', 4: 'The Oruze Construct' }, - 2: { // Relic - 1: 'Forgotten Frontier Quarantine Outpost', - 2: 'Forgotten Frontier Recursive Depot' - }, - 3: { // Data - 1: 'Unsecured Frontier Database', - 2: 'Unsecured Frontier Receiver' - }, + 2: $.extend({}, nullSecRelicSites, { // Relic + 1: 'Forgotten Frontier Quarantine Outpost', //* + 2: 'Forgotten Frontier Recursive Depot' //* + }), + 3: $.extend({}, nullSecDataSites, { // Data + 1: 'Unsecured Frontier Database', //* + 2: 'Unsecured Frontier Receiver' //* + }), 4: { // Gas 1: 'Barren Perimeter Reservoir', //* 2: 'Token Perimeter Reservoir', //* @@ -192,13 +251,14 @@ define([], function() { }, 4: { // Gas 1: 'Barren Perimeter Reservoir', //* - 2: 'Token Perimeter Reservoir', //* - 3: 'Sizeable Perimeter Reservoir', //* - 4: 'Ordinary Perimeter Reservoir', //* - 5: 'Bountiful Frontier Reservoir', //* - 6: 'Instrumental Core Reservoir', //* - 7: 'Vital Core Reservoir', //* - 8: 'Minor Perimeter Reservoir' //* + 2: 'Minor Perimeter Reservoir', //* + 3: 'Ordinary Perimeter Reservoir', //* + 4: 'Sizeable Perimeter Reservoir', //* + 5: 'Token Perimeter Reservoir', //* + 6: 'Bountiful Frontier Reservoir', //* + 7: 'Vast Frontier Reservoir', //* + 8: 'Instrumental Core Reservoir', //* + 9: 'Vital Core Reservoir' //* }, 5: { // Wormhole 1: 'D792 - HS', @@ -206,9 +266,16 @@ define([], function() { 3: 'Z142 - 0.0' }, 6: { // ORE - 1: 'Ordinary Perimeter Deposit', //* - 2: 'Common Perimeter Deposit', //* - 3: 'Rarified Core Deposit' //* + 1: 'Average Frontier Deposit', //* + 2: 'Unexceptional Frontier Deposit', //* + 3: 'Uncommon Core Deposit', //* + 4: 'Ordinary Perimeter Deposit', //* + 5: 'Common Perimeter Deposit', //* + 6: 'Exceptional Core Deposit', //* + 7: 'Infrequent Core Deposit', //* + 8: 'Unusual Core Deposit', //* + 9: 'Rarified Core Deposit', //* + 10: 'Isolated Core Deposit' //* }, 7: { // Ghost @@ -230,14 +297,15 @@ define([], function() { 2: 'Unsecured Core Emergence' //* }, 4: { // Gas - 1: 'Token Perimeter Reservoir', //* + 1: 'Barren Perimeter Reservoir', //* 2: 'Minor Perimeter Reservoir', //* - 3: 'Sizeable Perimeter Reservoir', //* - 4: 'Ordinary Perimeter Reservoir', //* - 5: 'Bountiful Frontier Reservoir', //* - 6: 'Vast Frontier Reservoir', //* - 7: 'Instrumental Core Reservoir', //* - 8: 'Vital Core Reservoir' //* + 3: 'Ordinary Perimeter Reservoir', //* + 4: 'Sizeable Perimeter Reservoir', //* + 5: 'Token Perimeter Reservoir', //* + 6: 'Bountiful Frontier Reservoir', //* + 7: 'Vast Frontier Reservoir', //* + 8: 'Instrumental Core Reservoir', //* + 9: 'Vital Core Reservoir' //* }, 5: { // Wormhole 1: 'D792 - HS', diff --git a/js/app/init.js b/js/app/init.js index 477638d3..c98e5bac 100644 --- a/js/app/init.js +++ b/js/app/init.js @@ -11,12 +11,13 @@ define(['jquery'], function($) { img: 'public/img/', // path for images // user API getCaptcha: 'api/user/getCaptcha', // ajax URL - get captcha image + getServerStatus: 'api/user/getEveServerStatus', // ajax URL - get EVE-Online server status sendInviteKey: 'api/user/sendInvite', // ajax URL - send registration key + getCookieCharacterData: 'api/user/getCookieCharacter', // ajax URL - get character data from cookie logIn: 'api/user/logIn', // ajax URL - login - logOut: 'api/user/logOut', // ajax URL - logout + logout: 'api/user/logout', // ajax URL - logout deleteLog: 'api/user/deleteLog', // ajax URL - delete character log saveUserConfig: 'api/user/saveAccount', // ajax URL - saves/update user account - saveSharingConfig: 'api/user/saveSharingConfig', // ajax URL - save "sharing settings" dialog deleteAccount: 'api/user/deleteAccount', // ajax URL - delete Account data // access API searchAccess: 'api/access/search', // ajax URL - search user/corporation/ally by name @@ -34,6 +35,7 @@ define(['jquery'], function($) { deleteSystem: 'api/system/delete', // ajax URL - delete system from map getSystemGraphData: 'api/system/graphData', // ajax URL - get all system graph data getConstellationData: 'api/system/constellationData', // ajax URL - get system constellation data + setDestination: 'api/system/setDestination', // ajax URL - set destination // connection API saveConnection: 'api/connection/save', // ajax URL - save new connection to map @@ -125,7 +127,7 @@ define(['jquery'], function($) { }, redGiant: { class: 'pf-system-effect-redgiant', - name: 'red gaint' + name: 'red giant' }, pulsar: { class: 'pf-system-effect-pulsar', @@ -329,34 +331,58 @@ define(['jquery'], function($) { // frigate wormholes frigateWormholes: { 1: { // C1 - + 1: 'E004 - C1', + 2: 'L005 - C2', + 3: 'Z006 - C3', + 4: 'M001 - C4', + 5: 'C008 - C5', + 6: 'G008 - C6', + 7: 'Q003 - 0.0' }, 2: { // C2 - 1: 'L005 - C2', - 2: 'C008 - C5', - 3: 'Q003 - 0.0' + 1: 'E004 - C1', + 2: 'L005 - C2', + 3: 'Z006 - C3', + 4: 'M001 - C4', + 5: 'C008 - C5', + 6: 'G008 - C6', + 7: 'Q003 - 0.0' }, 3: { // C3 1: 'E004 - C1', 2: 'L005 - C2', - 3: 'M001 - C4' + 3: 'Z006 - C3', + 4: 'M001 - C4', + 5: 'C008 - C5', + 6: 'G008 - C6', + 7: 'Q003 - 0.0' }, 4: { // C4 - 1: 'L005 - C2', - 2: 'G008 - C6', - 3: 'Q003 - 0.0' - }, + 1: 'E004 - C1', + 2: 'L005 - C2', + 3: 'Z006 - C3', + 4: 'M001 - C4', + 5: 'C008 - C5', + 6: 'G008 - C6', + 7: 'Q003 - 0.0' + }, 5: { // C5 1: 'E004 - C1', 2: 'L005 - C2', 3: 'Z006 - C3', - 4: 'C008 - C5', - 5: 'Q003 - 0.0' + 4: 'M001 - C4', + 5: 'C008 - C5', + 6: 'G008 - C6', + 7: 'Q003 - 0.0' }, 6: { // C6 1: 'E004 - C1', - 2: 'Z006 - C3', - 5: 'Q003 - 0.0' + 2: 'L005 - C2', + 3: 'Z006 - C3', + 4: 'M001 - C4', + 5: 'C008 - C5', + 6: 'G008 - C6', + 7: 'Q003 - 0.0' }, 13: { // Shattered Wormholes (some of them are static) 1: 'E004 - C1', @@ -365,8 +391,7 @@ define(['jquery'], function($) { 4: 'M001 - C4', 5: 'C008 - C5', 6: 'G008 - C6', - 7: 'Q003 - C7' - + 7: 'Q003 - 0.0' } }, // incoming wormholes @@ -382,4 +407,4 @@ define(['jquery'], function($) { }; return Config; -}); \ No newline at end of file +}); diff --git a/js/app/logging.js b/js/app/logging.js index b9280511..ad74c1b9 100644 --- a/js/app/logging.js +++ b/js/app/logging.js @@ -83,6 +83,7 @@ define([ autoWidth: false, hover: false, pageLength: 15, + lengthMenu: [[10, 15, 25, 50, 50], [10, 15, 25, 50, 50]], data: logData, // load cached logs (if available) language: { emptyTable: 'No entries', diff --git a/js/app/login.js b/js/app/login.js index 38f94603..40f3f305 100644 --- a/js/app/login.js +++ b/js/app/login.js @@ -27,6 +27,7 @@ define([ splashOverlayClass: 'pf-splash', // class for "splash" overlay // header + headerId: 'pf-landing-top', // id for header headerContainerId: 'pf-header-container', // id for header container logoContainerId: 'pf-logo-container', // id for main header logo container headHeaderMapId: 'pf-header-map', // id for header image (svg animation) @@ -43,11 +44,15 @@ define([ navigationLinkLicenseClass: 'pf-navbar-license', // class for "license" trigger link navigationVersionLinkClass: 'pf-navbar-version-info', // class for "version information" - // login form - loginFormId: 'pf-login-form', // id for login form - loginButtonClass: 'pf-login-button', // class for "login" button(s) - registerButtonClass: 'pf-register-button', // class for "register" button(s) - loginMessageContainerId: 'pf-login-message-container', // id for login form message container + // cookie hint + cookieHintId: 'pf-cookie-hint', // id for "cookie hint" element + + // character select + characterSelectionClass: 'pf-character-selection', // class for character panel wrapper + characterRowAnimateClass: 'pf-character-row-animate', // class for character panel row during animation + characterImageWrapperClass: 'pf-character-image-wrapper', // class for image wrapper (animated) + characterImageInfoClass: 'pf-character-info', // class for character info layer (visible on hover) + dynamicMessageContainerClass: 'pf-dynamic-message-container', // class for "dynamic" (JS) message container // gallery galleryId: 'pf-gallery', // id for gallery container @@ -55,101 +60,82 @@ define([ galleryThumbContainerId: 'pf-landing-gallery-thumb-container', // id for gallery thumb images galleryCarouselId: 'pf-landing-gallery-carousel', // id for "carousel" element + // server panel + serverPanelId: 'pf-server-panel', // id for EVE Online server status panel + // animation animateElementClass: 'pf-animate-on-visible' // class for elements that will be animated to show }; + /** + * set a cookie + * @param cname + * @param cvalue + * @param exdays + */ + var setCookie = function(cname, cvalue, exdays) { + var d = new Date(); + d.setTime(d.getTime() + (exdays*24*60*60*1000)); + var expires = 'expires=' + d.toUTCString(); + document.cookie = cname + '=' + cvalue + '; ' + expires; + }; + + /** + * get cookie value by name + * @param cname + * @returns {*} + */ + var getCookie = function(cname) { + var name = cname + '='; + var ca = document.cookie.split(';'); + + for(var i = 0; i 0 ){ form.showFormMessage(responseData.error); - - }else{ $('.modal').modal('hide'); Util.showNotify({title: 'Registration Key send', text: 'Check your Mails', type: 'success'}); @@ -288,7 +272,7 @@ define([ return newSlideContent[0]; }; - // initialize carousel ------------------------------------------ + // initialize carousel ------------------------------------------------ var carousel = new Gallery([ { title: 'IGB', @@ -411,29 +395,31 @@ define([ var initYoutube = function(){ - $(".youtube").each(function() { + $('.youtube').each(function() { // Based on the YouTube ID, we can easily find the thumbnail image $(this).css('background-image', 'url(https://i.ytimg.com/vi/' + this.id + '/sddefault.jpg)'); // Overlay the Play icon to make it look like a video player $(this).append($('
', {'class': 'play'})); - $(document).delegate('#'+this.id, 'click', function() { + $(document).delegate('#' + this.id, 'click', function() { // Create an iFrame with autoplay set to true - var iframe_url = "https://www.youtube.com/embed/" + this.id + "?autoplay=1&autohide=1"; - if ($(this).data('params')) iframe_url+='&'+$(this).data('params'); + var iFrameUrl = 'https://www.youtube.com/embed/' + this.id + '?autoplay=1&autohide=1'; + if ( $(this).data('params') ){ + iFrameUrl += '&'+$(this).data('params'); + } // The height and width of the iFrame should be the same as parent - var iframe = $('