Merge pull request #750 from exodus4d/develop

v1.5.0
This commit is contained in:
Mark Friedrich
2019-03-01 14:01:46 +01:00
committed by GitHub
187 changed files with 3792 additions and 1914 deletions

View File

@@ -14,14 +14,14 @@ DEBUG = 0
; How to behave on 'non-fatal' errors
; If TRUE, the framework, after having logged stack trace and errors, stops execution
; (die without any status) when a non-fatal error is detected.
; Tip: You should not change this.
; Hint: You should not change this.
; Syntax: TRUE | FALSE
; Default: FALSE
HALT = FALSE
; Timezone to use
; Sync Pathfinder with EVE server time.
; Tip: You should not change this.
; Hint: You should not change this.
; Default: UTC
TZ = UTC
@@ -35,26 +35,45 @@ LANGUAGE = en-US
; Cache key prefix
; Same for all cache values for this installation.
; CLI (cronjob) scripts use it for cache manipulation.
; Tip: You should not change this.
; Hint: You should not change this.
; Syntax String
; Default: {{ md5(@SERVER.SERVER_NAME) }}
SEED = {{ md5(@SERVER.SERVER_NAME) }}
; Cache backend
; This sets the primary cache backend for Pathfinder. Used for e.g.:
; DB query, DB schema, HTTP response, or even simple key->value caches
; Can handle Redis, Memcache module, APC, WinCache, XCache and a filesystem-based cache.
; Tip: Redis is recommended and gives the best performance.
; Hint: Redis is recommended and gives the best performance.
; Syntax: folder=[DIR] | redis=[SERVER]
; Default: folder=tmp/cache/
; Value: folder=[DIR]
; Value: FALSE
; - Disables caching
; folder=[DIR]
; - Cache data is stored on disc
; redis=[SERVER]
; - Cache data is stored in Redis (e.g. redis=localhost:6379)
; - Cache data is stored in Redis. redis=[host]:[port]:[db] (e.g. redis=localhost:6379:1)
CACHE = folder=tmp/cache/
; Cache backend for API data
; This sets the cache backend for API response data and other temp data relates to API requests.
; Response data with proper 'Expire' HTTP Header will be cached here and speed up further requests.
; As default 'API_CACHE' and 'CACHE' share the same backend (cache location)
; Hint1: You can specify e.g. a dedicated Redis DB here, then 'CACHE' and 'API_CACHE' can be cleared independently
; Hint2: Redis is recommended and gives the best performance.
; Default: {{@CACHE}}
; Value: FALSE
; - Disables caching
; folder=[DIR]
; - Cache data is stored on disc
; redis=[SERVER]
; - Cache data is stored in Redis. redis=[host]:[port]:[db] (e.g. redis=localhost:6379:2)
API_CACHE = {{@CACHE}}
; Cache backend used by PHPs Session handler.
; Tip1: Best performance and recommended configuration for Pathfinder is to configured Redis as PHPs default Session handler
; Hint1: Best performance and recommended configuration for Pathfinder is to configured Redis as PHPs default Session handler
; in your php.ini and set 'default' value here in order to use Redis (fastest)
; Tip2: If Redis is not available for you, leave this at 'mysql' (faster than PHPs default files bases Sessions)
; Hint2: If Redis is not available for you, leave this at 'mysql' (faster than PHPs default files bases Sessions)
; Syntax: mysql | default
; Default: mysql
; Value: mysql

View File

@@ -56,6 +56,9 @@ deleteExpiredCacheData = Cron\Cache->deleteExpiredData, @downtime
; delete old statistics (activity log) data
deleteStatisticsData = Cron\StatisticsUpdate->deleteStatisticsData, @weekly
; truncate map history log files
truncateMapHistoryLogFiles = Cron\MapHistory->truncateFiles, @halfHour
; updates small amount of static system data from CCP API
;updateUniverseSystems = Cron\Universe->updateUniverseSystems, @instant

View File

@@ -33,7 +33,7 @@ CCP_SSO_SECRET_KEY =
CCP_SSO_DOWNTIME = 11:00
; CCP ESI API
CCP_ESI_URL = https://esi.tech.ccp.is
CCP_ESI_URL = https://esi.evetech.net
CCP_ESI_DATASOURCE = singularity
CCP_ESI_SCOPES = esi-location.read_online.v1,esi-location.read_location.v1,esi-location.read_ship_type.v1,esi-ui.write_waypoint.v1,esi-ui.open_window.v1,esi-universe.read_structures.v1,esi-corporations.read_corporation_membership.v1
CCP_ESI_SCOPES_ADMIN =
@@ -86,7 +86,7 @@ CCP_SSO_SECRET_KEY =
CCP_SSO_DOWNTIME = 11:00
; CCP ESI API
CCP_ESI_URL = https://esi.tech.ccp.is
CCP_ESI_URL = https://esi.evetech.net
CCP_ESI_DATASOURCE = tranquility
CCP_ESI_SCOPES = esi-location.read_online.v1,esi-location.read_location.v1,esi-location.read_ship_type.v1,esi-ui.write_waypoint.v1,esi-ui.open_window.v1,esi-universe.read_structures.v1,esi-corporations.read_corporation_membership.v1
CCP_ESI_SCOPES_ADMIN =
@@ -103,4 +103,4 @@ SMTP_ERROR = admin@pathfinder-w.space
; TCP Socket configuration (optional) (advanced)
;SOCKET_HOST = 127.0.0.1
;SOCKET_PORT = 5555
;SOCKET_PORT = 5555

View File

@@ -1,5 +1,48 @@
CHANGELOG
3.6.5 (24 December 2018)
* NEW: Log, added timestamp to each line
* NEW: Auth, added support for custom compare method, [#116](https://github.com/bcosca/fatfree-core/issues/116)
* NEW: cache tag support for mongo & jig mapper, ref [#166](https://github.com/bcosca/fatfree-core/issues/116)
* NEW: Allow PHP functions as template token filters
* Web: Fix double redirect bug when running cURL with open_basedir disabled
* Web: Cope with responses from HTTP/2 servers
* Web->filler: remove very first space, when $std is false
* Web\OAuth2: Cope with HTTP/2 responses
* Web\OAuth2: take Content-Type header into account for json decoding, [#250](https://github.com/bcosca/fatfree-core/issues/250) [#251](https://github.com/bcosca/fatfree-core/issues/251)
* Web\OAuth2: fixed empty results on some endpoints [#250](https://github.com/bcosca/fatfree-core/issues/250)
* DB\SQL\Mapper: optimize mapper->count memory usage
* DB\SQL\Mapper: New table alias operator
* DB\SQL\Mapper: fix count() performance on non-grouped result sets, [bcosca/fatfree#1114](https://github.com/bcosca/fatfree/issues/1114)
* DB\SQL: Support for CTE in postgreSQL, [bcosca/fatfree#1107](https://github.com/bcosca/fatfree/issues/1107), [bcosca/fatfree#1116](https://github.com/bcosca/fatfree/issues/1116), [bcosca/fatfree#1021](https://github.com/bcosca/fatfree/issues/1021)
* DB\SQL->log: Remove extraneous whitespace
* DB\SQL: Added ability to add inline comments per SQL query
* CLI\WS, Refactoring: Streamline socket server
* CLI\WS: Add option for dropping query in OAuth2 URI
* CLI\WS: Add URL-safe base64 encoding
* CLI\WS: Detect errors in returned JSON values
* CLI\WS: Added support for Sec-WebSocket-Protocol header
* Matrix->calendar: Allow unix timestamp as date argument
* Basket: Access basket item by _id [#260](https://github.com/bcosca/fatfree-core/issues/260)
* SMTP: Added TLS 1.2 support [bcosca/fatfree#1115](https://github.com/bcosca/fatfree/issues/1115)
* SMTP->send: Respect $log argument
* Base->cast: recognize binary and octal numbers in config
* Base->cast: add awareness of hexadecimal literals
* Base->abort: Remove unnecessary Content-Encoding header
* Base->abort: Ensure headers have not been flushed
* Base->format: Differentiate between long- and full-date (with localized weekday) formats
* Base->format: Conform with intl extension's number output
* Enable route handler to override Access-Control headers in response to OPTIONS request, [#257](https://github.com/bcosca/fatfree-core/issues/257)
* Augment filters with a var_export function
* Bug fix php7.3: Fix template parse regex to be compatible with strict PCRE2 rules for hyphen placement in a character class
* Bug fix, Cache->set: update creation time when updating existing cache entries
* Bug fix: incorrect ICU date/time formatting
* Bug fix, Jig: lazy write on empty data
* Bug fix: Method uppercase to avoid route failure [#252](https://github.com/bcosca/fatfree-core/issues/252)
* Fixed error description when (PSR-11) `CONTAINER` fails to resolve a class [#253](https://github.com/bcosca/fatfree-core/issues/253)
* Mitigate CSRF predictability/vulnerability
* Expose Mapper->factory() method
3.6.4 (19 April 2018)
* NEW: Added Dependency Injection support with CONTAINER variable [#221](https://github.com/bcosca/fatfree-core/issues/221)
* NEW: configurable LOGGABLE error codes [#1091](https://github.com/bcosca/fatfree/issues/1091#issuecomment-364674701)

View File

@@ -2,7 +2,7 @@
/*
Copyright (c) 2009-2017 F3::Factory/Bong Cosca, All rights reserved.
Copyright (c) 2009-2018 F3::Factory/Bong Cosca, All rights reserved.
This file is part of the Fat-Free Framework (http://fatfreeframework.com).
@@ -35,7 +35,9 @@ class Auth {
//! Mapper object
$mapper,
//! Storage options
$args;
$args,
//! Custom compare function
$func;
/**
* Jig storage handler
@@ -45,22 +47,26 @@ class Auth {
* @param $realm string
**/
protected function _jig($id,$pw,$realm) {
return (bool)
$success = (bool)
call_user_func_array(
[$this->mapper,'load'],
[
array_merge(
[
'@'.$this->args['id'].'==? AND '.
'@'.$this->args['pw'].'==?'.
'@'.$this->args['id'].'==?'.
($this->func?'':' AND @'.$this->args['pw'].'==?').
(isset($this->args['realm'])?
(' AND @'.$this->args['realm'].'==?'):''),
$id,$pw
$id
],
($this->func?[]:[$pw]),
(isset($this->args['realm'])?[$realm]:[])
)
]
);
if ($success && $this->func)
$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw']));
return $success;
}
/**
@@ -71,15 +77,16 @@ class Auth {
* @param $realm string
**/
protected function _mongo($id,$pw,$realm) {
return (bool)
$success = (bool)
$this->mapper->load(
[
$this->args['id']=>$id,
$this->args['pw']=>$pw
]+
[$this->args['id']=>$id]+
($this->func?[]:[$this->args['pw']=>$pw])+
(isset($this->args['realm'])?
[$this->args['realm']=>$realm]:[])
);
if ($success && $this->func)
$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw']));
return $success;
}
/**
@@ -90,22 +97,26 @@ class Auth {
* @param $realm string
**/
protected function _sql($id,$pw,$realm) {
return (bool)
$success = (bool)
call_user_func_array(
[$this->mapper,'load'],
[
array_merge(
[
$this->args['id'].'=? AND '.
$this->args['pw'].'=?'.
$this->args['id'].'=?'.
($this->func?'':' AND '.$this->args['pw'].'=?').
(isset($this->args['realm'])?
(' AND '.$this->args['realm'].'=?'):''),
$id,$pw
$id
],
($this->func?[]:[$pw]),
(isset($this->args['realm'])?[$realm]:[])
)
]
);
if ($success && $this->func)
$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw']));
return $success;
}
/**
@@ -234,8 +245,9 @@ class Auth {
* @return object
* @param $storage string|object
* @param $args array
* @param $func callback
**/
function __construct($storage,array $args=NULL) {
function __construct($storage,array $args=NULL,$func=NULL) {
if (is_object($storage) && is_a($storage,'DB\Cursor')) {
$this->storage=$storage->dbtype();
$this->mapper=$storage;
@@ -244,6 +256,7 @@ class Auth {
else
$this->storage=$storage;
$this->args=$args;
$this->func=$func;
}
}

View File

@@ -45,7 +45,7 @@ final class Base extends Prefab implements ArrayAccess {
//@{ Framework details
const
PACKAGE='Fat-Free Framework',
VERSION='3.6.4-Release';
VERSION='3.6.5-Release';
//@}
//@{ HTTP status codes (RFC 2616)
@@ -207,11 +207,13 @@ final class Base extends Prefab implements ArrayAccess {
}
/**
* cast string variable to php type or constant
* Cast string variable to PHP type or constant
* @param $val
* @return mixed
*/
function cast($val) {
if (preg_match('/^(?:0x[0-9a-f]+|0[0-7]+|0b[01]+)$/i',$val))
return intval($val,0);
if (is_numeric($val))
return $val+0;
$val=trim($val);
@@ -241,13 +243,13 @@ final class Base extends Prefab implements ArrayAccess {
$out='['.
(isset($sub[3])?
$this->compile($sub[3]):
var_export($sub[1],TRUE)).
$this->export($sub[1])).
']';
}
else
$out=function_exists($sub[1])?
$sub[0]:
('['.var_export($sub[1],TRUE).']'.$sub[2]);
('['.$this->export($sub[1]).']'.$sub[2]);
return $out;
},
$expr[2]
@@ -680,7 +682,7 @@ final class Base extends Prefab implements ArrayAccess {
$str='';
foreach (get_object_vars($arg) as $key=>$val)
$str.=($str?',':'').
var_export($key,TRUE).'=>'.
$this->export($key).'=>'.
$this->stringify($val,
array_merge($stack,[$arg]));
return get_class($arg).'::__set_state(['.$str.'])';
@@ -690,11 +692,11 @@ final class Base extends Prefab implements ArrayAccess {
ctype_digit(implode('',array_keys($arg)));
foreach ($arg as $key=>$val)
$str.=($str?',':'').
($num?'':(var_export($key,TRUE).'=>')).
($num?'':($this->export($key).'=>')).
$this->stringify($val,array_merge($stack,[$arg]));
return '['.$str.']';
default:
return var_export($arg,TRUE);
return $this->export($arg);
}
}
@@ -890,8 +892,14 @@ final class Base extends Prefab implements ArrayAccess {
return $expr[0];
if (isset($type)) {
if (isset($this->hive['FORMATS'][$type]))
return $this->call($this->hive['FORMATS'][$type],
[$args[$pos],isset($mod)?$mod:null,isset($prop)?$prop:null]);
return $this->call(
$this->hive['FORMATS'][$type],
[
$args[$pos],
isset($mod)?$mod:null,
isset($prop)?$prop:null
]
);
switch ($type) {
case 'plural':
preg_match_all('/(?<tag>\w+)'.
@@ -964,16 +972,22 @@ final class Base extends Prefab implements ArrayAccess {
$args[$pos]*100,0,$decimal_point,
$thousands_sep).'%';
}
$frac=$args[$pos]-(int)$args[$pos];
return number_format(
$args[$pos],isset($prop)?$prop:2,
$args[$pos],
isset($prop)?
$prop:
$frac?strlen($frac)-2:0,
$decimal_point,$thousands_sep);
case 'date':
$prop='%d %B %Y';
if (empty($mod) || $mod=='short')
$prop='%x';
elseif ($mod=='long')
$prop='%A, %d %B %Y';
elseif ($mod=='full')
$prop='%A, '.$prop;
return strftime($prop,$args[$pos]);
case 'time':
$prop='%r';
if (empty($mod) || $mod=='short')
$prop='%X';
return strftime($prop,$args[$pos]);
@@ -987,6 +1001,15 @@ final class Base extends Prefab implements ArrayAccess {
);
}
/**
* Return string representation of expression
* @return string
* @param $expr mixed
**/
function export($expr) {
return var_export($expr,TRUE);
}
/**
* Assign/auto-detect language
* @return string
@@ -1244,7 +1267,8 @@ final class Base extends Prefab implements ArrayAccess {
if (!is_array($loggable))
$loggable=$this->split($loggable);
foreach ($loggable as $status)
if ($status=='*' || preg_match('/^'.preg_replace('/\D/','\d',$status).'$/',$code)) {
if ($status=='*' ||
preg_match('/^'.preg_replace('/\D/','\d',$status).'$/',$code)) {
error_log($text);
foreach (explode("\n",$trace) as $nexus)
if ($nexus)
@@ -1270,7 +1294,14 @@ final class Base extends Prefab implements ArrayAccess {
'beforeroute,afterroute')===FALSE) &&
!$prior && !$this->hive['CLI'] && !$this->hive['QUIET'])
echo $this->hive['AJAX']?
json_encode(array_diff_key($this->hive['ERROR'],$this->hive['DEBUG']?[]:['trace'=>1])):
json_encode(
array_diff_key(
$this->hive['ERROR'],
$this->hive['DEBUG']?
[]:
['trace'=>1]
)
):
('<!DOCTYPE html>'.$eol.
'<html>'.$eol.
'<head>'.
@@ -1557,7 +1588,7 @@ final class Base extends Prefab implements ArrayAccess {
$cors=$this->hive['CORS'];
header('Access-Control-Allow-Origin: '.$cors['origin']);
header('Access-Control-Allow-Credentials: '.
var_export($cors['credentials'],TRUE));
$this->export($cors['credentials']));
$preflight=
isset($this->hive['HEADERS']['Access-Control-Request-Method']);
}
@@ -1674,12 +1705,15 @@ final class Base extends Prefab implements ArrayAccess {
// URL doesn't match any route
$this->error(404);
elseif (!$this->hive['CLI']) {
// Unhandled HTTP method
header('Allow: '.implode(',',array_unique($allowed)));
if (!preg_grep('/Allow:/',$headers_send=headers_list()))
// Unhandled HTTP method
header('Allow: '.implode(',',array_unique($allowed)));
if ($cors) {
header('Access-Control-Allow-Methods: OPTIONS,'.
implode(',',$allowed));
if ($cors['headers'])
if (!preg_grep('/Access-Control-Allow-Methods:/',$headers_send))
header('Access-Control-Allow-Methods: OPTIONS,'.
implode(',',$allowed));
if ($cors['headers'] &&
!preg_grep('/Access-Control-Allow-Headers:/',$headers_send))
header('Access-Control-Allow-Headers: '.
(is_array($cors['headers'])?
implode(',',$cors['headers']):
@@ -1733,7 +1767,9 @@ final class Base extends Prefab implements ArrayAccess {
}
/**
* Disconnect HTTP client
* Disconnect HTTP client;
* Set FcgidOutputBufferSize to zero if server uses mod_fcgid;
* Disable mod_deflate when rendering text/html output
**/
function abort() {
if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
@@ -1741,9 +1777,10 @@ final class Base extends Prefab implements ArrayAccess {
$out='';
while (ob_get_level())
$out=ob_get_clean().$out;
header('Content-Encoding: none');
header('Content-Length: '.strlen($out));
header('Connection: close');
if (!headers_sent()) {
header('Content-Length: '.strlen($out));
header('Connection: close');
}
session_commit();
echo $out;
flush();
@@ -1771,11 +1808,13 @@ final class Base extends Prefab implements ArrayAccess {
$parts[1]=call_user_func([$container,'get'],$parts[1]);
elseif (is_callable($container))
$parts[1]=call_user_func($container,$parts[1],$args);
elseif (is_string($container) && is_subclass_of($container,'Prefab'))
$parts[1]=call_user_func($container.'::instance')->get($parts[1]);
elseif (is_string($container) &&
is_subclass_of($container,'Prefab'))
$parts[1]=call_user_func($container.'::instance')->
get($parts[1]);
else
user_error(sprintf(self::E_Class,
$this->stringify($container)),
$this->stringify($parts[1])),
E_USER_ERROR);
}
else {
@@ -1916,7 +1955,8 @@ final class Base extends Prefab implements ArrayAccess {
call_user_func_array(
[$this,$cmd[1]],
array_merge([$match['lval']],
str_getcsv($cmd[1]=='config'?$this->cast($match['rval']):
str_getcsv($cmd[1]=='config'?
$this->cast($match['rval']):
$match['rval']))
);
}
@@ -1931,9 +1971,11 @@ final class Base extends Prefab implements ArrayAccess {
$args=array_map(
function($val) {
$val=$this->cast($val);
return is_string($val)
? preg_replace('/\\\\"/','"',$val)
: $val;
if (is_string($val))
$val=strlen($val)?
preg_replace('/\\\\"/','"',$val):
NULL;
return $val;
},
// Mark quoted strings with 0x00 whitespace
str_getcsv(preg_replace(
@@ -2221,6 +2263,7 @@ final class Base extends Prefab implements ArrayAccess {
);
if (!isset($_SERVER['SERVER_NAME']) || $_SERVER['SERVER_NAME']==='')
$_SERVER['SERVER_NAME']=gethostname();
$headers=[];
if ($cli=PHP_SAPI=='cli') {
// Emulate HTTP request
$_SERVER['REQUEST_METHOD']='GET';
@@ -2251,33 +2294,30 @@ final class Base extends Prefab implements ArrayAccess {
$_SERVER['REQUEST_URI']=$req;
parse_str($query,$GLOBALS['_GET']);
}
$headers=[];
if (!$cli) {
if (function_exists('getallheaders')) {
foreach (getallheaders() as $key=>$val) {
$tmp=strtoupper(strtr($key,'-','_'));
// TODO: use ucwords delimiters for php 5.4.32+ & 5.5.16+
$key=strtr(ucwords(strtolower(strtr($key,'-',' '))),' ','-');
$headers[$key]=$val;
if (isset($_SERVER['HTTP_'.$tmp]))
$headers[$key]=&$_SERVER['HTTP_'.$tmp];
}
}
else {
if (isset($_SERVER['CONTENT_LENGTH']))
$headers['Content-Length']=&$_SERVER['CONTENT_LENGTH'];
if (isset($_SERVER['CONTENT_TYPE']))
$headers['Content-Type']=&$_SERVER['CONTENT_TYPE'];
foreach (array_keys($_SERVER) as $key)
if (substr($key,0,5)=='HTTP_')
$headers[strtr(ucwords(strtolower(strtr(
substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key];
elseif (function_exists('getallheaders')) {
foreach (getallheaders() as $key=>$val) {
$tmp=strtoupper(strtr($key,'-','_'));
// TODO: use ucwords delimiters for php 5.4.32+ & 5.5.16+
$key=strtr(ucwords(strtolower(strtr($key,'-',' '))),' ','-');
$headers[$key]=$val;
if (isset($_SERVER['HTTP_'.$tmp]))
$headers[$key]=&$_SERVER['HTTP_'.$tmp];
}
}
else {
if (isset($_SERVER['CONTENT_LENGTH']))
$headers['Content-Length']=&$_SERVER['CONTENT_LENGTH'];
if (isset($_SERVER['CONTENT_TYPE']))
$headers['Content-Type']=&$_SERVER['CONTENT_TYPE'];
foreach (array_keys($_SERVER) as $key)
if (substr($key,0,5)=='HTTP_')
$headers[strtr(ucwords(strtolower(strtr(
substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key];
}
if (isset($headers['X-HTTP-Method-Override']))
$_SERVER['REQUEST_METHOD']=$headers['X-HTTP-Method-Override'];
elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method']))
$_SERVER['REQUEST_METHOD']=$_POST['_method'];
$_SERVER['REQUEST_METHOD']=strtoupper($_POST['_method']);
$scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' ||
isset($headers['X-Forwarded-Proto']) &&
$headers['X-Forwarded-Proto']=='https'?'https':'http';
@@ -2493,10 +2533,9 @@ class Cache extends Prefab {
if (!$this->dsn)
return TRUE;
$ndx=$this->prefix.'.'.$key;
$time=microtime(TRUE);
if ($cached=$this->exists($key))
list($time,$ttl)=$cached;
$data=$fw->serialize([$val,$time,$ttl]);
$ttl=$cached[1];
$data=$fw->serialize([$val,microtime(TRUE),$ttl]);
$parts=explode('=',$this->dsn,2);
switch ($parts[0]) {
case 'apc':
@@ -2513,7 +2552,8 @@ class Cache extends Prefab {
case 'xcache':
return xcache_set($ndx,$data,$ttl);
case 'folder':
return $fw->write($parts[1].str_replace(['/','\\'],'',$ndx),$data);
return $fw->write($parts[1].
str_replace(['/','\\'],'',$ndx),$data);
}
return FALSE;
}
@@ -2824,6 +2864,7 @@ class Preview extends View {
'c'=>'$this->c',
'esc'=>'$this->esc',
'raw'=>'$this->raw',
'export'=>'Base::instance()->export',
'alias'=>'Base::instance()->alias',
'format'=>'Base::instance()->format'
];
@@ -2833,7 +2874,7 @@ class Preview extends View {
$interpolation=true;
/**
* enable/disable markup parsing interpolation
* Enable/disable markup parsing interpolation
* mainly used for adding appropriate newlines
* @param $bool bool
*/
@@ -2867,16 +2908,18 @@ class Preview extends View {
$str,$parts)) {
$str=trim($parts[1]);
foreach ($fw->split(trim($parts[2],"\xC2\xA0")) as $func)
$str=is_string($cmd=$this->filter($func))?
$str=((empty($this->filter[$cmd=$func]) &&
function_exists($cmd)) ||
is_string($cmd=$this->filter($func)))?
$cmd.'('.$str.')':
'Base::instance()->'.
'call($this->filter(\''.$func.'\'),['.$str.'])';
'call($this->filter(\''.$func.'\'),['.$str.'])';
}
return $str;
}
/**
* Register or get (a specific one or all) token filters
* Register or get (one specific or all) token filters
* @param string $key
* @param string|closure $func
* @return array|closure|string

View File

@@ -90,7 +90,8 @@ class Basket extends Magic {
if (isset($_SESSION[$this->key])) {
foreach ($_SESSION[$this->key] as $id=>$item)
if (!isset($key) ||
array_key_exists($key,$item) && $item[$key]==$val) {
array_key_exists($key,$item) && $item[$key]==$val ||
$key=='_id' && $id==$val) {
$obj=clone($this);
$obj->id=$id;
$obj->item=$item;

View File

@@ -52,6 +52,7 @@ class WS {
$ctx,
$wait,
$sockets,
$protocol,
$agents=[],
$events=[];
@@ -61,16 +62,14 @@ class WS {
* @param $socket resource
**/
function alloc($socket) {
if (is_bool($str=$this->read($socket))) {
$this->close($socket);
if (is_bool($buf=$this->read($socket)))
return;
}
// Get WebSocket headers
$hdrs=[];
$CRLF="\r\n";
$EOL="\r\n";
$verb=NULL;
$uri=NULL;
foreach (explode($CRLF,trim($str)) as $line)
foreach (explode($EOL,trim($buf)) as $line)
if (preg_match('/^(\w+)\s(.+)\sHTTP\/1\.\d$/',
trim($line),$match)) {
$verb=$match[1];
@@ -98,35 +97,29 @@ class WS {
if ($verb && $uri)
$this->write(
$socket,
$str='HTTP/1.1 400 Bad Request'.$CRLF.
'Connection: close'.$CRLF.$CRLF
'HTTP/1.1 400 Bad Request'.$EOL.
'Connection: close'.$EOL.$EOL
);
$this->close($socket);
return;
}
// Handshake
$bytes=$this->write(
$socket,
$str='HTTP/1.1 101 Switching Protocols'.$CRLF.
'Upgrade: websocket'.$CRLF.
'Connection: Upgrade'.$CRLF.
'Sec-WebSocket-Accept: '.
base64_encode(
sha1(
$hdrs['Sec-Websocket-Key'].
self::Magic,
TRUE
)
).$CRLF.$CRLF
);
if ($bytes) {
$buf='HTTP/1.1 101 Switching Protocols'.$EOL.
'Upgrade: websocket'.$EOL.
'Connection: Upgrade'.$EOL;
if (isset($hdrs['Sec-Websocket-Protocol']))
$buf.='Sec-WebSocket-Protocol: '.
$hdrs['Sec-Websocket-Protocol'].$EOL;
$buf.='Sec-WebSocket-Accept: '.
base64_encode(
sha1($hdrs['Sec-Websocket-Key'].WS::Magic,TRUE)
).$EOL.$EOL;
if ($this->write($socket,$buf)) {
// Connect agent to server
$this->sockets[]=$socket;
$this->sockets[(int)$socket]=$socket;
$this->agents[(int)$socket]=
new Agent($this,$socket,$verb,$uri,$hdrs);
}
else
$this->close($socket);
}
/**
@@ -135,34 +128,26 @@ class WS {
* @param $socket resource
**/
function close($socket) {
if (isset($this->agents[(int)$socket]))
unset($this->sockets[(int)$socket],$this->agents[(int)$socket]);
stream_socket_shutdown($socket,STREAM_SHUT_WR);
@fclose($socket);
}
/**
* Free stream socket
* @return bool
* @param $socket resource
**/
function free($socket) {
unset($this->sockets[array_search($socket,$this->sockets)]);
unset($this->agents[(int)$socket]);
$this->close($socket);
}
/**
* Read from stream socket
* @return string|FALSE
* @param $socket resource
**/
function read($socket) {
if (is_string($str=@fread($socket,self::Packet)) &&
strlen($str) &&
strlen($str)<self::Packet)
return $str;
if (is_string($buf=@fread($socket,WS::Packet)) &&
strlen($buf) &&
strlen($buf)<WS::Packet)
return $buf;
if (isset($this->events['error']) &&
is_callable($func=$this->events['error']))
$func($this);
$this->close($socket);
return FALSE;
}
@@ -170,16 +155,17 @@ class WS {
* Write to stream socket
* @return int|FALSE
* @param $socket resource
* @param $str string
* @param $buf string
**/
function write($socket,$str) {
for ($i=0,$bytes=0;$i<strlen($str);$i+=$bytes) {
if (($bytes=@fwrite($socket,substr($str,$i))) &&
function write($socket,$buf) {
for ($i=0,$bytes=0;$i<strlen($buf);$i+=$bytes) {
if (($bytes=@fwrite($socket,substr($buf,$i))) &&
@fflush($socket))
continue;
if (isset($this->events['error']) &&
is_callable($func=$this->events['error']))
$func($this);
$this->close($socket);
return FALSE;
}
return $bytes;
@@ -248,7 +234,7 @@ class WS {
register_shutdown_function(function() use($listen) {
foreach ($this->sockets as $socket)
if ($socket!=$listen)
$this->free($socket);
$this->close($socket);
$this->close($listen);
if (isset($this->events['stop']) &&
is_callable($func=$this->events['stop']))
@@ -259,7 +245,7 @@ class WS {
if (isset($this->events['start']) &&
is_callable($func=$this->events['start']))
$func($this);
$this->sockets=[$listen];
$this->sockets=[(int)$listen=>$listen];
$empty=[];
$wait=$this->wait;
while (TRUE) {
@@ -289,26 +275,8 @@ class WS {
}
else {
$id=(int)$socket;
if (isset($this->agents[$id]) &&
$raw=$this->agents[$id]->fetch()) {
list($op,$data)=$raw;
// Dispatch
switch ($op & self::OpCode) {
case self::Ping:
$this->agents[$id]->send(self::Pong);
break;
case self::Close:
$this->free($socket);
break;
case self::Text:
$data=trim($data);
case self::Binary:
if (isset($this->events['receive']) &&
is_callable($func=$this->events['receive']))
$func($this->agents[$id],$op,$data);
break;
}
}
if (isset($this->agents[$id]))
$this->agents[$id]->fetch();
}
}
$wait-=microtime(TRUE)-$mark;
@@ -319,10 +287,9 @@ class WS {
}
if (!$count) {
$mark=microtime(TRUE);
foreach ($this->sockets as $socket) {
foreach ($this->sockets as $id=>$socket) {
if (!is_resource($socket))
continue;
$id=(int)$socket;
if ($socket!=$listen &&
isset($this->agents[$id]) &&
isset($this->events['idle']) &&
@@ -362,8 +329,7 @@ class Agent {
$verb,
$uri,
$headers,
$events,
$buffer;
$events;
/**
* Return server instance
@@ -381,6 +347,14 @@ class Agent {
return $this->id;
}
/**
* Return socket
* @return object
**/
function socket() {
return $this->socket;
}
/**
* Return request method
* @return string
@@ -413,22 +387,20 @@ class Agent {
* @param $payload string
**/
function send($op,$data='') {
$server=$this->server;
$mask=WS::Finale | $op & WS::OpCode;
$len=strlen($data);
$str='';
$buf='';
if ($len>0xffff)
$str=pack('CCNN',$mask,0x7f,$len);
$buf=pack('CCNN',$mask,0x7f,$len);
else
if ($len>0x7d)
$str=pack('CCn',$mask,0x7e,$len);
$buf=pack('CCn',$mask,0x7e,$len);
else
$str=pack('CC',$mask,$len);
$str.=$data;
$server=$this->server();
if (is_bool($server->write($this->socket,$str))) {
$this->free();
$buf=pack('CC',$mask,$len);
$buf.=$data;
if (is_bool($server->write($this->socket,$buf)))
return FALSE;
}
if (!in_array($op,[WS::Pong,WS::Close]) &&
isset($this->events['send']) &&
is_callable($func=$this->events['send']))
@@ -442,12 +414,9 @@ class Agent {
**/
function fetch() {
// Unmask payload
$server=$this->server();
if (is_bool($buf=$server->read($this->socket))) {
$this->free();
$server=$this->server;
if (is_bool($buf=$server->read($this->socket)))
return FALSE;
}
$buf=($this->buffer.=$buf);
$op=ord($buf[0]) & WS::OpCode;
$len=ord($buf[1]) & WS::Length;
$pos=2;
@@ -468,18 +437,25 @@ class Agent {
return FALSE;
for ($i=0,$data='';$i<$len;$i++)
$data.=chr(ord($buf[$pos+$i])^$mask[$i%4]);
$this->buffer='';
// Dispatch
switch ($op & WS::OpCode) {
case WS::Ping:
$this->send(WS::Pong);
break;
case WS::Close:
$server->close($this->socket);
break;
case WS::Text:
$data=trim($data);
case WS::Binary:
if (isset($this->events['receive']) &&
is_callable($func=$this->events['receive']))
$func($this,$op,$data);
break;
}
return [$op,$data];
}
/**
* Free stream socket
* @return NULL
**/
function free() {
$this->server->free($this->socket);
}
/**
* Destroy object
* @return NULL
@@ -507,7 +483,6 @@ class Agent {
$this->uri=$uri;
$this->headers=$hdrs;
$this->events=$server->events();
$this->buffer='';
if (isset($this->events['connect']) &&
is_callable($func=$this->events['connect']))
$func($this);

View File

@@ -167,7 +167,7 @@ class Jig {
function __destruct() {
if ($this->lazy) {
$this->lazy = FALSE;
foreach ($this->data as $file => $data)
foreach ($this->data?:[] as $file => $data)
$this->write($file,$data);
}
}

View File

@@ -93,7 +93,7 @@ class Mapper extends \DB\Cursor {
* @param $id string
* @param $row array
**/
protected function factory($id,$row) {
function factory($id,$row) {
$mapper=clone($this);
$mapper->reset();
$mapper->id=$id;
@@ -153,7 +153,7 @@ class Mapper extends \DB\Cursor {
* @return static[]|FALSE
* @param $filter array
* @param $options array
* @param $ttl int
* @param $ttl int|array
* @param $log bool
**/
function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) {
@@ -170,9 +170,12 @@ class Mapper extends \DB\Cursor {
$db=$this->db;
$now=microtime(TRUE);
$data=[];
$tag='';
if (is_array($ttl))
list($ttl,$tag)=$ttl;
if (!$fw->CACHE || !$ttl || !($cached=$cache->exists(
$hash=$fw->hash($this->db->dir().
$fw->stringify([$filter,$options])).'.jig',$data)) ||
$fw->stringify([$filter,$options])).($tag?'.'.$tag:'').'.jig',$data)) ||
$cached[0]+$ttl<microtime(TRUE)) {
$data=$db->read($this->file);
if (is_null($data))
@@ -347,7 +350,7 @@ class Mapper extends \DB\Cursor {
* @return int
* @param $filter array
* @param $options array
* @param $ttl int
* @param $ttl int|array
**/
function count($filter=NULL,array $options=NULL,$ttl=0) {
$now=microtime(TRUE);

View File

@@ -180,7 +180,11 @@ class Session extends Mapper {
register_shutdown_function('session_commit');
$fw=\Base::instance();
$headers=$fw->HEADERS;
$this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand());
$this->_csrf=$fw->hash($fw->SEED.
extension_loaded('openssl')?
implode(unpack('L',openssl_random_pseudo_bytes(4))):
mt_rand()
);
if ($key)
$fw->$key=$this->_csrf;
$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:'';

View File

@@ -91,7 +91,7 @@ class Mapper extends \DB\Cursor {
* @return static
* @param $row array
**/
protected function factory($row) {
function factory($row) {
$mapper=clone($this);
$mapper->reset();
foreach ($row as $key=>$val)
@@ -119,7 +119,7 @@ class Mapper extends \DB\Cursor {
* @param $fields string
* @param $filter array
* @param $options array
* @param $ttl int
* @param $ttl int|array
**/
function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) {
if (!$options)
@@ -130,10 +130,13 @@ class Mapper extends \DB\Cursor {
'limit'=>0,
'offset'=>0
];
$tag='';
if (is_array($ttl))
list($ttl,$tag)=$ttl;
$fw=\Base::instance();
$cache=\Cache::instance();
if (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn().
$fw->stringify([$fields,$filter,$options])).'.mongo',
$fw->stringify([$fields,$filter,$options])).($tag?'.'.$tag:'').'.mongo',
$result)) || !$ttl || $cached[0]+$ttl<microtime(TRUE)) {
if ($options['group']) {
$grp=$this->collection->group(
@@ -194,7 +197,7 @@ class Mapper extends \DB\Cursor {
* @return static[]
* @param $filter array
* @param $options array
* @param $ttl int
* @param $ttl int|array
**/
function find($filter=NULL,array $options=NULL,$ttl=0) {
if (!$options)
@@ -213,13 +216,16 @@ class Mapper extends \DB\Cursor {
* @return int
* @param $filter array
* @param $options array
* @param $ttl int
* @param $ttl int|array
**/
function count($filter=NULL,array $options=NULL,$ttl=0) {
$fw=\Base::instance();
$cache=\Cache::instance();
$tag='';
if (is_array($ttl))
list($ttl,$tag)=$ttl;
if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify(
[$filter])).'.mongo',$result)) || !$ttl ||
[$filter])).($tag?'.'.$tag:'').'.mongo',$result)) || !$ttl ||
$cached[0]+$ttl<microtime(TRUE)) {
$result=$this->collection->count($filter?:[]);
if ($fw->CACHE && $ttl)

View File

@@ -180,7 +180,11 @@ class Session extends Mapper {
register_shutdown_function('session_commit');
$fw=\Base::instance();
$headers=$fw->HEADERS;
$this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand());
$this->_csrf=$fw->hash($fw->SEED.
extension_loaded('openssl')?
implode(unpack('L',openssl_random_pseudo_bytes(4))):
mt_rand()
);
if ($key)
$fw->$key=$this->_csrf;
$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:'';

View File

@@ -220,7 +220,7 @@ class SQL {
'/';
}
if ($log)
$this->log.=($stamp?(date('r').' '):'').' (-0ms) '.
$this->log.=($stamp?(date('r').' '):'').'(-0ms) '.
preg_replace($keys,$vals,
str_replace('?',chr(0).'?',$cmd),1).PHP_EOL;
$query->execute();
@@ -235,7 +235,7 @@ class SQL {
user_error('PDOStatement: '.$error[2],E_USER_ERROR);
}
if (preg_match('/(?:^[\s\(]*'.
'(?:EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\b/is',$cmd) ||
'(?:WITH|EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\b/is',$cmd) ||
(preg_match('/^\s*(?:CALL|EXEC)\b/is',$cmd) &&
$query->columnCount())) {
$result=$query->fetchall(\PDO::FETCH_ASSOC);

View File

@@ -34,6 +34,8 @@ class Mapper extends \DB\Cursor {
$source,
//! SQL table (quoted)
$table,
//! Alias for SQL table
$as,
//! Last insert ID
$_id,
//! Defined fields
@@ -156,7 +158,7 @@ class Mapper extends \DB\Cursor {
* @return static
* @param $row array
**/
protected function factory($row) {
function factory($row) {
$mapper=clone($this);
$mapper->reset();
foreach ($row as $key=>$val) {
@@ -207,10 +209,13 @@ class Mapper extends \DB\Cursor {
'group'=>NULL,
'order'=>NULL,
'limit'=>0,
'offset'=>0
'offset'=>0,
'comment'=>NULL
];
$db=$this->db;
$sql='SELECT '.$fields.' FROM '.$this->table;
if (isset($this->as))
$sql.=' AS '.$this->db->quotekey($this->as);
$args=[];
if (is_array($filter)) {
$args=isset($filter[1]) && is_array($filter[1])?
@@ -237,9 +242,10 @@ class Mapper extends \DB\Cursor {
}
if ($options['order']) {
$char=substr($db->quotekey(''),0,1);// quoting char
$order=' ORDER BY '.(FALSE===strpos($options['order'],$char)?
$order=' ORDER BY '.(is_bool(strpos($options['order'],$char))?
implode(',',array_map(function($str) use($db) {
return preg_match('/^\h*(\w+[._\-\w]*)(?:\h+((?:ASC|DESC)[\w\h]*))?\h*$/i',
return preg_match('/^\h*(\w+[._\-\w]*)'.
'(?:\h+((?:ASC|DESC)[\w\h]*))?\h*$/i',
$str,$parts)?
($db->quotekey($parts[1]).
(isset($parts[2])?(' '.$parts[2]):'')):$str;
@@ -281,6 +287,8 @@ class Mapper extends \DB\Cursor {
if ($options['offset'])
$sql.=' OFFSET '.(int)$options['offset'];
}
if ($options['comment'])
$sql.="\n".' /* '.$options['comment'].' */';
return [$sql,$args];
}
@@ -345,19 +353,30 @@ class Mapper extends \DB\Cursor {
* @param $ttl int|array
**/
function count($filter=NULL,array $options=NULL,$ttl=0) {
$adhoc='';
if (!($subquery_mode=($options && !empty($options['group']))))
$this->adhoc['_rows']=['expr'=>'COUNT(*)','value'=>NULL];
$adhoc=[];
foreach ($this->adhoc as $key=>$field)
$adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key);
$fields='*'.$adhoc;
if (preg_match('/mssql|dblib|sqlsrv/',$this->engine))
$fields='TOP 100 PERCENT '.$fields;
// Add all adhoc fields
// (make them available for grouping, sorting, having)
$adhoc[]=$field['expr'].' AS '.$this->db->quotekey($key);
$fields=implode(',',$adhoc);
if ($subquery_mode) {
if (empty($fields))
// Select at least one field, ideally the grouping fields
// or sqlsrv fails
$fields=preg_replace('/HAVING.+$/i','',$options['group']);
if (preg_match('/mssql|dblib|sqlsrv/',$this->engine))
$fields='TOP 100 PERCENT '.$fields;
}
list($sql,$args)=$this->stringify($fields,$filter,$options);
$sql='SELECT COUNT(*) AS '.$this->db->quotekey('_rows').' '.
'FROM ('.$sql.') AS '.$this->db->quotekey('_temp');
if ($subquery_mode)
$sql='SELECT COUNT(*) AS '.$this->db->quotekey('_rows').' '.
'FROM ('.$sql.') AS '.$this->db->quotekey('_temp');
$result=$this->db->exec($sql,$args,$ttl);
unset($this->adhoc['_rows']);
return (int)$result[0]['_rows'];
}
/**
* Return record at specified offset using same criteria as
* previous load() call and make it active
@@ -651,6 +670,15 @@ class Mapper extends \DB\Cursor {
return new \ArrayIterator($this->cast());
}
/**
* Assign alias for table
* @param $alias string
**/
function alias($alias) {
$this->as=$alias;
return $this;
}
/**
* Instantiate class
* @param $db \DB\SQL

View File

@@ -204,7 +204,11 @@ class Session extends Mapper {
register_shutdown_function('session_commit');
$fw=\Base::instance();
$headers=$fw->HEADERS;
$this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand());
$this->_csrf=$fw->hash($fw->SEED.
extension_loaded('openssl')?
implode(unpack('L',openssl_random_pseudo_bytes(4))):
mt_rand()
);
if ($key)
$fw->$key=$this->_csrf;
$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:'';

View File

@@ -35,14 +35,15 @@ class Log {
**/
function write($text,$format='r') {
$fw=Base::instance();
$fw->write(
$this->file,
date($format).
(isset($_SERVER['REMOTE_ADDR'])?
(' ['.$_SERVER['REMOTE_ADDR'].']'):'').' '.
trim($text).PHP_EOL,
TRUE
);
foreach (preg_split('/\r?\n|\r/',trim($text)) as $line)
$fw->write(
$this->file,
date($format).
(isset($_SERVER['REMOTE_ADDR'])?
(' ['.$_SERVER['REMOTE_ADDR'].']'):'').' '.
trim($line).PHP_EOL,
TRUE
);
}
/**

View File

@@ -92,13 +92,15 @@ class Matrix extends Prefab {
* Return month calendar of specified date, with optional setting for
* first day of week (0 for Sunday)
* @return array
* @param $date string
* @param $date string|int
* @param $first int
**/
function calendar($date='now',$first=0) {
$out=FALSE;
if (extension_loaded('calendar')) {
$parts=getdate(strtotime($date));
if (is_string($date))
$date=strtotime($date);
$parts=getdate($date);
$days=cal_days_in_month(CAL_GREGORIAN,$parts['mon'],$parts['year']);
$ref=date('w',strtotime(date('Y-m',$parts[0]).'-01'))+(7-$first)%7;
$out=[];

View File

@@ -182,7 +182,11 @@ class Session {
register_shutdown_function('session_commit');
$fw=\Base::instance();
$headers=$fw->HEADERS;
$this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand());
$this->_csrf=$fw->hash($fw->SEED.
extension_loaded('openssl')?
implode(unpack('L',openssl_random_pseudo_bytes(4))):
mt_rand()
);
if ($key)
$fw->$key=$this->_csrf;
$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:'';

View File

@@ -204,14 +204,19 @@ class SMTP extends Magic {
stream_set_blocking($socket,TRUE);
}
// Get server's initial response
$this->dialog(NULL,TRUE,$mock);
$this->dialog(NULL,$log,$mock);
// Announce presence
$reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock);
if (strtolower($this->scheme)=='tls') {
$this->dialog('STARTTLS',$log,$mock);
if (!$mock)
stream_socket_enable_crypto(
$socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT);
if (!$mock) {
$method=STREAM_CRYPTO_METHOD_TLS_CLIENT;
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
$method|=STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
$method|=STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
}
stream_socket_enable_crypto($socket,TRUE,$method);
}
$reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock);
}
$message=wordwrap($message,998);

View File

@@ -273,7 +273,7 @@ class Template extends Preview {
// Build tree structure
for ($ptr=0,$w=5,$len=strlen($text),$tree=[],$tmp='';$ptr<$len;)
if (preg_match('/^(.{0,'.$w.'}?)<(\/?)(?:F3:)?'.
'('.$this->tags.')\b((?:\s+[\w-.:@!]+'.
'('.$this->tags.')\b((?:\s+[\w.:@!-]+'.
'(?:\h*=\h*(?:"(?:.*?)"|\'(?:.*?)\'))?|'.
'\h*\{\{.+?\}\})*)\h*(\/?)>/is',
substr($text,$ptr),$match)) {

View File

@@ -269,7 +269,7 @@ class Web extends Prefab {
**/
protected function _curl($url,$options) {
$curl=curl_init($url);
if (!ini_get('open_basedir'))
if (!$open_basedir=ini_get('open_basedir'))
curl_setopt($curl,CURLOPT_FOLLOWLOCATION,
$options['follow_location']);
curl_setopt($curl,CURLOPT_MAXREDIRS,
@@ -306,7 +306,7 @@ class Web extends Prefab {
curl_close($curl);
$body=ob_get_clean();
if (!$err &&
$options['follow_location'] &&
$options['follow_location'] && $open_basedir &&
preg_grep('/HTTP\/1\.\d 3\d{2}/',$headers) &&
preg_match('/^Location: (.+)$/m',implode(PHP_EOL,$headers),$loc)) {
$options['max_redirects']--;
@@ -350,7 +350,7 @@ class Web extends Prefab {
if (is_string($body)) {
$match=NULL;
foreach ($headers as $header)
if (preg_match('/Content-Encoding: (.+)/',$header,$match))
if (preg_match('/Content-Encoding: (.+)/i',$header,$match))
break;
if ($match)
switch ($match[1]) {
@@ -442,7 +442,7 @@ class Web extends Prefab {
$headers=array_merge($headers,$current=explode($eol,$html[0]));
$match=NULL;
foreach ($current as $header)
if (preg_match('/Content-Encoding: (.+)/',$header,$match))
if (preg_match('/Content-Encoding: (.+)/i',$header,$match))
break;
if ($match)
switch ($match[1]) {
@@ -550,7 +550,7 @@ class Web extends Prefab {
);
if (isset($options['content']) && is_string($options['content'])) {
if ($options['method']=='POST' &&
!preg_grep('/^Content-Type:/',$options['header']))
!preg_grep('/^Content-Type:/i',$options['header']))
$this->subst($options['header'],
'Content-Type: application/x-www-form-urlencoded');
$this->subst($options['header'],
@@ -588,7 +588,7 @@ class Web extends Prefab {
$result['cached']=TRUE;
}
elseif (preg_match('/Cache-Control:(?:.*)max-age=(\d+)(?:,?.*'.
preg_quote($eol).')/',implode($eol,$result['headers']),$exp))
preg_quote($eol).')/i',implode($eol,$result['headers']),$exp))
$cache->set($hash,$result,$exp[1]);
}
$req=[$options['method'].' '.$url];
@@ -903,7 +903,7 @@ class Web extends Prefab {
for ($i=0,$add=$count-(int)$std;$i<$add;$i++) {
shuffle($rnd);
$words=array_slice($rnd,0,mt_rand(3,$max));
$out.=' '.ucfirst(implode(' ',$words)).'.';
$out.=(!$std&&$i==0?'':' ').ucfirst(implode(' ',$words)).'.';
}
return $out;
}

View File

@@ -64,8 +64,11 @@ class Geo extends \Prefab {
$out=@geoip_record_by_name($ip)) {
$out['request']=$ip;
$out['region_code']=$out['region'];
$out['region_name']=(!empty($out['country_code']) && !empty($out['region']))
? geoip_region_name_by_code($out['country_code'],$out['region']) : '';
$out['region_name']='';
if (!empty($out['country_code']) && !empty($out['region']))
$out['region_name']=geoip_region_name_by_code(
$out['country_code'],$out['region']
);
unset($out['country_code3'],$out['region'],$out['postal_code']);
return $out;
}

View File

@@ -33,9 +33,10 @@ class OAuth2 extends \Magic {
* Return OAuth2 authentication URI
* @return string
* @param $endpoint string
* @param $query bool
**/
function uri($endpoint) {
return $endpoint.'?'.http_build_query($this->args);
function uri($endpoint,$query=TRUE) {
return $endpoint.($query?('?'.http_build_query($this->args)):'');
}
/**
@@ -54,7 +55,7 @@ class OAuth2 extends \Magic {
];
if ($token)
array_push($options['header'],'Authorization: Bearer '.$token);
elseif ($method=='POST')
elseif ($method=='POST' && isset($this->args['client_id']))
array_push($options['header'],'Authorization: Basic '.
base64_encode(
$this->args['client_id'].':'.
@@ -64,10 +65,20 @@ class OAuth2 extends \Magic {
$response=$web->request($uri,$options);
if ($response['error'])
user_error($response['error'],E_USER_ERROR);
return $response['body'] &&
preg_grep('/HTTP\/1\.\d 200/',$response['headers'])?
json_decode($response['body'],TRUE):
FALSE;
if (isset($response['body'])) {
if (preg_grep('/^Content-Type:.*application\/json/i',
$response['headers'])) {
$token=json_decode($response['body'],TRUE);
if (isset($token['error_description']))
user_error($token['error_description'],E_USER_ERROR);
if (isset($token['error']))
user_error($token['error'],E_USER_ERROR);
return $token;
}
else
return $response['body'];
}
return FALSE;
}
/**
@@ -78,16 +89,21 @@ class OAuth2 extends \Magic {
function jwt($token) {
return json_decode(
base64_decode(
str_replace(
['-','_'],
['+','/'],
explode('.',$token)[1]
)
str_replace(['-','_'],['+','/'],explode('.',$token)[1])
),
TRUE
);
}
/**
* URL-safe base64 encoding
* @return array
* @param $data string
**/
function b64url($data) {
return trim(strtr(base64_encode($data),'+/','-_'),'=');
}
/**
* Return TRUE if scope/claim exists
* @return bool

View File

@@ -26,7 +26,7 @@ class AccessController extends Controller {
if($return = parent::beforeroute($f3, $params)){
// Any route/endpoint of a child class of this one,
// requires a valid logged in user!
if( !$this->isLoggedIn($f3) ){
if($this->isLoggedIn($f3) !== 'OK'){
// no character found or login timer expired
$this->logoutCharacter($f3);
// skip route handler and afterroute()
@@ -40,51 +40,40 @@ class AccessController extends Controller {
/**
* get current character and check if it is a valid character
* @param \Base $f3
* @return bool
* @return string
* @throws \Exception
*/
protected function isLoggedIn(\Base $f3): bool {
$loginCheck = false;
if( $character = $this->getCharacter() ){
if($this->checkLogTimer($f3, $character)){
if($character->isAuthorized() === 'OK'){
$loginCheck = true;
protected function isLoggedIn(\Base $f3): string {
$loginStatus = 'UNKNOWN';
if($character = $this->getCharacter()){
if($character->checkLoginTimer()){
if(( $authStatus = $character->isAuthorized()) === 'OK'){
$loginStatus = 'OK';
}else{
$loginStatus = $authStatus;
}
}else{
$loginStatus = 'MAX LOGIN TIME EXCEEDED';
}
}else{
$loginStatus = 'NO SESSION FOUND';
}
return $loginCheck;
}
/**
* checks whether a user/character is currently logged in
* @param \Base $f3
* @param Model\CharacterModel $character
* @return bool
*/
private function checkLogTimer(\Base $f3, Model\CharacterModel $character){
$loginCheck = false;
// log character access status in debug mode
if(
!$character->dry() &&
$character->lastLogin
$loginStatus !== 'OK' &&
$f3->get('DEBUG') === 3
){
// check logIn time
$logInTime = new \DateTime($character->lastLogin);
$now = new \DateTime();
$timeDiff = $now->diff($logInTime);
$minutes = $timeDiff->days * 60 * 24 * 60;
$minutes += $timeDiff->h * 60;
$minutes += $timeDiff->i;
if($minutes <= Config::getPathfinderData('timer.logged')){
$loginCheck = true;
}
self::getLogger('CHARACTER_ACCESS')->write(
sprintf(Model\CharacterModel::LOG_ACCESS,
$character->_id ,
$loginStatus,
$character->name
)
);
}
return $loginCheck;
return $loginStatus;
}
/**

View File

@@ -1,4 +1,5 @@
<?php
<?php /** @noinspection PhpUndefinedMethodInspection */
/**
* Created by PhpStorm.
* User: exodus4d
@@ -7,6 +8,8 @@
*/
namespace Controller\Api;
use lib\Config;
use Controller;
@@ -18,135 +21,68 @@ use Controller;
*/
class GitHub extends Controller\Controller {
protected function getBaseRequestOptions() : array {
return [
'timeout' => 3,
'user_agent' => $this->getUserAgent(),
'follow_location' => false // otherwise CURLOPT_FOLLOWLOCATION will fail
];
}
/**
* get HTTP request options for API (curl) request
* @return array
*/
protected function getRequestReleaseOptions() : array {
$options = $this->getBaseRequestOptions();
$options['method'] = 'GET';
return $options;
}
/**
* get HTTP request options for API (curl) request
* @param string $text
* @return array
*/
protected function getRequestMarkdownOptions(string $text) : array {
$params = [
'text' => $text,
'mode' => 'gfm',
'context' => 'exodus4d/pathfinder'
];
$options = $this->getBaseRequestOptions();
$options['method'] = 'POST';
$options['content'] = json_encode($params, JSON_UNESCAPED_SLASHES);
return $options;
}
/**
* get release information from GitHub
* @param \Base $f3
*/
public function releases(\Base $f3){
$cacheKey = 'CACHE_GITHUB_RELEASES';
$ttl = 60 * 30; // 30min
$releaseCount = 4;
if( !$f3->exists($cacheKey, $return) ){
$apiReleasePath = Config::getPathfinderData('api.git_hub') . '/repos/exodus4d/pathfinder/releases';
$apiMarkdownPath = Config::getPathfinderData('api.git_hub') . '/markdown';
$return = (object) [];
$return->releasesData = [];
$return->version = (object) [];
$return->version->current = Config::getPathfinderData('version');
$return->version->last = '';
$return->version->delta = null;
$return->version->dev = false;
// build request URL
$apiResponse = \Web::instance()->request($apiReleasePath, $this->getRequestReleaseOptions() );
$md = \Markdown::instance();
if($apiResponse['body']){
$return = (object) [];
$return->releasesData = [];
$return->version = (object) [];
$return->version->current = Config::getPathfinderData('version');
$return->version->last = '';
$return->version->delta = null;
$return->version->dev = false;
$releases = $f3->gitHubClient()->getProjectReleases('exodus4d/pathfinder', $releaseCount);
// request succeeded -> format "Markdown" to "HTML"
// result is JSON formed
$releasesData = (array)json_decode($apiResponse['body']);
// check max release count
if(count($releasesData) > $releaseCount){
$releasesData = array_slice($releasesData, 0, $releaseCount);
foreach($releases as $key => &$release){
// check version ------------------------------------------------------------------------------------------
if($key === 0){
$return->version->last = $release['name'];
if(version_compare( $return->version->current, $return->version->last, '>')){
$return->version->dev = true;
}
$md = \Markdown::instance();
foreach($releasesData as $key => &$releaseData){
// check version ----------------------------------------------------------------------------------
if($key === 0){
$return->version->last = $releaseData->tag_name;
if(version_compare( $return->version->current, $return->version->last, '>')){
$return->version->dev = true;
}
}
if(
!$return->version->dev &&
version_compare( $releaseData->tag_name, $return->version->current, '>=')
){
$return->version->delta = ($key === count($releasesData) - 1) ? '>= ' . $key : $key;
}
// format body ------------------------------------------------------------------------------------
if(isset($releaseData->body)){
$body = $releaseData->body;
// remove "update information" from release text
// -> keep everything until first "***" -> horizontal line
if( ($pos = strpos($body, '***')) !== false){
$body = substr($body, 0, $pos);
}
// convert list style
$body = str_replace(' - ', '* ', $body );
// convert Markdown to HTML -> use either gitHub API (in oder to create abs, issue links)
// -> or F3´s markdown as fallback
$markdownResponse = \Web::instance()->request($apiMarkdownPath, $this->getRequestMarkdownOptions($body) );
if($markdownResponse['body']){
$body = $markdownResponse['body'];
}else{
$body = $md->convert( trim($body) );
}
$releaseData->body = $body;
}
}
$return->releasesData = $releasesData;
$f3->set($cacheKey, $return, $ttl);
}else{
// request failed -> cache failed result (respect API request limit)
$f3->set($cacheKey, false, 60 * 15);
}
if(
!$return->version->dev &&
version_compare($release['name'], $return->version->current, '>=')
){
$return->version->delta = ($key === count($releases) - 1) ? '>= ' . $key : $key;
}
// format body ------------------------------------------------------------------------------------
$body = $release['body'];
// remove "update information" from release text
// -> keep everything until first "***" -> horizontal line
if( ($pos = strpos($body, '***')) !== false){
$body = substr($body, 0, $pos);
}
// convert list style
$body = str_replace(' - ', '* ', $body);
// convert Markdown to HTML -> use either gitHub API (in oder to create abs, issue links)
// -> or F3´s markdown as fallback
$html = $f3->gitHubClient()->markdownToHtml('exodus4d/pathfinder', $body);
if(!empty($html)){
$body = $html;
}else{
$body = $md->convert(trim($body));
}
$release['body'] = $body;
}
// set 503 if service unavailable or temp cached data = false
if( !$f3->get($cacheKey) ){
$f3->status(503);
}
$return->releasesData = $releases;
echo json_encode($f3->get($cacheKey));
echo json_encode($return);
}
}

View File

@@ -183,6 +183,11 @@ class Map extends Controller\AccessController {
'zKillboard' => Config::getPathfinderData('api.z_killboard')
];
// Character default config -------------------------------------------------------------------------------
$return->character = [
'autoLocationSelect' => (bool)Config::getPathfinderData('character.auto_location_select')
];
// Slack integration status -------------------------------------------------------------------------------
$return->slack = [
'status' => (bool)Config::getPathfinderData('slack.status')
@@ -204,6 +209,9 @@ class Map extends Controller\AccessController {
$validInitData = $validInitData ? !empty($structureData) : $validInitData;
// get available wormhole types ---------------------------------------------------------------------------
/**
* @var $wormhole Model\Universe\WormholeModel
*/
$wormhole = Model\Universe\BasicUniverseModel::getNew('WormholeModel');
$wormholesData = [];
if($rows = $wormhole->find(null, ['order' => 'name asc'])){
@@ -419,7 +427,7 @@ class Map extends Controller\AccessController {
$return->error = [];
if( isset($formData['id']) ){
$activeCharacter = $this->getCharacter(0);
$activeCharacter = $this->getCharacter();
/**
* @var $map Model\MapModel
@@ -865,13 +873,13 @@ class Map extends Controller\AccessController {
$getMapUserData = (bool)$postData['getMapUserData'];
$mapTracking = (bool)$postData['mapTracking'];
$systemData = (array)$postData['systemData'];
$activeCharacter = $this->getCharacter(0);
$activeCharacter = $this->getCharacter();
$return = (object)[];
// update current location
// -> suppress temporary timeout errors
$activeCharacter = $activeCharacter->updateLog(['suppressHTTPErrors' => true]);
$activeCharacter = $activeCharacter->updateLog();
if( !empty($mapIds) ){
// IMPORTANT for now -> just update a single map (save performance)

View File

@@ -92,6 +92,9 @@ class Route extends Controller\AccessController {
$rows = $this->getDB()->exec($query, null, $this->staticJumpDataCacheTime);
if(count($rows) > 0){
array_walk($rows, function(&$row){
$row['jumpNodes'] = array_map('intval', explode(':', $row['jumpNodes']));
});
$this->updateJumpData($rows);
}
}
@@ -172,58 +175,65 @@ class Route extends Controller\AccessController {
$whereQuery .= " `connection`.`eolUpdated` IS NULL AND ";
}
$query = "SELECT
`system_src`.`systemId` systemId,
(
SELECT
GROUP_CONCAT( NULLIF(`system_tar`.`systemId`, NULL) SEPARATOR ':')
$query = "SELECT
`system_src`.`systemId` systemSourceId,
`system_tar`.`systemId` systemTargetId
FROM
`connection` INNER JOIN
`system` system_tar ON
`system_tar`.`id` = `connection`.`source` OR
`system_tar`.`id` = `connection`.`target`
`map` ON
`map`.`id` = `connection`.`mapId` AND
`map`.`active` = 1 INNER JOIN
`system` `system_src` ON
`system_src`.`id` = `connection`.`source` AND
`system_src`.`active` = 1 INNER JOIN
`system` `system_tar` ON
`system_tar`.`id` = `connection`.`target` AND
`system_tar`.`active` = 1
WHERE
`connection`.`mapId` " . $whereMapIdsQuery . " AND
`connection`.`active` = 1 AND
(
`connection`.`source` = `system_src`.`id` OR
`connection`.`target` = `system_src`.`id`
) AND
" . $whereQuery . "
`system_tar`.`id` != `system_src`.`id` AND
`system_tar`.`active` = 1
) jumpNodes
FROM
`system` `system_src` INNER JOIN
`map` ON
`map`.`id` = `system_src`.`mapId`
WHERE
`system_src`.`mapId` " . $whereMapIdsQuery . " AND
`system_src`.`active` = 1 AND
`map`.`active` = 1
HAVING
-- skip systems without neighbors (e.g. WHs)
jumpNodes IS NOT NULL
";
" . $whereQuery . "
`connection`.`active` = 1 AND
`connection`.`mapId` " . $whereMapIdsQuery . "
";
$rows = $this->getDB()->exec($query, null, $this->dynamicJumpDataCacheTime);
if(count($rows) > 0){
// enrich $row data with static system data (from universe DB)
$jumpData = [];
$universe = new Universe();
for($i = 0; $i < count($rows); $i++){
if($staticData = $universe->getSystemData($rows[$i]['systemId'])){
$rows[$i]['systemName'] = $staticData->name;
$rows[$i]['constellationId'] = $staticData->constellation->id;
$rows[$i]['regionId'] = $staticData->constellation->region->id;
$rows[$i]['trueSec'] = $staticData->trueSec;
/**
* enrich dynamic jump data with static system data (from universe DB)
* @param array $row
* @param string $systemSourceKey
* @param string $systemTargetKey
*/
$enrichJumpData = function(array &$row, string $systemSourceKey, string $systemTargetKey) use (&$jumpData, &$universe) {
if(
!array_key_exists($row[$systemSourceKey], $jumpData) &&
!is_null($staticData = $universe->getSystemData($row[$systemSourceKey]))
){
$jumpData[$row[$systemSourceKey]] = [
'systemId' => (int)$row[$systemSourceKey],
'systemName' => $staticData->name,
'constellationId' => $staticData->constellation->id,
'regionId' => $staticData->constellation->region->id,
'trueSec' => $staticData->trueSec,
];
}
if( !in_array($row[$systemTargetKey], (array)$jumpData[$row[$systemSourceKey]]['jumpNodes']) ){
$jumpData[$row[$systemSourceKey]]['jumpNodes'][] = (int)$row[$systemTargetKey];
}
};
for($i = 0; $i < count($rows); $i++){
$enrichJumpData($rows[$i], 'systemSourceId', 'systemTargetId');
$enrichJumpData($rows[$i], 'systemTargetId', 'systemSourceId');
}
// update jump data for this instance
$this->updateJumpData($rows);
$this->updateJumpData($jumpData);
}
}
}
}
@@ -259,7 +269,7 @@ class Route extends Controller\AccessController {
if( !is_array($this->jumpArray[$systemId]) ){
$this->jumpArray[$systemId] = [];
}
$this->jumpArray[$systemId] = array_merge(array_map('intval', explode(':', $row['jumpNodes'])), $this->jumpArray[$systemId]);
$this->jumpArray[$systemId] = array_merge($row['jumpNodes'], $this->jumpArray[$systemId]);
// add systemName to end (if not already there)
if(end($this->jumpArray[$systemId]) != $systemName){
@@ -580,7 +590,7 @@ class Route extends Controller\AccessController {
'connections' => $connections
];
$result = $this->getF3()->ccpClient->getRouteData($systemFromId, $systemToId, $options);
$result = $this->getF3()->ccpClient()->getRouteData($systemFromId, $systemToId, $options);
// format result ------------------------------------------------------------------------------------------

View File

@@ -135,7 +135,7 @@ class System extends Controller\AccessController {
];
foreach($postData['systemData'] as $systemData){
$response = $f3->ccpClient->setWaypoint($systemData['systemId'], $accessToken, $options);
$response = $f3->ccpClient()->setWaypoint($systemData['systemId'], $accessToken, $options);
if(empty($response)){
$return->systemData[] = $systemData;

View File

@@ -41,24 +41,24 @@ class User extends Controller\Controller{
/**
* login a valid character
* @param Model\CharacterModel $characterModel
* @param Model\CharacterModel $character
* @return bool
* @throws Exception
*/
protected function loginByCharacter(Model\CharacterModel &$characterModel){
protected function loginByCharacter(Model\CharacterModel &$character) : bool {
$login = false;
if($user = $characterModel->getUser()){
if($user = $character->getUser()){
// check if character belongs to current user
// -> If there is already a logged in user! (e.g. multi character use)
$currentUser = $this->getUser();
$timezone = $this->getF3()->get('getTimeZone')();
$sessionCharacters = [
[
'ID' => $characterModel->_id,
'NAME' => $characterModel->name,
'TIME' => (new \DateTime())->getTimestamp(),
'UPDATE_RETRY' => 0
'ID' => $character->_id,
'NAME' => $character->name,
'TIME' => (new \DateTime('now', $timezone))->getTimestamp()
]
];
@@ -74,29 +74,29 @@ class User extends Controller\Controller{
]);
}else{
// user has NOT changed -----------------------------------------------------------
$sessionCharacters = $characterModel::mergeSessionCharacterData($sessionCharacters);
$sessionCharacters = $character::mergeSessionCharacterData($sessionCharacters);
}
$this->getF3()->set(self::SESSION_KEY_CHARACTERS, $sessionCharacters);
// save user login information --------------------------------------------------------
$characterModel->roleId = $characterModel->requestRole();
$characterModel->touch('lastLogin');
$characterModel->save();
$character->roleId = $character->requestRole();
$character->touch('lastLogin');
$character->save();
// write login log --------------------------------------------------------------------
self::getLogger('LOGIN')->write(
self::getLogger('CHARACTER_LOGIN')->write(
sprintf(self::LOG_LOGGED_IN,
$user->_id,
$user->name,
$characterModel->_id,
$characterModel->name
$character->_id,
$character->name
)
);
// set temp character data ------------------------------------------------------------
// -> pass character data over for next http request (reroute())
$this->setTempCharacterData($characterModel->_id);
$this->setTempCharacterData($character->_id);
$login = true;
}
@@ -112,31 +112,30 @@ class User extends Controller\Controller{
*/
public function getCookieCharacter(\Base $f3){
$data = $f3->get('POST');
$cookieName = (string)$data['cookie'];
$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!)
// -> add characters WITHOUT permission to log in too!
if( !empty($characters = $this->getCookieCharacters(array_slice($cookieData, 0, 1, true), false)) ){
// character is valid and allowed to login
$return->character = reset($characters)->getData();
// get Session status for character
if($activeCharacter = $this->getCharacter()){
if($activeUser = $activeCharacter->getUser()){
if($sessionCharacterData = $activeUser->findSessionCharacterData($return->character->id)){
$return->character->hasActiveSession = true;
}
if( !empty($cookieData = $this->getCookieByName($cookieName) )){
// cookie data is valid -> validate data against DB (security check!)
// -> add characters WITHOUT permission to log in too!
if( !empty($characters = $this->getCookieCharacters(array_slice($cookieData, 0, 1, true), false)) ){
// character is valid and allowed to login
$return->character = reset($characters)->getData();
// get Session status for character
if($activeCharacter = $this->getCharacter()){
if($activeUser = $activeCharacter->getUser()){
if($sessionCharacterData = $activeUser->findSessionCharacterData($return->character->id)){
$return->character->hasActiveSession = true;
}
}
}else{
$characterError = (object) [];
$characterError->type = 'warning';
$characterError->message = 'This can happen through "invalid cookies(SSO)", "login restrictions", "ESI problems".';
$return->error[] = $characterError;
}
}else{
$characterError = (object) [];
$characterError->type = 'warning';
$characterError->message = 'This can happen through "invalid cookies(SSO)", "login restrictions", "ESI problems".';
$return->error[] = $characterError;
}
}
@@ -221,7 +220,7 @@ class User extends Controller\Controller{
if( $targetId = (int)$data['targetId']){
$activeCharacter = $this->getCharacter();
$response = $f3->ccpClient->openWindow($targetId, $activeCharacter->getAccessToken());
$response = $f3->ccpClient()->openWindow($targetId, $activeCharacter->getAccessToken());
if(empty($response)){
$return->targetId = $targetId;
@@ -260,7 +259,7 @@ class User extends Controller\Controller{
$formData = $data['formData'];
try{
if($activeCharacter = $this->getCharacter(0)){
if($activeCharacter = $this->getCharacter()){
$user = $activeCharacter->getUser();
// captcha is send -> check captcha -------------------------------------------
@@ -329,7 +328,7 @@ class User extends Controller\Controller{
// character config -----------------------------------------------------------
if(isset($formData['character'])){
$activeCharacter->logLocation = (int)$formData['logLocation'];
$activeCharacter->copyfrom($formData, ['logLocation', 'selectLocation']);
$activeCharacter->save();
}
@@ -371,7 +370,7 @@ class User extends Controller\Controller{
!empty($data['captcha']) &&
$data['captcha'] === $captcha
){
$activeCharacter = $this->getCharacter(0);
$activeCharacter = $this->getCharacter();
$user = $activeCharacter->getUser();
if($user){

View File

@@ -1,17 +1,18 @@
<?php
<?php /** @noinspection PhpUndefinedMethodInspection */
/**
* Created by PhpStorm.
* User: Exodus
* Date: 23.01.2016
* Time: 17:18
*
* Handles access to EVE-Online "ESI API" and "SSO" auth functions
* Handles access to EVE-Online "ESI API" and "SSO" oAuth 2.0 functions
* - Add your API credentials in "environment.ini"
* - Check "PATHFINDER.API" in "pathfinder.ini" for correct API URLs
* Hint: \Web::instance()->request automatically caches responses by their response "Cache-Control" header!
*/
namespace Controller\Ccp;
use Controller;
use Controller\Api as Api;
use Model;
@@ -24,11 +25,6 @@ class Sso extends Api\User{
*/
const SSO_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';
@@ -45,8 +41,7 @@ class Sso extends Api\User{
const ERROR_CHARACTER_DATA = 'Failed to load characterData from ESI';
const ERROR_CHARACTER_FORBIDDEN = 'Character "%s" is not authorized to log in. Reason: %s';
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';
const ERROR_COOKIE_LOGIN = 'Login from Cookie failed (data not found). Please retry by CCP SSO';
/**
* redirect user to CCP SSO page and request authorization
@@ -72,7 +67,7 @@ class Sso extends Api\User{
if(
isset($params['characterId']) &&
( $activeCharacter = $this->getCharacter(0) )
( $activeCharacter = $this->getCharacter() )
){
// authentication restricted to a characterId -----------------------------------------------
// restrict login to this characterId e.g. for character switch on map page
@@ -104,9 +99,7 @@ class Sso extends Api\User{
$character->hasUserCharacter() &&
($character->isAuthorized() === 'OK')
){
$loginCheck = $this->loginByCharacter($character);
if($loginCheck){
if($this->loginByCharacter($character)){
// set "login" cookie
$this->setLoginCookie($character);
@@ -132,7 +125,7 @@ class Sso extends Api\User{
* @param array $scopes
* @param string $rootAlias
*/
private function rerouteAuthorization(\Base $f3, $scopes = [], $rootAlias = 'login'){
private function rerouteAuthorization(\Base $f3, array $scopes = [], string $rootAlias = 'login'){
if( !empty( Controller\Controller::getEnvironmentData('CCP_SSO_CLIENT_ID') ) ){
// used for "state" check between request and callback
$state = bin2hex( openssl_random_pseudo_bytes(12) );
@@ -146,7 +139,9 @@ class Sso extends Api\User{
'state' => $state
];
$ssoAuthUrl = self::getAuthorizationEndpoint() . '?' . http_build_query($urlParams, '', '&', PHP_QUERY_RFC3986 );
$ssoAuthUrl = $f3->ssoClient()->getUrl();
$ssoAuthUrl .= $f3->ssoClient()->getAuthorizationEndpointURI();
$ssoAuthUrl .= '?' . http_build_query($urlParams, '', '&', PHP_QUERY_RFC3986 );
$f3->status(302);
$f3->reroute($ssoAuthUrl);
@@ -190,28 +185,26 @@ class Sso extends Api\User{
$accessData = $this->getSsoAccessData($getParams['code']);
if(
isset($accessData->accessToken) &&
isset($accessData->refreshToken)
){
if(isset($accessData->accessToken, $accessData->esiAccessTokenExpires, $accessData->refreshToken)){
// login succeeded -> get basic character data for current login
$verificationCharacterData = $this->verifyCharacterData($accessData->accessToken);
if( !is_null($verificationCharacterData)){
if( !empty($verificationCharacterData) ){
// check if login is restricted to a characterID
// verification available data. Data is needed for "ownerHash" check
// get character data from ESI
$characterData = $this->getCharacterData((int)$verificationCharacterData->CharacterID);
$characterData = $this->getCharacterData((int)$verificationCharacterData['characterId']);
if( isset($characterData->character) ){
// add "ownerHash" and SSO tokens
$characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash;
$characterData->character['crestAccessToken'] = $accessData->accessToken;
$characterData->character['crestRefreshToken'] = $accessData->refreshToken;
$characterData->character['esiScopes'] = Lib\Util::convertScopesString($verificationCharacterData->Scopes);
$characterData->character['ownerHash'] = $verificationCharacterData['characterOwnerHash'];
$characterData->character['esiAccessToken'] = $accessData->accessToken;
$characterData->character['esiAccessTokenExpires'] = $accessData->esiAccessTokenExpires;
$characterData->character['esiRefreshToken'] = $accessData->refreshToken;
$characterData->character['esiScopes'] = $verificationCharacterData['scopes'];
// add/update static character data
$characterModel = $this->updateCharacter($characterData);
@@ -253,9 +246,7 @@ class Sso extends Api\User{
$characterModel = $userCharactersModel->getCharacter();
// login by character
$loginCheck = $this->loginByCharacter($characterModel);
if($loginCheck){
if($this->loginByCharacter($characterModel)){
// set "login" cookie
$this->setLoginCookie($characterModel);
@@ -306,7 +297,7 @@ class Sso extends Api\User{
*/
public function login(\Base $f3){
$data = (array)$f3->get('GET');
$cookieName = empty($data['cookie']) ? '' : $data['cookie'];
$cookieName = (string)$data['cookie'];
$character = null;
if( !empty($cookieName) ){
@@ -319,17 +310,19 @@ class Sso extends Api\User{
}
}
if( is_object($character)){
if(is_object($character)){
// login by character
$loginCheck = $this->loginByCharacter($character);
if($loginCheck){
if($this->loginByCharacter($character)){
// route to "map"
$f3->reroute(['map', ['*' => '']]);
}else{
$f3->set(self::SESSION_KEY_SSO_ERROR, sprintf(self::ERROR_LOGIN_FAILED, $character->name));
}
}else{
$f3->set(self::SESSION_KEY_SSO_ERROR, self::ERROR_COOKIE_LOGIN);
}
// on error -> route back to login form
$f3->set(self::SESSION_KEY_SSO_ERROR, self::ERROR_COOKIE_LOGIN);
$f3->reroute(['login']);
}
@@ -341,7 +334,7 @@ class Sso extends Api\User{
* @param bool $authCode
* @return null|\stdClass
*/
public function getSsoAccessData($authCode){
protected function getSsoAccessData($authCode){
$accessData = null;
if( !empty($authCode) ){
@@ -357,10 +350,10 @@ class Sso extends Api\User{
/**
* verify authorization code, and get an "access_token" data
* @param $authCode
* @param string $authCode
* @return \stdClass
*/
protected function verifyAuthorizationCode($authCode){
protected function verifyAuthorizationCode(string $authCode){
$requestParams = [
'grant_type' => 'authorization_code',
'code' => $authCode
@@ -372,10 +365,10 @@ class Sso extends Api\User{
/**
* get new "access_token" by an existing "refresh_token"
* -> if "access_token" is expired, this function gets a fresh one
* @param $refreshToken
* @param string $refreshToken
* @return \stdClass
*/
public function refreshAccessToken($refreshToken){
public function refreshAccessToken(string $refreshToken){
$requestParams = [
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
@@ -388,61 +381,42 @@ class Sso extends Api\User{
* 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
* @param array $requestParams
* @return \stdClass
*/
protected function requestAccessData($requestParams){
$verifyAuthCodeUrl = self::getVerifyAuthorizationCodeEndpoint();
$verifyAuthCodeUrlParts = parse_url($verifyAuthCodeUrl);
protected function requestAccessData(array $requestParams) : \stdClass {
$accessData = (object) [];
$accessData->accessToken = null;
$accessData->refreshToken = null;
$accessData->esiAccessTokenExpires = 0;
if($verifyAuthCodeUrlParts){
$contentType = 'application/x-www-form-urlencoded';
$requestOptions = [
'timeout' => self::SSO_TIMEOUT,
'method' => 'POST',
'user_agent' => $this->getUserAgent(),
'header' => [
'Authorization: Basic ' . $this->getAuthorizationHeader(),
'Content-Type: ' . $contentType,
'Host: ' . $verifyAuthCodeUrlParts['host']
]
];
$authCodeRequestData = $this->getF3()->ssoClient()->getAccessData($this->getAuthorizationData(), $requestParams);
// content (parameters to send with)
$requestOptions['content'] = http_build_query($requestParams);
if( !empty($authCodeRequestData) ){
if( !empty($authCodeRequestData['accessToken']) ){
// accessToken is required for endpoints that require Auth
$accessData->accessToken = $authCodeRequestData['accessToken'];
}
$apiResponse = Lib\Web::instance()->request($verifyAuthCodeUrl, $requestOptions);
if( !empty($authCodeRequestData['expiresIn']) ){
// expire time for accessToken
try{
$timezone = $this->getF3()->get('getTimeZone')();
$accessTokenExpires = new \DateTime('now', $timezone);
$accessTokenExpires->add(new \DateInterval('PT' . (int)$authCodeRequestData['expiresIn'] . 'S'));
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'];
}
$accessData->esiAccessTokenExpires = $accessTokenExpires->format('Y-m-d H:i:s');
}catch(\Exception $e){
$this->getF3()->error(500, $e->getMessage(), $e->getTrace());
}
}else{
self::getSSOLogger()->write(
sprintf(
self::ERROR_ACCESS_TOKEN,
print_r($requestParams, true)
)
);
}
if( !empty($authCodeRequestData['refreshToken']) ){
// this token is used to refresh/get a new access_token when expires
$accessData->refreshToken = $authCodeRequestData['refreshToken'];
}
}else{
self::getSSOLogger()->write(
sprintf(self::ERROR_CCP_SSO_URL, __METHOD__)
);
self::getSSOLogger()->write(sprintf(self::ERROR_ACCESS_TOKEN, print_r($requestParams, true)));
}
return $accessData;
@@ -452,34 +426,17 @@ class Sso extends Api\User{
* verify character data by "access_token"
* -> get some basic information (like character id)
* -> if more character information is required, use ESI "characters" endpoints request instead
* @param $accessToken
* @return mixed|null
* @param string $accessToken
* @return array
*/
public function verifyCharacterData($accessToken){
$verifyUserUrl = self::getVerifyUserEndpoint();
$verifyUrlParts = parse_url($verifyUserUrl);
$characterData = null;
public function verifyCharacterData(string $accessToken) : array {
$characterData = $this->getF3()->ssoClient()->getVerifyCharacterData($accessToken);
if($verifyUrlParts){
$requestOptions = [
'timeout' => self::SSO_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::getSSOLogger()->write(sprintf(self::ERROR_VERIFY_CHARACTER, __METHOD__));
}
if( !empty($characterData) ){
// convert string with scopes to array
$characterData['scopes'] = Lib\Util::convertScopesString($characterData['scopes']);
}else{
self::getSSOLogger()->write(sprintf(self::ERROR_CCP_SSO_URL, __METHOD__));
self::getSSOLogger()->write(sprintf(self::ERROR_VERIFY_CHARACTER, __METHOD__));
}
return $characterData;
@@ -495,7 +452,7 @@ class Sso extends Api\User{
$characterData = (object) [];
if($characterId){
$characterDataBasic = $this->getF3()->ccpClient->getCharacterData($characterId);
$characterDataBasic = $this->getF3()->ccpClient()->getCharacterData($characterId);
if( !empty($characterDataBasic) ){
// remove some "unwanted" data -> not relevant for Pathfinder
@@ -549,7 +506,7 @@ class Sso extends Api\User{
$character = Model\BasicModel::getNew('CharacterModel');
$character->getById((int)$characterData->character['id'], 0);
$character->copyfrom($characterData->character, [
'id', 'name', 'ownerHash', 'crestAccessToken', 'crestRefreshToken', 'esiScopes', 'securityStatus'
'id', 'name', 'ownerHash', 'esiAccessToken', 'esiAccessTokenExpires', 'esiRefreshToken', 'esiScopes', 'securityStatus'
]);
$character->corporationId = $characterData->corporation;
@@ -561,15 +518,16 @@ class Sso extends Api\User{
}
/**
* get "Authorization:" Header data
* get data for HTTP "Authorization:" Header
* -> This header is required for any Auth-required endpoints!
* @return string
* @return array
*/
protected function getAuthorizationHeader(){
return base64_encode(
Controller\Controller::getEnvironmentData('CCP_SSO_CLIENT_ID') . ':'
. Controller\Controller::getEnvironmentData('CCP_SSO_SECRET_KEY')
);
protected function getAuthorizationData() : array {
return [
Controller\Controller::getEnvironmentData('CCP_SSO_CLIENT_ID'),
Controller\Controller::getEnvironmentData('CCP_SSO_SECRET_KEY'),
'basic'
];
}
/**
@@ -577,7 +535,7 @@ class Sso extends Api\User{
* -> throw error if url is broken/missing
* @return string
*/
static function getSsoUrlRoot(){
static function getSsoUrlRoot() : string {
$url = '';
if( \Audit::instance()->url(self::getEnvironmentData('CCP_SSO_URL')) ){
$url = self::getEnvironmentData('CCP_SSO_URL');
@@ -590,23 +548,11 @@ class Sso extends Api\User{
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 SSO logging
* @return \Log
*/
static function getSSOLogger(){
static function getSSOLogger() : \Log {
return parent::getLogger('SSO');
}
}

View File

@@ -22,7 +22,7 @@ class Universe extends Controller {
$regionsWhitelist = [
10000002 // The Forge (13 constellations -> 93 systems)
];
$regionIds = $f3->ccpClient->getUniverseRegions();
$regionIds = $f3->ccpClient()->getUniverseRegions();
$regionIds = array_intersect ($regionsWhitelist, $regionIds);
$region = Model\Universe\BasicUniverseModel::getNew('RegionModel');
@@ -43,7 +43,7 @@ class Universe extends Controller {
$constellationsWhitelist = [
20000014 // Mal (11 systems)
];
$constellationIds = $f3->ccpClient->getUniverseConstellations();
$constellationIds = $f3->ccpClient()->getUniverseConstellations();
$constellationIds = array_intersect ($constellationsWhitelist, $constellationIds);
$constellation = Model\Universe\BasicUniverseModel::getNew('ConstellationModel');
foreach($constellationIds as $constellationId){
@@ -92,7 +92,7 @@ class Universe extends Controller {
*/
protected function setupCategories(array $categoriesWhitelist = []){
$return = [];
$categoryIds = $this->getF3()->ccpClient->getUniverseCategories();
$categoryIds = $this->getF3()->ccpClient()->getUniverseCategories();
$categoryIds = array_intersect ($categoriesWhitelist, $categoryIds);
foreach($categoryIds as $categoryId){
$return[$categoryId] = $this->setupCategory($categoryId);
@@ -112,7 +112,7 @@ class Universe extends Controller {
*/
protected function setupGroups(array $groupsWhitelist = []){
$return = [];
$groupIds = $this->getF3()->ccpClient->getUniverseGroups();
$groupIds = $this->getF3()->ccpClient()->getUniverseGroups();
$groupIds = array_intersect ($groupsWhitelist, $groupIds);
/**
* @var $group Model\Universe\GroupModel
@@ -288,13 +288,13 @@ class Universe extends Controller {
$f3 = \Base::instance();
$universeNameData = [];
if( !empty($categories) && !empty($search)){
$universeIds = $f3->ccpClient->search($categories, $search, $strict);
$universeIds = $f3->ccpClient()->search($categories, $search, $strict);
if(isset($universeIds['error'])){
// ESI error
$universeNameData = $universeIds;
}elseif( !empty($universeIds) ){
$universeIds = Util::arrayFlattenByValue($universeIds);
$universeNameData = $f3->ccpClient->getUniverseNamesData($universeIds);
$universeNameData = $f3->ccpClient()->getUniverseNamesData($universeIds);
}
}
return $universeNameData;

View File

@@ -10,6 +10,7 @@ namespace Controller;
use Controller\Api as Api;
use Exception\PathfinderException;
use lib\api\CcpClient;
use lib\Config;
use lib\Resource;
use lib\Monolog;
@@ -126,31 +127,30 @@ class Controller {
protected function initSession(\Base $f3){
$session = null;
/**
* callback() for suspect sessions
* @param $session
* @param $sid
* @return bool
*/
$onSuspect = function($session, $sid){
self::getLogger('SESSION_SUSPECT')->write( sprintf(
self::ERROR_SESSION_SUSPECT,
$sid,
$session->ip(),
$session->agent()
));
// .. continue with default onSuspect() handler
// -> destroy session
return false;
};
if(
$f3->get('SESSION_CACHE') === 'mysql' &&
$this->getDB('PF') instanceof DB\SQL
){
if(!headers_sent() && session_status()!=PHP_SESSION_ACTIVE){
$session = new DB\SQL\Session($this->getDB('PF'), 'sessions', true, $onSuspect);
/**
* callback() for suspect sessions
* @param $session
* @param $sid
* @return bool
*/
$onSuspect = function($session, $sid){
self::getLogger('SESSION_SUSPECT')->write( sprintf(
self::ERROR_SESSION_SUSPECT,
$sid,
$session->ip(),
$session->agent()
));
// .. continue with default onSuspect() handler
// -> destroy session
return false;
};
new DB\SQL\MySQL\Session($this->getDB('PF'), 'sessions', true, $onSuspect);
}
}
@@ -212,7 +212,7 @@ class Controller {
$data[$name] = $value;
}
}
}elseif( isset($cookieData[$cookieName]) ){
}elseif(isset($cookieData[$cookieName])){
// look for a single cookie
$data[$cookieName] = $cookieData[$cookieName];
}
@@ -539,36 +539,90 @@ class Controller {
/**
* get EVE server status from ESI
* @param \Base $f3
* @throws \Exception
*/
public function getEveServerStatus(\Base $f3){
$esiStatusVersion = 'latest';
$cacheKey = 'eve_server_status';
if( !$f3->exists($cacheKey, $return) ){
$return = (object) [];
$return->error = [];
$return->status = [
'serverName' => strtoupper( self::getEnvironmentData('CCP_ESI_DATASOURCE') ),
'serviceStatus' => 'offline'
];
$response = $f3->ccpClient->getServerStatus();
/**
* @var $client CcpClient
*/
if($client = $f3->ccpClient()){
$return->server = [
'name' => strtoupper(self::getEnvironmentData('CCP_ESI_DATASOURCE')),
'status' => 'offline',
'statusColor' => 'red',
];
$return->api = [
'name' => 'ESI API',
'status' => 'offline',
'statusColor' => 'red',
'url' => $client->getUrl(),
'timeout' => $client->getTimeout(),
'connectTimeout' => $client->getConnectTimeout(),
'readTimeout' => $client->getReadTimeout(),
'proxy' => ($proxy = $client->getProxy()) ? : 'false',
'verify' => $client->getVerify(),
'debug' => $client->getDebugRequests(),
'dataSource' => $client->getDataSource(),
'statusVersion' => $esiStatusVersion,
'routes' => []
];
if( !empty($response) ){
// calculate time diff since last server restart
$timezone = $f3->get('getTimeZone')();
$dateNow = new \DateTime('now', $timezone);
$dateServerStart = new \DateTime($response['startTime']);
$interval = $dateNow->diff($dateServerStart);
$startTimestampFormat = $interval->format('%hh %im');
if($interval->days > 0){
$startTimestampFormat = $interval->days . 'd ' . $startTimestampFormat;
$serverStatus = $client->getServerStatus();
if( !isset($serverStatus['error']) ){
$statusData = $serverStatus['status'];
// calculate time diff since last server restart
$timezone = $f3->get('getTimeZone')();
$dateNow = new \DateTime('now', $timezone);
$dateServerStart = new \DateTime($statusData['startTime']);
$interval = $dateNow->diff($dateServerStart);
$startTimestampFormat = $interval->format('%hh %im');
if($interval->days > 0){
$startTimestampFormat = $interval->days . 'd ' . $startTimestampFormat;
}
$statusData['name'] = $return->server['name'];
$statusData['status'] = 'online';
$statusData['statusColor'] = 'green';
$statusData['startTime'] = $startTimestampFormat;
$return->server = $statusData;
}else{
$return->error[] = (new PathfinderException($serverStatus['error'], 500))->getError();
}
$response['serverName'] = strtoupper( self::getEnvironmentData('CCP_ESI_DATASOURCE') );
$response['serviceStatus'] = 'online';
$response['startTime'] = $startTimestampFormat;
$return->status = $response;
$apiStatus = $client->getStatusForRoutes('latest');
if( !isset($apiStatus['error']) ){
// find top status
$status = 'OK';
$color = 'green';
foreach($apiStatus['status'] as $statusData){
if('red' == $statusData['status']){
$status = 'unstable';
$color = $statusData['status'];
break;
}
if('yellow' == $statusData['status']){
$status = 'degraded';
$color = $statusData['status'];
}
}
$f3->set($cacheKey, $return, 60);
$return->api['status'] = $status;
$return->api['statusColor'] = $color;
$return->api['routes'] = $apiStatus['status'];
}else{
$return->error[] = (new PathfinderException($apiStatus['error'], 500))->getError();
}
if(empty($return->error)){
$f3->set($cacheKey, $return, 60);
}
}
}
@@ -795,9 +849,8 @@ class Controller {
* @param string $authType
* @return array
*/
static function getScopesByAuthType($authType = ''){
static function getScopesByAuthType(string $authType = '') : array {
$scopes = array_filter((array)self::getEnvironmentData('CCP_ESI_SCOPES'));
switch($authType){
case 'admin':
$scopesAdmin = array_filter((array)self::getEnvironmentData('CCP_ESI_SCOPES_ADMIN'));
@@ -896,9 +949,9 @@ class Controller {
* get a Logger object by Hive key
* -> set in pathfinder.ini
* @param string $type
* @return \Log|null
* @return \Log
*/
static function getLogger($type){
static function getLogger($type = 'DEBUG') : \Log {
return LogController::getLogger($type);
}

View File

@@ -162,9 +162,9 @@ class LogController extends \Prefab {
/**
* get Logger instance
* @param string $type
* @return \Log|null
* @return \Log
*/
public static function getLogger($type){
public static function getLogger(string $type) : \Log {
$logFiles = Config::getPathfinderData('logfiles');
$logFileName = empty($logFiles[$type]) ? 'error' : $logFiles[$type];

View File

@@ -167,9 +167,6 @@ class Setup extends Controller {
// js view (file)
$f3->set('tplJsView', 'setup');
// set render functions (called within template)
$f3->set('cacheType', $this->getCacheType($f3));
// simple counter (called within template)
$counter = [];
$f3->set('tplCounter', function(string $action = 'increment', string $type = 'default', $val = 0) use (&$counter){
@@ -187,19 +184,6 @@ class Setup extends Controller {
echo \Template::instance()->render( Config::getPathfinderData('view.index') );
}
/**
* get Cache backend type for F3
* @param \Base $f3
* @return string
*/
protected function getCacheType(\Base &$f3) : string {
$cacheType = $f3->get('CACHE');
if(strpos($cacheType, 'redis') !== false){
$cacheType = 'redis';
}
return $cacheType;
}
/**
* main setup route handler
* works as dispatcher for setup functions
@@ -229,47 +213,63 @@ class Setup extends Controller {
case 'exportTable':
$this->exportTable($params['model']);
break;
case 'clearCache':
$this->clearCache($f3);
case 'clearFiles':
$this->clearFiles((string)$params['path']);
break;
case 'flushRedisDb':
$this->flushRedisDb((string)$params['host'], (int)$params['port'], (int)$params['db']);
break;
case 'invalidateCookies':
$this->invalidateCookies($f3);
break;
}
// set template data ----------------------------------------------------------------
// set environment information
$f3->set('environmentInformation', $this->getEnvironmentInformation($f3));
// ============================================================================================================
// Template data
// ============================================================================================================
// set server information
// Server -----------------------------------------------------------------------------------------------------
// Server information
$f3->set('serverInformation', $this->getServerInformation($f3));
// set requirement check information
$f3->set('checkRequirements', $this->checkRequirements($f3));
// Pathfinder directory config
$f3->set('directoryConfig', $this->getDirectoryConfig($f3));
// set php config check information
$f3->set('checkPHPConfig', $this->checkPHPConfig($f3));
// set system config check information
// Server environment variables
$f3->set('checkSystemConfig', $this->checkSystemConfig($f3));
// set map default config
// Environment ------------------------------------------------------------------------------------------------
// Server requirement
$f3->set('checkRequirements', $this->checkRequirements($f3));
// PHP config
$f3->set('checkPHPConfig', $this->checkPHPConfig($f3));
// Settings ---------------------------------------------------------------------------------------------------
// Pathfinder environment config
$f3->set('environmentInformation', $this->getEnvironmentInformation($f3));
// Pathfinder map default config
$f3->set('mapsDefaultConfig', $this->getMapsDefaultConfig($f3));
// set database connection information
// Database ---------------------------------------------------------------------------------------------------
// Database config
$f3->set('checkDatabase', $this->checkDatabase($f3, $fixColumns));
// set socket information
// Redis ------------------------------------------------------------------------------------------------------
// Redis information
$f3->set('checkRedisInformation', $this->checkRedisInformation($f3));
// Socket -----------------------------------------------------------------------------------------------------
// WebSocket information
$f3->set('socketInformation', $this->getSocketInformation());
// set index information
// Administration ---------------------------------------------------------------------------------------------
// Index information
$f3->set('indexInformation', $this->getIndexData($f3));
// set cache size
$f3->set('cacheSize', $this->getCacheData($f3));
// set Redis config check information
$f3->set('checkRedisConfig', $this->checkRedisConfig($f3));
// Filesystem (cache) size
$f3->set('checkDirSize', $this->checkDirSize($f3));
}
/**
@@ -290,6 +290,9 @@ class Setup extends Controller {
'database' => [
'icon' => 'fa-database'
],
'cache' => [
'icon' => 'fa-hdd'
],
'socket' => [
'icon' => 'fa-exchange-alt'
],
@@ -306,7 +309,7 @@ class Setup extends Controller {
* @param \Base $f3
* @return array
*/
protected function getEnvironmentInformation(\Base $f3){
protected function getEnvironmentInformation(\Base $f3) : array {
$environmentData = [];
// exclude some sensitive data (e.g. database, passwords)
$excludeVars = [
@@ -346,7 +349,7 @@ class Setup extends Controller {
* @param \Base $f3
* @return array
*/
protected function getServerInformation(\Base $f3){
protected function getServerInformation(\Base $f3) : array {
$serverInfo = [
'time' => [
'label' => 'Time',
@@ -389,15 +392,90 @@ class Setup extends Controller {
return $serverInfo;
}
/**
* get information for used directories
* @param \Base $f3
* @return array
*/
protected function getDirectoryConfig(\Base $f3) : array {
$directoryData = [
'TEMP' => [
'label' => 'TEMP',
'value' => $f3->get('TEMP'),
'check' => true,
'tooltip' => 'Temporary folder for pre compiled templates.',
'chmod' => Util::filesystemInfo($f3->get('TEMP'))['chmod']
],
'CACHE' => [
'label' => 'CACHE',
'value' => $f3->get('CACHE'),
'check' => true,
'tooltip' => 'Cache backend. Support for Redis, Memcache, APC, WinCache, XCache and a filesystem-based (default) cache.',
'chmod' => ((Config::parseDSN($f3->get('CACHE'), $confCache)) && $confCache['type'] == 'folder') ?
Util::filesystemInfo((string)$confCache['folder'])['chmod'] : ''
],
'API_CACHE' => [
'label' => 'API_CACHE',
'value' => $f3->get('API_CACHE'),
'check' => true,
'tooltip' => 'Cache backend for API related cache data. Support for Redis and a filesystem-based (default) cache.',
'chmod' => ((Config::parseDSN($f3->get('API_CACHE'), $confCacheApi)) && $confCacheApi['type'] == 'folder') ?
Util::filesystemInfo((string)$confCacheApi['folder'])['chmod'] : ''
],
'LOGS' => [
'label' => 'LOGS',
'value' => $f3->get('LOGS'),
'check' => true,
'tooltip' => 'Folder for pathfinder logs (e.g. cronjob-, error-logs, ...).',
'chmod' => Util::filesystemInfo($f3->get('LOGS'))['chmod']
],
'UI' => [
'label' => 'UI',
'value' => $f3->get('UI'),
'check' => true,
'tooltip' => 'Folder for public accessible resources (templates, js, css, images,..).',
'chmod' => Util::filesystemInfo($f3->get('UI'))['chmod']
],
'AUTOLOAD' => [
'label' => 'AUTOLOAD',
'value' => $f3->get('AUTOLOAD'),
'check' => true,
'tooltip' => 'Autoload folder for PHP files.',
'chmod' => Util::filesystemInfo($f3->get('AUTOLOAD'))['chmod']
],
'FAVICON' => [
'label' => 'FAVICON',
'value' => $f3->get('FAVICON'),
'check' => true,
'tooltip' => 'Folder for Favicons.',
'chmod' => Util::filesystemInfo($f3->get('FAVICON'))['chmod']
],
'HISTORY' => [
'label' => 'HISTORY [optional]',
'value' => Config::getPathfinderData('history.log'),
'check' => true,
'tooltip' => 'Folder for log history files. (e.g. change logs for maps).',
'chmod' => Util::filesystemInfo(Config::getPathfinderData('history.log'))['chmod']
],
'CONFIG' => [
'label' => 'CONFIG PATH [optional]',
'value' => implode(' ', (array)$f3->get('CONF')),
'check' => true,
'tooltip' => 'Folder for custom *.ini files. (e.g. when overwriting of default values in app/*.ini)'
]
];
return $directoryData;
}
/**
* check all required backend requirements
* (Fat Free Framework)
* @param \Base $f3
* @return array
*/
protected function checkRequirements(\Base $f3){
protected function checkRequirements(\Base $f3) : array {
// server type ------------------------------------------------------------------
$serverData = self::getServerData(0);
$checkRequirements = [
@@ -516,7 +594,7 @@ class Setup extends Controller {
$modNotFoundMsg = 'Module status can not be identified. '
. 'This can happen if PHP runs as \'FastCGI\'. Please check manual! ';
// mod_rewrite check ------------------------------------------------------------
// mod_rewrite check --------------------------------------------------------------------------------------
$modRewriteCheck = false;
$modRewriteVersion = 'disabled';
$modRewriteTooltip = false;
@@ -539,7 +617,7 @@ class Setup extends Controller {
'tooltip' => $modRewriteTooltip
];
// mod_headers check ------------------------------------------------------------
// mod_headers check --------------------------------------------------------------------------------------
$modHeadersCheck = false;
$modHeadersVersion = 'disabled';
$modHeadersTooltip = false;
@@ -572,6 +650,11 @@ class Setup extends Controller {
* @return array
*/
protected function checkPHPConfig(\Base $f3): array {
$memoryLimit = (int)ini_get('memory_limit');
$maxInputVars = (int)ini_get('max_input_vars');
$maxExecutionTime = (int)ini_get('max_execution_time'); // 0 == infinite
$htmlErrors = (int)ini_get('html_errors');
$phpConfig = [
'exec' => [
'label' => 'exec()',
@@ -583,29 +666,29 @@ class Setup extends Controller {
'memoryLimit' => [
'label' => 'memory_limit',
'required' => $f3->get('REQUIREMENTS.PHP.MEMORY_LIMIT'),
'version' => ini_get('memory_limit'),
'check' => ini_get('memory_limit') >= $f3->get('REQUIREMENTS.PHP.MEMORY_LIMIT'),
'version' => $memoryLimit,
'check' => $memoryLimit >= $f3->get('REQUIREMENTS.PHP.MEMORY_LIMIT'),
'tooltip' => 'PHP default = 64MB.'
],
'maxInputVars' => [
'label' => 'max_input_vars',
'required' => $f3->get('REQUIREMENTS.PHP.MAX_INPUT_VARS'),
'version' => ini_get('max_input_vars'),
'check' => ini_get('max_input_vars') >= $f3->get('REQUIREMENTS.PHP.MAX_INPUT_VARS'),
'version' => $maxInputVars,
'check' => $maxInputVars >= $f3->get('REQUIREMENTS.PHP.MAX_INPUT_VARS'),
'tooltip' => 'PHP default = 1000. Increase it in order to import larger maps.'
],
'maxExecutionTime' => [
'label' => 'max_execution_time',
'required' => $f3->get('REQUIREMENTS.PHP.MAX_EXECUTION_TIME'),
'version' => ini_get('max_execution_time'),
'check' => ini_get('max_execution_time') >= $f3->get('REQUIREMENTS.PHP.MAX_EXECUTION_TIME'),
'version' => $maxExecutionTime,
'check' => !$maxExecutionTime || $maxExecutionTime >= $f3->get('REQUIREMENTS.PHP.MAX_EXECUTION_TIME'),
'tooltip' => 'PHP default = 30. Max execution time for PHP scripts.'
],
'htmlErrors' => [
'label' => 'html_errors',
'required' => $f3->get('REQUIREMENTS.PHP.HTML_ERRORS'),
'version' => (int)ini_get('html_errors'),
'check' => (bool)ini_get('html_errors') == (bool)$f3->get('REQUIREMENTS.PHP.HTML_ERRORS'),
'version' => $htmlErrors,
'check' => (bool)$htmlErrors == (bool)$f3->get('REQUIREMENTS.PHP.HTML_ERRORS'),
'tooltip' => 'Formatted HTML StackTrace on error.'
],
[
@@ -640,70 +723,209 @@ class Setup extends Controller {
* @param \Base $f3
* @return array
*/
protected function checkRedisConfig(\Base $f3): array {
protected function checkRedisInformation(\Base $f3): array {
$redisConfig = [];
if($this->getCacheType($f3) === 'redis'){
// we need to access the "protected" member $ref from F3´s Cache class
// to get access to the underlying Redis() class
$ref = new \ReflectionObject($cache = \Cache::instance());
$prop = $ref->getProperty('ref');
$prop->setAccessible(true);
if(
extension_loaded('redis') &&
class_exists('\Redis')
){
// collection of DSN specific $conf array (host, port, db,..)
$dsnData = [];
/**
* @var $redis \Redis
* get client information for a Redis client
* @param \Redis $client
* @param array $conf
* @return array
*/
$redis = $prop->getValue($cache);
$getClientInfo = function(\Redis $client, array $conf) : array {
$redisInfo = [
'dsn' => [
'label' => 'DNS',
'value' => $conf['host'] . ':' . $conf['port']
],
'connected' => [
'label' => 'status',
'value' => $client->isConnected()
]
];
$redisServerInfo = (array)$redis->info('SERVER');
$redisMemoryInfo = (array)$redis->info('MEMORY');
$redisStatsInfo = (array)$redis->info('STATS');
return $redisInfo;
};
$redisConfig = [
'redisVersion' => [
'label' => 'redis_version',
'required' => number_format((float)$f3->get('REQUIREMENTS.REDIS.VERSION'), 1, '.', ''),
'version' => $redisServerInfo['redis_version'],
'check' => version_compare( $redisServerInfo['redis_version'], $f3->get('REQUIREMENTS.REDIS.VERSION'), '>='),
'tooltip' => 'Redis server version'
],
'maxMemory' => [
'label' => 'maxmemory',
'required' => $this->convertBytes($f3->get('REQUIREMENTS.REDIS.MAX_MEMORY')),
'version' => $this->convertBytes($redisMemoryInfo['maxmemory']),
'check' => $redisMemoryInfo['maxmemory'] >= $f3->get('REQUIREMENTS.REDIS.MAX_MEMORY'),
'tooltip' => 'Max memory limit for Redis'
],
'usedMemory' => [
'label' => 'used_memory',
'version' => $this->convertBytes($redisMemoryInfo['used_memory']),
'check' => $redisMemoryInfo['used_memory'] < $redisMemoryInfo['maxmemory'],
'tooltip' => 'Current memory used by Redis'
],
'usedMemoryPeak' => [
'label' => 'used_memory_peak',
'version' => $this->convertBytes($redisMemoryInfo['used_memory_peak']),
'check' => $redisMemoryInfo['used_memory_peak'] <= $redisMemoryInfo['maxmemory'],
'tooltip' => 'Peak memory used by Redis'
],
'maxmemoryPolicy' => [
'label' => 'maxmemory_policy',
'required' => $f3->get('REQUIREMENTS.REDIS.MAXMEMORY_POLICY'),
'version' => $redisMemoryInfo['maxmemory_policy'],
'check' => $redisMemoryInfo['maxmemory_policy'] == $f3->get('REQUIREMENTS.REDIS.MAXMEMORY_POLICY'),
'tooltip' => 'How Redis behaves if \'maxmemory\' limit reached'
],
'evictedKeys' => [
'label' => 'evicted_keys',
'version' => $redisStatsInfo['evicted_keys'],
'check' => !(bool)$redisStatsInfo['evicted_keys'],
'tooltip' => 'Number of evicted keys due to maxmemory limit'
],
'dbSize' . $redis->getDbNum() => [
'label' => 'Size DB (' . $redis->getDbNum() . ')',
'version' => $redis->dbSize(),
'check' => $redis->dbSize() > 0,
'tooltip' => 'Keys found in DB (' . $redis->getDbNum() . ') [Cache DB]'
]
/**
* get status information for a Redis client
* @param \Redis $client
* @return array
*/
$getClientStats = function(\Redis $client) use ($f3) : array {
$redisStats = [];
if($client->isConnected()){
$redisServerInfo = (array)$client->info('SERVER');
$redisMemoryInfo = (array)$client->info('MEMORY');
$redisStatsInfo = (array)$client->info('STATS');
$redisStats = [
'redisVersion' => [
'label' => 'redis_version',
'required' => number_format((float)$f3->get('REQUIREMENTS.REDIS.VERSION'), 1, '.', ''),
'version' => $redisServerInfo['redis_version'],
'check' => version_compare( $redisServerInfo['redis_version'], $f3->get('REQUIREMENTS.REDIS.VERSION'), '>='),
'tooltip' => 'Redis server version'
],
'maxMemory' => [
'label' => 'maxmemory',
'required' => $this->convertBytes($f3->get('REQUIREMENTS.REDIS.MAX_MEMORY')),
'version' => $this->convertBytes($redisMemoryInfo['maxmemory']),
'check' => $redisMemoryInfo['maxmemory'] >= $f3->get('REQUIREMENTS.REDIS.MAX_MEMORY'),
'tooltip' => 'Max memory limit for Redis'
],
'usedMemory' => [
'label' => 'used_memory',
'version' => $this->convertBytes($redisMemoryInfo['used_memory']),
'check' => $redisMemoryInfo['used_memory'] < $redisMemoryInfo['maxmemory'],
'tooltip' => 'Current memory used by Redis'
],
'usedMemoryPeak' => [
'label' => 'used_memory_peak',
'version' => $this->convertBytes($redisMemoryInfo['used_memory_peak']),
'check' => $redisMemoryInfo['used_memory_peak'] <= $redisMemoryInfo['maxmemory'],
'tooltip' => 'Peak memory used by Redis'
],
'maxmemoryPolicy' => [
'label' => 'maxmemory_policy',
'required' => $f3->get('REQUIREMENTS.REDIS.MAXMEMORY_POLICY'),
'version' => $redisMemoryInfo['maxmemory_policy'],
'check' => $redisMemoryInfo['maxmemory_policy'] == $f3->get('REQUIREMENTS.REDIS.MAXMEMORY_POLICY'),
'tooltip' => 'How Redis behaves if \'maxmemory\' limit reached'
],
'evictedKeys' => [
'label' => 'evicted_keys',
'version' => $redisStatsInfo['evicted_keys'],
'check' => !(bool)$redisStatsInfo['evicted_keys'],
'tooltip' => 'Number of evicted keys due to maxmemory limit'
],
[
'label' => 'Databases'
]
];
}
return $redisStats;
};
/**
* get database status for current selected db
* @param \Redis $client
* @param string $tag
* @return array
*/
$getDatabaseStatus = function(\Redis $client, string $tag) : array {
$redisDatabases = [];
if($client->isConnected()){
$dbNum = $client->getDbNum();
$dbSize = $client->dbSize();
$redisDatabases = [
'db_' . $dbNum => [
'label' => '<i class="fas fa-fw fa-database"></i> db(' . $dbNum . ') : ' . $tag,
'version' => $dbSize . ' keys',
'check' => $dbSize > 0,
'tooltip' => 'Keys in db(' . $dbNum . ')',
'task' => [
[
'action' => http_build_query([
'action' => 'flushRedisDb',
'host' => $client->getHost(),
'port' => $client->getPort(),
'db' => $dbNum
]) . '#pf-setup-cache',
'label' => 'Flush',
'icon' => 'fa-trash',
'btn' => 'btn-danger' . (($dbSize > 0) ? '' : ' disabled')
]
]
]
];
}
return $redisDatabases;
};
/**
* build (modify) $redisConfig with DNS $conf data
* @param array $conf
*/
$buildRedisConfig = function(array $conf) use (&$redisConfig, $getClientInfo, $getClientStats, $getDatabaseStatus){
if($conf['type'] == 'redis'){
// is Redis -> group all DNS by host:port
$client = new \Redis();
try{
$client->connect($conf['host'], $conf['port'], 0.3);
if(isset($conf['db'])) {
$client->select($conf['db']);
}
$conf['db'] = $client->getDbNum();
}catch(\RedisException $e){
// connection failed
}
if(!array_key_exists($uid = $conf['host'] . ':' . $conf['port'], $redisConfig)){
$redisConfig[$uid] = $getClientInfo($client, $conf);
$redisConfig[$uid]['status'] = $getClientStats($client) + $getDatabaseStatus($client, $conf['tag']);
}elseif(!array_key_exists($uidDb = 'db_' . $conf['db'], $redisConfig[$uid]['status'])){
$redisConfig[$uid]['status'] += $getDatabaseStatus($client, $conf['tag']);
}else{
$redisConfig[$uid]['status'][$uidDb]['label'] .= '; ' . $conf['tag'];
}
$client->close();
}
};
// potential Redis caches ---------------------------------------------------------------------------------
$redisCaches = [
'CACHE' => $f3->get('CACHE'),
'API_CACHE' => $f3->get('API_CACHE')
];
foreach($redisCaches as $tag => $dsn){
if(Config::parseDSN($dsn, $conf)){
$conf['tag'] = $tag;
$dsnData[] = $conf;
}
}
// if Session handler is also Redis -> add this as well ---------------------------------------------------
// -> the DSN format is not the same, convert URL format into DSN
if(
strtolower(session_module_name()) == 'redis' &&
($parts = parse_url(strtolower(session_save_path())))
){
// parse URL parameters
parse_str((string)$parts['query'], $params);
$conf = [
'type' => 'redis',
'host' => $parts['host'],
'port' => $parts['port'],
'db' => !empty($params['database']) ? (int)$params['database'] : 0,
'tag' => 'SESSION'
];
$dsnData[] = $conf;
}
// sort all $dsnData by 'db' number -----------------------------------------------------------------------
usort($dsnData, function($a, $b){
return $a['db'] <=> $b['db'];
});
foreach($dsnData as $conf){
$buildRedisConfig($conf);
}
}
return $redisConfig;
@@ -989,14 +1211,14 @@ class Setup extends Controller {
$changedIndex = false;
$addConstraints = [];
// set (new) column information -------------------------------------------------------
// set (new) column information -----------------------------------------------------------
$requiredTables[$requiredTableName]['fieldConf'][$columnName]['exists'] = true;
$requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentType'] = $currentColType;
$requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentNullable'] = $hasNullable;
$requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentIndex'] = $hasIndex;
$requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentUnique'] = $hasUnique;
// check constraint -------------------------------------------------------------------
// check constraint -----------------------------------------------------------------------
if(isset($fieldConf['constraint'])){
// add or update constraints
foreach((array)$fieldConf['constraint'] as $constraintData){
@@ -1022,7 +1244,7 @@ class Setup extends Controller {
}
}
// check type changed -----------------------------------------------------------------
// check type changed ---------------------------------------------------------------------
if(
$fieldConf['type'] !== 'JSON' &&
!$schema->isCompatible($fieldConf['type'], $currentColType)
@@ -1033,14 +1255,14 @@ class Setup extends Controller {
$tableStatusCheckCount++;
}
// check if column nullable changed ---------------------------------------------------
// check if column nullable changed -------------------------------------------------------
if( $currentNullable != $fieldConf['nullable']){
$changedNullable = true;
$columnStatusCheck = false;
$tableStatusCheckCount++;
}
// check if column index changed ------------------------------------------------------
// check if column index changed ----------------------------------------------------------
$indexUpdate = false;
$indexKey = (bool)$hasIndex;
$indexUnique = (bool)$hasUnique;
@@ -1054,7 +1276,7 @@ class Setup extends Controller {
$indexKey = (bool)$fieldConf['index'];
}
// check if column unique changed -----------------------------------------------------
// check if column unique changed ---------------------------------------------------------
if($currentColIndexData['unique'] != $fieldConf['unique']){
$changedUnique = true;
$columnStatusCheck = false;
@@ -1064,7 +1286,7 @@ class Setup extends Controller {
$indexUnique = (bool)$fieldConf['unique'];
}
// build table with changed columns ---------------------------------------------------
// build table with changed columns -------------------------------------------------------
if(!$columnStatusCheck || !$foreignKeyStatusCheck){
if(!$columnStatusCheck ){
@@ -1106,7 +1328,7 @@ class Setup extends Controller {
}
}
// set (new) column information -------------------------------------------------------
// set (new) column information -----------------------------------------------------------
$requiredTables[$requiredTableName]['fieldConf'][$columnName]['changedType'] = $changedType;
$requiredTables[$requiredTableName]['fieldConf'][$columnName]['changedNullable'] = $changedNullable;
$requiredTables[$requiredTableName]['fieldConf'][$columnName]['changedUnique'] = $changedUnique;
@@ -1186,12 +1408,13 @@ class Setup extends Controller {
return $this->databases;
}
/** check MySQL params
/**
* check MySQL params
* @param \Base $f3
* @param $db
* @param SQL $db
* @return array
*/
protected function checkDBConfig(\Base $f3, $db){
protected function checkDBConfig(\Base $f3, SQL $db){
// some db like "Maria DB" have some strange version strings....
$dbVersionString = $db->version();
@@ -1214,19 +1437,13 @@ class Setup extends Controller {
]
];
// get specific MySQL config Value
$getDBConfigValue = function($db, $param){
$result = $db->exec([
//"USE " . $db->name(),
"SHOW VARIABLES LIKE '" . strtolower($param) . "'"
]);
$tmpResult = reset($result);
return !empty($result)? end($tmpResult) : 'unknown';
};
$mySQLConfigParams = $f3->get('REQUIREMENTS.MYSQL.VARS');
$mySQLConfigParams = (array)$f3->get('REQUIREMENTS.MYSQL.VARS');
foreach($mySQLConfigParams as $param => $requiredValue){
$value = $getDBConfigValue($db, $param);
// get current MySQL config value for $param
$result = $db->exec("SHOW VARIABLES LIKE '" . strtolower($param) . "'");
$tmpResult = reset($result);
$value = !empty($result)? end($tmpResult) : 'unknown';
$dbConfig[] = [
'label' => strtolower($param),
'required' => $requiredValue,
@@ -1360,7 +1577,7 @@ class Setup extends Controller {
[
'action' => 'clearIndex',
'label' => 'Clear',
'icon' => 'fa-times',
'icon' => 'fa-trash',
'btn' => 'btn-danger'
],[
'action' => 'buildIndex',
@@ -1473,39 +1690,96 @@ class Setup extends Controller {
}
/**
* get cache folder size as string
* get cache folder size
* @param \Base $f3
* @return array
*/
protected function getCacheData(\Base $f3){
protected function checkDirSize(\Base $f3) : array {
// limit shown cache size. Reduce page load on big cache. In Bytes
$maxBytes = 10 * 1024 * 1024; // 10MB
$dirTemp = (string)$f3->get('TEMP');
$cacheDsn = (string)$f3->get('CACHE');
Config::parseDSN($cacheDsn, $conf);
// if 'CACHE' is e.g. redis=... -> show default dir for cache
$dirCache = $conf['type'] == 'folder' ? $conf['folder'] : $dirTemp . 'cache/';
// get all cache -----------------------------------------------------------------------------------------
$cacheFilesAll = Search::getFilesByMTime( $f3->get('TEMP') );
$dirAll = [
'TEMP' => [
'label' => 'Temp dir',
'path' => $dirTemp
],
'CACHE' => [
'label' => 'Cache dir',
'path' => $dirCache
]
];
$maxHitAll = false;
$bytesAll = 0;
foreach($cacheFilesAll as $filename => $file) {
$bytesAll += $file->getSize();
}
// get data cache -----------------------------------------------------------------------------------------
$cacheFilesData = Search::getFilesByMTime( $f3->get('TEMP') . 'cache/' );
$bytesData = 0;
foreach($cacheFilesData as $filename => $file) {
$bytesData += $file->getSize();
foreach($dirAll as $key => $dirData){
$maxHit = false;
$bytes = 0;
$files = Search::getFilesByMTime($dirData['path']);
foreach($files as $filename => $file) {
$bytes += $file->getSize();
if($bytes > $maxBytes){
$maxHit = $maxHitAll = true;
break;
}
}
$bytesAll += $bytes;
$dirAll[$key]['size'] = ($maxHit ? '>' : '') . $this->convertBytes($bytes);
$dirAll[$key]['task'] = [
[
'action' => http_build_query([
'action' => 'clearFiles',
'path' => $dirData['path']
]),
'label' => 'Delete files',
'icon' => 'fa-trash',
'btn' => 'btn-danger' . (($bytes > 0) ? '' : ' disabled')
]
];
}
return [
'all' => $this->convertBytes($bytesAll),
'data' => $this->convertBytes($bytesData),
'template' => $this->convertBytes($bytesAll - $bytesData)
'sizeAll' => ($maxHitAll ? '>' : '') . $this->convertBytes($bytesAll),
'dirAll' => $dirAll
];
}
/**
* clear all cached files
* @param \Base $f3
* clear directory
* @param string $path
*/
protected function clearCache(\Base $f3){
$f3->clear('CACHE');
protected function clearFiles(string $path){
$files = Search::getFilesByMTime($path);
foreach($files as $filename => $file){
/**
* @var $file \SplFileInfo
*/
if($file->isFile()){
if($file->isWritable()){
unlink($file->getRealPath());
}
}
}
}
/**
* clear all key in a specific Redis database
* @param string $host
* @param int $port
* @param int $db
*/
protected function flushRedisDb(string $host, int $port, int $db = 0){
$client = new \Redis();
$client->connect($host, $port, 0.3);
$client->select($db);
$client->flushDB();
$client->close();
}
/**

View File

@@ -104,13 +104,13 @@ class CcpSystemsUpdate extends AbstractCron {
// get current jump data --------------------------------------------------------------------------------------
$time_start = microtime(true);
$jumpData = $f3->ccpClient->getUniverseJumps();
$jumpData = $f3->ccpClient()->getUniverseJumps();
$time_end = microtime(true);
$execTimeGetJumpData = $time_end - $time_start;
// get current kill data --------------------------------------------------------------------------------------
$time_start = microtime(true);
$killData = $f3->ccpClient->getUniverseKills();
$killData = $f3->ccpClient()->getUniverseKills();
$time_end = microtime(true);
$execTimeGetKillData = $time_end - $time_start;

View File

@@ -67,8 +67,7 @@ class CharacterUpdate extends AbstractCron {
if(is_object($characterLog->characterId)){
// force characterLog as "updated" even if no changes were made
$characterLog->characterId->updateLog([
'markUpdated' => true,
'suppressHTTPErrors' => true
'markUpdated' => true
]);
}else{
// character_log does not have a character assigned -> delete

View File

@@ -0,0 +1,125 @@
<?php
/**
* Created by PhpStorm.
* User: Exodus 4D
* Date: 22.12.2018
* Time: 15:48
*/
namespace Cron;
use data\mapper\SortingIterator;
use data\file\FileHandler;
use data\filesystem\Search;
class MapHistory extends AbstractCron {
const LOG_TEXT = '%s [%4s] log files, [%4s] not writable, [%4s] read error, [%4s] write error, [%4s] rename error, [%4s] delete error, exec (%.3Fs)';
/**
* default log file size limit before truncate, bytes (1MB)
*/
const LOG_SIZE_THRESHOLD = 1024 * 1024;
/**
* default count of log files that will be truncated
*/
const LOG_COUNT = 3;
/**
* default line limit after truncate
*/
const LOG_LINES = 1000;
/**
* get max log size threshold before truncate
* @param \Base $f3
* @return int
*/
protected function getMaxLogSize(\Base $f3) : int {
$logSize = (int)$f3->get('PATHFINDER.HISTORY.LOG_SIZE_THRESHOLD');
return ($logSize >= 0) ? ($logSize * 1024 * 1024) : self::LOG_SIZE_THRESHOLD;
}
/**
* get max log entries (lines) after truncate
* @param \Base $f3
* @return int
*/
protected function getMaxLogLines(\Base $f3) : int {
$logLines = (int)$f3->get('PATHFINDER.HISTORY.LOG_LINES');
return ($logLines >= 0) ? $logLines : self::LOG_LINES;
}
/**
* truncate map history log files and keep size small
* >> php index.php "/cron/truncateMapHistoryLogFiles"
* @param \Base $f3
*/
function truncateFiles(\Base $f3){
$timeStart = microtime(true);
$largeFiles = 0;
$notWritableFiles = 0;
$readErrors = 0;
$writeErrors = 0;
$renameErrors = 0;
$deleteErrors = 0;
if($f3->exists('PATHFINDER.HISTORY.LOG', $dir)){
$fileHandler = FileHandler::instance();
$dir = $f3->fixslashes('./' . $dir . 'map/');
$files = Search::getFilesBySize($dir, $this->getMaxLogSize($f3));
// sort by file size
$files = new SortingIterator($files, function( \SplFileInfo $a, \SplFileInfo $b){
return $b->getSize() - $a->getSize();
});
// limit files count for truncate
$files = new \LimitIterator($files, 0, self::LOG_COUNT);
foreach($files as $filename => $file){
/**
* @var $file \SplFileInfo
*/
if($file->isFile()){
$largeFiles++;
if($file->isWritable()){
// read newest logs from large files (reverse order) -> new log entries were appended...
$rowsData = $fileHandler->readFileReverse($file->getRealPath(), 0, self::LOG_LINES);
if(!empty($rowsData)){
// create temp file...
$temp = tempnam(sys_get_temp_dir(), 'map_');
// write newest logs into temp file...
$fileSizeNew = file_put_contents($temp, implode(PHP_EOL, array_reverse($rowsData)) . PHP_EOL, LOCK_EX);
if($fileSizeNew){
// move temp file from PHP temp dir into Pathfinders history log dir...
// ... overwrite old log file with new file
if(rename($temp, $file->getRealPath())){
// map history logs should be writable non cronjob user too
@chmod($file->getRealPath(), 0666);
}else{
$renameErrors++;
}
}else{
$writeErrors++;
}
}else{
$readErrors++;
}
}else{
$notWritableFiles++;
}
}
}
}
$execTime = microtime(true) - $timeStart;
// Log ------------------------
$log = new \Log('cron_' . __FUNCTION__ . '.log');
$log->write(sprintf(self::LOG_TEXT, __FUNCTION__, $largeFiles, $notWritableFiles, $readErrors, $writeErrors, $renameErrors, $deleteErrors, $execTime));
}
}

View File

@@ -34,7 +34,7 @@ class Universe extends AbstractCron {
}
/**
* format Byte §size for output
* format Byte $size for output
* @param int $size
* @return string
*/
@@ -177,7 +177,7 @@ class Universe extends AbstractCron {
switch($type){
case 'system':
// load systems + dependencies (planets, star, types,...)
$ids = $f3->ccpClient->getUniverseSystems();
$ids = $f3->ccpClient()->getUniverseSystems();
$modelClass = 'SystemModel';
$setupModel = function(Model\Universe\SystemModel &$model, int $id){
$model->loadById($id);
@@ -186,7 +186,7 @@ class Universe extends AbstractCron {
break;
case 'stargate':
// load all stargates. Systems must be present first!
$ids = $f3->ccpClient->getUniverseSystems();
$ids = $f3->ccpClient()->getUniverseSystems();
$modelClass = 'SystemModel';
$setupModel = function(Model\Universe\SystemModel &$model, int $id){
$model->loadById($id);
@@ -195,7 +195,7 @@ class Universe extends AbstractCron {
break;
case 'index_system':
// setup system index, Systems must be present first!
$ids = $f3->ccpClient->getUniverseSystems();
$ids = $f3->ccpClient()->getUniverseSystems();
$modelClass = 'SystemModel';
$setupModel = function(Model\Universe\SystemModel &$model, int $id){
$model->getById($id); // no loadById() here! would take "forever" when system not exists and build up first...

View File

@@ -21,20 +21,19 @@ class FileHandler extends \Prefab {
const Log_File_LIMIT_MAX = 100;
/**
* parse local log file from end to first line
* -> Each row is a JSON object
* parse file from end to first line
* @param string $sourceFile
* @param int $offset
* @param int $limit
* @param null|callable $formatter
* @param \Closure|null $rowParser
* @return array
*/
public static function readLogFile(
public function readFileReverse(
string $sourceFile,
int $offset = self::LOG_FILE_OFFSET,
int $limit = self::LOG_FILE_LIMIT,
$formatter = null
): array {
\Closure $rowParser = null
) : array {
$data = [];
if(is_file($sourceFile)){
@@ -43,11 +42,11 @@ class FileHandler extends \Prefab {
$file->setFlags(\SplFileObject::DROP_NEW_LINE | \SplFileObject::READ_AHEAD | \SplFileObject::SKIP_EMPTY);
foreach( new \LimitIterator($file, 0, $limit) as $i => $rowData){
if( !empty($rowDataObj = (array)json_decode($rowData, true)) ){
if(is_callable($formatter)){
$formatter($rowDataObj);
}
$data[] = $rowDataObj;
if(is_callable($rowParser)){
// custom parser for row data -> manipulate $data by ref
$rowParser($rowData, $data);
}else{
$data[] = $rowData;
}
}
}else{

View File

@@ -12,58 +12,79 @@ namespace data\filesystem;
class Search {
/**
* max file count that should be deleted in this session
* max file count that can be returned
*/
const DEFAULT_FILE_LIMIT = 1000;
/**
* timestamp (seconds) filter files by mTime()
* -> default = "no filter"
* @var int
*/
static $filterTime = 0;
/**
* recursive file filter by mTime
* @param string $dir
* @param int $mTime
* @param null $mTime
* @param int $limit
* @return array|\LimitIterator
* @return \Traversable
*/
static function getFilesByMTime(string $dir, $mTime = null, $limit = self::DEFAULT_FILE_LIMIT){
$files = [];
static function getFilesByMTime(string $dir, $mTime = null, $limit = self::DEFAULT_FILE_LIMIT) : \Traversable {
$mTime = is_null($mTime) ? time() : (int)$mTime;
if(is_dir($dir)){
if(is_null($mTime)){
self::$filterTime = time();
}else{
self::$filterTime = (int)$mTime;
$filterCallback = function($current, $key, $iterator) use ($mTime) {
/**
* @var $current \RecursiveDirectoryIterator
*/
if (
!$current->isFile() || // allow recursion
(
strpos($current->getFilename(), '.') !== 0 && // skip e.g. ".gitignore"
$current->getMTime() < $mTime // filter last modification date
)
){
return true;
}
return false;
};
$directory = new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS );
$files = new \RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) {
// Check for last modification date
/**
* @var $current \RecursiveDirectoryIterator
*/
if (
!$current->isFile() || // allow recursion
(
strpos($current->getFilename(), '.') !== 0 && // skip e.g. ".gitignore"
$current->getMTime() < self::$filterTime // check last modification date
)
){
return true;
}
return false;
});
// limit max files
$files = new \LimitIterator($files, 0, $limit);
}
return $files;
return self::getFilesByCallback($dir, $filterCallback, $limit);
}
/**
* recursive file filter by size
* @param string $dir
* @param int $size
* @param int $limit
* @return \Traversable
*/
static function getFilesBySize(string $dir, int $size = 0, int $limit = self::DEFAULT_FILE_LIMIT) : \Traversable {
$filterCallback = function($current, $key, $iterator) use ($size) {
/**
* @var $current \RecursiveDirectoryIterator
*/
if (
!$current->isFile() || // allow recursion
(
strpos($current->getFilename(), '.') !== 0 && // skip e.g. ".gitignore"
$current->getSize() > $size // filter file size
)
){
return true;
}
return false;
};
return self::getFilesByCallback($dir, $filterCallback, $limit);
}
/**
* @param string $dir
* @param \Closure $filterCallback
* @param int $limit
* @return \Traversable
*/
private static function getFilesByCallback(string $dir, \Closure $filterCallback, int $limit = self::DEFAULT_FILE_LIMIT) : \Traversable {
$files = new \ArrayIterator();
if(is_dir($dir)){
$directory = new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS );
$files = new \RecursiveCallbackFilterIterator($directory, $filterCallback);
}
return new \LimitIterator($files, 0, $limit);
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Created by PhpStorm.
* User: Exodus 4D
* Date: 24.12.2018
* Time: 00:55
*/
namespace data\mapper;
class SortingIterator extends \ArrayIterator {
public function __construct(\Traversable $iterator, callable $callback){
parent::__construct(iterator_to_array($iterator));
// sort by custom function
$this->uasort($callback);
}
}

View File

@@ -7,6 +7,8 @@
*/
namespace DB;
use controller\LogController;
use lib\Config;
@@ -29,7 +31,7 @@ class Database extends \Prefab {
* @param string $dbKey
* @return SQL|null
*/
public function connectToServer(string $dbKey = 'PF'){
public function connectToServer(string $dbKey = 'PF') : ?SQL {
$dbConfig = Config::getDatabaseConfig($dbKey);
$dbConfig['DNS'] = str_replace(';dbname=', '', $dbConfig['DNS'] );
$dbConfig['NAME'] = '';
@@ -42,7 +44,7 @@ class Database extends \Prefab {
* @param string $dbKey
* @return SQL|null
*/
public function createDB(string $dbKey = 'PF'){
public function createDB(string $dbKey = 'PF') : ?SQL {
$db = null;
$dbConfig = Config::getDatabaseConfig($dbKey);
// remove database from $dsn (we want to crate it)
@@ -51,6 +53,9 @@ class Database extends \Prefab {
$dbConfig['NAME'] = '';
$dbConfig['DNS'] = str_replace(';dbname=', '', $dbConfig['DNS'] );
/**
* @var $db SQL|null
*/
$db = call_user_func_array([$this, 'connect'], $dbConfig);
if(!is_null($db)){
@@ -83,12 +88,15 @@ class Database extends \Prefab {
* @param string $dbKey
* @return SQL|null
*/
public function getDB(string $dbKey = 'PF'){
public function getDB(string $dbKey = 'PF') : ?SQL {
$f3 = \Base::instance();
// "Hive" Key for DB object cache
$dbHiveKey = $this->getDbHiveKey($dbKey);
if( !$f3->exists($dbHiveKey, $db) ){
$dbConfig = Config::getDatabaseConfig($dbKey);
/**
* @var $db SQL|null
*/
$db = call_user_func_array([$this, 'connect'], $dbConfig);
if(!is_null($db)){
self::prepareDBConnection($db);
@@ -104,7 +112,7 @@ class Database extends \Prefab {
* @param $dbKey
* @return string
*/
protected function getDbHiveKey($dbKey){
protected function getDbHiveKey(string $dbKey) : string {
return 'DB_' . $dbKey;
}
@@ -117,7 +125,7 @@ class Database extends \Prefab {
* @param string $alias
* @return SQL|null
*/
protected function connect($dns, $name, $user, $password, $alias){
protected function connect(string $dns, string $name, string $user, string $password, string $alias) : ?SQL {
$db = null;
$f3 = \Base::instance();
@@ -155,18 +163,18 @@ class Database extends \Prefab {
* @param string $dbKey
* @return array|bool
*/
public function getTables($dbKey = 'PF'){
public function getTables(string $dbKey = 'PF'){
$schema = new SQL\Schema( $this->getDB($dbKey) );
return $schema->getTables();
}
/**
* checks whether a table exists on a DB or not
* @param $table
* @param string $table
* @param string $dbKey
* @return bool
*/
public function tableExists($table, $dbKey = 'PF'){
public function tableExists(string $table, string $dbKey = 'PF') : bool {
$tableNames = $this->getTables($dbKey);
return in_array($table, $tableNames);
}
@@ -174,11 +182,11 @@ class Database extends \Prefab {
/**
* get current row (data) count for an existing table
* -> returns 0 if table not exists or empty
* @param $table
* @param string $table
* @param string $dbKey
* @return int
*/
public function getRowCount($table, $dbKey = 'PF') {
public function getRowCount(string $table, string $dbKey = 'PF') : int {
$count = 0;
if( $this->tableExists($table, $dbKey) ){
$db = $this->getDB($dbKey);
@@ -193,7 +201,7 @@ class Database extends \Prefab {
/**
* @return bool
*/
public function isSilent() : bool{
public function isSilent() : bool {
return $this->silent;
}
@@ -251,11 +259,15 @@ class Database extends \Prefab {
*/
public static function prepareDBConnection(SQL &$db){
// set DB timezone to UTC +00:00 (eve server time)
$db->exec('SET @@session.time_zone = "+00:00";');
// set default storage engine
$db->exec('SET @@session.default_storage_engine = "' .
self::getRequiredMySqlVariables('DEFAULT_STORAGE_ENGINE') . '"');
$db->exec([
'SET @@session.time_zone = :time_zone',
'SET @@session.default_storage_engine = :storage_engine'
], [
[':time_zone' => '+00:00'],
[':storage_engine' => self::getRequiredMySqlVariables('DEFAULT_STORAGE_ENGINE')]
]
);
}
/**
@@ -277,7 +289,7 @@ class Database extends \Prefab {
* @param string $key
* @return string|null
*/
public static function getRequiredMySqlVariables(string $key){
public static function getRequiredMySqlVariables(string $key) : ?string {
\Base::instance()->exists('REQUIREMENTS[MYSQL][VARS][' . $key . ']', $data);
return $data;
}
@@ -286,7 +298,7 @@ class Database extends \Prefab {
* get logger for DB logging
* @return \Log
*/
static function getLogger(){
static function getLogger() : \Log {
return LogController::getLogger('ERROR');
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Created by PhpStorm.
* User: Exodus 4D
* Date: 02.12.2018
* Time: 15:40
*/
namespace DB\SQL\MySQL;
class Session extends \DB\SQL\Session {
function __construct(\DB\SQL $db, string $table = 'sessions', bool $force = true, $onsuspect = null, $key = null){
if($force){
// create sessions table
// -> We use this "custom" SQl rather than the default in parent::__construct()
// because of the defaults 'data' column type TEXT
$dbName = $db->name();
$sql = "CREATE TABLE IF NOT EXISTS ";
$sql .= $dbName ? $db->quotekey($dbName,FALSE) . "." : "";
$sql .= $db->quotekey($table,FALSE) . " (";
$sql .= $db->quotekey('session_id') . " VARCHAR(255),";
$sql .= $db->quotekey('data') . " MEDIUMTEXT,";
$sql .= $db->quotekey('ip') . " VARCHAR(45),";
$sql .= $db->quotekey('agent') . " VARCHAR(300),";
$sql .= $db->quotekey('stamp') . " INT(11),";
$sql .= "PRIMARY KEY (" . $db->quotekey('session_id') . ")";
$sql .= ");";
$db->exec($sql);
}
// $force = false for parent constructor -> skip default create SQL
parent::__construct($db, $table, false, $onsuspect, $key);
}
}

View File

@@ -0,0 +1,337 @@
<?php
/**
* Created by PhpStorm.
* User: Exodus 4D
* Date: 26.12.2018
* Time: 17:41
*/
namespace lib\api;
use Cache\Adapter\Filesystem\FilesystemCachePool;
use Cache\Adapter\PHPArray\ArrayCachePool;
use Cache\Adapter\Redis\RedisCachePool;
use Cache\Namespaced\NamespacedCachePool;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem;
use lib\Config;
use lib\Util;
use lib\logging;
use controller\LogController;
use Exodus4D\ESI\Client\ApiInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\RequestInterface;
/**
* Class AbstractClient
* @package lib\api
*
* @method ApiInterface getUrl()
* @method ApiInterface getTimeout()
* @method ApiInterface getConnectTimeout()
* @method ApiInterface getReadTimeout()
* @method ApiInterface getProxy()
* @method ApiInterface getVerify()
* @method ApiInterface getDebugRequests()
* @method ApiInterface getDataSource()
*/
abstract class AbstractClient extends \Prefab {
const ERROR_CLIENT_INVALID = "HTTP API client not found → Check installed Composer packages";
/**
* @var string|null
*/
const CLIENT_NAME = null;
/**
* @var string|null
*/
protected $clientName = null;
/**
* @var ApiInterface|null
*/
protected $client = null;
/**
* PSR-6 compatible CachePool instance
* -> can be Redis, Filesystem or Array cachePool
* -> used by e.g. GuzzleCacheMiddleware
* @var CacheItemPoolInterface|null
*/
protected $cachePool = null;
/**
* @param \Base $f3
* @return ApiInterface|null
*/
abstract protected function getClient(\Base $f3) : ?ApiInterface;
/**
* get userAgent
* @return string
*/
protected function getUserAgent() : string {
$userAgent = '';
$userAgent .= Config::getPathfinderData('name');
$userAgent .= ' - ' . Config::getPathfinderData('version');
$userAgent .= ' | ' . Config::getPathfinderData('contact');
$userAgent .= ' (' . $_SERVER['SERVER_NAME'] . ')';
return $userAgent;
}
/**
* returns a new Log object used within the Api for logging
* @return \Closure
*/
protected function newLog() : \Closure {
return function(string $action, string $level = 'warning') : logging\LogInterface {
$log = new logging\ApiLog($action, $level);
$log->addHandler('stream', 'json', $this->getStreamConfig($action));
return $log;
};
}
/**
* returns a new instance of PSR-6 compatible CacheItemPoolInterface
* -> this Cache backend will be used across Guzzle Middleware
* e.g. GuzzleCacheMiddleware
* @see http://www.php-cache.com
* @param \Base $f3
* @return \Closure
*/
protected function getCachePool(\Base $f3) : \Closure {
// determine cachePool options
$poolConfig = $this->getCachePoolConfig($f3);
return function() use ($poolConfig) : ?CacheItemPoolInterface {
// an active CachePool should be re-used
// -> no need for e.g. a new Redis->connect()
// and/or re-init when it is used the next time
if(!is_null($this->cachePool)){
return $this->cachePool;
}
// Redis is preferred option (best performance) -----------------------------------------------------------
if(
$poolConfig['type'] == 'redis' &&
extension_loaded('redis') &&
class_exists('\Redis') &&
class_exists(RedisCachePool::class)
){
$client = new \Redis();
if(
$client->connect(
$poolConfig['host'],
$poolConfig['port'],
Config::REDIS_OPT_TIMEOUT,
null,
Config::REDIS_OPT_RETRY_INTERVAL,
Config::REDIS_OPT_READ_TIMEOUT
)
){
if(isset($poolConfig['db'])){
$client->select($poolConfig['db']);
}
$poolRedis = new RedisCachePool($client);
// RedisCachePool supports "Hierarchy" store slots
// -> "Hierarchy" support is required to use it in a NamespacedCachePool
// This helps to separate keys by a namespace
// @see http://www.php-cache.com/en/latest/
$this->cachePool = new NamespacedCachePool($poolRedis, static::CLIENT_NAME);
}
}
// Filesystem is second option and fallback for failed Redis pool -----------------------------------------
if(
is_null($this->cachePool) &&
in_array($poolConfig['type'], ['redis', 'folder']) &&
class_exists(FilesystemCachePool::class)
){
$filesystemAdapter = new Local('./');
$filesystem = new Filesystem($filesystemAdapter);
$poolFilesystem = new FilesystemCachePool($filesystem);
$poolFilesystem->setFolder($poolConfig['folder']);
$this->cachePool = $poolFilesystem;
}
// Array cache pool fallback (not persistent) -------------------------------------------------------------
if(
is_null($this->cachePool) &&
in_array($poolConfig['type'], ['redis', 'folder', 'array']) &&
class_exists(ArrayCachePool::class)
){
$this->cachePool = new ArrayCachePool(2000);
}
return $this->cachePool;
};
}
/**
* get cachePool config from [D]ata [S]ource [N]ame string
* @param \Base $f3
* @return array
*/
protected function getCachePoolConfig(\Base $f3) : array {
$dsn = (string)$f3->get('API_CACHE');
// fallback
$conf = ['type' => 'array'];
if(!empty($folder = (string)$f3->get('TEMP'))){
// filesystem (better than 'array' cache)
$conf = [
'type' => 'folder',
'folder' => $folder . 'cache/'
];
}
// redis or filesystem -> overwrites $conf
Config::parseDSN($dsn, $conf);
return $conf;
}
/**
* return callback function that expects a $request and checks
* whether it should be logged (in case of errors)
* @param \Base $f3
* @return \Closure
*/
protected function isLoggable(\Base $f3) : \Closure {
return function(RequestInterface $request) use ($f3) : bool {
// we need the timestamp for $request that should be checked
// -> we assume $request was "recently" send. -> current server time is used for check
$requestTime = $f3->get('getDateTime')();
// ... "interpolate" time to short interval
// -> this might help to re-use sequential calls of this method
Util::roundToInterval($requestTime);
// check if request was send within ESI downTime range
// -> errors during downTime should not be logged
$inDowntimeRange = Config::inDownTimeRange($requestTime);
return !$inDowntimeRange;
};
}
/**
* get Logger
* @param string $ype
* @return \Log
*/
protected function getLogger(string $ype = 'ERROR') : \Log {
return LogController::getLogger($ype);
}
/**
* get error msg for missing $this->client class
* @param string $class
* @return string
*/
protected function getMissingClassError(string $class) : string {
return sprintf(Config::ERROR_CLASS_NOT_EXISTS_COMPOSER, $class);
}
/**
* get error msg for undefined method in $this->client class
* @param string $class
* @param string $method
* @return string
*/
protected function getMissingMethodError(string $class, string $method) : string {
return sprintf(Config::ERROR_METHOD_NOT_EXISTS_COMPOSER, $method, $class);
}
/**
* get config for stream logging
* @param string $logFileName
* @param bool $abs
* @return \stdClass
*/
protected function getStreamConfig(string $logFileName, bool $abs = false) : \stdClass {
$f3 = \Base::instance();
$config = (object) [];
$config->stream = '';
if( $f3->exists('LOGS', $dir) ){
$config->stream .= $abs ? $f3->get('ROOT') . '/' : './';
$config->stream .= $dir . $logFileName . '.log';
$config->stream = $f3->fixslashes($config->stream);
}
return $config;
}
/**
* call request API data
* @param string $name
* @param array $arguments
* @return array|mixed
*/
public function __call(string $name, array $arguments = []){
$return = [];
if(is_object($this->client)){
if( method_exists($this->client, $name) ){
$return = call_user_func_array([$this->client, $name], $arguments);
}else{
$errorMsg = $this->getMissingMethodError(get_class($this->client), $name);
$this->getLogger('ERROR')->write($errorMsg);
\Base::instance()->error(501, $errorMsg);
}
}else{
\Base::instance()->error(501, self::ERROR_CLIENT_INVALID);
}
return $return;
}
/**
* init web client on __invoke()
* -> no need to init client on __construct()
* maybe it is nerer used...
* @return AbstractClient
*/
function __invoke() : self {
$f3 = \Base::instance();
if(
!($this->client instanceof ApiInterface) &&
($this->getClient($f3) instanceof ApiInterface)
){
// web client not initialized
$client = $this->getClient($f3);
$client->setTimeout(5);
$client->setConnectTimeout(5);
$client->setUserAgent($this->getUserAgent());
$client->setDecodeContent('gzip, deflate');
$client->setDebugLevel($f3->get('DEBUG'));
$client->setNewLog($this->newLog());
$client->setIsLoggable($this->isLoggable($f3));
$client->setLogStats(true); // add cURL stats (e.g. transferTime) to logged requests
$client->setLogCache(true); // add cache info (e.g. from cached) to logged requests
//$client->setLogAllStatus(true); // log all requests regardless of response HTTP status code
$client->setLogFile('esi_requests');//
$client->setRetryLogFile('esi_retry_requests');
$client->setCacheDebug(true);
$client->setCachePool($this->getCachePool($f3));
// use local proxy server for debugging requests
//$client->setProxy('127.0.0.1:8888');
// disable SSL certificate verification -> allow proxy to decode(view) request
$client->setVerify(false);
//$client->setDebugRequests(true);
$this->client = $client;
}
return $this;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Created by PhpStorm.
* User: Exodus 4D
* Date: 26.12.2018
* Time: 17:43
*/
namespace lib\api;
use lib\Config;
use Exodus4D\ESI\Client\ESI as Client;
use Exodus4D\ESI\Client\ApiInterface;
use Exodus4D\ESI\Client\EsiInterface;
/**
* Class CcpClient
* @package lib\api
*
* @method EsiInterface getServerStatus()
* @method EsiInterface getStatusForRoutes(string $version)
*/
class CcpClient extends AbstractClient {
/**
* @var string
*/
const CLIENT_NAME = 'ccpClient';
/**
* @param \Base $f3
* @return ApiInterface|null
*/
protected function getClient(\Base $f3) : ?ApiInterface {
$client = null;
if(class_exists(Client::class)){
$client = new Client(Config::getEnvironmentData('CCP_ESI_URL'));
$client->setDataSource(Config::getEnvironmentData('CCP_ESI_DATASOURCE'));
}else{
$this->getLogger()->write($this->getMissingClassError(Client::class));
}
return $client;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Created by PhpStorm.
* User: Exodus 4D
* Date: 29.01.2019
* Time: 22:23
*/
namespace lib\api;
use lib\Config;
use Exodus4D\ESI\Client\Github as Client;
use Exodus4D\ESI\Client\ApiInterface;
class GitHubClient extends AbstractClient {
/**
* @var string
*/
const CLIENT_NAME = 'gitHubClient';
/**
* @param \Base $f3
* @return ApiInterface|null
*/
protected function getClient(\Base $f3) : ?ApiInterface {
$client = null;
if(class_exists(Client::class)){
$client = new Client(Config::getPathfinderData('api.git_hub'));
}else{
$this->getLogger()->write($this->getMissingClassError(Client::class));
}
return $client;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Created by PhpStorm.
* User: Exodus 4D
* Date: 26.12.2018
* Time: 17:39
*/
namespace lib\api;
use lib\Config;
use Exodus4D\ESI\Client\SSO as Client;
use Exodus4D\ESI\Client\ApiInterface;
class SsoClient extends AbstractClient {
/**
* @var string
*/
const CLIENT_NAME = 'ssoClient';
/**
* @param \Base $f3
* @return ApiInterface|null
*/
protected function getClient(\Base $f3) : ?ApiInterface {
$client = null;
if(class_exists(Client::class)){
$client = new Client(Config::getEnvironmentData('CCP_SSO_URL'));
}else{
$this->getLogger()->write($this->getMissingClassError(Client::class));
}
return $client;
}
}

View File

@@ -1,90 +0,0 @@
<?php
/**
* Created by PhpStorm.
* User: Exodus4D
* Date: 26.03.2017
* Time: 19:17
*/
namespace lib;
use controller\LogController;
use \Exodus4D\ESI\ESI as ApiClient;
class CcpClient extends \Prefab {
private $apiClient;
public function __construct(\Base $f3){
$this->apiClient = $this->getClient($f3);
$f3->set('ccpClient', $this);
}
/**
* get ApiClient instance
* @param \Base $f3
* @return ApiClient|null
*/
protected function getClient(\Base $f3){
$client = null;
if(class_exists(ApiClient::class)){
$client = new ApiClient();
$client->setUrl( Config::getEnvironmentData('CCP_ESI_URL') );
$client->setDatasource( Config::getEnvironmentData('CCP_ESI_DATASOURCE') );
$client->setUserAgent($this->getUserAgent());
$client->setDebugLevel($f3->get('DEBUG'));
//$client->setDebugLogRequests(true);
}else{
LogController::getLogger('ERROR')->write(sprintf(Config::ERROR_CLASS_NOT_EXISTS_COMPOSER, ApiClient::class));
}
return $client;
}
/**
* @return string
*/
protected function getUserAgent(){
$userAgent = '';
$userAgent .= Config::getPathfinderData('name');
$userAgent .= ' - ' . Config::getPathfinderData('version');
$userAgent .= ' | ' . Config::getPathfinderData('contact');
$userAgent .= ' (' . $_SERVER['SERVER_NAME'] . ')';
return $userAgent;
}
/**
* get error msg for undefined method in ApiClient() class
* @param $method
* @return string
*/
protected function getMissingMethodError($method){
return "Method '" . $method . "()' not found in class '" . get_class($this->apiClient) . "'. -> Check installed Composer package version.'";
}
/**
* call request API data
* @param $name
* @param $arguments
* @return array|mixed
*/
public function __call($name, $arguments){
$return = [];
if(is_object($this->apiClient)){
if( method_exists($this->apiClient, $name) ){
$return = call_user_func_array([$this->apiClient, $name], $arguments);
}else{
LogController::getLogger('ERROR')->write($this->getMissingMethodError($name));
\Base::instance()->error(501, $this->getMissingMethodError($name));
}
}else{
LogController::getLogger('ERROR')->write(sprintf(Config::ERROR_CLASS_NOT_EXISTS_COMPOSER, ApiClient::class));
\Base::instance()->error(501, sprintf(Config::ERROR_CLASS_NOT_EXISTS_COMPOSER, ApiClient::class));
}
return $return;
}
}

View File

@@ -8,8 +8,10 @@
namespace lib;
use controller\LogController;
use Exception;
use lib\api\CcpClient;
use lib\api\GitHubClient;
use lib\api\SsoClient;
class Config extends \Prefab {
@@ -20,8 +22,41 @@ class Config extends \Prefab {
const CACHE_KEY_SOCKET_VALID = 'CACHED_SOCKET_VALID';
const CACHE_TTL_SOCKET_VALID = 60;
const ERROR_CONF_PATHFINDER = 'Config value missing in pathfinder.ini file [%s]';
const ERROR_CLASS_NOT_EXISTS_COMPOSER = 'Class "%s" not found. -> Check installed Composer packages';
// ================================================================================================================
// Redis
// ================================================================================================================
/**
* Redis connect timeout (seconds)
*/
const REDIS_OPT_TIMEOUT = 2;
/**
* Redis read timeout (seconds)
*/
const REDIS_OPT_READ_TIMEOUT = 10;
/**
* redis retry interval (milliseconds)
*/
const REDIS_OPT_RETRY_INTERVAL = 200;
// ================================================================================================================
// EVE downtime
// ================================================================================================================
/**
* SSO downtime length (estimation), minutes
*/
const DOWNTIME_LENGTH = 8;
/**
* SSO downtime buffer length extends downtime length, minutes
*/
const DOWNTIME_BUFFER = 1;
const ERROR_CLASS_NOT_EXISTS_COMPOSER = 'Class "%s" not found. → Check installed Composer packages';
const ERROR_METHOD_NOT_EXISTS_COMPOSER = 'Method "%s()" not found in class "%s". → Check installed Composer packages';
/**
@@ -50,10 +85,21 @@ class Config extends \Prefab {
// -> overwrites default configuration
$this->setHiveVariables($f3);
// set global function for current DateTimeZone()
$f3->set('getTimeZone', function() use ($f3){
// set global getter for \DateTimeZone
$f3->set('getTimeZone', function() use ($f3) : \DateTimeZone {
return new \DateTimeZone( $f3->get('TZ') );
});
// set global getter for new \DateTime
$f3->set('getDateTime', function(string $time = 'now', ?\DateTimeZone $timeZone = null) use ($f3) : \DateTime {
$timeZone = $timeZone ? : $f3->get('getTimeZone')();
return new \DateTime($time, $timeZone);
});
// lazy init Web Api clients
$f3->set(SsoClient::CLIENT_NAME, SsoClient::instance());
$f3->set(CcpClient::CLIENT_NAME, CcpClient::instance());
$f3->set(GitHubClient::CLIENT_NAME, GitHubClient::instance());
}
/**
@@ -382,13 +428,8 @@ class Config extends \Prefab {
*/
static function getPathfinderData($key = ''){
$hiveKey = self::HIVE_KEY_PATHFINDER . ($key ? '.' . strtoupper($key) : '');
$data = null; // make sure it is always defined
try{
if( !\Base::instance()->exists($hiveKey, $data) ){
throw new Exception\ConfigException(sprintf(self::ERROR_CONF_PATHFINDER, $hiveKey));
}
}catch (Exception\ConfigException $e){
LogController::getLogger('ERROR')->write($e->getMessage());
if( !\Base::instance()->exists($hiveKey, $data) ){
$data = null;
}
return $data;
}
@@ -406,15 +447,41 @@ class Config extends \Prefab {
return $status;
}
/**
* parse [D]ata [S]ource [N]ame string from *.ini into $conf parts
* -> $dsn = redis=localhost:6379:2
* $conf = ['type' => 'redis', 'host' => 'localhost', 'port' => 6379, 'db' => 2]
* -> some $conf values might be NULL if not found in $dsn!
* -> some missing values become defaults
* @param string $dsn
* @param array|null $conf
* @return bool
*/
static function parseDSN(string $dsn, ?array &$conf = []) : bool {
// reset reference
if($matches = (bool)preg_match('/^(\w+)\h*=\h*(.+)/', strtolower(trim($dsn)), $parts)){
$conf['type'] = $parts[1];
if($conf['type'] == 'redis'){
list($conf['host'], $conf['port'], $conf['db']) = explode(':', $parts[2]) + [1 => 6379, 2 => null];
}elseif($conf['type'] == 'folder'){
$conf['folder'] = $parts[2];
}
// int cast numeric values
$conf = array_map(function($val){
return is_numeric($val) ? intval($val) : $val;
}, $conf);
}
return $matches;
}
/**
* check if a given DateTime() is within downTime range: downtime + 10m
* -> can be used for prevent logging errors during downTime
* @param \DateTime|null $dateCheck
* @return bool
* @throws Exception\DateException
* @throws \Exception
*/
static function inDownTimeRange(\DateTime $dateCheck = null) : bool {
$inRange = false;
// default daily downtime 00:00am
$downTimeParts = [0, 0];
if( !empty($downTime = (string)self::getEnvironmentData('CCP_SSO_DOWNTIME')) ){
@@ -425,19 +492,28 @@ class Config extends \Prefab {
}
}
// downTime Range is 10m
$downtimeInterval = new \DateInterval('PT10M');
$timezone = \Base::instance()->get('getTimeZone')();
try{
// downTime Range is 10m
$downtimeLength = self::DOWNTIME_LENGTH + (2 * self::DOWNTIME_BUFFER);
$timezone = \Base::instance()->get('getTimeZone')();
// if set -> use current time
$dateCheck = is_null($dateCheck) ? new \DateTime('now', $timezone) : $dateCheck;
$dateDowntimeStart = new \DateTime('now', $timezone);
$dateDowntimeStart->setTime($downTimeParts[0],$downTimeParts[1]);
$dateDowntimeEnd = clone $dateDowntimeStart;
$dateDowntimeEnd->add($downtimeInterval);
// if not set -> use current time
$dateCheck = is_null($dateCheck) ? new \DateTime('now', $timezone) : $dateCheck;
$dateDowntimeStart = new \DateTime('now', $timezone);
$dateDowntimeStart->setTime($downTimeParts[0],$downTimeParts[1]);
$dateDowntimeStart->sub(new \DateInterval('PT' . self::DOWNTIME_BUFFER . 'M'));
$dateRange = new DateRange($dateDowntimeStart, $dateDowntimeEnd);
return $dateRange->inRange($dateCheck);
$dateDowntimeEnd = clone $dateDowntimeStart;
$dateDowntimeEnd->add(new \DateInterval('PT' . $downtimeLength . 'M'));
$dateRange = new DateRange($dateDowntimeStart, $dateDowntimeEnd);
$inRange = $dateRange->inRange($dateCheck);
}catch(\Exception $e){
$f3 = \Base::instance();
$f3->error(500, $e->getMessage(), $e->getTrace());
}
return $inRange;
}
}

View File

@@ -198,7 +198,7 @@ abstract class AbstractLog implements LogInterface {
* @param \stdClass|null $handlerParams
* @return LogInterface
*/
public function addHandler(string $handlerKey, string $formatterKey = null, \stdClass $handlerParams = null): LogInterface{
public function addHandler(string $handlerKey, string $formatterKey = null, \stdClass $handlerParams = null): LogInterface {
if(!$this->hasHandlerKey($handlerKey)){
$this->handlerConfig[$handlerKey] = $formatterKey;
// add more configuration params for the new handler
@@ -214,7 +214,7 @@ abstract class AbstractLog implements LogInterface {
* @param string $handlerKey
* @return LogInterface
*/
public function addHandlerGroup(string $handlerKey): LogInterface{
public function addHandlerGroup(string $handlerKey) : LogInterface {
if(
$this->hasHandlerKey($handlerKey) &&
!$this->hasHandlerGroupKey($handlerKey)
@@ -227,7 +227,7 @@ abstract class AbstractLog implements LogInterface {
/**
* @return array
*/
public function getHandlerConfig(): array{
public function getHandlerConfig() : array{
return $this->handlerConfig;
}
@@ -237,7 +237,7 @@ abstract class AbstractLog implements LogInterface {
* @return array
* @throws \Exception
*/
public function getHandlerParams(string $handlerKey): array{
public function getHandlerParams(string $handlerKey) : array {
$params = [];
if($this->hasHandlerKey($handlerKey)){
@@ -267,14 +267,14 @@ abstract class AbstractLog implements LogInterface {
/**
* @return array
*/
public function getHandlerParamsConfig(): array{
public function getHandlerParamsConfig(): array {
return $this->handlerParamsConfig;
}
/**
* @return array
*/
public function getProcessorConfig(): array{
public function getProcessorConfig(): array {
return $this->processorConfig;
}
@@ -420,6 +420,9 @@ abstract class AbstractLog implements LogInterface {
$params = [];
if( !empty($conf = $this->handlerParamsConfig['stream']) ){
$params[] = $conf->stream;
$params[] = Logger::toMonologLevel($this->getLevel()); // min level that is handled;
$params[] = true; // bubble
$params[] = 0666; // permissions (default 644)
}
return $params;

View File

@@ -0,0 +1,40 @@
<?php
/**
* Created by PhpStorm.
* User: Exodus 4D
* Date: 01.01.2019
* Time: 16:42
*/
namespace lib\logging;
class ApiLog extends AbstractLog {
/**
* List of possible handlers (tested)
* -> final handler will be set dynamic for per instance
* @var array
*/
protected $handlerConfig = [
//'stream' => 'json'
];
protected $channelType = 'api';
public function __construct(string $action, string $level){
parent::__construct($action);
$this->setLevel($level);
}
/**
* overwrites parent
* -> we need unique channelNames for different $actions within same $channelType
* -> otherwise logs would be bundled into the first log file handler
* @return string
*/
public function getChannelName(): string{
return $this->getChannelType() . '_' . $this->getAction();
}
}

View File

@@ -15,6 +15,8 @@ interface LogInterface {
public function setLevel(string $level);
public function setTag(string $tag);
public function setData(array $data): LogInterface;
public function setTempData(array $data): LogInterface;
@@ -61,4 +63,5 @@ interface LogInterface {
public function removeHandlerGroup(string $handlerKey);
public function buffer();
}

View File

@@ -72,7 +72,7 @@ class RallyLog extends AbstractCharacterLog{
}
// add human readable changes to string ---------------------------------------------------
$data['formatted'] =$this->formatData($data);
$data['formatted'] = $this->formatData($data);
return $data;
}
@@ -89,10 +89,10 @@ class RallyLog extends AbstractCharacterLog{
!empty($data['channel'])
){
$replace = [
'{objName}' => $data['object']['objName'],
'{objId}' => $data['object']['objId'],
'{objName}' => $data['object']['objName'],
'{objId}' => $data['object']['objId'],
'{channelName}' => $data['channel']['channelName'],
'{channelId}' => $data['channel']['channelId']
'{channelId}' => $data['channel']['channelId']
];
$string = str_replace(array_keys($replace), array_values($replace), $this->getMessage());
}

View File

@@ -130,4 +130,54 @@ class Util {
sort($scopes);
return md5(serialize($scopes));
}
/**
* get some information about a $source file/dir
* @param string|null $source
* @return array
*/
static function filesystemInfo(?string $source) : array {
$info = [];
if(is_dir($source)){
$info['isDir'] = true;
}elseif(is_file($source)){
$info['isFile'] = true;
}
if(!empty($info)){
$info['chmod'] = substr(sprintf('%o', fileperms($source)), -4);
}
return $info;
}
/**
* round DateTime to interval
* @param \DateTime $dateTime
* @param string $type
* @param int $interval
* @param string $round
*/
static function roundToInterval(\DateTime &$dateTime, string $type = 'sec', int $interval = 5, string $round = 'floor'){
$hours = $minutes = $seconds = 0;
$roundInterval = function(string $format, int $interval, string $round) : int {
return call_user_func($round, $format / $interval) * $interval;
};
switch($type){
case 'hour':
$hours = $roundInterval($dateTime->format('H'), $interval, $round);
break;
case 'min':
$hours = $dateTime->format('H');
$minutes = $roundInterval($dateTime->format('i'), $interval, $round);
break;
case 'sec':
$hours = $dateTime->format('H');
$minutes = $dateTime->format('i');
$seconds = $roundInterval($dateTime->format('s'), $interval, $round);
break;
}
$dateTime->setTime($hours, $minutes, $seconds);
}
}

View File

@@ -1,247 +0,0 @@
<?php
/**
* Created by PhpStorm.
* User: Exodus
* Date: 12.03.2016
* Time: 12:28
*/
namespace lib;
use controller\LogController;
class Web extends \Web {
const ERROR_STATUS_LOG = 'HTTP %s: \'%s\' | url: %s \'%s\'%s';
/**
* max number of curls calls for a single resource until giving up...
* this is because SSO API is not very stable
*/
const RETRY_COUNT_MAX = 3;
/**
* end of line
* @var string
*/
private $eol = "\r\n";
/**
* get status code from Header data array
* @param array $headers
* @return int
*/
protected function getStatusCodeFromHeaders($headers = []){
$statusCode = 0;
if(
preg_match(
'/HTTP\/1\.\d (\d{3}?)/',
implode($this->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 call
* @return array|FALSE|mixed
* @throws \Exception\DateException
*/
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'])
);
// if request not within downTime time range -> log error
if( !Config::inDownTimeRange() ){
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'])
);
// if request not within downTime time range -> log error
if( !Config::inDownTimeRange() ){
LogController::getLogger('ERROR')->write($errorMsg);
}
// trigger error
if($additionalOptions['suppressHTTPErrors'] !== true){
$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'])
);
// if request not within downTime time range -> log error
if( !Config::inDownTimeRange() ){
LogController::getLogger('ERROR')->write($errorMsg);
}
if($additionalOptions['suppressHTTPErrors'] !== true){
$f3->error(504, $errorMsg);
}
}
break;
default:
// unknown status
$errorMsg = $this->getErrorMessageFromJsonResponse(
$statusCode,
$options['method'],
$url
);
if( !Config::inDownTimeRange() ){
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);
}
}

View File

@@ -141,7 +141,7 @@ class AllianceModel extends BasicModel {
$alliance = parent::getById($id, $ttl, $isActive);
if($alliance->isOutdated()){
// request alliance data
$allianceData = self::getF3()->ccpClient->getAllianceData($id);
$allianceData = self::getF3()->ccpClient()->getAllianceData($id);
if( !empty($allianceData) ){
$alliance->copyfrom($allianceData, ['id', 'name', 'ticker']);
$alliance->save();

View File

@@ -11,7 +11,6 @@ namespace Model;
use Controller\Ccp\Sso as Sso;
use Controller\Api\User as User;
use DB\SQL\Schema;
use lib\Util;
use lib\Config;
use lib\Socket;
use Model\Universe;
@@ -23,7 +22,9 @@ class CharacterModel extends BasicModel {
/**
* cache key prefix for getData(); result WITH log data
*/
const DATA_CACHE_KEY_LOG = 'LOG';
const DATA_CACHE_KEY_LOG = 'LOG';
const LOG_ACCESS = 'charId: [%20s], status: %s, charName: %s';
/**
* character authorization status
@@ -75,15 +76,15 @@ class CharacterModel extends BasicModel {
'nullable' => false,
'default' => ''
],
'crestAccessToken' => [
'esiAccessToken' => [
'type' => Schema::DT_VARCHAR256
],
'crestAccessTokenUpdated' => [
'esiAccessTokenExpires' => [
'type' => Schema::DT_TIMESTAMP,
'default' => Schema::DF_CURRENT_TIMESTAMP,
'index' => true
],
'crestRefreshToken' => [
'esiRefreshToken' => [
'type' => Schema::DT_VARCHAR256
],
'esiScopes' => [
@@ -142,6 +143,11 @@ class CharacterModel extends BasicModel {
'nullable' => false,
'default' => 1
],
'selectLocation' => [
'type' => Schema::DT_BOOL,
'nullable' => false,
'default' => 0
],
'securityStatus' => [
'type' => Schema::DT_FLOAT,
'nullable' => false,
@@ -186,10 +192,7 @@ class CharacterModel extends BasicModel {
$characterData->role = $this->roleId->getData();
$characterData->shared = $this->shared;
$characterData->logLocation = $this->logLocation;
if($this->authStatus){
$characterData->authStatus = $this->authStatus;
}
$characterData->selectLocation = $this->selectLocation;
if($addCharacterLogData){
if($logModel = $this->getLog()){
@@ -213,6 +216,11 @@ class CharacterModel extends BasicModel {
$this->updateCacheData($characterData, $cacheKeyModifier);
}
// temp "authStatus" should not be cached
if($this->authStatus){
$characterData->authStatus = $this->authStatus;
}
return $characterData;
}
@@ -255,20 +263,6 @@ class CharacterModel extends BasicModel {
return $ownerHash;
}
/**
* set API 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;
}
/**
* setter for "kicked" until time
* @param $minutes
@@ -478,47 +472,54 @@ class CharacterModel extends BasicModel {
*/
public function getAccessToken(){
$accessToken = false;
$refreshToken = true;
$timezone = self::getF3()->get('getTimeZone')();
$now = new \DateTime('now', $timezone);
// check if there is already an "accessToken" for this user
// check expire timer for stored "accessToken"
if(
!empty($this->crestAccessToken) &&
!empty($this->crestAccessTokenUpdated)
!empty($this->esiAccessToken) &&
!empty($this->esiAccessTokenExpires)
){
$timezone = self::getF3()->get('getTimeZone')();
$tokenTime = \DateTime::createFromFormat(
$expireTime = \DateTime::createFromFormat(
'Y-m-d H:i:s',
$this->crestAccessTokenUpdated,
$this->esiAccessTokenExpires,
$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;
// check if token is not expired
if($expireTime->getTimestamp() > $now->getTimestamp()){
// token still valid
$accessToken = $this->esiAccessToken;
// check if token should be renewed (close to expire)
$timeBuffer = 2 * 60;
$expireTime->sub(new \DateInterval('PT' . $timeBuffer . 'S'));
if($expireTime->getTimestamp() > $now->getTimestamp()){
// token NOT close to expire
$refreshToken = false;
}
}
}
// if no "accessToken" was found -> get a fresh one by an existing "refreshToken"
// no valid "accessToken" found OR
// existing token is close to expire
// -> get a fresh one by an existing "refreshToken"
// -> in case request for new token fails (e.g. timeout) and old token is still valid -> keep old token
if(
!$accessToken &&
!empty($this->crestRefreshToken)
$refreshToken &&
!empty($this->esiRefreshToken)
){
// no accessToken found OR token is deprecated
$ssoController = new Sso();
$accessData = $ssoController->refreshAccessToken($this->crestRefreshToken);
$accessData = $ssoController->refreshAccessToken($this->esiRefreshToken);
if(
isset($accessData->accessToken) &&
isset($accessData->refreshToken)
){
$this->crestAccessToken = $accessData->accessToken;
if(isset($accessData->accessToken, $accessData->esiAccessTokenExpires, $accessData->refreshToken)){
$this->esiAccessToken = $accessData->accessToken;
$this->esiAccessTokenExpires = $accessData->esiAccessTokenExpires;
$this->save();
$accessToken = $this->crestAccessToken;
$accessToken = $this->esiAccessToken;
}
}
@@ -529,24 +530,59 @@ class CharacterModel extends BasicModel {
* check if character is currently kicked
* @return bool
*/
public function isKicked(){
public function isKicked() : bool {
$kicked = false;
if( !is_null($this->kicked) ){
$kickedUntil = new \DateTime();
$kickedUntil->setTimestamp( (int)strtotime($this->kicked) );
$now = new \DateTime();
$kicked = ($kickedUntil > $now);
try{
$kickedUntil = new \DateTime();
$kickedUntil->setTimestamp( (int)strtotime($this->kicked) );
$now = new \DateTime();
$kicked = ($kickedUntil > $now);
}catch(\Exception $e){
self::getF3()->error(500, $e->getMessage(), $e->getTrace());
}
}
return $kicked;
}
/**
* checks whether this character is authorized to log in
* -> check corp/ally whitelist config (pathfinder.ini)
* checks whether this character is currently logged in
* @return bool
*/
public function isAuthorized(){
public function checkLoginTimer() : bool {
$loginCheck = false;
if( !$this->dry() && $this->lastLogin ){
// get max login time (minutes) from config
$maxLoginMinutes = (int)Config::getPathfinderData('timer.logged');
if($maxLoginMinutes){
$timezone = self::getF3()->get('getTimeZone')();
try{
$now = new \DateTime('now', $timezone);
$logoutTime = new \DateTime($this->lastLogin, $timezone);
$logoutTime->add(new \DateInterval('PT' . $maxLoginMinutes . 'M'));
if($logoutTime->getTimestamp() > $now->getTimestamp()){
$loginCheck = true;
}
}catch(\Exception $e){
self::getF3()->error(500, $e->getMessage(), $e->getTrace());
}
}else{
// no "max login" timer configured -> character still logged in
$loginCheck = true;
}
}
return $loginCheck;
}
/**
* checks whether this character is authorized to log in
* -> check corp/ally whitelist config (pathfinder.ini)
* @return string
*/
public function isAuthorized() : string {
$authStatus = 'UNKNOWN';
// check whether character is banned or temp kicked
@@ -714,12 +750,12 @@ class CharacterModel extends BasicModel {
){
// Try to pull data from API
if( $accessToken = $this->getAccessToken() ){
$onlineData = self::getF3()->ccpClient->getCharacterOnlineData($this->_id, $accessToken, $additionalOptions);
$onlineData = self::getF3()->ccpClient()->getCharacterOnlineData($this->_id, $accessToken);
// check whether character is currently ingame online
if(is_bool($onlineData['online'])){
if($onlineData['online'] === true){
$locationData = self::getF3()->ccpClient->getCharacterLocationData($this->_id, $accessToken, $additionalOptions);
$locationData = self::getF3()->ccpClient()->getCharacterLocationData($this->_id, $accessToken);
if( !empty($locationData['system']['id']) ){
// character is currently in-game
@@ -763,7 +799,7 @@ class CharacterModel extends BasicModel {
// get "more" data for systemId and/or stationId -----------------------------------------
if( !empty($lookupUniverseIds) ){
// get "more" information for some Ids (e.g. name)
$universeData = self::getF3()->ccpClient->getUniverseNamesData($lookupUniverseIds, $additionalOptions);
$universeData = self::getF3()->ccpClient()->getUniverseNamesData($lookupUniverseIds);
if( !empty($universeData) && !isset($universeData['error']) ){
// We expect max ONE system AND/OR station data, not an array of e.g. systems
@@ -816,7 +852,7 @@ class CharacterModel extends BasicModel {
// check ship data for changes ------------------------------------------------------------
if( !$deleteLog ){
$shipData = self::getF3()->ccpClient->getCharacterShipData($this->_id, $accessToken, $additionalOptions);
$shipData = self::getF3()->ccpClient()->getCharacterShipData($this->_id, $accessToken);
// IDs for "shipTypeId" that require more data
$lookupShipTypeId = 0;
@@ -887,35 +923,8 @@ class CharacterModel extends BasicModel {
$deleteLog = true;
}
//in case of failure (invalid API response) increase or reset "retry counter"
if( $user = $this->getUser() ){
// Session data does not exists in CLI mode (Cronjob)
if( $sessionCharacterData = $user->getSessionCharacterData($this->id, false) ){
$updateRetry = (int)$sessionCharacterData['UPDATE_RETRY'];
$newRetry = $updateRetry;
if($invalidResponse){
$newRetry++;
if($newRetry >= 3){
// no proper character log data (3 fails in a row))
$newRetry = 0;
$deleteLog = true;
}
}else{
// reset retry counter
$newRetry = 0;
}
if($updateRetry !== $newRetry){
// update retry counter
$sessionCharacterData['UPDATE_RETRY'] = $newRetry;
$sessionCharacters = self::mergeSessionCharacterData([$sessionCharacterData]);
self::getF3()->set(User::SESSION_KEY_CHARACTERS, $sessionCharacters);
}
}
}
if($deleteLog){
self::log('DELETE LOG!');
$this->deleteLog();
}
@@ -945,14 +954,14 @@ class CharacterModel extends BasicModel {
// -> 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
!empty( $verificationCharacterData = $ssoController->verifyCharacterData($accessToken) ) &&
$verificationCharacterData['characterId'] === $this->_id
){
// get character data from API
$characterData = $ssoController->getCharacterData($this->_id);
if( !empty($characterData->character) ){
$characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash;
$characterData->character['esiScopes'] = Util::convertScopesString($verificationCharacterData->Scopes);
$characterData->character['ownerHash'] = $verificationCharacterData['characterOwnerHash'];
$characterData->character['esiScopes'] = $verificationCharacterData['scopes'];
$this->copyfrom($characterData->character, ['ownerHash', 'esiScopes', 'securityStatus']);
$this->corporationId = $characterData->corporation;
@@ -1105,7 +1114,7 @@ class CharacterModel extends BasicModel {
}
// delete auth cookie data ------------------------------------------------------------------------------------
if($deleteCookie ){
if($deleteCookie){
$this->deleteAuthentications();
}
}
@@ -1115,7 +1124,7 @@ class CharacterModel extends BasicModel {
* @param array $characterDataBase
* @return array
*/
public static function mergeSessionCharacterData(array $characterDataBase = []){
public static function mergeSessionCharacterData(array $characterDataBase = []) : array {
$addData = [];
// get current session characters to be merged with
$characterData = (array)self::getF3()->get(User::SESSION_KEY_CHARACTERS);

View File

@@ -284,7 +284,7 @@ class CorporationModel extends BasicModel {
!empty($accessToken) &&
!$this->isNPC
){
$response = self::getF3()->ccpClient->getCorporationRoles($this->_id, $accessToken);
$response = self::getF3()->ccpClient()->getCorporationRoles($this->_id, $accessToken);
if( !empty($response['roles']) ){
$characterRolesData = (array)$response['roles'];
}
@@ -351,10 +351,10 @@ class CorporationModel extends BasicModel {
$corporation = parent::getById($id, $ttl, $isActive);
if($corporation->isOutdated()){
// request corporation data
$corporationData = self::getF3()->ccpClient->getCorporationData($id);
$corporationData = self::getF3()->ccpClient()->getCorporationData($id);
if( !empty($corporationData) ){
// check for NPC corporation
$corporationData['isNPC'] = self::getF3()->ccpClient->isNpcCorporation($id);
$corporationData['isNPC'] = self::getF3()->ccpClient()->isNpcCorporation($id);
$corporation->copyfrom($corporationData, ['id', 'name', 'ticker', 'memberCount', 'isNPC']);
$corporation->save();

View File

@@ -1020,7 +1020,7 @@ class MapModel extends AbstractMapTrackingModel {
* get object relevant data for model log channel
* @return array
*/
public function getLogChannelData() : array{
public function getLogChannelData() : array {
return [
'channelId' => $this->_id,
'channelName' => $this->name
@@ -1030,13 +1030,17 @@ class MapModel extends AbstractMapTrackingModel {
* get object relevant data for model log object
* @return array
*/
public function getLogObjectData() : array{
public function getLogObjectData() : array {
return [
'objId' => $this->_id,
'objName' => $this->name
];
}
/**
* map log formatter callback
* @return \Closure
*/
protected function getLogFormatter(){
return function(&$rowDataObj){
unset($rowDataObj['extra']);
@@ -1258,9 +1262,18 @@ class MapModel extends AbstractMapTrackingModel {
* @param int $limit
* @return array
*/
public function getLogData(int $offset = FileHandler::LOG_FILE_OFFSET, int $limit = FileHandler::LOG_FILE_LIMIT): array {
public function getLogData(int $offset = FileHandler::LOG_FILE_OFFSET, int $limit = FileHandler::LOG_FILE_LIMIT) : array {
$streamConf = $this->getStreamConfig();
return FileHandler::readLogFile($streamConf->stream, $offset, $limit, $this->getLogFormatter());
$rowFormatter = $this->getLogFormatter();
$rowParser = function(string &$rowData, array &$data) use ($rowFormatter){
if( !empty($rowDataObj = (array)json_decode($rowData, true)) ){
$rowFormatter($rowDataObj);
$data[] = $rowDataObj;
}
};
return FileHandler::instance()->readFileReverse($streamConf->stream, $offset, $limit, $rowParser);
}
/**

View File

@@ -10,7 +10,7 @@ namespace Model;
use DB\SQL\Schema;
class MapTypeModel extends BasicModel{
class MapTypeModel extends BasicModel {
protected $table = 'map_type';

View File

@@ -665,7 +665,7 @@ class SystemModel extends AbstractMapTrackingModel {
public function sendRallyPoke(array $rallyData, CharacterModel $characterModel){
// rally log needs at least one handler to be valid
$isValidLog = false;
$log = new Logging\RallyLog('rallySet', $this->getMap()->getLogChannelData());
$log = new logging\RallyLog('rallySet', $this->getMap()->getLogChannelData());
// Slack poke -----------------------------------------------------------------------------
$slackChannelKey = 'slackChannelRally';

View File

@@ -109,7 +109,7 @@ class CategoryModel extends BasicUniverseModel {
* @param array $additionalOptions
*/
protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){
$data = self::getF3()->ccpClient->getUniverseCategoryData($id);
$data = self::getF3()->ccpClient()->getUniverseCategoryData($id);
if(!empty($data)){
$this->copyfrom($data, ['id', 'name', 'published']);
$this->save();
@@ -125,7 +125,7 @@ class CategoryModel extends BasicUniverseModel {
public function loadGroupsData(int $offset = 0, int $length = 0) : array {
$groupIds = [];
if( !$this->dry() ){
$data = self::getF3()->ccpClient->getUniverseCategoryData($this->_id);
$data = self::getF3()->ccpClient()->getUniverseCategoryData($this->_id);
if(!empty($data)){
array_multisort($data['groups'], SORT_ASC, SORT_NUMERIC);
if($length){

View File

@@ -86,7 +86,7 @@ class ConstellationModel extends BasicUniverseModel {
* @param array $additionalOptions
*/
protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){
$data = self::getF3()->ccpClient->getUniverseConstellationData($id);
$data = self::getF3()->ccpClient()->getUniverseConstellationData($id);
if(!empty($data)){
/**
* @var $region RegionModel
@@ -105,7 +105,7 @@ class ConstellationModel extends BasicUniverseModel {
*/
public function loadSystemsData(){
if( !$this->dry() ){
$data = self::getF3()->ccpClient->getUniverseConstellationData($this->_id);
$data = self::getF3()->ccpClient()->getUniverseConstellationData($this->_id);
if(!empty($data)){
foreach((array)$data['systems'] as $systemId){
/**

View File

@@ -111,7 +111,7 @@ class GroupModel extends BasicUniverseModel {
* @param array $additionalOptions
*/
protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){
$data = self::getF3()->ccpClient->getUniverseGroupData($id);
$data = self::getF3()->ccpClient()->getUniverseGroupData($id);
if(!empty($data)){
/**
* @var $category CategoryModel
@@ -132,7 +132,7 @@ class GroupModel extends BasicUniverseModel {
public function loadTypesData(){
$count = 0;
if( !$this->dry() ){
$data = self::getF3()->ccpClient->getUniverseGroupData($this->_id);
$data = self::getF3()->ccpClient()->getUniverseGroupData($this->_id);
if(!empty($data)){
foreach((array)$data['types'] as $typeId){
/**

View File

@@ -96,7 +96,7 @@ class PlanetModel extends BasicUniverseModel {
* @param array $additionalOptions
*/
protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){
$data = self::getF3()->ccpClient->getUniversePlanetData($id);
$data = self::getF3()->ccpClient()->getUniversePlanetData($id);
if(!empty($data)){
/**
* @var $system SystemModel

View File

@@ -46,7 +46,7 @@ class RegionModel extends BasicUniverseModel {
* @param array $additionalOptions
*/
protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){
$data = self::getF3()->ccpClient->getUniverseRegionData($id);
$data = self::getF3()->ccpClient()->getUniverseRegionData($id);
if(!empty($data)){
$this->copyfrom($data, ['id', 'name', 'description']);
$this->save();
@@ -58,7 +58,7 @@ class RegionModel extends BasicUniverseModel {
*/
public function loadConstellationsData(){
if( !$this->dry() ){
$data = self::getF3()->ccpClient->getUniverseRegionData($this->_id);
$data = self::getF3()->ccpClient()->getUniverseRegionData($this->_id);
if(!empty($data)){
foreach((array)$data['constellations'] as $constellationsId){
/**

View File

@@ -104,7 +104,7 @@ class StargateModel extends BasicUniverseModel {
* @param array $additionalOptions
*/
protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){
$data = self::getF3()->ccpClient->getUniverseStargateData($id);
$data = self::getF3()->ccpClient()->getUniverseStargateData($id);
if(!empty($data)){

View File

@@ -80,7 +80,7 @@ class StarModel extends BasicUniverseModel {
* @param array $additionalOptions
*/
protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){
$data = self::getF3()->ccpClient->getUniverseStarData($id);
$data = self::getF3()->ccpClient()->getUniverseStarData($id);
if(!empty($data)){
/**
* @var $type TypeModel

View File

@@ -77,7 +77,7 @@ class StructureModel extends BasicUniverseModel {
* @param array $additionalOptions
*/
protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){
$data = self::getF3()->ccpClient->getUniverseStructureData($id, $accessToken, $additionalOptions);
$data = self::getF3()->ccpClient()->getUniverseStructureData($id, $accessToken);
if(!empty($data)){
/**
* @var $type TypeModel

View File

@@ -333,7 +333,7 @@ class SystemModel extends BasicUniverseModel {
* @param array $additionalOptions
*/
protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){
$data = self::getF3()->ccpClient->getUniverseSystemData($id);
$data = self::getF3()->ccpClient()->getUniverseSystemData($id);
if(!empty($data)){
/**
@@ -363,7 +363,7 @@ class SystemModel extends BasicUniverseModel {
*/
public function loadPlanetsData(){
if( !$this->dry() ){
$data = self::getF3()->ccpClient->getUniverseSystemData($this->_id);
$data = self::getF3()->ccpClient()->getUniverseSystemData($this->_id);
if($data['planets']){
// planets are optional since ESI v4 (e.g. Abyssal systems)
foreach((array)$data['planets'] as $planetData){
@@ -384,7 +384,7 @@ class SystemModel extends BasicUniverseModel {
*/
public function loadStargatesData(){
if( !$this->dry() ){
$data = self::getF3()->ccpClient->getUniverseSystemData($this->_id);
$data = self::getF3()->ccpClient()->getUniverseSystemData($this->_id);
if(!empty($data)){
foreach((array)$data['stargates'] as $stargateId){
/**

View File

@@ -136,7 +136,7 @@ class TypeModel extends BasicUniverseModel {
* @param array $additionalOptions
*/
protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){
$data = self::getF3()->ccpClient->getUniverseTypesData($id, $additionalOptions);
$data = self::getF3()->ccpClient()->getUniverseTypesData($id);
if(!empty($data)){
$group = $this->rel('groupId');
$group->loadById($data['groupId'], $accessToken, $additionalOptions);

View File

@@ -1,54 +1,116 @@
; Main Config
; Pathfinder Config
[PATHFINDER]
; Name of installation
; This can be changed to any name
; This name is used in e.g. emails, user interface
; Syntax: String
; Default: Pathfinder
NAME = Pathfinder
; installed version (used for CSS/JS cache busting)
VERSION = v1.4.3
; contact information [optional]
; Pathfinder version
; Version number should not be changed manually.
; Version is used for CSS/JS cache busting and is part of the URL for static resources:
; e.g. public/js/vX.X.X/app.js
; Syntax: String (current version)
; Default: v1.5.0
VERSION = v1.5.0
; Contact information [optional]
; Shown on 'licence', 'contact' page.
; Syntax: String
; Default: https://github.com/exodus4d
CONTACT = https://github.com/exodus4d
; public contact email [optional]
; Public contact email [optional]
; Syntax: String
; Default:
EMAIL =
; source code [optional]
; Repository URL [optional]
; Used for 'licence', 'contact' page.
; Syntax: String
; Default: https://github.com/exodus4d/pathfinder
REPO = https://github.com/exodus4d/pathfinder
; show warning on "login" form if /setup route is active
; DO NOT disable this warning unless /setup route is protected by e.g. WebAuth
; Show warning on 'login' page if /setup route is active
; DO NOT disable this warning unless /setup route is protected or commented in routes.ini
; Syntax: 0 | 1
; Default: 1
SHOW_SETUP_WARNING = 1
; show complete login page
; if disabled, some section dont appear (Slideshow, Features, Admin, Install, About) (default: 1)
; Show full login page
; If disabled, some section don´t appear:
; 'Slideshow', 'Features', 'Admin', 'Install', 'About'
; Syntax: 0 | 1
; Default: 1
SHOW_COMPLETE_LOGIN_PAGE = 1
; REGISTRATION ====================================================================================
[PATHFINDER.REGISTRATION]
; registration status (for new users) (0=disabled, 1=enabled)
; Registration status (for new users)
; If disabled, users can no longer register a new account on this installation.
; Syntax: 0 | 1
; Default: 1
STATUS = 1
[PATHFINDER.LOGIN]
; expire time (in days) for login cookies
; Expire time for login cookies
; Login Cookie information send by clients is re-validated by the server.
; The expire time for each cookie is stored in DB. Expired Cookies become invalid.
; Syntax: Integer (days)
; Default: 30
COOKIE_EXPIRE = 30
; shows "scheduled maintenance" warning to users (default: 0)
; Show 'scheduled maintenance' warning
; If enabled, active users will see a notification panel.
; This can be used to inform users about upcoming maintenance shutdown.
; This flag can be enabled "on the fly" (no page reload required to see the notice).
; Syntax: 0 | 1
; Default: 0
MODE_MAINTENANCE = 0
; restrict login to specific corporations/alliances by id (e.g. 1000166,1000080)
; Login restrictions (white lists)
; Login/registration can be restricted to specific groups.
; Use comma separated strings for CCP Ids (e.g. 1000166,1000080).
; If no groups are specified, all characters are allowed.
; Syntax: String (comma separated)
; Default:
CHARACTER =
CORPORATION =
ALLIANCE =
; Slack API integration ===========================================================================
[PATHFINDER.SLACK]
; Global Slack API status, check PATHFINDER.MAP section for individual control (0=disabled, 1=enabled)
STATUS = 1
[PATHFINDER.CHARACTER]
; Auto location select for characters
; If enabled, characters can activate the "auto location select" checkbox in their account settings.
; If checkbox active, solar systems get auto selected on map based on their current system.
; Hint: This can increase server load because of more client requests.
; Syntax: 0 | 1
; Default: 1
AUTO_LOCATION_SELECT = 1
; Slack API integration ===========================================================================
[PATHFINDER.SLACK]
; Slack API status
; This is a global toggle for all Slack related features.
; Check PATHFINDER.MAP section for individual control.
; Syntax: 0 | 1
; Default: 1
STATUS = 1
; Discord API integration =========================================================================
[PATHFINDER.DISCORD]
; Global Discord API status, check PATHFINDER.MAP section for individual control (0=disabled, 1=enabled)
; Discord API status
; This is a global toggle for all Discord related features.
; Check PATHFINDER.MAP section for individual control.
; Syntax: 0 | 1
; Default: 1
STATUS = 1
; View ============================================================================================
[PATHFINDER.VIEW]
; static page templates
; Page templates
; Hint: You should not change this.
INDEX = templates/view/index.html
SETUP = templates/view/setup.html
LOGIN = templates/view/login.html
@@ -56,33 +118,38 @@ ADMIN = templates/view/admin.html
; HTTP status pages ===============================================================================
[PATHFINDER.STATUS]
; error pages
; Error page templates
; Hint: You should not change this.
4XX = templates/status/4xx.html
5XX = templates/status/5xx.html
; MAP =============================================================================================
; Map settings for "private", "corporation" and "alliance" maps
; LIFETIME (days):
; - Map will be deleted after "X" days, by cronjob
; MAX_COUNT:
; - Users can create/view up to "X" maps of a type
; MAX_SHARED:
; - Max number of shared entities per map
; MAX_SYSTEMS:
; - Max number of active systems per map
; LOG_ACTIVITY_ENABLED (0: disable, 1: enable):
; - Whether user activity statistics can be anabled for a map type
; - E.g. create/update/delete of systems/connections/signatures/...
; LOG_HISTORY_ENABLED (0: disable, 1: enable):
; - Whether map change history should be logged to separat *.log files
; - see: [PATHFINDER.HISTORY] config section below
; SEND_HISTORY_SLACK_ENABLED (0: disable, 1: enable):
; - Send map updates to a Slack channel per map
; SEND_RALLY_SLACK_ENABLED (0: disable, 1: enable):
; - Send rally point pokes to a Slack channel per map
; SEND_RALLY_Mail_ENABLED (0: disable, 1: enable):
; - Send rally point pokes by mail
; - see: [PATHFINDER.NOTIFICATION] section below
; Map settings for 'private', 'corporation' and 'alliance' maps:
; LIFETIME (days)
; - Map will be deleted after 'X' days, by cronjob
; MAX_COUNT
; - Users can create/view up to 'X' maps of a type
; MAX_SHARED
; - Max number of shared entities per map
; MAX_SYSTEMS
; - Max number of active systems per map
; LOG_ACTIVITY_ENABLED (Syntax: 0 | 1)
; - Whether user activity statistics can be enabled for a map type
; - E.g. create/update/delete of systems/connections/signatures/...
; LOG_HISTORY_ENABLED (Syntax: 0 | 1)
; - Whether map change history should be logged to separate *.log files
; - see: [PATHFINDER.HISTORY] config section below
; SEND_HISTORY_SLACK_ENABLED (Syntax: 0 | 1)
; - Send map updates to a Slack channel per map
; SEND_RALLY_SLACK_ENABLED (Syntax: 0 | 1)
; - Send rally point pokes to a Slack channel per map
; SEND_HISTORY_DISCORD_ENABLED (Syntax: 0 | 1)
; - Send map updates to a Discord channel per map
; SEND_RALLY_DISCORD_ENABLED (Syntax: 0 | 1)
; - Send rally point pokes to a Discord channel per map
; SEND_RALLY_Mail_ENABLED (Syntax: 0 | 1)
; - Send rally point pokes by mail
; - see: [PATHFINDER.NOTIFICATION] section below
[PATHFINDER.MAP.PRIVATE]
LIFETIME = 60
MAX_COUNT = 3
@@ -124,75 +191,152 @@ SEND_RALLY_Mail_ENABLED = 0
; Route search ====================================================================================
[PATHFINDER.ROUTE]
; max recursive search depth for routes (default: 9000)
; decrease it on performance problems
; Search depth for system route search
; Recursive search depth for search algorithm.
; This is only used in case ESIs /route/ API responds with errors and the custom search algorithm is used.
; Hint: Higher values can lead to high CPU load. If to low, routes might not be found even if exist.
; Syntax: Integer
; Default: 9000
SEARCH_DEPTH = 9000
; default count of routes that will be checked (initial) when a system is selected (default: 4)
; Initial count of routes that will be checked when a system becomes active
; Syntax: Integer
; Default: 4
SEARCH_DEFAULT_COUNT = 4
; max count of routes that can be selected in "route settings" dialog (default: 6)
; Max count of routes that can be selected in 'route settings' dialog
; Syntax: Integer
; Default: 6
MAX_DEFAULT_COUNT = 6
; max count of routes that will be checked (MAX_COUNT + custom routes ) (default: 8)
; Max count of routes that will be checked (MAX_DEFAULT_COUNT + custom routes)
; Syntax: Integer
; Default: 8
LIMIT = 8
; Email notifications =============================================================================
; Requires SMTP configuration (see environment.ini)
; Set mail address for recipient (e.g. pathfinder.notification@[YOUR_DOMAIN] )
[PATHFINDER.NOTIFICATION]
; Email address for rally point pokes
; Requires SMTP configuration (see environment.ini).
; Hint: This only makes sens if the installation is restricted to allied groups only.
; This email address is used for all maps on this installation.
; Syntax: String
; Default:
RALLY_SET =
; TIMER ===========================================================================================
; Timer values should NOT be changed unless you know what they affect!
; =================================================================================================
[PATHFINDER.TIMER]
; login time (minutes) (default: 480)
; Login time for characters. Users get logged out after X minutes
; Hint: Set to 0 disables login time and characters stay logged in until Cookie data expires
; Syntax: Integer (minutes)
; Default: 480
LOGGED = 480
; double click timer (milliseconds) (default: 250)
; Double click timer
; Syntax: Integer (milliseconds)
; Default: 250
DBL_CLICK = 250
; time for status change visibility in header (milliseconds) (default: 5000)
; Time for status change visibility in header
; Syntax: Integer (milliseconds)
; Default: 5000
PROGRAM_STATUS_VISIBLE = 5000
; main map update ping (ajax) (milliseconds)
[PATHFINDER.TIMER.UPDATE_SERVER_MAP]
; Map data update interval (ajax long polling)
; This is not used for 'WebSocket' configured installations.
; Syntax: Integer (milliseconds)
; Default: 5000
DELAY = 5000
; Execution limit for map data update request (ajax long polling)
; Requests that exceed the limit are logged as 'warning'.
; Syntax: Integer (milliseconds)
; Default: 200
EXECUTION_LIMIT = 200
; update client map data (milliseconds)
[PATHFINDER.TIMER.UPDATE_CLIENT_MAP]
; Execution limit for client side (javascript) map data updates
; Map data updates that exceed the limit are logged as 'warning'.
; Syntax: Integer (milliseconds)
; Default: 50
EXECUTION_LIMIT = 50
; map user update ping (ajax) (milliseconds)
[PATHFINDER.TIMER.UPDATE_SERVER_USER_DATA]
; User data update interval (ajax long polling)
; This is not used for 'WebSocket' configured installations.
; Syntax: Integer (milliseconds)
; Default: 5000
DELAY = 5000
; Execution limit for user data update request (ajax long polling)
; Requests that exceed the limit are logged as 'warning'.
; Syntax: Integer (milliseconds)
; Default: 500
EXECUTION_LIMIT = 500
; update client user data (milliseconds)
[PATHFINDER.TIMER.UPDATE_CLIENT_USER_DATA]
; Execution limit for client side (javascript) user data updates
; User data updates that exceed the limit are logged as 'warning'.
; Syntax: Integer (milliseconds)
; Default: 50
EXECUTION_LIMIT = 50
; CACHE ===========================================================================================
[PATHFINDER.CACHE]
; delete character log data if if nothing (ship/system/...) for X seconds (seconds) (default: 3min)
; Delete character log data if nothing (ship/system/...) changed for X seconds
; Syntax: Integer (seconds)
; Default: 180
CHARACTER_LOG_INACTIVE = 180
; max expire time. Expired cache files will be deleted by cronjob (seconds) (default: 10d)
; Max expire time for cache files
; Files will be deleted by cronjob afterwards.
; This setting only affects 'file cache'. Redis installations are not affected by this.
; Syntax: Integer (seconds)
; Default: 864000 (10d)
EXPIRE_MAX = 864000
; expire time for EOL (end of life) connections (seconds) (default: 4h + 15min)
; Expire time for EOL (end of life) connections
; EOL connections get auto deleted by cronjob afterwards.
; Syntax: Integer (seconds)
; Default: 15300 (4h + 15min)
EXPIRE_CONNECTIONS_EOL = 15300
; expire time for WH connections (seconds) (default: 2d)
; Expire time for WH connections
; WH connections get auto deleted by cronjob afterwards.
; This can be overwritten for each map in the UI.
; Syntax: Integer (seconds)
; Default: 172800 (2d)
EXPIRE_CONNECTIONS_WH = 172800
; expire time for signatures (inactive systems) (seconds) (default 3d)
; Expire time for signatures (inactive systems)
; Signatures get auto deleted by cronjob afterwards.
; This can be overwritten for each map in the UI.
; Syntax: Integer (seconds)
; Default: 259200 (3d)
EXPIRE_SIGNATURES = 259200
; LOGGING =========================================================================================
; Log file configurations
; Log files are location in [PATHFINDER]/logs/ dir (see: config.ini)
; Syntax: String
[PATHFINDER.LOGFILES]
; error log
; Error log
ERROR = error
; SSO error log
SSO = sso
; login information
LOGIN = login
; session warnings (suspect)
; Login info
CHARACTER_LOGIN = character_login
; Character access
CHARACTER_ACCESS = character_access
; Session warnings (mysql sessions only)
SESSION_SUSPECT = session_suspect
; account deleted
; Account deleted
DELETE_ACCOUNT = account_delete
; admin action (e.g. kick, bann) log
; Admin action (e.g. kick, ban)
ADMIN = admin
; TCP socket errors
SOCKET_ERROR = socket_error
@@ -200,11 +344,26 @@ SOCKET_ERROR = socket_error
DEBUG = debug
[PATHFINDER.HISTORY]
; cache time for parsed log files (seconds) (default: 5)
; cache time for parsed history log file data
; Syntax: Integer (seconds)
; Default: 5
CACHE = 5
; file folder for 'history' logs (e.g. map history) (default: history/)
; File folder for 'history' logs (e.g. map history)
; Syntax: String
; Default: history/
LOG = history/
; Max file size for 'history' logs before getting truncated by cronjob
; Syntax: Integer (MB)
; Default: 2
LOG_SIZE_THRESHOLD = 2
; log entries (lines) after file getting truncated by cronjob
; Syntax: Integer
; Default: 1000
LOG_LINES = 1000
; ADMIN ===========================================================================================
; "SUPER" admins and additional "CORPORATION" admins can be added here
;[PATHFINDER.ROLES]

View File

@@ -9,7 +9,7 @@ APACHE.VERSION = 2.5
NGINX.VERSION = 1.9
[REQUIREMENTS.PHP]
VERSION = 7.0
VERSION = 7.1
; 64-bit version of PHP (4 = 32-bit, 8 = 64-bit)
PHP_INT_SIZE = 8
@@ -69,6 +69,7 @@ CHARACTER_SET_CONNECTION = utf8
COLLATION_DATABASE = utf8_general_ci
COLLATION_CONNECTION = utf8_general_ci
FOREIGN_KEY_CHECKS = ON
INNODB_FILE_PER_TABLE = ON
[REQUIREMENTS.REDIS]
VERSION = 3.0

View File

@@ -20,7 +20,7 @@
"url": "../pathfinder_esi"
}],
"require": {
"php-64bit": ">=7.0",
"php-64bit": ">=7.1",
"ext-pdo": "*",
"ext-openssl": "*",
"ext-curl": "*",
@@ -33,6 +33,14 @@
"websoftwares/monolog-zmq-handler": "0.2.*",
"swiftmailer/swiftmailer": "^6.0",
"league/html-to-markdown": "4.8.*",
"cache/redis-adapter": "1.0.*",
"cache/filesystem-adapter": "1.0.*",
"cache/array-adapter": "1.0.*",
"cache/void-adapter": "1.0.*",
"cache/namespaced-cache": "1.0.*",
"exodus4d/pathfinder_esi": "dev-develop as 0.0.x-dev"
},
"suggest": {
"ext-redis": "Redis can be used as cache backend."
}
}

View File

@@ -20,7 +20,7 @@
"url": "https://github.com/exodus4d/pathfinder_esi"
}],
"require": {
"php-64bit": ">=7.0",
"php-64bit": ">=7.1",
"ext-pdo": "*",
"ext-openssl": "*",
"ext-curl": "*",
@@ -33,6 +33,14 @@
"websoftwares/monolog-zmq-handler": "0.2.*",
"swiftmailer/swiftmailer": "^6.0",
"league/html-to-markdown": "4.8.*",
"exodus4d/pathfinder_esi": "dev-master#v1.2.5"
"cache/redis-adapter": "1.0.*",
"cache/filesystem-adapter": "1.0.*",
"cache/array-adapter": "1.0.*",
"cache/void-adapter": "1.0.*",
"cache/namespaced-cache": "1.0.*",
"exodus4d/pathfinder_esi": "dev-master#v1.3.0"
},
"suggest": {
"ext-redis": "Redis can be used as cache backend."
}
}

View File

@@ -14,9 +14,6 @@ $f3->config('app/config.ini', true);
// load environment dependent config
lib\Config::instance($f3);
// initiate CCP API Client (ESI)
lib\CcpClient::instance($f3);
// initiate cron-jobs
Cron::instance();

View File

@@ -54,7 +54,7 @@ requirejs.config({
blueImpGalleryBootstrap: 'lib/bootstrap-image-gallery', // v3.4.2 Bootstrap extension for Blue Imp Gallery - https://blueimp.github.io/Bootstrap-Image-Gallery
bootstrapConfirmation: 'lib/bootstrap-confirmation', // v1.0.5 Bootstrap extension for inline confirm dialog - https://github.com/tavicu/bs-confirmation
bootstrapToggle: 'lib/bootstrap-toggle.min', // v2.2.0 Bootstrap Toggle (Checkbox) - http://www.bootstraptoggle.com
lazyload: 'lib/jquery.lazyload.min', // v1.9.7 LazyLoader images - http://www.appelsiini.net/projects/lazyload
lazyload: 'lib/jquery.lazyload.min', // v1.9.7 LazyLoader images - https://appelsiini.net/projects/lazyload/
sortable: 'lib/sortable.min', // v1.6.0 Sortable - drag&drop reorder - https://github.com/rubaxa/Sortable
'summernote.loader': './app/summernote.loader', // v0.8.10 Summernote WYSIWYG editor -https://summernote.org

View File

@@ -102,7 +102,7 @@ define(['jquery'], ($) => {
4: 'M609 - C4',
5: 'L614 - C5',
6: 'S804 - C6',
7: 'F353 - Thera'
7: 'F353 - C12 Thera'
},
6: { // ORE
1: 'Ordinary Perimeter Deposit', //*
@@ -146,7 +146,7 @@ define(['jquery'], ($) => {
4: 'Y683 - C4',
5: 'N062 - C5',
6: 'R474 - C6',
7: 'F135 - Thera'
7: 'F135 - C12 Thera'
},
6: { // ORE
1: 'Ordinary Perimeter Deposit', //*
@@ -192,7 +192,7 @@ define(['jquery'], ($) => {
4: 'T405 - C4',
5: 'N770 - C5',
6: 'A982 - C6',
7: 'F135 - Thera'
7: 'F135 - C12 Thera'
},
6: { // ORE
1: 'Ordinary Perimeter Deposit', //*
@@ -341,6 +341,14 @@ define(['jquery'], ($) => {
1: 'Superior Blood Raider Covert Research Facility' //*
}
},
12: { // Thera wormhole
1: { // Combat
1: 'Epicenter',
2: 'Expedition Command Outpost Wreck',
3: 'Planetary Colonization Office Wreck',
4: 'Testing Facilities'
}
},
13: { // Shattered Wormholes
5: { // Wormhole (some of them are static)
1: 'P060 - C1',
@@ -378,7 +386,7 @@ define(['jquery'], ($) => {
}
}, // system type (k-space)
2: {
10: { // High Sec
30: { // High Sec
5: { // Wormhole
1: 'Z971 - C1',
2: 'R943 - C2',
@@ -389,10 +397,10 @@ define(['jquery'], ($) => {
7: 'A641 - H',
8: 'R051 - L',
9: 'V283 - 0.0',
10: 'T458 - Thera'
10: 'T458 - C12 Thera'
}
},
11: { // Low Sec
31: { // Low Sec
5: { // Wormhole
1: 'Z971 - C1',
2: 'R943 - C2',
@@ -403,10 +411,10 @@ define(['jquery'], ($) => {
7: 'B449 - H',
8: 'N944 - L',
9: 'S199 - 0.0',
10: 'M164 - Thera'
10: 'M164 - C12 Thera'
}
},
12: { // 0.0
32: { // 0.0
5: { // Wormhole
1: 'Z971 - C1',
2: 'R943 - C2',
@@ -417,7 +425,7 @@ define(['jquery'], ($) => {
7: 'B449 - H',
8: 'N944 - L',
9: 'S199 - 0.0',
10: 'L031 - Thera'
10: 'L031 - C12 Thera'
}
}
}

215
js/app/console.js Normal file
View File

@@ -0,0 +1,215 @@
/**
* Console module
* -> extends default window.console log object
*/
define([], () => {
'use strict';
/**
* init custom window.console object
* -> extend console obj with custom methods for styling and logging
*/
let initConsole = () => {
window.console = (origConsole => {
// save orig methods for byPassing args to original methods
let log = origConsole.log;
let info = origConsole.info;
let warn = origConsole.warn;
let error = origConsole.error;
let styles = {
'indentDefault': {
'padding-left': '3px'
},
'global': {
'font-weight': 500,
'font-size': '11px',
'line-height': '19px',
'font-family': '"Fira Code", "Lucida Console"',
},
'ok': {
'color': '#5cb85c'
},
'log': {
'color': '#adadad'
},
'info': {
'color': '#428bca'
},
'warn': {
'color': '#ffdd9e'
},
'error': {
'color': '#ff8080'
},
'pf': {
'color': '#568a89'
},
'brand': {
'color': '#375959',
'line-height': '35px',
'font-size': '25px'
}
};
let placeholders = {
'%s': {
'style': ['color: #e93f3b; font-style: italic', 'color: inherit']
},
'%i': {
'style': ['color: #9980ff', 'color: inherit'],
},
'%d': {
'style': ['color: #9980ff', 'color: inherit']
},
'%f': {
'style': ['color: #9980ff', 'color: inherit']
},
'%o': {
'style': ['', '']
},
'%O': {
'style': ['', '']
}
};
let findPlaceholders = str => {
let exp = new RegExp(Object.keys(placeholders).join('|'), 'g');
let matches = str.match(exp);
return matches ? matches : [];
};
let addStylePlaceholder = str => {
let exp = new RegExp(Object.keys(placeholders).join('|'), 'g');
return str.replace(exp, function(matched){
return '%c' + matched + '%c';
});
};
let getStyleByPlaceholder = (placeholder, clear = false) => {
let css = '';
if(placeholders.hasOwnProperty(placeholder)){
css = placeholders[placeholder].style[clear ? 1 : 0];
}
return css;
};
let getStyleByLogType = (logType, props = []) => {
let css = '';
if(styles.hasOwnProperty(logType)){
css = Object.keys(styles[logType])
.filter(prop => props.length ? props.includes(prop) : true)
.reduce((css, prop,i, affe) => {
css += prop + ':' + styles[logType][prop] + ';';
return css;
}, '');
}
return css;
};
let setLineStyleByLogType = (logType, args) => {
if(args.length){
let lineStyle = getStyleByLogType('global') + getStyleByLogType(logType);
lineStyle += ['ok', 'log', 'info', 'pf'].includes(logType) ? getStyleByLogType('indentDefault') : '';
let bullet = ['ok', 'log', 'info', 'pf'].includes(logType) ? '●' : '';
if(typeof args[0] === 'string'){
// prepend placeholder to existing message
args[0] = '%c' + bullet + ' ' + args[0];
}else{
// prepend placeholder as new message
args.splice(0, 0, '%c' + bullet + ' ' + logType + ':');
}
// set line style as 2nd argument
args.splice(1, 0, lineStyle);
}
};
let setMessageStyleByLogType = (logType, args) => {
if(typeof args[0] === 'string') {
let placeholdersFound = findPlaceholders(args[0]);
let placeholderCount = placeholdersFound.length;
// add c% placeholders around other placeholders
args[0] = addStylePlaceholder(args[0]);
// add style args for c% placeholders
let placeholderIndex = 0;
let argIndexStart = 1;
let argIndexEnd = argIndexStart + placeholderCount;
let argIndexOffset = 0;
for (let argIndex = argIndexStart; argIndex < argIndexEnd; argIndex++) {
args.splice(argIndex + argIndexOffset, 0, getStyleByPlaceholder(placeholdersFound[placeholderIndex]));
argIndexOffset += 2;
args.splice(argIndex + argIndexOffset, 0, getStyleByPlaceholder(placeholdersFound[placeholderIndex], true) + ';' + getStyleByLogType('global') + getStyleByLogType(logType));
placeholderIndex++;
}
}
};
origConsole.ok = (...args) => {
setMessageStyleByLogType('ok', args);
setLineStyleByLogType('ok', args);
info.apply(origConsole, args);
};
origConsole.info = (...args) => {
setMessageStyleByLogType('info', args);
setLineStyleByLogType('info', args);
info.apply(origConsole, args);
};
origConsole.log = (...args) => {
setMessageStyleByLogType('log', args);
setLineStyleByLogType('log', args);
log.apply(origConsole, args);
};
origConsole.warn = (...args) => {
setMessageStyleByLogType('warn', args);
setLineStyleByLogType('warn', args);
warn.apply(origConsole, args);
};
origConsole.error = (...args) => {
setMessageStyleByLogType('error', args);
setLineStyleByLogType('error', args);
error.apply(origConsole, args);
};
origConsole.pf = (...args) => {
setMessageStyleByLogType('pf', args);
setLineStyleByLogType('pf', args);
info.apply(origConsole, args);
};
origConsole.brand = (...args) => {
setMessageStyleByLogType('brand', args);
setLineStyleByLogType('brand', args);
info.apply(origConsole, args);
};
return origConsole;
})(window.console);
};
initConsole();
/**
* show current program version information console
* @param version
*/
let showVersionInfo = (version) => {
console.ok('%c PATHFINDER',
'color: #477372; font-size: 25px; margin-left: 10px; line-height: 100px; text-shadow: 1px 1px 0 #212C30; ' +
'background: url(https://i.imgur.com/1Gw8mjL.png) no-repeat;');
console.pf('Release: %s', version);
};
return {
showVersionInfo: showVersionInfo
};
});

View File

@@ -202,7 +202,7 @@ define(['jquery'], ($) => {
},
// system security
systemSecurity: {
security: {
'security': {
class: 'pf-system-sec'
},
'A': {
@@ -220,23 +220,26 @@ define(['jquery'], ($) => {
'0.0': {
class: 'pf-system-sec-nullSec'
},
'C6': {
class: 'pf-system-sec-high'
},
'C5': {
class: 'pf-system-sec-high'
},
'C4': {
class: 'pf-system-sec-mid'
},
'C3': {
class: 'pf-system-sec-mid'
'C1': {
class: 'pf-system-sec-low'
},
'C2': {
class: 'pf-system-sec-low'
},
'C1': {
class: 'pf-system-sec-low'
'C3': {
class: 'pf-system-sec-mid'
},
'C4': {
class: 'pf-system-sec-mid'
},
'C5': {
class: 'pf-system-sec-high'
},
'C6': {
class: 'pf-system-sec-high'
},
'C12': {
class: 'pf-system-sec-special'
}
},
// true sec
@@ -495,6 +498,36 @@ define(['jquery'], ($) => {
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
30: { // High Sec
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
31: { // Low Sec
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
32: { // 0.0
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
}
},
// incoming wormholes
@@ -505,7 +538,7 @@ define(['jquery'], ($) => {
4: 'K162 - H',
5: 'K162 - L',
6: 'K162 - 0.0',
7: 'K162 - Thera'
7: 'K162 - C12 Thera'
}
};

View File

@@ -17,7 +17,8 @@ define([
'dialog/notification',
'dialog/manual',
'dialog/changelog',
'dialog/credit'
'dialog/credit',
'dialog/api_status',
], ($, Init, Util, Render, Gallery, bootbox) => {
'use strict';
@@ -70,6 +71,8 @@ define([
stickyPanelServerId: 'pf-landing-server-panel', // id for EVE Online server status panel
stickyPanelAdminId: 'pf-landing-admin-panel', // id for admin login panel
apiStatusTriggerClass: 'pf-api-status-trigger', // class for "api status" dialog trigger elements
// animation
animateElementClass: 'pf-animate-on-visible', // class for elements that will be animated to show
@@ -463,30 +466,40 @@ define([
dataType: 'json'
}).done(function(responseData, textStatus, request){
if(responseData.hasOwnProperty('status')){
let data = responseData.status;
data.stickyPanelServerId = config.stickyPanelServerId;
data.stickyPanelClass = config.stickyPanelClass;
let statusClass = '';
switch(data.serviceStatus.toLowerCase()){
case 'online': statusClass = 'txt-color-green'; break;
case 'vip': statusClass = 'txt-color-orange'; break;
case 'offline': statusClass = 'txt-color-redDarker'; break;
let data = {
stickyPanelServerId: config.stickyPanelServerId,
stickyPanelClass: config.stickyPanelClass,
apiStatusTriggerClass: config.apiStatusTriggerClass,
server: responseData.server,
api: responseData.api,
statusFormat: () => {
return (val, render) => {
switch(render(val)){
case 'online':
case 'green': return 'txt-color-green';
case 'vip':
case 'yellow': return 'txt-color-orange';
case 'offline':
case 'red': return 'txt-color-red';
default: return '';
}
};
}
data.serviceStatus = {
eve: data.serviceStatus,
style: statusClass
};
};
requirejs(['text!templates/ui/server_panel.html', 'mustache'], function(template, Mustache){
let content = Mustache.render(template, data);
$('#' + config.headerId).prepend(content);
$('#' + config.stickyPanelServerId).velocity('transition.slideLeftBigIn', {
duration: 240
});
requirejs(['text!templates/ui/server_panel.html', 'mustache'], function(template, Mustache){
let content = Mustache.render(template, data);
$('#' + config.headerId).prepend(content);
let stickyPanelServer = $('#' + config.stickyPanelServerId);
stickyPanelServer.velocity('transition.slideLeftBigIn', {
duration: 240
});
}
// set observer for api status dialog
stickyPanelServer.on('click', '.' + config.apiStatusTriggerClass, function(){
$.fn.apiStatusDialog(data.api);
});
});
}).fail(handleAjaxErrorResponse);
};

View File

@@ -1495,7 +1495,7 @@ define([
hiddenOptions.push('delete_system');
}
let mapElement = component.parents('.' + config.mapClass);
let mapElement = component.closest('.' + config.mapClass);
if( !mapElement.find('.' + config.systemActiveClass).length ){
hiddenOptions.push('find_route');
}
@@ -1827,7 +1827,7 @@ define([
let single = function(e){
// check if click was performed on "popover" (x-editable)
let popoverClick = false;
if( $(e.target).parents('.popover').length ){
if( $(e.target).closest('.popover').length ){
popoverClick = true;
}
@@ -2014,7 +2014,7 @@ define([
// register all available connection types ----------------------------------------------------------------
newJsPlumbInstance.registerConnectionTypes(globalMapConfig.connectionTypes);
// event after a new connection is established --------------------------
// event after a new connection is established ------------------------------------------------------------
newJsPlumbInstance.bind('connection', function(info, e){
// set connection observer
setConnectionObserver(newJsPlumbInstance, info.connection);
@@ -2114,6 +2114,54 @@ define([
return MapUtil.getMapInstance(mapId);
};
/**
* check if there is an focus() element found as parent of tabContentElement
* -> or if there is any other active UI element found (e.g. dialog, xEditable, Summernote)
* @param tabContentElement
* @returns {*}
*/
let systemFormsActive = (tabContentElement) => {
let activeNode = null;
if(tabContentElement.length){
// tabContentElement exists ...
tabContentElement = tabContentElement[0];
// ... check for current active/focus() element and is not the default <body> element ...
if(
Util.isDomElement(document.activeElement) &&
document.activeElement !== document.body
){
let activeElementTagName = document.activeElement.tagName.toLocaleLowerCase();
// ... check for active form elements ...
let isFormElement = ['input', 'select', 'textarea'].includes(activeElementTagName);
let isChildElement = tabContentElement.contains(document.activeElement);
if(isFormElement && isChildElement){
activeNode = activeElementTagName;
}else{
// ... check for open dialogs/xEditable elements ...
if(Util.isDomElement(document.querySelector('.bootbox'))){
activeNode = 'dialogOpen';
}else if(Util.isDomElement(document.querySelector('.editable-open'))){
activeNode = 'xEditableOpen';
}else{
// ... check for open Summernote editor
let summernoteElement = tabContentElement.querySelector('.' + Util.config.summernoteClass);
if(
Util.isDomElement(summernoteElement) &&
typeof $(summernoteElement).data().summernote === 'object'
){
activeNode = 'SummernoteOpen';
}
}
}
}
}
return activeNode;
};
/**
* set observer for a map container
* @param map
@@ -2484,17 +2532,30 @@ define([
// triggered from "header" link (if user is active in one of the systems)
mapContainer.on('pf:menuSelectSystem', function(e, data){
let tempMapContainer = $(this);
let systemId = MapUtil.getSystemId(tempMapContainer.data('id'), data.systemId);
let system = $(this).find('#' + systemId);
let mapElement = $(this);
let systemId = MapUtil.getSystemId(mapElement.data('id'), data.systemId);
let system = mapElement.find('#' + systemId);
if(system.length === 1){
// scroll to system
let tempMapWrapper = tempMapContainer.parents('.' + config.mapWrapperClass);
tempMapWrapper.mCustomScrollbar('scrollTo', system);
// system found on map ...
let select = Util.getObjVal(data, 'forceSelect') !== false;
// select system
MapUtil.showSystemInfo(map, system);
if(!select){
// ... select is NOT "forced" -> auto select system on jump
let activeElement = systemFormsActive(MapUtil.getTabContentElementByMapElement(system));
if(activeElement !== null){
console.info('Skip auto select systemId %i. Reason: %o', data.systemId, activeElement);
}else{
select = true;
}
}
if(select){
let mapWrapper = mapElement.closest('.' + config.mapWrapperClass);
mapWrapper.scrollToSystem(MapUtil.getSystemPosition(system));
// select system
MapUtil.showSystemInfo(map, system);
}
}
});
@@ -2595,118 +2656,125 @@ define([
/**
* updates all systems on map with current user Data (all users on this map)
* update the Data of the user that is currently viewing the map (if available)
* @param mapElement
* @param userData
* @returns {boolean}
* @returns {Promise<any>}
*/
$.fn.updateUserData = function(userData){
let returnStatus = true;
let updateUserData = (mapElement, userData) => {
// get new map instance or load existing
let map = getMapInstance(userData.config.id);
let mapElement = map.getContainer();
// container must exist! otherwise systems can not be updated
if(mapElement !== undefined){
mapElement = $(mapElement);
// check if map is frozen
if(mapElement.data('frozen') === true){
return returnStatus;
}
// compact/small system layout or not
let compactView = mapElement.hasClass(MapUtil.config.mapCompactClass);
// get current character log data
let characterLogExists = false;
let currentCharacterLog = Util.getCurrentCharacterLog();
// data for header update
let headerUpdateData = {
mapId: userData.config.id,
userCountInside: 0, // active user on a map
userCountOutside: 0, // active user NOT on map
userCountInactive: 0 // inactive users (no location)
let updateUserDataExecutor = (resolve, reject) => {
let payload = {
action: 'updateUserData'
};
if(
currentCharacterLog &&
currentCharacterLog.system
){
characterLogExists = true;
headerUpdateData.currentSystemName = currentCharacterLog.system.name;
}
// get new map instance or load existing
let map = getMapInstance(userData.config.id);
let mapElement = map.getContainer();
// check if current user was found on the map
let currentUserOnMap = false;
// container must exist! otherwise systems can not be updated
if(mapElement !== undefined){
mapElement = $(mapElement);
// get all systems
let systems = mapElement.find('.' + config.systemClass);
for(let i = 0; i < systems.length; i++){
// get user Data for System
let system = $( systems[i] );
let systemId = $(system).data('systemId');
let tempUserData = null;
// check if user is currently in "this" system
let currentUserIsHere = false;
let j = userData.data.systems.length;
// search backwards to avoid decrement the counter after splice()
while(j--){
let systemData = userData.data.systems[j];
// check if any user is in this system
if(systemId === systemData.id){
tempUserData = systemData;
// add "user count" to "total map user count"
headerUpdateData.userCountInside += tempUserData.user.length;
// remove system from "search" array -> speed up loop
userData.data.systems.splice(j, 1);
}
// no user update for 'frozen' maps...
if(mapElement.data('frozen') === true){
return resolve(payload);
}
// the current user can only be in a single system ----------------------------------------------------
// compact/small system layout or not
let compactView = mapElement.hasClass(MapUtil.config.mapCompactClass);
// get current character log data
let characterLogExists = false;
let currentCharacterLog = Util.getCurrentCharacterLog();
// data for header update
let headerUpdateData = {
mapId: userData.config.id,
userCountInside: 0, // active user on a map
userCountOutside: 0, // active user NOT on map
userCountInactive: 0, // inactive users (no location)
currentLocation: {
id: 0, // systemId for current active user
name: false // systemName for current active user
}
};
if(
characterLogExists &&
currentCharacterLog.system.id === systemId
currentCharacterLog &&
currentCharacterLog.system
){
if( !currentUserOnMap ){
currentUserIsHere = true;
currentUserOnMap = true;
characterLogExists = true;
headerUpdateData.currentLocation.name = currentCharacterLog.system.name;
}
// set current location data for header update
headerUpdateData.currentSystemId = $(system).data('id');
headerUpdateData.currentSystemName = currentCharacterLog.system.name;
// check if current user was found on the map
let currentUserOnMap = false;
// get all systems
let systems = mapElement.find('.' + config.systemClass);
for(let system of systems){
system = $(system);
let systemId = system.data('systemId');
let tempUserData = null;
// check if user is currently in "this" system
let currentUserIsHere = false;
let j = userData.data.systems.length;
// search backwards to avoid decrement the counter after splice()
while(j--){
let systemData = userData.data.systems[j];
// check if any user is in this system
if(systemId === systemData.id){
tempUserData = systemData;
// add "user count" to "total map user count"
headerUpdateData.userCountInside += tempUserData.user.length;
// remove system from "search" array -> speed up loop
userData.data.systems.splice(j, 1);
}
}
// the current user can only be in a single system ------------------------------------------------
if(
characterLogExists &&
currentCharacterLog.system.id === systemId
){
if( !currentUserOnMap ){
currentUserIsHere = true;
currentUserOnMap = true;
// set current location data for header update
headerUpdateData.currentLocation.id = system.data('id');
headerUpdateData.currentLocation.name = currentCharacterLog.system.name;
}
}
system.updateSystemUserData(map, tempUserData, currentUserIsHere, {compactView: compactView});
}
// users who are not in any map system ----------------------------------------------------------------
for(let systemData of userData.data.systems){
// users without location are grouped in systemId: 0
if(systemData.id){
headerUpdateData.userCountOutside += systemData.user.length;
}else{
headerUpdateData.userCountInactive += systemData.user.length;
}
}
system.updateSystemUserData(map, tempUserData, currentUserIsHere, {compactView: compactView});
// trigger document event -> update header
$(document).trigger('pf:updateHeaderMapData', headerUpdateData);
}
// users who are not in any map system --------------------------------------------------------------------
for(let i = 0; i < userData.data.systems.length; i++){
// users without location are grouped in systemId: 0
if(userData.data.systems[i].id){
headerUpdateData.userCountOutside += userData.data.systems[i].user.length;
}else{
headerUpdateData.userCountInactive += userData.data.systems[i].user.length;
}
}
resolve(payload);
};
// trigger document event -> update header
$(document).trigger('pf:updateHeaderMapData', headerUpdateData);
}
return returnStatus;
return new Promise(updateUserDataExecutor);
};
/**
@@ -2851,17 +2919,7 @@ define([
updated: parseInt( system.data('updated') )
};
systemData.userCount = (system.data('userCount') ? parseInt( system.data('userCount') ) : 0);
// position ---------------------------------------------------------------------------------------------------
let positionData = {};
let currentX = system.css('left');
let currentY = system.css('top');
// remove 'px'
positionData.x = parseInt( currentX.substring(0, currentX.length - 2) );
positionData.y = parseInt( currentY.substring(0, currentY.length - 2) );
systemData.position = positionData;
systemData.position = MapUtil.getSystemPosition(system);
return systemData;
};
@@ -2982,6 +3040,7 @@ define([
return {
getMapInstance: getMapInstance,
loadMap: loadMap,
updateUserData: updateUserData,
saveSystemCallback: saveSystemCallback
};

View File

@@ -70,7 +70,7 @@ define([
};
/**
* scroll to a specific position in the map
* scroll to a specific position on map
* demo: http://manos.malihu.gr/repository/custom-scrollbar/demo/examples/scrollTo_demo.html
* @param position
*/
@@ -79,4 +79,31 @@ define([
$(this).mCustomScrollbar('scrollTo', position);
});
};
/**
* scroll to a specific system on map
* -> subtract some offset for tooltips/connections
* @param position
* @returns {*}
*/
$.fn.scrollToSystem = function(position){
position = getOffsetPosition(position, {x: -15, y: -35});
return this.each(function(){
$(this).mCustomScrollbar('scrollTo', position);
});
};
/**
* add/subtract offset coordinates from position
* -> no negative values returned
* @param position
* @param offset
* @returns {{x: number, y: number}}
*/
let getOffsetPosition = (position, offset) => {
return {
x: Math.max(0, position.x + offset.x),
y: Math.max(0, position.y + offset.y)
};
};
});

View File

@@ -482,7 +482,7 @@ define([
* @param label
* @returns {string}
*/
let getEndpointOverlayContent = (label) => {
let getEndpointOverlayContent = label => {
let newLabel = '';
let colorClass = 'txt-color-grayLighter';
@@ -508,17 +508,14 @@ define([
* @param element
* @returns {*}
*/
let getTabContentElementByMapElement = (element) => {
let tabContentElement = $(element).parents('.' + config.mapTabContentClass);
return tabContentElement;
};
let getTabContentElementByMapElement = element => $(element).closest('.' + config.mapTabContentClass);
/**
* checks if there is an "active" connection on a map
* @param map
* @returns {boolean}
*/
let hasActiveConnection = (map) => {
let hasActiveConnection = map => {
let activeConnections = getConnectionsByType(map, 'active');
return activeConnections.length > 0;
};
@@ -1203,6 +1200,21 @@ define([
return new Promise(setMapDefaultOptionsExecutor);
};
/**
* get system coordinates from systemElement
* @param system
* @returns {{x: number, y: number}}
*/
let getSystemPosition = system => {
let x = system.css('left');
let y = system.css('top');
return {
x: parseInt(x.substring(0, x.length - 2)),
y: parseInt(y.substring(0, y.length - 2))
};
};
/**
* scroll map to default (stored) x/y coordinates
* @param mapElement
@@ -1755,6 +1767,7 @@ define([
deleteLocalData: deleteLocalData,
visualizeMap: visualizeMap,
setMapDefaultOptions: setMapDefaultOptions,
getSystemPosition: getSystemPosition,
scrollToDefaultPosition: scrollToDefaultPosition,
getSystemId: getSystemId,
checkRight: checkRight,

View File

@@ -143,6 +143,7 @@ define([
Init.characterStatus = response.characterStatus;
Init.routes = response.routes;
Init.url = response.url;
Init.character = response.character;
Init.slack = response.slack;
Init.discord = response.discord;
Init.structureStatus = response.structureStatus;
@@ -308,10 +309,10 @@ define([
.then(payload => Promise.all([initMapModule(payload[0]), initMapWorker(payload[1])]))
.then(payload => {
// mapModule initialized and WebSocket configuration working
console.info('%s() complete! command: "%s"; syncStatus: "%s"',
payload[1].action,
payload[1].data.command,
payload[1].data.syncStatus
console.ok('Client syncStatus: %s. %O resolved by command: %s!',
payload[1].data.syncStatus,
payload[1].action + '()',
payload[1].data.command
);
})
.catch(payload => {
@@ -322,10 +323,10 @@ define([
break;
case 'initMapWorker':
// WebSocket not working -> no error here -> fallback to Ajax
console.warn('%s() rejects Promise. command: "%s"; syncStatus: "%s", payload: %o',
payload.action,
payload.data.command,
console.info('Client syncStatus: %s. %O rejected by command: %s! payload: %o',
payload.data.syncStatus,
payload.action + '()',
payload.data.command,
payload.data
);
break;

View File

@@ -458,7 +458,7 @@ define([
mapElement.trigger('pf:updateLocal', currentMapUserData);
// update map with current user data
mapElement.updateUserData(currentMapUserData);
Map.updateUserData(mapElement, currentMapUserData);
}
}

View File

@@ -491,7 +491,7 @@ define([
pageElement.prepend(headRendered);
// init header =====================================================================
// init header ================================================================================================
// init slide menus
let slideMenu = new $.slidebars({
@@ -516,7 +516,7 @@ define([
// current location
$('#' + Util.config.headCurrentLocationId).find('a').on('click', function(){
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: $(this).data('systemId') });
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: $(this).data('systemId')});
});
// program status
@@ -589,7 +589,7 @@ define([
pageElement.prepend(footerElement);
// init footer ==================================================
// init footer ================================================================================================
pageElement.find('.' + config.footerLicenceLinkClass).on('click', function(){
//show credits info dialog
$.fn.showCreditsDialog();
@@ -728,7 +728,7 @@ define([
return false;
});
// END menu events =============================================================================
// END menu events ============================================================================================
// global "popover" callback (for all popovers)
$('.' + Util.config.popoverTriggerClass).on('hide.bs.popover', function(e){
@@ -772,7 +772,7 @@ define([
userCountInside = data.userCountInside;
userCountOutside = data.userCountOutside;
userCountInactive = data.userCountInactive;
currentLocationData = data;
currentLocationData = data.currentLocation;
}
updateHeaderActiveUserCount(userCountInside, userCountOutside, userCountInactive);
updateHeaderCurrentLocation(currentLocationData);
@@ -825,7 +825,7 @@ define([
Util.showNotify({title: 'Logged out', text: data.reason, type: 'error'}, false);
// remove map -------------------------------------------------------
// remove map ---------------------------------------------------------------------------------------------
Util.getMapModule().velocity('fadeOut', {
duration: 300,
complete: function(){
@@ -933,7 +933,7 @@ define([
}
};
// check for character/ship changes ---------------------------------------------
// check for character/ship changes ---------------------------------------------------------------------------
if(
userData &&
userData.character
@@ -953,7 +953,7 @@ define([
return data.id;
});
// update user character data ---------------------------------------------------
// update user character data ---------------------------------------------------------------------------------
if(currentCharactersOptionIds.toString() !== newCharactersOptionIds.toString()){
let currentCharacterChanged = false;
@@ -976,7 +976,7 @@ define([
userInfoElement.data('characterOptionIds', newCharactersOptionIds);
}
// update user ship data --------------------------------------------------------
// update user ship data --------------------------------------------------------------------------------------
if(currentShipId !== newShipData.typeId){
// set new data for next check
userShipElement.data('shipData', newShipData);
@@ -1058,37 +1058,46 @@ define([
};
/**
* update the "current location" element in head
* update the "current location" link element in head
* @param locationData
*/
let updateHeaderCurrentLocation = function(locationData){
let currentLocationElement = $('#' + Util.config.headCurrentLocationId);
let linkElement = currentLocationElement.find('a');
let textElement = linkElement.find('span');
let updateHeaderCurrentLocation = locationData => {
let systemId = locationData.id || 0;
let systemName = locationData.name || false;
let tempSystemName = (locationData.currentSystemName) ? locationData.currentSystemName : false;
let tempSystemId = (locationData.currentSystemId) ? locationData.currentSystemId : 0;
let currentLocationData = Util.getCurrentLocationData();
if(
linkElement.data('systemName') !== tempSystemName ||
linkElement.data('systemId') !== tempSystemId
currentLocationData.name !== systemName ||
currentLocationData.id !== systemId
){
linkElement.data('systemName', tempSystemName);
linkElement.data('systemId', tempSystemId);
linkElement.toggleClass('disabled', !tempSystemId);
Util.setCurrentLocationData(systemId, systemName);
if(tempSystemName !== false){
textElement.text(locationData.currentSystemName);
let currentLocationElement = $('#' + Util.config.headCurrentLocationId);
let linkElement = currentLocationElement.find('a');
linkElement.toggleClass('disabled', !systemId);
if(systemName !== false){
linkElement.find('span').text(locationData.name);
currentLocationElement.velocity('fadeIn', {duration: Init.animationSpeed.headerLink});
}else{
if(currentLocationElement.is(':visible')){
currentLocationElement.velocity('fadeOut', {duration: Init.animationSpeed.headerLink});
}
}
// auto select current system -----------------------------------------------------------------------------
let userData = Util.getCurrentUserData();
if(
Boolean(Util.getObjVal(Init, 'character.autoLocationSelect')) &&
Util.getObjVal(userData, 'character.selectLocation')
){
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: systemId, forceSelect: false});
}
}
};
/**
* shows a test notification for desktop messages
*/

View File

@@ -55,6 +55,7 @@ define([
formErrorContainerClass: Util.config.formErrorContainerClass,
ccpImageServer: Init.url.ccpImageServer,
roleLabel: Util.getLabelByRole(Util.getObjVal(Util.getCurrentUserData(), 'character.role')).prop('outerHTML'),
characterAutoLocationSelectEnabled: Boolean(Util.getObjVal(Init, 'character.autoLocationSelect'))
};
let content = Mustache.render(template, data);

View File

@@ -0,0 +1,76 @@
/**
* changelog dialog (GitHub API repository information)
*/
define([
'jquery',
'app/init',
'app/util',
'app/render',
'bootbox'
], ($, Init, Util, Render, bootbox) => {
'use strict';
let config = {
apiStatusDialogClass: 'pf-api-status-dialog' // class for "api status" dialog
};
/**
* show api status dialog
* @param apiData
*/
$.fn.apiStatusDialog = function(apiData){
let data = {
apiData: apiData,
methodFormat: () => {
return (val, render) => {
switch(render(val)){
case 'get': return 'txt-color-blue';
case 'post': return 'txt-color-green';
case 'put': return 'txt-color-yellow';
case 'delete': return 'txt-color-red';
default: return '';
}
};
},
statusTitle: () => {
return (val, render) => {
switch(render(val)){
case 'green': return 'ok';
case 'yellow': return 'degraded: Slow or potentially dropping requests';
case 'red': return 'bad: Most requests are not succeeding and/or are very slow (5s+) on average';
default: return 'unknown';
}
};
},
secondsFormat: () => {
return (val, render) => {
return parseFloat(render(val)).toFixed(2) + 's';
};
}
};
requirejs(['text!templates/dialog/api_status.html', 'mustache'], (template, Mustache) => {
let apiStatusDialog = bootbox.dialog({
className: config.apiStatusDialogClass,
title: 'API status',
message: Mustache.render(template, data),
show: false,
buttons: {
close: {
label: 'cancel',
className: 'btn-default'
}
}
});
apiStatusDialog.initTooltips();
// show dialog
apiStatusDialog.modal('show');
});
};
});

View File

@@ -88,7 +88,7 @@ define([
let data = {
isFirst: (i === 0),
isOdd: (i % 2 !== 0),
releaseDate: releaseData.published_at.substr(0, 10),
releaseDate: releaseData.publishedAt.substr(0, 10),
releaseData: releaseData
};

View File

@@ -307,7 +307,7 @@ define([
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
// select system
$(cell).on('click', function(e){
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: rowData.id });
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: rowData.id});
});
}
},{
@@ -597,7 +597,7 @@ define([
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
// select system
$(cell).on('click', function(e){
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: rowData.source.id });
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: rowData.source.id});
});
}
},{
@@ -615,7 +615,7 @@ define([
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
// select system
$(cell).on('click', function(e){
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: rowData.target.id });
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: rowData.target.id});
});
}
},{

View File

@@ -85,7 +85,7 @@ define([
let markup = '';
if(parts.length === 2){
// wormhole data -> 2 columns
let securityClass = Util.getSecurityClassForSystem(parts[1].length > 3 ? parts[1].substring(0, 2) : parts[1]);
let securityClass = Util.getSecurityClassForSystem(getSystemSecurityFromLabel(parts[1]));
markup += '<span>' + parts[0] + '</span>&nbsp;&nbsp;';
markup += '<i class="fas fa-long-arrow-alt-right txt-color txt-color-grayLight"></i>';
markup += '<span class="' + securityClass + ' ' + Util.config.popoverTriggerClass + ' ' + Util.config.helpDefaultClass +
@@ -118,7 +118,7 @@ define([
let parts = data.text.split(' - ');
if(parts.length === 2){
// wormhole data -> 2 columns
let securityClass = Util.getSecurityClassForSystem(parts[1].length > 3 ? parts[1].substring(0, 2) : parts[1]);
let securityClass = Util.getSecurityClassForSystem(getSystemSecurityFromLabel(parts[1]));
switch(formatType){
case 'wormhole':
@@ -182,6 +182,21 @@ define([
return $(markup);
};
/**
* try to parse a security label into security name
* -> "C1/2/3 (unknown)" -> C1
* "C3" -> C3
* "H" -> H
* "0.0" -> 0.0
* "C12 Thera" -> C12
* @param security
* @returns {string}
*/
let getSystemSecurityFromLabel = security => {
let matches = security.match(/^(\w+\.?\w?)/i);
return matches ? matches[1] : '';
};
/**
* init a select element as "select2" for map selection
*/

View File

@@ -168,7 +168,7 @@ define([
class: 'pf-link',
html: connectionData.sourceAlias + '&nbsp;&nbsp;'
}).on('click', function(){
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: connectionData.source });
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: connectionData.source});
}),
$('<span>', {
class: [config.connectionInfoTableLabelSourceClass].join(' ')
@@ -183,7 +183,7 @@ define([
class: 'pf-link',
html: '&nbsp;&nbsp;' + connectionData.targetAlias
}).on('click', function(){
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: connectionData.target });
Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: connectionData.target});
})
)
)

View File

@@ -39,7 +39,7 @@ define([
// description field
descriptionAreaClass: 'pf-system-info-description-area', // class for "description" area
addDescriptionButtonClass: 'pf-system-info-description-button', // class for "add description" button
descriptionTextareaElementClass: 'pf-system-info-description', // class for "description" textarea element (xEditable)
descriptionTextareaElementClass: 'pf-system-info-description', // class for "description" textarea element (Summernote)
// fonts
fontTriglivianClass: 'pf-triglivian', // class for "Triglivian" names (e.g. Abyssal systems)
@@ -177,6 +177,7 @@ define([
descriptionAreaClass: config.descriptionAreaClass,
descriptionButtonClass: config.addDescriptionButtonClass,
descriptionTextareaClass: config.descriptionTextareaElementClass,
summernoteClass: Util.config.summernoteClass,
systemNameClass: () => {
return (val, render) => {
return render(val) === 'A' ? config.fontTriglivianClass : '';
@@ -298,7 +299,7 @@ define([
},
callbacks: {
onInit: function(context){
// make editable field a big larger
// make editable field a bit larger
context.editable.css('height', '150px');
// set default background color

View File

@@ -4,6 +4,7 @@
define([
'jquery',
'app/init',
'app/console',
'conf/system_effect',
'conf/signature_type',
'bootbox',
@@ -18,7 +19,7 @@ define([
'bootstrapConfirmation',
'bootstrapToggle',
'select2'
], ($, Init, SystemEffect, SignatureType, bootbox, localforage) => {
], ($, Init, Con, SystemEffect, SignatureType, bootbox, localforage) => {
'use strict';
@@ -83,6 +84,9 @@ define([
popoverSmallClass: 'pf-popover-small', // class for small "popover"
popoverCharacterClass: 'pf-popover-character', // class for character "popover"
// Summernote
summernoteClass: 'pf-summernote', // class for Summernote "WYSIWYG" elements
// help
helpDefaultClass: 'pf-help-default', // class for "help" tooltip elements
helpClass: 'pf-help', // class for "help" tooltip elements
@@ -878,9 +882,7 @@ define([
/**
* show current program version information in browser console
*/
let showVersionInfo = () => {
console.info('PATHFINDER ' + getVersion());
};
let showVersionInfo = () => Con.showVersionInfo(getVersion());
/**
* polyfill for "passive" events
@@ -1842,21 +1844,20 @@ define([
let areaId = 0;
switch(security){
case 'H':
areaId = 10;
areaId = 30;
break;
case 'L':
areaId = 11;
areaId = 31;
break;
case '0.0':
areaId = 12;
areaId = 32;
break;
case 'SH':
case 'C13':
areaId = 13;
break;
default:
// w-space
for(let i = 1; i <= 6; i++){
for(let i = 1; i <= 18; i++){
if(security === 'C' + i){
areaId = i;
break;
@@ -2771,16 +2772,28 @@ define([
return Init.currentSystemData;
};
/**
* set current location data
* -> system data where current user is located
* @param systemId
* @param systemName
*/
let setCurrentLocationData = (systemId, systemName) => {
let locationLink = $('#' + config.headCurrentLocationId).find('a');
locationLink.data('systemId', systemId);
locationLink.data('systemName', systemName);
};
/**
* get current location data
* -> system data where current user is located
* @returns {{id: *, name: *}}
*/
let getCurrentLocationData = () => {
let currentLocationLink = $('#' + config.headCurrentLocationId).find('a');
let locationLink = $('#' + config.headCurrentLocationId).find('a');
return {
id: currentLocationLink.data('systemId'),
name: currentLocationLink.data('systemName')
id: locationLink.data('systemId') || 0,
name: locationLink.data('systemName') || false
};
};
@@ -3012,6 +3025,13 @@ define([
return Array.from(doc.body.childNodes).some(node => node.nodeType === 1);
};
/**
* checks if a given object is a DOM element
* @param obj
* @returns {boolean}
*/
let isDomElement = obj => !!(obj && obj.nodeType === 1);
/**
* get deep json object value if exists
* -> e.g. key = 'first.last.third' string
@@ -3189,6 +3209,7 @@ define([
getCurrentCharacterId: getCurrentCharacterId,
setCurrentSystemData: setCurrentSystemData,
getCurrentSystemData: getCurrentSystemData,
setCurrentLocationData: setCurrentLocationData,
getCurrentLocationData: getCurrentLocationData,
getCurrentUserInfo: getCurrentUserInfo,
getCurrentCharacterLog: getCurrentCharacterLog,
@@ -3216,6 +3237,7 @@ define([
htmlEncode: htmlEncode,
htmlDecode: htmlDecode,
isValidHtml: isValidHtml,
isDomElement: isDomElement,
getObjVal: getObjVal,
redirect: redirect,
logout: logout,

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More