diff --git a/.gitignore b/.gitignore
index 4a64b2bd..ee44fc35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,3 +55,4 @@ Temporary Items
/node_modules/
/public/js/vX.X.X/
/vendor/
+/history/
diff --git a/README.md b/README.md
index c07f7f55..efbfacc2 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Mapping tool for [*EVE ONLINE*](https://www.eveonline.com)
- Database will be cleared from time to time
- Installation guide:
- [wiki](https://github.com/exodus4d/pathfinder/wiki)
-- Developer chat [Slack](https://slack.com) :
+- Developer [Slack](https://slack.com) chat:
- https://pathfinder-eve-online.slack.com
- Please send me a mail for invite: pathfinder@exodus4d.de
@@ -28,44 +28,46 @@ Issues should be reported in the [Issue](https://github.com/exodus4d/pathfinder/
### Project structure
```
- |-- (0755) app --> backend [*.php]
- |-- app --> "Fat Free Framework" extensions
- |-- lib --> "Fat Free Framework"
- |-- main --> "PATHFINDER" root
+ |-- [0755] app/ --> backend [*.php]
+ |-- app/ --> "Fat Free Framework" extensions
+ |-- lib/ --> "Fat Free Framework"
+ |-- main/ --> "PATHFINDER" root
|-- config.ini --> config "f3" framework
|-- cron.ini --> config - cronjobs
|-- environment.ini --> config - system environment
|-- pathfinder.ini --> config - pathfinder
|-- requirements.ini --> config - system requirements
|-- routes.ini --> config - routes
- |-- (0755) export --> DB export data
- |-- sql --> static DB data for import (pathfinder.sql)
- |-- (0755) favicon --> Favicons
- |-- (0755) js --> JS source files (raw)
- |-- app --> "PASTHFINDER" core files (not used for production)
- |-- lib --> 3rd partie extension/library (not used for production)
+ |-- [0755] export/ --> static data
+ |-- csv/ --> *.csv used by /setup page
+ |-- json/ --> *.json used by /setup page
+ |-- sql/ --> DB dump for import (pathfinder.sql)
+ |-- [0755] favicon/ --> Favicons
+ |-- [0777] history/ --> log files (map history logs) [optional]
+ |-- [0755] js/ --> JS source files (raw)
+ |-- app/ --> "PASTHFINDER" core files (not used for production)
+ |-- lib/ --> 3rd partie extension/library (not used for production)
|-- app.js --> require.js config (!required for production!)
- |-- (0777) logs --> log files
+ |-- [0777] logs/ --> log files
|-- ...
- | -- node_modules --> node.js modules (not used for production)
+ | -- node_modules/ --> node.js modules (not used for production)
|-- ...
- |-- (0755) public --> frontend source
- |-- css --> CSS dist/build folder (minified)
- |-- fonts --> (icon)-Fonts
- |-- img --> images
- |-- js --> JS dist/build folder and source maps (minified, uglified)
- |-- templates --> templates
- |-- sass --> SCSS source (not used for production)
- |-- ...
- |-- (0777) tmp --> cache folder
- |-- ...
- |-- (0755) .htaccess --> reroute/caching rules ("Apache" only!)
- |-- (0755) index.php
+ |-- [0755] public/ --> frontend source
+ |-- css/ --> CSS dist/build folder (minified)
+ |-- fonts/ --> (icon)-Fonts
+ |-- img/ --> images
+ |-- js/ --> JS dist/build folder and source maps (minified, uglified)
+ |-- templates/ --> templates
+ |-- sass/ --> SCSS source (not used for production)
+ |-- [0777] tmp/ --> cache folder
+ |-- [0755] .htaccess --> reroute/caching rules ("Apache" only!)
+ |-- [0755] index.php
--------------------------
CI/CD config files:
--------------------------
|-- .jshintrc --> "JSHint" config (not used for production)
+ |-- composer.json --> Composer package definition
|-- config.rb --> "Compass" config (not used for production)
|-- gulpfile.js --> "Gulp" task config (not used for production )
|-- package.json --> "Node.js" dependency config (not used for production)
diff --git a/app/app/schema.php b/app/app/schema.php
index ea6bbfd7..1aaee30f 100644
--- a/app/app/schema.php
+++ b/app/app/schema.php
@@ -34,23 +34,23 @@ class Schema extends Controller
$this->f3->set('CACHE', false);
$dbs = array(
- /*'mysql' => new \DB\SQL(
+ 'mysql' => new \DB\SQL(
'mysql:host=localhost;port=3306;dbname=fatfree', 'fatfree', ''
- ),*/
+ ),
'sqlite' => new \DB\SQL(
'sqlite::memory:'
- // 'sqlite:db/sqlite.db'
+// 'sqlite:db/sqlite.db'
),
- /*'pgsql' => new \DB\SQL(
+ 'pgsql' => new \DB\SQL(
'pgsql:host=localhost;dbname=fatfree', 'fatfree', 'fatfree'
- ),*/
- /*'sqlsrv2012' => new \DB\SQL(
- 'sqlsrv:SERVER=LOCALHOST\SQLEXPRESS2012;Database=fatfree','fatfree', 'fatfree'
- ),*/
- /*'sqlsrv2008' => new \DB\SQL(
- 'sqlsrv:SERVER=LOCALHOST\SQLEXPRESS2008;Database=fatfree','fatfree', 'fatfree'
- )*/
- );
+ ),
+// 'sqlsrv2012' => new \DB\SQL(
+// 'sqlsrv:SERVER=LOCALHOST\SQLEXPRESS2012;Database=fatfree','fatfree', 'fatfree'
+// ),
+// 'sqlsrv2008' => new \DB\SQL(
+// 'sqlsrv:SERVER=LOCALHOST\SQLEXPRESS2008;Database=fatfree','fatfree', 'fatfree'
+// )
+ );
$this->roundTime = microtime(TRUE) - \Base::instance()->get('timer');
$this->tname = 'test_table';
@@ -117,10 +117,27 @@ class Schema extends Controller
$this->getTestDesc('adding column ['.$field.'], nullable')
);
}
+
+ $r1 = $table->getCols(true);
+ foreach (array_keys($schema->dataTypes) as $index => $field) {
+ if (isset($r1['column_'.$index])) {
+ $datType=$schema->findQuery($schema->dataTypes[$field]);
+ $compatible = $schema->isCompatible($field,$r1['column_'.$index]['type']);
+ $this->test->expect(
+ $compatible,
+ $this->getTestDesc('reverse lookup compatible: '.
+ ($compatible?'YES':'NO').
+ ', '.$field.': ['.$datType.' > '.$r1['column_'.$index]['type'].']')
+ );
+ }
+ }
unset($r1);
+
// adding some testing data
$mapper = new \DB\SQL\Mapper($db, $this->tname);
+ $mapper->column_5 = 123.456;
+ $mapper->column_6 = 123456.789012;
$mapper->column_7 = 'hello world';
$mapper->save();
$mapper->reset();
@@ -130,11 +147,33 @@ class Schema extends Controller
$result['column_7'] == 'hello world',
$this->getTestDesc('mapping dummy data')
);
+ $this->test->expect(
+ $result['column_5'] == 123.456,
+ $this->getTestDesc('testing float value: '.$result['column_5'])
+ );
+ $this->test->expect(
+ $result['column_6'] == 123456.789012,
+ $this->getTestDesc('testing decimal value: '.$result['column_6'])
+ );
+
+
+ $mapper = new \DB\SQL\Mapper($db, $this->tname);
+ $mapper->load();
+ $num = $this->current_engine == 'sqlite' ? '123456789.012345' : '123456789012.345678';
+ $mapper->column_6 = $num;
+ $mapper->save();
+ $mapper->reset();
+ $result = $mapper->findone(array('column_7 = ?', 'hello world'))->cast();
+ $this->test->expect(
+ $result['column_6'] == $num,
+ $this->getTestDesc('testing max decimal precision: '.$result['column_6'])
+ );
+ unset($mapper);
// default value text, not nullable
$table->addColumn('text_default_not_null')
- ->type($schema::DT_VARCHAR128)
- ->nullable(false)->defaults('foo bar');
+ ->type($schema::DT_VARCHAR128)
+ ->nullable(false)->defaults('foo bar');
$table->build();
$r1 = $table->getCols(true);
$this->test->expect(
@@ -160,7 +199,7 @@ class Schema extends Controller
// default value numeric, not nullable
$table->addColumn('int_default_not_null')
- ->type($schema::DT_INT4)->nullable(false)->defaults(123);
+ ->type($schema::DT_INT4)->nullable(false)->defaults(123);
$table->build();
$r1 = $table->getCols(true);
$this->test->expect(
@@ -187,8 +226,8 @@ class Schema extends Controller
// default value text, nullable
$table->addColumn('text_default_nullable')
- ->type($schema::DT_VARCHAR128)
- ->defaults('foo bar');
+ ->type($schema::DT_VARCHAR128)
+ ->defaults('foo bar');
$table->build();
$r1 = $table->getCols(true);
$this->test->expect(
@@ -253,9 +292,9 @@ class Schema extends Controller
// current timestamp
$table->addColumn('stamp')
- ->type($schema::DT_TIMESTAMP)
- ->nullable(false)
- ->defaults($schema::DF_CURRENT_TIMESTAMP);
+ ->type($schema::DT_TIMESTAMP)
+ ->nullable(false)
+ ->defaults($schema::DF_CURRENT_TIMESTAMP);
$table->build();
$r1 = $table->getCols(true);
$this->test->expect(
@@ -321,7 +360,7 @@ class Schema extends Controller
$table->renameColumn('title123', 'text_default_not_null');
$table->build();
unset($result,$mapper);
-
+
// remove column
$table->dropColumn('column_1');
$table->build();
@@ -380,7 +419,7 @@ class Schema extends Controller
// adding composite primary keys
$table = $schema->createTable($this->tname);
$table->addColumn('version')->type($schema::DT_INT4)
- ->defaults(1)->nullable(false);
+ ->defaults(1)->nullable(false);
$table->primary(array('id', 'version'));
$table = $table->build();
$r1 = $table->getCols(true);
@@ -399,7 +438,7 @@ class Schema extends Controller
$table->addColumn('title')->type($schema::DT_VARCHAR256);
$table->addColumn('title2')->type($schema::DT_TEXT);
$table->addColumn('title_notnull')
- ->type($schema::DT_VARCHAR128)->nullable(false)->defaults("foo");
+ ->type($schema::DT_VARCHAR128)->nullable(false)->defaults("foo");
$table->build();
$r1 = $table->getCols(true);
$this->test->expect(
@@ -461,6 +500,18 @@ class Schema extends Controller
$this->getTestDesc('adding items with composite primary-keys')
);
+ $mapper = new \DB\SQL\Mapper($db, $this->tname);
+ $mapper->load();
+ $rec_count_cur = $mapper->loaded();
+ $schema->truncateTable($this->tname);
+ $mapper->reset();
+ $mapper->load();
+ $rec_count_new = $mapper->loaded();
+ $this->test->expect(
+ $rec_count_cur==3 && $rec_count_new == 0,
+ $this->getTestDesc('truncate table')
+ );
+
$schema->dropTable($this->tname);
// indexes
@@ -520,11 +571,54 @@ class Schema extends Controller
$table->updateColumn('bar',$schema::DT_TEXT);
$table->build();
$r1 = $table->getCols(true);
+ $text = preg_match('/sybase|dblib|odbc|sqlsrv/',$this->current_engine)
+ ? 'nvarchar' : 'text';
$this->test->expect(
- array_key_exists('bar', $r1) && $r1['bar']['type'] == 'text',
+ array_key_exists('bar', $r1) && $r1['bar']['type'] == $text,
$this->getTestDesc('update column')
);
+ // update column
+ $cols = $table->getCols(true);
+ $bar = $cols['bar'];
+ $col = new \DB\SQL\Column('bar',$table);
+ $col->copyfrom($bar);
+ $col->type_varchar(60);
+ $col->defaults('great');
+ $table->updateColumn('bar',$col);
+ $table->build();
+ $r1 = $table->getCols(true);
+ $this->test->expect(
+ array_key_exists('bar', $r1)
+ && $r1['bar']['default'] == 'great',
+ $this->getTestDesc('update column and default')
+ );
+
+ // update column default only
+ $cols = $table->getCols(true);
+ $bar = $cols['bar'];
+ $col = new \DB\SQL\Column('bar',$table);
+ $col->copyfrom($bar);
+ $col->passThrough();
+ $col->defaults('');
+ $table->updateColumn('bar',$col);
+ $table->build();
+ $r1 = $table->getCols(true);
+ $this->test->expect(
+ array_key_exists('bar', $r1) && $r1['bar']['default'] == '',
+ $this->getTestDesc('update default value')
+ );
+
+ $col->nullable(false);
+ $table->updateColumn('bar',$col);
+ $table->build();
+ $r1 = $table->getCols(true);
+ $this->test->expect(
+ array_key_exists('bar', $r1) && $r1['bar']['nullable'] == false,
+ $this->getTestDesc('update nullable flag')
+ );
+
+
// create table with text not nullable column
$table2 = $schema->createTable($this->tname.'_notnulltext');
$table2->addColumn('desc')->type($schema::DT_TEXT)->nullable(false);
@@ -538,7 +632,37 @@ class Schema extends Controller
);
$table2->drop();
-
+ // boolean fields are actually bit/tinyint
+ $schema->dropTable($this->tname.'_notnullbool');
+ $table2 = $schema->createTable($this->tname.'_notnullbool');
+ $table2->addColumn('active')->type($schema::DT_BOOL)->nullable(false);
+ $table2 = $table2->build();
+ $r1 = $schema->getTables();
+ $r2 = $table2->getCols(true);
+ $this->test->expect(
+ in_array($this->tname.'_notnullbool', $r1) && array_key_exists('active', $r2)
+ && $r2['active']['nullable']==false,
+ $this->getTestDesc('create new table with not nullable boolean column')
+ );
+
+ $table2->addColumn('active2')->type($schema::DT_BOOL)->nullable(false)->defaults(0);
+ $table2->addColumn('active3')->type($schema::DT_BOOL)->nullable(false)->defaults(1);
+ $table2->build();
+ $r1 = $schema->getTables();
+ $r2 = $table2->getCols(true);
+ $this->test->expect(
+ in_array($this->tname.'_notnullbool', $r1)
+ && array_key_exists('active2', $r2) && $r2['active2']['nullable']==false &&
+ ((int)$r2['active2']['default']==0||$r2['active2']['default']=='false')
+ && array_key_exists('active3', $r2) && $r2['active3']['nullable']==false &&
+ ((int)$r2['active3']['default']==1||$r2['active3']['default']=='true'),
+ $this->getTestDesc('add not nullable boolean columns with default to existing table')
+ );
+
+
+ $table2->drop();
+
+
}
}
\ No newline at end of file
diff --git a/app/config.ini b/app/config.ini
index 59222d7b..94ce3573 100644
--- a/app/config.ini
+++ b/app/config.ini
@@ -21,6 +21,13 @@ TZ = UTC
CACHE = folder=tmp/cache/
;CACHE = redis=localhost:6379
+; Cache backend used by Session handler.
+; default
+; -If CACHE is enabled (see above), the same location is used for Session data (e.g. fileCache, RedisDB)
+; mysql
+; - Session data get stored in your 'PathfinderDB' table 'sessions' (faster)
+SESSION_CACHE = mysql
+
; Callback functions ==============================================================================
ONERROR = Controller\Controller->showError
UNLOAD = Controller\Controller->unload
diff --git a/app/environment.ini b/app/environment.ini
index 2eca34fd..afed2b33 100644
--- a/app/environment.ini
+++ b/app/environment.ini
@@ -14,13 +14,13 @@ BASE =
URL = {{@SCHEME}}://local.pathfinder
; level of debug/error stack trace
DEBUG = 3
-; main db
-DB_DNS = mysql:host=localhost;port=3306;dbname=
-DB_NAME = pathfinder
-DB_USER = root
-DB_PASS =
+; Pathfinder database
+DB_PF_DNS = mysql:host=localhost;port=3306;dbname=
+DB_PF_NAME = pathfinder
+DB_PF_USER = root
+DB_PF_PASS =
-; EVE-Online CCP Database export
+; EVE-Online CCP database export
DB_CCP_DNS = mysql:host=localhost;port=3306;dbname=
DB_CCP_NAME = eve_citadel_min
DB_CCP_USER = root
@@ -60,11 +60,11 @@ BASE =
URL = {{@SCHEME}}://www.pathfinder-w.space
; level of debug/error stack trace
DEBUG = 0
-; main db
-DB_DNS = mysql:host=localhost;port=3306;dbname=
-DB_NAME =
-DB_USER =
-DB_PASS =
+; Pathfinder database
+DB_PF_DNS = mysql:host=localhost;port=3306;dbname=
+DB_PF_NAME =
+DB_PF_USER =
+DB_PF_PASS =
; EVE-Online CCP Database export
DB_CCP_DNS = mysql:host=localhost;port=3306;dbname=
diff --git a/app/lib/db/cortex.php b/app/lib/db/cortex.php
index c922eb34..b1fbdc7b 100644
--- a/app/lib/db/cortex.php
+++ b/app/lib/db/cortex.php
@@ -18,8 +18,8 @@
* https://github.com/ikkez/F3-Sugar/
*
* @package DB
- * @version 1.5.0-dev
- * @date 27.02.2017
+ * @version 1.5.0
+ * @date 30.06.2017
* @since 24.04.2012
*/
@@ -312,6 +312,7 @@ class Cortex extends Cursor {
static public function setup($db=null, $table=null, $fields=null) {
/** @var Cortex $self */
$self = get_called_class();
+ $self::$schema_cache=[];
if (is_null($db) || is_null($table) || is_null($fields))
$df = $self::resolveConfiguration();
if (!is_object($db=(is_string($db=($db?:$df['db']))?\Base::instance()->get($db):$db)))
@@ -836,9 +837,14 @@ class Cortex extends Cursor {
array_unshift($filter,$crit);
}
}
+ if ($options) {
+ $options = $this->queryParser->prepareOptions($options,$this->dbsType);
+ if ($count)
+ unset($options['order']);
+ }
return ($count)
? $this->mapper->count($filter,$options,$ttl)
- : $this->mapper->find($filter,$this->queryParser->prepareOptions($options,$this->dbsType),$ttl);
+ : $this->mapper->find($filter,$options,$ttl);
}
/**
@@ -1250,12 +1256,12 @@ class Cortex extends Cursor {
$mmTable = $this->mmTable($relConf,$key);
$filter = array($mmTable.'.'.$relConf['relField']
.' = '.$this->table.'.'.$this->primary);
- $from=$mmTable;
+ $from = $this->db->quotekey($mmTable);
if (array_key_exists($key, $this->relFilter) &&
!empty($this->relFilter[$key][0])) {
$options=array();
- $from = $mmTable.' '.$this->_sql_left_join($key,$mmTable,
- $relConf['relPK'],$relConf['relTable']);
+ $from = $this->db->quotekey($mmTable).' '.
+ $this->_sql_left_join($key,$mmTable,$relConf['relPK'],$relConf['relTable']);
$relFilter = $this->relFilter[$key];
$this->_sql_mergeRelCondition($relFilter,$relConf['relTable'],
$filter,$options);
@@ -1266,8 +1272,8 @@ class Cortex extends Cursor {
if (count($filter)>0)
$this->preBinds=array_merge($this->preBinds,$filter);
$this->mapper->set($alias,
- '(select count('.$this->db->quotekey($mmTable.'.'.$relConf['relField']).') from '.
- $this->db->quotekey($from).' where '.$crit.
+ '(select count('.$this->db->quotekey($mmTable.'.'.$relConf['relField']).')'.
+ ' from '.$from.' where '.$crit.
' group by '.$this->db->quotekey($mmTable.'.'.$relConf['relField']).')');
if ($this->whitelist && !in_array($alias,$this->whitelist))
$this->whitelist[] = $alias;
@@ -2444,6 +2450,8 @@ class CortexQueryParser extends \Prefab {
$child = array();
for ($i = 0, $max = count($parts); $i < $max; $i++) {
$part = $parts[$i];
+ if (is_string($part))
+ $part = trim($part);
if ($part == '(') {
// add sub-bracket to parse array
if ($b_offset > 0)
@@ -2460,14 +2468,15 @@ class CortexQueryParser extends \Prefab {
else
// add sub-bracket to parse array
$child[] = $part;
- } // add to parse array
- elseif ($b_offset > 0)
- $child[] = $part;
- // condition type
- elseif (!is_array($part)) {
- if (strtoupper(trim($part)) == 'AND')
+ }
+ elseif ($b_offset > 0) {
+ // add to parse array
+ $child[]=$part;
+ // condition type
+ } elseif (!is_array($part)) {
+ if (strtoupper($part) == 'AND')
$add = true;
- elseif (strtoupper(trim($part)) == 'OR')
+ elseif (strtoupper($part) == 'OR')
$or = true;
} else // skip
$ncond[] = $part;
@@ -2589,7 +2598,7 @@ class CortexQueryParser extends \Prefab {
if (array_key_exists('group', $options) && is_string($options['group'])) {
$keys = explode(',',$options['group']);
$options['group']=array('keys'=>array(),'initial'=>array(),
- 'reduce'=>'function (obj, prev) {}','finalize'=>'');
+ 'reduce'=>'function (obj, prev) {}','finalize'=>'');
$keys = array_combine($keys,array_fill(0,count($keys),1));
$options['group']['keys']=$keys;
$options['group']['initial']=$keys;
diff --git a/app/lib/db/sql/schema.php b/app/lib/db/sql/schema.php
index bea09c30..06ddfea8 100644
--- a/app/lib/db/sql/schema.php
+++ b/app/lib/db/sql/schema.php
@@ -32,77 +32,77 @@ class Schema extends DB_Utils {
public
$dataTypes = array(
'BOOLEAN' => array('mysql' => 'tinyint(1)',
- 'sqlite2?|pgsql' => 'BOOLEAN',
- 'mssql|sybase|dblib|odbc|sqlsrv' => 'bit',
- 'ibm' => 'numeric(1,0)',
+ 'sqlite2?|pgsql' => 'BOOLEAN',
+ 'mssql|sybase|dblib|odbc|sqlsrv' => 'bit',
+ 'ibm' => 'numeric(1,0)',
),
'INT1' => array('mysql' => 'tinyint(4)',
- 'sqlite2?' => 'integer(4)',
- 'mssql|sybase|dblib|odbc|sqlsrv' => 'tinyint',
- 'pgsql|ibm' => 'smallint',
+ 'sqlite2?' => 'integer(4)',
+ 'mssql|sybase|dblib|odbc|sqlsrv' => 'tinyint',
+ 'pgsql|ibm' => 'smallint',
),
'INT2' => array('mysql' => 'smallint(6)',
- 'sqlite2?' => 'integer(6)',
- 'pgsql|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'smallint',
+ 'sqlite2?' => 'integer(6)',
+ 'pgsql|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'smallint',
),
'INT4' => array('sqlite2?' => 'integer(11)',
- 'pgsql|imb' => 'integer',
- 'mysql' => 'int(11)',
- 'mssql|dblib|sybase|odbc|sqlsrv' => 'int',
+ 'pgsql|imb' => 'integer',
+ 'mysql' => 'int(11)',
+ 'mssql|dblib|sybase|odbc|sqlsrv' => 'int',
),
'INT8' => array('sqlite2?' => 'integer(20)',
- 'pgsql|mssql|sybase|dblib|odbc|sqlsrv|imb' => 'bigint',
- 'mysql' => 'bigint(20)',
+ 'pgsql|mssql|sybase|dblib|odbc|sqlsrv|imb' => 'bigint',
+ 'mysql' => 'bigint(20)',
),
'FLOAT' => array('mysql|sqlite2?' => 'FLOAT',
- 'pgsql' => 'double precision',
- 'mssql|sybase|dblib|odbc|sqlsrv' => 'float',
- 'imb' => 'decfloat'
+ 'pgsql' => 'double precision',
+ 'mssql|sybase|dblib|odbc|sqlsrv' => 'float',
+ 'imb' => 'decfloat'
),
'DOUBLE' => array('mysql|ibm' => 'decimal(18,6)',
- 'sqlite2?' => 'decimal(15,6)', // max 15-digit on sqlite
- 'pgsql' => 'numeric(18,6)',
- 'mssql|dblib|sybase|odbc|sqlsrv' => 'decimal(18,6)',
+ 'sqlite2?' => 'decimal(15,6)', // max 15-digit on sqlite
+ 'pgsql' => 'numeric(18,6)',
+ 'mssql|dblib|sybase|odbc|sqlsrv' => 'decimal(18,6)',
),
'VARCHAR128' => array('mysql|sqlite2?|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'varchar(128)',
- 'pgsql' => 'character varying(128)',
+ 'pgsql' => 'character varying(128)',
),
'VARCHAR256' => array('mysql|sqlite2?|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'varchar(255)',
- 'pgsql' => 'character varying(255)',
+ 'pgsql' => 'character varying(255)',
),
'VARCHAR512' => array('mysql|sqlite2?|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'varchar(512)',
- 'pgsql' => 'character varying(512)',
+ 'pgsql' => 'character varying(512)',
),
'TEXT' => array('mysql|sqlite2?|pgsql|mssql' => 'text',
- 'sybase|dblib|odbc|sqlsrv' => 'nvarchar(max)',
- 'ibm' => 'BLOB SUB_TYPE TEXT',
+ 'sybase|dblib|odbc|sqlsrv' => 'nvarchar(max)',
+ 'ibm' => 'BLOB SUB_TYPE TEXT',
),
'LONGTEXT' => array('mysql' => 'LONGTEXT',
- 'sqlite2?|pgsql|mssql' => 'text',
- 'sybase|dblib|odbc|sqlsrv' => 'nvarchar(max)',
- 'ibm' => 'CLOB(2000000000)',
+ 'sqlite2?|pgsql|mssql' => 'text',
+ 'sybase|dblib|odbc|sqlsrv' => 'nvarchar(max)',
+ 'ibm' => 'CLOB(2000000000)',
),
'DATE' => array('mysql|sqlite2?|pgsql|mssql|sybase|dblib|odbc|sqlsrv|ibm' => 'date',
),
'DATETIME' => array('pgsql' => 'timestamp without time zone',
- 'mysql|sqlite2?|mssql|sybase|dblib|odbc|sqlsrv' => 'datetime',
- 'ibm' => 'timestamp',
+ 'mysql|sqlite2?|mssql|sybase|dblib|odbc|sqlsrv' => 'datetime',
+ 'ibm' => 'timestamp',
),
'TIMESTAMP' => array('mysql|ibm' => 'timestamp',
- 'pgsql|odbc' => 'timestamp without time zone',
- 'sqlite2?|mssql|sybase|dblib|sqlsrv'=>'DATETIME',
+ 'pgsql|odbc' => 'timestamp without time zone',
+ 'sqlite2?|mssql|sybase|dblib|sqlsrv'=>'DATETIME',
),
'BLOB' => array('mysql|odbc|sqlite2?|ibm' => 'blob',
- 'pgsql' => 'bytea',
- 'mssql|sybase|dblib' => 'image',
- 'sqlsrv' => 'varbinary(max)',
+ 'pgsql' => 'bytea',
+ 'mssql|sybase|dblib' => 'image',
+ 'sqlsrv' => 'varbinary(max)',
),
),
$defaultTypes = array(
'CUR_STAMP' => array('mysql' => 'CURRENT_TIMESTAMP',
- 'mssql|sybase|dblib|odbc|sqlsrv' => 'getdate()',
- 'pgsql' => 'LOCALTIMESTAMP(0)',
- 'sqlite2?' => "(datetime('now','localtime'))",
+ 'mssql|sybase|dblib|odbc|sqlsrv' => 'getdate()',
+ 'pgsql' => 'LOCALTIMESTAMP(0)',
+ 'sqlite2?' => "(datetime('now','localtime'))",
),
);
diff --git a/app/main/controller/accesscontroller.php b/app/main/controller/accesscontroller.php
index e35cca71..1c095b5b 100644
--- a/app/main/controller/accesscontroller.php
+++ b/app/main/controller/accesscontroller.php
@@ -17,30 +17,30 @@ class AccessController extends Controller {
/**
* event handler
* @param \Base $f3
- * @param array $params
+ * @param $params
+ * @return bool
*/
- function beforeroute(\Base $f3, $params) {
- parent::beforeroute($f3, $params);
+ function beforeroute(\Base $f3, $params): bool {
+ 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) ){
+ // no character found or login timer expired
+ $this->logoutCharacter();
- // Any route/endpoint of a child class of this one,
- // requires a valid logged in user!
- $loginCheck = $this->isLoggedIn($f3);
-
- if( !$loginCheck ){
- // no user found or login timer expired
- $this->logout($f3);
-
- if( $f3->get('AJAX') ){
- // unauthorized request
- $f3->status(403);
- }else{
- // redirect to landing page
- $f3->reroute(['login']);
+ if($f3->get('AJAX')){
+ // unauthorized request
+ $f3->status(403);
+ }else{
+ // redirect to landing page
+ $f3->reroute(['login']);
+ }
+ // skip route handler and afterroute()
+ $return = false;
}
-
- // die() triggers unload() function
- die();
}
+
+ return $return;
}
/**
@@ -48,7 +48,7 @@ class AccessController extends Controller {
* @param \Base $f3
* @return bool
*/
- protected function isLoggedIn(\Base $f3){
+ protected function isLoggedIn(\Base $f3): bool {
$loginCheck = false;
if( $character = $this->getCharacter() ){
if($this->checkLogTimer($f3, $character)){
@@ -84,7 +84,7 @@ class AccessController extends Controller {
$minutes += $timeDiff->h * 60;
$minutes += $timeDiff->i;
- if($minutes <= $f3->get('PATHFINDER.TIMER.LOGGED')){
+ if($minutes <= Config::getPathfinderData('timer.logged')){
$loginCheck = true;
}
}
diff --git a/app/main/controller/admin.php b/app/main/controller/admin.php
index 378bfbd1..20a514b9 100644
--- a/app/main/controller/admin.php
+++ b/app/main/controller/admin.php
@@ -32,9 +32,11 @@ class Admin extends Controller{
* event handler for all "views"
* some global template variables are set in here
* @param \Base $f3
+ * @param $params
+ * @return bool
*/
- function beforeroute(\Base $f3, $params) {
- parent::beforeroute($f3, $params);
+ function beforeroute(\Base $f3, $params): bool {
+ $return = parent::beforeroute($f3, $params);
$f3->set('tplPage', 'login');
@@ -54,6 +56,8 @@ class Admin extends Controller{
// body element class
$f3->set('tplBodyClass', 'pf-landing');
+
+ return $return;
}
/**
@@ -291,7 +295,9 @@ class Admin extends Controller{
}
foreach($corporations as $corporation){
- $data->corpMembers[$corporation->name] = $corporation->getCharacters();
+ if($characters = $corporation->getCharacters()){
+ $data->corpMembers[$corporation->name] = $corporation->getCharacters();
+ }
}
// sort corporation from current user first
diff --git a/app/main/controller/api/access.php b/app/main/controller/api/access.php
index c323a54b..94898f54 100644
--- a/app/main/controller/api/access.php
+++ b/app/main/controller/api/access.php
@@ -12,16 +12,6 @@ use Model;
class Access extends Controller\AccessController {
- /**
- * event handler
- * @param \Base $f3
- * @param array $params
- */
- function beforeroute(\Base $f3, $params) {
- // set header for all routes
- header('Content-type: application/json');
- parent::beforeroute($f3, $params);
- }
/**
* search character/corporation or alliance by name
diff --git a/app/main/controller/api/connection.php b/app/main/controller/api/connection.php
index 89717d28..85a490c6 100644
--- a/app/main/controller/api/connection.php
+++ b/app/main/controller/api/connection.php
@@ -12,15 +12,6 @@ use Model;
class Connection extends Controller\AccessController {
- /**
- * @param \Base $f3
- * @param array $params
- */
- function beforeroute(\Base $f3, $params) {
- // set header for all routes
- header('Content-type: application/json');
- parent::beforeroute($f3, $params);
- }
/**
* save a new connection or updates an existing (drag/drop) between two systems
@@ -29,7 +20,10 @@ class Connection extends Controller\AccessController {
*/
public function save(\Base $f3){
$postData = (array)$f3->get('POST');
- $newConnectionData = [];
+
+ $return = (object) [];
+ $return->error = [];
+ $return->connectionData = (object) [];
if(
isset($postData['connectionData']) &&
@@ -75,27 +69,23 @@ class Connection extends Controller\AccessController {
$connectionData['scope'] = 'wh';
$connectionData['type'] = ['wh_fresh'];
}
-
$connectionData['mapId'] = $map;
- // "updated" should not be set by client e.g. after manual drag&drop
- unset($connectionData['updated']);
-
$connection->setData($connectionData);
- if( $connection->isValid() ){
- $connection->save();
-
- $newConnectionData = $connection->getData();
+ if($connection->save($activeCharacter)){
+ $return->connectionData = $connection->getData();
// broadcast map changes
$this->broadcastMapData($connection->mapId);
+ }else{
+ $return->error = $connection->getErrors();
}
}
}
}
- echo json_encode($newConnectionData);
+ echo json_encode($return);
}
/**
diff --git a/app/main/controller/api/github.php b/app/main/controller/api/github.php
index e9078788..18ccc093 100644
--- a/app/main/controller/api/github.php
+++ b/app/main/controller/api/github.php
@@ -7,7 +7,7 @@
*/
namespace Controller\Api;
-use Model;
+use lib\Config;
use Controller;
@@ -43,7 +43,7 @@ class GitHub extends Controller\Controller {
$releaseCount = 4;
if( !$f3->exists($cacheKey) ){
- $apiPath = $this->getF3()->get('PATHFINDER.API.GIT_HUB') . '/repos/exodus4d/pathfinder/releases';
+ $apiPath = Config::getPathfinderData('api.git_hub') . '/repos/exodus4d/pathfinder/releases';
// build request URL
$options = $this->getRequestOptions();
diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php
index 37362da1..2719ff45 100644
--- a/app/main/controller/api/map.php
+++ b/app/main/controller/api/map.php
@@ -8,9 +8,11 @@
namespace Controller\Api;
use Controller;
+use data\file\FileHandler;
use lib\Config;
use lib\Socket;
use Model;
+use Exception;
/**
* Map controller
@@ -23,24 +25,15 @@ class Map extends Controller\AccessController {
const CACHE_KEY_INIT = 'CACHED_INIT';
const CACHE_KEY_MAP_DATA = 'CACHED.MAP_DATA.%s';
const CACHE_KEY_USER_DATA = 'CACHED.USER_DATA.%s_%s';
+ const CACHE_KEY_HISTORY = 'CACHED_MAP_HISTORY_%s';
- /**
- * event handler
- * @param \Base $f3
- * @param array $params
- */
- function beforeroute(\Base $f3, $params) {
- // set header for all routes
- header('Content-type: application/json');
- parent::beforeroute($f3, $params);
- }
/**
* get map data cache key
* @param Model\CharacterModel $character
* @return string
*/
- protected function getMapDataCacheKey(Model\CharacterModel $character){
+ protected function getMapDataCacheKey(Model\CharacterModel $character): string {
return sprintf(self::CACHE_KEY_MAP_DATA, 'CHAR_' . $character->_id);
}
@@ -62,10 +55,19 @@ class Map extends Controller\AccessController {
* @param int $systemId
* @return string
*/
- protected function getUserDataCacheKey($mapId, $systemId = 0){
+ protected function getUserDataCacheKey($mapId, $systemId = 0): string {
return sprintf(self::CACHE_KEY_USER_DATA, 'MAP_' . $mapId, 'SYS_' . $systemId);
}
+ /**
+ * get log history data cache key
+ * @param int $mapId
+ * @return string
+ */
+ protected function getHistoryDataCacheKey(int $mapId): string {
+ return sprintf(self::CACHE_KEY_HISTORY, 'MAP_' . $mapId);
+ }
+
/**
* Get all required static config data for program initialization
* @param \Base $f3
@@ -79,10 +81,10 @@ class Map extends Controller\AccessController {
$return = (object) [];
$return->error = [];
- // static program data ----------------------------------------------------------------------------------------
- $return->timer = $f3->get('PATHFINDER.TIMER');
+ // static program data ------------------------------------------------------------------------------------
+ $return->timer = Config::getPathfinderData('timer');
- // get all available map types --------------------------------------------------------------------------------
+ // get all available map types ----------------------------------------------------------------------------
$mapType = Model\BasicModel::getNew('MapTypeModel');
$rows = $mapType->find('active = 1', null, $expireTimeSQL);
@@ -103,7 +105,7 @@ class Map extends Controller\AccessController {
}
$return->mapTypes = $mapTypeData;
- // get all available map scopes -------------------------------------------------------------------------------
+ // get all available map scopes ---------------------------------------------------------------------------
$mapScope = Model\BasicModel::getNew('MapScopeModel');
$rows = $mapScope->find('active = 1', null, $expireTimeSQL);
$mapScopeData = [];
@@ -116,7 +118,7 @@ class Map extends Controller\AccessController {
}
$return->mapScopes = $mapScopeData;
- // get all available system status ----------------------------------------------------------------------------
+ // get all available system status ------------------------------------------------------------------------
$systemStatus = Model\BasicModel::getNew('SystemStatusModel');
$rows = $systemStatus->find('active = 1', null, $expireTimeSQL);
$systemScopeData = [];
@@ -130,7 +132,7 @@ class Map extends Controller\AccessController {
}
$return->systemStatus = $systemScopeData;
- // get all available system types -----------------------------------------------------------------------------
+ // get all available system types -------------------------------------------------------------------------
$systemType = Model\BasicModel::getNew('SystemTypeModel');
$rows = $systemType->find('active = 1', null, $expireTimeSQL);
$systemTypeData = [];
@@ -143,7 +145,7 @@ class Map extends Controller\AccessController {
}
$return->systemType = $systemTypeData;
- // get available connection scopes ----------------------------------------------------------------------------
+ // get available connection scopes ------------------------------------------------------------------------
$connectionScope = Model\BasicModel::getNew('ConnectionScopeModel');
$rows = $connectionScope->find('active = 1', null, $expireTimeSQL);
$connectionScopeData = [];
@@ -157,7 +159,7 @@ class Map extends Controller\AccessController {
}
$return->connectionScopes = $connectionScopeData;
- // get available character status -----------------------------------------------------------------------------
+ // get available character status -------------------------------------------------------------------------
$characterStatus = Model\BasicModel::getNew('CharacterStatusModel');
$rows = $characterStatus->find('active = 1', null, $expireTimeSQL);
$characterStatusData = [];
@@ -171,21 +173,27 @@ class Map extends Controller\AccessController {
}
$return->characterStatus = $characterStatusData;
- // route search config ----------------------------------------------------------------------------------------
+ // route search config ------------------------------------------------------------------------------------
$return->routeSearch = [
- 'defaultCount' => $this->getF3()->get('PATHFINDER.ROUTE.SEARCH_DEFAULT_COUNT'),
- 'maxDefaultCount' => $this->getF3()->get('PATHFINDER.ROUTE.MAX_Default_COUNT'),
- 'limit' => $this->getF3()->get('PATHFINDER.ROUTE.LIMIT'),
+ 'defaultCount' => Config::getPathfinderData('route.search_default_count'),
+ 'maxDefaultCount' => Config::getPathfinderData('route.max_default_count'),
+ 'limit' => Config::getPathfinderData('route.limit')
];
- // get program routes -----------------------------------------------------------------------------------------
+ // get program routes -------------------------------------------------------------------------------------
$return->routes = [
'ssoLogin' => $this->getF3()->alias( 'sso', ['action' => 'requestAuthorization'] )
];
- // get notification status ------------------------------------------------------------------------------------
- $return->notificationStatus = [
- 'rallySet' => (bool)Config::getNotificationMail('RALLY_SET')
+ // get third party APIs -----------------------------------------------------------------------------------
+ $return->url = [
+ 'ccpImageServer' => Config::getPathfinderData('api.ccp_image_server'),
+ 'zKillboard' => Config::getPathfinderData('api.z_killboard')
+ ];
+
+ // Slack integration status -------------------------------------------------------------------------------
+ $return->slack = [
+ 'status' => (bool)Config::getPathfinderData('slack.status')
];
$f3->set(self::CACHE_KEY_INIT, $return, $expireTimeCache );
@@ -195,7 +203,7 @@ class Map extends Controller\AccessController {
// program mode (e.g. "maintenance") --------------------------------------------------------------------------
$return->programMode = [
- 'maintenance' => $this->getF3()->get('PATHFINDER.LOGIN.MODE_MAINTENANCE')
+ 'maintenance' => Config::getPathfinderData('login.mode_maintenance')
];
// get SSO error messages that should be shown immediately ----------------------------------------------------
@@ -263,17 +271,12 @@ class Map extends Controller\AccessController {
isset($mapData['data']['systems']) &&
isset($mapData['data']['connections'])
){
- if(isset($mapData['config']['id'])){
- unset($mapData['config']['id']);
- }
-
-
$systemCount = count($mapData['data']['systems']);
if( $systemCount <= $defaultConfig['max_systems']){
$map->setData($mapData['config']);
$map->typeId = (int)$importData['typeId'];
- $map->save();
+ $map->save($activeCharacter);
// new system IDs will be generated
// therefore we need to temp store a mapping between IDs
@@ -282,13 +285,10 @@ class Map extends Controller\AccessController {
foreach($mapData['data']['systems'] as $systemData){
if(isset($systemData['id'])){
$oldId = (int)$systemData['id'];
- unset($systemData['id']);
$system->setData($systemData);
$system->mapId = $map;
- $system->createdCharacterId = $activeCharacter;
- $system->updatedCharacterId = $activeCharacter;
- $system->save();
+ $system->save($activeCharacter);
$tempSystemIdMapping[$oldId] = $system->id;
$system->reset();
@@ -301,15 +301,11 @@ class Map extends Controller\AccessController {
isset( $tempSystemIdMapping[$connectionData['source']] ) &&
isset( $tempSystemIdMapping[$connectionData['target']] )
){
- if(isset($connectionData['id'])){
- unset($connectionData['id']);
- }
-
$connection->setData($connectionData);
$connection->mapId = $map;
$connection->source = $tempSystemIdMapping[$connectionData['source']];
$connection->target = $tempSystemIdMapping[$connectionData['target']];
- $connection->save();
+ $connection->save($activeCharacter);
$connection->reset();
}
@@ -391,150 +387,161 @@ class Map extends Controller\AccessController {
$map->dry() ||
$map->hasAccess($activeCharacter)
){
- // new map
- $map->setData($formData);
- $map = $map->save();
+ try{
+ // new map
+ $map->setData($formData);
+ $map = $map->save($activeCharacter);
- // save global map access. Depends on map "type"
- if($map->isPrivate()){
+ $mapDefaultConf = Config::getMapsDefaultConfig();
- // share map between characters -> set access
- if(isset($formData['mapCharacters'])){
- // remove character corporation (re-add later)
- $accessCharacters = array_diff($formData['mapCharacters'], [$activeCharacter->_id]);
+ // save global map access. Depends on map "type"
+ if($map->isPrivate()){
- // avoid abuse -> respect share limits
- $maxShared = max($f3->get('PATHFINDER.MAP.PRIVATE.MAX_SHARED') - 1, 0);
- $accessCharacters = array_slice($accessCharacters, 0, $maxShared);
-
- // clear map access. In case something has removed from access list
- $map->clearAccess();
-
- if($accessCharacters){
- /**
- * @var $tempCharacter Model\CharacterModel
- */
- $tempCharacter = Model\BasicModel::getNew('CharacterModel');
-
- foreach($accessCharacters as $characterId){
- $tempCharacter->getById( (int)$characterId );
-
- if(
- !$tempCharacter->dry() &&
- $tempCharacter->shared == 1 // check if map shared is enabled
- ){
- $map->setAccess($tempCharacter);
- }
-
- $tempCharacter->reset();
- }
- }
- }
-
- // the current character itself should always have access
- // just in case he removed himself :)
- $map->setAccess($activeCharacter);
- }elseif($map->isCorporation()){
- $corporation = $activeCharacter->getCorporation();
-
- if($corporation){
- // the current user has to have a corporation when
- // working on corporation maps!
-
- // share map between corporations -> set access
- if(isset($formData['mapCorporations'])){
+ // share map between characters -> set access
+ if(isset($formData['mapCharacters'])){
// remove character corporation (re-add later)
- $accessCorporations = array_diff($formData['mapCorporations'], [$corporation->_id]);
+ $accessCharacters = array_diff($formData['mapCharacters'], [$activeCharacter->_id]);
// avoid abuse -> respect share limits
- $maxShared = max($f3->get('PATHFINDER.MAP.CORPORATION.MAX_SHARED') - 1, 0);
- $accessCorporations = array_slice($accessCorporations, 0, $maxShared);
+ $maxShared = max($mapDefaultConf['private']['max_shared'] - 1, 0);
+ $accessCharacters = array_slice($accessCharacters, 0, $maxShared);
// clear map access. In case something has removed from access list
$map->clearAccess();
- if($accessCorporations){
+ if($accessCharacters){
/**
- * @var $tempCorporation Model\CorporationModel
+ * @var $tempCharacter Model\CharacterModel
*/
- $tempCorporation = Model\BasicModel::getNew('CorporationModel');
+ $tempCharacter = Model\BasicModel::getNew('CharacterModel');
- foreach($accessCorporations as $corporationId){
- $tempCorporation->getById( (int)$corporationId );
+ foreach($accessCharacters as $characterId){
+ $tempCharacter->getById( (int)$characterId );
if(
- !$tempCorporation->dry() &&
- $tempCorporation->shared == 1 // check if map shared is enabled
+ !$tempCharacter->dry() &&
+ $tempCharacter->shared == 1 // check if map shared is enabled
){
- $map->setAccess($tempCorporation);
+ $map->setAccess($tempCharacter);
}
- $tempCorporation->reset();
+ $tempCharacter->reset();
}
}
}
- // the corporation of the current user should always have access
- $map->setAccess($corporation);
- }
- }elseif($map->isAlliance()){
- $alliance = $activeCharacter->getAlliance();
+ // the current character itself should always have access
+ // just in case he removed himself :)
+ $map->setAccess($activeCharacter);
+ }elseif($map->isCorporation()){
+ $corporation = $activeCharacter->getCorporation();
- if($alliance){
- // the current user has to have a alliance when
- // working on alliance maps!
+ if($corporation){
+ // the current user has to have a corporation when
+ // working on corporation maps!
- // share map between alliances -> set access
- if(isset($formData['mapAlliances'])){
- // remove character alliance (re-add later)
- $accessAlliances = array_diff($formData['mapAlliances'], [$alliance->_id]);
+ // share map between corporations -> set access
+ if(isset($formData['mapCorporations'])){
+ // remove character corporation (re-add later)
+ $accessCorporations = array_diff($formData['mapCorporations'], [$corporation->_id]);
- // avoid abuse -> respect share limits
- $maxShared = max($f3->get('PATHFINDER.MAP.ALLIANCE.MAX_SHARED') - 1, 0);
- $accessAlliances = array_slice($accessAlliances, 0, $maxShared);
+ // avoid abuse -> respect share limits
+ $maxShared = max($mapDefaultConf['corporation']['max_shared'] - 1, 0);
+ $accessCorporations = array_slice($accessCorporations, 0, $maxShared);
- // clear map access. In case something has removed from access list
- $map->clearAccess();
+ // clear map access. In case something has removed from access list
+ $map->clearAccess();
- if($accessAlliances){
- /**
- * @var $tempAlliance Model\AllianceModel
- */
- $tempAlliance = Model\BasicModel::getNew('AllianceModel');
+ if($accessCorporations){
+ /**
+ * @var $tempCorporation Model\CorporationModel
+ */
+ $tempCorporation = Model\BasicModel::getNew('CorporationModel');
- foreach($accessAlliances as $allianceId){
- $tempAlliance->getById( (int)$allianceId );
+ foreach($accessCorporations as $corporationId){
+ $tempCorporation->getById( (int)$corporationId );
- if(
- !$tempAlliance->dry() &&
- $tempAlliance->shared == 1 // check if map shared is enabled
- ){
- $map->setAccess($tempAlliance);
+ if(
+ !$tempCorporation->dry() &&
+ $tempCorporation->shared == 1 // check if map shared is enabled
+ ){
+ $map->setAccess($tempCorporation);
+ }
+
+ $tempCorporation->reset();
}
-
- $tempAlliance->reset();
}
}
- }
- // the alliance of the current user should always have access
- $map->setAccess($alliance);
+ // the corporation of the current user should always have access
+ $map->setAccess($corporation);
+ }
+ }elseif($map->isAlliance()){
+ $alliance = $activeCharacter->getAlliance();
+
+ if($alliance){
+ // the current user has to have a alliance when
+ // working on alliance maps!
+
+ // share map between alliances -> set access
+ if(isset($formData['mapAlliances'])){
+ // remove character alliance (re-add later)
+ $accessAlliances = array_diff($formData['mapAlliances'], [$alliance->_id]);
+
+ // avoid abuse -> respect share limits
+ $maxShared = max($mapDefaultConf['alliance']['max_shared'] - 1, 0);
+ $accessAlliances = array_slice($accessAlliances, 0, $maxShared);
+
+ // clear map access. In case something has removed from access list
+ $map->clearAccess();
+
+ if($accessAlliances){
+ /**
+ * @var $tempAlliance Model\AllianceModel
+ */
+ $tempAlliance = Model\BasicModel::getNew('AllianceModel');
+
+ foreach($accessAlliances as $allianceId){
+ $tempAlliance->getById( (int)$allianceId );
+
+ if(
+ !$tempAlliance->dry() &&
+ $tempAlliance->shared == 1 // check if map shared is enabled
+ ){
+ $map->setAccess($tempAlliance);
+ }
+
+ $tempAlliance->reset();
+ }
+ }
+ }
+
+ // the alliance of the current user should always have access
+ $map->setAccess($alliance);
+ }
}
+ // reload the same map model (refresh)
+ // this makes sure all data is up2date
+ $map->getById( $map->_id, 0 );
+
+
+ $charactersData = $map->getCharactersData();
+ $characterIds = array_map(function ($data){
+ return $data->id;
+ }, $charactersData);
+
+ // broadcast map Access -> and send map Data
+ $this->broadcastMapAccess($map, $characterIds);
+
+ $return->mapData = $map->getData();
+ }catch(Exception\ValidationException $e){
+ $validationError = (object) [];
+ $validationError->type = 'error';
+ $validationError->field = $e->getField();
+ $validationError->message = $e->getMessage();
+ $return->error[] = $validationError;
}
- // reload the same map model (refresh)
- // this makes sure all data is up2date
- $map->getById( $map->_id, 0 );
-
- $charactersData = $map->getCharactersData();
- $characterIds = array_map(function ($data){
- return $data->id;
- }, $charactersData);
-
- // broadcast map Access -> and send map Data
- $this->broadcastMapAccess($map, $characterIds);
-
- $return->mapData = $map->getData();
}else{
// map access denied
$captchaError = (object) [];
@@ -559,18 +566,30 @@ class Map extends Controller\AccessController {
*/
public function delete(\Base $f3){
$mapData = (array)$f3->get('POST.mapData');
- $activeCharacter = $this->getCharacter();
+ $mapId = (int)$mapData['id'];
+ $return = (object) [];
+ $return->deletedMapIds = [];
- /**
- * @var $map Model\MapModel
- */
- $map = Model\BasicModel::getNew('MapModel');
- $map->getById($mapData['id']);
- $map->delete( $activeCharacter, function($mapId){
- $this->broadcastMapDeleted($mapId);
- });
+ if($mapId){
+ $activeCharacter = $this->getCharacter();
- echo json_encode([]);
+ /**
+ * @var $map Model\MapModel
+ */
+ $map = Model\BasicModel::getNew('MapModel');
+ $map->getById($mapId);
+
+ if($map->hasAccess($activeCharacter)){
+ $map->setActive(false);
+ $map->save($activeCharacter);
+ $return->deletedMapIds[] = $mapId;
+
+ // broadcast map delete
+ $this->broadcastMapDeleted($mapId);
+ }
+ }
+
+ echo json_encode($return);
}
/**
@@ -654,7 +673,7 @@ class Map extends Controller\AccessController {
$return = (object) [];
$return->error = [];
- // get current map data ===============================================================================
+ // get current map data ===================================================================================
$maps = $activeCharacter->getMaps();
// loop all submitted map data that should be saved
@@ -680,13 +699,13 @@ class Map extends Controller\AccessController {
count($connections) > 0
){
- // map changes expected =======================================================================
+ // map changes expected ===========================================================================
// loop current user maps and check for changes
foreach($maps as $map){
$mapChanged = false;
- // update system data ---------------------------------------------------------------------
+ // update system data -------------------------------------------------------------------------
foreach($systems as $i => $systemData){
// check if current system belongs to the current map
@@ -703,25 +722,25 @@ class Map extends Controller\AccessController {
// system belongs to the current map
if(is_object($filteredMap->systems)){
// update
- unset($systemData['updated']);
/**
* @var $system Model\SystemModel
*/
$system = $filteredMap->systems->current();
$system->setData($systemData);
- $system->updatedCharacterId = $activeCharacter;
- $system->save();
- $mapChanged = true;
-
- // a system belongs to ONE map -> speed up for multiple maps
- unset($systemData[$i]);
+ if($system->save($activeCharacter)){
+ $mapChanged = true;
+ // one system belongs to ONE map -> speed up for multiple maps
+ unset($systemData[$i]);
+ }else{
+ $return->error = array_merge($return->error, $system->getErrors());
+ }
}
}
}
- // update connection data -----------------------------------------------------------------
+ // update connection data ---------------------------------------------------------------------
foreach($connections as $i => $connectionData){
// check if the current connection belongs to the current map
@@ -738,19 +757,20 @@ class Map extends Controller\AccessController {
// connection belongs to the current map
if(is_object($filteredMap->connections)){
// update
- unset($connectionData['updated']);
/**
* @var $connection Model\ConnectionModel
*/
$connection = $filteredMap->connections->current();
$connection->setData($connectionData);
- $connection->save();
- $mapChanged = true;
-
- // a connection belongs to ONE map -> speed up for multiple maps
- unset($connectionData[$i]);
+ if($connection->save($activeCharacter)){
+ $mapChanged = true;
+ // one connection belongs to ONE map -> speed up for multiple maps
+ unset($connectionData[$i]);
+ }else{
+ $return->error = array_merge($return->error, $connection->getErrors());
+ }
}
}
}
@@ -767,7 +787,7 @@ class Map extends Controller\AccessController {
// cache time(s) per user should be equal or less than this function is called
// prevent request flooding
- $responseTTL = (int)$f3->get('PATHFINDER.TIMER.UPDATE_SERVER_MAP.DELAY') / 1000;
+ $responseTTL = (int)Config::getPathfinderData('timer.update_server_map.delay') / 1000;
$f3->set($cacheKey, $return, $responseTTL);
}
@@ -852,7 +872,7 @@ class Map extends Controller\AccessController {
// cache time (seconds) should be equal or less than request trigger time
// prevent request flooding
- $responseTTL = (int)$f3->get('PATHFINDER.TIMER.UPDATE_SERVER_USER_DATA.DELAY') / 1000;
+ $responseTTL = (int)Config::getPathfinderData('timer.update_server_user_data.delay') / 1000;
// cache response
$f3->set($cacheKey, $return, $responseTTL);
@@ -1003,13 +1023,13 @@ class Map extends Controller\AccessController {
break;
}
- // save source system -------------------------------------------------------------------------------------
+ // save source system ---------------------------------------------------------------------------------
if(
$addSourceSystem &&
$sourceSystem &&
!$sourceExists
){
- $sourceSystem = $map->saveSystem($sourceSystem, $systemPosX, $systemPosY, $character);
+ $sourceSystem = $map->saveSystem($sourceSystem, $character, $systemPosX, $systemPosY);
// get updated maps object
if($sourceSystem){
$map = $sourceSystem->mapId;
@@ -1021,13 +1041,13 @@ class Map extends Controller\AccessController {
}
}
- // save target system -------------------------------------------------------------------------------------
+ // save target system ---------------------------------------------------------------------------------
if(
$addTargetSystem &&
$targetSystem &&
!$targetExists
){
- $targetSystem = $map->saveSystem($targetSystem, $systemPosX, $systemPosY, $character);
+ $targetSystem = $map->saveSystem($targetSystem, $character, $systemPosX, $systemPosY);
// get updated maps object
if($targetSystem){
$map = $targetSystem->mapId;
@@ -1036,7 +1056,7 @@ class Map extends Controller\AccessController {
}
}
- // save connection ----------------------------------------------------------------------------------------
+ // save connection ------------------------------------------------------------------------------------
if(
$addConnection &&
$sourceExists &&
@@ -1046,7 +1066,7 @@ class Map extends Controller\AccessController {
!$map->searchConnection( $sourceSystem, $targetSystem )
){
$connection = $map->getNewConnection($sourceSystem, $targetSystem);
- $connection = $map->saveConnection($connection);
+ $connection = $map->saveConnection($connection, $character);
// get updated maps object
if($connection){
$map = $connection->mapId;
@@ -1097,6 +1117,53 @@ class Map extends Controller\AccessController {
echo json_encode($connectionData);
}
+ /**
+ * get map log data
+ * @param \Base $f3
+ */
+ public function getLogData(\Base $f3){
+ $postData = (array)$f3->get('POST');
+ $return = (object) [];
+ $return->data = [];
+
+ // validate query parameters
+ $return->query = [
+ 'mapId' => (int) $postData['mapId'],
+ 'offset' => FileHandler::validateOffset( (int)$postData['offset'] ),
+ 'limit' => FileHandler::validateLimit( (int)$postData['limit'] )
+ ];
+
+ if($mapId = (int)$postData['mapId']){
+ $activeCharacter = $this->getCharacter();
+
+ /**
+ * @var Model\MapModel $map
+ */
+ $map = Model\BasicModel::getNew('MapModel');
+ $map->getById($mapId);
+
+ if($map->hasAccess($activeCharacter)){
+ $cacheKey = $this->getHistoryDataCacheKey($mapId);
+ if($return->query['offset'] === 0){
+ // check cache
+ $return->data = $f3->get($cacheKey);
+ }
+
+ if(empty($return->data)){
+ $return->data = $map->getLogData($return->query['offset'], $return->query['limit']);
+ if(
+ $return->query['offset'] === 0 &&
+ !empty($return->data))
+ {
+ $f3->set($cacheKey, $return->data, (int)Config::getPathfinderData('history.cache'));
+ }
+ }
+ }
+ }
+
+ echo json_encode($return);
+ }
+
}
diff --git a/app/main/controller/api/route.php b/app/main/controller/api/route.php
index a6b69e59..807b0233 100644
--- a/app/main/controller/api/route.php
+++ b/app/main/controller/api/route.php
@@ -8,6 +8,7 @@
namespace Controller\Api;
use Controller;
+use lib\Config;
use Model;
@@ -530,7 +531,7 @@ class Route extends Controller\AccessController {
$map = Model\BasicModel::getNew('MapModel');
// limit max search routes to max limit
- array_splice($routesData, $f3->get('PATHFINDER.ROUTE.LIMIT'));
+ array_splice($routesData, Config::getPathfinderData('route.limit'));
foreach($routesData as $key => $routeData){
// mapIds are optional. If mapIds is empty or not set
@@ -609,7 +610,7 @@ class Route extends Controller\AccessController {
$returnRoutData = $cachedData;
}else{
// max search depth for search
- $searchDepth = $f3->get('PATHFINDER.ROUTE.SEARCH_DEPTH');
+ $searchDepth = Config::getPathfinderData('route.search_depth');
// set jump data for following route search
// --> don´t filter some systems (e.g. systemFrom, systemTo) even if they are are WH,LS,0.0
diff --git a/app/main/controller/api/signature.php b/app/main/controller/api/signature.php
index 6a3b0e55..39b8250d 100644
--- a/app/main/controller/api/signature.php
+++ b/app/main/controller/api/signature.php
@@ -13,16 +13,6 @@ use Model;
class Signature extends Controller\AccessController {
- /**
- * event handler
- * @param \Base $f3
- * @param array $params
- */
- function beforeroute(\Base $f3, $params) {
- // set header for all routes
- header('Content-type: application/json');
- parent::beforeroute($f3, $params);
- }
/**
* get signature data for systems
@@ -120,8 +110,6 @@ class Signature extends Controller\AccessController {
if($signature->dry()){
// new signature
$signature->systemId = $system;
- $signature->updatedCharacterId = $activeCharacter;
- $signature->createdCharacterId = $activeCharacter;
$signature->setData($data);
}else{
// update signature
@@ -181,13 +169,11 @@ class Signature extends Controller\AccessController {
}
if( $signature->hasChanged($newData) ){
- // Character should only be changed if something else has changed
- $signature->updatedCharacterId = $activeCharacter;
$signature->setData($newData);
}
}
- $signature->save();
+ $signature->save($activeCharacter);
$updatedSignatureIds[] = $signature->id;
// get a fresh signature object with the new data. This is a bad work around!
diff --git a/app/main/controller/api/statistic.php b/app/main/controller/api/statistic.php
index ab89776e..e42dc68c 100644
--- a/app/main/controller/api/statistic.php
+++ b/app/main/controller/api/statistic.php
@@ -9,6 +9,7 @@
namespace controller\api;
use Controller;
+use lib\Config;
use Model\CharacterModel;
class Statistic extends Controller\AccessController {
@@ -131,19 +132,19 @@ class Statistic extends Controller\AccessController {
$objectId = 0;
// add map-"typeId" (private/corp/ally) condition -------------------------------------------------------------
- // check if "ACTIVITY_LOGGING" is active for a given "typeId"
+ // check if "LOG_ACTIVITY_ENABLED" is active for a given "typeId"
$sqlMapType = "";
switch($typeId){
case 2:
- if( $this->getF3()->get('PATHFINDER.MAP.PRIVATE.ACTIVITY_LOGGING') ){
+ if( Config::getMapsDefaultConfig('private')['log_activity_enabled'] ){
$sqlMapType .= " AND `character`.`id` = :objectId ";
$objectId = $character->_id;
}
break;
case 3:
if(
- $this->getF3()->get('PATHFINDER.MAP.CORPORATION.ACTIVITY_LOGGING') &&
+ Config::getMapsDefaultConfig('corporation')['log_activity_enabled'] &&
$character->hasCorporation()
){
$sqlMapType .= " AND `character`.`corporationId` = :objectId ";
@@ -152,7 +153,7 @@ class Statistic extends Controller\AccessController {
break;
case 4:
if(
- $this->getF3()->get('PATHFINDER.MAP.ALLIANCE.ACTIVITY_LOGGING') &&
+ Config::getMapsDefaultConfig('alliance')['log_activity_enabled'] &&
$character->hasAlliance()
){
$sqlMapType .= " AND `character`.`allianceId` = :objectId ";
@@ -181,6 +182,9 @@ class Statistic extends Controller\AccessController {
`log`.`characterId`,
`character`.`name`,
`character`.`lastLogin`,
+ SUM(`log`.`mapCreate`) `mapCreate`,
+ SUM(`log`.`mapUpdate`) `mapUpdate`,
+ SUM(`log`.`mapDelete`) `mapDelete`,
SUM(`log`.`systemCreate`) `systemCreate`,
SUM(`log`.`systemUpdate`) `systemUpdate`,
SUM(`log`.`systemDelete`) `systemDelete`,
diff --git a/app/main/controller/api/system.php b/app/main/controller/api/system.php
index 6fac7a27..be00945b 100644
--- a/app/main/controller/api/system.php
+++ b/app/main/controller/api/system.php
@@ -8,8 +8,8 @@
namespace Controller\Api;
use Controller;
-use Controller\Ccp\Sso;
use Data\Mapper as Mapper;
+use lib\Config;
use Model;
class System extends Controller\AccessController {
@@ -65,16 +65,6 @@ class System extends Controller\AccessController {
private $limitQuery = "";
- /**
- * @param \Base $f3
- * @param array $params
- */
- function beforeroute(\Base $f3, $params) {
- parent::beforeroute($f3, $params);
-
- // set header for all routes
- header('Content-type: application/json');
- }
/**
* build query
@@ -187,9 +177,12 @@ class System extends Controller\AccessController {
* @param \Base $f3
*/
public function save(\Base $f3){
- $newSystemData = [];
$postData = (array)$f3->get('POST');
+ $return = (object) [];
+ $return->error = [];
+ $return->systemData = (object) [];
+
if(
isset($postData['systemData']) &&
isset($postData['mapData'])
@@ -229,23 +222,21 @@ class System extends Controller\AccessController {
*/
$map = Model\BasicModel::getNew('MapModel');
$map->getById($mapData['id']);
- if(
- !$map->dry() &&
- $map->hasAccess($activeCharacter)
- ){
+ if( $map->hasAccess($activeCharacter) ){
// make sure system is not already on map
// --> (e.g. multiple simultaneously save() calls for the same system)
$systemModel = $map->getSystemByCCPId($systemData['systemId']);
if( is_null($systemModel) ){
// system not found on map -> get static system data (CCP DB)
$systemModel = $map->getNewSystem($systemData['systemId']);
- $systemModel->createdCharacterId = $activeCharacter;
- $systemModel->statusId = isset($systemData['statusId']) ? $systemData['statusId'] : 1;
+ $defaultStatusId = 1;
}else{
// system already exists (e.g. was inactive)
- $systemModel->statusId = isset($systemData['statusId']) ? $systemData['statusId'] : $systemModel->statusId;
+ $defaultStatusId = $systemModel->statusId;
}
+ $systemModel->statusId = isset($systemData['statusId']) ? $systemData['statusId'] : $defaultStatusId;
+
// map is not changeable for a system! (security)
$systemData['mapId'] = $map;
}
@@ -255,28 +246,32 @@ class System extends Controller\AccessController {
// "statusId" was set above
unset($systemData['statusId']);
unset($systemData['mapId']);
- unset($systemData['createdCharacterId']);
- unset($systemData['updatedCharacterId']);
// set/update system
$systemModel->setData($systemData);
// activate system (e.g. was inactive))
$systemModel->setActive(true);
- $systemModel->updatedCharacterId = $activeCharacter;
- $systemModel->save();
- // get data from "fresh" model (e.g. some relational data has changed: "statusId")
- $newSystemModel = Model\BasicModel::getNew('SystemModel');
- $newSystemModel->getById( $systemModel->id, 0);
- $newSystemModel->clearCacheData();
- $newSystemData = $newSystemModel->getData();
- // broadcast map changes
- $this->broadcastMapData($newSystemModel->mapId);
+ if($systemModel->save($activeCharacter)){
+ // get data from "fresh" model (e.g. some relational data has changed: "statusId")
+ /**
+ * @var $newSystemModel Model\SystemModel
+ */
+ $newSystemModel = Model\BasicModel::getNew('SystemModel');
+ $newSystemModel->getById( $systemModel->id, 0);
+ $newSystemModel->clearCacheData();
+ $return->systemData = $newSystemModel->getData();
+
+ // broadcast map changes
+ $this->broadcastMapData($newSystemModel->mapId);
+ }else{
+ $return->error = $systemModel->getErrors();
+ }
}
}
- echo json_encode($newSystemData);
+ echo json_encode($return);
}
/**
@@ -358,7 +353,7 @@ class System extends Controller\AccessController {
$return->systemData[] = $systemModel->getData();
}
- $f3->set($cacheKey, $return->systemData, $f3->get('PATHFINDER.CACHE.CONSTELLATION_SYSTEMS') );
+ $f3->set($cacheKey, $return->systemData, Config::getPathfinderData('cache.constellation_systems'));
}
}
@@ -407,6 +402,37 @@ class System extends Controller\AccessController {
echo json_encode($return);
}
+ /**
+ * send Rally Point poke
+ * @param \Base $f3
+ */
+ public function pokeRally(\Base $f3){
+ $rallyData = (array)$f3->get('POST');
+ $systemId = (int)$rallyData['systemId'];
+ $return = (object) [];
+
+ if($systemId){
+ $activeCharacter = $this->getCharacter();
+
+ /**
+ * @var Model\SystemModel $system
+ */
+ $system = Model\BasicModel::getNew('SystemModel');
+ $system->getById($systemId);
+
+ if($system->hasAccess($activeCharacter)){
+ $rallyData['pokeDesktop'] = $rallyData['pokeDesktop'] === '1';
+ $rallyData['pokeMail'] = $rallyData['pokeMail'] === '1';
+ $rallyData['pokeSlack'] = $rallyData['pokeSlack'] === '1';
+ $rallyData['message'] = trim($rallyData['message']);
+
+ $system->sendRallyPoke($rallyData, $activeCharacter);
+ }
+ }
+
+ echo json_encode($return);
+ }
+
/**
* delete systems and all its connections from map
* -> set "active" flag
@@ -428,7 +454,7 @@ class System extends Controller\AccessController {
$map = Model\BasicModel::getNew('MapModel');
$map->getById($mapId);
- if( $map->hasAccess($activeCharacter) ){
+ if($map->hasAccess($activeCharacter)){
foreach($systemIds as $systemId){
if( $system = $map->getSystemById($systemId) ){
// check whether system should be deleted OR set "inactive"
@@ -437,7 +463,7 @@ class System extends Controller\AccessController {
}else{
// keep data -> set "inactive"
$system->setActive(false);
- $system->save();
+ $system->save($activeCharacter);
}
$system->reset();
diff --git a/app/main/controller/api/user.php b/app/main/controller/api/user.php
index d268af9a..7b33b808 100644
--- a/app/main/controller/api/user.php
+++ b/app/main/controller/api/user.php
@@ -8,7 +8,6 @@
namespace Controller\Api;
use Controller;
-use controller\MailController;
use Model;
use Exception;
@@ -26,8 +25,8 @@ class User extends Controller\Controller{
// character specific session keys
const SESSION_KEY_CHARACTERS = 'SESSION.CHARACTERS';
- // temp login character ID (during HTTP redirects on login)
- const SESSION_KEY_TEMP_CHARACTER_ID = 'SESSION.TEMP_CHARACTER_ID';
+ // temp login character data (during HTTP redirects on login)
+ const SESSION_KEY_TEMP_CHARACTER_DATA = 'SESSION.TEMP_CHARACTER_DATA';
// log text
const LOG_LOGGED_IN = 'userId: [%10s], userName: [%30s], charId: [%20s], charName: %s';
@@ -43,9 +42,10 @@ class User extends Controller\Controller{
/**
* login a valid character
* @param Model\CharacterModel $characterModel
+ * @param string $browserTabId
* @return bool
*/
- protected function loginByCharacter(Model\CharacterModel &$characterModel){
+ protected function loginByCharacter(Model\CharacterModel &$characterModel, string $browserTabId){
$login = false;
if($user = $characterModel->getUser()){
@@ -68,7 +68,7 @@ class User extends Controller\Controller{
){
// user has changed OR new user ---------------------------------------------------
//-> set user/character data to session
- $this->f3->set(self::SESSION_KEY_USER, [
+ $this->getF3()->set(self::SESSION_KEY_USER, [
'ID' => $user->_id,
'NAME' => $user->name
]);
@@ -77,7 +77,7 @@ class User extends Controller\Controller{
$sessionCharacters = $characterModel::mergeSessionCharacterData($sessionCharacters);
}
- $this->f3->set(self::SESSION_KEY_CHARACTERS, $sessionCharacters);
+ $this->getF3()->set(self::SESSION_KEY_CHARACTERS, $sessionCharacters);
// save user login information --------------------------------------------------------
$characterModel->roleId = $characterModel->requestRoleId();
@@ -94,6 +94,10 @@ class User extends Controller\Controller{
)
);
+ // set temp character data ------------------------------------------------------------
+ // -> pass character data over for next http request (reroute())
+ $this->setTempCharacterData($characterModel->_id, $browserTabId);
+
$login = true;
}
@@ -118,6 +122,14 @@ class User extends Controller\Controller{
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';
@@ -179,9 +191,7 @@ class User extends Controller\Controller{
*/
public function deleteLog(\Base $f3){
if($activeCharacter = $this->getCharacter()){
- if($characterLog = $activeCharacter->getLog()){
- $characterLog->erase();
- }
+ $activeCharacter->logout(false, true, false);
}
}
@@ -190,8 +200,7 @@ class User extends Controller\Controller{
* @param \Base $f3
*/
public function logout(\Base $f3){
- $this->deleteLog($f3);
- parent::logout($f3);
+ $this->logoutCharacter(false, true, true, true);
$return = (object) [];
$return->reroute = rtrim(self::getEnvironmentData('URL'), '/') . $f3->alias('login');
@@ -371,23 +380,15 @@ class User extends Controller\Controller{
$user = $activeCharacter->getUser();
if($user){
- // try to send delete account mail
- $msg = 'Hello ' . $user->name . ',
';
- $msg .= 'your account data has been successfully deleted.';
-
- $mailController = new MailController();
- $mailController->sendDeleteAccount($user->email, $msg);
-
// save log
self::getLogger('DELETE_ACCOUNT')->write(
sprintf(self::LOG_DELETE_ACCOUNT, $user->id, $user->name)
);
- // remove user
+ $this->logoutCharacter(true, true, true, true);
$user->erase();
- $this->logout($f3);
- die();
+ $return->reroute = rtrim(self::getEnvironmentData('URL'), '/') . $f3->alias('login');
}
}else{
// captcha not valid -> return error
diff --git a/app/main/controller/appcontroller.php b/app/main/controller/appcontroller.php
index 85777142..d730a82e 100644
--- a/app/main/controller/appcontroller.php
+++ b/app/main/controller/appcontroller.php
@@ -13,6 +13,33 @@ use lib\Config;
class AppController extends Controller {
+ public function beforeroute(\Base $f3, $params) : bool{
+ // page title
+ $f3->set('tplPageTitle', Config::getPathfinderData('name'));
+
+ // main page content
+ $f3->set('tplPageContent', Config::getPathfinderData('view.login'));
+
+ // body element class
+ $f3->set('tplBodyClass', 'pf-landing');
+
+ // JS main file
+ $f3->set('tplJsView', 'login');
+
+ if($return = parent::beforeroute($f3, $params)){
+ // href for SSO Auth
+ $f3->set('tplAuthType', $f3->alias( 'sso', ['action' => 'requestAuthorization'] ));
+
+ // characters from cookies
+ $f3->set('cookieCharacters', $this->getCookieByName(self::COOKIE_PREFIX_CHARACTER, true));
+ $f3->set('getCharacterGrid', function($characters){
+ return ( ((12 / count($characters)) <= 3) ? 3 : (12 / count($characters)) );
+ });
+ }
+
+ return $return;
+ }
+
/**
* event handler after routing
* @param \Base $f3
@@ -31,26 +58,7 @@ class AppController extends Controller {
* @param \Base $f3
*/
public function init(\Base $f3) {
- // page title
- $f3->set('tplPageTitle', Config::getPathfinderData('name'));
- // main page content
- $f3->set('tplPageContent', Config::getPathfinderData('view.login'));
-
- // body element class
- $f3->set('tplBodyClass', 'pf-landing');
-
- // JS main file
- $f3->set('tplJsView', 'login');
-
- // href for SSO Auth
- $f3->set('tplAuthType', $f3->alias( 'sso', ['action' => 'requestAuthorization'] ));
-
- // characters from cookies
- $f3->set('cookieCharacters', $this->getCookieByName(self::COOKIE_PREFIX_CHARACTER, true));
- $f3->set('getCharacterGrid', function($characters){
- return ( ((12 / count($characters)) <= 3) ? 3 : (12 / count($characters)) );
- });
}
}
\ No newline at end of file
diff --git a/app/main/controller/ccp/sso.php b/app/main/controller/ccp/sso.php
index 6e7050d8..5e509e6c 100644
--- a/app/main/controller/ccp/sso.php
+++ b/app/main/controller/ccp/sso.php
@@ -34,6 +34,7 @@ class Sso extends Api\User{
const SESSION_KEY_SSO_ERROR = 'SESSION.SSO.ERROR';
const SESSION_KEY_SSO_STATE = 'SESSION.SSO.STATE';
const SESSION_KEY_SSO_FROM = 'SESSION.SSO.FROM';
+ const SESSION_KEY_SSO_TAB_ID = 'SESSION.SSO.TABID';
// error messages
const ERROR_CCP_SSO_URL = 'Invalid "ENVIRONMENT.[ENVIRONMENT].CCP_SSO_URL" url. %s';
@@ -53,6 +54,8 @@ class Sso extends Api\User{
* @param \Base $f3
*/
public function requestAdminAuthorization($f3){
+ // store browser tabId to be "targeted" after login
+ $f3->set(self::SESSION_KEY_SSO_TAB_ID, '');
$f3->set(self::SESSION_KEY_SSO_FROM, 'admin');
$scopes = self::getScopesByAuthType('admin');
@@ -66,6 +69,10 @@ class Sso extends Api\User{
*/
public function requestAuthorization($f3){
$params = $f3->get('GET');
+ $browserTabId = trim((string)$params['tabId']);
+
+ // store browser tabId to be "targeted" after login
+ $f3->set(self::SESSION_KEY_SSO_TAB_ID, $browserTabId);
if(
isset($params['characterId']) &&
@@ -73,7 +80,7 @@ class Sso extends Api\User{
){
// authentication restricted to a characterId -----------------------------------------------
// restrict login to this characterId e.g. for character switch on map page
- $characterId = (int)trim($params['characterId']);
+ $characterId = (int)trim((string)$params['characterId']);
/**
* @var Model\CharacterModel $character
@@ -101,15 +108,12 @@ class Sso extends Api\User{
$character->hasUserCharacter() &&
($character->isAuthorized() === 'OK')
){
- $loginCheck = $this->loginByCharacter($character);
+ $loginCheck = $this->loginByCharacter($character, $browserTabId);
if($loginCheck){
// set "login" cookie
$this->setLoginCookie($character);
- // -> pass current character data to target page
- $f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_ID, $character->_id);
-
// route to "map"
$f3->reroute(['map']);
}
@@ -174,6 +178,8 @@ class Sso extends Api\User{
$rootAlias = $f3->get(self::SESSION_KEY_SSO_FROM);
}
+ $browserTabId = (string)$f3->get(self::SESSION_KEY_SSO_TAB_ID) ;
+
if($f3->exists(self::SESSION_KEY_SSO_STATE)){
// check response and validate 'state'
if(
@@ -186,6 +192,7 @@ class Sso extends Api\User{
// clear 'state' for new next login request
$f3->clear(self::SESSION_KEY_SSO_STATE);
$f3->clear(self::SESSION_KEY_SSO_FROM);
+ $f3->clear(self::SESSION_KEY_SSO_TAB_ID);
$accessData = $this->getSsoAccessData($getParams['code']);
@@ -252,14 +259,14 @@ class Sso extends Api\User{
$characterModel = $userCharactersModel->getCharacter();
// login by character
- $loginCheck = $this->loginByCharacter($characterModel);
+ $loginCheck = $this->loginByCharacter($characterModel, $browserTabId);
if($loginCheck){
// set "login" cookie
$this->setLoginCookie($characterModel);
// -> pass current character data to target page
- $f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_ID, $characterModel->_id);
+ $f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA, $characterModel->_id);
// route to "map"
if($rootAlias == 'admin'){
@@ -302,6 +309,7 @@ class Sso extends Api\User{
public function login(\Base $f3){
$data = (array)$f3->get('GET');
$cookieName = empty($data['cookie']) ? '' : $data['cookie'];
+ $browserTabId = empty($data['tabId']) ? '' : $data['tabId'];
$character = null;
if( !empty($cookieName) ){
@@ -316,12 +324,8 @@ class Sso extends Api\User{
if( is_object($character)){
// login by character
- $loginCheck = $this->loginByCharacter($character);
+ $loginCheck = $this->loginByCharacter($character, $browserTabId);
if($loginCheck){
- // set character id
- // -> pass current character data to target page
- $f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_ID, $character->_id);
-
// route to "map"
$f3->reroute(['map']);
}
diff --git a/app/main/controller/controller.php b/app/main/controller/controller.php
index 44d636aa..cf305d58 100644
--- a/app/main/controller/controller.php
+++ b/app/main/controller/controller.php
@@ -9,6 +9,7 @@
namespace Controller;
use Controller\Api as Api;
use lib\Config;
+use Lib\Monolog;
use lib\Socket;
use Lib\Util;
use Model;
@@ -21,8 +22,8 @@ class Controller {
const COOKIE_PREFIX_CHARACTER = 'char';
// log text
- const LOG_UNAUTHORIZED = 'User-Agent: [%s]';
- const ERROR_SESSION_SUSPECT = 'Suspect id: [%45s], ip: [%45s], new ip: [%45s], User-Agent: [%s]';
+ const ERROR_SESSION_SUSPECT = 'id: [%45s], ip: [%45s], User-Agent: [%s]';
+ const ERROR_TEMP_CHARACTER_ID = 'Invalid temp characterId: %s';
/**
* @var \Base
@@ -48,46 +49,38 @@ class Controller {
return $this->template;
}
- /**
- * set $f3 base object
- * @param \Base $f3
- */
- protected function setF3(\Base $f3){
- $this->f3 = $f3;
- }
-
/**
* get $f3 base object
* @return \Base
*/
protected function getF3(){
- if( !($this->f3 instanceof \Base) ){
- $this->setF3( \Base::instance() );
- }
- return $this->f3;
+ return \Base::instance();
}
/**
* event handler for all "views"
* some global template variables are set in here
* @param \Base $f3
- * @param array $params
+ * @param $params
+ * @return bool
*/
- function beforeroute(\Base $f3, $params) {
- $this->setF3($f3);
-
+ function beforeroute(\Base $f3, $params): bool {
// initiate DB connection
- DB\Database::instance('PF');
+ DB\Database::instance()->getDB('PF');
// init user session
- $this->initSession();
+ $this->initSession($f3);
- if( !$f3->get('AJAX') ){
+ if($f3->get('AJAX')){
+ header('Content-type: application/json');
+ }else{
// js path (build/minified or raw uncompressed files)
$f3->set('tplPathJs', 'public/js/' . Config::getPathfinderData('version') );
$this->setTemplate( Config::getPathfinderData('view.index') );
}
+
+ return true;
}
/**
@@ -96,9 +89,6 @@ class Controller {
* @param \Base $f3
*/
public function afterroute(\Base $f3){
- // store all user activities that are buffered for logging in this request
- self::storeActivities();
-
if($this->getTemplate()){
// Ajax calls don´t need a page render..
// this happens on client side
@@ -118,32 +108,35 @@ class Controller {
/**
* init new Session handler
*/
- protected function initSession(){
+ protected function initSession(\Base $f3){
+ $sessionCacheKey = $f3->get('SESSION_CACHE');
+ $session = null;
- // init DB based Session (not file based)
- if( $this->getDB('PF') instanceof DB\SQL){
- // init session with custom "onsuspect()" handler
- new DB\SQL\Session($this->getDB('PF'), 'sessions', true, function($session, $sid){
- $f3 = $this->getF3();
- if( ($ip = $session->ip() )!= $f3->get('IP') ){
- // IP address changed -> not critical
- self::getLogger('SESSION_SUSPECT')->write( sprintf(
- self::ERROR_SESSION_SUSPECT,
- $sid,
- $session->ip(),
- $f3->get('IP'),
- $f3->get('AGENT')
- ));
- // no more error handling here
- return true;
- }elseif($session->agent() != $f3->get('AGENT') ){
- // The default behaviour destroys the suspicious session.
- return false;
- }
+ /**
+ * 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;
+ };
- return true;
- });
+ if(
+ $sessionCacheKey === 'mysql' &&
+ $this->getDB('PF') instanceof DB\SQL
+ ){
+ $session = new DB\SQL\Session($this->getDB('PF'), 'sessions', true, $onSuspect);
}
+
}
/**
@@ -190,12 +183,11 @@ class Controller {
* @param Model\CharacterModel $character
*/
protected function setLoginCookie(Model\CharacterModel $character){
-
if( $this->getCookieState() ){
- $expireSeconds = (int) $this->getF3()->get('PATHFINDER.LOGIN.COOKIE_EXPIRE');
+ $expireSeconds = (int)Config::getPathfinderData('login.cookie_expire');
$expireSeconds *= 24 * 60 * 60;
- $timezone = new \DateTimeZone( $this->getF3()->get('TZ') );
+ $timezone = $this->getF3()->get('getTimeZone')();
$expireTime = new \DateTime('now', $timezone);
// add cookie expire time
@@ -258,7 +250,7 @@ class Controller {
*/
$characterAuth = Model\BasicModel::getNew('CharacterAuthenticationModel');
- $timezone = new \DateTimeZone( $this->getF3()->get('TZ') );
+ $timezone = $this->getF3()->get('getTimeZone')();
$currentTime = new \DateTime('now', $timezone);
foreach($cookieData as $name => $value){
@@ -268,7 +260,7 @@ class Controller {
$data = explode(':', $value);
if(count($data) === 2){
// cookie data is well formatted
- $characterAuth->getByForeignKey('selector', $data[0], ['limit' => 1], 0);
+ $characterAuth->getByForeignKey('selector', $data[0], ['limit' => 1]);
// validate "scope hash"
// -> either "normal" scopes OR "admin" scopes
@@ -354,28 +346,37 @@ class Controller {
$data = [];
if($user = $this->getUser()){
- $requestedCharacterId = 0;
+ $header = self::getRequestHeaders();
+ $requestedCharacterId = (int)$header['Pf-Character'];
+ $browserTabId = (string)$header['Pf-Tab-Id'];
+ $tempCharacterData = (array)$this->getF3()->get(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA);
- // get all characterData from currently active characters
if($this->getF3()->get('AJAX')){
- // Ajax request -> get characterId from Header (if already available!)
- $header = $this->getRequestHeaders();
- $requestedCharacterId = (int)$header['Pf-Character'];
+
+ // _blank browser tab don´t have a $browserTabId jet..
+ // first Ajax call from that new tab with empty $requestedCharacterId -> bind to that new tab
+ if(
+ !empty($browserTabId) &&
+ $requestedCharacterId <= 0 &&
+ (int)$tempCharacterData['ID'] > 0 &&
+ empty($tempCharacterData['TAB_ID'])
+ ){
+ $tempCharacterData['TAB_ID'] = $browserTabId;
+ // update tempCharacterData (SESSION)
+ $this->setTempCharacterData($tempCharacterData['ID'], $tempCharacterData['TAB_ID']);
+ }
if(
- $requestedCharacterId > 0 &&
- (int)$this->getF3()->get(Api\User::SESSION_KEY_TEMP_CHARACTER_ID) === $requestedCharacterId
+ !empty($browserTabId) &&
+ !empty($tempCharacterData['TAB_ID']) &&
+ (int)$tempCharacterData['ID'] > 0 &&
+ $browserTabId === $tempCharacterData['TAB_ID']
){
- // requested characterId is "now" available on the client (Javascript)
- // -> clear temp characterId for next character login/switch
- $this->getF3()->clear(Api\User::SESSION_KEY_TEMP_CHARACTER_ID);
+ $requestedCharacterId = (int)$tempCharacterData['ID'];
}
- }
- if($requestedCharacterId <= 0){
- // Ajax BUT characterID not yet set as HTTP header
- // OR non Ajax -> get characterId from temp session (e.g. from HTTP redirect)
- $requestedCharacterId = (int)$this->getF3()->get(Api\User::SESSION_KEY_TEMP_CHARACTER_ID);
+ }elseif((int)$tempCharacterData['ID'] > 0){
+ $requestedCharacterId = (int)$tempCharacterData['ID'];
}
$data = $user->getSessionCharacterData($requestedCharacterId);
@@ -420,21 +421,18 @@ class Controller {
public function getUser($ttl = 0){
$user = null;
- if( $this->getF3()->exists(Api\User::SESSION_KEY_USER_ID) ){
- $userId = (int)$this->getF3()->get(Api\User::SESSION_KEY_USER_ID);
- if($userId){
- /**
- * @var $userModel Model\UserModel
- */
- $userModel = Model\BasicModel::getNew('UserModel');
- $userModel->getById($userId, $ttl);
+ if($this->getF3()->exists(Api\User::SESSION_KEY_USER_ID, $userId)){
+ /**
+ * @var $userModel Model\UserModel
+ */
+ $userModel = Model\BasicModel::getNew('UserModel');
+ $userModel->getById($userId, $ttl);
- if(
- !$userModel->dry() &&
- $userModel->hasUserCharacters()
- ){
- $user = &$userModel;
- }
+ if(
+ !$userModel->dry() &&
+ $userModel->hasUserCharacters()
+ ){
+ $user = &$userModel;
}
}
@@ -442,26 +440,57 @@ class Controller {
}
/**
- * log out current character
- * @param \Base $f3
+ * set temp login character data (required during HTTP redirects on login)
+ * @param int $characterId
+ * @param string $browserTabId
+ * @throws \Exception
*/
- public function logout(\Base $f3){
- $params = (array)$f3->get('POST');
+ protected function setTempCharacterData(int $characterId, string $browserTabId){
+ if($characterId > 0){
+ $tempCharacterData = [
+ 'ID' => $characterId,
+ 'TAB_ID' => trim($browserTabId)
+ ];
+ $this->getF3()->set(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA, $tempCharacterData);
+ }else{
+ throw new \Exception( sprintf(self::ERROR_TEMP_CHARACTER_ID, $characterId) );
+ }
+ }
- if( $activeCharacter = $this->getCharacter() ){
+ /**
+ * log out current character or all active characters (multiple browser tabs)
+ * @param bool $all
+ * @param bool $deleteSession
+ * @param bool $deleteLog
+ * @param bool $deleteCookie
+ */
+ protected function logoutCharacter(bool $all = false, bool $deleteSession = true, bool $deleteLog = true, bool $deleteCookie = false){
+ $sessionCharacterData = (array)$this->getF3()->get(Api\User::SESSION_KEY_CHARACTERS);
- if($params['clearCookies'] === '1'){
- // delete server side cookie validation data
- // for the active character
- $activeCharacter->logout();
+ if($sessionCharacterData){
+ $activeCharacterId = ($activeCharacter = $this->getCharacter()) ? $activeCharacter->_id : 0;
+ /**
+ * @var Model\CharacterModel $character
+ */
+ $character = Model\BasicModel::getNew('CharacterModel');
+ $characterIds = [];
+ foreach($sessionCharacterData as $characterData){
+ if($characterData['ID'] === $activeCharacterId){
+ $characterIds[] = $activeCharacter->_id;
+ $activeCharacter->logout($deleteSession, $deleteLog, $deleteCookie);
+ }elseif($all){
+ $character->getById($characterData['ID']);
+ $characterIds[] = $character->_id;
+ $character->logout($deleteSession, $deleteLog, $deleteCookie);
+ }
+ $character->reset();
}
- // broadcast logout information to webSocket server
- (new Socket( Config::getSocketUri() ))->sendData('characterLogout', $activeCharacter->_id);
+ if($characterIds){
+ // broadcast logout information to webSocket server
+ (new Socket( Config::getSocketUri() ))->sendData('characterLogout', $characterIds);
+ }
}
-
- // destroy session login data -------------------------------
- $f3->clear('SESSION');
}
/**
@@ -482,7 +511,7 @@ class Controller {
if( !empty($response) ){
// calculate time diff since last server restart
- $timezone = new \DateTimeZone( $f3->get('TZ') );
+ $timezone = $f3->get('getTimeZone')();
$dateNow = new \DateTime('now', $timezone);
$dateServerStart = new \DateTime($response['startTime']);
$interval = $dateNow->diff($dateServerStart);
@@ -504,14 +533,24 @@ class Controller {
}
/**
- * get error object is a user is not found/logged of
+ * @param int $code
+ * @param string $message
+ * @param string $status
+ * @param null $trace
* @return \stdClass
*/
- protected function getLogoutError(){
- $userError = (object) [];
- $userError->type = 'error';
- $userError->message = 'User not found';
- return $userError;
+ protected function getErrorObject(int $code, string $message = '', string $status = '', $trace = null): \stdClass{
+ $object = (object) [];
+ $object->type = 'error';
+ $object->code = $code;
+ $object->status = empty($status) ? @constant('Base::HTTP_' . $code) : $status;
+ if(!empty($message)){
+ $object->message = $message;
+ }
+ if(!empty($trace)){
+ $object->trace = $trace;
+ }
+ return $object;
}
/**
@@ -558,59 +597,53 @@ class Controller {
* -> on AJAX request -> return JSON with error information
* -> on HTTP request -> render error page
* @param \Base $f3
+ * @return bool
*/
public function showError(\Base $f3){
- // set HTTP status
- $errorCode = $f3->get('ERROR.code');
- if(!empty($errorCode)){
- $f3->status($errorCode);
- }
+ if(!headers_sent()){
+ // collect error info -------------------------------------------------------------------------------------
+ $error = $this->getErrorObject(
+ $f3->get('ERROR.code'),
+ $f3->get('ERROR.status'),
+ $f3->get('ERROR.text'),
+ $f3->get('DEBUG') === 3 ? $f3->get('ERROR.trace') : null
+ );
- // collect error info ---------------------------------------
- $return = (object) [];
- $error = (object) [];
- $error->type = 'error';
- $error->code = $errorCode;
- $error->status = $f3->get('ERROR.status');
- $error->message = $f3->get('ERROR.text');
+ // check if error is a PDO Exception ----------------------------------------------------------------------
+ if(strpos(strtolower( $f3->get('ERROR.text') ), 'duplicate') !== false){
+ preg_match_all('/\'([^\']+)\'/', $f3->get('ERROR.text'), $matches, PREG_SET_ORDER);
- // append stack trace for greater debug level
- if( $f3->get('DEBUG') === 3){
- $error->trace = $f3->get('ERROR.trace');
- }
-
- // check if error is a PDO Exception
- if(strpos(strtolower( $f3->get('ERROR.text') ), 'duplicate') !== false){
- preg_match_all('/\'([^\']+)\'/', $f3->get('ERROR.text'), $matches, PREG_SET_ORDER);
-
- if(count($matches) === 2){
- $error->field = $matches[1][1];
- $error->message = 'Value "' . $matches[0][1] . '" already exists';
- }
- }
- $return->error[] = $error;
-
- // return error information ---------------------------------
- if($f3->get('AJAX')){
- header('Content-type: application/json');
- echo json_encode($return);
- die();
- }else{
- $f3->set('tplPageTitle', 'ERROR - ' . $error->code . ' | Pathfinder');
- // set error data for template rendering
- $error->redirectUrl = $this->getRouteUrl();
- $f3->set('errorData', $error);
-
- if( preg_match('/^4[0-9]{2}$/', $error->code) ){
- // 4xx error -> render error page
- $f3->set('tplPageContent', Config::getPathfinderData('STATUS.4XX') );
- }elseif( preg_match('/^5[0-9]{2}$/', $error->code) ){
- $f3->set('tplPageContent', Config::getPathfinderData('STATUS.5XX'));
+ if(count($matches) === 2){
+ $error->field = $matches[1][1];
+ $error->message = 'Value "' . $matches[0][1] . '" already exists';
+ }
}
- echo \Template::instance()->render( Config::getPathfinderData('view.index') );
- die();
+ // set response status ------------------------------------------------------------------------------------
+ if(!empty($error->code)){
+ $f3->status($error->code);
+ }
+
+ if($f3->get('AJAX')){
+ $return = (object) [];
+ $return->error[] = $error;
+ echo json_encode($return);
+ }else{
+ $f3->set('tplPageTitle', 'ERROR - ' . $error->code);
+ // set error data for template rendering
+ $error->redirectUrl = $this->getRouteUrl();
+ $f3->set('errorData', $error);
+
+ if( preg_match('/^4[0-9]{2}$/', $error->code) ){
+ // 4xx error -> render error page
+ $f3->set('tplPageContent', Config::getPathfinderData('STATUS.4XX') );
+ }elseif( preg_match('/^5[0-9]{2}$/', $error->code) ){
+ $f3->set('tplPageContent', Config::getPathfinderData('STATUS.5XX'));
+ }
+ }
}
+
+ return true;
}
/**
@@ -624,44 +657,36 @@ class Controller {
// track some 4xx Client side errors
// 5xx errors are handled in "ONERROR" callback
$status = http_response_code();
- $halt = false;
+ if(!headers_sent() && $status >= 300){
+ if($f3->get('AJAX')){
+ $params = (array)$f3->get('POST');
+ $return = (object) [];
+ if((bool)$params['reroute']){
+ $return->reroute = rtrim(self::getEnvironmentData('URL'), '/') . $f3->alias('login');
+ }else{
+ // no reroute -> errors can be shown
+ $return->error[] = $this->getErrorObject($status, Config::getMessageFromHTTPStatus($status));
+ }
- switch( $status ){
- case 403: // Unauthorized
- self::getLogger('UNAUTHORIZED')->write(sprintf(
- self::LOG_UNAUTHORIZED,
- $f3->get('AGENT')
- ));
- $halt = true;
- break;
- }
-
- // Ajax
- if(
- $halt &&
- $f3->get('AJAX')
- ){
- $params = (array)$f3->get('POST');
- $response = (object) [];
- $response->type = 'error';
- $response->code = $status;
- $response->message = 'Access denied: User not found';
-
- $return = (object) [];
- if( (bool)$params['reroute']){
- $return->reroute = rtrim(self::getEnvironmentData('URL'), '/') . $f3->alias('login');
- }else{
- // no reroute -> errors can be shown
- $return->error[] = $response;
+ echo json_encode($return);
}
-
- echo json_encode($return);
- die();
}
+ // store all user activities that are buffered for logging in this request
+ // this should work even on non HTTP200 responses
+ $this->logActivities();
+
return true;
}
+ /**
+ * store activity log data to DB
+ */
+ protected function logActivities(){
+ LogController::instance()->logActivities();
+ Monolog::instance()->log();
+ }
+
/**
* get controller by class name
* -> controller class is searched within all controller directories
@@ -793,7 +818,7 @@ class Controller {
* @return int
*/
static function getRegistrationStatus(){
- return (int)\Base::instance()->get('PATHFINDER.REGISTRATION.STATUS');
+ return (int)Config::getPathfinderData('registration.status');
}
/**
@@ -806,13 +831,6 @@ class Controller {
return LogController::getLogger($type);
}
- /**
- * store activity log data to DB
- */
- static function storeActivities(){
- LogController::instance()->storeActivities();
- }
-
/**
* removes illegal characters from a Hive-key that are not allowed
* @param $key
@@ -842,20 +860,4 @@ class Controller {
(new Socket( Config::getSocketUri(), $ttl ))->sendData('healthCheck', $load);
}
- /**
- * get required MySQL variable value
- * @param $key
- * @return string|null
- */
- static function getRequiredMySqlVariables($key){
- $f3 = \Base::instance();
- $requiredMySqlVarKey = 'REQUIREMENTS[MYSQL][VARS][' . $key . ']';
- $data = null;
-
- if( $f3->exists($requiredMySqlVarKey) ){
- $data = $f3->get($requiredMySqlVarKey);
- }
- return $data;
- }
-
}
\ No newline at end of file
diff --git a/app/main/controller/logcontroller.php b/app/main/controller/logcontroller.php
index c00a60af..b3819714 100644
--- a/app/main/controller/logcontroller.php
+++ b/app/main/controller/logcontroller.php
@@ -8,42 +8,73 @@
namespace controller;
use DB;
+use lib\Config;
+use Lib\Logging\MapLog;
+use Model\ActivityLogModel;
+use Model\BasicModel;
class LogController extends \Prefab {
+ const CACHE_KEY_ACTIVITY_COLUMNS = 'CACHED_ACTIVITY_COLUMNS';
+ const CACHE_TTL_ACTIVITY_COLUMNS = 300;
+
+ /**
+ * @var string[]
+ */
+ protected $activityLogColumns = [];
+
/**
* buffered activity log data for this singleton LogController() class
* -> this buffered data can be stored somewhere (e.g. DB) before HTTP response
* -> should be cleared afterwards!
* @var array
*/
- protected $activityLogBuffer = [];
+ protected $activityLogBuffer = [];
/**
- * reserve a "new" character activity for logging
- * @param $characterId
- * @param $mapId
- * @param $action
+ * get columns from ActivityLogModel that can be uses as counter
+ * @return array
*/
- public function bufferActivity($characterId, $mapId, $action){
- $characterId = (int)$characterId;
- $mapId = (int)$mapId;
+ protected function getActivityLogColumns(): array{
+ if(empty($this->activityLogColumns)){
+ $f3 = \Base::instance();
+ if(!$f3->exists(self::CACHE_KEY_ACTIVITY_COLUMNS, $this->activityLogColumns)){
+ /**
+ * @var $activityLogModel ActivityLogModel
+ */
+ $activityLogModel = BasicModel::getNew('ActivityLogModel');
+ $this->activityLogColumns = $activityLogModel->getCountableColumnNames();
+ $f3->set(self::CACHE_KEY_ACTIVITY_COLUMNS, self::CACHE_TTL_ACTIVITY_COLUMNS);
+ }
+ }
- if(
- $characterId > 0 &&
- $mapId > 0
- ){
- $key = $this->getBufferedActivityKey($characterId, $mapId);
+ return $this->activityLogColumns;
+ }
- if( is_null($key) ){
- $activity = [
- 'characterId' => $characterId,
- 'mapId' => $mapId,
- $action => 1
- ];
- $this->activityLogBuffer[] = $activity;
- }else{
- $this->activityLogBuffer[$key][$action]++;
+ /**
+ * buffered activity log data for this singleton LogController() class
+ * -> this buffered data can be stored somewhere (e.g. DB) before HTTP response
+ * -> should be cleared afterwards!
+ * @param MapLog $log
+ */
+ public function push(MapLog $log){
+ $action = $log->getAction();
+
+ // check $action to be valid (table column exists)
+ if($action && in_array($action, $this->getActivityLogColumns())){
+ if($mapId = $log->getChannelId()){
+ $logData = $log->getData();
+ if($characterId = (int)$logData['character']['id']){
+ if($index = $this->getBufferedActivityIndex($characterId, $mapId)){
+ $this->activityLogBuffer[$index][$action]++;
+ }else{
+ $this->activityLogBuffer[] = [
+ 'characterId' => $characterId,
+ 'mapId' => $mapId,
+ $action => 1
+ ];
+ }
+ }
}
}
}
@@ -51,7 +82,7 @@ class LogController extends \Prefab {
/**
* store all buffered activity log data to DB
*/
- public function storeActivities(){
+ public function logActivities(){
if( !empty($this->activityLogBuffer) ){
$db = DB\Database::instance()->getDB('PF');
@@ -104,24 +135,20 @@ class LogController extends \Prefab {
}
/**
- * get array key from "buffered activity log" array
+ * get array key/index from "buffered activity log" array
* @param int $characterId
* @param int $mapId
- * @return int|null
+ * @return int
*/
- private function getBufferedActivityKey($characterId, $mapId){
- $activityKey = null;
-
- if(
- $characterId > 0 &&
- $mapId > 0
- ){
+ private function getBufferedActivityIndex(int $characterId, int $mapId): int {
+ $activityKey = 0;
+ if($characterId > 0 && $mapId > 0 ){
foreach($this->activityLogBuffer as $key => $activityData){
if(
$activityData['characterId'] === $characterId &&
$activityData['mapId'] === $mapId
){
- $activityKey = $key;
+ $activityKey = (int)$key;
break;
}
}
@@ -136,8 +163,7 @@ class LogController extends \Prefab {
* @return \Log|null
*/
public static function getLogger($type){
- $f3 = \Base::instance();
- $logFiles = $f3->get('PATHFINDER.LOGFILES');
+ $logFiles = Config::getPathfinderData('logfiles');
$logFileName = empty($logFiles[$type]) ? 'error' : $logFiles[$type];
$logFile = $logFileName . '.log';
diff --git a/app/main/controller/mailcontroller.php b/app/main/controller/mailcontroller.php
deleted file mode 100644
index 2477d66c..00000000
--- a/app/main/controller/mailcontroller.php
+++ /dev/null
@@ -1,80 +0,0 @@
-set('Errors-to', '<' . Controller::getEnvironmentData('SMTP_ERROR') . '>');
- $this->set('MIME-Version', '1.0');
- $this->set('Content-Type', 'text/html; charset=ISO-8859-1');
- }
-
- /**
- * send mail to removed user account
- * @param $to
- * @param $msg
- * @return bool
- */
- public function sendDeleteAccount($to, $msg){
- $status = false;
-
- if( !empty($to)){
- $this->set('To', '<' . $to . '>');
- $this->set('From', '"Pathfinder" <' . Controller::getEnvironmentData('SMTP_FROM') . '>');
- $this->set('Subject', 'Account deleted');
- $status = $this->send($msg);
- }
-
- return $status;
- }
-
- /**
- * send notification mail for new rally point systems
- * @param $to
- * @param $msg
- * @return bool
- */
- public function sendRallyPoint($to, $msg){
- $status = false;
-
- if( !empty($to)){
- $this->set('To', '<' . $to . '>');
- $this->set('From', '"Pathfinder" <' . Controller::getEnvironmentData('SMTP_FROM') . '>');
- $this->set('Subject', 'PATHFINDER - New rally point');
- $status = $this->send($msg);
- }
-
- return $status;
- }
-
- public function send($message, $log = true, $mock = false){
- $status = false;
-
- if(
- !empty($this->host) &&
- !empty($this->port)
- ){
- $status = parent::send($message, $log, $mock);
- }
-
- return $status;
- }
-}
\ No newline at end of file
diff --git a/app/main/controller/setup.php b/app/main/controller/setup.php
index 080c39aa..2c929f62 100644
--- a/app/main/controller/setup.php
+++ b/app/main/controller/setup.php
@@ -13,6 +13,7 @@ use DB;
use DB\SQL;
use DB\SQL\MySQL as MySQL;
use lib\Config;
+use Lib\Util;
use Model;
class Setup extends Controller {
@@ -26,10 +27,10 @@ class Setup extends Controller {
'BASE',
'URL',
'DEBUG',
- 'DB_DNS',
- 'DB_NAME',
- 'DB_USER',
- 'DB_PASS',
+ 'DB_PF_DNS',
+ 'DB_PF_NAME',
+ 'DB_PF_USER',
+ 'DB_PF_PASS',
'DB_CCP_DNS',
'DB_CCP_NAME',
'DB_CCP_USER',
@@ -95,15 +96,15 @@ class Setup extends Controller {
],
'tables' => []
],
- /* WIP ...
'UNIVERSE' => [
'info' => [],
'models' => [
- 'Model\Universe\RegionModel',
- 'Model\Universe\ConstellationModel'
+ 'Model\Universe\TypeModel',
+ //'Model\Universe\RegionModel',
+ //'Model\Universe\ConstellationModel'
],
'tables' => []
- ], */
+ ],
'CCP' => [
'info' => [],
'models' => [],
@@ -120,19 +121,28 @@ class Setup extends Controller {
]
];
+ /**
+ * @var DB\Database
+ */
+ protected $dbLib = null;
+
/**
* database error
* @var bool
*/
- protected $databaseCheck = true;
+ protected $databaseHasError = false;
/**
* event handler for all "views"
* some global template variables are set in here
* @param \Base $f3
* @param array $params
+ * @return bool
*/
- function beforeroute(\Base $f3, $params) {
+ function beforeroute(\Base $f3, $params): bool {
+ // init dbLib class. Manages all DB connections
+ $this->dbLib = DB\Database::instance();
+
// page title
$f3->set('tplPageTitle', 'Setup | ' . Config::getPathfinderData('name'));
@@ -144,8 +154,13 @@ class Setup extends Controller {
// js path (build/minified or raw uncompressed files)
$f3->set('tplPathJs', 'public/js/' . Config::getPathfinderData('version') );
+
+ return true;
}
+ /**
+ * @param \Base $f3
+ */
public function afterroute(\Base $f3) {
// js view (file)
$f3->set('tplJsView', 'setup');
@@ -159,6 +174,16 @@ class Setup extends Controller {
return $cacheType;
});
+ // simple counter (called within template)
+ $counter = 0;
+ $f3->set('tplCounter', function(string $action = 'add') use (&$counter){
+ switch($action){
+ case 'add': $counter++; break;
+ case 'get': return $counter; break;
+ case 'reset': $counter = 0; break;
+ }
+ });
+
// render view
echo \Template::instance()->render( Config::getPathfinderData('view.index') );
}
@@ -175,26 +200,31 @@ class Setup extends Controller {
// enables automatic column fix
$fixColumns = false;
- // bootstrap database from model class definition
- if( !empty($params['db']) ){
- $this->bootstrapDB($params['db']);
-
- // reload page
- // -> remove GET param
- $f3->reroute('@setup');
- return;
- }elseif( !empty($params['fixCols']) ){
- $fixColumns = true;
- }elseif( !empty($params['buildIndex']) ){
- $this->setupSystemJumpTable();
- }elseif( !empty($params['importTable']) ){
- $this->importTable($params['importTable']);
- }elseif( !empty($params['exportTable']) ){
- $this->exportTable($params['exportTable']);
- }elseif( !empty($params['clearCache']) ){
- $this->clearCache($f3);
- }elseif( !empty($params['invalidateCookies']) ){
- $this->invalidateCookies($f3);
+ switch($params['action']){
+ case 'createDB':
+ $this->createDB($params['db']);
+ break;
+ case 'bootstrapDB':
+ $this->bootstrapDB($params['db']);
+ break;
+ case 'fixCols':
+ $fixColumns = true;
+ break;
+ case 'buildIndex':
+ $this->setupSystemJumpTable();
+ break;
+ case 'importTable':
+ $this->importTable($params['model']);
+ break;
+ case 'exportTable':
+ $this->exportTable($params['model']);
+ break;
+ case 'clearCache':
+ $this->clearCache($f3);
+ break;
+ case 'invalidateCookies':
+ $this->invalidateCookies($f3);
+ break;
}
// set template data ----------------------------------------------------------------
@@ -207,6 +237,12 @@ class Setup extends Controller {
// set requirement check information
$f3->set('checkRequirements', $this->checkRequirements($f3));
+ // set php config check information
+ $f3->set('checkPHPConfig', $this->checkPHPConfig($f3));
+
+ // set map default config
+ $f3->set('mapsDefaultConfig', $this->getMapsDefaultConfig($f3));
+
// set database connection information
$f3->set('checkDatabase', $this->checkDatabase($f3, $fixColumns));
@@ -320,9 +356,9 @@ class Setup extends Controller {
protected function getEnvironmentInformation(\Base $f3){
$environmentData = [];
// exclude some sensitive data (e.g. database, passwords)
- $excludeVars = ['DB_DNS', 'DB_NAME', 'DB_USER',
- 'DB_PASS', 'DB_CCP_DNS', 'DB_CCP_NAME',
- 'DB_CCP_USER', 'DB_CCP_PASS'
+ $excludeVars = [
+ 'DB_PF_DNS', 'DB_PF_NAME', 'DB_PF_USER', 'DB_PF_PASS',
+ 'DB_CCP_DNS', 'DB_CCP_NAME', 'DB_CCP_USER', 'DB_CCP_PASS'
];
// obscure some values
@@ -338,10 +374,7 @@ class Setup extends Controller {
$check = false;
$value = '[missing]';
}elseif( in_array($var, $obscureVars)){
- $length = strlen($value);
- $hideChars = ($length < 10) ? $length : 10;
- $value = substr_replace($value, str_repeat('.', 3), -$hideChars);
- $value .= ' [' . $length . ']';
+ $value = Util::obscureString($value);
}
$environmentData[$var] = [
@@ -444,6 +477,9 @@ class Setup extends Controller {
'version' => (PHP_INT_SIZE * 8) . '-bit',
'check' => $f3->get('REQUIREMENTS.PHP.PHP_INT_SIZE') == PHP_INT_SIZE
],
+ [
+ 'label' => 'PHP extensions'
+ ],
'pcre' => [
'label' => 'PCRE',
'required' => $f3->get('REQUIREMENTS.PHP.PCRE_VERSION'),
@@ -486,20 +522,6 @@ class Setup extends Controller {
'version' => (extension_loaded('curl') && function_exists('curl_version')) ? 'installed' : 'missing',
'check' => (extension_loaded('curl') && function_exists('curl_version'))
],
- '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'),
- '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'),
- 'tooltip' => 'PHP default = 30. Max execution time for PHP scripts.'
- ],
[
'label' => 'Redis Server [optional]'
],
@@ -594,6 +616,125 @@ class Setup extends Controller {
return $checkRequirements;
}
+ /**
+ * check PHP config (php.ini)
+ * @param \Base $f3
+ * @return array
+ */
+ protected function checkPHPConfig(\Base $f3): array {
+ $phpConfig = [
+ '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'),
+ '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'),
+ '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'),
+ 'tooltip' => 'Formatted HTML StackTrace on error.'
+ ],
+ [
+ 'label' => 'Session'
+ ],
+ 'sessionSaveHandler' => [
+ 'label' => 'save_handler',
+ 'version' => ini_get('session.save_handler'),
+ 'check' => true,
+ 'tooltip' => 'PHP Session save handler (Redis is preferred).'
+ ],
+ 'sessionSavePath' => [
+ 'label' => 'session.save_path',
+ 'version' => ini_get('session.save_path'),
+ 'check' => true,
+ 'tooltip' => 'PHP Session save path (Redis is preferred).'
+ ],
+ 'sessionName' => [
+ 'label' => 'session.name',
+ 'version' => ini_get('session.name'),
+ 'check' => true,
+ 'tooltip' => 'PHP Session name.'
+ ]
+ ];
+
+ return $phpConfig;
+ }
+
+ /**
+ * get default map config
+ * @param \Base $f3
+ * @return array
+ */
+ protected function getMapsDefaultConfig(\Base $f3): array {
+ $matrix = \Matrix::instance();
+ $mapsDefaultConfig = (array)Config::getMapsDefaultConfig();
+ $matrix->transpose($mapsDefaultConfig);
+
+ $mapConfig = ['mapTypes' => array_keys(reset($mapsDefaultConfig))];
+
+ foreach($mapsDefaultConfig as $option => $defaultConfig){
+ $tooltip = '';
+ switch($option){
+ case 'lifetime':
+ $label = 'Map lifetime (days)';
+ $tooltip = 'Unchanged/inactive maps get auto deleted afterwards (cronjob).';
+ break;
+ case 'max_count':
+ $label = 'Max. maps count/user';
+ break;
+ case 'max_shared':
+ $label = 'Map share limit/map';
+ $tooltip = 'E.g. A Corp map can be shared with X other corps.';
+ break;
+ case 'max_systems':
+ $label = 'Max. systems count/map';
+ break;
+ case 'log_activity_enabled':
+ $label = ' Activity statistics';
+ $tooltip = 'If "enabled", map admins can enable user statistics for a map.';
+ break;
+ case 'log_history_enabled':
+ $label = ' History log files';
+ $tooltip = 'If "enabled", map admins can pipe map logs to file. (one file per map)';
+ break;
+ case 'send_history_slack_enabled':
+ $label = ' History log Slack';
+ $tooltip = 'If "enabled", map admins can set a Slack channel were map logs get piped to.';
+ break;
+ case 'send_rally_slack_enabled':
+ $label = ' Rally point poke Slack';
+ $tooltip = 'If "enabled", map admins can set a Slack channel for rally point pokes.';
+ break;
+ case 'send_rally_mail_enabled':
+ $label = ' Rally point poke Email';
+ $tooltip = 'If "enabled", rally point pokes can be send by Email (SMTP config + recipient address required).';
+ break;
+ default:
+ $label = 'unknown';
+ }
+
+ $mapsDefaultConfig[$option] = [
+ 'label' => $label,
+ 'tooltip' => $tooltip,
+ 'data' => $defaultConfig
+ ];
+ }
+
+ $mapConfig['mapConfig'] = $mapsDefaultConfig;
+
+ return $mapConfig;
+ }
+
/**
* get database connection information
* @param \Base $f3
@@ -611,7 +752,9 @@ class Setup extends Controller {
$dbConnected = false;
// DB type (e.g. MySql,..)
$dbDriver = 'unknown';
- // enable database ::setup() function in UI
+ // enable database ::create() function on UI
+ $dbCreate = false;
+ // enable database ::setup() function on UI
$dbSetupEnable = false;
// check of everything is OK (connection, tables, columns, indexes,..)
$dbStatusCheckCount = 0;
@@ -622,7 +765,9 @@ class Setup extends Controller {
// get DB config
$dbConfigValues = Config::getDatabaseConfig($dbKey);
// check DB for valid connection
- $db = DB\Database::instance()->getDB($dbKey);
+ $db = $this->dbLib->getDB($dbKey);
+ // collection for errors
+ $dbErrors = [];
// check config that does NOT require a valid DB connection
switch($dbKey){
@@ -631,8 +776,9 @@ class Setup extends Controller {
case 'CCP': $dbLabel = 'EVE-Online [SDE]'; break;
}
- $dbName = $dbConfigValues['NAME'];
- $dbUser = $dbConfigValues['USER'];
+ $dbName = $dbConfigValues['NAME'];
+ $dbUser = $dbConfigValues['USER'];
+ $dbAlias = $dbConfigValues['ALIAS'];
if($db){
switch($dbKey){
@@ -858,27 +1004,47 @@ class Setup extends Controller {
}else{
// DB connection failed
$dbStatusCheckCount++;
+
+ foreach($this->dbLib->getErrors($dbAlias, 10) as $dbException){
+ $dbErrors[] = $dbException->getMessage();
+ }
+
+ // try to connect without! DB (-> offer option to create them)
+ // do not log errors (silent)
+ $this->dbLib->setSilent(true);
+ $dbServer = $this->dbLib->connectToServer($dbAlias);
+ $this->dbLib->setSilent(false);
+ if(!is_null($dbServer)){
+ // connection succeeded
+ $dbCreate = true;
+ $dbDriver = $dbServer->driver();
+ }
}
if($dbStatusCheckCount !== 0){
- $this->databaseCheck = false;
+ $this->databaseHasError = true;
}
// sort tables for better readability
ksort($requiredTables);
$this->databases[$dbKey]['info'] = [
- 'db' => $db,
- 'label' => $dbLabel,
- 'driver' => $dbDriver,
- 'name' => $dbName,
- 'user' => $dbUser,
- 'dbConfig' => $dbConfig,
- 'setupEnable' => $dbSetupEnable,
- 'connected' => $dbConnected,
- 'statusCheckCount' => $dbStatusCheckCount,
- 'columnQueries' => $dbColumnQueries,
- 'tableData' => $requiredTables
+ // 'db' => $db,
+ 'label' => $dbLabel,
+ 'host' => Config::getDatabaseDNSValue((string)$dbConfigValues['DNS'], 'host'),
+ 'port' => Config::getDatabaseDNSValue((string)$dbConfigValues['DNS'], 'port'),
+ 'driver' => $dbDriver,
+ 'name' => $dbName,
+ 'user' => $dbUser,
+ 'pass' => Util::obscureString((string)$dbConfigValues['PASS'], 8),
+ 'dbConfig' => $dbConfig,
+ 'dbCreate' => $dbCreate,
+ 'setupEnable' => $dbSetupEnable,
+ 'connected' => $dbConnected,
+ 'statusCheckCount' => $dbStatusCheckCount,
+ 'columnQueries' => $dbColumnQueries,
+ 'tableData' => $requiredTables,
+ 'errors' => $dbErrors
];
}
@@ -941,24 +1107,46 @@ class Setup extends Controller {
return $dbConfig;
}
+ /**
+ * try to create a fresh database
+ * @param string $dbKey
+ */
+ protected function createDB(string $dbKey){
+ // check for valid key
+ if(!empty($this->databases[$dbKey])){
+ // disable logging (we expect the DB connect to fail -> no db created)
+ $this->dbLib->setSilent(true);
+ // try to connect
+ $db = $this->dbLib->getDB($dbKey);
+ // enable logging
+ $this->dbLib->setSilent(false, true);
+ if(is_null($db)){
+ // try create new db
+ $db = $this->dbLib->createDB($dbKey);
+ if(is_null($db)){
+ foreach($this->dbLib->getErrors($dbKey, 5) as $error){
+ // ... no further error handling here -> check log files
+ //$error->getMessage()
+ }
+ }
+ }
+ }
+ }
+
/**
* init the complete database
* - create tables
* - create indexes
* - set default static values
- * @param $dbKey
+ * @param string $dbKey
* @return array
*/
- protected function bootstrapDB($dbKey){
- $db = DB\Database::instance()->getDB($dbKey);
-
+ protected function bootstrapDB(string $dbKey){
+ $db = $this->dbLib->getDB($dbKey);
$checkTables = [];
if($db){
- // set/change default "character set" and "collation"
- $db->exec('ALTER DATABASE ' . $db->quotekey($db->name())
- . ' CHARACTER SET ' . self::getRequiredMySqlVariables('CHARACTER_SET_DATABASE')
- . ' COLLATE ' . self::getRequiredMySqlVariables('COLLATION_DATABASE')
- );
+ // set some default config for this database
+ DB\Database::prepareDatabase($db);
// setup tables
foreach($this->databases[$dbKey]['models'] as $modelClass){
@@ -976,10 +1164,10 @@ class Setup extends Controller {
// $ttl for health check
$ttl = 600;
- $heachCheckToken = microtime(true);
+ $healthCheckToken = microtime(true);
// ping TCP Socket with checkToken
- self::checkTcpSocket($ttl, $heachCheckToken);
+ self::checkTcpSocket($ttl, $healthCheckToken);
$socketInformation = [
'tcpSocket' => [
@@ -1004,7 +1192,7 @@ class Setup extends Controller {
'check' => !empty( $ttl )
]
],
- 'token' => $heachCheckToken
+ 'token' => $healthCheckToken
],
'webSocket' => [
'label' => 'WebSocket (clients) [HTTP]',
@@ -1026,78 +1214,77 @@ class Setup extends Controller {
* @return array
*/
protected function getIndexData(){
-
// active DB and tables are required for obtain index data
- if( $this->databaseCheck ){
+ if(!$this->databaseHasError){
$indexInfo = [
'SystemNeighbourModel' => [
- 'action' => [
+ 'task' => [
[
- 'task' => 'buildIndex',
+ 'action' => 'buildIndex',
'label' => 'build',
'icon' => 'fa-refresh',
'btn' => 'btn-primary'
]
],
'table' => Model\BasicModel::getNew('SystemNeighbourModel')->getTable(),
- 'count' => DB\Database::instance()->getRowCount( Model\BasicModel::getNew('SystemNeighbourModel')->getTable() )
+ 'count' => $this->dbLib->getRowCount( Model\BasicModel::getNew('SystemNeighbourModel')->getTable() )
],
'WormholeModel' => [
- 'action' => [
+ 'task' => [
[
- 'task' => 'exportTable',
+ 'action' => 'exportTable',
'label' => 'export',
'icon' => 'fa-download',
'btn' => 'btn-default'
],[
- 'task' => 'importTable',
+ 'action' => 'importTable',
'label' => 'import',
'icon' => 'fa-upload',
'btn' => 'btn-primary'
]
],
'table' => Model\BasicModel::getNew('WormholeModel')->getTable(),
- 'count' => DB\Database::instance()->getRowCount( Model\BasicModel::getNew('WormholeModel')->getTable() )
+ 'count' => $this->dbLib->getRowCount( Model\BasicModel::getNew('WormholeModel')->getTable() )
],
'SystemWormholeModel' => [
- 'action' => [
+ 'task' => [
[
- 'task' => 'exportTable',
+ 'action' => 'exportTable',
'label' => 'export',
'icon' => 'fa-download',
'btn' => 'btn-default'
],[
- 'task' => 'importTable',
+ 'action' => 'importTable',
'label' => 'import',
'icon' => 'fa-upload',
'btn' => 'btn-primary'
]
],
'table' => Model\BasicModel::getNew('SystemWormholeModel')->getTable(),
- 'count' => DB\Database::instance()->getRowCount( Model\BasicModel::getNew('SystemWormholeModel')->getTable() )
+ 'count' => $this->dbLib->getRowCount( Model\BasicModel::getNew('SystemWormholeModel')->getTable() )
],
'ConstellationWormholeModel' => [
- 'action' => [
+ 'task' => [
[
- 'task' => 'exportTable',
+ 'action' => 'exportTable',
'label' => 'export',
'icon' => 'fa-download',
'btn' => 'btn-default'
],[
- 'task' => 'importTable',
+ 'action' => 'importTable',
'label' => 'import',
'icon' => 'fa-upload',
'btn' => 'btn-primary'
]
],
'table' => Model\BasicModel::getNew('ConstellationWormholeModel')->getTable(),
- 'count' => DB\Database::instance()->getRowCount( Model\BasicModel::getNew('ConstellationWormholeModel')->getTable() )
+ 'count' => $this->dbLib->getRowCount( Model\BasicModel::getNew('ConstellationWormholeModel')->getTable() )
]
];
}else{
$indexInfo = [
'SystemNeighbourModel' => [
- 'action' => [],
+ 'task' => [],
'table' => 'Fix database errors first!'
]
];
@@ -1178,7 +1365,7 @@ class Setup extends Controller {
/**
* import table data from existing dump file (e.g *.csv)
- * @param $modelClass
+ * @param string $modelClass
* @return bool
* @throws \Exception
*/
@@ -1189,7 +1376,7 @@ class Setup extends Controller {
/**
* export table data
- * @param $modelClass
+ * @param string $modelClass
* @throws \Exception
*/
protected function exportTable($modelClass){
diff --git a/app/main/cron/cache.php b/app/main/cron/cache.php
index a635fb8d..63832068 100644
--- a/app/main/cron/cache.php
+++ b/app/main/cron/cache.php
@@ -12,11 +12,25 @@ use data\filesystem\Search;
class Cache {
- const LOG_TEXT = '%s [%\'_10s] files, size [%\'_10s] byte, not writable [%\'_10s] files, errors [%\'_10s], exec (%.3Fs)';
+ const LOG_TEXT = '%s [%\'_10s] files, size [%\'_10s] byte, not writable [%\'_10s] files, errors [%\'_10s], exec (%.3Fs)';
+ /**
+ * default max expire for files (seconds)
+ */
+ const CACHE_EXPIRE_MAX = 864000;
+
+ /**
+ * @param \Base $f3
+ * @return int
+ */
+ protected function getExpireMaxTime(\Base $f3): int {
+ $expireTime = (int)$f3->get('PATHFINDER.CACHE.EXPIRE_MAX');
+ return ($expireTime >= 0) ? $expireTime : self::CACHE_EXPIRE_MAX;
+ }
+
/**
* clear expired cached files
- * >> >php index.php "/cron/deleteExpiredCacheData"
+ * >> php index.php "/cron/deleteExpiredCacheData"
* @param \Base $f3
*/
function deleteExpiredData(\Base $f3){
@@ -25,8 +39,8 @@ class Cache {
// cache dir (dir is recursively searched...)
$cacheDir = $f3->get('TEMP');
- $filterTime = (int)strtotime('-' . $f3->get('PATHFINDER.CACHE.EXPIRE_MAX') . ' seconds');
- $expiredFiles = Search::getFilesByMTime($cacheDir, $filterTime);
+ $filterTime = (int)strtotime('-' . $this->getExpireMaxTime($f3) . ' seconds');
+ $expiredFiles = Search::getFilesByMTime($cacheDir, $filterTime, Search::DEFAULT_FILE_LIMIT);
$deletedFiles = 0;
$deletedSize = 0;
@@ -36,16 +50,18 @@ class Cache {
/**
* @var $file \SplFileInfo
*/
- if( $file->isWritable() ){
- $tmpSize = $file->getSize();
- if( unlink($file->getRealPath()) ){
- $deletedSize += $tmpSize;
- $deletedFiles++;
+ if($file->isFile()){
+ if( $file->isWritable() ){
+ $tmpSize = $file->getSize();
+ if( unlink($file->getRealPath()) ){
+ $deletedSize += $tmpSize;
+ $deletedFiles++;
+ }else{
+ $deleteErrors++;
+ }
}else{
- $deleteErrors++;
+ $notWritableFiles++;
}
- }else{
- $notWritableFiles++;
}
}
diff --git a/app/main/cron/mapupdate.php b/app/main/cron/mapupdate.php
index 3882ce3d..74d53f1c 100644
--- a/app/main/cron/mapupdate.php
+++ b/app/main/cron/mapupdate.php
@@ -8,6 +8,7 @@
namespace cron;
use DB;
+use lib\Config;
use Model;
class MapUpdate {
@@ -23,19 +24,20 @@ class MapUpdate {
* @param \Base $f3
*/
function deactivateMapData(\Base $f3){
- $privateMapLifetime = (int)$f3->get('PATHFINDER.MAP.PRIVATE.LIFETIME');
+ $privateMapLifetime = (int)Config::getMapsDefaultConfig('private.lifetime');
if($privateMapLifetime > 0){
$pfDB = DB\Database::instance()->getDB('PF');
-
- $sqlDeactivateExpiredMaps = "UPDATE map SET
+ if($pfDB){
+ $sqlDeactivateExpiredMaps = "UPDATE map SET
active = 0
WHERE
map.active = 1 AND
map.typeId = 2 AND
TIMESTAMPDIFF(DAY, map.updated, NOW() ) > :lifetime";
- $pfDB->exec($sqlDeactivateExpiredMaps, ['lifetime' => $privateMapLifetime]);
+ $pfDB->exec($sqlDeactivateExpiredMaps, ['lifetime' => $privateMapLifetime]);
+ }
}
}
@@ -45,18 +47,31 @@ class MapUpdate {
* @param \Base $f3
*/
function deleteMapData(\Base $f3){
-
$pfDB = DB\Database::instance()->getDB('PF');
+ $deletedMapsCount = 0;
- $sqlDeleteDisabledMaps = "DELETE FROM
+ if($pfDB){
+ $sqlDeleteDisabledMaps = "SELECT
+ id
+ FROM
map
WHERE
map.active = 0 AND
TIMESTAMPDIFF(DAY, map.updated, NOW() ) > :deletion_time";
- $pfDB->exec($sqlDeleteDisabledMaps, ['deletion_time' => self::DAYS_UNTIL_MAP_DELETION]);
+ $disabledMaps = $pfDB->exec($sqlDeleteDisabledMaps, ['deletion_time' => self::DAYS_UNTIL_MAP_DELETION]);
- $deletedMapsCount = $pfDB->count();
+ if($deletedMapsCount = $pfDB->count()){
+ $mapModel = Model\BasicModel::getNew('MapModel');
+ foreach($disabledMaps as $data){
+ $mapModel->getById( (int)$data['id'], 3, false );
+ if( !$mapModel->dry() ){
+ $mapModel->erase();
+ }
+ $mapModel->reset();
+ }
+ }
+ }
// Log ------------------------
$log = new \Log('cron_' . __FUNCTION__ . '.log');
@@ -73,8 +88,8 @@ class MapUpdate {
if($eolExpire > 0){
$pfDB = DB\Database::instance()->getDB('PF');
-
- $sql = "SELECT
+ if($pfDB){
+ $sql = "SELECT
`con`.`id`
FROM
`connection` `con` INNER JOIN
@@ -85,20 +100,21 @@ class MapUpdate {
TIMESTAMPDIFF(SECOND, `con`.`eolUpdated`, NOW() ) > :expire_time
";
- $connectionsData = $pfDB->exec($sql, [
- 'deleteEolConnections' => 1,
- 'expire_time' => $eolExpire
- ]);
+ $connectionsData = $pfDB->exec($sql, [
+ 'deleteEolConnections' => 1,
+ 'expire_time' => $eolExpire
+ ]);
- if($connectionsData){
- /**
- * @var $connection Model\ConnectionModel
- */
- $connection = Model\BasicModel::getNew('ConnectionModel');
- foreach($connectionsData as $data){
- $connection->getById( (int)$data['id'] );
- if( !$connection->dry() ){
- $connection->erase();
+ if($connectionsData){
+ /**
+ * @var $connection Model\ConnectionModel
+ */
+ $connection = Model\BasicModel::getNew('ConnectionModel');
+ foreach($connectionsData as $data){
+ $connection->getById( (int)$data['id'] );
+ if( !$connection->dry() ){
+ $connection->erase();
+ }
}
}
}
@@ -115,8 +131,8 @@ class MapUpdate {
if($whExpire > 0){
$pfDB = DB\Database::instance()->getDB('PF');
-
- $sql = "SELECT
+ if($pfDB){
+ $sql = "SELECT
`con`.`id`
FROM
`connection` `con` INNER JOIN
@@ -128,21 +144,22 @@ class MapUpdate {
TIMESTAMPDIFF(SECOND, `con`.`created`, NOW() ) > :expire_time
";
- $connectionsData = $pfDB->exec($sql, [
- 'deleteExpiredConnections' => 1,
- 'scope' => 'wh',
- 'expire_time' => $whExpire
- ]);
+ $connectionsData = $pfDB->exec($sql, [
+ 'deleteExpiredConnections' => 1,
+ 'scope' => 'wh',
+ 'expire_time' => $whExpire
+ ]);
- if($connectionsData){
- /**
- * @var $connection Model\ConnectionModel
- */
- $connection = Model\BasicModel::getNew('ConnectionModel');
- foreach($connectionsData as $data){
- $connection->getById( (int)$data['id'] );
- if( !$connection->dry() ){
- $connection->erase();
+ if($connectionsData){
+ /**
+ * @var $connection Model\ConnectionModel
+ */
+ $connection = Model\BasicModel::getNew('ConnectionModel');
+ foreach($connectionsData as $data){
+ $connection->getById( (int)$data['id'] );
+ if( !$connection->dry() ){
+ $connection->erase();
+ }
}
}
}
@@ -159,8 +176,8 @@ class MapUpdate {
if($signatureExpire > 0){
$pfDB = DB\Database::instance()->getDB('PF');
-
- $sqlDeleteExpiredSignatures = "DELETE `sigs` FROM
+ if($pfDB){
+ $sqlDeleteExpiredSignatures = "DELETE `sigs` FROM
`system_signature` `sigs` INNER JOIN
`system` ON
`system`.`id` = `sigs`.`systemId`
@@ -169,7 +186,8 @@ class MapUpdate {
TIMESTAMPDIFF(SECOND, `sigs`.`updated`, NOW() ) > :lifetime
";
- $pfDB->exec($sqlDeleteExpiredSignatures, ['lifetime' => $signatureExpire]);
+ $pfDB->exec($sqlDeleteExpiredSignatures, ['lifetime' => $signatureExpire]);
+ }
}
}
diff --git a/app/main/data/file/filehandler.php b/app/main/data/file/filehandler.php
new file mode 100644
index 00000000..be313301
--- /dev/null
+++ b/app/main/data/file/filehandler.php
@@ -0,0 +1,90 @@
+ Each row is a JSON object
+ * @param string $sourceFile
+ * @param int $offset
+ * @param int $limit
+ * @param null|callable $formatter
+ * @return array
+ */
+ public static function readLogFile(
+ string $sourceFile,
+ int $offset = self::LOG_FILE_OFFSET,
+ int $limit = self::LOG_FILE_LIMIT,
+ $formatter = null
+ ): array {
+ $data = [];
+
+ if(is_file($sourceFile)){
+ if(is_readable($sourceFile)){
+ $file = new ReverseSplFileObject($sourceFile, $offset);
+ $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;
+ }
+ }
+ }else{
+ \Base::instance()->error(500, sprintf(self::ERROR_STREAM_READABLE, $sourceFile));
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * validate offset
+ * @param int $offset
+ * @return int
+ */
+ public static function validateOffset(int $offset): int{
+ if(
+ $offset < self::LOG_FILE_OFFSET_MIN ||
+ $offset > self::LOG_FILE_OFFSET_MAX
+ ){
+ $offset = self::LOG_FILE_OFFSET;
+ }
+ return $offset;
+ }
+
+ /**
+ * validate limit
+ * @param int $limit
+ * @return int
+ */
+ public static function validateLimit(int $limit): int{
+ if(
+ $limit < self::LOG_FILE_LIMIT_MIN ||
+ $limit > self::Log_File_LIMIT_MAX
+ ){
+ $limit = self::LOG_FILE_LIMIT;
+ }
+ return $limit;
+ }
+}
\ No newline at end of file
diff --git a/app/main/data/file/reversesplfileobject.php b/app/main/data/file/reversesplfileobject.php
new file mode 100644
index 00000000..29cc4ea6
--- /dev/null
+++ b/app/main/data/file/reversesplfileobject.php
@@ -0,0 +1,219 @@
+ 'start with 2nd last line')
+ * @var int
+ */
+ protected $offset = 0;
+
+ /**
+ * total lines found in file
+ * @var int
+ */
+ protected $lineCount = 0;
+
+ /**
+ * empty lines found in file
+ * @var int
+ */
+ protected $lineCountEmpty = 0;
+
+ /**
+ * current pointer position
+ * @var int
+ */
+ protected $pointer = 0;
+
+ /**
+ * position increments when valid row data found
+ * @var
+ */
+ protected $position;
+
+ /**
+ * control characters
+ * @var array
+ */
+ protected $eol = ["\r", "\n"];
+
+ public function __construct($sourceFile, $offset = 0){
+ parent::__construct($sourceFile);
+
+ // set total line count of the file
+ $this->setLineCount();
+
+ //Seek to the first position of the file and record its position
+ //Should be 0
+ $this->fseek(0);
+ $this->begin = $this->ftell();
+ $this->offset = $offset;
+
+ //Seek to the last position from the end of the file
+ //This varies depending on the file
+ $this->fseek($this->pointer, SEEK_END);
+ }
+
+ /**
+ * reverse rewind file.
+ */
+ public function rewind(){
+ //Set the line position to 0 - First Line
+ $this->position = 0;
+
+ //Reset the file pointer to the end of the file minus 1 character. "0" == false
+ $this->fseek(-1, SEEK_END);
+
+ $this->findLineBegin();
+ //... File pointer is now at the beginning of the last line that contains data
+
+ // add custom line offset
+ if($this->offset){
+ // calculate offset start line
+ $offsetLine = $this->lineCount - $this->lineCountEmpty - $this->offset;
+
+ if($offsetLine > 0){
+ // row is zero based
+ $offsetIndex = $offsetLine - 1;
+
+ parent::seek($offsetIndex);
+ // seek() sets pointer to next line... set it back to previous
+ $this->fseek(-2, SEEK_CUR);
+
+ $this->findLineBegin();
+ //... File pointer is now at the beginning of the last line that contains data from $offset
+ }else{
+ // negative offsetLine -> invalid!
+ $this->pointer = $this->begin -1;
+ }
+
+ }
+ }
+
+ /**
+ * Return the current line after the file pointer
+ * @return string
+ */
+ public function current(){
+ return trim($this->fgets());
+ }
+
+ /**
+ * Return the current key of the line we're on
+ * These go in reverse order
+ * @return mixed
+ */
+ public function key(){
+ return $this->position;
+ }
+
+ /**
+ * move one line up
+ */
+ public function next(){
+ //Step the file pointer back one step to the last letter of the previous line
+ --$this->pointer;
+ if($this->pointer < $this->begin){
+ return;
+ }
+
+ $this->fseek($this->pointer);
+
+ $this->findLineBegin();
+
+ //File pointer is now on the next previous line
+ //Increment the line position
+ ++$this->position;
+ }
+
+ /**
+ * Check the current file pointer to make sure we are not at the beginning of the file
+ * @return bool
+ */
+ public function valid(){
+ return ($this->pointer >= $this->begin);
+ }
+
+ /**
+ * seek to previous lines
+ * @param int $lineCount
+ */
+ public function seek($lineCount){
+ for($i = 0; $i < $lineCount; $i++){
+ $this->next();
+ }
+ }
+
+ /**
+ * move pointer to line begin
+ * -> skip line breaks
+ */
+ private function findLineBegin(){
+ //Check the character over and over till we hit another new line
+ $c = $this->fgetc();
+
+ // skip empty lines
+ while(in_array($c, $this->eol)){
+ $this->fseek(-2, SEEK_CUR);
+ if(!$this->pointer = $this->ftell()){
+ break;
+ }
+ $c = $this->fgetc();
+
+ $this->lineCountEmpty++;
+ }
+
+ //Check the last character to make sure it is not a new line
+ while(!in_array($c, $this->eol)){
+ $this->fseek(-2, SEEK_CUR);
+ if(!$this->pointer = $this->ftell()){
+ break;
+ }
+ $c = $this->fgetc();
+ }
+ }
+
+ /**
+ * set total line count. No matter if there are empty lines in between
+ */
+ private function setLineCount(){
+ // Store flags and position
+ $flags = $this->getFlags();
+ $currentPointer = $this->ftell();
+
+ // Prepare count by resetting flags as READ_CSV for example make the tricks very slow
+ $this->setFlags(null);
+
+ // Go to the larger INT we can as seek will not throw exception, errors, notice if we go beyond the bottom line
+ //$this->seek(PHP_INT_MAX);
+ parent::seek(PHP_INT_MAX);
+
+ // We store the key position
+ // As key starts at 0, we add 1
+ $this->lineCount = parent::key() + 1;
+
+ // We move to old position
+ // As seek method is longer with line number < to the max line number, it is better to count at the beginning of iteration
+ //parent::seek($currentPointer);
+ $this->fseek($currentPointer);
+
+ // Re set flags
+ $this->setFlags($flags);
+ }
+}
\ No newline at end of file
diff --git a/app/main/data/filesystem/search.php b/app/main/data/filesystem/search.php
index 36c1d8b0..d94958fe 100644
--- a/app/main/data/filesystem/search.php
+++ b/app/main/data/filesystem/search.php
@@ -11,20 +11,26 @@ namespace data\filesystem;
class Search {
+ /**
+ * max file count that should be deleted in this session
+ */
+ const DEFAULT_FILE_LIMIT = 1000;
+
/**
* timestamp (seconds) filter files by mTime()
* -> default = "no filter"
* @var int
*/
- static $filterTime = 0;
+ static $filterTime = 0;
/**
* recursive file filter by mTime
* @param string $dir
* @param int $mTime
- * @return array|\RecursiveCallbackFilterIterator
+ * @param int $limit
+ * @return array|\LimitIterator
*/
- static function getFilesByMTime($dir, $mTime = null){
+ static function getFilesByMTime(string $dir, $mTime = null, $limit = self::DEFAULT_FILE_LIMIT){
$files = [];
if(is_dir($dir)){
@@ -53,7 +59,8 @@ class Search {
return false;
});
- $files = new \RecursiveIteratorIterator($files);
+ // limit max files
+ $files = new \LimitIterator($files, 0, $limit);
}
return $files;
diff --git a/app/main/db/database.php b/app/main/db/database.php
index f6049ff9..f5f09bcb 100644
--- a/app/main/db/database.php
+++ b/app/main/db/database.php
@@ -7,45 +7,71 @@
*/
namespace DB;
-use Controller;
use controller\LogController;
use lib\Config;
class Database extends \Prefab {
+ /**
+ * if true, errors will not get logged
+ * @var bool
+ */
+ private $silent = false;
- function __construct($database = 'PF'){
- // set database
- $this->setDB($database);
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * connect to the DB server itself -> NO database is used
+ * -> can be used to check if a certain DB exists without connecting to it directly
+ * @param string $dbKey
+ * @return SQL|null
+ */
+ public function connectToServer(string $dbKey = 'PF'){
+ $dbConfig = Config::getDatabaseConfig($dbKey);
+ $dbConfig['DNS'] = str_replace(';dbname=', '', $dbConfig['DNS'] );
+ $dbConfig['NAME'] = '';
+ return call_user_func_array([$this, 'connect'], $dbConfig);
}
/**
- * set database
- * @param string $database
- * @return SQL
+ * tries to create a database if not exists
+ * -> DB user needs rights to create a DB
+ * @param string $dbKey
+ * @return SQL|null
*/
- public function setDB($database = 'PF'){
- $f3 = \Base::instance();
+ public function createDB(string $dbKey = 'PF'){
+ $db = null;
+ $dbConfig = Config::getDatabaseConfig($dbKey);
+ // remove database from $dsn (we want to crate it)
+ $newDbName = $dbConfig['NAME'];
+ if(!empty($newDbName)){
+ $dbConfig['NAME'] = '';
+ $dbConfig['DNS'] = str_replace(';dbname=', '', $dbConfig['DNS'] );
- // "Hive" Key for DB storage
- $dbHiveKey = $this->getDbHiveKey($database);
+ $db = call_user_func_array([$this, 'connect'], $dbConfig);
- // check if DB connection already exists
- if( !$f3->exists($dbHiveKey, $db) ){
- $dbConfig = Config::getDatabaseConfig($database);
+ if(!is_null($db)){
+ $schema = new SQL\Schema($db);
+ if(!in_array($newDbName, $schema->getDatabases())){
+ $db->exec("CREATE DATABASE IF NOT EXISTS
+ `" . $newDbName . "` DEFAULT CHARACTER SET utf8
+ COLLATE utf8_general_ci;");
+ $db->exec("USE `" . $newDbName . "`");
- $db = call_user_func_array([$this, 'connect'], $dbConfig);
-
- if( !is_null($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 = "' .
- Controller\Controller::getRequiredMySqlVariables('DEFAULT_STORAGE_ENGINE') . '"');
-
- // store DB object
- $f3->set($dbHiveKey, $db);
+ // check if DB create was successful
+ $dbCheck = $db->exec("SELECT DATABASE()");
+ if(
+ !empty($dbCheck[0]) &&
+ !empty($checkDbName = reset($dbCheck[0])) &&
+ $checkDbName == $newDbName
+ ){
+ self::prepareDBConnection($db);
+ self::prepareDatabase($db);
+ }
+ }
}
}
@@ -54,14 +80,20 @@ class Database extends \Prefab {
/**
* get database
- * @param string $database
- * @return SQL
+ * @param string $dbKey
+ * @return SQL|null
*/
- public function getDB($database = 'PF'){
+ public function getDB(string $dbKey = 'PF'){
$f3 = \Base::instance();
- $dbHiveKey = $this->getDbHiveKey($database);
+ // "Hive" Key for DB object cache
+ $dbHiveKey = $this->getDbHiveKey($dbKey);
if( !$f3->exists($dbHiveKey, $db) ){
- $db = $this->setDB($database);
+ $dbConfig = Config::getDatabaseConfig($dbKey);
+ $db = call_user_func_array([$this, 'connect'], $dbConfig);
+ if(!is_null($db)){
+ self::prepareDBConnection($db);
+ $f3->set($dbHiveKey, $db);
+ }
}
return $db;
@@ -69,23 +101,23 @@ class Database extends \Prefab {
/**
* get a unique hive key for each DB connection
- * @param $database
+ * @param $dbKey
* @return string
*/
- protected function getDbHiveKey($database){
- return 'DB_' . $database;
+ protected function getDbHiveKey($dbKey){
+ return 'DB_' . $dbKey;
}
-
/**
* connect to a database
- * @param $dns
- * @param $name
- * @param $user
- * @param $password
- * @return SQL
+ * @param string $dns
+ * @param string $name
+ * @param string $user
+ * @param string $password
+ * @param string $alias
+ * @return SQL|null
*/
- protected function connect($dns, $name, $user, $password){
+ protected function connect($dns, $name, $user, $password, $alias){
$db = null;
$f3 = \Base::instance();
@@ -109,9 +141,10 @@ class Database extends \Prefab {
$options
);
}catch(\PDOException $e){
- // DB connection error
- // -> log it
- self::getLogger()->write($e->getMessage());
+ $this->pushError($alias, $e);
+ if(!$this->isSilent()){
+ self::getLogger()->write($e);
+ }
}
return $db;
@@ -119,22 +152,22 @@ class Database extends \Prefab {
/**
* get all table names from a DB
- * @param string $database
+ * @param string $dbKey
* @return array|bool
*/
- public function getTables($database = 'PF'){
- $schema = new SQL\Schema( $this->getDB($database) );
+ public function getTables($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 $database
+ * @param string $dbKey
* @return bool
*/
- public function tableExists($table, $database = 'PF'){
- $tableNames = $this->getTables($database);
+ public function tableExists($table, $dbKey = 'PF'){
+ $tableNames = $this->getTables($dbKey);
return in_array($table, $tableNames);
}
@@ -142,13 +175,13 @@ 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 $database
+ * @param string $dbKey
* @return int
*/
- public function getRowCount($table, $database = 'PF') {
+ public function getRowCount($table, $dbKey = 'PF') {
$count = 0;
- if( $this->tableExists($table, $database) ){
- $db = $this->getDB($database);
+ if( $this->tableExists($table, $dbKey) ){
+ $db = $this->getDB($dbKey);
$countRes = $db->exec("SELECT COUNT(*) `num` FROM " . $db->quotekey($table));
if(isset($countRes[0]['num'])){
$count = (int)$countRes[0]['num'];
@@ -157,6 +190,98 @@ class Database extends \Prefab {
return $count;
}
+ /**
+ * @return bool
+ */
+ public function isSilent() : bool{
+ return $this->silent;
+ }
+
+ /**
+ * set "silent" mode (no error logging)
+ * -> optional clear $this->errors
+ * @param bool $silent
+ * @param bool $clearErrors
+ */
+ public function setSilent(bool $silent, bool $clearErrors = false){
+ $this->silent = $silent;
+ if($clearErrors){
+ $this->errors = [];
+ }
+ }
+
+ /**
+ * push new Exception into static error history
+ * @param string $alias
+ * @param \PDOException $e
+ */
+ protected function pushError(string $alias, \PDOException $e){
+ if(!is_array($this->errors[$alias])){
+ $this->errors[$alias] = [];
+ }
+
+ // prevent adding same errors twice
+ if(!empty($this->errors[$alias])){
+ $lastError = array_values($this->errors[$alias])[0];
+ if($lastError->getMessage() === $e->getMessage()){
+ return;
+ }
+ }
+
+ array_unshift($this->errors[$alias], $e);
+ if(count($this->errors[$alias]) > 5){
+ $this->errors[$alias] = array_pop($this->errors[$alias]);
+ }
+ }
+
+ /**
+ * get last recent Exceptions from error history
+ * @param string $alias
+ * @param int $limit
+ * @return \PDOException[]
+ */
+ public function getErrors(string $alias, int $limit = 1){
+ return array_slice((array)$this->errors[$alias] , 0, $limit);
+ }
+
+ /**
+ * prepare current DB
+ * -> set session connection variables
+ * @param SQL $db
+ */
+ 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') . '"');
+ }
+
+ /**
+ * set some default config for current DB
+ * @param SQL $db
+ */
+ public static function prepareDatabase(SQL &$db){
+ if($db->name()){
+ // set/change default "character set" and "collation"
+ $db->exec('ALTER DATABASE ' . $db->quotekey($db->name())
+ . ' CHARACTER SET ' . self::getRequiredMySqlVariables('CHARACTER_SET_DATABASE')
+ . ' COLLATE ' . self::getRequiredMySqlVariables('COLLATION_DATABASE')
+ );
+ }
+ }
+
+ /**
+ * get required MySQL variable value
+ * @param string $key
+ * @return string|null
+ */
+ public static function getRequiredMySqlVariables(string $key){
+ \Base::instance()->exists('REQUIREMENTS[MYSQL][VARS][' . $key . ']', $data);
+ return $data;
+ }
+
/**
* get logger for DB logging
* @return \Log
diff --git a/app/main/exception/baseexception.php b/app/main/exception/baseexception.php
index 9262e4c4..af4b580c 100644
--- a/app/main/exception/baseexception.php
+++ b/app/main/exception/baseexception.php
@@ -11,11 +11,12 @@ namespace Exception;
class BaseException extends \Exception {
- const VALIDATION_FAILED = 403;
- const REGISTRATION_FAILED = 403;
- const CONFIGURATION_FAILED = 500;
+ const VALIDATION_EXCEPTION = 403;
+ const REGISTRATION_EXCEPTION = 403;
+ const CONFIG_VALUE_EXCEPTION = 500;
+ const DB_EXCEPTION = 500;
- public function __construct($message, $code = 0){
+ public function __construct(string $message, int $code = 0){
parent::__construct($message, $code);
}
diff --git a/app/main/exception/databaseexception.php b/app/main/exception/databaseexception.php
new file mode 100644
index 00000000..a6a72bec
--- /dev/null
+++ b/app/main/exception/databaseexception.php
@@ -0,0 +1,16 @@
+setField($field);
}
}
\ No newline at end of file
diff --git a/app/main/exception/validationexception.php b/app/main/exception/validationexception.php
index 74a3db58..200f4d0c 100644
--- a/app/main/exception/validationexception.php
+++ b/app/main/exception/validationexception.php
@@ -11,27 +11,41 @@ namespace Exception;
class ValidationException extends BaseException {
+ /**
+ * table column that triggers the exception
+ * @var string
+ */
private $field;
/**
- * @return mixed
+ * @return string
*/
- public function getField(){
+ public function getField(): string {
return $this->field;
}
/**
- * @param mixed $field
+ * @param string $field
*/
- public function setField($field){
+ public function setField(string $field){
$this->field = $field;
}
- public function __construct($message, $field = 0){
-
- parent::__construct($message, self::VALIDATION_FAILED);
-
+ public function __construct(string $message, string $field = ''){
+ parent::__construct($message, self::VALIDATION_EXCEPTION);
$this->setField($field);
}
+
+ /**
+ * get error object
+ * @return \stdClass
+ */
+ public function getError(){
+ $error = (object) [];
+ $error->type = 'error';
+ $error->field = $this->getField();
+ $error->message = $this->getMessage();
+ return $error;
+ }
}
\ No newline at end of file
diff --git a/app/main/lib/Monolog.php b/app/main/lib/Monolog.php
new file mode 100644
index 00000000..b28c1e48
--- /dev/null
+++ b/app/main/lib/Monolog.php
@@ -0,0 +1,216 @@
+ 'Monolog\Formatter\LineFormatter',
+ 'json' => 'Monolog\Formatter\JsonFormatter',
+ 'html' => 'Monolog\Formatter\HtmlFormatter',
+ 'mail' => 'Lib\Logging\Formatter\MailFormatter'
+ ];
+
+ const HANDLER = [
+ 'stream' => 'Monolog\Handler\StreamHandler',
+ 'mail' => 'Monolog\Handler\SwiftMailerHandler',
+ 'slackMap' => 'Lib\Logging\Handler\SlackMapWebhookHandler',
+ 'slackRally' => 'Lib\Logging\Handler\SlackRallyWebhookHandler',
+ 'zmq' => 'Lib\Logging\Handler\ZMQHandler'
+ ];
+
+ const PROCESSOR = [
+ 'psr' => 'Monolog\Processor\PsrLogMessageProcessor'
+ ];
+
+ /**
+ * @var Logging\LogCollection[][]|Logging\MapLog[][]
+ */
+ private $logs = [
+ 'solo' => [],
+ 'groups' => []
+ ];
+
+ public function __construct(){
+ // set timezone for all Logger instances
+ if(class_exists(Logger::class)){
+ if( is_callable($getTimezone = \Base::instance()->get('getTimeZone')) ){
+ Logger::setTimezone($getTimezone());
+ };
+ }else{
+ LogController::getLogger('ERROR')->write(sprintf(Config::ERROR_CLASS_NOT_EXISTS_COMPOSER, Logger::class));
+ }
+ }
+
+ /**
+ * buffer log object, add to objectStorage collection
+ * -> this buffered data can be stored/logged somewhere (e.g. DB/file) at any time
+ * -> should be cleared afterwards!
+ * @param Logging\AbstractLog $log
+ */
+ public function push(Logging\AbstractLog $log){
+ // check whether $log should be "grouped" by common handlers
+ if($log->isGrouped()){
+ $groupHash = $log->getGroupHash();
+
+ if(!isset($this->logs['groups'][$groupHash])){
+ // create new log collection
+ // $this->logs['groups'][$groupHash] = new Logging\LogCollection($log->getChannelName());
+ $this->logs['groups'][$groupHash] = new Logging\LogCollection('mapDelete');
+ }
+ $this->logs['groups'][$groupHash]->addLog($log);
+
+ // remove "group" handler from $log
+ // each log should only be logged once per handler!
+ $log->removeHandlerGroups();
+ }
+
+ $this->logs['solo'][] = $log;
+ }
+
+ /**
+ * bulk process all stored logs -> send to Monolog lib
+ */
+ public function log(){
+
+ foreach($this->logs as $logType => $logs){
+ foreach($logs as $logKey => $log){
+ $groupHash = $log->getGroupHash();
+ $level = Logger::toMonologLevel($log->getLevel());
+
+ // add new logger to Registry if not already exists
+ if(Registry::hasLogger($groupHash)){
+ $logger = Registry::getInstance($groupHash);
+ }else{
+ $logger = new Logger($log->getChannelName());
+
+ // disable microsecond timestamps (seconds should be fine)
+ $logger->useMicrosecondTimestamps(true);
+
+ // configure new $logger --------------------------------------------------------------------------
+ // get Monolog Handler with Formatter config
+ // -> $log could have multiple handler with different Formatters
+ $handlerConf = $log->getHandlerConfig();
+ foreach($handlerConf as $handlerKey => $formatterKey){
+ // get Monolog Handler class
+ $handlerParams = $log->getHandlerParams($handlerKey);
+ $handler = $this->getHandler($handlerKey, $handlerParams);
+
+ // get Monolog Formatter
+ $formatter = $this->getFormatter((string)$formatterKey);
+ if( $formatter instanceof FormatterInterface){
+ $handler->setFormatter($formatter);
+ }
+
+ if($log->hasBuffer()){
+ // wrap Handler into bufferHandler
+ // -> bulk save all logs for this $logger
+ $bufferHandler = new BufferHandler($handler);
+ $logger->pushHandler($bufferHandler);
+ }else{
+ $logger->pushHandler($handler);
+ }
+ }
+
+ // get Monolog Processor config
+ $processorConf = $log->getProcessorConfig();
+ foreach($processorConf as $processorKey => $processorCallback){
+ if(is_callable($processorCallback)){
+ // custom Processor callback function
+ $logger->pushProcessor($processorCallback);
+ }else{
+ // get Monolog Processor class
+ $processor = $this->getProcessor($processorKey);
+ $logger->pushProcessor($processor);
+ }
+ }
+
+ Registry::addLogger($logger, $groupHash);
+ }
+
+ $logger->addRecord($level, $log->getMessage(), $log->getContext());
+ }
+ }
+
+ // clear log object storage
+ $this->logs['groups'] = [];
+ $this->logs['solo'] = [];
+ }
+
+ /**
+ * get Monolog Formatter instance by key
+ * @param string $formatKey
+ * @return FormatterInterface|null
+ * @throws \Exception
+ */
+ private function getFormatter(string $formatKey){
+ $formatter = null;
+ if(!empty($formatKey)){
+ if(array_key_exists($formatKey, self::FORMATTER)){
+ $formatClass = self::FORMATTER[$formatKey];
+ $formatter = new $formatClass();
+ }else{
+ throw new \Exception(sprintf(self::ERROR_FORMATTER, $formatKey));
+ }
+ }
+
+ return $formatter;
+ }
+
+ /**
+ * get Monolog Handler instance by key
+ * @param string $handlerKey
+ * @param array $handlerParams
+ * @return HandlerInterface
+ * @throws \Exception
+ */
+ private function getHandler(string $handlerKey, array $handlerParams = []): HandlerInterface{
+ if(array_key_exists($handlerKey, self::HANDLER)){
+ $handlerClass = self::HANDLER[$handlerKey];
+ $handler = new $handlerClass(...$handlerParams);
+ }else{
+ throw new \Exception(sprintf(self::ERROR_HANDLER, $handlerKey));
+ }
+
+ return $handler;
+ }
+
+ /**
+ * get Monolog Processor instance by key
+ * @param string $processorKey
+ * @return callable
+ * @throws \Exception
+ */
+ private function getProcessor(string $processorKey): callable {
+ if(array_key_exists($processorKey, self::PROCESSOR)){
+ $ProcessorClass = self::PROCESSOR[$processorKey];
+ $processor = new $ProcessorClass();
+ }else{
+ throw new \Exception(sprintf(self::ERROR_PROCESSOR, $processorKey));
+ }
+
+ return $processor;
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/main/lib/ccpclient.php b/app/main/lib/ccpclient.php
index aef5a774..fa018950 100644
--- a/app/main/lib/ccpclient.php
+++ b/app/main/lib/ccpclient.php
@@ -16,24 +16,26 @@ class CcpClient extends \Prefab {
private $apiClient;
public function __construct(\Base $f3){
- $this->apiClient = $this->getClient();
+ $this->apiClient = $this->getClient($f3);
$f3->set('ccpClient', $this);
}
/**
* get ApiClient instance
+ * @param \Base $f3
* @return ApiClient|null
*/
- protected function getClient(){
+ protected function getClient(\Base $f3){
$client = null;
- if( !class_exists(ApiClient::class) ){
- LogController::getLogger('ERROR')->write($this->getMissingClientError());
- }else{
+ 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'));
+ }else{
+ LogController::getLogger('ERROR')->write(sprintf(Config::ERROR_CLASS_NOT_EXISTS_COMPOSER, ApiClient::class));
}
return $client;
@@ -52,14 +54,6 @@ class CcpClient extends \Prefab {
return $userAgent;
}
- /**
- * get error msg for failed ApiClient() class -> Composer package not found
- * @return string
- */
- protected function getMissingClientError(){
- return "Class '" . ApiClient::class . "' not found. -> Check installed Composer packages.'";
- }
-
/**
* get error msg for undefined method in ApiClient() class
* @param $method
@@ -86,8 +80,8 @@ class CcpClient extends \Prefab {
\Base::instance()->error(501, $this->getMissingMethodError($name));
}
}else{
- LogController::getLogger('ERROR')->write($this->getMissingClientError());
- \Base::instance()->error(501, $this->getMissingClientError());
+ 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;
diff --git a/app/main/lib/config.php b/app/main/lib/config.php
index 9aa28bb9..a663fd87 100644
--- a/app/main/lib/config.php
+++ b/app/main/lib/config.php
@@ -17,8 +17,11 @@ class Config extends \Prefab {
const ARRAY_DELIMITER = '-';
const HIVE_KEY_PATHFINDER = 'PATHFINDER';
const HIVE_KEY_ENVIRONMENT = 'ENVIRONMENT';
+ 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';
/**
@@ -43,6 +46,11 @@ class Config extends \Prefab {
// set hive configuration variables
// -> overwrites default configuration
$this->setHiveVariables($f3);
+
+ // set global function for current DateTimeZone()
+ $f3->set('getTimeZone', function() use ($f3){
+ return new \DateTimeZone( $f3->get('TZ') );
+ });
}
/**
@@ -164,7 +172,6 @@ class Config extends \Prefab {
static function getEnvironmentData($key){
$hiveKey = self::HIVE_KEY_ENVIRONMENT . '.' . $key;
\Base::instance()->exists($hiveKey, $data);
-
return $data;
}
@@ -173,16 +180,74 @@ class Config extends \Prefab {
* @param string $dbKey
* @return array
*/
- static function getDatabaseConfig($dbKey = 'PF'){
- $dbConfKey = ($dbKey === 'PF') ? '' : $dbKey . '_';
+ static function getDatabaseConfig(string $dbKey = 'PF'){
+ $dbKey = strtoupper($dbKey);
return [
- 'DNS' => self::getEnvironmentData('DB_' . $dbConfKey . 'DNS'),
- 'NAME' => self::getEnvironmentData('DB_' . $dbConfKey . 'NAME'),
- 'USER' => self::getEnvironmentData('DB_' . $dbConfKey . 'USER'),
- 'PASS' => self::getEnvironmentData('DB_' . $dbConfKey . 'PASS')
+ 'DNS' => self::getEnvironmentData('DB_' . $dbKey . '_DNS'),
+ 'NAME' => self::getEnvironmentData('DB_' . $dbKey . '_NAME'),
+ 'USER' => self::getEnvironmentData('DB_' . $dbKey . '_USER'),
+ 'PASS' => self::getEnvironmentData('DB_' . $dbKey . '_PASS'),
+ 'ALIAS' => $dbKey
];
}
+ /**
+ * get DB config value from PDO connect $dns string
+ * @param string $dns
+ * @param string $key
+ * @return bool
+ */
+ static function getDatabaseDNSValue(string $dns, string $key = 'dbname'){
+ $value = false;
+ if(preg_match('/' . preg_quote($key, '/') . '=([[:alnum:]]+)/is', $dns, $parts)){
+ $value = $parts[1];
+ }
+ return $value;
+ }
+
+ /**
+ * get SMTP config values
+ * @return \stdClass
+ */
+ static function getSMTPConfig(): \stdClass{
+ $config = new \stdClass();
+ $config->host = self::getEnvironmentData('SMTP_HOST');
+ $config->port = self::getEnvironmentData('SMTP_PORT');
+ $config->scheme = self::getEnvironmentData('SMTP_SCHEME');
+ $config->username = self::getEnvironmentData('SMTP_USER');
+ $config->password = self::getEnvironmentData('SMTP_PASS');
+ $config->from = [
+ self::getEnvironmentData('SMTP_FROM') => self::getPathfinderData('name')
+ ];
+ return $config;
+ }
+
+ /**
+ * validates an SMTP config
+ * @param \stdClass $config
+ * @return bool
+ */
+ static function isValidSMTPConfig(\stdClass $config): bool {
+ // validate email from either an configured array or plain string
+ $validateMailConfig = function($mailConf = null): bool {
+ $email = null;
+ if(is_array($mailConf)){
+ reset($mailConf);
+ $email = key($mailConf);
+ }elseif(is_string($mailConf)){
+ $email = $mailConf;
+ }
+ return \Audit::instance()->email($email);
+ };
+
+ return (
+ !empty($config->host) &&
+ !empty($config->username) &&
+ $validateMailConfig($config->from) &&
+ $validateMailConfig($config->to)
+ );
+ }
+
/**
* get email for notifications by hive key
* @param $key
@@ -196,7 +261,7 @@ class Config extends \Prefab {
* get map default config values for map types (private/corp/ally)
* -> read from pathfinder.ini
* @param string $mapType
- * @return array
+ * @return mixed
*/
static function getMapsDefaultConfig($mapType = ''){
if( $mapConfig = self::getPathfinderData('map' . ($mapType ? '.' . $mapType : '')) ){
@@ -206,6 +271,92 @@ class Config extends \Prefab {
return $mapConfig;
}
+ /**
+ * get custom $message for a a HTTP $status
+ * -> use this in addition to the very general Base::HTTP_XXX labels
+ * @param int $status
+ * @return string
+ */
+ static function getMessageFromHTTPStatus(int $status): string {
+ switch($status){
+ case 403:
+ $message = 'Access denied: User not found'; break;
+ default:
+ $message = '';
+ }
+ return $message;
+ }
+
+ /**
+ * check whether this installation fulfills all requirements
+ * -> check for ZMQ PHP extension and installed ZQM version
+ * -> this does NOT check versions! -> those can be verified on /setup page
+ * @return bool
+ */
+ static function checkSocketRequirements(): bool {
+ return extension_loaded('zmq') && class_exists('ZMQ');
+ }
+
+ /**
+ * use this function to "validate" the socket connection.
+ * The result will be CACHED for a few seconds!
+ * This function is intended to pre-check a Socket connection if it MIGHT exists.
+ * No data will be send to the Socket, this function just validates if a socket is available
+ * -> see pingDomain()
+ * @return bool
+ */
+ static function validSocketConnect(): bool{
+ $valid = false;
+ $f3 = \Base::instance();
+
+ if( !$f3->exists(self::CACHE_KEY_SOCKET_VALID, $valid) ){
+ if(self::checkSocketRequirements() && ($socketUrl = self::getSocketUri()) ){
+ // get socket URI parts -> not elegant...
+ $domain = parse_url( $socketUrl, PHP_URL_SCHEME) . '://' . parse_url( $socketUrl, PHP_URL_HOST);
+ $port = parse_url( $socketUrl, PHP_URL_PORT);
+ // check connection -> get ms
+ $status = self::pingDomain($domain, $port);
+ if($status >= 0){
+ // connection OK
+ $valid = true;
+ }else{
+ // connection error/timeout
+ $valid = false;
+ }
+ }else{
+ // requirements check failed or URL not valid
+ $valid = false;
+ }
+
+ $f3->set(self::CACHE_KEY_SOCKET_VALID, $valid, self::CACHE_TTL_SOCKET_VALID);
+ }
+
+ return $valid;
+ }
+
+ /**
+ * get response time for a host in ms or -1 on error/timeout
+ * @param string $domain
+ * @param int $port
+ * @param int $timeout
+ * @return int
+ */
+ static function pingDomain(string $domain, int $port, $timeout = 1): int {
+ $starttime = microtime(true);
+ $file = @fsockopen ($domain, $port, $errno, $errstr, $timeout);
+ $stoptime = microtime(true);
+
+ if (!$file){
+ // Site is down
+ $status = -1;
+ }else {
+ fclose($file);
+ $status = ($stoptime - $starttime) * 1000;
+ $status = floor($status);
+ }
+ return $status;
+ }
+
/**
* get URI for TCP socket
* @return bool|string
diff --git a/app/main/lib/logging/AbstractChannelLog.php b/app/main/lib/logging/AbstractChannelLog.php
new file mode 100644
index 00000000..9f865da1
--- /dev/null
+++ b/app/main/lib/logging/AbstractChannelLog.php
@@ -0,0 +1,87 @@
+setChannelData($channelData);
+
+ // add log processor -> remove $channelData from log
+ $processorClearChannelData = function($record){
+ $record['context'] = array_diff_key($record['context'], $this->getChannelData());
+ return $record;
+ };
+
+ // init processorConfig. IMPORTANT: first processor gets executed at the end!
+ $this->processorConfig = ['clearChannelData' => $processorClearChannelData] + $this->processorConfig;
+ }
+
+ /**
+ * @param array $channelData
+ */
+ protected function setChannelData(array $channelData){
+ $this->channelData = $channelData;
+ }
+
+ /**
+ * @return array
+ */
+ public function getChannelData() : array{
+ return $this->channelData;
+ }
+
+ /**
+ * @return int
+ */
+ public function getChannelId() : int{
+ return (int)$this->getChannelData()['channelId'];
+ }
+
+ /**
+ * @return string
+ */
+ public function getChannelName() : string{
+ return (string)$this->getChannelData()['channelName'];
+ }
+
+ /**
+ * @return array
+ */
+ public function getData() : array{
+ $data['main'] = parent::getData();
+
+ if(!empty($channelLogData = $this->getChannelData())){
+ $channelData['channel'] = $channelLogData;
+ $data = $channelData + $data;
+ }
+
+ return $data;
+ }
+
+ /**
+ * @return array
+ */
+ public function getContext(): array{
+ $context = parent::getContext();
+
+ // add temp data (e.g. used for $message placeholder replacement
+ $context += $this->getChannelData();
+
+ return $context;
+ }
+}
\ No newline at end of file
diff --git a/app/main/lib/logging/AbstractCharacterLog.php b/app/main/lib/logging/AbstractCharacterLog.php
new file mode 100644
index 00000000..52a66339
--- /dev/null
+++ b/app/main/lib/logging/AbstractCharacterLog.php
@@ -0,0 +1,81 @@
+ remove $channelData from log
+ $processorAddThumbData = function($record){
+ $record['extra']['thumb']['url'] = $this->getThumbUrl();
+ return $record;
+ };
+
+ // init processorConfig. IMPORTANT: first processor gets executed at the end!
+ $this->processorConfig = ['addThumbData' => $processorAddThumbData] + $this->processorConfig;
+ }
+
+ /**
+ * CharacterModel $character
+ * @param CharacterModel $character
+ * @return LogInterface
+ */
+ public function setCharacter(CharacterModel $character): LogInterface{
+ $this->character = $character;
+ return $this;
+ }
+
+ /**
+ * @return CharacterModel
+ */
+ public function getCharacter(): CharacterModel{
+ return $this->character;
+ }
+
+ /**
+ * @return array
+ */
+ public function getData() : array{
+ $data = parent::getData();
+
+ if(is_object($character = $this->getCharacter())){
+ $characterData['character'] = [
+ 'id' => $character->_id,
+ 'name' => $character->name
+ ];
+ $data = $characterData + $data;
+ }
+
+ return $data;
+ }
+
+ /**
+ * get character thumbnailUrl
+ * @return string
+ */
+ protected function getThumbUrl(): string {
+ $url = '';
+ if(is_object($character = $this->getCharacter())){
+ $url = Config::getPathfinderData('api.ccp_image_server') . '/Character/' . $character->_id . '_128.jpg';
+ }
+
+ return $url;
+ }
+
+}
\ No newline at end of file
diff --git a/app/main/lib/logging/AbstractLog.php b/app/main/lib/logging/AbstractLog.php
new file mode 100644
index 00000000..a772dd10
--- /dev/null
+++ b/app/main/lib/logging/AbstractLog.php
@@ -0,0 +1,549 @@
+ check Monolog::HANDLER and Monolog::FORMATTER
+ * @var array
+ */
+ protected $handlerConfig = ['stream' => 'line'];
+
+ /**
+ * log Processors, array with either callable functions or Processor class with __invoce() method
+ * -> functions used to add "extra" data to a log
+ * @var array
+ */
+ protected $processorConfig = ['psr' => null];
+
+ /**
+ * some handler need individual configuration parameters
+ * -> see $handlerConfig end getHandlerParams()
+ * @var array
+ */
+ protected $handlerParamsConfig = [];
+
+ /**
+ * multiple Log() objects can be marked as "grouped"
+ * -> Logs with Slack Handler should be grouped by map (send multiple log data in once
+ * @var array
+ */
+ protected $handlerGroups = [];
+
+ /**
+ * @var string
+ */
+ protected $message = '';
+
+ /**
+ * @var string
+ */
+ protected $action = '';
+
+ /**
+ * @var string
+ */
+ protected $channelType = '';
+
+ /**
+ * log level from self::LEVEL
+ * -> private - use setLevel() to set
+ * @var string
+ */
+ private $level = 'debug';
+
+ /**
+ * log tag from self::TAG
+ * -> private - use setTag() to set
+ * @var string
+ */
+ private $tag = 'default';
+
+ /**
+ * log data (main log data)
+ * @var array
+ */
+ private $data = [];
+
+ /**
+ * (optional) temp data for logger (will not be stored with the log entry)
+ * @var array
+ */
+ private $tmpData = [];
+
+ /**
+ * buffer multiple logs with the same chanelType and store all at once
+ * @var bool
+ */
+ private $buffer = true;
+
+
+ public function __construct(string $action){
+ $this->setF3();
+ $this->action = $action;
+
+ // add custom log processor callback -> add "extra" (meta) data
+ $f3 = $this->f3;
+ $processorExtraData = function($record) use(&$f3){
+ $record['extra'] = [
+ 'path' => $f3->get('PATH'),
+ 'ip' => $f3->get('IP')
+ ];
+ return $record;
+ };
+
+ // add log processor -> remove §tempData from log
+ $processorClearTempData = function($record){
+ $record['context'] = array_diff_key($record['context'], $this->getTempData());
+ return $record;
+ };
+
+ // init processorConfig. IMPORTANT: first processor gets executed at the end!
+ $this->processorConfig = ['cleaTempData' => $processorClearTempData] + [ 'addExtra' => $processorExtraData] + $this->processorConfig;
+ }
+
+ /**
+ * set $f3 base object
+ */
+ public function setF3(){
+ $this->f3 = \Base::instance();
+ }
+
+ /**
+ * @param $message
+ */
+ public function setMessage(string $message){
+ $this->message = $message;
+ }
+
+ /**
+ * @param string $level
+ * @throws \Exception
+ */
+ public function setLevel(string $level){
+ if( in_array($level, self::LEVEL)){
+ $this->level = $level;
+ }else{
+ throw new \Exception( sprintf(self::ERROR_LEVEL, $level));
+ }
+ }
+
+ /**
+ * @param string $tag
+ * @throws \Exception
+ */
+ public function setTag(string $tag){
+ if( in_array($tag, self::TAG)){
+ $this->tag = $tag;
+ }else{
+ throw new \Exception( sprintf(self::ERROR_TAG, $tag));
+ }
+ }
+
+ /**
+ * @param array $data
+ * @return LogInterface
+ */
+ public function setData(array $data): LogInterface{
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * @param array $data
+ * @return LogInterface
+ */
+ public function setTempData(array $data): LogInterface{
+ $this->tmpData = $data;
+ return $this;
+ }
+
+ /**
+ * add new Handler by $handlerKey
+ * set its default Formatter by $formatterKey
+ * @param string $handlerKey
+ * @param string|null $formatterKey
+ * @param \stdClass|null $handlerParams
+ * @return LogInterface
+ */
+ public function addHandler(string $handlerKey, string $formatterKey = null, \stdClass $handlerParams = null): LogInterface{
+ if(!$this->hasHandlerKey($handlerKey)){
+ $this->handlerConfig[$handlerKey] = $formatterKey;
+ // add more configuration params for the new handler
+ if(!is_null($handlerParams)){
+ $this->handlerParamsConfig[$handlerKey] = $handlerParams;
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * add new handler for Log() grouping
+ * @param string $handlerKey
+ * @return LogInterface
+ */
+ public function addHandlerGroup(string $handlerKey): LogInterface{
+ if(
+ $this->hasHandlerKey($handlerKey) &&
+ !$this->hasHandlerGroupKey($handlerKey)
+ ){
+ $this->handlerGroups[] = $handlerKey;
+ }
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getHandlerConfig(): array{
+ return $this->handlerConfig;
+ }
+
+ /**
+ * get __construct() parameters for a given $handlerKey
+ * @param string $handlerKey
+ * @return array
+ * @throws \Exception
+ */
+ public function getHandlerParams(string $handlerKey): array{
+ $params = [];
+
+ if($this->hasHandlerKey($handlerKey)){
+ switch($handlerKey){
+ case 'stream': $params = $this->getHandlerParamsStream();
+ break;
+ case 'zmq': $params = $this->getHandlerParamsZMQ();
+ break;
+ case 'mail': $params = $this->getHandlerParamsMail();
+ break;
+ case 'slackMap':
+ case 'slackRally':
+ $params = $this->getHandlerParamsSlack($handlerKey);
+ break;
+ default:
+ throw new \Exception( sprintf(self::ERROR_HANDLER_PARAMS, $handlerKey));
+ }
+ }else{
+ throw new \Exception( sprintf(self::ERROR_HANDLER_KEY, $handlerKey, implode(', ', array_flip($this->handlerConfig))));
+ }
+
+ return $params;
+ }
+
+ /**
+ * @return array
+ */
+ public function getHandlerParamsConfig(): array{
+ return $this->handlerParamsConfig;
+ }
+
+ /**
+ * @return array
+ */
+ public function getProcessorConfig(): array{
+ return $this->processorConfig;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMessage(): string{
+ return $this->message;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAction(): string{
+ return $this->action;
+ }
+
+ /**
+ * @return string
+ */
+ public function getChannelType(): string{
+ return $this->channelType;
+ }
+
+ /**
+ * @return string
+ */
+ public function getChannelName(): string{
+ return $this->getChannelType();
+ }
+
+ /**
+ * @return string
+ */
+ public function getLevel(): string{
+ return $this->level;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTag(): string{
+ return $this->tag;
+ }
+
+ /**
+ * @return array
+ */
+ public function getData(): array{
+ return $this->data;
+ }
+ /**
+ * @return array
+ */
+ public function getContext(): array{
+ $context = [
+ 'data' => $this->getData(),
+ 'tag' => $this->getTag()
+ ];
+
+ // add temp data (e.g. used for $message placeholder replacement
+ $context += $this->getTempData();
+
+ return $context;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getTempData(): array {
+ return $this->tmpData;
+ }
+
+ /**
+ * @return array
+ */
+ public function getHandlerGroups(): array{
+ return $this->handlerGroups;
+ }
+
+ /**
+ * get unique hash for this kind of logs (channel) and same $handlerGroups
+ * @return string
+ */
+ public function getGroupHash(): string {
+ $groupName = $this->getChannelName();
+ if($this->isGrouped()){
+ $groupName .= '_' . implode('_', $this->getHandlerGroups());
+ }
+
+ return $this->f3->hash($groupName);
+ }
+
+ /**
+ * @param string $handlerKey
+ * @return bool
+ */
+ public function hasHandlerKey(string $handlerKey): bool{
+ return array_key_exists($handlerKey, $this->handlerConfig);
+ }
+
+ /**
+ * @param string $handlerKey
+ * @return bool
+ */
+ public function hasHandlerGroupKey(string $handlerKey): bool{
+ return in_array($handlerKey, $this->getHandlerGroups());
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasBuffer(): bool{
+ return $this->buffer;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isGrouped(): bool{
+ return !empty($this->getHandlerGroups());
+ }
+
+ /**
+ * remove all group handlers and their config params
+ */
+ public function removeHandlerGroups(){
+ foreach($this->getHandlerGroups() as $handlerKey){
+ $this->removeHandlerGroup($handlerKey);
+ }
+ }
+
+ /**
+ * @param string $handlerKey
+ */
+ public function removeHandlerGroup(string $handlerKey){
+ unset($this->handlerConfig[$handlerKey]);
+ unset($this->handlerParamsConfig[$handlerKey]);
+ }
+
+ // Handler parameters for Monolog\Handler\AbstractHandler ---------------------------------------------------------
+ protected function getHandlerParamsStream(): array{
+ $params = [];
+ if( !empty($conf = $this->handlerParamsConfig['stream']) ){
+ $params[] = $conf->stream;
+ }
+
+ return $params;
+ }
+
+ /**
+ * get __construct() parameters for ZMQHandler() call
+ * @return array
+ */
+ protected function getHandlerParamsZMQ(): array {
+ $params = [];
+ if( !empty($conf = $this->handlerParamsConfig['zmq']) ){
+ // meta data (required by receiver socket)
+ $meta = [
+ 'logType' => 'mapLog',
+ 'stream'=> $conf->streamConf->stream
+ ];
+
+ $context = new \ZMQContext();
+ $pusher = $context->getSocket(\ZMQ::SOCKET_PUSH);
+ $pusher->connect($conf->uri);
+
+ $params[] = $pusher;
+ $params[] = \ZMQ::MODE_DONTWAIT;
+ $params[] = false; // multipart
+ $params[] = Logger::toMonologLevel($this->getLevel()); // min level that is handled
+ $params[] = true; // bubble
+ $params[] = $meta;
+ }
+
+ return $params;
+ }
+
+ /**
+ * get __construct() parameters for SwiftMailerHandler() call
+ * @return array
+ */
+ protected function getHandlerParamsMail(): array{
+ $params = [];
+ if( !empty($conf = $this->handlerParamsConfig['mail']) ){
+ $transport = (new \Swift_SmtpTransport())
+ ->setHost($conf->host)
+ ->setPort($conf->port)
+ ->setEncryption($conf->scheme)
+ ->setUsername($conf->username)
+ ->setPassword($conf->password)
+ ->setStreamOptions([
+ 'ssl' => [
+ 'allow_self_signed' => true,
+ 'verify_peer' => false
+ ]
+ ]);
+
+ $mailer = new \Swift_Mailer($transport);
+
+ // callback function used instead of Swift_Message() object
+ // -> we want the formatted/replaced message as subject
+ $messageCallback = function($content, $records) use ($conf){
+ $subject = 'No Subject';
+ if(!empty($records)){
+ // build subject from first record -> remove "markdown"
+ $subject = str_replace(['*', '_'], '', $records[0]['message']);
+ }
+
+ $jsonData = @json_encode($records, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+
+
+ $message = (new \Swift_Message())
+ ->setSubject($subject)
+ ->addPart($jsonData)
+ ->setFrom($conf->from)
+ ->setTo($conf->to)
+ ->setContentType('text/html')
+ ->setCharset('utf-8')
+ ->setMaxLineLength(1000);
+
+ if($conf->addJson){
+ $jsonAttachment = (new \Swift_Attachment())
+ ->setFilename('data.json')
+ ->setContentType('application/json')
+ ->setBody($jsonData);
+ $message->attach($jsonAttachment);
+ }
+
+ return $message;
+ };
+
+ $params[] = $mailer;
+ $params[] = $messageCallback;
+ $params[] = Logger::toMonologLevel($this->getLevel()); // min level that is handled
+ $params[] = true; // bubble
+ }
+
+ return $params;
+ }
+
+ /**
+ * get __construct() params for SlackWebhookHandler() call
+ * @param string $handlerKey
+ * @return array
+ */
+ protected function getHandlerParamsSlack(string $handlerKey): array {
+ $params = [];
+ if( !empty($conf = $this->handlerParamsConfig[$handlerKey]) ){
+ $params[] = $conf->slackWebHookURL;
+ $params[] = $conf->slackChannel;
+ $params[] = $conf->slackUsername;
+ $params[] = true; // $useAttachment
+ $params[] = $conf->slackIcon;
+ $params[] = true; // $includeContext
+ $params[] = false; // $includeExtra
+ $params[] = Logger::toMonologLevel($this->getLevel()); // min level that is handled
+ $params[] = true; // $bubble
+ //$params[] = ['extra', 'context.tag']; // $excludeFields
+ $params[] = []; // $excludeFields
+ }
+
+ return $params;
+ }
+
+ /**
+ * send this Log to global log buffer storage
+ */
+ public function buffer(){
+ Monolog::instance()->push($this);
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/main/lib/logging/DefaultLog.php b/app/main/lib/logging/DefaultLog.php
new file mode 100644
index 00000000..5b0fb122
--- /dev/null
+++ b/app/main/lib/logging/DefaultLog.php
@@ -0,0 +1,19 @@
+ no default is set
+ * @var array
+ */
+ protected $handlerConfig = [];
+
+ /**
+ * processors for this collection
+ * -> no default is set
+ * @var array
+ */
+ protected $processorConfig = [];
+
+ /**
+ * @var null|\SplObjectStorage
+ */
+ private $collection = null;
+
+ public function __construct(string $action){
+ parent::__construct($action);
+
+ $this->collection = new \SplObjectStorage();
+ }
+
+ /**
+ * get first Log from Collection
+ * @return AbstractLog
+ * @throws \Exception
+ */
+ protected function getPrimaryLog(): AbstractLog{
+ $this->collection->rewind();
+ if($this->collection->valid()){
+ /**
+ * @var $log AbstractLog
+ */
+ $log = $this->collection->current();
+ }else{
+ throw new \Exception( self::ERROR_EMPTY);
+ }
+
+ return $log;
+ }
+
+ /**
+ * add a new log object to this collection
+ * @param AbstractLog $log
+ */
+ public function addLog(AbstractLog $log){
+ if(!$this->collection->contains($log)){
+ if(!$this->collection->count()){
+ // first log sets the default for this collection
+ $this->channelType = $log->getChannelType();
+
+ // get relevant handlerKeys for this collection
+ $handlerGroups = array_flip($log->getHandlerGroups());
+
+ // remove handlers that are not relevant for this collection
+ $handlerConfig = $log->getHandlerConfig();
+ $handlerConfigGroup = array_intersect_key($handlerConfig, $handlerGroups);
+
+ // remove handlersParams that are not relevant for this collection
+ $handlerParamsConfig = $log->getHandlerParamsConfig();
+ $handlerParamsConfigGroup = array_intersect_key($handlerParamsConfig, $handlerGroups);
+
+ // add all handlers that are relevant for this collection
+ foreach($handlerConfigGroup as $handlerKey => $formatterKey){
+ $handlerParams = array_key_exists($handlerKey, $handlerParamsConfigGroup) ? $handlerParamsConfigGroup[$handlerKey] : null;
+ $this->addHandler($handlerKey, $formatterKey, $handlerParams);
+ }
+
+ // add processors for this collection
+ $this->processorConfig = $log->getProcessorConfig();
+ }
+
+ $this->setMessage($log->getMessage());
+ $this->setTag($log->getTag());
+
+ $this->collection->attach($log);
+ }
+ }
+
+ /**
+ * @param string $message
+ */
+ public function setMessage(string $message){
+ $currentMessage = parent::getMessage();
+ if(empty($currentMessage)){
+ $newMessage = $message;
+ }elseif($message !== $currentMessage){
+ $newMessage = 'multi changes';
+ }else{
+ $newMessage = $currentMessage ;
+ }
+
+ parent::setMessage($newMessage);
+ }
+
+ /**
+ * @param string $tag
+ */
+ public function setTag(string $tag){
+ $currentTag = parent::getTag();
+ switch($currentTag){
+ case 'default':
+ // no specific tag set so far... set new
+ $newTag = $tag; break;
+ case 'information':
+ // do not change "information" tag (mixed tag logs in this collection)
+ $newTag = $currentTag; break;
+ default:
+ // set mixed tag -> "information"
+ $newTag = ($tag !== $currentTag) ? 'information': $tag;
+ }
+
+ parent::setTag($newTag);
+ }
+
+ /**
+ * get log data for all logs in this collection
+ * @return array
+ */
+ public function getData() : array{
+ $this->collection->rewind();
+ $data = [];
+ while($this->collection->valid()){
+ $data[] = $this->collection->current()->getData();
+ $this->collection->next();
+ }
+ return $data;
+ }
+
+ /**
+ * @return string
+ */
+ public function getChannelName() : string{
+ return $this->getPrimaryLog()->getChannelName();
+ }
+
+ /**
+ * @return string
+ */
+ public function getLevel() : string{
+ return $this->getPrimaryLog()->getLevel();
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasBuffer() : bool{
+ return $this->getPrimaryLog()->hasBuffer();
+ }
+
+ /**
+ * @return array
+ */
+ public function getTempData() : array{
+ return $this->getPrimaryLog()->getTempData();
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/app/main/lib/logging/LogInterface.php b/app/main/lib/logging/LogInterface.php
new file mode 100644
index 00000000..3845b892
--- /dev/null
+++ b/app/main/lib/logging/LogInterface.php
@@ -0,0 +1,64 @@
+ final handler will be set dynamic for per instance
+ * @var array
+ */
+ protected $handlerConfig = [
+ //'stream' => 'json',
+ //'zmq' => 'json',
+ //'slackMap' => 'json'
+ ];
+
+ /**
+ * @var string
+ */
+ protected $channelType = 'map';
+
+ /**
+ * @var bool
+ */
+ protected $logActivity = false;
+
+
+ public function __construct(string $action, array $objectData){
+ parent::__construct($action, $objectData);
+
+ $this->setLevel('info');
+ $this->setTag($this->getTagFromAction());
+ }
+
+ /**
+ * get log tag depending on log action
+ * @return string
+ */
+ public function getTagFromAction(){
+ $tag = parent::getTag();
+ $actionParts = $this->getActionParts();
+ switch($actionParts[1]){
+ case 'create': $tag = 'success'; break;
+ case 'update': $tag = 'warning'; break;
+ case 'delete': $tag = 'danger'; break;
+ }
+
+ return $tag;
+ }
+
+ /**
+ * @return string
+ */
+ public function getChannelName(): string{
+ return $this->getChannelType() . '_' . $this->getChannelId();
+ }
+
+ /**
+ * @return string
+ */
+ public function getMessage() : string{
+ return $this->getActionParts()[0] . " '{objName}'";
+ }
+
+ /**
+ * @return array
+ */
+ public function getData() : array{
+ $data = parent::getData();
+
+ // add system, connection, signature data -------------------------------------------------
+ if(!empty($tempLogData = $this->getTempData())){
+ $objectData['object'] = $tempLogData;
+ $data = $objectData + $data;
+ }
+
+ // add human readable changes to string ---------------------------------------------------
+ $data['formatted'] = $this->formatData($data);
+
+ return $data;
+ }
+
+ /**
+ * @param array $data
+ * @return string
+ */
+ protected function formatData(array $data): string{
+ $actionParts = $this->getActionParts();
+ $objectString = !empty($data['object']) ? "'" . $data['object']['objName'] . "'" . ' #' . $data['object']['objId'] : '';
+ $string = ucfirst($actionParts[1]) . 'd ' . $actionParts[0] . " " . $objectString;
+
+ // format changed columns (recursive) ---------------------------------------------
+ switch($actionParts[1]){
+ case 'create':
+ case 'update':
+ $formatChanges = function(array $changes) use ( &$formatChanges ): string{
+ $string = '';
+ foreach($changes as $field => $value){
+ if(is_array($value)){
+ $string .= $field . ": ";
+ $string .= $formatChanges($value);
+ $string .= next( $changes ) ? " , " : '';
+ }else{
+ if(is_numeric($value)){
+ $formattedValue = $value;
+ }elseif(is_null($value)){
+ $formattedValue = "NULL";
+ }elseif(empty($value)){
+ $formattedValue = "' '";
+ }elseif(is_string($value)){
+ $formattedValue = "'" . $value . "'";
+ }else{
+ $formattedValue = (string)$value;
+ }
+
+ $string .= $formattedValue;
+ if($field == 'old'){
+ $string .= " ➜ ";
+ }
+ }
+ }
+ return $string;
+ };
+
+ $string .= ' | ' . $formatChanges($data['main']);
+ break;
+ }
+
+ return $string;
+ }
+
+ /**
+ * split $action "CamelCase" wise
+ * @return array
+ */
+ protected function getActionParts(): array{
+ return array_map('strtolower', preg_split('/(?=[A-Z])/', $this->getAction()));
+ }
+
+ /**
+ * @param bool $logActivity
+ */
+ public function logActivity(bool $logActivity){
+ $this->logActivity = $logActivity;
+ }
+
+ public function buffer(){
+ parent::buffer();
+
+ if($this->logActivity){
+ // map logs should also used for "activity" logging
+ LogController::instance()->push($this);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/main/lib/logging/RallyLog.php b/app/main/lib/logging/RallyLog.php
new file mode 100644
index 00000000..ea52e9c8
--- /dev/null
+++ b/app/main/lib/logging/RallyLog.php
@@ -0,0 +1,105 @@
+ final handler will be set dynamic for per instance
+ * @var array
+ */
+ protected $handlerConfig = [
+ // 'slackRally' => 'json',
+ // 'mail' => 'html'
+ ];
+
+ /**
+ * @var string
+ */
+ protected $channelType = 'rally';
+
+
+ public function __construct(string $action, array $objectData){
+ parent::__construct($action, $objectData);
+
+ $this->setLevel('notice');
+ $this->setTag('information');
+ }
+
+ /**
+ * @return string
+ */
+ protected function getThumbUrl() : string{
+ $url = '';
+ if(is_object($character = $this->getCharacter())){
+ $characterLog = $character->getLog();
+ if($characterLog && !empty($characterLog->shipTypeId)){
+ $url = Config::getPathfinderData('api.ccp_image_server') . '/Render/' . $characterLog->shipTypeId . '_64.png';
+ }else{
+ $url = parent::getThumbUrl();
+ }
+ }
+
+ return $url;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMessage() : string{
+ return "*New RallyPoint system '{objName}'* _#{objId}_ *map '{channelName}'* _#{channelId}_ ";
+ }
+
+ /**
+ * @return array
+ */
+ public function getData() : array{
+ $data = parent::getData();
+
+ // add system -----------------------------------------------------------------------------
+ if(!empty($tempLogData = $this->getTempData())){
+ $objectData['object'] = $tempLogData;
+ $data = $objectData + $data;
+ }
+
+ // add human readable changes to string ---------------------------------------------------
+ $data['formatted'] =$this->formatData($data);
+
+ return $data;
+ }
+
+ /**
+ * @param array $data
+ * @return string
+ */
+ protected function formatData(array $data): string{
+ $string = '';
+
+ if(
+ !empty($data['object']) &&
+ !empty($data['channel'])
+ ){
+ $replace = [
+ '{objName}' => $data['object']['objName'],
+ '{objId}' => $data['object']['objId'],
+ '{channelName}' => $data['channel']['channelName'],
+ '{channelId}' => $data['channel']['channelId']
+ ];
+ $string = str_replace(array_keys($replace), array_values($replace), $this->getMessage());
+ }
+
+ return $string;
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/app/main/lib/logging/UserLog.php b/app/main/lib/logging/UserLog.php
new file mode 100644
index 00000000..3c6e8ba7
--- /dev/null
+++ b/app/main/lib/logging/UserLog.php
@@ -0,0 +1,36 @@
+ final handler will be set dynamic for per instance
+ * @var array
+ */
+ protected $handlerConfig = [
+ // 'mail' => 'html'
+ ];
+
+ /**
+ * @var string
+ */
+ protected $channelType = 'user';
+
+ public function __construct(string $action, array $objectData){
+ parent::__construct($action, $objectData);
+
+ $this->setLevel('notice');
+ $this->setTag('information');
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/main/lib/logging/formatter/MailFormatter.php b/app/main/lib/logging/formatter/MailFormatter.php
new file mode 100644
index 00000000..cc980035
--- /dev/null
+++ b/app/main/lib/logging/formatter/MailFormatter.php
@@ -0,0 +1,46 @@
+ $record['message'],
+ 'tplGreeting' => \Markdown::instance()->convert(str_replace('*', '', $record['message'])),
+ 'message' => false,
+ 'tplText2' => false,
+ 'tplClosing' => 'Fly save!',
+ 'actionPrimary' => false,
+ 'appName' => Config::getPathfinderData('name'),
+ 'appUrl' => Config::getEnvironmentData('URL'),
+ 'appHost' => $_SERVER['HTTP_HOST'],
+ 'appContact' => Config::getPathfinderData('contact'),
+ 'appMail' => Config::getPathfinderData('email'),
+ ];
+
+ $tplData = array_replace_recursive($tplDefaultData, (array)$record['context']['data']['main']);
+
+ return \Template::instance()->render('templates/mail/basic_inline.html', 'text/html', $tplData);
+ }
+
+ public function formatBatch(array $records){
+ $message = '';
+ foreach ($records as $key => $record) {
+ $message .= $this->format($record);
+ }
+
+ return $message;
+ }
+
+}
\ No newline at end of file
diff --git a/app/main/lib/logging/handler/AbstractSlackWebhookHandler.php b/app/main/lib/logging/handler/AbstractSlackWebhookHandler.php
new file mode 100644
index 00000000..ab774ea8
--- /dev/null
+++ b/app/main/lib/logging/handler/AbstractSlackWebhookHandler.php
@@ -0,0 +1,260 @@
+webhookUrl = $webhookUrl;
+ $this->channel = $channel;
+ $this->username = $username;
+ $this->userIcon = trim($iconEmoji, ':');
+ $this->useAttachment = $useAttachment;
+ $this->includeContext = $includeContext;
+ $this->includeExtra = $includeExtra;
+ $this->excludeFields = $excludeFields;
+
+ parent::__construct($level, $bubble);
+
+ }
+
+ /**
+ * format
+ * @param array $record
+ * @return array
+ */
+ protected function getSlackData(array $record): array {
+ $postData = [];
+
+ if ($this->username) {
+ $postData['username'] = $this->username;
+ }
+
+ if ($this->channel) {
+ $postData['channel'] = $this->channel;
+ }
+
+ $postData['text'] = (string)$record['message'];
+
+ if ($this->userIcon) {
+ if (filter_var($this->userIcon, FILTER_VALIDATE_URL)) {
+ $postData['icon_url'] = $this->userIcon;
+ } else {
+ $postData['icon_emoji'] = ":{$this->userIcon}:";
+ }
+ }
+
+ return $postData;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param array $record
+ */
+ protected function write(array $record){
+ $record = $this->excludeFields($record);
+
+ $postData = $this->getSlackData($record);
+
+ $postData = $this->cleanAttachments($postData);
+
+ $postString = json_encode($postData);
+
+ $ch = curl_init();
+ $options = [
+ CURLOPT_URL => $this->webhookUrl,
+ CURLOPT_CUSTOMREQUEST => 'POST',
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_HTTPHEADER => ['Content-type: application/json'],
+ CURLOPT_POSTFIELDS => $postString
+ ];
+ if (defined('CURLOPT_SAFE_UPLOAD')) {
+ $options[CURLOPT_SAFE_UPLOAD] = true;
+ }
+
+ curl_setopt_array($ch, $options);
+
+ Handler\Curl\Util::execute($ch);
+ }
+
+ /**
+ * @param array $postData
+ * @return array
+ */
+ protected function cleanAttachments(array $postData): array{
+ $attachmentCount = count($postData['attachments']);
+ if( $attachmentCount > $this->maxAttachments){
+ $text = 'To many attachments! ' . ($attachmentCount - $this->maxAttachments) . ' of ' . $attachmentCount . ' attachments not visible';
+ $postData['attachments'] = array_slice($postData['attachments'], 0, $this->maxAttachments);
+
+ $attachment = [
+ 'title' => $text,
+ 'fallback' => $text,
+ 'color' => $this->getAttachmentColor('information')
+ ];
+
+ $postData['attachments'][] = $attachment;
+ }
+
+ return $postData;
+ }
+
+ /**
+ * @param array $attachment
+ * @param array $characterData
+ * @return array
+ */
+ protected function setAuthor(array $attachment, array $characterData): array {
+ if( !empty($characterData['id']) && !empty($characterData['name'])){
+ $attachment['author_name'] = $characterData['name'] . ' #' . $characterData['id'];
+ $attachment['author_link'] = Config::getPathfinderData('api.z_killboard') . '/character/' . $characterData['id'] . '/';
+ $attachment['author_icon'] = Config::getPathfinderData('api.ccp_image_server') . '/Character/' . $characterData['id'] . '_32.jpg';
+ }
+
+ return $attachment;
+ }
+
+ /**
+ * @param array $attachment
+ * @param array $thumbData
+ * @return array
+ */
+ protected function setThumb(array $attachment, array $thumbData): array {
+ if( !empty($thumbData['url'])) {
+ $attachment['thumb_url'] = $thumbData['url'];
+ }
+
+ return $attachment;
+ }
+
+ /**
+ * @param $title
+ * @param $value
+ * @param bool $format
+ * @param bool $short
+ * @return array
+ */
+ protected function generateAttachmentField($title, $value, $format = false, $short = true){
+ return [
+ 'title' => $title,
+ 'value' => !empty($value) ? ( $format ? sprintf('`%s`', $value) : $value ) : '',
+ 'short' => $short
+ ];
+ }
+
+ /**
+ * @param string $tag
+ * @return string
+ */
+ protected function getAttachmentColor(string $tag): string {
+ switch($tag){
+ case 'information': $color = '#428bca'; break;
+ case 'success': $color = '#4f9e4f'; break;
+ case 'warning': $color = '#e28a0d'; break;
+ case 'danger': $color = '#a52521'; break;
+ default: $color = '#313335'; break;
+ }
+ return $color;
+ }
+
+ /**
+ * Get a copy of record with fields excluded according to $this->excludeFields
+ * @param array $record
+ * @return array
+ */
+ private function excludeFields(array $record){
+ foreach($this->excludeFields as $field){
+ $keys = explode('.', $field);
+ $node = &$record;
+ $lastKey = end($keys);
+ foreach($keys as $key){
+ if(!isset($node[$key])){
+ break;
+ }
+ if($lastKey === $key){
+ unset($node[$key]);
+ break;
+ }
+ $node = &$node[$key];
+ }
+ }
+
+ return $record;
+ }
+}
\ No newline at end of file
diff --git a/app/main/lib/logging/handler/SlackMapWebhookHandler.php b/app/main/lib/logging/handler/SlackMapWebhookHandler.php
new file mode 100644
index 00000000..d942c754
--- /dev/null
+++ b/app/main/lib/logging/handler/SlackMapWebhookHandler.php
@@ -0,0 +1,101 @@
+getTimestamp();
+ $text = '';
+
+ if (
+ $this->useAttachment &&
+ !empty( $attachmentsData = $record['context']['data'])
+ ) {
+
+ // convert non grouped data (associative array) to multi dimensional (sequential) array
+ // -> see "group" records
+ $attachmentsData = Util::is_assoc($attachmentsData) ? [$attachmentsData] : $attachmentsData;
+
+ $thumbData = (array)$record['extra']['thumb'];
+
+ $postData['attachments'] = [];
+
+ foreach($attachmentsData as $attachmentData){
+ $channelData = (array)$attachmentData['channel'];
+ $characterData = (array)$attachmentData['character'];
+ $formatted = (string)$attachmentData['formatted'];
+
+ // get "message" from $formatted
+ $msgParts = explode('|', $formatted, 2);
+
+ // build main text from first Attachment (they belong to same channel)
+ if(!empty($channelData)){
+ $text = "*Map '" . $channelData['channelName'] . "'* _#" . $channelData['channelId'] . "_ *changed*";
+ }
+
+ $attachment = [
+ 'title' => !empty($msgParts[0]) ? $msgParts[0] : 'No Title',
+ //'pretext' => '',
+ 'text' => !empty($msgParts[1]) ? sprintf('```%s```', $msgParts[1]) : '',
+ 'fallback' => !empty($msgParts[1]) ? $msgParts[1] : 'No Fallback',
+ 'color' => $this->getAttachmentColor($tag),
+ 'fields' => [],
+ 'mrkdwn_in' => ['fields', 'text'],
+ 'footer' => 'Pathfinder API',
+ //'footer_icon'=> '',
+ 'ts' => $timestamp
+ ];
+
+ $attachment = $this->setAuthor($attachment, $characterData);
+ $attachment = $this->setThumb($attachment, $thumbData);
+
+
+ // set 'field' array ----------------------------------------------------------------------------------
+ if ($this->includeExtra) {
+ $attachment['fields'][] = $this->generateAttachmentField('', 'Meta data:', false, false);
+
+ if(!empty($record['extra']['path'])){
+ $attachment['fields'][] = $this->generateAttachmentField('Path', $record['extra']['path'], true);
+ }
+
+ if(!empty($tag)){
+ $attachment['fields'][] = $this->generateAttachmentField('Tag', $tag, true);
+ }
+
+ if(!empty($record['level_name'])){
+ $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name'], true);
+ }
+
+ if(!empty($record['extra']['ip'])){
+ $attachment['fields'][] = $this->generateAttachmentField('IP', $record['extra']['ip'], true);
+ }
+ }
+
+ $postData['attachments'][] = $attachment;
+ }
+ }
+
+ $postData['text'] = empty($text) ? $postData['text'] : $text;
+
+
+ return $postData;
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/main/lib/logging/handler/SlackRallyWebhookHandler.php b/app/main/lib/logging/handler/SlackRallyWebhookHandler.php
new file mode 100644
index 00000000..f85f120c
--- /dev/null
+++ b/app/main/lib/logging/handler/SlackRallyWebhookHandler.php
@@ -0,0 +1,135 @@
+getTimestamp();
+ $text = '';
+
+ if (
+ $this->useAttachment &&
+ !empty( $attachmentsData = $record['context']['data'])
+ ){
+ // convert non grouped data (associative array) to multi dimensional (sequential) array
+ // -> see "group" records
+ $attachmentsData = Util::is_assoc($attachmentsData) ? [$attachmentsData] : $attachmentsData;
+
+ $thumbData = (array)$record['extra']['thumb'];
+
+ $postData['attachments'] = [];
+
+ foreach($attachmentsData as $attachmentData){
+ $characterData = (array)$attachmentData['character'];
+
+ $text = 'No Title';
+ if( !empty($attachmentData['formatted']) ){
+ $text = $attachmentData['formatted'];
+ }
+
+ $attachment = [
+ 'title' => !empty($attachmentData['main']['message']) ? 'Message' : '',
+ //'pretext' => '',
+ 'text' => !empty($attachmentData['main']['message']) ? sprintf('```%s```', $attachmentData['main']['message']) : '',
+ 'fallback' => !empty($attachmentData['main']['message']) ? $attachmentData['main']['message'] : 'No Fallback',
+ 'color' => $this->getAttachmentColor($tag),
+ 'fields' => [],
+ 'mrkdwn_in' => ['fields', 'text'],
+ 'footer' => 'Pathfinder API',
+ //'footer_icon'=> '',
+ 'ts' => $timestamp
+ ];
+
+ $attachment = $this->setAuthor($attachment, $characterData);
+ $attachment = $this->setThumb($attachment, $thumbData);
+
+ // set 'field' array ----------------------------------------------------------------------------------
+ if ($this->includeContext) {
+ if(!empty($objectData = $attachmentData['object'])){
+ if(!empty($objectData['objAlias'])){
+ // System alias
+ $attachment['fields'][] = $this->generateAttachmentField('Alias', $objectData['objAlias']);
+ }
+
+ if(!empty($objectData['objName'])){
+ // System name
+ $attachment['fields'][] = $this->generateAttachmentField('System', $objectData['objName']);
+ }
+
+ if(!empty($objectData['objRegion'])){
+ // System region
+ $attachment['fields'][] = $this->generateAttachmentField('Region', $objectData['objRegion']);
+ }
+
+ if(isset($objectData['objIsWormhole'])){
+ // Is wormhole
+ $attachment['fields'][] = $this->generateAttachmentField('Wormhole', $objectData['objIsWormhole'] ? 'Yes' : 'No');
+ }
+
+ if(!empty($objectData['objSecurity'])){
+ // System security
+ $attachment['fields'][] = $this->generateAttachmentField('Security', $objectData['objSecurity']);
+ }
+
+ if(!empty($objectData['objEffect'])){
+ // System effect
+ $attachment['fields'][] = $this->generateAttachmentField('Effect', $objectData['objEffect']);
+ }
+
+ if(!empty($objectData['objTrueSec'])){
+ // System trueSec
+ $attachment['fields'][] = $this->generateAttachmentField('TrueSec', $objectData['objTrueSec']);
+ }
+
+ if(!empty($objectData['objDescription'])){
+ // System trueSec
+ $attachment['fields'][] = $this->generateAttachmentField('System description', '```' . $objectData['objDescription'] . '```', false, false);
+ }
+ }
+ }
+
+ if($this->includeExtra){
+ if(!empty($record['extra']['path'])){
+ $attachment['fields'][] = $this->generateAttachmentField('Path', $record['extra']['path'], true);
+ }
+
+ if(!empty($tag)){
+ $attachment['fields'][] = $this->generateAttachmentField('Tag', $tag, true);
+ }
+
+ if(!empty($record['level_name'])){
+ $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name'], true);
+ }
+
+ if(!empty($record['extra']['ip'])){
+ $attachment['fields'][] = $this->generateAttachmentField('IP', $record['extra']['ip'], true);
+ }
+ }
+
+ $postData['attachments'][] = $attachment;
+ }
+ }
+
+ $postData['text'] = empty($text) ? $postData['text'] : $text;
+
+ return $postData;
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/main/lib/logging/handler/ZMQHandler.php b/app/main/lib/logging/handler/ZMQHandler.php
new file mode 100644
index 00000000..f049414c
--- /dev/null
+++ b/app/main/lib/logging/handler/ZMQHandler.php
@@ -0,0 +1,65 @@
+metaData = $metaData;
+
+ parent::__construct($zmqSocket, $zmqMode, $multipart, $level, $bubble);
+ }
+
+ /**
+ * overwrite default handle()
+ * -> change data structure after processor() calls and before formatter() calls
+ * @param array $record
+ * @return bool
+ * @throws \Exception
+ */
+ public function handle(array $record){
+ if (!$this->isHandling($record)) {
+ return false;
+ }
+
+ $record = $this->processRecord($record);
+
+ $record = [
+ 'task' => 'logData',
+ 'load' => [
+ 'meta' => $this->metaData,
+ 'log' => $record
+ ]
+ ];
+
+ $record['formatted'] = $this->getFormatter()->format($record);
+
+ $this->write($record);
+
+ return false === $this->bubble;
+ }
+
+}
\ No newline at end of file
diff --git a/app/main/lib/socket.php b/app/main/lib/socket.php
index fb96c108..98cc7e8e 100644
--- a/app/main/lib/socket.php
+++ b/app/main/lib/socket.php
@@ -79,27 +79,19 @@ class Socket {
/**
* init new socket
*/
- /*
public function initSocket(){
- if(self::checkRequirements()){
- $context = new \ZMQContext();
- $this->socket = $context->getSocket(\ZMQ::SOCKET_REQ);
- // The linger value of the socket. Specifies how long the socket blocks trying flush messages after it has been closed
- $this->socket->setSockOpt(\ZMQ::SOCKOPT_LINGER, 0);
- }
- } */
-
- /**
- * init new socket
- */
- public function initSocket(){
- if(self::checkRequirements()){
+ if(Config::checkSocketRequirements()){
$context = new \ZMQContext();
$this->socket = $context->getSocket(\ZMQ::SOCKET_PUSH);
}
}
- public function sendData($task, $load = ''){
+ /**
+ * @param $task
+ * @param string $load
+ * @return bool|string
+ */
+ public function sendData(string $task, $load = ''){
$response = false;
$this->initSocket();
@@ -122,7 +114,7 @@ class Socket {
//$this->socket->send(json_encode($send), \ZMQ::MODE_DONTWAIT);
$this->socket->send(json_encode($send));
- $this->socket->disconnect($this->socketUri);
+ // $this->socket->disconnect($this->socketUri);
$response = 'OK';
@@ -230,24 +222,5 @@ class Socket {
return $response;
}*/
- /**
- * check whether this installation fulfills all requirements
- * -> check for ZMQ PHP extension and installed ZQM version
- * -> this does NOT check versions! -> those can be verified on /setup page
- * @return bool
- */
- static function checkRequirements(){
- $check = false;
-
- if(
- extension_loaded('zmq') &&
- class_exists('ZMQ')
- ){
- $check = true;
- }
-
- return $check;
- }
-
}
\ No newline at end of file
diff --git a/app/main/lib/util.php b/app/main/lib/util.php
index 1001cf99..cf4e2c19 100644
--- a/app/main/lib/util.php
+++ b/app/main/lib/util.php
@@ -17,11 +17,32 @@ class Util {
* @return array
*/
static function arrayChangeKeyCaseRecursive($arr, $case = CASE_LOWER){
- return array_map( function($item){
- if( is_array($item) )
- $item = self::arrayChangeKeyCaseRecursive($item);
- return $item;
- }, array_change_key_case($arr, $case));
+ if(is_array($arr)){
+ $arr = array_map( function($item){
+ if( is_array($item) )
+ $item = self::arrayChangeKeyCaseRecursive($item);
+ return $item;
+ }, array_change_key_case((array)$arr, $case));
+ }
+
+ return $arr;
+ }
+
+ /**
+ * checks whether an array is associative or not (sequential)
+ * @param mixed $array
+ * @return bool
+ */
+ static function is_assoc($array): bool {
+ $isAssoc = false;
+ if(
+ is_array($array) &&
+ array_keys($array) !== range(0, count($array) - 1)
+ ){
+ $isAssoc = true;
+ }
+
+ return $isAssoc;
}
/**
@@ -59,6 +80,23 @@ class Util {
return $scopes;
}
+ /**
+ * obsucre string e.g. password (hide last characters)
+ * @param string $string
+ * @param int $maxHideChars
+ * @return string
+ */
+ static function obscureString(string $string, int $maxHideChars = 10): string {
+ $formatted = '';
+ $length = mb_strlen((string)$string);
+ if($length > 0){
+ $hideChars = ($length < $maxHideChars) ? $length : $maxHideChars;
+ $formatted = substr_replace($string, str_repeat('_', min(3, $length)), -$hideChars) .
+ ' [' . $length . ']';
+ }
+ return $formatted;
+ }
+
/**
* get hash from an array of ESI scopes
* @param array $scopes
diff --git a/app/main/model/abstractmaptrackingmodel.php b/app/main/model/abstractmaptrackingmodel.php
new file mode 100644
index 00000000..5098eef4
--- /dev/null
+++ b/app/main/model/abstractmaptrackingmodel.php
@@ -0,0 +1,139 @@
+ [
+ 'type' => Schema::DT_INT,
+ 'index' => true,
+ 'belongs-to-one' => 'Model\CharacterModel',
+ 'constraint' => [
+ [
+ 'table' => 'character',
+ 'on-delete' => 'CASCADE'
+ ]
+ ],
+ 'validate' => 'validate_notDry'
+ ],
+ 'updatedCharacterId' => [
+ 'type' => Schema::DT_INT,
+ 'index' => true,
+ 'belongs-to-one' => 'Model\CharacterModel',
+ 'constraint' => [
+ [
+ 'table' => 'character',
+ 'on-delete' => 'CASCADE'
+ ]
+ ],
+ 'validate' => 'validate_notDry'
+ ]
+ ];
+
+ /**
+ * get static character fields for this model instance
+ * @return array
+ */
+ protected function getStaticFieldConf(): array{
+ return array_merge(parent::getStaticFieldConf(), $this->trackingFieldConf);
+ }
+
+ /**
+ * validates a model field to be a valid relational model
+ * @param $key
+ * @param $val
+ * @return bool
+ */
+ protected function validate_notDry($key, $val): bool {
+ $valid = true;
+ if($colConf = $this->fieldConf[$key]){
+ if(isset($colConf['belongs-to-one'])){
+ if( (is_int($val) || ctype_digit($val)) && (int)$val > 0){
+ $valid = true;
+ }elseif( is_a($val, $colConf['belongs-to-one']) && !$val->dry() ){
+ $valid = true;
+ }else{
+ $valid = false;
+ $msg = 'Validation failed: "' . get_class($this) . '->' . $key . '" must be a valid instance of ' . $colConf['belongs-to-one'];
+ $this->throwValidationException($key, $msg);
+ }
+ }
+ }
+
+ return $valid;
+ }
+
+ /**
+ * log character activity create/update/delete events
+ * @param string $action
+ */
+ protected function logActivity($action){
+ // check if activity logging is enabled for this object
+ if($this->enableActivityLogging){
+ // check for field changes
+ if(
+ mb_stripos(mb_strtolower($action), 'delete') !== false ||
+ !empty($this->fieldChanges)
+ ){
+ $this->newLog($action)->setCharacter($this->updatedCharacterId)->setData($this->fieldChanges)->buffer();
+ }
+ }
+ }
+
+ /**
+ * validates all required columns of this class
+ * @return bool
+ * @throws \Exception\ValidationException
+ */
+ public function isValid(): bool {
+ if($valid = parent::isValid()){
+ foreach($this->trackingFieldConf as $key => $colConf){
+ if($this->exists($key)){
+ $valid = $this->validateField($key, $this->$key);
+ if(!$valid){
+ break;
+ }
+ }else{
+ $valid = false;
+ $this->throwDbException('Missing table column "' . $this->getTable(). '.' . $key . '"');
+ break;
+ }
+ }
+ }
+
+ return $valid;
+ }
+
+ /**
+ * get log file data
+ * @return array
+ */
+ public function getLogData(): array {
+ return [];
+ }
+
+
+ /**
+ * save connection
+ * @param CharacterModel $characterModel
+ * @return ConnectionModel|false
+ */
+ public function save(CharacterModel $characterModel = null){
+ if($this->dry()){
+ $this->createdCharacterId = $characterModel;
+ }
+ $this->updatedCharacterId = $characterModel;
+
+ return parent::save();
+ }
+
+}
\ No newline at end of file
diff --git a/app/main/model/activitylogmodel.php b/app/main/model/activitylogmodel.php
index 19a32b79..bcee4e71 100644
--- a/app/main/model/activitylogmodel.php
+++ b/app/main/model/activitylogmodel.php
@@ -44,22 +44,46 @@ class ActivityLogModel extends BasicModel {
]
],
+ // map actions -----------------------------------------------------
+
+ 'mapCreate' => [
+ 'type' => Schema::DT_SMALLINT,
+ 'nullable' => false,
+ 'default' => 0,
+ 'counter' => true
+ ],
+ 'mapUpdate' => [
+ 'type' => Schema::DT_SMALLINT,
+ 'nullable' => false,
+ 'default' => 0,
+ 'counter' => true
+ ],
+ 'mapDelete' => [
+ 'type' => Schema::DT_SMALLINT,
+ 'nullable' => false,
+ 'default' => 0,
+ 'counter' => true
+ ],
+
// system actions -----------------------------------------------------
'systemCreate' => [
'type' => Schema::DT_SMALLINT,
'nullable' => false,
'default' => 0,
+ 'counter' => true
],
'systemUpdate' => [
'type' => Schema::DT_SMALLINT,
'nullable' => false,
'default' => 0,
+ 'counter' => true
],
'systemDelete' => [
'type' => Schema::DT_SMALLINT,
'nullable' => false,
'default' => 0,
+ 'counter' => true
],
// connection actions -------------------------------------------------
@@ -68,16 +92,19 @@ class ActivityLogModel extends BasicModel {
'type' => Schema::DT_SMALLINT,
'nullable' => false,
'default' => 0,
+ 'counter' => true
],
'connectionUpdate' => [
'type' => Schema::DT_SMALLINT,
'nullable' => false,
'default' => 0,
+ 'counter' => true
],
'connectionDelete' => [
'type' => Schema::DT_SMALLINT,
'nullable' => false,
'default' => 0,
+ 'counter' => true
],
// signature actions -------------------------------------------------
@@ -86,16 +113,19 @@ class ActivityLogModel extends BasicModel {
'type' => Schema::DT_SMALLINT,
'nullable' => false,
'default' => 0,
+ 'counter' => true
],
'signatureUpdate' => [
'type' => Schema::DT_SMALLINT,
'nullable' => false,
'default' => 0,
+ 'counter' => true
],
'signatureDelete' => [
'type' => Schema::DT_SMALLINT,
'nullable' => false,
'default' => 0,
+ 'counter' => true
],
];
@@ -128,6 +158,20 @@ class ActivityLogModel extends BasicModel {
}
}
+ /**
+ * get all table columns that are used as "counter" columns
+ * @return array
+ */
+ public function getCountableColumnNames(): array {
+ $fieldConf = $this->getFieldConfiguration();
+
+ $filterCounterColumns = function($key, $value){
+ return isset($value['counter']) ? $key : false;
+ };
+
+ return array_values(array_filter(array_map($filterCounterColumns, array_keys($fieldConf), $fieldConf)));
+ }
+
/**
* overwrites parent
* @param null $db
diff --git a/app/main/model/alliancemodel.php b/app/main/model/alliancemodel.php
index d5bca132..23bcb703 100644
--- a/app/main/model/alliancemodel.php
+++ b/app/main/model/alliancemodel.php
@@ -9,6 +9,7 @@
namespace Model;
use DB\SQL\Schema;
+use lib\Config;
class AllianceModel extends BasicModel {
@@ -60,8 +61,6 @@ class AllianceModel extends BasicModel {
public function getMaps(){
$maps = [];
- $f3 = self::getF3();
-
$this->filter('mapAlliances',
['active = ?', 1],
['order' => 'created']
@@ -72,7 +71,7 @@ class AllianceModel extends BasicModel {
foreach($this->mapAlliances as $mapAlliance){
if(
$mapAlliance->mapId->isActive() &&
- $mapCount < $f3->get('PATHFINDER.MAP.ALLIANCE.MAX_COUNT')
+ $mapCount < Config::getMapsDefaultConfig('alliance')['max_count']
){
$maps[] = $mapAlliance->mapId;
$mapCount++;
diff --git a/app/main/model/basicmodel.php b/app/main/model/basicmodel.php
index 272080f7..bb0e7e96 100644
--- a/app/main/model/basicmodel.php
+++ b/app/main/model/basicmodel.php
@@ -9,9 +9,11 @@
namespace Model;
use DB\SQL\Schema;
-use Exception;
use Controller;
use DB;
+use Lib\Logging;
+use Exception\ValidationException;
+use Exception\DatabaseException;
abstract class BasicModel extends \DB\Cortex {
@@ -19,7 +21,7 @@ abstract class BasicModel extends \DB\Cortex {
* Hive key with DB object
* @var string
*/
- protected $db = 'DB_PF';
+ protected $db = 'DB_PF';
/**
* caching time of field schema - seconds
@@ -27,26 +29,20 @@ abstract class BasicModel extends \DB\Cortex {
* -> leave this at a higher value
* @var int
*/
- protected $ttl = 120;
+ protected $ttl = 60;
/**
* caching for relational data
* @var int
*/
- protected $rel_ttl = 0;
+ protected $rel_ttl = 0;
/**
* ass static columns for this table
* -> can be overwritten in child models
* @var bool
*/
- protected $addStaticFields = true;
-
- /**
- * field validation array
- * @var array
- */
- protected $validate = [];
+ protected $addStaticFields = true;
/**
* enables check for $fieldChanges on update/insert
@@ -54,7 +50,7 @@ abstract class BasicModel extends \DB\Cortex {
* in $fieldConf config
* @var bool
*/
- protected $enableActivityLogging = true;
+ protected $enableActivityLogging = true;
/**
* enables change for "active" column
@@ -62,40 +58,53 @@ abstract class BasicModel extends \DB\Cortex {
* -> $this->active = false; will NOT work (prevent abuse)!
* @var bool
*/
- private $allowActiveChange = false;
+ private $allowActiveChange = false;
/**
* getData() cache key prefix
* -> do not change, otherwise cached data is lost
* @var string
*/
- private $dataCacheKeyPrefix = 'DATACACHE';
+ private $dataCacheKeyPrefix = 'DATACACHE';
/**
* enables data export for this table
* -> can be overwritten in child models
* @var bool
*/
- public static $enableDataExport = false;
+ public static $enableDataExport = false;
/**
* enables data import for this table
* -> can be overwritten in child models
* @var bool
*/
- public static $enableDataImport = false;
+ public static $enableDataImport = false;
/**
* changed fields (columns) on update/insert
* -> e.g. for character "activity logging"
* @var array
*/
- protected $fieldChanges = [];
+ protected $fieldChanges = [];
/**
- * default TTL for getData(); cache
+ * collection for validation errors
+ * @var array
*/
- const DEFAULT_CACHE_TTL = 120;
+ protected $validationError = [];
+
+ /**
+ * default caching time of field schema - seconds
+ */
+ const DEFAULT_TTL = 86400;
+
+ /**
+ * default TTL for getData(); cache - seconds
+ */
+ const DEFAULT_CACHE_TTL = 120;
+
+ const ERROR_INVALID_MODEL_CLASS = 'Model class (%s) not found';
public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = 0){
@@ -135,8 +144,8 @@ abstract class BasicModel extends \DB\Cortex {
/**
* @param string $key
* @param mixed $val
- * @return mixed|void
- * @throws Exception\ValidationException
+ * @return mixed
+ * @throws ValidationException
*/
public function set($key, $val){
if(
@@ -167,15 +176,13 @@ abstract class BasicModel extends \DB\Cortex {
$val = trim($val);
}
- $valid = $this->validateField($key, $val);
-
- if(!$valid){
- $this->throwValidationError($key);
+ if( !$this->validateField($key, $val) ){
+ $this->throwValidationException($key);
}else{
$this->checkFieldForActivityLogging($key, $val);
-
- return parent::set($key, $val);
}
+
+ return parent::set($key, $val);
}
/**
@@ -206,15 +213,25 @@ abstract class BasicModel extends \DB\Cortex {
$val = (int)$val;
}
+ if(is_object($val)){
+ $val = $val->_id;
+ }
+
if( $fieldConf['type'] === self::DT_JSON){
$currentValue = $this->get($key);
}else{
$currentValue = $this->get($key, true);
}
+
if($currentValue !== $val){
// field has changed
- in_array($key, $this->fieldChanges) ?: $this->fieldChanges[] = $key;
+ if( !array_key_exists($key, $this->fieldChanges) ){
+ $this->fieldChanges[$key] = [
+ 'old' => $currentValue,
+ 'new' => $val
+ ];
+ }
}
}
}
@@ -239,11 +256,12 @@ abstract class BasicModel extends \DB\Cortex {
}
/**
- * extent the fieldConf Array with static fields for each table
+ * get static fields for this model instance
+ * @return array
*/
- private function addStaticFieldConfig(){
+ protected function getStaticFieldConf(): array {
+ $staticFieldConfig = [];
- // add static fields to this mapper
// static tables (fixed data) do not require them...
if($this->addStaticFields){
$staticFieldConfig = [
@@ -258,61 +276,37 @@ abstract class BasicModel extends \DB\Cortex {
'index' => true
]
];
-
-
- $this->fieldConf = array_merge($staticFieldConfig, $this->fieldConf);
}
+
+ return $staticFieldConfig;
+ }
+
+ /**
+ * extent the fieldConf Array with static fields for each table
+ */
+ private function addStaticFieldConfig(){
+ $this->fieldConf = array_merge($this->getStaticFieldConf(), $this->fieldConf);
}
/**
* validates a table column based on validation settings
- * @param $col
+ * @param string $key
* @param $val
* @return bool
*/
- private function validateField($col, $val){
+ protected function validateField(string $key, $val): bool {
$valid = true;
-
- if(array_key_exists($col, $this->validate)){
-
- $fieldValidationOptions = $this->validate[$col];
-
- foreach($fieldValidationOptions as $validateKey => $validateOption ){
- if(is_array($fieldValidationOptions[$validateKey])){
- $fieldSubValidationOptions = $fieldValidationOptions[$validateKey];
-
- foreach($fieldSubValidationOptions as $validateSubKey => $validateSubOption ){
- switch($validateKey){
- case 'length':
- switch($validateSubKey){
- case 'min';
- if(strlen($val) < $validateSubOption){
- $valid = false;
- }
- break;
- case 'max';
-
- if(strlen($val) > $validateSubOption){
- $valid = false;
- }
- break;
- }
- break;
- }
- }
-
+ if($fieldConf = $this->fieldConf[$key]){
+ if($method = $this->fieldConf[$key]['validate']){
+ if( !is_string($method)){
+ $method = 'validate_' . $key;
+ }
+ if(method_exists($this, $method)){
+ // validate $key (column) with this method...
+ $valid = $this->$method($key, $val);
}else{
- switch($validateKey){
- case 'regex':
- $valid = (bool)preg_match($fieldValidationOptions[$validateKey], $val);
- break;
- }
- }
-
- // a validation rule failed
- if(!$valid){
- break;
- }
+ self::getF3()->error(501, 'Method ' . get_class($this) . '->' . $method . '() is not implemented');
+ };
}
}
@@ -428,13 +422,22 @@ abstract class BasicModel extends \DB\Cortex {
}
/**
- * Throws a validation error for a giben column
- * @param $col
- * @throws \Exception\ValidationException
+ * throw validation exception for a model property
+ * @param string $col
+ * @param string $msg
+ * @throws ValidationException
*/
- protected function throwValidationError($col){
- throw new Exception\ValidationException('Validation failed: "' . $col . '".', $col);
+ protected function throwValidationException(string $col, string $msg = ''){
+ $msg = empty($msg) ? 'Validation failed: "' . $col . '".' : $msg;
+ throw new ValidationException($msg, $col);
+ }
+ /**
+ * @param string $msg
+ * @throws DatabaseException
+ */
+ protected function throwDbException(string $msg){
+ throw new DatabaseException($msg);
}
/**
@@ -458,11 +461,11 @@ abstract class BasicModel extends \DB\Cortex {
* get single dataSet by id
* @param $id
* @param int $ttl
+ * @param bool $isActive
* @return \DB\Cortex
*/
- public function getById($id, $ttl = 3) {
-
- return $this->getByForeignKey('id', (int)$id, ['limit' => 1], $ttl);
+ public function getById(int $id, int $ttl = 3, bool $isActive = true){
+ return $this->getByForeignKey('id', (int)$id, ['limit' => 1], $ttl, $isActive);
}
/**
@@ -492,10 +495,10 @@ abstract class BasicModel extends \DB\Cortex {
* @param $value
* @param array $options
* @param int $ttl
+ * @param bool $isActive
* @return \DB\Cortex
*/
- public function getByForeignKey($key, $value, $options = [], $ttl = 60){
-
+ public function getByForeignKey($key, $value, $options = [], $ttl = 0, $isActive = true){
$querySet = [];
$query = [];
if($this->exists($key)){
@@ -504,7 +507,7 @@ abstract class BasicModel extends \DB\Cortex {
}
// check active column
- if($this->exists('active')){
+ if($isActive && $this->exists('active')){
$query[] = "active = :active";
$querySet[':active'] = 1;
}
@@ -594,7 +597,7 @@ abstract class BasicModel extends \DB\Cortex {
* function should be overwritten in parent classes
* @return bool
*/
- public function isValid(){
+ public function isValid(): bool {
return true;
}
@@ -678,7 +681,7 @@ abstract class BasicModel extends \DB\Cortex {
$status = $this->importStaticData($tableData);
$this->getF3()->status(202);
}else{
- $this->getF3()->error(502, 'File could not be read');
+ $this->getF3()->error(500, 'File could not be read');
}
}else{
$this->getF3()->error(404, 'File not found: ' . $filePath);
@@ -727,15 +730,54 @@ abstract class BasicModel extends \DB\Cortex {
}
/**
- * buffer a new activity (action) logging
- * -> increment buffered counter
- * -> log character activity create/update/delete events
- * @param int $characterId
- * @param int $mapId
+ * get "default" logging object for this kind of model
+ * -> can be overwritten
* @param string $action
+ * @return Logging\LogInterface
*/
- protected function bufferActivity($characterId, $mapId, $action){
- Controller\LogController::instance()->bufferActivity($characterId, $mapId, $action);
+ protected function newLog($action = ''): Logging\LogInterface{
+ return new Logging\DefaultLog($action);
+ }
+
+ /**
+ * get formatter callback function for parsed logs
+ * @return null
+ */
+ protected function getLogFormatter(){
+ return null;
+ }
+
+ /**
+ * add new validation error
+ * @param ValidationException $e
+ */
+ protected function setValidationError(ValidationException $e){
+ $this->validationError[] = $e->getError();
+ }
+
+ /**
+ * get all validation errors
+ * @return array
+ */
+ public function getErrors(): array {
+ return $this->validationError;
+ }
+
+ public function save(){
+ try{
+ return parent::save();
+ }catch(ValidationException $e){
+ $this->setValidationError($e);
+ }catch(DatabaseException $e){
+ self::getF3()->error($e->getCode(), $e->getMessage(), $e->getTrace());
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString(){
+ return $this->getTable();
}
/**
@@ -755,14 +797,14 @@ abstract class BasicModel extends \DB\Cortex {
* @return BasicModel
* @throws \Exception
*/
- public static function getNew($model, $ttl = 86400){
+ public static function getNew($model, $ttl = self::DEFAULT_TTL){
$class = null;
$model = '\\' . __NAMESPACE__ . '\\' . $model;
if(class_exists($model)){
$class = new $model( null, null, null, $ttl );
}else{
- throw new \Exception('No model class found');
+ throw new \Exception(sprintf(self::ERROR_INVALID_MODEL_CLASS, $model));
}
return $class;
diff --git a/app/main/model/characterlogmodel.php b/app/main/model/characterlogmodel.php
index 1411a5bc..b978cd1e 100644
--- a/app/main/model/characterlogmodel.php
+++ b/app/main/model/characterlogmodel.php
@@ -57,6 +57,11 @@ class CharacterLogModel extends BasicModel {
'type' => Schema::DT_BIGINT,
'index' => true
],
+ 'shipMass' => [
+ 'type' => Schema::DT_FLOAT,
+ 'nullable' => false,
+ 'default' => 0
+ ],
'shipName' => [
'type' => Schema::DT_VARCHAR128,
'nullable' => false,
@@ -92,11 +97,13 @@ class CharacterLogModel extends BasicModel {
$this->shipTypeName = $logData['ship']['typeName'];
$this->shipId = (int)$logData['ship']['id'];
$this->shipName = $logData['ship']['name'];
+ $this->shipMass = (float)$logData['ship']['mass'];
}else{
$this->shipTypeId = null;
$this->shipTypeName = '';
$this->shipId = null;
$this->shipName = '';
+ $this->shipMass = 0;
}
if( isset($logData['station']) ){
@@ -125,6 +132,7 @@ class CharacterLogModel extends BasicModel {
$logData->ship->typeName = $this->shipTypeName;
$logData->ship->id = $this->shipId;
$logData->ship->name = $this->shipName;
+ $logData->ship->mass = $this->shipMass;
$logData->station = (object) [];
$logData->station->id = (int)$this->stationId;
@@ -191,23 +199,26 @@ class CharacterLogModel extends BasicModel {
* update session data for active character
* @param int $systemId
*/
- protected function updateCharacterSessionLocation($systemId){
+ protected function updateCharacterSessionLocation(int $systemId){
$controller = new Controller();
if(
!empty($sessionCharacter = $controller->getSessionCharacterData()) &&
$sessionCharacter['ID'] === $this->get('characterId', true)
){
- $prevSystemId = (int)$sessionCharacter['PREV_SYSTEM_ID'];
-
- if($prevSystemId === 0){
+ $systemChanged = false;
+ if((int)$sessionCharacter['PREV_SYSTEM_ID'] === 0){
$sessionCharacter['PREV_SYSTEM_ID'] = (int)$systemId;
- }else{
+ $systemChanged = true;
+ }elseif((int)$sessionCharacter['PREV_SYSTEM_ID'] !== $this->systemId){
$sessionCharacter['PREV_SYSTEM_ID'] = $this->systemId;
+ $systemChanged = true;
}
- $sessionCharacters = CharacterModel::mergeSessionCharacterData([$sessionCharacter]);
- $this->getF3()->set(User::SESSION_KEY_CHARACTERS, $sessionCharacters);
+ if($systemChanged){
+ $sessionCharacters = CharacterModel::mergeSessionCharacterData([$sessionCharacter]);
+ $this->getF3()->set(User::SESSION_KEY_CHARACTERS, $sessionCharacters);
+ }
}
}
diff --git a/app/main/model/charactermodel.php b/app/main/model/charactermodel.php
index 0259fc6e..62ee4da0 100644
--- a/app/main/model/charactermodel.php
+++ b/app/main/model/charactermodel.php
@@ -12,6 +12,8 @@ use Controller\Ccp\Sso as Sso;
use Controller\Api\User as User;
use DB\SQL\Schema;
use Lib\Util;
+use lib\Config;
+use Model\Universe;
class CharacterModel extends BasicModel {
@@ -279,7 +281,7 @@ class CharacterModel extends BasicModel {
if($minutes){
$seconds = $minutes * 60;
- $timezone = new \DateTimeZone( self::getF3()->get('TZ') );
+ $timezone = self::getF3()->get('getTimeZone')();
$kickedUntil = new \DateTime('now', $timezone);
// add cookie expire time
@@ -306,7 +308,7 @@ class CharacterModel extends BasicModel {
$banned = null;
if($status){
- $timezone = new \DateTimeZone( self::getF3()->get('TZ') );
+ $timezone = self::getF3()->get('getTimeZone')();
$bannedSince = new \DateTime('now', $timezone);
$banned = $bannedSince->format('Y-m-d H:i:s');
}
@@ -479,7 +481,7 @@ class CharacterModel extends BasicModel {
!empty($this->crestAccessToken) &&
!empty($this->crestAccessTokenUpdated)
){
- $timezone = new \DateTimeZone( self::getF3()->get('TZ') );
+ $timezone = self::getF3()->get('getTimeZone')();
$tokenTime = \DateTime::createFromFormat(
'Y-m-d H:i:s',
$this->crestAccessTokenUpdated,
@@ -547,8 +549,8 @@ class CharacterModel extends BasicModel {
if(is_null($this->banned)){
if( !$this->isKicked() ){
$f3 = self::getF3();
- $whitelistCorporations = array_filter( array_map('trim', (array)$f3->get('PATHFINDER.LOGIN.CORPORATION') ) );
- $whitelistAlliance = array_filter( array_map('trim', (array)$f3->get('PATHFINDER.LOGIN.ALLIANCE') ) );
+ $whitelistCorporations = array_filter( array_map('trim', (array)Config::getPathfinderData('login.corporation') ) );
+ $whitelistAlliance = array_filter( array_map('trim', (array)Config::getPathfinderData('login.alliance') ) );
if(
empty($whitelistCorporations) &&
@@ -676,9 +678,7 @@ class CharacterModel extends BasicModel {
if( !empty($locationData['system']['id']) ){
// character is currently in-game
- // IDs for "systemId", "stationId and "shipTypeId" that require more data
- $lookupIds = [];
-
+ // get current $characterLog or get new ---------------------------------------------------
if( !($characterLog = $this->getLog()) ){
// create new log
$characterLog = $this->rel('characterLog');
@@ -687,12 +687,17 @@ class CharacterModel extends BasicModel {
// get current log data and modify on change
$logData = json_decode(json_encode( $characterLog->getData()), true);
+ // check system and station data for changes ----------------------------------------------
+
+ // IDs for "systemId", "stationId" that require more data
+ $lookupUniverseIds = [];
+
if(
empty($logData['system']['name']) ||
$logData['system']['id'] !== $locationData['system']['id']
){
// system changed -> request "system name" for current system
- $lookupIds[] = $locationData['system']['id'];
+ $lookupUniverseIds[] = $locationData['system']['id'];
}
if( !empty($locationData['station']['id']) ){
@@ -701,7 +706,7 @@ class CharacterModel extends BasicModel {
$logData['station']['id'] !== $locationData['station']['id']
){
// station changed -> request "station name" for current station
- $lookupIds[] = $locationData['station']['id'];
+ $lookupUniverseIds[] = $locationData['station']['id'];
}
}else{
unset($logData['station']);
@@ -709,30 +714,10 @@ class CharacterModel extends BasicModel {
$logData = array_replace_recursive($logData, $locationData);
- // get current ship data
- $shipData = self::getF3()->ccpClient->getCharacterShipData($this->_id, $accessToken, $additionalOptions);
-
- if( !empty($shipData['ship']['typeId']) ){
- if(
- empty($logData['ship']['typeName']) ||
- $logData['ship']['typeId'] !== $shipData['ship']['typeId']
- ){
- // ship changed -> request "station name" for current station
- $lookupIds[] = $shipData['ship']['typeId'];
- }
-
- // "shipName"/"shipId" could have changed...
- $logData = array_replace_recursive($logData, $shipData);
- }else{
- // ship data should never be empty -> keep current one
- //unset($logData['ship']);
- $invalidResponse = true;
- }
-
- if( !empty($lookupIds) ){
+ // 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($lookupIds, $additionalOptions);
-
+ $universeData = self::getF3()->ccpClient->getUniverseNamesData($lookupUniverseIds, $additionalOptions);
if( !empty($universeData) ){
$logData = array_replace_recursive($logData, $universeData);
}else{
@@ -741,6 +726,48 @@ class CharacterModel extends BasicModel {
}
}
+
+ // check ship data for changes ------------------------------------------------------------
+ if( !$deleteLog ){
+ $shipData = self::getF3()->ccpClient->getCharacterShipData($this->_id, $accessToken, $additionalOptions);
+
+ // IDs for "systemId", "stationId" that require more data
+ $lookupShipTypeId = 0;
+
+ if( !empty($shipData['ship']['typeId']) ){
+ if(
+ empty($logData['ship']['typeName']) ||
+ $logData['ship']['typeId'] !== $shipData['ship']['typeId']
+ ){
+ // ship changed -> request "station name" for current station
+ $lookupShipTypeId = $shipData['ship']['typeId'];
+ }
+
+ // "shipName"/"shipId" could have changed...
+ $logData = array_replace_recursive($logData, $shipData);
+ }else{
+ // ship data should never be empty -> keep current one
+ //unset($logData['ship']);
+ $invalidResponse = true;
+ }
+
+ // get "more" data for shipTypeId ----------------------------------------------------
+ if($lookupShipTypeId > 0){
+ /**
+ * @var $typeModel Universe\TypeModel
+ */
+ $typeModel = Universe\BasicUniverseModel::getNew('TypeModel');
+ $typeModel->loadById($lookupShipTypeId, $additionalOptions);
+ if(!$typeModel->dry()){
+ $shipData['ship'] = (array)$typeModel->getShipData();
+ $logData = array_replace_recursive($logData, $shipData);
+ }else{
+ // this is important! ship data is a MUST HAVE!
+ $deleteLog = true;
+ }
+ }
+ }
+
if( !$deleteLog ){
// mark log as "updated" even if no changes were made
if($additionalOptions['markUpdated'] === true){
@@ -921,7 +948,7 @@ class CharacterModel extends BasicModel {
$mapCountPrivate = 0;
foreach($this->characterMaps as $characterMap){
if(
- $mapCountPrivate < self::getF3()->get('PATHFINDER.MAP.PRIVATE.MAX_COUNT') &&
+ $mapCountPrivate < Config::getMapsDefaultConfig('private')['max_count'] &&
$characterMap->mapId->isActive()
){
$maps[] = $characterMap->mapId;
@@ -934,11 +961,19 @@ class CharacterModel extends BasicModel {
}
/**
- * character logout
- * -> clear authentication data
+ * delete current location
*/
- public function logout(){
- if( is_object($this->characterAuthentications) ){
+ protected function deleteLog(){
+ if($characterLog = $this->getLog()){
+ $characterLog->erase();
+ }
+ }
+
+ /**
+ * delete authentications data
+ */
+ protected function deleteAuthentications(){
+ if(is_object($this->characterAuthentications)){
foreach($this->characterAuthentications as $characterAuthentication){
/**
* @var $characterAuthentication CharacterAuthenticationModel
@@ -947,6 +982,40 @@ class CharacterModel extends BasicModel {
}
}
}
+ /**
+ * character logout
+ * @param bool $deleteLog
+ * @param bool $deleteSession
+ * @param bool $deleteCookie
+ */
+ public function logout(bool $deleteSession = true, bool $deleteLog = true, bool $deleteCookie = false){
+ // delete current session data --------------------------------------------------------------------------------
+ if($deleteSession){
+ $sessionCharacterData = (array)$this->getF3()->get(User::SESSION_KEY_CHARACTERS);
+ $sessionCharacterData = array_filter($sessionCharacterData, function($data){
+ return ($data['ID'] != $this->_id);
+ });
+
+ if(empty($sessionCharacterData)){
+ // no active characters logged in -> log user out
+ $this->getF3()->clear(User::SESSION_KEY_USER);
+ $this->getF3()->clear(User::SESSION_KEY_CHARACTERS);
+ }else{
+ // update remaining active characters
+ $this->getF3()->set(User::SESSION_KEY_CHARACTERS, $sessionCharacterData);
+ }
+ }
+
+ // delete current location data -------------------------------------------------------------------------------
+ if($deleteLog){
+ $this->deleteLog();
+ }
+
+ // delete auth cookie data ------------------------------------------------------------------------------------
+ if($deleteCookie ){
+ $this->deleteAuthentications();
+ }
+ }
/**
* merges two multidimensional characterSession arrays by checking characterID
diff --git a/app/main/model/connectionmodel.php b/app/main/model/connectionmodel.php
index 29f77e25..bdc2ed8d 100644
--- a/app/main/model/connectionmodel.php
+++ b/app/main/model/connectionmodel.php
@@ -9,10 +9,10 @@
namespace Model;
use DB\SQL\Schema;
-use Controller;
use Controller\Api\Route;
+use Lib\Logging;
-class ConnectionModel extends BasicModel{
+class ConnectionModel extends AbstractMapTrackingModel {
protected $table = 'connection';
@@ -79,10 +79,16 @@ class ConnectionModel extends BasicModel{
/**
* set an array with all data for a system
- * @param $systemData
+ * @param array $data
*/
- public function setData($systemData){
- foreach((array)$systemData as $key => $value){
+ public function setData($data){
+ unset($data['id']);
+ unset($data['created']);
+ unset($data['updated']);
+ unset($data['createdCharacterId']);
+ unset($data['updatedCharacterId']);
+
+ foreach((array)$data as $key => $value){
if( !is_array($value) ){
if( $this->exists($key) ){
$this->$key = $value;
@@ -100,7 +106,6 @@ class ConnectionModel extends BasicModel{
* @return \stdClass
*/
public function getData($addSignatureData = false){
-
$connectionData = (object) [];
$connectionData->id = $this->id;
$connectionData->source = $this->source->id;
@@ -189,21 +194,21 @@ class ConnectionModel extends BasicModel{
* check whether this model is valid or not
* @return bool
*/
- public function isValid(){
- $isValid = true;
-
- // check if source/target system are not equal
- // check if source/target belong to same map
- if(
- is_object($this->source) &&
- is_object($this->target) &&
- $this->get('source', true) === $this->get('target', true) ||
- $this->source->get('mapId', true) !== $this->target->get('mapId', true)
- ){
- $isValid = false;
+ public function isValid(): bool {
+ if($valid = parent::isValid()){
+ // check if source/target system are not equal
+ // check if source/target belong to same map
+ if(
+ is_object($this->source) &&
+ is_object($this->target) &&
+ $this->get('source', true) === $this->get('target', true) ||
+ $this->source->get('mapId', true) !== $this->target->get('mapId', true)
+ ){
+ $valid = false;
+ }
}
- return $isValid;
+ return $valid;
}
/**
@@ -218,7 +223,6 @@ class ConnectionModel extends BasicModel{
// check for "default" connection type and add them if missing
// -> get() with "true" returns RAW data! important for JSON table column check!
$types = (array)json_decode( $this->get('type', true) );
-
if(
!$this->scope ||
empty($types)
@@ -226,7 +230,7 @@ class ConnectionModel extends BasicModel{
$this->setDefaultTypeData();
}
- return parent::beforeInsertEvent($self, $pkeys);
+ return $this->isValid() ? parent::beforeInsertEvent($self, $pkeys) : false;
}
/**
@@ -263,35 +267,18 @@ class ConnectionModel extends BasicModel{
}
/**
- * log character activity create/update/delete events
* @param string $action
+ * @return Logging\LogInterface
*/
- protected function logActivity($action){
-
- if(
- $this->enableActivityLogging &&
- (
- $action === 'connectionDelete' ||
- !empty($this->fieldChanges)
- ) &&
- $this->get('mapId')->isActivityLogEnabled()
- ){
- // TODO implement "dependency injection" for active character object...
- $controller = new Controller\Controller();
- $currentActiveCharacter = $controller->getCharacter();
- $characterId = is_null($currentActiveCharacter) ? 0 : $currentActiveCharacter->_id;
- $mapId = $this->get('mapId', true);
-
- parent::bufferActivity($characterId, $mapId, $action);
- }
+ public function newLog($action = ''): Logging\LogInterface{
+ return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData());
}
/**
- * save connection and check if obj is valid
- * @return ConnectionModel|false
+ * @return MapModel
*/
- public function save(){
- return ( $this->isValid() ) ? parent::save() : false;
+ public function getMap(): MapModel{
+ return $this->get('mapId');
}
/**
@@ -307,6 +294,17 @@ class ConnectionModel extends BasicModel{
}
}
+ /**
+ * get object relevant data for model log
+ * @return array
+ */
+ public function getLogObjectData() : array{
+ return [
+ 'objId' => $this->_id,
+ 'objName' => $this->scope
+ ];
+ }
+
/**
* see parent
*/
diff --git a/app/main/model/corporationmodel.php b/app/main/model/corporationmodel.php
index 02cfc789..a503d7df 100644
--- a/app/main/model/corporationmodel.php
+++ b/app/main/model/corporationmodel.php
@@ -9,6 +9,7 @@
namespace Model;
use DB\SQL\Schema;
+use lib\Config;
class CorporationModel extends BasicModel {
@@ -131,8 +132,6 @@ class CorporationModel extends BasicModel {
public function getMaps(){
$maps = [];
- $f3 = self::getF3();
-
$this->filter('mapCorporations',
['active = ?', 1],
['order' => 'created']
@@ -143,7 +142,7 @@ class CorporationModel extends BasicModel {
foreach($this->mapCorporations as $mapCorporation){
if(
$mapCorporation->mapId->isActive() &&
- $mapCount < $f3->get('PATHFINDER.MAP.CORPORATION.MAX_COUNT')
+ $mapCount < Config::getMapsDefaultConfig('corporation')['max_count']
){
$maps[] = $mapCorporation->mapId;
$mapCount++;
diff --git a/app/main/model/logmodelinterface.php b/app/main/model/logmodelinterface.php
new file mode 100644
index 00000000..cca816aa
--- /dev/null
+++ b/app/main/model/logmodelinterface.php
@@ -0,0 +1,19 @@
+ [
@@ -31,7 +32,7 @@ class MapModel extends BasicModel {
'nullable' => false,
'default' => 1,
'index' => true,
- 'after' => 'updated'
+ 'activity-log' => true
],
'scopeId' => [
'type' => Schema::DT_INT,
@@ -42,7 +43,9 @@ class MapModel extends BasicModel {
'table' => 'map_scope',
'on-delete' => 'CASCADE'
]
- ]
+ ],
+ 'validate' => 'validate_notDry',
+ 'activity-log' => true
],
'typeId' => [
'type' => Schema::DT_INT,
@@ -53,32 +56,82 @@ class MapModel extends BasicModel {
'table' => 'map_type',
'on-delete' => 'CASCADE'
]
- ]
+ ],
+ 'validate' => 'validate_notDry',
+ 'activity-log' => true
],
'name' => [
'type' => Schema::DT_VARCHAR128,
'nullable' => false,
- 'default' => ''
+ 'default' => '',
+ 'activity-log' => true,
+ 'validate' => true
],
'icon' => [
'type' => Schema::DT_VARCHAR128,
'nullable' => false,
- 'default' => ''
+ 'default' => '',
+ 'activity-log' => true
],
'deleteExpiredConnections' => [
'type' => Schema::DT_BOOL,
'nullable' => false,
- 'default' => 1
+ 'default' => 1,
+ 'activity-log' => true
],
'deleteEolConnections' => [
'type' => Schema::DT_BOOL,
'nullable' => false,
- 'default' => 1
+ 'default' => 1,
+ 'activity-log' => true
],
'persistentAliases' => [
'type' => Schema::DT_BOOL,
'nullable' => false,
- 'default' => 1
+ 'default' => 1,
+ 'activity-log' => true
+ ],
+ 'logActivity' => [
+ 'type' => Schema::DT_BOOL,
+ 'nullable' => false,
+ 'default' => 1,
+ 'activity-log' => true
+ ],
+ 'logHistory' => [
+ 'type' => Schema::DT_BOOL,
+ 'nullable' => false,
+ 'default' => 0,
+ 'activity-log' => true
+ ],
+ 'slackWebHookURL' => [
+ 'type' => Schema::DT_VARCHAR128,
+ 'nullable' => false,
+ 'default' => '',
+ 'validate' => true
+ ],
+ 'slackUsername' => [
+ 'type' => Schema::DT_VARCHAR128,
+ 'nullable' => false,
+ 'default' => '',
+ 'activity-log' => true
+ ],
+ 'slackIcon' => [
+ 'type' => Schema::DT_VARCHAR128,
+ 'nullable' => false,
+ 'default' => '',
+ 'activity-log' => true
+ ],
+ 'slackChannelHistory' => [
+ 'type' => Schema::DT_VARCHAR128,
+ 'nullable' => false,
+ 'default' => '',
+ 'activity-log' => true
+ ],
+ 'slackChannelRally' => [
+ 'type' => Schema::DT_VARCHAR128,
+ 'nullable' => false,
+ 'default' => '',
+ 'activity-log' => true
],
'systems' => [
'has-many' => ['Model\SystemModel', 'mapId']
@@ -97,37 +150,18 @@ class MapModel extends BasicModel {
]
];
- protected $validate = [
- 'name' => [
- 'length' => [
- 'min' => 3
- ]
- ],
- 'icon' => [
- 'length' => [
- 'min' => 3
- ]
- ],
- 'scopeId' => [
- 'regex' => '/^[1-9]+$/'
- ],
- 'typeId' => [
- 'regex' => '/^[1-9]+$/'
- ]
- ];
-
/**
* set map data by an associative array
- * @param $data
+ * @param array $data
*/
public function setData($data){
+ unset($data['id']);
+ unset($data['created']);
+ unset($data['updated']);
+ unset($data['createdCharacterId']);
+ unset($data['updatedCharacterId']);
foreach((array)$data as $key => $value){
-
- if($key == 'created'){
- continue;
- }
-
if(!is_array($value)){
if($this->exists($key)){
$this->$key = $value;
@@ -155,35 +189,62 @@ class MapModel extends BasicModel {
if(is_null($mapDataAll)){
// no cached map data found
- $mapData = (object) [];
- $mapData->id = $this->id;
- $mapData->name = $this->name;
- $mapData->icon = $this->icon;
- $mapData->deleteExpiredConnections = $this->deleteExpiredConnections;
- $mapData->deleteEolConnections = $this->deleteEolConnections;
- $mapData->persistentAliases = $this->persistentAliases;
- $mapData->created = strtotime($this->created);
- $mapData->updated = strtotime($this->updated);
+ $mapData = (object) [];
+ $mapData->id = $this->id;
+ $mapData->name = $this->name;
+ $mapData->icon = $this->icon;
+ $mapData->deleteExpiredConnections = $this->deleteExpiredConnections;
+ $mapData->deleteEolConnections = $this->deleteEolConnections;
+ $mapData->persistentAliases = $this->persistentAliases;
// map scope
- $mapData->scope = (object) [];
- $mapData->scope->id = $this->scopeId->id;
- $mapData->scope->name = $this->scopeId->name;
- $mapData->scope->label = $this->scopeId->label;
+ $mapData->scope = (object) [];
+ $mapData->scope->id = $this->scopeId->id;
+ $mapData->scope->name = $this->scopeId->name;
+ $mapData->scope->label = $this->scopeId->label;
// map type
- $mapData->type = (object) [];
- $mapData->type->id = $this->typeId->id;
- $mapData->type->name = $this->typeId->name;
- $mapData->type->classTab = $this->typeId->classTab;
+ $mapData->type = (object) [];
+ $mapData->type->id = $this->typeId->id;
+ $mapData->type->name = $this->typeId->name;
+ $mapData->type->classTab = $this->typeId->classTab;
+
+ // map logging
+ $mapData->logging = (object) [];
+ $mapData->logging->activity = $this->isActivityLogEnabled();
+ $mapData->logging->history = $this->isHistoryLogEnabled();
+
+ // map Slack logging
+ $mapData->logging->slackHistory = $this->isSlackChannelEnabled('slackChannelHistory');
+ $mapData->logging->slackRally = $this->isSlackChannelEnabled('slackChannelRally');
+ $mapData->logging->slackWebHookURL = $this->slackWebHookURL;
+ $mapData->logging->slackUsername = $this->slackUsername;
+ $mapData->logging->slackIcon = $this->slackIcon;
+ $mapData->logging->slackChannelHistory = $this->slackChannelHistory;
+ $mapData->logging->slackChannelRally = $this->slackChannelRally;
+
+ // map mail logging
+ $mapData->logging->mailRally = $this->isMailSendEnabled('RALLY_SET');
// map access
- $mapData->access = (object) [];
- $mapData->access->character = [];
- $mapData->access->corporation = [];
- $mapData->access->alliance = [];
+ $mapData->access = (object) [];
+ $mapData->access->character = [];
+ $mapData->access->corporation = [];
+ $mapData->access->alliance = [];
- // get access object data -------------------------------------
+ $mapData->created = (object) [];
+ $mapData->created->created = strtotime($this->created);
+ if(is_object($this->createdCharacterId)){
+ $mapData->created->character = $this->createdCharacterId->getData();
+ }
+
+ $mapData->updated = (object) [];
+ $mapData->updated->updated = strtotime($this->updated);
+ if(is_object($this->updatedCharacterId)){
+ $mapData->updated->character = $this->updatedCharacterId->getData();
+ }
+
+ // get access object data ---------------------------------------------------------------------------------
if($this->isPrivate()){
$characters = $this->getCharacters();
$characterData = [];
@@ -209,14 +270,14 @@ class MapModel extends BasicModel {
$mapData->access->alliance = $allianceData;
}
- // merge all data ---------------------------------------------
+ // merge all data -----------------------------------------------------------------------------------------
$mapDataAll = (object) [];
$mapDataAll->mapData = $mapData;
- // map system data --------------------------------------------
+ // map system data ----------------------------------------------------------------------------------------
$mapDataAll->systems = $this->getSystemData();
- // map connection data ----------------------------------------
+ // map connection data ------------------------------------------------------------------------------------
$mapDataAll->connections = $this->getConnectionData();
// max caching time for a map
@@ -228,6 +289,70 @@ class MapModel extends BasicModel {
return $mapDataAll;
}
+ /**
+ * validate name column
+ * @param string $key
+ * @param string $val
+ * @return bool
+ */
+ protected function validate_name(string $key, string $val): bool {
+ $valid = true;
+ if(mb_strlen($val) < 3){
+ $valid = false;
+ $this->throwValidationException($key);
+ }
+ return $valid;
+ }
+
+ /**
+ * validate Slack WebHook URL
+ * @param string $key
+ * @param string $val
+ * @return bool
+ */
+ protected function validate_slackWebHookURL(string $key, string $val): bool {
+ $valid = true;
+ if( !empty($val) ){
+ if(
+ !\Audit::instance()->url($val) ||
+ parse_url($val, PHP_URL_HOST) !== 'hooks.slack.com'
+ ){
+ $valid = false;
+ $this->throwValidationException($key);
+ }
+ }
+ return $valid;
+ }
+
+ /**
+ * @param $channel
+ * @return string
+ */
+ protected function set_slackChannelHistory($channel){
+ return $this->formatSlackChannelName($channel);
+ }
+
+ /**
+ * @param $channel
+ * @return string
+ */
+ protected function set_slackChannelRally($channel){
+ return $this->formatSlackChannelName($channel);
+ }
+
+ /**
+ * convert a Slack channel name into correct format
+ * @param $channel
+ * @return string
+ */
+ private function formatSlackChannelName($channel){
+ $channel = strtolower(str_replace(' ','', trim(trim((string)$channel), '#@')));
+ if($channel){
+ $channel = '#' . $channel;
+ }
+ return $channel;
+ }
+
/**
* Event "Hook" function
* @param self $self
@@ -235,6 +360,7 @@ class MapModel extends BasicModel {
*/
public function afterInsertEvent($self, $pkeys){
$self->clearCacheData();
+ $self->logActivity('mapCreate');
}
/**
@@ -244,6 +370,9 @@ class MapModel extends BasicModel {
*/
public function afterUpdateEvent($self, $pkeys){
$self->clearCacheData();
+
+ $activity = ($self->isActive()) ? 'mapUpdate' : 'mapDelete';
+ $self->logActivity($activity);
}
/**
@@ -253,6 +382,8 @@ class MapModel extends BasicModel {
*/
public function afterEraseEvent($self, $pkeys){
$self->clearCacheData();
+ $self->logActivity('mapDelete');
+ $self->deleteLogFile();
}
/**
@@ -701,49 +832,185 @@ class MapModel extends BasicModel {
}
/**
- * delete this map and all dependencies
- * @param CharacterModel $characterModel
- * @param null $callback
+ * @param string $action
+ * @return Logging\LogInterface
*/
- public function delete(CharacterModel $characterModel, $callback = null){
+ public function newLog($action = ''): Logging\LogInterface{
+ $logChannelData = $this->getLogChannelData();
+ $logObjectData = $this->getLogObjectData();
+ $log = (new Logging\MapLog($action, $logChannelData))->setTempData($logObjectData);
- if( !$this->dry() ){
- // check if character has access
- if($this->hasAccess($characterModel)){
- // all map related tables will be deleted on cascade
- if(
- $this->erase() &&
- is_callable($callback)
- ){
- $callback($this->_id);
- }
+ // update map history *.log files -----------------------------------------------------------------------------
+ if($this->isHistoryLogEnabled()){
+ // check socket config
+ if(Config::validSocketConnect()){
+ $log->addHandler('zmq', 'json', $this->getSocketConfig());
+ }else{
+ // update log file local (slow)
+ $log->addHandler('stream', 'json', $this->getStreamConfig());
}
}
+
+ // send map history to Slack channel --------------------------------------------------------------------------
+ $slackChannelKey = 'slackChannelHistory';
+ if($this->isSlackChannelEnabled($slackChannelKey)){
+ $log->addHandler('slackMap', null, $this->getSlackWebHookConfig($slackChannelKey));
+ $log->addHandlerGroup('slackMap');
+ }
+
+ // update map activity ----------------------------------------------------------------------------------------
+ $log->logActivity($this->isActivityLogEnabled());
+
+ return $log;
+ }
+
+ /**
+ * @return MapModel
+ */
+ public function getMap(): MapModel{
+ return $this;
+ }
+
+ /**
+ * get object relevant data for model log channel
+ * @return array
+ */
+ public function getLogChannelData() : array{
+ return [
+ 'channelId' => $this->_id,
+ 'channelName' => $this->name
+ ];
+ }
+ /**
+ * get object relevant data for model log object
+ * @return array
+ */
+ public function getLogObjectData() : array{
+ return [
+ 'objId' => $this->_id,
+ 'objName' => $this->name
+ ];
+ }
+
+ protected function getLogFormatter(){
+ return function(&$rowDataObj){
+ unset($rowDataObj['extra']);
+ };
}
/**
* check if "activity logging" is enabled for this map type
* @return bool
*/
- public function isActivityLogEnabled(){
- $f3 = self::getF3();
- $activityLogEnabled = false;
+ public function isActivityLogEnabled(): bool {
+ return $this->logActivity && (bool) Config::getMapsDefaultConfig($this->typeId->name)['log_activity_enabled'];
+ }
- if( $this->isAlliance() ){
- if( $f3->get('PATHFINDER.MAP.ALLIANCE.ACTIVITY_LOGGING') ){
- $activityLogEnabled = true;
+ /**
+ * check if "history logging" is enabled for this map type
+ * @return bool
+ */
+ public function isHistoryLogEnabled(): bool {
+ return $this->logHistory && (bool) Config::getMapsDefaultConfig($this->typeId->name)['log_history_enabled'];
+ }
+
+ /**
+ * check if "Slack WebHook" is enabled for this map type
+ * @param string $channel
+ * @return bool
+ * @throws PathfinderException
+ */
+ public function isSlackChannelEnabled(string $channel): bool {
+ $enabled = false;
+ // check global Slack status
+ if((bool)Config::getPathfinderData('slack.status')){
+ // check global map default config for this channel
+ switch($channel){
+ case 'slackChannelHistory': $defaultMapConfigKey = 'send_history_slack_enabled'; break;
+ case 'slackChannelRally': $defaultMapConfigKey = 'send_rally_slack_enabled'; break;
+ default: throw new PathfinderException(sprintf(self::ERROR_SLACK_CHANNEL, $channel));
}
- }elseif( $this->isCorporation() ){
- if( $f3->get('PATHFINDER.MAP.CORPORATION.ACTIVITY_LOGGING') ){
- $activityLogEnabled = true;
- }
- }elseif( $this->isPrivate() ){
- if( $f3->get('PATHFINDER.MAP.PRIVATE.ACTIVITY_LOGGING') ){
- $activityLogEnabled = true;
+
+ if((bool) Config::getMapsDefaultConfig($this->typeId->name)[$defaultMapConfigKey]){
+ $config = $this->getSlackWebHookConfig($channel);
+ if($config->slackWebHookURL && $config->slackChannel){
+ $enabled = true;
+ }
}
}
- return $activityLogEnabled;
+ return $enabled;
+ }
+
+ /**
+ * check if "E-Mail" Log is enabled for this map
+ * @param string $type
+ * @return bool
+ */
+ public function isMailSendEnabled(string $type): bool{
+ $enabled = false;
+ if((bool) Config::getMapsDefaultConfig($this->typeId->name)['send_rally_mail_enabled']){
+ $enabled = Config::isValidSMTPConfig($this->getSMTPConfig($type));
+ }
+
+ return $enabled;
+ }
+
+ /**
+ * get config for stream logging
+ * @param bool $abs absolute path
+ * @return \stdClass
+ */
+ public function getStreamConfig(bool $abs = false): \stdClass{
+ $config = (object) [];
+ $config->stream = '';
+ if( $this->getF3()->exists('PATHFINDER.HISTORY.LOG', $dir) ){
+ $config->stream .= $abs ? $this->getF3()->get('ROOT') . '/' : './';
+ $config->stream .= $dir . 'map/map_' . $this->_id . '.log';
+ $config->stream = $this->getF3()->fixslashes($config->stream);
+ }
+ return $config;
+ }
+
+ /**
+ * get config for Socket connection (e.g. where to send log data)
+ * @return \stdClass
+ */
+ public function getSocketConfig(): \stdClass{
+ $config = (object) [];
+ $config->uri = Config::getSocketUri();
+ $config->streamConf = $this->getStreamConfig(true);
+ return $config;
+ }
+
+ /**
+ * get Config for Slack WebHook cURL calls
+ * -> https://api.slack.com/incoming-webhooks
+ * @param string $channel
+ * @return \stdClass
+ */
+ public function getSlackWebHookConfig(string $channel = ''): \stdClass{
+ $config = (object) [];
+ $config->slackWebHookURL = $this->slackWebHookURL;
+ $config->slackUsername = $this->slackUsername;
+ $config->slackIcon = $this->slackIcon;
+ if($channel && $this->exists($channel)){
+ $config->slackChannel = $this->$channel;
+ }
+ return $config;
+ }
+
+ /**
+ * get Config for SMTP connection and recipient address
+ * @param string $type
+ * @param bool $addJson
+ * @return \stdClass
+ */
+ public function getSMTPConfig(string $type, bool $addJson = true): \stdClass{
+ $config = Config::getSMTPConfig();
+ $config->to = Config::getNotificationMail($type);
+ $config->addJson = $addJson;
+ return $config;
}
/**
@@ -782,22 +1049,31 @@ class MapModel extends BasicModel {
return $scope;
}
+ /**
+ * get log file data
+ * @param int $offset
+ * @param int $limit
+ * @return array
+ */
+ public function getLogData(int $offset = FileHandler::LOG_FILE_OFFSET, int $limit = FileHandler::LOG_FILE_LIMIT): array {
+ $streamConf = $this->getStreamConfig();
+ return FileHandler::readLogFile($streamConf->stream, $offset, $limit, $this->getLogFormatter());
+ }
+
/**
* save a system to this map
* @param SystemModel $system
+ * @param CharacterModel $character
* @param int $posX
* @param int $posY
- * @param null|CharacterModel $character
- * @return mixed
+ * @return false|ConnectionModel
*/
- public function saveSystem( SystemModel $system, $posX = 10, $posY = 0, $character = null){
+ public function saveSystem( SystemModel $system, CharacterModel $character, $posX = 10, $posY = 0){
$system->setActive(true);
$system->mapId = $this->id;
$system->posX = $posX;
$system->posY = $posY;
- $system->createdCharacterId = $character;
- $system->updatedCharacterId = $character;
- return $system->save();
+ return $system->save($character);
}
/**
@@ -839,11 +1115,26 @@ class MapModel extends BasicModel {
* save new connection
* -> connection scope/type is automatically added
* @param ConnectionModel $connection
+ * @param CharacterModel $character
* @return false|ConnectionModel
*/
- public function saveConnection(ConnectionModel $connection){
+ public function saveConnection(ConnectionModel $connection, CharacterModel $character){
$connection->mapId = $this;
- return $connection->save();
+ return $connection->save($character);
+ }
+
+ /**
+ * delete existing log file
+ */
+ protected function deleteLogFile(){
+ $config = $this->getStreamConfig();
+ if(is_file($config->stream)){
+ // try to set write access
+ if(!is_writable($config->stream)){
+ chmod($config->stream, 0666);
+ }
+ @unlink($config->stream);
+ }
}
/**
@@ -909,12 +1200,11 @@ class MapModel extends BasicModel {
}
/**
- * save a map
- * @return mixed
+ * @param CharacterModel|null $characterModel
+ * @return false|ConnectionModel
*/
- public function save(){
-
- $mapModel = parent::save();
+ public function save(CharacterModel $characterModel = null){
+ $mapModel = parent::save($characterModel);
// check if map type has changed and clear access objects
if( !$mapModel->dry() ){
diff --git a/app/main/model/systemmodel.php b/app/main/model/systemmodel.php
index a8b786f1..9d8e5984 100644
--- a/app/main/model/systemmodel.php
+++ b/app/main/model/systemmodel.php
@@ -8,11 +8,10 @@
namespace Model;
-use controller\MailController;
use DB\SQL\Schema;
-use lib\Config;
+use Lib\Logging;
-class SystemModel extends BasicModel {
+class SystemModel extends AbstractMapTrackingModel {
const MAX_POS_X = 2300;
const MAX_POS_Y = 498;
@@ -124,7 +123,8 @@ class SystemModel extends BasicModel {
'rallyPoke' => [
'type' => Schema::DT_BOOL,
'nullable' => false,
- 'default' => 0
+ 'default' => 0,
+ 'activity-log' => true
],
'description' => [
'type' => Schema::DT_VARCHAR512,
@@ -142,28 +142,6 @@ class SystemModel extends BasicModel {
'nullable' => false,
'default' => 0
],
- 'createdCharacterId' => [
- 'type' => Schema::DT_INT,
- 'index' => true,
- 'belongs-to-one' => 'Model\CharacterModel',
- 'constraint' => [
- [
- 'table' => 'character',
- 'on-delete' => 'CASCADE'
- ]
- ]
- ],
- 'updatedCharacterId' => [
- 'type' => Schema::DT_INT,
- 'index' => true,
- 'belongs-to-one' => 'Model\CharacterModel',
- 'constraint' => [
- [
- 'table' => 'character',
- 'on-delete' => 'CASCADE'
- ]
- ]
- ],
'signatures' => [
'has-many' => ['Model\SystemSignatureModel', 'systemId']
],
@@ -177,16 +155,16 @@ class SystemModel extends BasicModel {
/**
* set an array with all data for a system
- * @param array $systemData
+ * @param array $data
*/
- public function setData($systemData){
-
- foreach((array)$systemData as $key => $value){
-
- if($key == 'created'){
- continue;
- }
+ public function setData($data){
+ unset($data['id']);
+ unset($data['created']);
+ unset($data['updated']);
+ unset($data['createdCharacterId']);
+ unset($data['updatedCharacterId']);
+ foreach((array)$data as $key => $value){
if(!is_array($value)){
if($this->exists($key)){
$this->$key = $value;
@@ -361,8 +339,6 @@ class SystemModel extends BasicModel {
case 1:
// new rally point set
$rally = date('Y-m-d H:i:s', time());
- // flag system for mail poke -> after save()
- $this->virtual('newRallyPointSet', true);
break;
default:
$rally = date('Y-m-d H:i:s', $rally);
@@ -417,15 +393,6 @@ class SystemModel extends BasicModel {
*/
public function afterUpdateEvent($self, $pkeys){
$self->clearCacheData();
-
- // check if rally point mail should be send
- if(
- $self->newRallyPointSet &&
- $self->rallyPoke
- ){
- $self->sendRallyPointMail();
- }
-
$activity = ($self->isActive()) ? 'systemUpdate' : 'systemDelete';
$self->logActivity($activity);
}
@@ -441,23 +408,18 @@ class SystemModel extends BasicModel {
}
/**
- * log character activity create/update/delete events
* @param string $action
+ * @return Logging\LogInterface
*/
- protected function logActivity($action){
- if(
- $this->enableActivityLogging &&
- (
- $action === 'systemDelete' ||
- !empty($this->fieldChanges)
- ) &&
- $this->get('mapId')->isActivityLogEnabled()
- ){
- $characterId = $this->get('updatedCharacterId', true);
- $mapId = $this->get('mapId', true);
+ public function newLog($action = ''): Logging\LogInterface{
+ return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData());
+ }
- parent::bufferActivity($characterId, $mapId, $action);
- }
+ /**
+ * @return MapModel
+ */
+ public function getMap(): MapModel{
+ return $this->get('mapId');
}
/**
@@ -599,6 +561,52 @@ class SystemModel extends BasicModel {
return ($this->isWormhole() && $this->security === 'SH');
}
+ /**
+ * send rally point poke to various "APIs"
+ * -> send to a Slack channel
+ * -> send to an Email
+ * @param array $rallyData
+ * @param CharacterModel $characterModel
+ */
+ public function sendRallyPoke(array $rallyData, CharacterModel $characterModel){
+ // rally log needs at least one handler to be valid
+ $isValidLog = false;
+ $log = new Logging\RallyLog('rallySet', $this->getMap()->getLogChannelData());
+
+ // Slack poke -----------------------------------------------------------------------------
+ $slackChannelKey = 'slackChannelRally';
+ if(
+ $rallyData['pokeSlack'] === true &&
+ $this->getMap()->isSlackChannelEnabled($slackChannelKey)
+ ){
+ $isValidLog = true;
+ $log->addHandler('slackRally', null, $this->getMap()->getSlackWebHookConfig($slackChannelKey));
+ }
+
+ // Mail poke ------------------------------------------------------------------------------
+ $mailAddressKey = 'RALLY_SET';
+ if(
+ $rallyData['pokeMail'] === true &&
+ $this->getMap()->isMailSendEnabled('RALLY_SET')
+ ){
+ $isValidLog = true;
+ $mailConf = $this->getMap()->getSMTPConfig($mailAddressKey, false);
+ $log->addHandler('mail', 'mail', $mailConf);
+ }
+
+ // Buffer log -----------------------------------------------------------------------------
+ if($isValidLog){
+ $log->setTempData($this->getLogObjectData(true));
+ $log->setCharacter($characterModel);
+ if( !empty($rallyData['message']) ){
+ $log->setData([
+ 'message' => $rallyData['message']
+ ]);
+ }
+ $log->buffer();
+ }
+ }
+
/**
* get static WH data for this system
* -> any WH system has at least one static WH
@@ -641,34 +649,27 @@ class SystemModel extends BasicModel {
}
/**
- * send rally point information by mail
+ * get object relevant data for model log
+ * @param bool $fullData
+ * @return array
*/
- protected function sendRallyPointMail(){
- $recipient = Config::getNotificationMail('RALLY_SET');
+ public function getLogObjectData($fullData = false) : array{
+ $objectData = [
+ 'objId' => $this->_id,
+ 'objName' => $this->name
+ ];
- if(
- $recipient &&
- \Audit::instance()->email($recipient)
- ){
- $updatedCharacterId = (int) $this->get('updatedCharacterId', true);
- /**
- * @var $character CharacterModel
- */
- $character = $this->rel('updatedCharacterId');
- $character->getById( $updatedCharacterId );
- if( !$character->dry() ){
- $body = [];
- $body[] = "Map:\t\t" . $this->mapId->name;
- $body[] = "System:\t\t" . $this->name;
- $body[] = "Region:\t\t" . $this->region;
- $body[] = "Security:\t" . $this->security;
- $body[] = "Character:\t" . $character->name;
- $body[] = "Time:\t\t" . date('g:i a; F j, Y', strtotime($this->rallyUpdated) );
- $bodyMsg = implode("\r\n", $body);
-
- (new MailController())->sendRallyPoint($recipient, $bodyMsg);
- }
+ if($fullData){
+ $objectData['objAlias'] = $this->alias;
+ $objectData['objRegion'] = $this->region;
+ $objectData['objIsWormhole'] = $this->isWormhole();
+ $objectData['objEffect'] = $this->effect;
+ $objectData['objSecurity'] = $this->security;
+ $objectData['objTrueSec'] = $this->trueSec;
+ $objectData['objDescription'] = $this->description;
}
+
+ return $objectData;
}
/**
diff --git a/app/main/model/systemsignaturemodel.php b/app/main/model/systemsignaturemodel.php
index 553301af..402e8c9c 100644
--- a/app/main/model/systemsignaturemodel.php
+++ b/app/main/model/systemsignaturemodel.php
@@ -9,8 +9,9 @@
namespace Model;
use DB\SQL\Schema;
+use Lib\Logging;
-class SystemSignatureModel extends BasicModel {
+class SystemSignatureModel extends AbstractMapTrackingModel {
protected $table = 'system_signature';
@@ -62,52 +63,29 @@ class SystemSignatureModel extends BasicModel {
'type' => Schema::DT_VARCHAR128,
'nullable' => false,
'default' => '',
- 'activity-log' => true
+ 'activity-log' => true,
+ 'validate' => true
],
'description' => [
'type' => Schema::DT_VARCHAR512,
'nullable' => false,
'default' => '',
'activity-log' => true
- ],
- 'createdCharacterId' => [
- 'type' => Schema::DT_INT,
- 'index' => true,
- 'belongs-to-one' => 'Model\CharacterModel',
- 'constraint' => [
- [
- 'table' => 'character',
- 'on-delete' => 'CASCADE'
- ]
- ]
- ],
- 'updatedCharacterId' => [
- 'type' => Schema::DT_INT,
- 'index' => true,
- 'belongs-to-one' => 'Model\CharacterModel',
- 'constraint' => [
- [
- 'table' => 'character',
- 'on-delete' => 'CASCADE'
- ]
- ]
- ]
- ];
-
- protected $validate = [
- 'name' => [
- 'length' => [
- 'min' => 3
- ]
]
];
/**
* set an array with all data for a system
- * @param $signatureData
+ * @param $data
*/
- public function setData($signatureData){
- foreach((array)$signatureData as $key => $value){
+ public function setData($data){
+ unset($data['id']);
+ unset($data['created']);
+ unset($data['updated']);
+ unset($data['createdCharacterId']);
+ unset($data['updatedCharacterId']);
+
+ foreach((array)$data as $key => $value){
if(!is_array($value)){
if($this->exists($key)){
$this->$key = $value;
@@ -121,7 +99,6 @@ class SystemSignatureModel extends BasicModel {
* @return \stdClass
*/
public function getData(){
-
$signatureData = (object) [];
$signatureData->id = $this->id;
@@ -187,6 +164,36 @@ class SystemSignatureModel extends BasicModel {
return $validConnectionId;
}
+ /**
+ * validate name column
+ * @param string $key
+ * @param string $val
+ * @return bool
+ */
+ protected function validate_name(string $key, string $val): bool {
+ $valid = true;
+ if(mb_strlen($val) < 3){
+ $valid = false;
+ $this->throwValidationException($key);
+ }
+ return $valid;
+ }
+
+ /**
+ * @param string $action
+ * @return Logging\LogInterface
+ */
+ public function newLog($action = ''): Logging\LogInterface{
+ return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData());
+ }
+
+ /**
+ * @return MapModel
+ */
+ public function getMap(): MapModel{
+ return $this->get('systemId')->getMap();
+ }
+
/**
* get the connection (if attached)
* @return \Model\ConnectionModel|null
@@ -270,29 +277,14 @@ class SystemSignatureModel extends BasicModel {
}
/**
- * log character activity create/update/delete events
- * @param string $action
+ * get object relevant data for model log
+ * @return array
*/
- protected function logActivity($action){
- if($this->enableActivityLogging){
- /**
- * @var $map MapModel
- */
- $map = $this->get('systemId')->get('mapId');
-
- if(
- (
- $action === 'signatureDelete' ||
- !empty($this->fieldChanges)
- ) &&
- $map->isActivityLogEnabled()
- ){
- $characterId = $this->get('updatedCharacterId', true);
- $mapId = $map->_id;
-
- parent::bufferActivity($characterId, $mapId, $action);
- }
- }
+ public function getLogObjectData() : array{
+ return [
+ 'objId' => $this->_id,
+ 'objName' => $this->name
+ ];
}
/**
diff --git a/app/main/model/universe/basicuniversemodel.php b/app/main/model/universe/basicuniversemodel.php
index eb2ac4d1..681ab116 100644
--- a/app/main/model/universe/basicuniversemodel.php
+++ b/app/main/model/universe/basicuniversemodel.php
@@ -9,9 +9,30 @@
namespace Model\Universe;
+use DB\Database;
use Model\BasicModel;
-class BasicUniverseModel extends BasicModel {
+abstract class BasicUniverseModel extends BasicModel {
+
+ /**
+ * data from Universe tables is static and does not change frequently
+ * -> refresh static data after X days
+ */
+ const CACHE_MAX_DAYS = 7;
protected $db = 'DB_UNIVERSE';
+
+ public static function getNew($model, $ttl = self::DEFAULT_TTL){
+ $class = null;
+
+ $model = '\\' . __NAMESPACE__ . '\\' . $model;
+ if(class_exists($model)){
+ $db = Database::instance()->getDB('UNIVERSE');
+ $class = new $model($db, null, null, $ttl);
+ }else{
+ throw new \Exception(sprintf(self::ERROR_INVALID_MODEL_CLASS, $model));
+ }
+
+ return $class;
+ }
}
\ No newline at end of file
diff --git a/app/main/model/universe/typemodel.php b/app/main/model/universe/typemodel.php
new file mode 100644
index 00000000..7db8476a
--- /dev/null
+++ b/app/main/model/universe/typemodel.php
@@ -0,0 +1,145 @@
+ [
+ 'type' => Schema::DT_VARCHAR128,
+ 'nullable' => false,
+ 'default' => ''
+ ],
+ 'description' => [
+ 'type' => Schema::DT_TEXT
+ ],
+ 'published' => [
+ 'type' => Schema::DT_BOOL,
+ 'nullable' => false,
+ 'default' => 1,
+ 'index' => true
+ ],
+ 'radius' => [
+ 'type' => Schema::DT_FLOAT,
+ 'nullable' => false,
+ 'default' => 0
+ ],
+ 'volume' => [
+ 'type' => Schema::DT_FLOAT,
+ 'nullable' => false,
+ 'default' => 0
+ ],
+ 'capacity' => [
+ 'type' => Schema::DT_FLOAT,
+ 'nullable' => false,
+ 'default' => 0
+ ],
+ 'mass' => [
+ 'type' => Schema::DT_FLOAT,
+ 'nullable' => false,
+ 'default' => 0
+ ],
+ 'groupId' => [
+ 'type' => Schema::DT_INT,
+ 'nullable' => false,
+ 'default' => 0
+ ],
+ 'marketGroupId' => [
+ 'type' => Schema::DT_INT,
+ 'nullable' => false,
+ 'default' => 0
+ ],
+ 'packagedVolume' => [
+ 'type' => Schema::DT_FLOAT,
+ 'nullable' => false,
+ 'default' => 0
+ ],
+ 'portionSize' => [
+ 'type' => Schema::DT_INT,
+ 'nullable' => false,
+ 'default' => 0
+ ],
+ 'graphicId' => [
+ 'type' => Schema::DT_INT,
+ 'nullable' => false,
+ 'default' => 0
+ ]
+ ];
+
+ /**
+ * get shipData from object
+ * -> more fields can be added in here if needed
+ * @return \stdClass
+ */
+ public function getShipData(): \stdClass {
+ $shipData = (object) [];
+ if(!$this->dry()){
+ $shipData->typeId = $this->_id;
+ $shipData->typeName = $this->name;
+ $shipData->mass = $this->mass;
+ }
+ return $shipData;
+ }
+
+ /**
+ * load data from API into $this and save $this
+ * @param int $id
+ * @param array $additionalOptions
+ */
+ protected function loadData(int $id, array $additionalOptions = []){
+ $data = self::getF3()->ccpClient->getUniverseTypesData($id, $additionalOptions);
+ if(!empty($data)){
+ $this->copyfrom($data);
+ $this->save();
+ }
+ }
+
+ /**
+ * load object by $id
+ * -> if $id not exists in DB -> query API
+ * @param int $id
+ * @param array $additionalOptions
+ */
+ public function loadById(int $id, array $additionalOptions = []){
+ /**
+ * @var $model self
+ */
+ $model = parent::getById($id);
+ if($model->isOutdated()){
+ $model->loadData($id, $additionalOptions);
+ }
+ }
+
+ /**
+ * checks whether data is outdated and should be refreshed
+ * @return bool
+ */
+ protected function isOutdated(): bool {
+ $outdated = true;
+ if(!$this->dry()){
+ $timezone = $this->getF3()->get('getTimeZone')();
+ $currentTime = new \DateTime('now', $timezone);
+ $updateTime = \DateTime::createFromFormat(
+ 'Y-m-d H:i:s',
+ $this->updated,
+ $timezone
+ );
+ $interval = $updateTime->diff($currentTime);
+ if($interval->days < self::CACHE_MAX_DAYS ){
+ $outdated = false;
+ }
+ }
+ return $outdated;
+ }
+}
\ No newline at end of file
diff --git a/app/main/model/usermodel.php b/app/main/model/usermodel.php
index 93e59c84..5f7f8f5b 100644
--- a/app/main/model/usermodel.php
+++ b/app/main/model/usermodel.php
@@ -12,6 +12,8 @@ use DB\SQL\Schema;
use Controller;
use Controller\Api\User as User;
use Exception;
+use lib\Config;
+use Lib\Logging;
class UserModel extends BasicModel {
@@ -28,27 +30,20 @@ class UserModel extends BasicModel {
'type' => Schema::DT_VARCHAR128,
'nullable' => false,
'default' => '',
- 'index' => true
+ 'index' => true,
+ 'validate' => true
],
'email' => [
'type' => Schema::DT_VARCHAR128,
'nullable' => false,
- 'default' => ''
+ 'default' => '',
+ 'validate' => true
],
'userCharacters' => [
'has-many' => ['Model\UserCharacterModel', 'userId']
]
];
- protected $validate = [
- 'name' => [
- 'length' => [
- 'min' => 3,
- 'max' => 50
- ]
- ]
- ];
-
/**
* get all data for this user
* -> ! caution ! this function returns sensitive data! (e.g. email,..)
@@ -93,23 +88,6 @@ class UserModel extends BasicModel {
return $userData;
}
- /**
- * validate and set a email address for this user
- * -> empty email is allowed!
- * @param string $email
- * @return string
- */
- public function set_email($email){
- if (
- !empty($email) &&
- \Audit::instance()->email($email) == false
- ) {
- // no valid email address
- $this->throwValidationError('email');
- }
- return $email;
- }
-
/**
* check if new user registration is allowed
* @param UserModel $self
@@ -119,10 +97,8 @@ class UserModel extends BasicModel {
*/
public function beforeInsertEvent($self, $pkeys){
$registrationStatus = Controller\Controller::getRegistrationStatus();
-
switch($registrationStatus){
case 0:
- $f3 = self::getF3();
throw new Exception\RegistrationException('User registration is currently not allowed');
break;
case 1:
@@ -133,6 +109,80 @@ class UserModel extends BasicModel {
}
}
+ /**
+ * @param BasicModel $self
+ * @param $pkeys
+ */
+ public function afterEraseEvent($self, $pkeys){
+ $this->sendDeleteMail();
+ }
+
+ /**
+ * send delete confirm mail to this user
+ */
+ protected function sendDeleteMail(){
+ if($this->isMailSendEnabled()){
+ $log = new Logging\UserLog('userDelete', $this->getLogChannelData());
+ $log->addHandler('mail', 'mail', $this->getSMTPConfig());
+ $log->setMessage('Delete Account - {channelName}');
+ $log->setData([
+ 'message' =>'Your account was successfully deleted.'
+ ]);
+ $log->buffer();
+ }
+ }
+
+ /**
+ * checks whether user has a valid email address and pathfinder has a valid SMTP config
+ * @return bool
+ */
+ protected function isMailSendEnabled() : bool{
+ return Config::isValidSMTPConfig($this->getSMTPConfig());
+ }
+
+ /**
+ * get SMTP config for this user
+ * @return \stdClass
+ */
+ protected function getSMTPConfig() : \stdClass{
+ $config = Config::getSMTPConfig();
+ $config->to = $this->email;
+ return $config;
+ }
+
+ /**
+ * validate name column
+ * @param string $key
+ * @param string $val
+ * @return bool
+ */
+ protected function validate_name(string $key, string $val): bool {
+ $valid = true;
+ if(
+ mb_strlen($val) < 3 ||
+ mb_strlen($val) > 80
+ ){
+ $valid = false;
+ $this->throwValidationException($key);
+ }
+ return $valid;
+ }
+
+ /**
+ * validate email column
+ * @param string $key
+ * @param string $val
+ * @return bool
+ */
+ protected function validate_email(string $key, string $val): bool {
+ $valid = true;
+ if ( !empty($val) && \Audit::instance()->email($val) == false ){
+ $valid = false;
+ $this->throwValidationException($key);
+ }
+ return $valid;
+ }
+
/**
* check whether this character has already a user assigned to it
* @return bool
@@ -145,10 +195,10 @@ class UserModel extends BasicModel {
/**
* search for user by unique username
* @param $name
- * @return array|FALSE
+ * @return \DB\Cortex
*/
public function getByName($name){
- return $this->getByForeignKey('name', $name, [], 0);
+ return $this->getByForeignKey('name', $name, []);
}
/**
@@ -165,17 +215,9 @@ class UserModel extends BasicModel {
if($this->_id === $currentSessionUser['ID']){
// user matches session data
- $sessionCharacters = (array)$this->getF3()->get(User::SESSION_KEY_CHARACTERS);
-
if($characterId > 0){
- // search for specific characterData
- foreach($sessionCharacters as $characterData){
- if($characterId === (int)$characterData['ID']){
- $data = $characterData;
- break;
- }
- }
- }elseif( !empty($sessionCharacters) ){
+ $data = $this->findSessionCharacterData($characterId);
+ }elseif( !empty($sessionCharacters = (array)$this->getF3()->get(User::SESSION_KEY_CHARACTERS)) ){
// no character was requested ($requestedCharacterId = 0) AND session characters were found
// -> get first matched character (e.g. user open browser tab)
$data = $sessionCharacters[0];
@@ -206,6 +248,26 @@ class UserModel extends BasicModel {
return $data;
}
+ /**
+ * search in session data for $characterId
+ * @param int $characterId
+ * @return array
+ */
+ public function findSessionCharacterData(int $characterId): array{
+ $data = [];
+ if($characterId){
+ $sessionCharacters = (array)$this->getF3()->get(User::SESSION_KEY_CHARACTERS);
+ // search for specific characterData
+ foreach($sessionCharacters as $characterData){
+ if($characterId === (int)$characterData['ID']){
+ $data = $characterData;
+ break;
+ }
+ }
+ }
+ return $data;
+ }
+
/**
* get all userCharacters models for a user
* characters will be checked/updated on login by CCP API call
@@ -292,4 +354,16 @@ class UserModel extends BasicModel {
return $activeCharacters;
}
+ /**
+ * get object relevant data for model log channel
+ * @return array
+ */
+ public function getLogChannelData() : array{
+ return [
+ 'channelId' => $this->_id,
+ 'channelName' => $this->name
+ ];
+ }
+
+
}
\ No newline at end of file
diff --git a/app/pathfinder.ini b/app/pathfinder.ini
index df0dd45b..54063594 100644
--- a/app/pathfinder.ini
+++ b/app/pathfinder.ini
@@ -3,7 +3,7 @@
[PATHFINDER]
NAME = Pathfinder
; installed version (used for CSS/JS cache busting)
-VERSION = v1.2.5
+VERSION = v1.3.0
; contact information [optional]
CONTACT = https://github.com/exodus4d
; public contact email [optional]
@@ -31,6 +31,11 @@ MODE_MAINTENANCE = 0
CORPORATION =
ALLIANCE =
+; Slack API integration ===========================================================================
+[PATHFINDER.SLACK]
+; Global Slack API status, check PATHFINDER.MAP section for individual control (0=disabled, 1=enabled)
+STATUS = 1
+
; View ============================================================================================
[PATHFINDER.VIEW]
; static page templates
@@ -55,29 +60,51 @@ ADMIN = templates/view/admin.html
; - Max number of shared entities per map
; MAX_SYSTEMS:
; - Max number of active systems per map
-; ACTIVITY_LOGGING (0: disable, 1: enable):
-; - Whether user activity should be logged for a map type
-; - E.g. create/update/delete of systems/connections/signatures
+; 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
[PATHFINDER.MAP.PRIVATE]
-LIFETIME = 30
-MAX_COUNT = 3
-MAX_SHARED = 10
-MAX_SYSTEMS = 50
-ACTIVITY_LOGGING = 1
+LIFETIME = 60
+MAX_COUNT = 3
+MAX_SHARED = 10
+MAX_SYSTEMS = 50
+LOG_ACTIVITY_ENABLED = 1
+LOG_HISTORY_ENABLED = 1
+SEND_HISTORY_SLACK_ENABLED = 0
+SEND_RALLY_SLACK_ENABLED = 1
+SEND_RALLY_Mail_ENABLED = 0
[PATHFINDER.MAP.CORPORATION]
-LIFETIME = 99999
-MAX_COUNT = 3
-MAX_SHARED = 3
-MAX_SYSTEMS = 100
-ACTIVITY_LOGGING = 1
+LIFETIME = 99999
+MAX_COUNT = 3
+MAX_SHARED = 3
+MAX_SYSTEMS = 100
+LOG_ACTIVITY_ENABLED = 1
+LOG_HISTORY_ENABLED = 1
+SEND_HISTORY_SLACK_ENABLED = 1
+SEND_RALLY_SLACK_ENABLED = 1
+SEND_RALLY_Mail_ENABLED = 0
[PATHFINDER.MAP.ALLIANCE]
-LIFETIME = 99999
-MAX_COUNT = 3
-MAX_SHARED = 2
-MAX_SYSTEMS = 100
-ACTIVITY_LOGGING = 0
+LIFETIME = 99999
+MAX_COUNT = 3
+MAX_SHARED = 2
+MAX_SYSTEMS = 100
+LOG_ACTIVITY_ENABLED = 0
+LOG_HISTORY_ENABLED = 1
+SEND_HISTORY_SLACK_ENABLED = 1
+SEND_RALLY_SLACK_ENABLED = 1
+SEND_RALLY_Mail_ENABLED = 0
; Route search ====================================================================================
[PATHFINDER.ROUTE]
@@ -87,7 +114,7 @@ SEARCH_DEPTH = 7000
; default count of routes that will be checked (initial) when a system is selected (default: 2)
SEARCH_DEFAULT_COUNT = 2
; max count of routes that can be selected in "route settings" dialog (default: 4)
-MAX_Default_COUNT = 4
+MAX_DEFAULT_COUNT = 4
; max count of routes that will be checked (MAX_COUNT + custom routes ) (default: 6)
LIMIT = 6
@@ -151,8 +178,6 @@ LOGIN = login
SESSION_SUSPECT = session_suspect
; account deleted
DELETE_ACCOUNT = account_delete
-; unauthorized request (HTTP 401)
-UNAUTHORIZED = unauthorized
; admin action (e.g. kick, bann) log
ADMIN = admin
; TCP socket errors
@@ -160,7 +185,15 @@ SOCKET_ERROR = socket_error
; debug log for development
DEBUG = debug
+[PATHFINDER.HISTORY]
+; cache time for parsed log files (seconds) (default: 5)
+CACHE = 5
+; file folder for 'history' logs (e.g. map history) (default: history/)
+LOG = history/
+
; API =============================================================================================
[PATHFINDER.API]
+CCP_IMAGE_SERVER = https://image.eveonline.com
+Z_KILLBOARD = https://zkillboard.com/api
; GitHub Developer API
GIT_HUB = https://api.github.com
diff --git a/app/requirements.ini b/app/requirements.ini
index 5d9f6752..f475eede 100644
--- a/app/requirements.ini
+++ b/app/requirements.ini
@@ -30,7 +30,7 @@ ZMQ = 1.1.3
; https://pecl.php.net/package/event
EVENT = 2.3.0
-; max execution time for requests
+; max execution time for requests (seconds)
MAX_EXECUTION_TIME = 10
; max variable size for $_GET, $_POST and $_COOKIE
@@ -39,6 +39,9 @@ MAX_EXECUTION_TIME = 10
; PHP default = 1000
MAX_INPUT_VARS = 3000
+; Formatted HTML StackTraces
+HTML_ERRORS = 0
+
[REQUIREMENTS.LIBS]
ZMQ = 4.1.3
diff --git a/composer-dev.json b/composer-dev.json
index 206241ba..a0b84c78 100644
--- a/composer-dev.json
+++ b/composer-dev.json
@@ -21,8 +21,12 @@
}],
"require": {
"php-64bit": ">=7.0",
- "ext-zmq": "1.1.*",
+ "ext-curl": ">=7.0",
+ "ext-zmq": ">=1.1.3",
"react/zmq": "0.3.*",
+ "monolog/monolog": "1.*",
+ "websoftwares/monolog-zmq-handler": "0.2.*",
+ "swiftmailer/swiftmailer": "^6.0",
"exodus4d/pathfinder_esi": "dev-develop as 0.0.x-dev"
}
}
diff --git a/composer.json b/composer.json
index 2c7e48b2..9b7a8358 100644
--- a/composer.json
+++ b/composer.json
@@ -21,8 +21,12 @@
}],
"require": {
"php-64bit": ">=7.0",
- "ext-zmq": "1.1.*",
+ "ext-curl": ">=7.0",
+ "ext-zmq": ">=1.1.3",
"react/zmq": "0.3.*",
- "exodus4d/pathfinder_esi": "dev-master#v1.1.0"
+ "monolog/monolog": "1.*",
+ "websoftwares/monolog-zmq-handler": "0.2.*",
+ "swiftmailer/swiftmailer": "^6.0",
+ "exodus4d/pathfinder_esi": "dev-master#v1.2.0"
}
}
diff --git a/gulpfile.js b/gulpfile.js
index b57823fd..b39faf7f 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -499,7 +499,7 @@ gulp.task('task:hintJS', () => {
* concat/build JS files by modules
*/
gulp.task('task:concatJS', () => {
- let modules = ['login', 'mappage', 'setup', 'admin', 'notification'];
+ let modules = ['login', 'mappage', 'setup', 'admin', 'notification', 'datatables.loader'];
let srcModules = ['./js/app/*(' + modules.join('|') + ').js'];
return gulp.src(srcModules, {base: 'js'})
@@ -854,11 +854,15 @@ gulp.task(
'production',
gulp.series(
'task:configProduction',
- 'task:cleanJsBuild',
- 'task:cleanCssBuild',
gulp.parallel(
- 'task:buildJs',
- 'task:watchCss'
+ gulp.series(
+ 'task:cleanJsBuild',
+ 'task:buildJs'
+ ),
+ gulp.series(
+ 'task:cleanCssBuild',
+ 'task:watchCss'
+ )
)
)
);
diff --git a/js/app.js b/js/app.js
index 652d7af6..6683161c 100644
--- a/js/app.js
+++ b/js/app.js
@@ -11,7 +11,7 @@ requirejs.config({
paths: {
layout: 'layout',
- config: 'app/config', // path for "configuration" files dir
+ conf: 'app/conf', // path for "config" files dir
dialog: 'app/ui/dialog', // path for "dialog" files dir
templates: '../../templates', // template dir
img: '../../img', // images dir
@@ -59,11 +59,13 @@ requirejs.config({
tweenLite: 'lib/TweenLite.min',
// datatables // v1.10.12 DataTables - https://datatables.net
+ 'datatables.loader': './app/datatables.loader',
'datatables.net': 'lib/datatables/DataTables-1.10.12/js/jquery.dataTables.min',
'datatables.net-buttons': 'lib/datatables/Buttons-1.2.1/js/dataTables.buttons.min',
'datatables.net-buttons-html': 'lib/datatables/Buttons-1.2.1/js/buttons.html5.min',
'datatables.net-responsive': 'lib/datatables/Responsive-2.1.0/js/dataTables.responsive.min',
'datatables.net-select': 'lib/datatables/Select-1.2.0/js/dataTables.select.min',
+ 'datatables.plugins.render.ellipsis': 'lib/datatables/plugins/render/ellipsis',
// notification plugin
pnotify: 'lib/pnotify/pnotify', // v3.0.0 PNotify - notification core file - https://sciactive.com/pnotify/
@@ -94,6 +96,9 @@ requirejs.config({
customScrollbar: {
deps: ['jquery', 'mousewheel']
},
+ 'datatables.loader': {
+ deps: ['jquery']
+ },
'datatables.net': {
deps: ['jquery']
},
@@ -109,6 +114,9 @@ requirejs.config({
'datatables.net-select': {
deps: ['datatables.net']
},
+ 'datatables.plugins.render.ellipsis': {
+ deps: ['datatables.net']
+ },
xEditable: {
deps: ['bootstrap']
},
diff --git a/js/app/admin.js b/js/app/admin.js
index aecb732a..18a7c531 100644
--- a/js/app/admin.js
+++ b/js/app/admin.js
@@ -6,11 +6,7 @@ define([
'jquery',
'app/init',
'app/util',
- 'datatables.net',
- 'datatables.net-buttons',
- 'datatables.net-buttons-html',
- 'datatables.net-responsive',
- 'datatables.net-select'
+ 'datatables.loader'
], function($, Init, Util) {
'use strict';
diff --git a/js/app/config/signature_type.js b/js/app/conf/signature_type.js
similarity index 100%
rename from js/app/config/signature_type.js
rename to js/app/conf/signature_type.js
diff --git a/js/app/config/system_effect.js b/js/app/conf/system_effect.js
similarity index 100%
rename from js/app/config/system_effect.js
rename to js/app/conf/system_effect.js
diff --git a/js/app/datatables.loader.js b/js/app/datatables.loader.js
new file mode 100644
index 00000000..82f451bc
--- /dev/null
+++ b/js/app/datatables.loader.js
@@ -0,0 +1,11 @@
+define([
+ 'datatables.net',
+ 'datatables.net-buttons',
+ 'datatables.net-buttons-html',
+ 'datatables.net-responsive',
+ 'datatables.net-select'
+], (a, b) => {
+ 'use strict';
+
+ // all Datatables stuff is available...
+});
\ No newline at end of file
diff --git a/js/app/init.js b/js/app/init.js
index 0b0e6f8f..79360978 100644
--- a/js/app/init.js
+++ b/js/app/init.js
@@ -31,6 +31,7 @@ define(['jquery'], function($) {
deleteMap: 'api/map/delete', // ajax URL - delete map
importMap: 'api/map/import', // ajax URL - import map
getMapConnectionData: 'api/map/getConnectionData', // ajax URL - get connection data
+ getMapLogData: 'api/map/getLogData', // ajax URL - get logs data
// system API
searchSystem: 'api/system/search', // ajax URL - search system by name
saveSystem: 'api/system/save', // ajax URL - saves system to map
@@ -38,6 +39,7 @@ define(['jquery'], function($) {
getSystemGraphData: 'api/system/graphData', // ajax URL - get all system graph data
getConstellationData: 'api/system/constellationData', // ajax URL - get system constellation data
setDestination: 'api/system/setDestination', // ajax URL - set destination
+ pokeRally: 'api/system/pokeRally', // ajax URL - send rally point pokes
// connection API
saveConnection: 'api/connection/save', // ajax URL - save new connection to map
deleteConnection: 'api/connection/delete', // ajax URL - delete connection from map
@@ -52,10 +54,6 @@ define(['jquery'], function($) {
// GitHub API
gitHubReleases: 'api/github/releases' // ajax URL - get release info from GitHub
},
- url: {
- ccpImageServer: '//image.eveonline.com/', // CCP image Server
- zKillboard: '//zkillboard.com/api/' // killboard api
- },
breakpoints: [
{ name: 'desktop', width: Infinity },
{ name: 'tablet', width: 1200 },
diff --git a/js/app/key.js b/js/app/key.js
index 01a04cdb..5e264c3b 100644
--- a/js/app/key.js
+++ b/js/app/key.js
@@ -63,7 +63,7 @@ define([
};
/**
- * enables some console.log() information
+ * enables some debug output in console
* @type {boolean}
*/
let debug = false;
@@ -297,20 +297,29 @@ define([
// global dom remove listener -------------------------------------------------------------------
// -> check whether the removed element had an event listener active and removes them.
- document.body.addEventListener ('DOMNodeRemoved', function(e){
- if(typeof e.target.getAttribute === 'function'){
- let eventNames = e.target.getAttribute(dataKeyEvents);
- if(eventNames){
- eventNames.split(',').forEach((event) => {
- let index = allEvents[event].elements.indexOf(e.target);
- if(index > -1){
- // remove element from event list
- allEvents[event].elements.splice(index, 1);
+ new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ if(mutation.type === 'childList'){
+ for (let i = 0; i < mutation.removedNodes.length; i++){
+ let removedNode = mutation.removedNodes[i];
+ if(typeof removedNode.getAttribute === 'function'){
+ let eventNames = removedNode.getAttribute(dataKeyEvents);
+ if(eventNames){
+ let events = eventNames.split(',');
+ for(let j = 0; i < events.length; j++){
+ let event = events[j];
+ let index = allEvents[event].elements.indexOf(removedNode);
+ if(index > -1){
+ // remove element from event list
+ allEvents[event].elements.splice(index, 1);
+ }
+ }
+ }
}
- });
+ }
}
- }
- }, false);
+ });
+ }).observe(document.body, { childList: true, subtree: true });
isInit = true;
}
diff --git a/js/app/logging.js b/js/app/logging.js
index 61f62e94..0cfc5cf6 100644
--- a/js/app/logging.js
+++ b/js/app/logging.js
@@ -80,7 +80,7 @@ define([
let showDialog = function(){
// dialog content
- requirejs(['text!templates/dialog/task_manager.html', 'mustache'], function(templateTaskManagerDialog, Mustache) {
+ requirejs(['text!templates/dialog/task_manager.html', 'mustache', 'datatables.loader'], function(templateTaskManagerDialog, Mustache) {
let data = {
id: config.taskDialogId,
dialogDynamicAreaClass: config.dialogDynamicAreaClass,
diff --git a/js/app/login.js b/js/app/login.js
index 2d189bb4..30ed0997 100644
--- a/js/app/login.js
+++ b/js/app/login.js
@@ -676,7 +676,8 @@ define([
dataType: 'json',
context: {
cookieName: requestData.cookie,
- characterElement: characterElement
+ characterElement: characterElement,
+ browserTabId: Util.getBrowserTabId()
}
}).done(function(responseData, textStatus, request){
this.characterElement.hideLoadingAnimation();
@@ -698,9 +699,11 @@ define([
let data = {
link: this.characterElement.data('href'),
cookieName: this.cookieName,
+ browserTabId: this.browserTabId,
character: responseData.character,
authLabel: getCharacterAuthLabel(responseData.character.authStatus),
- authOK: responseData.character.authStatus === 'OK'
+ authOK: responseData.character.authStatus === 'OK',
+ hasActiveSession: responseData.character.hasActiveSession === true
};
let content = Mustache.render(template, data);
@@ -766,6 +769,12 @@ define([
* main init "landing" page
*/
$(function(){
+ // clear sessionStorage
+ Util.clearSessionStorage();
+
+ // set default AJAX config
+ Util.ajaxSetup();
+
// set Dialog default config
Util.initDefaultBootboxConfig();
diff --git a/js/app/map/local.js b/js/app/map/local.js
index 29bdf747..3468c6fd 100644
--- a/js/app/map/local.js
+++ b/js/app/map/local.js
@@ -28,9 +28,9 @@ define([
overlayLocalJumpsClass: 'pf-map-overlay-local-jumps', // class for jump distance for table results
// dataTable
- tableImageCellClass: 'pf-table-image-cell', // class for table "image" cells
- tableActionCellClass: 'pf-table-action-cell', // class for table "action" cells
- tableActionCellIconClass: 'pf-table-action-icon-cell', // class for table "action" icon (icon is part of cell content)
+ tableCellImageClass: 'pf-table-image-cell', // class for table "image" cells
+ tableCellActionClass: 'pf-table-action-cell', // class for table "action" cells
+ tableCellActionIconClass: 'pf-table-action-icon-cell', // class for table "action" icon (icon is part of cell content)
// toolbar
toolbarClass: 'pf-map-overlay-toolbar', // class for toolbar - content
@@ -288,236 +288,240 @@ define([
* @returns {*}
*/
$.fn.initLocalOverlay = function(mapId){
- return this.each(function(){
- let parentElement = $(this);
+ let parentElements = $(this);
- let overlay = $('