diff --git a/app/config.ini b/app/config.ini index 75346ab8..e21a138d 100644 --- a/app/config.ini +++ b/app/config.ini @@ -1,74 +1,123 @@ ; Global Framework Config [SERVER] -SERVER_NAME = PATHFINDER +SERVER_NAME = PATHFINDER [globals] -; Default Verbosity level of the stack trace. -; Assign values between 0 to 3 for increasing verbosity levels. Check (environment.ini) config for overwriting -; (default: 0) -DEBUG = 0 +; Verbosity level of error stack trace for errors +; This affects error logging and stack traces returned to clients on error. +; DEBUG level can be overwritten in environment.ini +; Syntax: 0 | 1 | 2 | 3 +; Default: 0 +DEBUG = 0 -; If TRUE, the framework, after having logged stack trace and errors, stops execution -; -> (die without any status) when a non-fatal error is detected. (default: FALSE) -HALT = FALSE +; How to behave on 'non-fatal' errors +; If TRUE, the framework, after having logged stack trace and errors, stops execution +; (die without any status) when a non-fatal error is detected. +; Tip: You should not change this. +; Syntax: TRUE | FALSE +; Default: FALSE +HALT = FALSE -; Timezone to use. Sync program with eve server time. (default: UTC) -TZ = UTC +; Timezone to use +; Sync Pathfinder with EVE server time. +; Tip: You should not change this. +; Default: UTC +TZ = UTC -; Cache key prefix. Same for all cache values for this installation -; CLI (cronjob) scripts use it for cache manipulation -SEED = {{ md5(@SERVER.SERVER_NAME) }} +; Default language +; Overwrites HTTP Accept-Language request header. +; Used by setlocale() and affects number formatting. +; Syntax: String +; Default: en-US +LANGUAGE = en-US -; Cache backend. Can handle Redis, Memcache module, APC, WinCache, XCache and a filesystem-based cache. -; (default: folder=tmp/cache/) -CACHE = folder=tmp/cache/ -;CACHE = redis=localhost:6379 +; Cache key prefix +; Same for all cache values for this installation. +; CLI (cronjob) scripts use it for cache manipulation. +; Tip: You should not change this. +; Syntax String +; Default: {{ md5(@SERVER.SERVER_NAME) }} +SEED = {{ md5(@SERVER.SERVER_NAME) }} -; 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 +; Cache backend +; Can handle Redis, Memcache module, APC, WinCache, XCache and a filesystem-based cache. +; Tip: Redis is recommended and gives the best performance. +; Syntax: folder=[DIR] | redis=[SERVER] +; Default: folder=tmp/cache/ +; Value: folder=[DIR] +; - Cache data is stored on disc +; redis=[SERVER] +; - Cache data is stored in Redis (e.g. redis=localhost:6379) +CACHE = folder=tmp/cache/ + +; Cache backend used by PHPs Session handler. +; Tip1: Best performance and recommended configuration for Pathfinder is to configured Redis as PHPs default Session handler +; in your php.ini and set 'default' value here in order to use Redis (fastest) +; Tip2: If Redis is not available for you, leave this at 'mysql' (faster than PHPs default files bases Sessions) +; Syntax: mysql | default +; Default: mysql +; Value: mysql +; - Session data get stored in 'pathfinder'.'sessions' table (environment.ini → DB_PF_NAME). +; Table `sessions` is auto created if not exist. +; default +; - Session data get stored in PHPs default Session handler (php.ini → session.save_handler and session.save_path) +; PHPs default session.save_handler is `files` and each Session is written to disc (slowest) +SESSION_CACHE = mysql ; Callback functions ============================================================================== -ONERROR = Controller\Controller->showError -UNLOAD = Controller\Controller->unload +ONERROR = Controller\Controller->showError +UNLOAD = Controller\Controller->unload ; Path configurations ============================================================================= -; relative to "BASE" dir +; All path configurations are relative to BASE dir and should NOT be changed -; Temporary folder for cache, filesystem locks, compiled F3 templates, etc. (default: tmp/) -TEMP = tmp/ +; Temporary folder for cache +; Used for compiled templates. +; Syntax: [DIR] +; Default: tmp/ +TEMP = tmp/ -; Log file folder. (default: logs/) -LOGS = logs/ +; Log file folder +; Syntax: [DIR] +; Default: logs/ +LOGS = logs/ -; UI/template folder. (default: public/) -UI = public/ +; UI folder +; Where all the public assets (templates, images, styles, scripts) are located. +; Syntax: [DIR] +; Default: public/ +UI = public/ -; Autoloader for user-defined PHP classes that the framework will attempt to autoload at runtime. (default: app/main/) -AUTOLOAD = app/main/ +; Autoload folder +; Where PHP attempts to autoload PHP classes at runtime. +; Syntax: [DIR] +; Default: app/main/ +AUTOLOAD = app/main/ -; Favicons. (default: favicon/) -FAVICON = favicon/ +; Favicon folder +; Syntax: [DIR] +; Default: favicon/ +FAVICON = favicon/ -; Export folder (e.g. static table data). (default: export/) -EXPORT = export/ +; Export folder +; Where DB dump files are located/created at. +; Syntax: [DIR] +; Default: export/ +EXPORT = export/ -; Default language (overwrites HTTP Accept-Language request header) used for "setlocale()" affects number formatting. (default: en-US) -LANGUAGE = en-US +; Custom *.ini file folder +; Can be used to overwrite default *.ini files and settings +; See: https://github.com/exodus4d/pathfinder/wiki/Configuration#custom-confpathfinderini +; Syntax: [DIR] +CONF.CUSTOM = conf/ +CONF.DEFAULT = app/ -; custom *.ini file folder, can be used to overwrite default *.ini files -CONF.CUSTOM = conf/ -CONF.DEFAULT = app/ - -; load additional config files -; DO NOT load environment.ini, it is loaded automatically +; Load additional config files +; DO NOT load environment.ini, it is loaded automatically [configs] -{{@CONF.DEFAULT}}routes.ini = true -{{@CONF.DEFAULT}}pathfinder.ini = true -{{@CONF.CUSTOM}}pathfinder.ini = true -{{@CONF.DEFAULT}}requirements.ini = true -{{@CONF.DEFAULT}}cron.ini = true \ No newline at end of file +{{@CONF.DEFAULT}}routes.ini = true +{{@CONF.DEFAULT}}pathfinder.ini = true +{{@CONF.CUSTOM}}pathfinder.ini = true +{{@CONF.DEFAULT}}requirements.ini = true +{{@CONF.DEFAULT}}cron.ini = true \ No newline at end of file diff --git a/app/environment.ini b/app/environment.ini index d963ea7f..1f4be57a 100644 --- a/app/environment.ini +++ b/app/environment.ini @@ -30,6 +30,7 @@ DB_UNIVERSE_PASS = CCP_SSO_URL = https://sisilogin.testeveonline.com CCP_SSO_CLIENT_ID = CCP_SSO_SECRET_KEY = +CCP_SSO_DOWNTIME = 11:00 ; CCP ESI API CCP_ESI_URL = https://esi.tech.ccp.is @@ -82,6 +83,7 @@ DB_CCP_PASS = CCP_SSO_URL = https://login.eveonline.com CCP_SSO_CLIENT_ID = CCP_SSO_SECRET_KEY = +CCP_SSO_DOWNTIME = 11:00 ; CCP ESI API CCP_ESI_URL = https://esi.tech.ccp.is diff --git a/app/main/controller/accesscontroller.php b/app/main/controller/accesscontroller.php index 19c1348b..72322209 100644 --- a/app/main/controller/accesscontroller.php +++ b/app/main/controller/accesscontroller.php @@ -20,7 +20,6 @@ class AccessController extends Controller { * @param $params * @return bool * @throws \Exception - * @throws \Exception\PathfinderException * @throws \ZMQSocketException */ function beforeroute(\Base $f3, $params): bool { @@ -29,15 +28,7 @@ class AccessController extends Controller { // requires a valid logged in user! if( !$this->isLoggedIn($f3) ){ // no character found or login timer expired - $this->logoutCharacter(); - - if($f3->get('AJAX')){ - // unauthorized request - $f3->status(403); - }else{ - // redirect to landing page - $f3->reroute(['login']); - } + $this->logoutCharacter($f3); // skip route handler and afterroute() $return = false; } @@ -51,7 +42,6 @@ class AccessController extends Controller { * @param \Base $f3 * @return bool * @throws \Exception - * @throws \Exception\PathfinderException */ protected function isLoggedIn(\Base $f3): bool { $loginCheck = false; @@ -71,7 +61,6 @@ class AccessController extends Controller { * @param \Base $f3 * @param Model\CharacterModel $character * @return bool - * @throws \Exception\PathfinderException */ private function checkLogTimer(\Base $f3, Model\CharacterModel $character){ $loginCheck = false; @@ -104,7 +93,6 @@ class AccessController extends Controller { * @param Model\MapModel $map * @return int (number of active connections for this map) * @throws \Exception - * @throws \Exception\PathfinderException * @throws \ZMQSocketException */ protected function broadcastMapData(Model\MapModel $map){ @@ -117,7 +105,6 @@ class AccessController extends Controller { * @param Model\MapModel $map * @return array * @throws \Exception - * @throws \Exception\PathfinderException */ protected function getFormattedMapData(Model\MapModel $map){ $mapData = $map->getData(); diff --git a/app/main/controller/admin.php b/app/main/controller/admin.php index de66a957..eb5a5a91 100644 --- a/app/main/controller/admin.php +++ b/app/main/controller/admin.php @@ -37,7 +37,6 @@ class Admin extends Controller{ * @param $params * @return bool * @throws \Exception - * @throws \Exception\PathfinderException */ function beforeroute(\Base $f3, $params): bool { $return = parent::beforeroute($f3, $params); @@ -50,7 +49,7 @@ class Admin extends Controller{ $this->dispatch($f3, $params, $character); } - $f3->set('tplAuthType', $f3->alias( 'sso', ['action' => 'requestAdminAuthorization'])); + $f3->set('tplAuthType', $f3->get('BASE') . $f3->alias( 'sso', ['action' => 'requestAdminAuthorization'])); // page title $f3->set('tplPageTitle', 'Admin | ' . Config::getPathfinderData('name')); @@ -67,7 +66,6 @@ class Admin extends Controller{ /** * event handler after routing * @param \Base $f3 - * @throws \Exception\PathfinderException */ public function afterroute(\Base $f3) { // js view (file) @@ -123,7 +121,6 @@ class Admin extends Controller{ * @param $params * @param null $character * @throws \Exception - * @throws \Exception\PathfinderException */ public function dispatch(\Base $f3, $params, $character = null){ if($character instanceof CharacterModel){ @@ -232,7 +229,6 @@ class Admin extends Controller{ * @param CharacterModel $character * @param int $kickCharacterId * @param int $minutes - * @throws \Exception\PathfinderException */ protected function kickCharacter(CharacterModel $character, $kickCharacterId, $minutes){ $kickOptions = self::KICK_OPTIONS; @@ -262,7 +258,6 @@ class Admin extends Controller{ * @param CharacterModel $character * @param int $banCharacterId * @param int $value - * @throws \Exception\PathfinderException */ protected function banCharacter(CharacterModel $character, $banCharacterId, $value){ $banCharacters = $this->filterValidCharacters($character, $banCharacterId); @@ -309,7 +304,6 @@ class Admin extends Controller{ * @param CharacterModel $character * @param int $mapId * @param int $value - * @throws \Exception\PathfinderException */ protected function activateMap(CharacterModel $character, int $mapId, int $value){ $maps = $this->filterValidMaps($character, $mapId); @@ -322,7 +316,6 @@ class Admin extends Controller{ /** * @param CharacterModel $character * @param int $mapId - * @throws \Exception\PathfinderException */ protected function deleteMap(CharacterModel $character, int $mapId){ $maps = $this->filterValidMaps($character, $mapId); @@ -336,7 +329,6 @@ class Admin extends Controller{ * @param CharacterModel $character * @param int $mapId * @return \DB\CortexCollection[]|MapModel[] - * @throws \Exception\PathfinderException */ protected function filterValidMaps(CharacterModel $character, int $mapId) { $maps = []; @@ -355,7 +347,6 @@ class Admin extends Controller{ * get log file for "admin" logs * @param string $type * @return \Log - * @throws \Exception\PathfinderException */ static function getLogger($type = 'ADMIN'){ return parent::getLogger('ADMIN'); @@ -406,7 +397,6 @@ class Admin extends Controller{ * init /maps page data * @param \Base $f3 * @param CharacterModel $character - * @throws \Exception\PathfinderException */ protected function initMaps(\Base $f3, CharacterModel $character){ $data = (object) []; diff --git a/app/main/controller/api/github.php b/app/main/controller/api/github.php index 20d4f0c2..720b8efd 100644 --- a/app/main/controller/api/github.php +++ b/app/main/controller/api/github.php @@ -29,7 +29,6 @@ class GitHub extends Controller\Controller { /** * get HTTP request options for API (curl) request * @return array - * @throws \Exception\PathfinderException */ protected function getRequestReleaseOptions() : array { $options = $this->getBaseRequestOptions(); @@ -41,7 +40,6 @@ class GitHub extends Controller\Controller { * get HTTP request options for API (curl) request * @param string $text * @return array - * @throws \Exception\PathfinderException */ protected function getRequestMarkdownOptions(string $text) : array { $params = [ @@ -59,7 +57,6 @@ class GitHub extends Controller\Controller { /** * get release information from GitHub * @param \Base $f3 - * @throws \Exception\PathfinderException */ public function releases(\Base $f3){ $cacheKey = 'CACHE_GITHUB_RELEASES'; diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php index 14ce90de..5e38300f 100644 --- a/app/main/controller/api/map.php +++ b/app/main/controller/api/map.php @@ -7,6 +7,7 @@ */ namespace Controller\Api; + use Controller; use data\file\FileHandler; use lib\Config; @@ -50,7 +51,6 @@ class Map extends Controller\AccessController { * Get all required static config data for program initialization * @param \Base $f3 * @throws Exception - * @throws Exception\PathfinderException */ public function initData(\Base $f3){ // expire time in seconds @@ -221,6 +221,7 @@ class Map extends Controller\AccessController { // universe category data --------------------------------------------------------------------------------- $return->universeCategories = [ + 6 => Model\Universe\BasicUniverseModel::getNew('CategoryModel')->getById(6)->getData(['mass']), 65 => Model\Universe\BasicUniverseModel::getNew('CategoryModel')->getById(65)->getData() ]; @@ -572,11 +573,7 @@ class Map extends Controller\AccessController { $return->mapData = $map->getData(); }catch(Exception\ValidationException $e){ - $validationError = (object) []; - $validationError->type = 'error'; - $validationError->field = $e->getField(); - $validationError->message = $e->getMessage(); - $return->error[] = $validationError; + $return->error[] = $e->getError(); } }else{ // map access denied @@ -634,7 +631,6 @@ class Map extends Controller\AccessController { * -> if characters with map access found -> broadcast mapData to them * @param Model\MapModel $map * @throws Exception - * @throws Exception\PathfinderException * @throws \ZMQSocketException */ protected function broadcastMapAccess(Model\MapModel $map){ @@ -708,7 +704,6 @@ class Map extends Controller\AccessController { * -> function is called continuously (trigger) by any active client * @param \Base $f3 * @throws Exception - * @throws Exception\PathfinderException */ public function updateData(\Base $f3){ $postData = (array)$f3->get('POST'); @@ -848,7 +843,6 @@ class Map extends Controller\AccessController { * @param Model\MapModel[] $mapModels * @return array * @throws Exception - * @throws Exception\PathfinderException */ protected function getFormattedMapsData($mapModels){ $mapData = []; @@ -864,7 +858,6 @@ class Map extends Controller\AccessController { * -> function is called continuously by any active client * @param \Base $f3 * @throws Exception - * @throws Exception\PathfinderException */ public function updateUserData(\Base $f3){ $postData = (array)$f3->get('POST'); @@ -1196,7 +1189,6 @@ class Map extends Controller\AccessController { * get map log data * @param \Base $f3 * @throws Exception - * @throws Exception\PathfinderException */ public function getLogData(\Base $f3){ $postData = (array)$f3->get('POST'); diff --git a/app/main/controller/api/rest/abstractrestcontroller.php b/app/main/controller/api/rest/abstractrestcontroller.php new file mode 100644 index 00000000..a782ff35 --- /dev/null +++ b/app/main/controller/api/rest/abstractrestcontroller.php @@ -0,0 +1,45 @@ + $_POST does not include request data -> request BODY might contain JSON + * @param \Base $f3 + * @return array + */ + protected function getRequestData(\Base $f3) : array { + $data = []; + if( !empty($body = $f3->get('BODY')) ){ + $bodyDecode = json_decode($body, true); + if(($jsonError = json_last_error()) === JSON_ERROR_NONE){ + $data = $bodyDecode; + }else{ + $f3->set('HALT', true); + $f3->error(400, 'Request data: ' . json_last_error_msg()); + } + } + + return $data; + } + + /** + * render API response to client + * @param $output + */ + protected function out($output){ + echo json_encode($output); + } + +} \ No newline at end of file diff --git a/app/main/controller/api/connection.php b/app/main/controller/api/rest/connection.php similarity index 56% rename from app/main/controller/api/connection.php rename to app/main/controller/api/rest/connection.php index 8fa19d63..709f0d50 100644 --- a/app/main/controller/api/connection.php +++ b/app/main/controller/api/rest/connection.php @@ -1,50 +1,40 @@ this function is called for update * @param \Base $f3 * @throws \Exception + * @throws \ZMQSocketException */ - public function save(\Base $f3){ - $postData = (array)$f3->get('POST'); - - $return = (object) []; - $return->error = []; - $return->connectionData = (object) []; - - if( - isset($postData['connectionData']) && - isset($postData['mapData']) - ){ - $mapData = (array)$postData['mapData']; - $connectionData = (array)$postData['connectionData']; + public function put(\Base $f3){ + $requestData = $this->getRequestData($f3); + $connectionData = []; + if($mapId = (int)$requestData['mapId']){ $activeCharacter = $this->getCharacter(); - // get map model and check map access /** * @var Model\MapModel $map */ $map = Model\BasicModel::getNew('MapModel'); - $map->getById( (int)$mapData['id'] ); + $map->getById($mapId); - if( $map->hasAccess($activeCharacter) ){ - $source = $map->getSystemById( $connectionData['source'] ); - $target = $map->getSystemById( $connectionData['target'] ); + if($map->hasAccess($activeCharacter)){ + $source = $map->getSystemById((int)$requestData['source']); + $target = $map->getSystemById((int)$requestData['target']); if( !is_null($source) && @@ -54,7 +44,7 @@ class Connection extends Controller\AccessController { * @var $connection Model\ConnectionModel */ $connection = Model\BasicModel::getNew('ConnectionModel'); - $connection->getById( (int)$connectionData['id'] ); + $connection->getById((int)$requestData['id']); $connection->mapId = $map; $connection->source = $source; @@ -65,30 +55,30 @@ class Connection extends Controller\AccessController { $connection->setDefaultTypeData(); if($connection->save($activeCharacter)){ - $return->connectionData = $connection->getData(); + $connectionData = $connection->getData(); // broadcast map changes $this->broadcastMapData($connection->mapId); - }else{ - $return->error = $connection->getErrors(); } } } } - echo json_encode($return); + $this->out($connectionData); } /** - * delete connection * @param \Base $f3 + * @param $params * @throws \Exception + * @throws \ZMQSocketException */ - public function delete(\Base $f3){ - $mapId = (int)$f3->get('POST.mapId'); - $connectionIds = (array)$f3->get('POST.connectionIds'); + public function delete(\Base $f3, $params){ + $requestData = $this->getRequestData($f3); + $connectionIds = array_map('intval', explode(',', (string)$params['id'])); + $deletedConnectionIds = []; - if($mapId){ + if($mapId = (int)$requestData['mapId']){ $activeCharacter = $this->getCharacter(); /** @@ -97,22 +87,23 @@ class Connection extends Controller\AccessController { $map = Model\BasicModel::getNew('MapModel'); $map->getById($mapId); - if( $map->hasAccess($activeCharacter) ){ + if($map->hasAccess($activeCharacter)){ foreach($connectionIds as $connectionId){ - if( $connection = $map->getConnectionById($connectionId) ){ + if($connection = $map->getConnectionById($connectionId)){ $connection->delete( $activeCharacter ); $connection->reset(); + $deletedConnectionIds[] = $connectionId; } } // broadcast map changes - $this->broadcastMapData($map); + if(count($deletedConnectionIds)){ + $this->broadcastMapData($map); + } } - } - echo json_encode([]); + $this->out($deletedConnectionIds); } - -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/main/controller/api/rest/log.php b/app/main/controller/api/rest/log.php new file mode 100644 index 00000000..e019ff5b --- /dev/null +++ b/app/main/controller/api/rest/log.php @@ -0,0 +1,111 @@ +getRequestData($f3); + $connectionData = []; + + if($connectionId = (int)$requestData['connectionId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var Model\ConnectionModel $connection + */ + $connection = Model\BasicModel::getNew('ConnectionModel'); + $connection->getById($connectionId); + + if($connection->hasAccess($activeCharacter)){ + $log = $connection->getNewLog(); + $log->setData($requestData); + $log->record = false; // log not recorded by ESI + $log->save(); + + $connectionData[] = $log->getConnection()->getData(true, true); + } + } + + $this->out($connectionData); + } + + /** + * delete (deactivate) log data + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function delete(\Base $f3, $params){ + $logId = (int)$params['id']; + $connectionData = []; + + if($log = $this->update($logId, ['active' => false])){ + $connectionData[] = $log->getConnection()->getData(true, true); + } + + $this->out($connectionData); + } + + /** + * update log data + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function patch(\Base $f3, $params){ + $logId = (int)$params['id']; + $requestData = $this->getRequestData($f3); + $connectionData = []; + + if($log = $this->update($logId, $requestData)){ + $connectionData[] = $log->getConnection()->getData(true, true); + } + + $this->out($connectionData); + } + + // ---------------------------------------------------------------------------------------------------------------- + + /** + * update existing connectionLog with new data + * @param int $logId + * @param array $logData + * @return bool|Model\ConnectionLogModel + * @throws \Exception + */ + private function update(int $logId, array $logData){ + $log = false; + if($logId){ + $activeCharacter = $this->getCharacter(); + /** + * @var Model\ConnectionLogModel $log + */ + $log = Model\BasicModel::getNew('ConnectionLogModel'); + $log->getById($logId, 0, false); + + if($log->hasAccess($activeCharacter)){ + $log->setData($logData); + + if(isset($logData['active'])){ + $log->setActive((bool)$logData['active']); + } + $log->save(); + } + } + return $log; + } +} \ No newline at end of file diff --git a/app/main/controller/api/rest/system.php b/app/main/controller/api/rest/system.php new file mode 100644 index 00000000..46694ec0 --- /dev/null +++ b/app/main/controller/api/rest/system.php @@ -0,0 +1,190 @@ +getRequestData($f3); + $systemData = []; + + if($mapId = (int)$requestData['mapId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Model\MapModel + */ + $map = Model\BasicModel::getNew('MapModel'); + $map->getById($mapId); + if($map->hasAccess($activeCharacter)){ + $system = $map->getNewSystem($requestData['systemId']); + $systemData = $this->update($system, $requestData)->getData(); + } + } + + $this->out($systemData); + } + + /** + * update existing system + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function patch(\Base $f3, $params){ + $requestData = $this->getRequestData($f3); + $systemData = []; + + if($systemId = (int)$params['id']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $system Model\SystemModel + */ + $system = Model\BasicModel::getNew('SystemModel'); + $system->getById($systemId); + + if($system->hasAccess($activeCharacter)){ + $systemData = $this->update($system, $requestData)->getData(); + } + } + + $this->out($systemData); + } + + /** + * @param \Base $f3 + * @param $params + * @throws \ZMQSocketException + * @throws \Exception + */ + public function delete(\Base $f3, $params){ + $requestData = $this->getRequestData($f3); + $systemIds = array_map('intval', explode(',', (string)$params['id'])); + $deletedSystemIds = []; + + if($mapId = (int)$requestData['mapId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var Model\MapModel $map + */ + $map = Model\BasicModel::getNew('MapModel'); + $map->getById($mapId); + + if($map->hasAccess($activeCharacter)){ + $newSystemModel = Model\BasicModel::getNew('SystemModel'); + foreach($systemIds as $systemId){ + if($system = $map->getSystemById($systemId)){ + // check whether system should be deleted OR set "inactive" + if($this->checkDeleteMode($map, $system)){ + // delete log + // -> first set updatedCharacterId -> required for activity log + $system->updatedCharacterId = $activeCharacter; + $system->update(); + + // ... now get fresh object and delete.. + $newSystemModel->getById($system->_id, 0); + $newSystemModel->erase(); + $newSystemModel->reset(); + }else{ + // keep data -> set "inactive" + $system->setActive(false); + $system->save($activeCharacter); + } + + $system->reset(); + + $deletedSystemIds[] = $systemId; + } + } + // broadcast map changes + if(count($deletedSystemIds)){ + $this->broadcastMapData($map); + } + } + } + + $this->out($deletedSystemIds); + } + + // ---------------------------------------------------------------------------------------------------------------- + + /** + * update system with new data + * @param Model\SystemModel $system + * @param array $systemData + * @return Model\SystemModel + * @throws \ZMQSocketException + * @throws \Exception + */ + private function update(Model\SystemModel $system, array $systemData) : Model\SystemModel { + $activeCharacter = $this->getCharacter(); + + // statusId === 0 is 'auto' status -> keep current status + // -> relevant systems that already have a status (inactive systems) + if( (int)$systemData['statusId'] <= 0 ){ + unset($systemData['statusId']); + } + + if( !$system->dry() ){ + // activate system (e.g. was inactive)) + $system->setActive(true); + } + + $system->setData($systemData); + $system->save($activeCharacter); + + // get data from "fresh" model (e.g. some relational data has changed: "statusId") + /** + * @var $newSystem Model\SystemModel + */ + $newSystem = Model\BasicModel::getNew('SystemModel'); + $newSystem->getById($system->_id, 0); + $newSystem->clearCacheData(); + + // broadcast map changes + $this->broadcastMapData($newSystem->mapId); + + return $newSystem; + } + + /** + * checks whether a system should be "deleted" or set "inactive" (keep some data) + * @param Model\MapModel $map + * @param Model\SystemModel $system + * @return bool + */ + private function checkDeleteMode(Model\MapModel $map, Model\SystemModel $system) : bool { + $delete = true; + + if( !empty($system->description) ){ + // never delete systems with custom description set! + $delete = false; + }elseif( + $map->persistentAliases && + !empty($system->alias) && + ($system->alias != $system->name) + ){ + // map setting "persistentAliases" is active (default) AND + // alias is set and != name + $delete = false; + } + + return $delete; + } + +} \ No newline at end of file diff --git a/app/main/controller/api/route.php b/app/main/controller/api/route.php index cecb0bcd..25272832 100644 --- a/app/main/controller/api/route.php +++ b/app/main/controller/api/route.php @@ -414,7 +414,7 @@ class Route extends Controller\AccessController { * @param array $mapIds * @param array $filterData * @return array - * @throws \Exception\PathfinderException + * @throws \Exception */ public function searchRoute(int $systemFromId, int $systemToId, $searchDepth = 0, array $mapIds = [], array $filterData = []) : array { // search root by ESI API @@ -439,7 +439,6 @@ class Route extends Controller\AccessController { * @param array $filterData * @return array * @throws \Exception - * @throws \Exception\PathfinderException */ private function searchRouteCustom(int $systemFromId, int $systemToId, $searchDepth = 0, array $mapIds = [], array $filterData = []) : array { // reset all previous set jump data @@ -519,7 +518,6 @@ class Route extends Controller\AccessController { * @param array $filterData * @return array * @throws \Exception - * @throws \Exception\PathfinderException */ private function searchRouteESI(int $systemFromId, int $systemToId, int $searchDepth = 0, array $mapIds = [], array $filterData = []) : array { // reset all previous set jump data @@ -645,7 +643,6 @@ class Route extends Controller\AccessController { * search multiple route between two systems * @param \Base $f3 * @throws \Exception - * @throws \Exception\PathfinderException */ public function search($f3){ $requestData = (array)$f3->get('POST'); diff --git a/app/main/controller/api/signature.php b/app/main/controller/api/signature.php index b930e18e..b9f563ba 100644 --- a/app/main/controller/api/signature.php +++ b/app/main/controller/api/signature.php @@ -135,8 +135,13 @@ class Signature extends Controller\AccessController { $system->saveSignature($signature, $activeCharacter); - $updatedSignatureIds[] = $signature->_id; - $return->signatures[] = $signature->getData(); + // TODO figure out why $system->connectionId is NULL after change/save + //-> workaround: get data from "new" $signature model + $signatureNew = Model\BasicModel::getNew('SystemSignatureModel'); + $signatureNew->getById($signature->_id); + + $updatedSignatureIds[] = $signatureNew->_id; + $return->signatures[] = $signatureNew->getData(); $signature->reset(); } diff --git a/app/main/controller/api/statistic.php b/app/main/controller/api/statistic.php index a2eb9d66..b002adb2 100644 --- a/app/main/controller/api/statistic.php +++ b/app/main/controller/api/statistic.php @@ -123,7 +123,6 @@ class Statistic extends Controller\AccessController { * @param int $yearEnd * @param int $weekEnd * @return array - * @throws \Exception\PathfinderException */ protected function queryStatistic( CharacterModel $character, $typeId, $yearStart, $weekStart, $yearEnd, $weekEnd){ $data = []; diff --git a/app/main/controller/api/system.php b/app/main/controller/api/system.php index 890ed6b2..090828e5 100644 --- a/app/main/controller/api/system.php +++ b/app/main/controller/api/system.php @@ -10,7 +10,6 @@ namespace Controller\Api; use Controller; use Model; -use Exception; class System extends Controller\AccessController { @@ -26,94 +25,6 @@ class System extends Controller\AccessController { return sprintf(self::CACHE_KEY_GRAPH, 'SYSTEM_' . $systemId); } - /** - * save a new system to a a map - * @param \Base $f3 - * @throws \Exception - */ - public function save(\Base $f3){ - $postData = (array)$f3->get('POST'); - - $return = (object) []; - $return->error = []; - $return->systemData = (object) []; - - if( - isset($postData['systemData']) && - isset($postData['mapData']) - ){ - $activeCharacter = $this->getCharacter(); - $systemData = (array)$postData['systemData']; - $mapData = (array)$postData['mapData']; - $systemModel = null; - - if( (int)$systemData['statusId'] <= 0 ){ - unset($systemData['statusId']); - } - - if( isset($systemData['id']) ){ - // update existing system (e.g. set description) ------------------------------------------------------ - /** - * @var $system Model\SystemModel - */ - $system = Model\BasicModel::getNew('SystemModel'); - $system->getById($systemData['id']); - if( - !$system->dry() && - $system->hasAccess($activeCharacter) - ){ - // system model found - // activate system (e.g. was inactive)) - $system->setActive(true); - $systemModel = $system; - } - }elseif( isset($mapData['id']) ){ - // save NEW system ------------------------------------------------------------------------------------ - /** - * @var $map Model\MapModel - */ - $map = Model\BasicModel::getNew('MapModel'); - $map->getById($mapData['id']); - if($map->hasAccess($activeCharacter)){ - $systemModel = $map->getNewSystem($systemData['systemId']); - } - } - - if( !is_null($systemModel) ){ - try{ - // set/update system custom data - $systemModel->copyfrom($systemData, ['statusId', 'locked', 'rallyUpdated', 'position', 'description']); - - 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(); - } - }catch(Exception\ValidationException $e){ - $validationError = (object) []; - $validationError->type = 'error'; - $validationError->field = $e->getField(); - $validationError->message = $e->getMessage(); - $return->error[] = $validationError; - } - - - } - } - - echo json_encode($return); - } - /** * get system log data from CCP API import * system Kills, Jumps,.... @@ -246,7 +157,6 @@ class System extends Controller\AccessController { * send Rally Point poke * @param \Base $f3 * @throws \Exception - * @throws \Exception\PathfinderException */ public function pokeRally(\Base $f3){ $rallyData = (array)$f3->get('POST'); @@ -301,87 +211,5 @@ class System extends Controller\AccessController { echo json_encode($return); } - /** - * delete systems and all its connections from map - * -> set "active" flag - * @param \Base $f3 - * @throws \Exception - */ - public function delete(\Base $f3){ - $mapId = (int)$f3->get('POST.mapId'); - $systemIds = array_map('intval', (array)$f3->get('POST.systemIds')); - - $return = (object) []; - $return->deletedSystemIds = []; - - if($mapId){ - $activeCharacter = $this->getCharacter(); - - /** - * @var Model\MapModel $map - */ - $map = Model\BasicModel::getNew('MapModel'); - $map->getById($mapId); - - if($map->hasAccess($activeCharacter)){ - $newSystemModel = Model\BasicModel::getNew('SystemModel'); - foreach($systemIds as $systemId){ - if( $system = $map->getSystemById($systemId) ){ - // check whether system should be deleted OR set "inactive" - if( $this->checkDeleteMode($map, $system) ){ - // delete log - // -> first set updatedCharacterId -> required for activity log - $system->updatedCharacterId = $activeCharacter; - $system->update(); - - // ... now get fresh object and delete.. - $newSystemModel->getById( $system->id, 0); - $newSystemModel->erase(); - $newSystemModel->reset(); - }else{ - // keep data -> set "inactive" - $system->setActive(false); - $system->save($activeCharacter); - } - - $system->reset(); - - $return->deletedSystemIds[] = $systemId; - } - } - // broadcast map changes - if(count($return->deletedSystemIds)){ - $this->broadcastMapData($map); - } - } - } - - echo json_encode($return); - } - - /** - * checks whether a system should be "deleted" or set "inactive" (keep some data) - * @param Model\MapModel $map - * @param Model\SystemModel $system - * @return bool - */ - protected function checkDeleteMode(Model\MapModel $map, Model\SystemModel $system){ - $delete = true; - - if( !empty($system->description) ){ - // never delete systems with custom description set! - $delete = false; - }elseif( - $map->persistentAliases && - !empty($system->alias) && - ($system->alias != $system->name) - ){ - // map setting "persistentAliases" is active (default) AND - // alias is set and != name - $delete = false; - } - - return $delete; - } } diff --git a/app/main/controller/api/user.php b/app/main/controller/api/user.php index 3d4d4464..2afca5b8 100644 --- a/app/main/controller/api/user.php +++ b/app/main/controller/api/user.php @@ -109,7 +109,6 @@ class User extends Controller\Controller{ * -> return character data (if valid) * @param \Base $f3 * @throws Exception - * @throws Exception\PathfinderException */ public function getCookieCharacter(\Base $f3){ $data = $f3->get('POST'); @@ -201,15 +200,10 @@ class User extends Controller\Controller{ /** * log the current user out + clear character system log data * @param \Base $f3 - * @throws Exception * @throws \ZMQSocketException */ public function logout(\Base $f3){ - $this->logoutCharacter(false, true, true, true); - - $return = (object) []; - $return->reroute = rtrim(self::getEnvironmentData('URL'), '/') . $f3->alias('login'); - echo json_encode($return); + $this->logoutCharacter($f3, false, true, true, true); } /** @@ -345,17 +339,9 @@ class User extends Controller\Controller{ } }catch(Exception\ValidationException $e){ - $validationError = (object) []; - $validationError->type = 'error'; - $validationError->field = $e->getField(); - $validationError->message = $e->getMessage(); - $return->error[] = $validationError; + $return->error[] = $e->getError(); }catch(Exception\RegistrationException $e){ - $registrationError = (object) []; - $registrationError->type = 'error'; - $registrationError->field = $e->getField(); - $registrationError->message = $e->getMessage(); - $return->error[] = $registrationError; + $return->error[] = $e->getError(); } // return new/updated user data @@ -394,10 +380,8 @@ class User extends Controller\Controller{ sprintf(self::LOG_DELETE_ACCOUNT, $user->id, $user->name) ); - $this->logoutCharacter(true, true, true, true); + $this->logoutCharacter($f3, true, true, true, true); $user->erase(); - - $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 305b2c10..6079c365 100644 --- a/app/main/controller/appcontroller.php +++ b/app/main/controller/appcontroller.php @@ -29,7 +29,7 @@ class AppController extends Controller { if($return = parent::beforeroute($f3, $params)){ // href for SSO Auth - $f3->set('tplAuthType', $f3->alias( 'sso', ['action' => 'requestAuthorization'] )); + $f3->set('tplAuthType', $f3->get('BASE') . $f3->alias( 'sso', ['action' => 'requestAuthorization'] )); // characters from cookies $f3->set('cookieCharacters', $this->getCookieByName(self::COOKIE_PREFIX_CHARACTER, true)); diff --git a/app/main/controller/ccp/sso.php b/app/main/controller/ccp/sso.php index 7c34fdff..7cbd3d5e 100644 --- a/app/main/controller/ccp/sso.php +++ b/app/main/controller/ccp/sso.php @@ -52,7 +52,6 @@ class Sso extends Api\User{ * redirect user to CCP SSO page and request authorization * -> cf. Controller->getCookieCharacters() ( equivalent cookie based login) * @param \Base $f3 - * @throws \Exception\PathfinderException */ public function requestAdminAuthorization($f3){ // store browser tabId to be "targeted" after login @@ -67,7 +66,6 @@ class Sso extends Api\User{ * -> cf. Controller->getCookieCharacters() ( equivalent cookie based login) * @param \Base $f3 * @throws \Exception - * @throws \Exception\PathfinderException */ public function requestAuthorization($f3){ $params = $f3->get('GET'); @@ -133,7 +131,6 @@ class Sso extends Api\User{ * @param \Base $f3 * @param array $scopes * @param string $rootAlias - * @throws \Exception\PathfinderException */ private function rerouteAuthorization(\Base $f3, $scopes = [], $rootAlias = 'login'){ if( !empty( Controller\Controller::getEnvironmentData('CCP_SSO_CLIENT_ID') ) ){ @@ -166,7 +163,6 @@ class Sso extends Api\User{ * -> see requestAuthorization() * @param \Base $f3 * @throws \Exception - * @throws \Exception\PathfinderException */ public function callbackAuthorization($f3){ $getParams = (array)$f3->get('GET'); @@ -307,7 +303,6 @@ class Sso extends Api\User{ * login by cookie name * @param \Base $f3 * @throws \Exception - * @throws \Exception\PathfinderException */ public function login(\Base $f3){ $data = (array)$f3->get('GET'); @@ -345,7 +340,6 @@ class Sso extends Api\User{ * -> else try to refresh auth and get fresh "access_token" * @param bool $authCode * @return null|\stdClass - * @throws \Exception\PathfinderException */ public function getSsoAccessData($authCode){ $accessData = null; @@ -365,7 +359,6 @@ class Sso extends Api\User{ * verify authorization code, and get an "access_token" data * @param $authCode * @return \stdClass - * @throws \Exception\PathfinderException */ protected function verifyAuthorizationCode($authCode){ $requestParams = [ @@ -381,7 +374,6 @@ class Sso extends Api\User{ * -> if "access_token" is expired, this function gets a fresh one * @param $refreshToken * @return \stdClass - * @throws \Exception\PathfinderException */ public function refreshAccessToken($refreshToken){ $requestParams = [ @@ -398,7 +390,6 @@ class Sso extends Api\User{ * OR by providing a valid "refresh_token" * @param $requestParams * @return \stdClass - * @throws \Exception\PathfinderException */ protected function requestAccessData($requestParams){ $verifyAuthCodeUrl = self::getVerifyAuthorizationCodeEndpoint(); @@ -463,7 +454,6 @@ class Sso extends Api\User{ * -> if more character information is required, use ESI "characters" endpoints request instead * @param $accessToken * @return mixed|null - * @throws \Exception\PathfinderException */ public function verifyCharacterData($accessToken){ $verifyUserUrl = self::getVerifyUserEndpoint(); @@ -586,7 +576,6 @@ class Sso extends Api\User{ * get CCP SSO url from configuration file * -> throw error if url is broken/missing * @return string - * @throws \Exception\PathfinderException */ static function getSsoUrlRoot(){ $url = ''; @@ -616,7 +605,6 @@ class Sso extends Api\User{ /** * get logger for SSO logging * @return \Log - * @throws \Exception\PathfinderException */ static function getSSOLogger(){ return parent::getLogger('SSO'); diff --git a/app/main/controller/controller.php b/app/main/controller/controller.php index 8e09414b..065b0440 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 Exception\PathfinderException; use lib\Config; use lib\Resource; use lib\Monolog; @@ -66,7 +67,6 @@ class Controller { * @param \Base $f3 * @param $params * @return bool - * @throws \Exception\PathfinderException */ function beforeroute(\Base $f3, $params): bool { // initiate DB connection @@ -103,10 +103,10 @@ class Controller { header($resource->buildHeader(), false); } - if($this->getTemplate()){ + if($file = $this->getTemplate()){ // Ajax calls don´t need a page render.. // this happens on client side - echo \Template::instance()->render( $this->getTemplate() ); + echo \Template::instance()->render($file); } } @@ -131,7 +131,6 @@ class Controller { * @param $session * @param $sid * @return bool - * @throws \Exception\PathfinderException */ $onSuspect = function($session, $sid){ self::getLogger('SESSION_SUSPECT')->write( sprintf( @@ -160,7 +159,6 @@ class Controller { /** * init new Resource handler * @param \Base $f3 - * @throws \Exception\PathfinderException */ protected function initResource(\Base $f3){ $resource = Resource::instance(); @@ -228,7 +226,6 @@ class Controller { * -> store validation data in DB * @param Model\CharacterModel $character * @throws \Exception - * @throws \Exception\PathfinderException */ protected function setLoginCookie(Model\CharacterModel $character){ if( $this->getCookieState() ){ @@ -286,7 +283,6 @@ class Controller { * @param bool $checkAuthorization * @return Model\CharacterModel[] * @throws \Exception - * @throws \Exception\PathfinderException */ protected function getCookieCharacters($cookieData = [], $checkAuthorization = true){ $characters = []; @@ -489,15 +485,16 @@ class Controller { /** * log out current character or all active characters (multiple browser tabs) + * -> send response data to client + * @param \Base $f3 * @param bool $all * @param bool $deleteSession * @param bool $deleteLog * @param bool $deleteCookie - * @throws \Exception * @throws \ZMQSocketException */ - 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); + protected function logoutCharacter(\Base $f3, bool $all = false, bool $deleteSession = true, bool $deleteLog = true, bool $deleteCookie = false){ + $sessionCharacterData = (array)$f3->get(Api\User::SESSION_KEY_CHARACTERS); if($sessionCharacterData){ $activeCharacterId = ($activeCharacter = $this->getCharacter()) ? $activeCharacter->_id : 0; @@ -523,6 +520,20 @@ class Controller { (new Socket( Config::getSocketUri() ))->sendData('characterLogout', $characterIds); } } + + if($f3->get('AJAX')){ + $status = 403; + $f3->status($status); + + $return = (object) []; + $return->reroute = rtrim(self::getEnvironmentData('URL'), '/') . $f3->alias('login'); + $return->error[] = $this->getErrorObject($status, Config::getMessageFromHTTPStatus($status)); + + echo json_encode($return); + }else{ + // redirect to landing page + $f3->reroute(['login']); + } } /** @@ -627,7 +638,6 @@ class Controller { /** * get a custom userAgent string for API calls * @return string - * @throws \Exception\PathfinderException */ protected function getUserAgent(){ $userAgent = ''; @@ -663,18 +673,26 @@ class Controller { * -> on HTTP request -> render error page * @param \Base $f3 * @return bool - * @throws \Exception\PathfinderException */ public function showError(\Base $f3){ 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 - ); + $errorData = $f3->get('ERROR'); + $exception = $f3->get('EXCEPTION'); + + if($exception instanceof PathfinderException){ + // ... handle Pathfinder exceptions (e.g. validation Exceptions,..) + $error = $exception->getError(); + }else{ + // ... handle error $f3->error() calls + $error = $this->getErrorObject( + $errorData['code'], + $errorData['status'], + $errorData['text'], + $f3->get('DEBUG') >= 1 ? $errorData['trace'] : null + ); + } // check if error is a PDO Exception ---------------------------------------------------------------------- if(strpos(strtolower( $f3->get('ERROR.text') ), 'duplicate') !== false){ @@ -725,24 +743,6 @@ class Controller { * @return bool */ public function unload(\Base $f3){ - // track some 4xx Client side errors - // 5xx errors are handled in "ONERROR" callback - $status = http_response_code(); - 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)); - } - - echo json_encode($return); - } - } - // store all user activities that are buffered for logging in this request // this should work even on non HTTP200 responses $this->logActivities(); @@ -887,7 +887,6 @@ class Controller { * get the current registration status * 0=registration stop |1=new registration allowed * @return int - * @throws \Exception\PathfinderException */ static function getRegistrationStatus(){ return (int)Config::getPathfinderData('registration.status'); @@ -898,7 +897,6 @@ class Controller { * -> set in pathfinder.ini * @param string $type * @return \Log|null - * @throws \Exception\PathfinderException */ static function getLogger($type){ return LogController::getLogger($type); diff --git a/app/main/controller/logcontroller.php b/app/main/controller/logcontroller.php index 891916b9..f8fd88bb 100644 --- a/app/main/controller/logcontroller.php +++ b/app/main/controller/logcontroller.php @@ -163,7 +163,6 @@ class LogController extends \Prefab { * get Logger instance * @param string $type * @return \Log|null - * @throws \Exception\PathfinderException */ public static function getLogger($type){ $logFiles = Config::getPathfinderData('logfiles'); diff --git a/app/main/controller/mapcontroller.php b/app/main/controller/mapcontroller.php index f3c0049d..c898be0d 100644 --- a/app/main/controller/mapcontroller.php +++ b/app/main/controller/mapcontroller.php @@ -16,7 +16,6 @@ class MapController extends AccessController { /** * @param \Base $f3 * @throws \Exception - * @throws \Exception\PathfinderException */ public function init(\Base $f3) { $character = $this->getCharacter(); diff --git a/app/main/controller/setup.php b/app/main/controller/setup.php index e7031daf..dff7dfe0 100644 --- a/app/main/controller/setup.php +++ b/app/main/controller/setup.php @@ -39,6 +39,7 @@ class Setup extends Controller { 'CCP_SSO_URL', 'CCP_SSO_CLIENT_ID', 'CCP_SSO_SECRET_KEY', + 'CCP_SSO_DOWNTIME', 'CCP_ESI_URL', 'CCP_ESI_DATASOURCE', 'SMTP_HOST', @@ -137,7 +138,6 @@ class Setup extends Controller { * @param \Base $f3 * @param array $params * @return bool - * @throws \Exception\PathfinderException */ function beforeroute(\Base $f3, $params): bool { $this->initResource($f3); @@ -162,7 +162,6 @@ class Setup extends Controller { /** * @param \Base $f3 - * @throws \Exception\PathfinderException */ public function afterroute(\Base $f3) { // js view (file) @@ -789,7 +788,6 @@ class Setup extends Controller { * get default map config * @param \Base $f3 * @return array - * @throws \Exception\PathfinderException */ protected function getMapsDefaultConfig(\Base $f3): array { $matrix = \Matrix::instance(); @@ -1517,8 +1515,8 @@ class Setup extends Controller { */ protected function invalidateCookies(\Base $f3){ $this->getDB('PF'); - $authentidationModel = Model\BasicModel::getNew('CharacterAuthenticationModel'); - $results = $authentidationModel->find(); + $authenticationModel = Model\BasicModel::getNew('CharacterAuthenticationModel'); + $results = $authenticationModel->find(); if($results){ foreach($results as $result){ $result->erase(); @@ -1537,7 +1535,7 @@ class Setup extends Controller { if($bytes){ $base = log($bytes, 1024); $suffixes = array('', 'KB', 'M', 'GB', 'TB'); - $result = round(pow(1024, $base - floor($base)), $precision) .''. $suffixes[floor($base)]; + $result = round(pow(1024, $base - floor($base)), $precision) .''. $suffixes[(int)floor($base)]; } return $result; } diff --git a/app/main/cron/mapupdate.php b/app/main/cron/mapupdate.php index 19e8e79d..24c62c5e 100644 --- a/app/main/cron/mapupdate.php +++ b/app/main/cron/mapupdate.php @@ -22,7 +22,6 @@ class MapUpdate extends AbstractCron { * deactivate all "private" maps whose lifetime is over * >> php index.php "/cron/deactivateMapData" * @param \Base $f3 - * @throws \Exception\PathfinderException */ function deactivateMapData(\Base $f3){ $this->setMaxExecutionTime(); diff --git a/app/main/db/database.php b/app/main/db/database.php index c947b6fd..91d121f6 100644 --- a/app/main/db/database.php +++ b/app/main/db/database.php @@ -116,7 +116,6 @@ class Database extends \Prefab { * @param string $password * @param string $alias * @return SQL|null - * @throws \Exception\PathfinderException */ protected function connect($dns, $name, $user, $password, $alias){ $db = null; @@ -128,7 +127,7 @@ class Database extends \Prefab { ]; // set ERRMODE depending on pathfinders global DEBUG level - if($f3->get('DEBUG') >= 3){ + if($f3->get('DEBUG') >= 1){ $options[\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_WARNING; }else{ $options[\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_EXCEPTION; @@ -286,7 +285,6 @@ class Database extends \Prefab { /** * get logger for DB logging * @return \Log - * @throws \Exception\PathfinderException */ static function getLogger(){ return LogController::getLogger('ERROR'); diff --git a/app/main/exception/baseexception.php b/app/main/exception/baseexception.php deleted file mode 100644 index af4b580c..00000000 --- a/app/main/exception/baseexception.php +++ /dev/null @@ -1,23 +0,0 @@ - 500 + ]; + +} \ No newline at end of file diff --git a/app/main/exception/databaseexception.php b/app/main/exception/databaseexception.php index a6a72bec..3aafc3c1 100644 --- a/app/main/exception/databaseexception.php +++ b/app/main/exception/databaseexception.php @@ -8,9 +8,13 @@ namespace Exception; -class DatabaseException extends BaseException { +class DatabaseException extends PathfinderException { + + protected $codes = [ + 1500 => 500 + ]; public function __construct(string $message){ - parent::__construct($message, self::DB_EXCEPTION); + parent::__construct($message, 1500); } } \ No newline at end of file diff --git a/app/main/exception/dateexception.php b/app/main/exception/dateexception.php new file mode 100644 index 00000000..4b578fa7 --- /dev/null +++ b/app/main/exception/dateexception.php @@ -0,0 +1,16 @@ + 500 // invalid DateRange + ]; +} \ No newline at end of file diff --git a/app/main/exception/pathfinderexception.php b/app/main/exception/pathfinderexception.php index f5aaad1a..2636e1d4 100644 --- a/app/main/exception/pathfinderexception.php +++ b/app/main/exception/pathfinderexception.php @@ -1,17 +1,61 @@ can be specified by using custom Exception codes + */ + const DEFAULT_RESPONSECODE = 500; + + /** + * lists all exception codes + * @var array + */ + protected $codes = [ + 0 => self::DEFAULT_RESPONSECODE + ]; + + public function __construct(string $message, int $code = 0){ + if( !array_key_exists($code, $this->codes) ){ + // exception code not specified by child class + $code = 0; + } + parent::__construct($message, $code); } -} \ No newline at end of file + + /** + * get error object + * @return \stdClass + */ + public function getError() : \stdClass { + $error = (object) []; + $error->type = 'error'; + $error->code = $this->getResponseCode(); + $error->status = Config::getHttpStatusByCode($this->getResponseCode()); + $error->message = $this->getMessage(); + if(\Base::instance()->get('DEBUG') >= 1){ + $error->trace = preg_split('/\R/', $this->getTraceAsString()); // no $this->>getTrace() here -> to much data + } + return $error; + } + + /** + * returns the HTTP response code for the client from exception + * -> if Exception is not handled/catched 'somewhere' this code is used by the final onError handler + * @return int + */ + public function getResponseCode() : int { + return $this->codes[$this->getCode()]; + } +} \ No newline at end of file diff --git a/app/main/exception/registrationexception.php b/app/main/exception/registrationexception.php index d91cbdc9..b98e107b 100644 --- a/app/main/exception/registrationexception.php +++ b/app/main/exception/registrationexception.php @@ -9,7 +9,11 @@ namespace Exception; -class RegistrationException extends BaseException{ +class RegistrationException extends PathfinderException{ + + protected $codes = [ + 2000 => 403 + ]; /** * form field name that causes this exception @@ -17,22 +21,18 @@ class RegistrationException extends BaseException{ */ private $field; - /** - * @return mixed - */ - public function getField(){ - return $this->field; - } - - /** - * @param mixed $field - */ - public function setField($field){ + public function __construct(string $message, string $field = ''){ + parent::__construct($message, 2000); $this->field = $field; } - public function __construct($message, $field = ''){ - parent::__construct($message, self::REGISTRATION_EXCEPTION); - $this->setField($field); + /** + * get error object + * @return \stdClass + */ + public function getError() : \stdClass { + $error = parent::getError(); + $error->field = $this->field; + return $error; } } \ No newline at end of file diff --git a/app/main/exception/validationexception.php b/app/main/exception/validationexception.php index 200f4d0c..abfbf668 100644 --- a/app/main/exception/validationexception.php +++ b/app/main/exception/validationexception.php @@ -9,7 +9,11 @@ namespace Exception; -class ValidationException extends BaseException { +class ValidationException extends PathfinderException { + + protected $codes = [ + 2000 => 422 + ]; /** * table column that triggers the exception @@ -17,35 +21,18 @@ class ValidationException extends BaseException { */ private $field; - /** - * @return string - */ - public function getField(): string { - return $this->field; - } - - /** - * @param string $field - */ - public function setField(string $field){ - $this->field = $field; - } - - public function __construct(string $message, string $field = ''){ - parent::__construct($message, self::VALIDATION_EXCEPTION); - $this->setField($field); + parent::__construct($message, 2000); + $this->field = $field; } /** * get error object * @return \stdClass */ - public function getError(){ - $error = (object) []; - $error->type = 'error'; - $error->field = $this->getField(); - $error->message = $this->getMessage(); + public function getError() : \stdClass { + $error = parent::getError(); + $error->field = $this->field; return $error; } } \ No newline at end of file diff --git a/app/main/lib/DateRange.php b/app/main/lib/DateRange.php new file mode 100644 index 00000000..3327ebb6 --- /dev/null +++ b/app/main/lib/DateRange.php @@ -0,0 +1,55 @@ +from = $from; + $this->to = $to; + } else { + $this->from = $to; + $this->to = $from; + } + } + } + + /** + * check if DateTime $dateCheck is within this range + * @param \DateTime $dateCheck + * @return bool + */ + public function inRange(\DateTime $dateCheck) : bool { + return $dateCheck >= $this->from && $dateCheck <= $this->to; + } +} \ No newline at end of file diff --git a/app/main/lib/Monolog.php b/app/main/lib/Monolog.php index 9fd71659..b3fc008c 100644 --- a/app/main/lib/Monolog.php +++ b/app/main/lib/Monolog.php @@ -36,8 +36,8 @@ class Monolog extends \Prefab { 'mail' => 'Monolog\Handler\SwiftMailerHandler', 'slackMap' => 'lib\logging\handler\SlackMapWebhookHandler', 'slackRally' => 'lib\logging\handler\SlackRallyWebhookHandler', - 'discordMap' => 'lib\logging\handler\SlackMapWebhookHandler', // use Slack handler for Discord - 'discordRally' => 'lib\logging\handler\SlackRallyWebhookHandler', // use Slack handler for Discord + 'discordMap' => 'lib\logging\handler\DiscordMapWebhookHandler', + 'discordRally' => 'lib\logging\handler\DiscordRallyWebhookHandler', 'zmq' => 'lib\logging\handler\ZMQHandler' ]; diff --git a/app/main/lib/ccpclient.php b/app/main/lib/ccpclient.php index 9c3c5e95..53ec41c6 100644 --- a/app/main/lib/ccpclient.php +++ b/app/main/lib/ccpclient.php @@ -24,7 +24,6 @@ class CcpClient extends \Prefab { * get ApiClient instance * @param \Base $f3 * @return ApiClient|null - * @throws \Exception\PathfinderException */ protected function getClient(\Base $f3){ $client = null; @@ -45,7 +44,6 @@ class CcpClient extends \Prefab { /** * @return string - * @throws \Exception\PathfinderException */ protected function getUserAgent(){ $userAgent = ''; @@ -71,7 +69,6 @@ class CcpClient extends \Prefab { * @param $name * @param $arguments * @return array|mixed - * @throws \Exception\PathfinderException */ public function __call($name, $arguments){ $return = []; diff --git a/app/main/lib/config.php b/app/main/lib/config.php index cec90539..75c4e3c2 100644 --- a/app/main/lib/config.php +++ b/app/main/lib/config.php @@ -30,6 +30,9 @@ class Config extends \Prefab { */ const ARRAY_KEYS = ['CCP_ESI_SCOPES', 'CCP_ESI_SCOPES_ADMIN']; + const + HTTP_422='Unprocessable Entity'; + /** * all environment data * @var array @@ -208,7 +211,6 @@ class Config extends \Prefab { /** * get SMTP config values * @return \stdClass - * @throws Exception\PathfinderException */ static function getSMTPConfig(): \stdClass{ $config = new \stdClass(); @@ -253,7 +255,6 @@ class Config extends \Prefab { * get email for notifications by hive key * @param $key * @return mixed - * @throws Exception\PathfinderException */ static function getNotificationMail($key){ return self::getPathfinderData('notification' . ($key ? '.' . $key : '')); @@ -264,7 +265,6 @@ class Config extends \Prefab { * -> read from pathfinder.ini * @param string $mapType * @return mixed - * @throws Exception\PathfinderException */ static function getMapsDefaultConfig($mapType = ''){ if( $mapConfig = self::getPathfinderData('map' . ($mapType ? '.' . $mapType : '')) ){ @@ -373,27 +373,71 @@ class Config extends \Prefab { ){ $uri = 'tcp://' . $ip . ':' . $port; } - return $uri; } /** * @param string $key * @return null|mixed - * @throws Exception\PathfinderException */ static function getPathfinderData($key = ''){ $hiveKey = self::HIVE_KEY_PATHFINDER . ($key ? '.' . strtoupper($key) : ''); $data = null; // make sure it is always defined try{ if( !\Base::instance()->exists($hiveKey, $data) ){ - throw new Exception\PathfinderException(sprintf(self::ERROR_CONF_PATHFINDER, $hiveKey)); + throw new Exception\ConfigException(sprintf(self::ERROR_CONF_PATHFINDER, $hiveKey)); } - }catch (Exception\PathfinderException $e){ + }catch (Exception\ConfigException $e){ LogController::getLogger('ERROR')->write($e->getMessage()); } - return $data; } + /** + * get HTTP status message by HTTP return code + * -> either from F3 or from self::Config constants + * @param int $code + * @return string + */ + static function getHttpStatusByCode(int $code) : string { + if(empty($status = @constant('Base::HTTP_' . $code))){ + $status = @constant('self::HTTP_' . $code); + } + return $status; + } + + /** + * check if a given DateTime() is within downTime range: downtime + 10m + * -> can be used for prevent logging errors during downTime + * @param \DateTime|null $dateCheck + * @return bool + * @throws Exception\DateException + * @throws \Exception + */ + static function inDownTimeRange(\DateTime $dateCheck = null) : bool { + // default daily downtime 00:00am + $downTimeParts = [0, 0]; + if( !empty($downTime = (string)self::getEnvironmentData('CCP_SSO_DOWNTIME')) ){ + $parts = array_map('intval', explode(':', $downTime)); + if(count($parts) === 2){ + // well formatted DOWNTIME found in config files + $downTimeParts = $parts; + } + } + + // downTime Range is 10m + $downtimeInterval = new \DateInterval('PT10M'); + $timezone = \Base::instance()->get('getTimeZone')(); + + // if set -> use current time + $dateCheck = is_null($dateCheck) ? new \DateTime('now', $timezone) : $dateCheck; + $dateDowntimeStart = new \DateTime('now', $timezone); + $dateDowntimeStart->setTime($downTimeParts[0],$downTimeParts[1]); + $dateDowntimeEnd = clone $dateDowntimeStart; + $dateDowntimeEnd->add($downtimeInterval); + + $dateRange = new DateRange($dateDowntimeStart, $dateDowntimeEnd); + return $dateRange->inRange($dateCheck); + } + } \ No newline at end of file diff --git a/app/main/lib/logging/AbstractCharacterLog.php b/app/main/lib/logging/AbstractCharacterLog.php index 1580b017..a4083070 100644 --- a/app/main/lib/logging/AbstractCharacterLog.php +++ b/app/main/lib/logging/AbstractCharacterLog.php @@ -68,7 +68,6 @@ abstract class AbstractCharacterLog extends AbstractChannelLog{ /** * get character thumbnailUrl * @return string - * @throws \Exception\PathfinderException */ protected function getThumbUrl(): string { $url = ''; diff --git a/app/main/lib/logging/RallyLog.php b/app/main/lib/logging/RallyLog.php index 7fa1b577..b6171c3c 100644 --- a/app/main/lib/logging/RallyLog.php +++ b/app/main/lib/logging/RallyLog.php @@ -37,7 +37,6 @@ class RallyLog extends AbstractCharacterLog{ /** * @return string - * @throws \Exception\PathfinderException */ protected function getThumbUrl() : string{ $url = ''; diff --git a/app/main/lib/logging/handler/AbstractMapWebhookHandler.php b/app/main/lib/logging/handler/AbstractMapWebhookHandler.php new file mode 100644 index 00000000..0a01ff32 --- /dev/null +++ b/app/main/lib/logging/handler/AbstractMapWebhookHandler.php @@ -0,0 +1,99 @@ +getTimestamp(); + $text = ''; + + if ( + $this->useAttachment && + !empty( $attachmentsData = $record['context']['data']) + ) { + + // convert non grouped data (associative array) to multi dimensional (sequential) array + // -> see "group" records + $attachmentsData = Util::is_assoc($attachmentsData) ? [$attachmentsData] : $attachmentsData; + + $thumbData = (array)$record['extra']['thumb']; + + $postData['attachments'] = []; + + foreach($attachmentsData as $attachmentData){ + $channelData = (array)$attachmentData['channel']; + $characterData = (array)$attachmentData['character']; + $formatted = (string)$attachmentData['formatted']; + + // get "message" from $formatted + $msgParts = explode('|', $formatted, 2); + + // build main text from first Attachment (they belong to same channel) + if(!empty($channelData)){ + $text = "*Map '" . $channelData['channelName'] . "'* _#" . $channelData['channelId'] . "_ *changed*"; + } + + $attachment = [ + 'title' => !empty($msgParts[0]) ? $msgParts[0] : 'No Title', + //'pretext' => '', + 'text' => !empty($msgParts[1]) ? sprintf('```%s```', $msgParts[1]) : '', + 'fallback' => !empty($msgParts[1]) ? $msgParts[1] : 'No Fallback', + 'color' => $this->getAttachmentColor($tag), + 'fields' => [], + 'mrkdwn_in' => ['fields', 'text'], + 'footer' => 'Pathfinder API', + //'footer_icon'=> '', + 'ts' => $timestamp + ]; + + $attachment = $this->setAuthor($attachment, $characterData); + $attachment = $this->setThumb($attachment, $thumbData); + + + // set 'field' array ---------------------------------------------------------------------------------- + if ($this->includeExtra) { + $attachment['fields'][] = $this->generateAttachmentField('', 'Meta data:', false, false); + + if(!empty($record['extra']['path'])){ + $attachment['fields'][] = $this->generateAttachmentField('Path', $record['extra']['path'], true); + } + + if(!empty($tag)){ + $attachment['fields'][] = $this->generateAttachmentField('Tag', $tag, true); + } + + if(!empty($record['level_name'])){ + $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name'], true); + } + + if(!empty($record['extra']['ip'])){ + $attachment['fields'][] = $this->generateAttachmentField('IP', $record['extra']['ip'], true); + } + } + + $postData['attachments'][] = $attachment; + } + } + + $postData['text'] = empty($text) ? $postData['text'] : $text; + + + return $postData; + } +} \ No newline at end of file diff --git a/app/main/lib/logging/handler/AbstractRallyWebhookHandler.php b/app/main/lib/logging/handler/AbstractRallyWebhookHandler.php new file mode 100644 index 00000000..f77d4dd2 --- /dev/null +++ b/app/main/lib/logging/handler/AbstractRallyWebhookHandler.php @@ -0,0 +1,156 @@ +getTimestamp(); + $text = ''; + + if ( + $this->useAttachment && + !empty( $attachmentsData = $record['context']['data']) + ){ + // convert non grouped data (associative array) to multi dimensional (sequential) array + // -> see "group" records + $attachmentsData = Util::is_assoc($attachmentsData) ? [$attachmentsData] : $attachmentsData; + + $thumbData = (array)$record['extra']['thumb']; + + $postData['attachments'] = []; + + foreach($attachmentsData as $attachmentData){ + $characterData = (array)$attachmentData['character']; + + $text = 'No Title'; + if( !empty($attachmentData['formatted']) ){ + $text = $attachmentData['formatted']; + } + + $attachment = [ + 'title' => !empty($attachmentData['main']['message']) ? 'Message' : '', + //'pretext' => '', + 'text' => !empty($attachmentData['main']['message']) ? sprintf('```%s```', $attachmentData['main']['message']) : '', + 'fallback' => !empty($attachmentData['main']['message']) ? $attachmentData['main']['message'] : 'No Fallback', + 'color' => $this->getAttachmentColor($tag), + 'fields' => [], + 'mrkdwn_in' => ['fields', 'text'], + 'footer' => 'Pathfinder API', + //'footer_icon'=> '', + 'ts' => $timestamp + ]; + + $attachment = $this->setAuthor($attachment, $characterData); + $attachment = $this->setThumb($attachment, $thumbData); + + // set 'field' array ---------------------------------------------------------------------------------- + if ($this->includeContext) { + if(!empty($objectData = $attachmentData['object'])){ + if(!empty($objectData['objAlias'])){ + // System alias + $attachment['fields'][] = $this->generateAttachmentField('Alias', $objectData['objAlias']); + } + + if(!empty($objectData['objName'])){ + // System name + $attachment['fields'][] = $this->generateAttachmentField('System', $objectData['objName']); + } + + if(!empty($objectData['objRegion'])){ + // System region + $attachment['fields'][] = $this->generateAttachmentField('Region', $objectData['objRegion']); + } + + if(isset($objectData['objIsWormhole'])){ + // Is wormhole + $attachment['fields'][] = $this->generateAttachmentField('Wormhole', $objectData['objIsWormhole'] ? 'Yes' : 'No'); + } + + if(!empty($objectData['objSecurity'])){ + // System security + $attachment['fields'][] = $this->generateAttachmentField('Security', $objectData['objSecurity']); + } + + if(!empty($objectData['objEffect'])){ + // System effect + $attachment['fields'][] = $this->generateAttachmentField('Effect', $objectData['objEffect']); + } + + if(!empty($objectData['objTrueSec'])){ + // System trueSec + $attachment['fields'][] = $this->generateAttachmentField('TrueSec', $objectData['objTrueSec']); + } + + if(!empty($objectData['objCountPlanets'])){ + // System planet count + $attachment['fields'][] = $this->generateAttachmentField('Planets', $objectData['objCountPlanets']); + } + + if(!empty($objectData['objDescription'])){ + // System description + $attachment['fields'][] = $this->generateAttachmentField('System description', '```' . $this->htmlToMarkdown($objectData['objDescription']) . '```', false, false); + } + + if(!empty($objectData['objUrl'])){ + // System deeeplink + $attachment['fields'][] = $this->generateAttachmentField('', $objectData['objUrl'] , false, false); + } + } + } + + if($this->includeExtra){ + if(!empty($record['extra']['path'])){ + $attachment['fields'][] = $this->generateAttachmentField('Path', $record['extra']['path'], true); + } + + if(!empty($tag)){ + $attachment['fields'][] = $this->generateAttachmentField('Tag', $tag, true); + } + + if(!empty($record['level_name'])){ + $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name'], true); + } + + if(!empty($record['extra']['ip'])){ + $attachment['fields'][] = $this->generateAttachmentField('IP', $record['extra']['ip'], true); + } + } + + $postData['attachments'][] = $attachment; + } + } + + $postData['text'] = empty($text) ? $postData['text'] : $text; + + return $postData; + } + + /** + * convert $html into Markdown + * @param $html + * @return string + */ + protected function htmlToMarkdown($html){ + $converter = new HtmlConverter(); + $converter->getConfig()->setOption('strip_tags', true); + $markdown = $converter->convert($html); + return $markdown; + } +} \ No newline at end of file diff --git a/app/main/lib/logging/handler/AbstractSlackWebhookHandler.php b/app/main/lib/logging/handler/AbstractWebhookHandler.php similarity index 98% rename from app/main/lib/logging/handler/AbstractSlackWebhookHandler.php rename to app/main/lib/logging/handler/AbstractWebhookHandler.php index 756fb37a..8c7d4adf 100644 --- a/app/main/lib/logging/handler/AbstractSlackWebhookHandler.php +++ b/app/main/lib/logging/handler/AbstractWebhookHandler.php @@ -12,7 +12,7 @@ use lib\Config; use Monolog\Handler; use Monolog\Logger; -abstract class AbstractSlackWebhookHandler extends Handler\AbstractProcessingHandler { +abstract class AbstractWebhookHandler extends Handler\AbstractProcessingHandler { /** * @var string @@ -179,7 +179,6 @@ abstract class AbstractSlackWebhookHandler extends Handler\AbstractProcessingHan * @param array $attachment * @param array $characterData * @return array - * @throws \Exception\PathfinderException */ protected function setAuthor(array $attachment, array $characterData): array { if( !empty($characterData['id']) && !empty($characterData['name'])){ diff --git a/app/main/lib/logging/handler/DiscordMapWebhookHandler.php b/app/main/lib/logging/handler/DiscordMapWebhookHandler.php new file mode 100644 index 00000000..1c6b34c0 --- /dev/null +++ b/app/main/lib/logging/handler/DiscordMapWebhookHandler.php @@ -0,0 +1,14 @@ +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; - } +class SlackMapWebhookHandler extends AbstractMapWebhookHandler { } \ No newline at end of file diff --git a/app/main/lib/logging/handler/SlackRallyWebhookHandler.php b/app/main/lib/logging/handler/SlackRallyWebhookHandler.php index 71850c06..40f77cbd 100644 --- a/app/main/lib/logging/handler/SlackRallyWebhookHandler.php +++ b/app/main/lib/logging/handler/SlackRallyWebhookHandler.php @@ -8,139 +8,10 @@ namespace lib\logging\handler; -use lib\Util; -class SlackRallyWebhookHandler extends AbstractSlackWebhookHandler { +class SlackRallyWebhookHandler extends AbstractRallyWebhookHandler { - /** - * @param array $record - * @return array - * @throws \Exception\PathfinderException - */ - protected function getSlackData(array $record) : array{ - $postData = parent::getSlackData($record); - $tag = (string)$record['context']['tag']; - $timestamp = (int)$record['datetime']->getTimestamp(); - $text = ''; - - if ( - $this->useAttachment && - !empty( $attachmentsData = $record['context']['data']) - ){ - // convert non grouped data (associative array) to multi dimensional (sequential) array - // -> see "group" records - $attachmentsData = Util::is_assoc($attachmentsData) ? [$attachmentsData] : $attachmentsData; - - $thumbData = (array)$record['extra']['thumb']; - - $postData['attachments'] = []; - - foreach($attachmentsData as $attachmentData){ - $characterData = (array)$attachmentData['character']; - - $text = 'No Title'; - if( !empty($attachmentData['formatted']) ){ - $text = $attachmentData['formatted']; - } - - $attachment = [ - 'title' => !empty($attachmentData['main']['message']) ? 'Message' : '', - //'pretext' => '', - 'text' => !empty($attachmentData['main']['message']) ? sprintf('```%s```', $attachmentData['main']['message']) : '', - 'fallback' => !empty($attachmentData['main']['message']) ? $attachmentData['main']['message'] : 'No Fallback', - 'color' => $this->getAttachmentColor($tag), - 'fields' => [], - 'mrkdwn_in' => ['fields', 'text'], - 'footer' => 'Pathfinder API', - //'footer_icon'=> '', - 'ts' => $timestamp - ]; - - $attachment = $this->setAuthor($attachment, $characterData); - $attachment = $this->setThumb($attachment, $thumbData); - - // set 'field' array ---------------------------------------------------------------------------------- - if ($this->includeContext) { - if(!empty($objectData = $attachmentData['object'])){ - if(!empty($objectData['objAlias'])){ - // System alias - $attachment['fields'][] = $this->generateAttachmentField('Alias', $objectData['objAlias']); - } - - if(!empty($objectData['objName'])){ - // System name - $attachment['fields'][] = $this->generateAttachmentField('System', $objectData['objName']); - } - - if(!empty($objectData['objRegion'])){ - // System region - $attachment['fields'][] = $this->generateAttachmentField('Region', $objectData['objRegion']); - } - - if(isset($objectData['objIsWormhole'])){ - // Is wormhole - $attachment['fields'][] = $this->generateAttachmentField('Wormhole', $objectData['objIsWormhole'] ? 'Yes' : 'No'); - } - - if(!empty($objectData['objSecurity'])){ - // System security - $attachment['fields'][] = $this->generateAttachmentField('Security', $objectData['objSecurity']); - } - - if(!empty($objectData['objEffect'])){ - // System effect - $attachment['fields'][] = $this->generateAttachmentField('Effect', $objectData['objEffect']); - } - - if(!empty($objectData['objTrueSec'])){ - // System trueSec - $attachment['fields'][] = $this->generateAttachmentField('TrueSec', $objectData['objTrueSec']); - } - - if(!empty($objectData['objCountPlanets'])){ - // System planet count - $attachment['fields'][] = $this->generateAttachmentField('Planets', $objectData['objCountPlanets']); - } - - if(!empty($objectData['objDescription'])){ - // System trueSec - $attachment['fields'][] = $this->generateAttachmentField('System description', '```' . $objectData['objDescription'] . '```', false, false); - } - - if(!empty($objectData['objUrl'])){ - // System deeeplink - $attachment['fields'][] = $this->generateAttachmentField('', $objectData['objUrl'] , false, false); - } - } - } - - if($this->includeExtra){ - if(!empty($record['extra']['path'])){ - $attachment['fields'][] = $this->generateAttachmentField('Path', $record['extra']['path'], true); - } - - if(!empty($tag)){ - $attachment['fields'][] = $this->generateAttachmentField('Tag', $tag, true); - } - - if(!empty($record['level_name'])){ - $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name'], true); - } - - if(!empty($record['extra']['ip'])){ - $attachment['fields'][] = $this->generateAttachmentField('IP', $record['extra']['ip'], true); - } - } - - $postData['attachments'][] = $attachment; - } - } - - $postData['text'] = empty($text) ? $postData['text'] : $text; - - return $postData; - } } \ No newline at end of file diff --git a/app/main/lib/web.php b/app/main/lib/web.php index 6b0eb91d..2a665c1d 100644 --- a/app/main/lib/web.php +++ b/app/main/lib/web.php @@ -100,9 +100,9 @@ class Web extends \Web { * @param array $additionalOptions * @param int $retryCount request counter for failed call * @return array|FALSE|mixed - * @throws \Exception\PathfinderException + * @throws \Exception\DateException */ - public function request($url,array $options = null, $additionalOptions = [], $retryCount = 0 ) { + public function request($url,array $options = null, $additionalOptions = [], $retryCount = 0){ $f3 = \Base::instance(); if( !$f3->exists( $hash = $this->getCacheKey($url, $options) ) ){ @@ -135,7 +135,11 @@ class Web extends \Web { $url, json_decode($result['body']) ); - LogController::getLogger('ERROR')->write($errorMsg); + + // if request not within downTime time range -> log error + if( !Config::inDownTimeRange() ){ + LogController::getLogger('ERROR')->write($errorMsg); + } break; case 500: case 501: @@ -151,7 +155,11 @@ class Web extends \Web { $url, json_decode($result['body']) ); - LogController::getLogger('ERROR')->write($errorMsg); + + // if request not within downTime time range -> log error + if( !Config::inDownTimeRange() ){ + LogController::getLogger('ERROR')->write($errorMsg); + } // trigger error if($additionalOptions['suppressHTTPErrors'] !== true){ @@ -174,8 +182,10 @@ class Web extends \Web { json_decode($result['body']) ); - // log error - LogController::getLogger('ERROR')->write($errorMsg); + // if request not within downTime time range -> log error + if( !Config::inDownTimeRange() ){ + LogController::getLogger('ERROR')->write($errorMsg); + } if($additionalOptions['suppressHTTPErrors'] !== true){ $f3->error(504, $errorMsg); @@ -190,7 +200,9 @@ class Web extends \Web { $url ); - LogController::getLogger('ERROR')->write($errorMsg); + if( !Config::inDownTimeRange() ){ + LogController::getLogger('ERROR')->write($errorMsg); + } break; } diff --git a/app/main/model/abstractmaptrackingmodel.php b/app/main/model/abstractmaptrackingmodel.php index 38e26a6b..9a2cce45 100644 --- a/app/main/model/abstractmaptrackingmodel.php +++ b/app/main/model/abstractmaptrackingmodel.php @@ -23,7 +23,7 @@ abstract class AbstractMapTrackingModel extends BasicModel implements LogModelIn 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'updatedCharacterId' => [ 'type' => Schema::DT_INT, @@ -35,7 +35,7 @@ abstract class AbstractMapTrackingModel extends BasicModel implements LogModelIn 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ] ]; diff --git a/app/main/model/activitylogmodel.php b/app/main/model/activitylogmodel.php index bcee4e71..33275cb7 100644 --- a/app/main/model/activitylogmodel.php +++ b/app/main/model/activitylogmodel.php @@ -178,6 +178,7 @@ class ActivityLogModel extends BasicModel { * @param null $table * @param null $fields * @return bool + * @throws \Exception */ public static function setup($db=null, $table=null, $fields=null){ $status = parent::setup($db,$table,$fields); diff --git a/app/main/model/alliancemodel.php b/app/main/model/alliancemodel.php index 5c74debf..274cc6c7 100644 --- a/app/main/model/alliancemodel.php +++ b/app/main/model/alliancemodel.php @@ -77,7 +77,6 @@ class AllianceModel extends BasicModel { /** * get all maps for this alliance * @return array|mixed - * @throws \Exception\PathfinderException */ public function getMaps(){ $maps = []; diff --git a/app/main/model/basicmodel.php b/app/main/model/basicmodel.php index 48c19eaa..817c3d18 100644 --- a/app/main/model/basicmodel.php +++ b/app/main/model/basicmodel.php @@ -118,7 +118,7 @@ abstract class BasicModel extends \DB\Cortex { parent::__construct($db, $table, $fluid, $ttl); // insert events ------------------------------------------------------------------------------------ - $this->beforeinsert( function($self, $pkeys){ + $this->beforeinsert(function($self, $pkeys){ return $self->beforeInsertEvent($self, $pkeys); }); @@ -127,21 +127,21 @@ abstract class BasicModel extends \DB\Cortex { }); // update events ------------------------------------------------------------------------------------ - $this->beforeupdate( function($self, $pkeys){ + $this->beforeupdate(function($self, $pkeys){ return $self->beforeUpdateEvent($self, $pkeys); }); - $this->afterupdate( function($self, $pkeys){ + $this->afterupdate(function($self, $pkeys){ $self->afterUpdateEvent($self, $pkeys); }); // erase events ------------------------------------------------------------------------------------- - $this->beforeerase( function($self, $pkeys){ + $this->beforeerase(function($self, $pkeys){ return $self->beforeEraseEvent($self, $pkeys); }); - $this->aftererase( function($self, $pkeys){ + $this->aftererase(function($self, $pkeys){ $self->afterEraseEvent($self, $pkeys); }); } @@ -264,7 +264,7 @@ abstract class BasicModel extends \DB\Cortex { * get static fields for this model instance * @return array */ - protected function getStaticFieldConf(): array { + protected function getStaticFieldConf() : array { $staticFieldConfig = []; // static tables (fixed data) do not require them... @@ -299,13 +299,14 @@ abstract class BasicModel extends \DB\Cortex { * @param $val * @return bool */ - protected function validateField(string $key, $val): bool { + protected function validateField(string $key, $val) : bool { $valid = true; if($fieldConf = $this->fieldConf[$key]){ if($method = $this->fieldConf[$key]['validate']){ if( !is_string($method)){ - $method = 'validate_' . $key; + $method = $key; } + $method = 'validate_' . $method; if(method_exists($this, $method)){ // validate $key (column) with this method... $valid = $this->$method($key, $val); @@ -325,7 +326,7 @@ abstract class BasicModel extends \DB\Cortex { * @return bool * @throws \Exception\ValidationException */ - protected function validate_notDry($key, $val): bool { + protected function validate_notDry($key, $val) : bool { $valid = true; if($colConf = $this->fieldConf[$key]){ if(isset($colConf['belongs-to-one'])){ @@ -344,6 +345,30 @@ abstract class BasicModel extends \DB\Cortex { return $valid; } + /** + * validates a model field to be not empty + * @param $key + * @param $val + * @return bool + */ + protected function validate_notEmpty($key, $val) : bool { + $valid = false; + + if($colConf = $this->fieldConf[$key]){ + switch($colConf['type']){ + case Schema::DT_INT: + case Schema::DT_FLOAT: + if( (is_int($val) || ctype_digit($val)) && (int)$val > 0){ + $valid = true; + } + break; + default: + } + } + + return $valid; + } + /** * get key for for all objects in this table * @return string @@ -628,7 +653,7 @@ abstract class BasicModel extends \DB\Cortex { * function should be overwritten in parent classes * @return bool */ - public function isValid(): bool { + public function isValid() : bool { return true; } @@ -776,7 +801,7 @@ abstract class BasicModel extends \DB\Cortex { * @param string $action * @return Logging\LogInterface */ - protected function newLog($action = ''): Logging\LogInterface{ + protected function newLog($action = '') : Logging\LogInterface{ return new Logging\DefaultLog($action); } @@ -800,7 +825,7 @@ abstract class BasicModel extends \DB\Cortex { * get all validation errors * @return array */ - public function getErrors(): array { + public function getErrors() : array { return $this->validationError; } @@ -808,7 +833,7 @@ abstract class BasicModel extends \DB\Cortex { * checks whether data is outdated and should be refreshed * @return bool */ - protected function isOutdated(): bool { + protected function isOutdated() : bool { $outdated = true; if(!$this->dry()){ $timezone = $this->getF3()->get('getTimeZone')(); @@ -832,7 +857,7 @@ abstract class BasicModel extends \DB\Cortex { }catch(ValidationException $e){ $this->setValidationError($e); }catch(DatabaseException $e){ - self::getF3()->error($e->getCode(), $e->getMessage(), $e->getTrace()); + self::getF3()->error($e->getResponseCode(), $e->getMessage(), $e->getTrace()); } } @@ -913,7 +938,6 @@ abstract class BasicModel extends \DB\Cortex { * debug log function * @param string $text * @param string $type - * @throws \Exception\PathfinderException */ public static function log($text, $type = 'DEBUG'){ Controller\LogController::getLogger($type)->write($text); diff --git a/app/main/model/charactermodel.php b/app/main/model/charactermodel.php index 2cb360e8..065dfa87 100644 --- a/app/main/model/charactermodel.php +++ b/app/main/model/charactermodel.php @@ -300,8 +300,9 @@ class CharacterModel extends BasicModel { /** * setter for "banned" status - * @param bool|int $status - * @return mixed + * @param $status + * @return mixed|string|null + * @throws \Exception */ public function set_banned($status){ if($this->allowBanChange){ @@ -405,6 +406,7 @@ class CharacterModel extends BasicModel { */ private function resetAdminColumns(){ $this->kick(); + $this->ban(); } /** @@ -473,7 +475,6 @@ class CharacterModel extends BasicModel { * get ESI API "access_token" from OAuth * @return bool|mixed * @throws \Exception - * @throws \Exception\PathfinderException */ public function getAccessToken(){ $accessToken = false; @@ -544,7 +545,6 @@ class CharacterModel extends BasicModel { * checks whether this character is authorized to log in * -> check corp/ally whitelist config (pathfinder.ini) * @return bool - * @throws \Exception\PathfinderException */ public function isAuthorized(){ $authStatus = 'UNKNOWN'; @@ -614,7 +614,6 @@ class CharacterModel extends BasicModel { * get Pathfinder role for character * @return RoleModel * @throws \Exception - * @throws \Exception\PathfinderException */ public function requestRole() : RoleModel{ $role = null; @@ -660,7 +659,6 @@ class CharacterModel extends BasicModel { * request all corporation roles granted to this character * @return array * @throws \Exception - * @throws \Exception\PathfinderException */ protected function requestRoles(){ $rolesData = []; @@ -1021,7 +1019,6 @@ class CharacterModel extends BasicModel { /** * get all accessible map models for this character * @return MapModel[] - * @throws \Exception\PathfinderException */ public function getMaps(){ $this->filter( diff --git a/app/main/model/connectionlogmodel.php b/app/main/model/connectionlogmodel.php index d4cc21fe..065e6c82 100644 --- a/app/main/model/connectionlogmodel.php +++ b/app/main/model/connectionlogmodel.php @@ -30,11 +30,19 @@ class ConnectionLogModel extends BasicModel { 'table' => 'connection', 'on-delete' => 'CASCADE' ] - ] + ], + 'validate' => 'notDry' + ], + 'record' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true ], 'shipTypeId' => [ 'type' => Schema::DT_INT, - 'index' => true + 'index' => true, + 'validate' => 'notEmpty' ], 'shipTypeName' => [ 'type' => Schema::DT_VARCHAR128, @@ -44,11 +52,13 @@ class ConnectionLogModel extends BasicModel { 'shipMass' => [ 'type' => Schema::DT_FLOAT, 'nullable' => false, - 'default' => 0 + 'default' => 0, + 'validate' => 'notEmpty' ], 'characterId' => [ 'type' => Schema::DT_INT, - 'index' => true + 'index' => true, + 'validate' => 'notEmpty' ], 'characterName' => [ 'type' => Schema::DT_VARCHAR128, @@ -57,6 +67,14 @@ class ConnectionLogModel extends BasicModel { ] ]; + /** + * set map data by an associative array + * @param array $data + */ + public function setData(array $data){ + $this->copyfrom($data, ['shipTypeId', 'shipTypeName', 'shipMass', 'characterId', 'characterName']); + } + /** * get connection log data * @return \stdClass @@ -64,6 +82,8 @@ class ConnectionLogModel extends BasicModel { public function getData() : \stdClass { $logData = (object) []; $logData->id = $this->id; + $logData->active = $this->active; + $logData->record = $this->record; $logData->connection = (object) []; $logData->connection->id = $this->get('connectionId', true); @@ -73,12 +93,46 @@ class ConnectionLogModel extends BasicModel { $logData->ship->typeName = $this->shipTypeName; $logData->ship->mass = $this->shipMass; + $logData->character = (object) []; + $logData->character->id = $this->characterId; + $logData->character->name = $this->characterName; + $logData->created = (object) []; $logData->created->created = strtotime($this->created); - $logData->created->character = (object) []; - $logData->created->character->id = $this->characterId; - $logData->created->character->name = $this->characterName; + + $logData->updated = (object) []; + $logData->updated->updated = strtotime($this->updated); return $logData; } + + /** + * validate shipTypeId + * @param string $key + * @param string $val + * @return bool + */ + protected function validate_shipTypeId(string $key, string $val): bool { + return !empty((int)$val); + } + + /** + * @return ConnectionModel + */ + public function getConnection() : ConnectionModel { + return $this->get('connectionId'); + } + + /** + * check object for model access + * @param CharacterModel $characterModel + * @return bool + */ + public function hasAccess(CharacterModel $characterModel) : bool { + $access = false; + if( !$this->dry() ){ + $access = $this->getConnection()->hasAccess($characterModel); + } + return $access; + } } \ No newline at end of file diff --git a/app/main/model/connectionmodel.php b/app/main/model/connectionmodel.php index dc4f04cf..8d295682 100644 --- a/app/main/model/connectionmodel.php +++ b/app/main/model/connectionmodel.php @@ -137,9 +137,9 @@ class ConnectionModel extends AbstractMapTrackingModel { /** * check object for model access * @param CharacterModel $characterModel - * @return mixed + * @return bool */ - public function hasAccess(CharacterModel $characterModel){ + public function hasAccess(CharacterModel $characterModel) : bool { $access = false; if( !$this->dry() ){ $access = $this->mapId->hasAccess($characterModel); @@ -149,7 +149,7 @@ class ConnectionModel extends AbstractMapTrackingModel { /** * set default connection type by search route between endpoints - * @throws \Exception\PathfinderException + * @throws \Exception */ public function setDefaultTypeData(){ if( @@ -215,7 +215,6 @@ class ConnectionModel extends AbstractMapTrackingModel { * @param $pkeys * @return bool * @throws \Exception\DatabaseException - * @throws \Exception\PathfinderException */ public function beforeInsertEvent($self, $pkeys){ // check for "default" connection type and add them if missing @@ -266,8 +265,8 @@ class ConnectionModel extends AbstractMapTrackingModel { /** * @param string $action - * @return Logging\LogInterface - * @throws \Exception\PathfinderException + * @return logging\LogInterface + * @throws \Exception\ConfigException */ public function newLog($action = ''): Logging\LogInterface{ return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData()); @@ -335,10 +334,6 @@ class ConnectionModel extends AbstractMapTrackingModel { */ public function getLogs(){ $logs = []; - $this->filter('connectionLog', [ - 'active = :active', - ':active' => 1 - ]); if($this->connectionLog){ $logs = $this->connectionLog; @@ -377,20 +372,34 @@ class ConnectionModel extends AbstractMapTrackingModel { return $logsData; } + /** + * get blank connectionLog model + * @return ConnectionLogModel + * @throws \Exception + */ + public function getNewLog() : ConnectionLogModel { + /** + * @var $log ConnectionLogModel + */ + $log = self::getNew('ConnectionLogModel'); + $log->connectionId = $this; + return $log; + } + /** * log new mass for this connection * @param CharacterLogModel $characterLog - * @return $this + * @return ConnectionModel + * @throws \Exception */ - public function logMass(CharacterLogModel $characterLog){ + public function logMass(CharacterLogModel $characterLog) : self { if( !$characterLog->dry() ){ - $log = $this->rel('connectionLog'); + $log = $this->getNewLog(); $log->shipTypeId = $characterLog->shipTypeId; $log->shipTypeName = $characterLog->shipTypeName; $log->shipMass = $characterLog->shipMass; $log->characterId = $characterLog->characterId->_id; $log->characterName = $characterLog->characterId->name; - $log->connectionId = $this; $log->save(); } diff --git a/app/main/model/corporationmodel.php b/app/main/model/corporationmodel.php index 8109e2af..8a5ecaa2 100644 --- a/app/main/model/corporationmodel.php +++ b/app/main/model/corporationmodel.php @@ -181,7 +181,6 @@ class CorporationModel extends BasicModel { * @param array $mapIds * @param array $options * @return array - * @throws \Exception\PathfinderException */ public function getMaps($mapIds = [], $options = []){ $maps = []; diff --git a/app/main/model/mapmodel.php b/app/main/model/mapmodel.php index e7ed18bf..c229c9aa 100644 --- a/app/main/model/mapmodel.php +++ b/app/main/model/mapmodel.php @@ -10,9 +10,9 @@ namespace Model; use DB\SQL\Schema; use data\file\FileHandler; +use Exception\ConfigException; use lib\Config; use lib\logging; -use Exception\PathfinderException; class MapModel extends AbstractMapTrackingModel { @@ -44,7 +44,7 @@ class MapModel extends AbstractMapTrackingModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry', + 'validate' => 'notDry', 'activity-log' => true ], 'typeId' => [ @@ -57,7 +57,7 @@ class MapModel extends AbstractMapTrackingModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry', + 'validate' => 'notDry', 'activity-log' => true ], 'name' => [ @@ -199,7 +199,6 @@ class MapModel extends AbstractMapTrackingModel { * get data * -> this includes system and connection data as well * @return \stdClass - * @throws PathfinderException * @throws \Exception */ public function getData(){ @@ -791,7 +790,6 @@ class MapModel extends AbstractMapTrackingModel { * checks whether a character has access to this map or not * @param CharacterModel $characterModel * @return bool - * @throws PathfinderException */ public function hasAccess(CharacterModel $characterModel) : bool { $hasAccess = false; @@ -972,8 +970,8 @@ class MapModel extends AbstractMapTrackingModel { /** * @param string $action - * @return Logging\LogInterface - * @throws PathfinderException + * @return logging\LogInterface + * @throws ConfigException */ public function newLog($action = ''): Logging\LogInterface{ $logChannelData = $this->getLogChannelData(); @@ -1048,7 +1046,6 @@ class MapModel extends AbstractMapTrackingModel { /** * check if "activity logging" is enabled for this map type * @return bool - * @throws PathfinderException */ public function isActivityLogEnabled(): bool { return $this->logActivity && (bool) Config::getMapsDefaultConfig($this->typeId->name)['log_activity_enabled']; @@ -1057,7 +1054,6 @@ class MapModel extends AbstractMapTrackingModel { /** * check if "history logging" is enabled for this map type * @return bool - * @throws PathfinderException */ public function isHistoryLogEnabled(): bool { return $this->logHistory && (bool) Config::getMapsDefaultConfig($this->typeId->name)['log_history_enabled']; @@ -1067,7 +1063,7 @@ class MapModel extends AbstractMapTrackingModel { * check if "Slack WebHook" is enabled for this map type * @param string $channel * @return bool - * @throws PathfinderException + * @throws ConfigException */ public function isSlackChannelEnabled(string $channel): bool { $enabled = false; @@ -1077,7 +1073,7 @@ class MapModel extends AbstractMapTrackingModel { 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)); + default: throw new ConfigException(sprintf(self::ERROR_SLACK_CHANNEL, $channel)); } if((bool) Config::getMapsDefaultConfig($this->typeId->name)[$defaultMapConfigKey]){ @@ -1095,7 +1091,7 @@ class MapModel extends AbstractMapTrackingModel { * check if "Discord WebHook" is enabled for this map type * @param string $channel * @return bool - * @throws PathfinderException + * @throws ConfigException */ public function isDiscordChannelEnabled(string $channel): bool { $enabled = false; @@ -1105,7 +1101,7 @@ class MapModel extends AbstractMapTrackingModel { switch($channel){ case 'discordWebHookURLHistory': $defaultMapConfigKey = 'send_history_discord_enabled'; break; case 'discordWebHookURLRally': $defaultMapConfigKey = 'send_rally_discord_enabled'; break; - default: throw new PathfinderException(sprintf(self::ERROR_DISCORD_CHANNEL, $channel)); + default: throw new ConfigException(sprintf(self::ERROR_DISCORD_CHANNEL, $channel)); } if((bool) Config::getMapsDefaultConfig($this->typeId->name)[$defaultMapConfigKey]){ @@ -1123,7 +1119,6 @@ class MapModel extends AbstractMapTrackingModel { * check if "E-Mail" Log is enabled for this map * @param string $type * @return bool - * @throws PathfinderException */ public function isMailSendEnabled(string $type): bool{ $enabled = false; @@ -1197,7 +1192,6 @@ class MapModel extends AbstractMapTrackingModel { * @param string $type * @param bool $addJson * @return \stdClass - * @throws PathfinderException */ public function getSMTPConfig(string $type, bool $addJson = true): \stdClass{ $config = Config::getSMTPConfig(); @@ -1350,7 +1344,6 @@ class MapModel extends AbstractMapTrackingModel { * get all active characters (with active log) * grouped by systems * @return \stdClass - * @throws PathfinderException * @throws \Exception */ public function getUserData(){ diff --git a/app/main/model/systemmodel.php b/app/main/model/systemmodel.php index 5d685093..3a1b63f1 100644 --- a/app/main/model/systemmodel.php +++ b/app/main/model/systemmodel.php @@ -95,8 +95,6 @@ class SystemModel extends AbstractMapTrackingModel { ], 'description' => [ 'type' => Schema::DT_TEXT, - 'nullable' => false, - 'default' => '', 'activity-log' => true, 'validate' => true ], @@ -121,6 +119,14 @@ class SystemModel extends AbstractMapTrackingModel { ] ]; + /** + * set map data by an associative array + * @param array $data + */ + public function setData(array $data){ + $this->copyfrom($data, ['statusId', 'locked', 'rallyUpdated', 'position', 'description']); + } + /** * get map data as object * @return \stdClass @@ -151,7 +157,7 @@ class SystemModel extends AbstractMapTrackingModel { $systemData->locked = $this->locked; $systemData->rallyUpdated = strtotime($this->rallyUpdated); $systemData->rallyPoke = $this->rallyPoke; - $systemData->description = $this->description; + $systemData->description = $this->description ? : ''; $systemData->position = (object) []; $systemData->position->x = $this->posX; @@ -273,7 +279,7 @@ class SystemModel extends AbstractMapTrackingModel { $valid = true; if(mb_strlen($val) > 9000){ $valid = false; - $this->throwValidationException($key); + $this->throwValidationException($key, 'Validation failed: "' . $key . '" too long'); } return $valid; } @@ -490,8 +496,8 @@ class SystemModel extends AbstractMapTrackingModel { /** * @param string $action - * @return Logging\LogInterface - * @throws \Exception\PathfinderException + * @return logging\LogInterface + * @throws \Exception\ConfigException */ public function newLog($action = ''): Logging\LogInterface{ return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData()); @@ -654,7 +660,7 @@ class SystemModel extends AbstractMapTrackingModel { * -> send to an Email * @param array $rallyData * @param CharacterModel $characterModel - * @throws \Exception\PathfinderException + * @throws \Exception\ConfigException */ public function sendRallyPoke(array $rallyData, CharacterModel $characterModel){ // rally log needs at least one handler to be valid diff --git a/app/main/model/systemsignaturemodel.php b/app/main/model/systemsignaturemodel.php index 323d49d3..80f4071a 100644 --- a/app/main/model/systemsignaturemodel.php +++ b/app/main/model/systemsignaturemodel.php @@ -162,8 +162,8 @@ class SystemSignatureModel extends AbstractMapTrackingModel { /** * @param string $action - * @return Logging\LogInterface - * @throws \Exception\PathfinderException + * @return logging\LogInterface + * @throws \Exception\ConfigException */ public function newLog($action = ''): Logging\LogInterface{ return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData()); @@ -194,11 +194,14 @@ class SystemSignatureModel extends AbstractMapTrackingModel { $hasChanged = false; foreach((array)$signatureData as $key => $value){ - if( - $this->exists($key) && - $this->$key != $value - ){ - $hasChanged = true; + if($this->exists($key)){ + if($this->$key instanceof ConnectionModel){ + $currentValue = $this->get($key, true); + }else{ + $currentValue = $this->$key; + } + + $hasChanged = $currentValue !== $value; break; } } diff --git a/app/main/model/universe/categorymodel.php b/app/main/model/universe/categorymodel.php index d75b37c3..dc0db9f4 100644 --- a/app/main/model/universe/categorymodel.php +++ b/app/main/model/universe/categorymodel.php @@ -33,14 +33,15 @@ class CategoryModel extends BasicUniverseModel { /** * get category data - * @return object + * @param array $additionalData + * @return null|object */ - public function getData(){ + public function getData(array $additionalData = []){ $categoryData = (object) []; $categoryData->id = $this->id; $categoryData->name = $this->name; - if($groupsData = $this->getGroupsData()){ + if($groupsData = $this->getGroupsData($additionalData)){ $categoryData->groups = $groupsData; } @@ -69,14 +70,15 @@ class CategoryModel extends BasicUniverseModel { } /** + * @param array $additionalData * @return array */ - protected function getGroupsData() : array { + protected function getGroupsData(array $additionalData = []) : array { $groupsData = []; $groups = $this->getGroups(); foreach($groups as $group){ - $groupsData[] = $group->getData(); + $groupsData[] = $group->getData($additionalData); } return $groupsData; diff --git a/app/main/model/universe/constellationmodel.php b/app/main/model/universe/constellationmodel.php index 05ff6019..8051a537 100644 --- a/app/main/model/universe/constellationmodel.php +++ b/app/main/model/universe/constellationmodel.php @@ -30,7 +30,7 @@ class ConstellationModel extends BasicUniverseModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'x' => [ 'type' => Schema::DT_BIGINT, diff --git a/app/main/model/universe/groupmodel.php b/app/main/model/universe/groupmodel.php index c81d271d..85ad5a92 100644 --- a/app/main/model/universe/groupmodel.php +++ b/app/main/model/universe/groupmodel.php @@ -36,7 +36,7 @@ class GroupModel extends BasicUniverseModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'types' => [ 'has-many' => ['Model\Universe\TypeModel', 'groupId'] @@ -45,14 +45,15 @@ class GroupModel extends BasicUniverseModel { /** * get group data - * @return object + * @param array $additionalData + * @return null|object */ - public function getData(){ + public function getData(array $additionalData = []){ $groupData = (object) []; $groupData->id = $this->id; $groupData->name = $this->name; - if($typesData = $this->getTypesData()){ + if($typesData = $this->getTypesData($additionalData)){ $groupData->types = $typesData; } @@ -81,14 +82,15 @@ class GroupModel extends BasicUniverseModel { } /** + * @param array $additionalData * @return array */ - protected function getTypesData() : array { + protected function getTypesData(array $additionalData = []) : array { $typesData = []; $types = $this->getTypes(); foreach($types as $type){ - $typesData[] = $type->getData(); + $typesData[] = $type->getData($additionalData); } return $typesData; diff --git a/app/main/model/universe/planetmodel.php b/app/main/model/universe/planetmodel.php index 7f60c5bd..f2f90431 100644 --- a/app/main/model/universe/planetmodel.php +++ b/app/main/model/universe/planetmodel.php @@ -30,7 +30,7 @@ class PlanetModel extends BasicUniverseModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'typeId' => [ 'type' => Schema::DT_INT, @@ -42,7 +42,7 @@ class PlanetModel extends BasicUniverseModel { 'on-delete' => 'SET NULL' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'x' => [ 'type' => Schema::DT_BIGINT, diff --git a/app/main/model/universe/stargatemodel.php b/app/main/model/universe/stargatemodel.php index 40782765..da6b8cbf 100644 --- a/app/main/model/universe/stargatemodel.php +++ b/app/main/model/universe/stargatemodel.php @@ -30,7 +30,7 @@ class StargateModel extends BasicUniverseModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'typeId' => [ 'type' => Schema::DT_INT, @@ -42,7 +42,7 @@ class StargateModel extends BasicUniverseModel { 'on-delete' => 'SET NULL' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'destinationSystemId' => [ 'type' => Schema::DT_INT, @@ -54,7 +54,7 @@ class StargateModel extends BasicUniverseModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'x' => [ 'type' => Schema::DT_BIGINT, diff --git a/app/main/model/universe/starmodel.php b/app/main/model/universe/starmodel.php index a8e64c19..8f57a9d2 100644 --- a/app/main/model/universe/starmodel.php +++ b/app/main/model/universe/starmodel.php @@ -30,7 +30,7 @@ class StarModel extends BasicUniverseModel { 'on-delete' => 'SET NULL' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'age' => [ 'type' => Schema::DT_BIGINT, diff --git a/app/main/model/universe/structuremodel.php b/app/main/model/universe/structuremodel.php index 5032ef81..1e40e9fe 100644 --- a/app/main/model/universe/structuremodel.php +++ b/app/main/model/universe/structuremodel.php @@ -37,7 +37,7 @@ class StructureModel extends BasicUniverseModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'x' => [ 'type' => Schema::DT_FLOAT, diff --git a/app/main/model/universe/systemmodel.php b/app/main/model/universe/systemmodel.php index 0756daac..af79fe66 100644 --- a/app/main/model/universe/systemmodel.php +++ b/app/main/model/universe/systemmodel.php @@ -32,7 +32,7 @@ class SystemModel extends BasicUniverseModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'starId' => [ 'type' => Schema::DT_INT, @@ -44,7 +44,7 @@ class SystemModel extends BasicUniverseModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'security' => [ 'type' => Schema::DT_VARCHAR128 diff --git a/app/main/model/universe/systemstaticmodel.php b/app/main/model/universe/systemstaticmodel.php index 9b4d32ca..8664ef73 100644 --- a/app/main/model/universe/systemstaticmodel.php +++ b/app/main/model/universe/systemstaticmodel.php @@ -25,7 +25,7 @@ class SystemStaticModel extends BasicUniverseModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'wormholeId' => [ 'type' => Schema::DT_INT, @@ -37,7 +37,7 @@ class SystemStaticModel extends BasicUniverseModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ] ]; diff --git a/app/main/model/universe/typemodel.php b/app/main/model/universe/typemodel.php index 45087636..7a69ab69 100644 --- a/app/main/model/universe/typemodel.php +++ b/app/main/model/universe/typemodel.php @@ -59,7 +59,7 @@ class TypeModel extends BasicUniverseModel { 'on-delete' => 'CASCADE' ] ], - 'validate' => 'validate_notDry', + 'validate' => 'notDry', ], 'marketGroupId' => [ 'type' => Schema::DT_INT, @@ -99,13 +99,18 @@ class TypeModel extends BasicUniverseModel { /** * get type data - * @return object + * @param array $additionalData + * @return null|object */ - public function getData(){ + public function getData(array $additionalData = []){ $typeData = (object) []; $typeData->id = $this->id; $typeData->name = $this->name; + foreach($additionalData as $key){ + $typeData->$key = $this->$key; + } + return $typeData; } diff --git a/app/main/model/universe/wormholemodel.php b/app/main/model/universe/wormholemodel.php index 35d4d202..725e5df3 100644 --- a/app/main/model/universe/wormholemodel.php +++ b/app/main/model/universe/wormholemodel.php @@ -35,7 +35,7 @@ class WormholeModel extends BasicUniverseModel { 'on-delete' => 'SET NULL' ] ], - 'validate' => 'validate_notDry' + 'validate' => 'notDry' ], 'static' => [ 'type' => Schema::DT_BOOL, diff --git a/app/main/model/usermodel.php b/app/main/model/usermodel.php index 27f4a3f5..4438c137 100644 --- a/app/main/model/usermodel.php +++ b/app/main/model/usermodel.php @@ -94,7 +94,6 @@ class UserModel extends BasicModel { * @param UserModel $self * @param $pkeys * @return bool - * @throws Exception\PathfinderException * @throws Exception\RegistrationException */ public function beforeInsertEvent($self, $pkeys){ @@ -137,7 +136,6 @@ class UserModel extends BasicModel { /** * checks whether user has a valid email address and pathfinder has a valid SMTP config * @return bool - * @throws Exception\PathfinderException */ protected function isMailSendEnabled() : bool{ return Config::isValidSMTPConfig($this->getSMTPConfig()); @@ -146,7 +144,6 @@ class UserModel extends BasicModel { /** * get SMTP config for this user * @return \stdClass - * @throws Exception\PathfinderException */ protected function getSMTPConfig() : \stdClass{ $config = Config::getSMTPConfig(); diff --git a/app/pathfinder.ini b/app/pathfinder.ini index ab225900..1e0cf9d6 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.4.2 +VERSION = v1.4.3 ; contact information [optional] CONTACT = https://github.com/exodus4d ; public contact email [optional] diff --git a/app/requirements.ini b/app/requirements.ini index 6df69822..0f4d9e4b 100644 --- a/app/requirements.ini +++ b/app/requirements.ini @@ -30,7 +30,7 @@ ZMQ = 1.1.3 EVENT = 2.3.0 ; exec() function required for run Shell scripts from PHP -EXEC = 1 +EXEC = 1 ; max execution time for requests (seconds) MAX_EXECUTION_TIME = 10 @@ -84,5 +84,5 @@ NPM = 3.10.0 [REQUIREMENTS.DATA] STRUCTURES = 33 -SHIPS = 490 +SHIPS = 491 NEIGHBOURS = 5201 diff --git a/app/routes.ini b/app/routes.ini index c897c9f5..fe164ec8 100644 --- a/app/routes.ini +++ b/app/routes.ini @@ -13,8 +13,12 @@ GET @map: /map* [sync] = Controller\MapContro ; admin panel GET @admin: /admin* [sync] = Controller\Admin->dispatch -; ajax wildcard APIs (throttled) +; AJAX API wildcard endpoints (not cached, throttled) GET|POST /api/@controller/@action [ajax] = Controller\Api\@controller->@action, 0, 512 GET|POST /api/@controller/@action/@arg1 [ajax] = Controller\Api\@controller->@action, 0, 512 GET|POST /api/@controller/@action/@arg1/@arg2 [ajax] = Controller\Api\@controller->@action, 0, 512 +[maps] +; REST API wildcard endpoints (not cached, throttled) +/api/rest/@controller* [ajax] = Controller\Api\Rest\@controller, 0, 512 +/api/rest/@controller/@id [ajax] = Controller\Api\Rest\@controller, 0, 512 \ No newline at end of file diff --git a/composer-dev.json b/composer-dev.json index e1d07696..3f1a5f0e 100644 --- a/composer-dev.json +++ b/composer-dev.json @@ -21,6 +21,8 @@ }], "require": { "php-64bit": ">=7.0", + "ext-pdo": "*", + "ext-openssl": "*", "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", @@ -30,6 +32,7 @@ "monolog/monolog": "1.*", "websoftwares/monolog-zmq-handler": "0.2.*", "swiftmailer/swiftmailer": "^6.0", + "league/html-to-markdown": "4.8.*", "exodus4d/pathfinder_esi": "dev-develop as 0.0.x-dev" } } diff --git a/composer.json b/composer.json index 6507b0cd..ebef794e 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,8 @@ }], "require": { "php-64bit": ">=7.0", + "ext-pdo": "*", + "ext-openssl": "*", "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", @@ -30,6 +32,7 @@ "monolog/monolog": "1.*", "websoftwares/monolog-zmq-handler": "0.2.*", "swiftmailer/swiftmailer": "^6.0", + "league/html-to-markdown": "4.8.*", "exodus4d/pathfinder_esi": "dev-master#v1.2.5" } } diff --git a/gulpfile.js b/gulpfile.js index 6644083d..125a3b8c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -109,14 +109,16 @@ let uglifyJsOptions = { let printError = (title, example) => { let cliLineLength = (cliBoxLength - 8); - log('').log(colors.red( '= ERROR ' + '=' . repeat(cliLineLength))); + log(''); + log(colors.red( '= ERROR ' + '=' . repeat(cliLineLength))); log(colors.red(title)); if(example){ log(` ${colors.gray(example)} `); } - log(colors.red('='.repeat(cliBoxLength))).log(''); + log(colors.red('='.repeat(cliBoxLength))); + log(''); }; // == Settings ======================================================================================================== @@ -270,34 +272,34 @@ let mergeConf = (confUser, confDefault) => { */ let printHelp = () => { let cliLineLength = (cliBoxLength - 7); - log('') - .log(colors.cyan( '= HELP ' + '='.repeat(cliLineLength))) - .log(` - ${colors.cyan('documentation:')} ${colors.gray('https://github.com/exodus4d/pathfinder/wiki/GulpJs')} + log(''); + log(colors.cyan( '= HELP ' + '='.repeat(cliLineLength))); + log(` + ${colors.cyan('documentation:')} ${colors.gray('https://github.com/exodus4d/pathfinder/wiki/GulpJs')} + + ${colors.cyan('usage:')} ${colors.gray('$ npm run gulp [task] -- [--options] ...')} + + ${colors.cyan('tasks:')} + ${colors.gray('help')} This view + ${colors.gray('default')} Development environment. Working with row src files and file watcher, default: + ${colors.gray('')} ${colors.gray('--jsUglify=false --jsSourcemaps=false --cssSourcemaps=false --jsGzip=false --cssGzip=false --jsBrotli=false --cssBrotli=false')} + ${colors.gray('production')} Production build. Concat and uglify static resources, default: + ${colors.gray('')} ${colors.gray('--jsUglify=true --jsSourcemaps=true --cssSourcemaps=true --jsGzip=true --cssGzip=true --jsBrotli=true --cssBrotli=true')} + + ${colors.cyan('options:')} + ${colors.gray('--tag')} Set build version. ${colors.gray('default: --tag="v1.2.4" -> dest path: public/js/v1.2.4')} + ${colors.gray('--jsUglify')} Set js uglification. ${colors.gray('(true || false)')} + ${colors.gray('--jsSourcemaps')} Set js sourcemaps generation. ${colors.gray('(true || false)')} + ${colors.gray('--jsGzip')} Set js "gzip" compression mode. ${colors.gray('(true || false)')} + ${colors.gray('--jsBrotli')} Set js "brotli" compression mode. ${colors.gray('(true || false)')} - ${colors.cyan('usage:')} ${colors.gray('$ npm run gulp [task] -- [--options] ...')} - - ${colors.cyan('tasks:')} - ${colors.gray('help')} This view - ${colors.gray('default')} Development environment. Working with row src files and file watcher, default: - ${colors.gray('')} ${colors.gray('--jsUglify=false --jsSourcemaps=false --cssSourcemaps=false --jsGzip=false --cssGzip=false --jsBrotli=false --cssBrotli=false')} - ${colors.gray('production')} Production build. Concat and uglify static resources, default: - ${colors.gray('')} ${colors.gray('--jsUglify=true --jsSourcemaps=true --cssSourcemaps=true --jsGzip=true --cssGzip=true --jsBrotli=true --cssBrotli=true')} - - ${colors.cyan('options:')} - ${colors.gray('--tag')} Set build version. ${colors.gray('default: --tag="v1.2.4" -> dest path: public/js/v1.2.4')} - ${colors.gray('--jsUglify')} Set js uglification. ${colors.gray('(true || false)')} - ${colors.gray('--jsSourcemaps')} Set js sourcemaps generation. ${colors.gray('(true || false)')} - ${colors.gray('--jsGzip')} Set js "gzip" compression mode. ${colors.gray('(true || false)')} - ${colors.gray('--jsBrotli')} Set js "brotli" compression mode. ${colors.gray('(true || false)')} - - ${colors.gray('--cssSourcemaps')} Set CSS sourcemaps generation. ${colors.gray('(true || false)')} - ${colors.gray('--cssGzip')} Set CSS "gzip" compression mode. ${colors.gray('(true || false)')} - ${colors.gray('--cssBrotli')} Set CSS "brotli" compression mode. ${colors.gray('(true || false)')} - ${colors.gray('--debug')} Set debug mode (more output). ${colors.gray('(true || false)')} - `) - .log(colors.cyan('='.repeat(cliBoxLength))) - .log(''); + ${colors.gray('--cssSourcemaps')} Set CSS sourcemaps generation. ${colors.gray('(true || false)')} + ${colors.gray('--cssGzip')} Set CSS "gzip" compression mode. ${colors.gray('(true || false)')} + ${colors.gray('--cssBrotli')} Set CSS "brotli" compression mode. ${colors.gray('(true || false)')} + ${colors.gray('--debug')} Set debug mode (more output). ${colors.gray('(true || false)')} + `); + log(colors.cyan('='.repeat(cliBoxLength))); + log(''); }; /** diff --git a/js/app.js b/js/app.js index de330a66..8ff598a1 100644 --- a/js/app.js +++ b/js/app.js @@ -54,7 +54,7 @@ requirejs.config({ blueImpGalleryBootstrap: 'lib/bootstrap-image-gallery', // v3.4.2 Bootstrap extension for Blue Imp Gallery - https://blueimp.github.io/Bootstrap-Image-Gallery bootstrapConfirmation: 'lib/bootstrap-confirmation', // v1.0.5 Bootstrap extension for inline confirm dialog - https://github.com/tavicu/bs-confirmation bootstrapToggle: 'lib/bootstrap-toggle.min', // v2.2.0 Bootstrap Toggle (Checkbox) - http://www.bootstraptoggle.com - lazyload: 'lib/jquery.lazyload.min', // v1.9.5 LazyLoader images - http://www.appelsiini.net/projects/lazyload + lazyload: 'lib/jquery.lazyload.min', // v1.9.7 LazyLoader images - http://www.appelsiini.net/projects/lazyload sortable: 'lib/sortable.min', // v1.6.0 Sortable - drag&drop reorder - https://github.com/rubaxa/Sortable 'summernote.loader': './app/summernote.loader', // v0.8.10 Summernote WYSIWYG editor -https://summernote.org diff --git a/js/app/datatables.loader.js b/js/app/datatables.loader.js index 7076767a..58c2109c 100644 --- a/js/app/datatables.loader.js +++ b/js/app/datatables.loader.js @@ -18,6 +18,9 @@ define([ lengthMenu: [[5, 10, 25, 50, -1], [5, 10, 25, 50, 'All']], order: [], // no default order because columnDefs is empty autoWidth: false, + language: { + info: '_START_ - _END_ of _TOTAL_ entries' + }, responsive: { breakpoints: Init.breakpoints, details: false diff --git a/js/app/init.js b/js/app/init.js index 38d1ccd1..8538d7cc 100644 --- a/js/app/init.js +++ b/js/app/init.js @@ -9,6 +9,7 @@ define(['jquery'], ($) => { let Config = { path: { img: '/public/img/', // path for images + api: '/api/rest', //ajax URL - REST API // user API getCaptcha: '/api/user/getCaptcha', // ajax URL - get captcha image getServerStatus: '/api/user/getEveServerStatus', // ajax URL - get EVE-Online server status @@ -34,14 +35,9 @@ define(['jquery'], ($) => { getMapLogData: '/api/map/getLogData', // ajax URL - get logs data // system API getSystemData: '/api/system/getData', // ajax URL - get system data - saveSystem: '/api/system/save', // ajax URL - saves system to map - deleteSystem: '/api/system/delete', // ajax URL - delete system from map getSystemGraphData: '/api/system/graphData', // ajax URL - get all system graph 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 // signature API saveSignatureData: '/api/signature/save', // ajax URL - save signature data for system deleteSignatureData: '/api/signature/delete', // ajax URL - delete signature data for system diff --git a/js/app/logging.js b/js/app/logging.js index c32e454e..3eb00ab6 100644 --- a/js/app/logging.js +++ b/js/app/logging.js @@ -20,7 +20,6 @@ define([ let config = { taskDialogId: 'pf-task-dialog', // id for map "task manager" dialog - dialogDynamicAreaClass: 'pf-dynamic-area', // class for dynamic areas timestampCounterClass: 'pf-timestamp-counter', // class for "timestamp" counter taskDialogStatusAreaClass: 'pf-task-dialog-status', // class for "status" dynamic area taskDialogLogTableAreaClass: 'pf-task-dialog-table', // class for "log table" dynamic area @@ -83,7 +82,7 @@ define([ requirejs(['text!templates/dialog/task_manager.html', 'mustache', 'datatables.loader'], function(templateTaskManagerDialog, Mustache){ let data = { id: config.taskDialogId, - dialogDynamicAreaClass: config.dialogDynamicAreaClass, + dialogDynamicAreaClass: Util.config.dynamicAreaClass, taskDialogStatusAreaClass: config.taskDialogStatusAreaClass, taskDialogLogTableAreaClass: config.taskDialogLogTableAreaClass }; @@ -218,7 +217,7 @@ define([ }); let graphArea = $('
', { - class: config.dialogDynamicAreaClass + class: Util.config.dynamicAreaClass }).append( graphElement ); let headline = $('

', { diff --git a/js/app/login.js b/js/app/login.js index f8bbbceb..40a2cad1 100644 --- a/js/app/login.js +++ b/js/app/login.js @@ -343,7 +343,7 @@ define([ let initGallery = (newElements) => { if( newElements.length > 0){ // We have to add ALL thumbnail elements to the gallery! - // -> even those wthat are invisible (not lazyLoaded) now! + // -> even those which are invisible (not lazyLoaded) now! // -> This is required for "swipe" through all images let allThumbLinks = getThumbnailElements(); @@ -582,7 +582,7 @@ define([ * update all character panels -> set CSS class (e.g. after some panels were added/removed,..) */ let updateCharacterPanels = function(){ - let characterRows = $('.' + config.characterSelectionClass + ' .pf-dynamic-area').parent(); + let characterRows = $('.' + config.characterSelectionClass + ' .' + Util.config.dynamicAreaClass).parent(); let rowClassIdentifier = ((12 / characterRows.length ) <= 3) ? 3 : (12 / characterRows.length); $(characterRows).removeClass().addClass('col-sm-' + rowClassIdentifier); }; @@ -635,7 +635,7 @@ define([ // request character data for each character panel requirejs(['text!templates/ui/character_panel.html', 'mustache'], function(template, Mustache){ - $('.' + config.characterSelectionClass + ' .pf-dynamic-area').each(function(){ + $('.' + config.characterSelectionClass + ' .' + Util.config.dynamicAreaClass).each(function(){ let characterElement = $(this); characterElement.showLoadingAnimation(); @@ -814,7 +814,7 @@ define([ // init carousel initCarousel(); - // init scrollspy + // init scrollSpy // -> after "Carousel"! required for correct "viewport" calculation (Gallery)! initScrollSpy(); diff --git a/js/app/map/map.js b/js/app/map/map.js index 8fa47343..1ff86a97 100644 --- a/js/app/map/map.js +++ b/js/app/map/map.js @@ -559,7 +559,7 @@ define([ // confirm dialog bootbox.confirm('Is this connection really gone?', function(result){ if(result){ - $().deleteConnections([activeConnection]); + MapUtil.deleteConnections([activeConnection]); } }); break; @@ -1385,158 +1385,73 @@ define([ * @param connection */ let saveConnection = function(connection){ - if( connection instanceof jsPlumb.Connection ){ + if(connection instanceof jsPlumb.Connection){ let map = connection._jsPlumb.instance; - let mapContainer = $( map.getContainer() ); - + let mapContainer = $(map.getContainer()); let mapId = mapContainer.data('id'); + let connectionData = MapUtil.getDataByConnection(connection); + connectionData.mapId = mapId; - let requestData = { - mapData: { - id: mapId - }, - connectionData: connectionData - }; + Util.request('PUT', 'connection', [], connectionData, { + connection: connection, + map: map, + mapId: mapId, + oldConnectionData: connectionData + }).then( + payload => { + let newConnectionData = payload.data; - $.ajax({ - type: 'POST', - url: Init.path.saveConnection, - data: requestData, - dataType: 'json', - context: { - connection: connection, - map: map, - mapId: mapId, - oldConnectionData: connectionData - } - }).done(function(responseData){ - let newConnectionData = responseData.connectionData; + if( !$.isEmptyObject(newConnectionData) ){ + let updateCon = false; - if( !$.isEmptyObject(newConnectionData) ){ - let updateCon = false; - - if(this.oldConnectionData.id > 0){ - // connection exists (e.g. drag&drop new target system... (ids should never changed) - let connection = $().getConnectionById(this.mapId, this.oldConnectionData.id); - updateCon = true; - }else{ - // new connection, check if connectionId was already updated (webSocket push is faster than ajax callback) - let connection = $().getConnectionById(this.mapId, newConnectionData.id); - - if(connection){ - // connection already updated - this.map.detach(this.connection, {fireEvent: false}); - }else{ - // .. else update this connection - connection = this.connection; + if(payload.context.oldConnectionData.id > 0){ + // connection exists (e.g. drag&drop new target system... (ids should never changed) + let connection = $().getConnectionById(payload.context.mapId, payload.context.oldConnectionData.id); updateCon = true; + }else{ + // new connection, check if connectionId was already updated (webSocket push is faster than ajax callback) + let connection = $().getConnectionById(payload.context.mapId, newConnectionData.id); + + if(connection){ + // connection already updated + payload.context.map.detach(payload.context.connection, {fireEvent: false}); + }else{ + // .. else update this connection + connection = payload.context.connection; + updateCon = true; + } } + + if(updateCon){ + // update connection data e.g. "scope" has auto detected + connection = updateConnection(connection, payload.context.oldConnectionData, newConnectionData); + + // new/updated connection should be cached immediately! + updateConnectionCache(payload.context.mapId, connection); + } + + // connection scope + let scope = MapUtil.getScopeInfoForConnection(newConnectionData.scope, 'label'); + + let title = 'New connection established'; + if(payload.context.oldConnectionData.id > 0){ + title = 'Connection switched'; + } + + Util.showNotify({title: title, text: 'Scope: ' + scope, type: 'success'}); + }else{ + // some save errors + payload.context.map.detach(payload.context.connection, {fireEvent: false}); } - - if(updateCon){ - // update connection data e.g. "scope" has auto detected - connection = updateConnection(connection, this.oldConnectionData, newConnectionData); - - // new/updated connection should be cached immediately! - updateConnectionCache(this.mapId, connection); - } - - // connection scope - let scope = MapUtil.getScopeInfoForConnection(newConnectionData.scope, 'label'); - - let title = 'New connection established'; - if(this.oldConnectionData.id > 0){ - title = 'Connection switched'; - } - - Util.showNotify({title: title, text: 'Scope: ' + scope, type: 'success'}); - }else{ - // some save errors - this.map.detach(this.connection, {fireEvent: false}); + }, + payload => { + // remove this connection from map + payload.context.map.detach(payload.context.connection, {fireEvent: false}); + Util.handleAjaxErrorResponse(payload); } - - // show errors - if( - responseData.error && - responseData.error.length > 0 - ){ - for(let i = 0; i < responseData.error.length; i++){ - let error = responseData.error[i]; - Util.showNotify({title: error.field + ' error', text: 'System: ' + error.message, type: error.type}); - } - } - }).fail(function(jqXHR, status, error){ - // remove this connection from map - this.map.detach(this.connection, {fireEvent: false}); - - let reason = status + ' ' + error; - Util.showNotify({title: jqXHR.status + ': saveConnection', text: reason, type: 'warning'}); - $(document).setProgramStatus('problem'); - }); - } - }; - - /** - * delete a connection and all related data - * @param connections - * @param callback - */ - $.fn.deleteConnections = function(connections, callback){ - if(connections.length > 0){ - - // remove connections from map - let removeConnections = function(tempConnections){ - for(let i = 0; i < tempConnections.length; i++){ - // if a connection is manually (drag&drop) detached, the jsPlumb instance does not exist any more - // connection is already deleted! - if(tempConnections[i]._jsPlumb){ - tempConnections[i]._jsPlumb.instance.detach(tempConnections[i], {fireEvent: false}); - } - } - }; - - // prepare delete request - let map = connections[0]._jsPlumb.instance; - let mapContainer = $( map.getContainer() ); - - let connectionIds = []; - // connectionIds for delete request - for(let i = 0; i < connections.length; i++){ - let connectionId = connections[i].getParameter('connectionId'); - // drag&drop a new connection does not have an id yet, if connection is not established correct - if(connectionId !== undefined){ - connectionIds[i] = connections[i].getParameter('connectionId'); - } - } - - if(connectionIds.length > 0){ - let requestData = { - mapId: mapContainer.data('id'), - connectionIds: connectionIds - }; - - $.ajax({ - type: 'POST', - url: Init.path.deleteConnection, - data: requestData, - dataType: 'json', - context: connections - }).done(function(data){ - // remove connections from map - removeConnections(this); - - // optional callback - if(callback){ - callback(); - } - }).fail(function(jqXHR, status, error){ - let reason = status + ' ' + error; - Util.showNotify({title: jqXHR.status + ': deleteSystem', text: reason, type: 'warning'}); - $(document).setProgramStatus('problem'); - }); - } + ); } }; @@ -2181,7 +2096,7 @@ define([ newJsPlumbInstance.bind('connectionDetached', function(info, e){ // a connection is manually (drag&drop) detached! otherwise this event should not be send! let connection = info.connection; - $().deleteConnections([connection]); + MapUtil.deleteConnections([connection]); }); newJsPlumbInstance.bind('checkDropAllowed', function(params){ diff --git a/js/app/map/scrollbar.js b/js/app/map/scrollbar.js index a2c2d09d..ff761fed 100644 --- a/js/app/map/scrollbar.js +++ b/js/app/map/scrollbar.js @@ -4,7 +4,9 @@ define([ 'jquery', 'app/init', - 'app/util' + 'app/util', + 'mousewheel', + 'customScrollbar' ], ($, Init, Util) => { 'use strict'; diff --git a/js/app/map/system.js b/js/app/map/system.js index be248a87..48acf959 100644 --- a/js/app/map/system.js +++ b/js/app/map/system.js @@ -56,47 +56,6 @@ define([ '- DPS and Logistic ships needed' }; - - /** - * save a new system and add it to the map - * @param requestData - * @param context - * @param callback - */ - let saveSystem = (requestData, context, callback) => { - $.ajax({ - type: 'POST', - url: Init.path.saveSystem, - data: requestData, - dataType: 'json', - context: context - }).done(function(responseData){ - let newSystemData = responseData.systemData; - - if( !$.isEmptyObject(newSystemData) ){ - Util.showNotify({title: 'New system', text: newSystemData.name, type: 'success'}); - - callback(newSystemData); - } - - if( - responseData.error && - responseData.error.length > 0 - ){ - for(let i = 0; i < responseData.error.length; i++){ - let error = responseData.error[i]; - Util.showNotify({title: error.field + ' error', text: 'System: ' + error.message, type: error.type}); - } - } - }).fail(function(jqXHR, status, error){ - let reason = status + ' ' + error; - Util.showNotify({title: jqXHR.status + ': saveSystem', text: reason, type: 'warning'}); - $(document).setProgramStatus('problem'); - }).always(function(){ - this.systemDialog.find('.modal-content').hideLoadingAnimation(); - }); - }; - /** * open "new system" dialog and add the system to map * optional the new system is connected to a "sourceSystem" (if available) @@ -256,7 +215,7 @@ define([ // get form Values let form = this.find('form'); - let systemDialogData = $(form).getFormValues(); + let formData = $(form).getFormValues(); // validate form form.validator('validate'); @@ -288,27 +247,31 @@ define([ }; } - systemDialogData.position = newPosition; + formData.position = newPosition; + formData.mapId = mapId; // ---------------------------------------------------------------------------------------- - let requestData = { - systemData: systemDialogData, - mapData: { - id: mapId - } - }; - this.find('.modal-content').showLoadingAnimation(); - saveSystem(requestData, { - systemDialog: this - }, (newSystemData) => { - // success callback - callback(map, newSystemData, sourceSystem); + Util.request('PUT', 'system', [], formData, { + systemDialog: systemDialog, + formElement: form, + map: map, + sourceSystem: sourceSystem + }, context => { + // always do + context.systemDialog.find('.modal-content').hideLoadingAnimation(); + }).then( + payload => { + Util.showNotify({title: 'New system', text: payload.data.name, type: 'success'}); + + callback(payload.context.map, payload.data, payload.context.sourceSystem); + bootbox.hideAll(); + }, + Util.handleAjaxErrorResponse + ); - bootbox.hideAll(); - }); return false; } } @@ -695,36 +658,29 @@ define([ */ let deleteSystems = (map, systems = [], callback = (systems) => {}) => { let mapContainer = $( map.getContainer() ); + let systemIds = systems.map(system => $(system).data('id')); - $.ajax({ - type: 'POST', - url: Init.path.deleteSystem, - data: { - mapId: mapContainer.data('id'), - systemIds: systems.map( system => $(system).data('id') ) + Util.request('DELETE', 'system', systemIds, { + mapId: mapContainer.data('id') + }, { + map: map, + systems: systems + }).then( + payload => { + // check if all systems were deleted that should get deleted + let deletedSystems = payload.context.systems.filter( + function(system){ + return this.indexOf( $(system).data('id') ) !== -1; + }, payload.data + ); + + // remove systems from map + removeSystems(payload.context.map, deletedSystems); + + callback(deletedSystems); }, - dataType: 'json', - context: { - map: map, - systems: systems - } - }).done(function(data){ - // check if all systems were deleted that should get deleted - let deletedSystems = this.systems.filter( - function(system){ - return this.indexOf( $(system).data('id') ) !== -1; - }, data.deletedSystemIds - ); - - // remove systems from map - removeSystems(this.map, deletedSystems); - - callback(deletedSystems); - }).fail(function(jqXHR, status, error){ - let reason = status + ' ' + error; - Util.showNotify({title: jqXHR.status + ': deleteSystem', text: reason, type: 'warning'}); - $(document).setProgramStatus('problem'); - }); + Util.handleAjaxErrorResponse + ); }; /** @@ -733,7 +689,7 @@ define([ * @param systems */ let removeSystems = (map, systems) => { - let removeSystemCallbak = deleteSystem => { + let removeSystemCallback = deleteSystem => { map.remove(deleteSystem); }; @@ -741,10 +697,10 @@ define([ system = $(system); // check if system is "active" - if( system.hasClass(config.systemActiveClass) ){ + if(system.hasClass(config.systemActiveClass)){ delete Init.currentSystemData; // get parent Tab Content and fire clear modules event - let tabContentElement = MapUtil.getTabContentElementByMapElement( system ); + let tabContentElement = MapUtil.getTabContentElementByMapElement(system); $(tabContentElement).trigger('pf:removeSystemModules'); } @@ -759,7 +715,7 @@ define([ // remove system system.velocity('transition.whirlOut', { duration: Init.animationSpeed.mapDeleteSystem, - complete: removeSystemCallbak + complete: removeSystemCallback }); } }; diff --git a/js/app/map/util.js b/js/app/map/util.js index 9db974d7..13f96227 100644 --- a/js/app/map/util.js +++ b/js/app/map/util.js @@ -350,6 +350,67 @@ define([ return data; }; + /** + * delete a connection and all related data + * @param connections + * @param callback + */ + let deleteConnections = (connections, callback) => { + if(connections.length > 0){ + + // remove connections from map + let removeConnections = connections => { + for(let connection of connections){ + connection._jsPlumb.instance.detach(connection, {fireEvent: false}); + } + }; + + // prepare delete request + let map = connections[0]._jsPlumb.instance; + let mapContainer = $(map.getContainer()); + + // connectionIds for delete request + let connectionIds = []; + for(let connection of connections){ + let connectionId = connection.getParameter('connectionId'); + // drag&drop a new connection does not have an id yet, if connection is not established correct + if(connectionId !== undefined){ + connectionIds.push(connectionId); + } + } + + if(connectionIds.length > 0){ + Util.request('DELETE', 'connection', connectionIds, { + mapId: mapContainer.data('id') + }, { + connections: connections + }).then( + payload => { + // check if all connections were deleted that should get deleted + let deletedConnections = payload.context.connections.filter( + function(connection){ + // if a connection is manually (drag&drop) detached, the jsPlumb instance does not exist any more + // connection is already deleted! + return ( + connection._jsPlumb && + this.indexOf( connection.getParameter('connectionId') ) !== -1 + ); + }, payload.data + ); + + // remove connections from map + removeConnections(deletedConnections); + + if(callback){ + callback(); + } + }, + Util.handleAjaxErrorResponse + ); + } + } + }; + /** * get connection related data from a connection * -> data requires a signature bind to that connection @@ -1682,6 +1743,7 @@ define([ setConnectionWHStatus: setConnectionWHStatus, getScopeInfoForConnection: getScopeInfoForConnection, getDataByConnections: getDataByConnections, + deleteConnections: deleteConnections, getConnectionDataFromSignatures: getConnectionDataFromSignatures, getEndpointOverlayContent: getEndpointOverlayContent, getTabContentElementByMapElement: getTabContentElementByMapElement, diff --git a/js/app/mappage.js b/js/app/mappage.js index 3ddef1fb..cc53aa73 100644 --- a/js/app/mappage.js +++ b/js/app/mappage.js @@ -79,15 +79,21 @@ define([ let reason = status + ' ' + jqXHR.status + ': ' + error; let errorData = []; + let redirect = false; // redirect user to other page e.g. login + let reload = true; // reload current page (default: true) if(jqXHR.responseJSON){ // handle JSON - let errorObj = jqXHR.responseJSON; + let responseObj = jqXHR.responseJSON; if( - errorObj.error && - errorObj.error.length > 0 + responseObj.error && + responseObj.error.length > 0 ){ - errorData = errorObj.error; + errorData = responseObj.error; + } + + if(responseObj.reroute){ + redirect = responseObj.reroute; } }else{ // handle HTML @@ -98,7 +104,13 @@ define([ } console.error(' ↪ %s Error response: %o', jqXHR.url, errorData); - $(document).trigger('pf:shutdown', {status: jqXHR.status, reason: reason, error: errorData}); + $(document).trigger('pf:shutdown', { + status: jqXHR.status, + reason: reason, + error: errorData, + redirect: redirect, + reload: reload + }); }; // map init functions ========================================================================================= diff --git a/js/app/page.js b/js/app/page.js index f7e91be7..eef0e1e7 100644 --- a/js/app/page.js +++ b/js/app/page.js @@ -787,12 +787,14 @@ define([ label: ' restart', className: ['btn-primary'].join(' '), callback: function(){ - // check if error was 5xx -> reload page - // -> else try to logout -> ajax request - if(data.status >= 500 && data.status < 600){ - // redirect to login - window.location = '../'; + if(data.redirect) { + // ... redirect user to e.g. login form page ... + Util.redirect(data.redirect, ['logout']); + }else if(data.reload){ + // ... or reload current page ... + location.reload(); }else{ + // ... fallback try to logout user documentElement.trigger('pf:menuLogout'); } } @@ -811,12 +813,9 @@ define([ }; // add error information (if available) - if( - data.error && - data.error.length - ){ - for(let i = 0; i < data.error.length; i++){ - options.content.textSmaller.push(data.error[i].message); + if(data.error && data.error.length){ + for(let error of data.error){ + options.content.textSmaller.push(error.message); } } diff --git a/js/app/ui/dialog/delete_account.js b/js/app/ui/dialog/delete_account.js index a3d7ce6a..30413023 100644 --- a/js/app/ui/dialog/delete_account.js +++ b/js/app/ui/dialog/delete_account.js @@ -76,7 +76,7 @@ define([ dialogElement.find('.modal-content').hideLoadingAnimation(); if(responseData.reroute !== undefined){ - Util.redirect(responseData.reroute, []); + Util.redirect(responseData.reroute); }else if( responseData.error && responseData.error.length > 0 diff --git a/js/app/ui/dialog/jump_info.js b/js/app/ui/dialog/jump_info.js index fe37d365..6f7f76cd 100644 --- a/js/app/ui/dialog/jump_info.js +++ b/js/app/ui/dialog/jump_info.js @@ -22,7 +22,7 @@ define([ * show jump info dialog */ $.fn.showJumpInfoDialog = function(){ - requirejs(['text!templates/dialog/jump_info.html', 'mustache'], (template, Mustache) => { + requirejs(['text!templates/dialog/jump_info.html', 'mustache', 'datatables.loader'], (template, Mustache) => { let data = { config: config, wormholes: Object.keys(Init.wormholes).map(function(k){ return Init.wormholes[k]; }), // convert Json to array diff --git a/js/app/ui/dialog/map_info.js b/js/app/ui/dialog/map_info.js index 2a5b07eb..84fb338e 100644 --- a/js/app/ui/dialog/map_info.js +++ b/js/app/ui/dialog/map_info.js @@ -654,7 +654,7 @@ define([ // deleteSignatures(row); let connection = $().getConnectionById(mapData.config.id, rowData.id); - $().deleteConnections([connection], function(){ + MapUtil.deleteConnections([connection], () => { // callback function after ajax "delete" success // remove table row tempTableElement.DataTable().rows(deleteRowElement).remove().draw(); diff --git a/js/app/ui/dialog/stats.js b/js/app/ui/dialog/stats.js index 2b64689c..6e3f2a8a 100644 --- a/js/app/ui/dialog/stats.js +++ b/js/app/ui/dialog/stats.js @@ -404,7 +404,7 @@ define([ */ let getStatsData = function(requestData, context){ - context.dynamicArea = $('#' + config.statsContainerId + ' .pf-dynamic-area'); + context.dynamicArea = $('#' + config.statsContainerId + ' .' + Util.config.dynamicAreaClass); context.dynamicArea.showLoadingAnimation(); $.ajax({ @@ -753,7 +753,7 @@ define([ * show activity stats dialog */ $.fn.showStatsDialog = function(){ - requirejs(['text!templates/dialog/stats.html', 'mustache', 'datatables.loader'], function(template, Mustache){ + requirejs(['text!templates/dialog/stats.html', 'mustache', 'datatables.loader'], (template, Mustache) => { // get current statistics map settings let logActivityEnabled = false; let activeMap = Util.getMapModule().getActiveMap(); @@ -806,13 +806,7 @@ define([ title: 'Statistics', message: content, size: 'large', - show: false, - buttons: { - close: { - label: 'close', - className: 'btn-default' - } - } + show: false }); // model events diff --git a/js/app/ui/dialog/system_effects.js b/js/app/ui/dialog/system_effects.js index da79dc8c..de19846c 100644 --- a/js/app/ui/dialog/system_effects.js +++ b/js/app/ui/dialog/system_effects.js @@ -2,7 +2,6 @@ * system effects dialog */ - define([ 'jquery', 'app/init', @@ -10,12 +9,14 @@ define([ 'app/render', 'bootbox', 'app/map/util' -], function($, Init, Util, Render, bootbox, MapUtil){ +], ($, Init, Util, Render, bootbox, MapUtil) => { 'use strict'; let config = { // system effect dialog - systemEffectDialogWrapperClass: 'pf-system-effect-dialog-wrapper' // class for system effect dialog + systemEffectDialogClass: 'pf-system-effect-dialog', // class for system effect dialog + + systemEffectTableClass: 'pf-system-effect-table' // Table class for effect tables }; let cache = { @@ -26,20 +27,23 @@ define([ * show system effect dialog */ $.fn.showSystemEffectInfoDialog = function(){ + requirejs(['datatables.loader'], () => { - // cache table structure - if(!cache.systemEffectDialog){ - - let dialogWrapperElement = $('
', { - class: config.systemEffectDialogWrapperClass + let rowElement = $('
', { + class: 'row' }); let systemEffectData = Util.getSystemEffectData(); - $.each( systemEffectData.wh, function(effectName, effectData ){ + // last active (hover) table columnIndex + let lastActiveColIndex = null; + + let colCount = 0; + for(let [effectName, effectData] of Object.entries(systemEffectData.wh)){ + colCount++; let table = $('', { - class: ['table', 'table-condensed'].join(' ') + class: ['compact', 'stripe', 'order-column', 'row-border', config.systemEffectTableClass].join(' ') }); let tbody = $(''); @@ -51,8 +55,7 @@ define([ let systemEffectName = MapUtil.getEffectInfoForSystem(effectName, 'name'); let systemEffectClass = MapUtil.getEffectInfoForSystem(effectName, 'class'); - $.each( effectData, function(areaId, areaData ){ - + for(let [areaId, areaData] of Object.entries(effectData)){ let systemType = 'C' + areaId; let securityClass = Util.getSecurityClassForSystem( systemType ); @@ -61,20 +64,20 @@ define([ thead.append( rows[0] ); rows[0].append( - $('') ); tbody.append(rows[i + 1]); @@ -87,21 +90,100 @@ define([ rows[i + 1].append( $('
').html('  ' + systemEffectName).prepend( + $('').html('  ' + systemEffectName).prepend( $('', { - class: ['fas', 'fa-square', 'fa-fw', systemEffectClass].join(' ') + class: ['fas', 'fa-square', systemEffectClass].join(' ') }) ) ); } - rows[0].append( $('', { + rows[0].append( $('', { class: ['text-right', 'col-xs-1', securityClass].join(' ') }).text( systemType )); - $.each( areaData, function(i, data ){ - + for(let [i, data] of Object.entries(areaData)){ + i = parseInt(i); if(areaId === '1'){ rows.push( $('
', { class: 'text-right' }).text( data.value )); - }); + } + } + let colElement = $('
', { + class: ['col-md-6'].join(' ') + }).append( + $('
', { + class: [Util.config.dynamicAreaClass].join(' ') + }).append( + table.append(thead).append(tbody) + ) + ); + rowElement.append(colElement); + + // add clearfix after even col count + if(colCount % 2 === 0){ + rowElement.append( + $('
', { + class: ['clearfix', 'visible-md', 'visible-lg'].join(' ') + }) + ); + } + + cache.systemEffectDialog = rowElement; + } + + let effectsDialog = bootbox.dialog({ + className: config.systemEffectDialogClass, + title: 'System effect information', + message: cache.systemEffectDialog, + size: 'large', + show: false + }); + + effectsDialog.on('show.bs.modal', function(e){ + let headerAll = $(); + let columnsAll = $(); + + let removeColumnHighlight = () => { + headerAll.removeClass('colHighlight'); + columnsAll.removeClass('colHighlight'); + }; + + let tableApis = $(this).find('table').DataTable({ + pageLength: -1, + paging: false, + lengthChange: false, + ordering: false, + searching: false, + info: false, + columnDefs: [], + data: null, // use DOM data overwrites [] default -> data.loader.js + initComplete: function(settings, json){ + let tableApi = this.api(); + + tableApi.tables().nodes().to$().on('mouseover', 'td', function(){ + // inside table cell -> get current hover colIndex + let colIndex = tableApi.cell(this).index().column; + + if(colIndex !== lastActiveColIndex){ + removeColumnHighlight(); + + lastActiveColIndex = colIndex; + + if(colIndex > 0){ + // active column changed -> highlight same colIndex on other tables + let tableApis = $.fn.dataTable.tables({ visible: false, api: true }) + .tables('.' + config.systemEffectTableClass); + + let columns = tableApis.columns(colIndex); + columns.header().flatten().to$().addClass('colHighlight'); + columns.nodes().flatten().to$().addClass('colHighlight'); + } + } + }).on('mouseleave', function(){ + // no longer inside table + lastActiveColIndex = null; + removeColumnHighlight(); + }); + } }); - dialogWrapperElement.append(table.append(thead).append(tbody)); - - cache.systemEffectDialog = dialogWrapperElement; + // table cells will not change so we should cache them once + headerAll = tableApis.columns().header().to$(); + columnsAll = tableApis.cells().nodes().to$(); }); - } - bootbox.dialog({ - title: 'System effect information', - message: cache.systemEffectDialog + effectsDialog.on('hide.bs.modal', function(e){ + // destroy logTable + $(this).find('table').DataTable().destroy(true); + }); + + effectsDialog.modal('show'); }); - }; }); \ No newline at end of file diff --git a/js/app/ui/form_element.js b/js/app/ui/form_element.js index 327433b8..061f356f 100644 --- a/js/app/ui/form_element.js +++ b/js/app/ui/form_element.js @@ -12,7 +12,8 @@ define([ let config = { // Select2 - resultOptionImageClass: 'pf-result-image' // class for Select2 result option entry with image + resultOptionImageClass: 'pf-result-image', // class for Select2 result option entry with image + select2ImageLazyLoadClass: 'pf-select2-image-lazyLoad' // class for Select2 result images that should be lazy loaded }; /** @@ -60,7 +61,7 @@ define([ } if(imagePath){ - thumb = ''; + thumb = ''; }else if(iconName){ thumb = ''; } @@ -150,8 +151,29 @@ define([ let markup = ''; if(parts.length === 2){ // wormhole data -> 2 columns + + let styleClass = ['pf-fake-connection-text']; + if(state.metaData){ + let metaData = state.metaData; + if(metaData.type){ + let type = metaData.type; + if(type.includes('wh_eol')){ + styleClass.push('pf-wh-eol'); + } + if(type.includes('wh_reduced')){ + styleClass.push('pf-wh-reduced'); + } + if(type.includes('wh_critical')){ + styleClass.push('pf-wh-critical'); + } + if(type.includes('frigate')){ + styleClass.push('pf-wh-frig'); + } + } + } + let securityClass = Util.getSecurityClassForSystem(parts[1]); - markup += '' + parts[0] + '  '; + markup += '' + parts[0] + '  '; markup += '' + parts[1] + ''; }else{ markup += '' + state.text + ''; @@ -564,6 +586,7 @@ define([ return group; }); },*/ + disabled: options.hasOwnProperty('disabled') ? options.disabled : false, allowClear: options.maxSelectionLength <= 1, maximumSelectionLength: options.maxSelectionLength, templateResult: formatCategoryTypeResultData @@ -605,6 +628,7 @@ define([ return { id: type.id, text: type.name, + mass: type.hasOwnProperty('mass') ? type.mass : null, groupId: this.groupId, categoryId: this.categoryId, categoryType: this.categoryType @@ -783,6 +807,11 @@ define([ return this.each(function(){ let selectElement = $(this); + + // remove existing from DOM in case "data" is explicit set + if(options.data){ + selectElement.empty(); + } selectElement.select2(options); // initial open dropDown diff --git a/js/app/ui/module/connection_info.js b/js/app/ui/module/connection_info.js index b9e4a6a7..81172a10 100644 --- a/js/app/ui/module/connection_info.js +++ b/js/app/ui/module/connection_info.js @@ -6,8 +6,9 @@ define([ 'jquery', 'app/init', 'app/util', + 'bootbox', 'app/map/util' -], ($, Init, Util, MapUtil) => { +], ($, Init, Util, bootbox, MapUtil) => { 'use strict'; let config = { @@ -30,7 +31,6 @@ define([ connectionInfoPanelClass: 'pf-connection-info-panel', // class for connection info panels connectionInfoPanelId: 'pf-connection-info-panel-', // id prefix for connection info panels - dynamicAreaClass: 'pf-dynamic-area', // class for "dynamic" areas controlAreaClass: 'pf-module-control-area', // class for "control" areas // info table @@ -47,9 +47,17 @@ define([ connectionInfoTableCellMassLeftClass: 'pf-connection-info-mass-left', // class for "mass left" table cell // dataTable + tableToolbarCondensedClass: 'pf-dataTable-condensed-toolbar', // class for condensed table toolbar connectionInfoTableClass: 'pf-connection-info-table', // class for connection tables tableCellImageClass: 'pf-table-image-cell', // class for table "image" cells tableCellCounterClass: 'pf-table-counter-cell', // class for table "counter" cells + tableCellActionClass: 'pf-table-action-cell', // class for "action" cells + + // connection dialog + connectionDialogId: 'pf-connection-info-dialog', // id for "connection" dialog + typeSelectId: 'pf-connection-info-dialog-type-select', // id for "ship type" select + shipMassId: 'pf-connection-info-dialog-mass', // id for "ship mass" input + characterSelectId: 'pf-connection-info-dialog-character-select', // id for "character" select // config showShip: true // default for "show current ship mass" toggle @@ -110,7 +118,7 @@ define([ */ let getInfoPanelControl = (mapId) => { let connectionElement = getConnectionElement(mapId, 0).append($('
', { - class: [config.dynamicAreaClass, config.controlAreaClass].join(' '), + class: [Util.config.dynamicAreaClass, config.controlAreaClass].join(' '), html: ' add connection  ctrl + click' })); @@ -128,7 +136,7 @@ define([ let scopeLabel = MapUtil.getScopeInfoForConnection(connectionData.scope, 'label'); let element = $('
', { - class: [config.dynamicAreaClass, config.controlAreaClass].join(' ') + class: [Util.config.dynamicAreaClass, config.controlAreaClass].join(' ') }).append( $('', { class: ['table', 'table-condensed', 'pf-table-fixed', config.moduleTableClass].join(' ') @@ -512,6 +520,35 @@ define([ return moduleElement.find('.' + config.connectionInfoPanelClass).not('#' + getConnectionElementId(0)); }; + /** + * enrich connectionData with "logs" data (if available) and other "missing" data + * @param connectionsData + * @param newConnectionsData + * @returns {*} + */ + let enrichConnectionsData = (connectionsData, newConnectionsData) => { + for(let i = 0; i < connectionsData.length; i++){ + for(let newConnectionData of newConnectionsData){ + if(connectionsData[i].id === newConnectionData.id){ + // copy some missing data + connectionsData[i].character = newConnectionData.character; + connectionsData[i].created = newConnectionData.created; + connectionsData[i].type = newConnectionData.type; + // check for mass logs and copy data + if(newConnectionData.logs && newConnectionData.logs.length){ + connectionsData[i].logs = newConnectionData.logs; + } + // check for signatures and copy data + if(newConnectionData.signatures && newConnectionData.signatures.length){ + connectionsData[i].signatures = newConnectionData.signatures; + } + break; + } + } + } + return connectionsData; + }; + /** * request connection log data * @param requestData @@ -531,24 +568,7 @@ define([ dataType: 'json', context: context }).done(function(connectionsData){ - // enrich connectionData with "logs" data (if available) and other "missing" data - for(let i = 0; i < this.connectionsData.length; i++){ - for(let connectionData of connectionsData){ - if(this.connectionsData[i].id === connectionData.id){ - // copy some missing data - this.connectionsData[i].created = connectionData.created; - // check for mass logs and copy data - if(connectionData.logs && connectionData.logs.length){ - this.connectionsData[i].logs = connectionData.logs; - } - // check for signatures and copy data - if(connectionData.signatures && connectionData.signatures.length){ - this.connectionsData[i].signatures = connectionData.signatures; - } - break; - } - } - } + this.connectionsData = enrichConnectionsData(this.connectionsData, connectionsData); callback(this.moduleElement, this.connectionsData); }).always(function(){ @@ -593,9 +613,9 @@ define([ */ let addConnectionsData = (moduleElement, connectionsData) => { - let getRowIndexesByData = (dataTable, colName, value) => { - return dataTable.rows().eq(0).filter((rowIdx) => { - return (dataTable.cell(rowIdx, colName + ':name').data() === value); + let getRowIndexesByData = (tableApi, colName, value) => { + return tableApi.rows().eq(0).filter((rowIdx) => { + return (tableApi.cell(rowIdx, colName + ':name').data() === value); }); }; @@ -608,42 +628,47 @@ define([ connectionInfoElement.data('connectionData', connectionData); // update dataTable --------------------------------------------------------------- - let dataTable = connectionElement.find('.dataTable').dataTable().api(); + let tableApi = connectionElement.find('.dataTable').dataTable().api(); if(connectionData.logs && connectionData.logs.length > 0){ for(let i = 0; i < connectionData.logs.length; i++){ let rowData = connectionData.logs[i]; - let row = null; + let rowNew = null; let animationStatus = null; - let indexes = getRowIndexesByData(dataTable, 'index', rowData.id); + let indexes = getRowIndexesByData(tableApi, 'index', rowData.id); if(indexes.length === 0){ // row not found -> add new row - row = dataTable.row.add( rowData ); + rowNew = tableApi.row.add(rowData); animationStatus = 'added'; - } - /* else{ - // we DON´t expect changes -> no row update) + }else{ // update row with FIRST index - //row = dataTable.row( parseInt(indexes[0]) ); - // update row data - //row.data(connectionData.logs[i]); - //animationStatus = 'changed'; - } */ + let row = tableApi.row( parseInt(indexes[0])); + let rowDataCurrent = row.data(); + + // check if row data changed + if(rowDataCurrent.updated.updated !== rowData.updated.updated){ + // ... row changed -> delete old and re-add + // -> cell actions might have changed + row.remove(); + rowNew = tableApi.row.add(rowData); + animationStatus = 'changed'; + } + } if( animationStatus !== null && - row.length > 0 + rowNew.length > 0 ){ - row.nodes().to$().data('animationStatus', animationStatus); + rowNew.nodes().to$().data('animationStatus', animationStatus); } } }else{ // clear table or leave empty - dataTable.clear(); + tableApi.clear(); } // redraw dataTable - dataTable.draw(false); + tableApi.draw(false); } } }; @@ -664,11 +689,46 @@ define([ let table = $('
', { class: ['compact', 'stripe', 'order-column', 'row-border', 'nowrap', config.connectionInfoTableClass].join(' ') - }).append(''); + }).append(''); connectionElement.append(table); // init empty table let logTable = table.DataTable({ + dom: '<"container-fluid"' + + '<"row ' + config.tableToolbarCondensedClass + '"' + + '<"col-xs-5"i><"col-xs-5"p><"col-xs-2 text-right"B>>' + + '<"row"tr>>', + buttons: { + name: 'tableTools', + buttons: [ + { + name: 'addLog', + className: config.moduleHeadlineIconClass, + text: '', + action: function(e, tableApi, node, conf){ + let logData = {}; + + // pre-fill form with current character data (if available) + let currentUserData = Util.getCurrentUserData(); + if(currentUserData && currentUserData.character){ + logData.character = { + id: currentUserData.character.id, + name: currentUserData.character.name + }; + if(currentUserData.character.log){ + logData.ship = { + id: currentUserData.character.log.ship.typeId, + name: currentUserData.character.log.ship.typeName, + mass: currentUserData.character.log.ship.mass + }; + } + } + + showLogDialog(moduleElement, connectionElement, connectionData, logData); + } + } + ] + }, pageLength: 8, paging: true, pagingType: 'simple', @@ -679,11 +739,14 @@ define([ searching: false, hover: false, autoWidth: false, - // rowId: 'systemTo', language: { emptyTable: 'No jumps recorded', - info: '_START_ to _END_ of _MAX_', - infoEmpty: '' + info: '_START_ - _END_ of _MAX_', + infoEmpty: '', + paginate: { + previous: '', + next: '' + } }, columnDefs: [ { @@ -693,10 +756,25 @@ define([ orderable: false, searchable: false, width: 20, - class: 'text-center', - data: 'id' + className: ['text-center', 'txt-color'].join(' '), + data: 'id', + createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ + if( + !rowData.record || + (rowData.updated.updated !== rowData.created.created) + ){ + // log was manually modified or added + $(cell) + .addClass(Util.config.helpClass) + .addClass( 'txt-color-orange').tooltip({ + container: 'body', + title: 'added/updated manually' + }); + } + } },{ targets: 1, + name: 'ship', title: '', width: 26, orderable: false, @@ -716,16 +794,17 @@ define([ } },{ targets: 2, + name: 'character', title: '', width: 26, orderable: false, className: [Util.config.helpDefaultClass, 'text-center', config.tableCellImageClass].join(' '), - data: 'created.character', + data: 'character', render: { - _: function(data, type, row){ - let value = data.name; + _: (cellData, type, rowData, meta) => { + let value = cellData.name; if(type === 'display'){ - value = ''; + value = ''; } return value; } @@ -735,20 +814,26 @@ define([ } },{ targets: 3, + name: 'mass', title: 'mass', className: ['text-right'].join(' ') , data: 'ship.mass', render: { - _: function(data, type, row){ - let value = data; + _: (cellData, type, rowData, meta) => { + let value = cellData; if(type === 'display'){ value = Util.formatMassValue(value); + if(!rowData.active){ + // log is "deleted" + value = '' + value + ''; + } } return value; } } },{ targets: 4, + name: 'created', title: 'log', width: 55, className: ['text-right', config.tableCellCounterClass].join(' '), @@ -756,6 +841,108 @@ define([ createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ $(cell).initTimestampCounter('d'); } + },{ + targets: 5, + name: 'edit', + title: '', + orderable: false, + searchable: false, + width: 10, + className: ['text-center', config.tableCellActionClass, config.moduleHeadlineIconClass].join(' '), + data: null, + render: { + display: data => { + let icon = ''; + if(data.active){ + icon = ''; + } + return icon; + } + }, + createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ + let tableApi = this.api(); + + if($(cell).is(':empty')){ + $(cell).removeClass(config.tableCellActionClass + ' ' + config.moduleHeadlineIconClass); + }else{ + $(cell).on('click', function(e){ + showLogDialog(moduleElement, connectionElement, connectionData, rowData); + }); + } + } + },{ + targets: 6, + name: 'delete', + title: '', + orderable: false, + searchable: false, + width: 10, + className: ['text-center', config.tableCellActionClass].join(' '), + data: 'active', + render: { + display: data => { + let val = ''; + if(data){ + val = ''; + } + return val; + } + }, + createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ + let tableApi = this.api(); + + if(rowData.active){ + let confirmationSettings = { + container: 'body', + placement: 'left', + btnCancelClass: 'btn btn-sm btn-default', + btnCancelLabel: 'cancel', + btnCancelIcon: 'fas fa-fw fa-ban', + title: 'delete jump log', + btnOkClass: 'btn btn-sm btn-danger', + btnOkLabel: 'delete', + btnOkIcon: 'fas fa-fw fa-times', + onConfirm : function(e, target){ + // get current row data (important!) + // -> "rowData" param is not current state, values are "on createCell()" state + rowData = tableApi.row($(cell).parents('tr')).data(); + + connectionElement.find('table').showLoadingAnimation(); + + Util.request('DELETE', 'log', rowData.id, {}, { + connectionElement: connectionElement + }, requestAlways) + .then( + payload => { + addConnectionsData(moduleElement, enrichConnectionsData([connectionData], payload.data)); + }, + Util.handleAjaxErrorResponse + ); + } + }; + + // init confirmation dialog + $(cell).confirmation(confirmationSettings); + }else { + $(cell).on('click', function(e){ + connectionElement.find('table').showLoadingAnimation(); + + let requestData = { + active: 1 + }; + + Util.request('PATCH', 'log', rowData.id, requestData, { + connectionElement: connectionElement + }, requestAlways) + .then( + payload => { + addConnectionsData(moduleElement, enrichConnectionsData([connectionData], payload.data)); + }, + Util.handleAjaxErrorResponse + ); + }); + } + } } ], drawCallback: function(settings){ @@ -773,27 +960,34 @@ define([ }, footerCallback: function(row, data, start, end, display ){ - - let api = this.api(); - let sumColumnIndexes = [3]; + let tableApi = this.api(); + let sumColumnIndexes = ['mass:name', 'delete:name']; // column data for "sum" columns over this page - let pageTotalColumns = api + let pageTotalColumns = tableApi .columns( sumColumnIndexes, { page: 'all'} ) .data(); // sum columns for "total" sum - pageTotalColumns.each((colData, index) => { - pageTotalColumns[index] = colData.reduce((a, b) => { - return parseInt(a) + parseInt(b); + pageTotalColumns.each((colData, colIndex) => { + pageTotalColumns[colIndex] = colData.reduce((sum, val, rowIndex) => { + // sum "mass" (colIndex 0) only if not "deleted" (colIndex 1) + if(colIndex === 0 && pageTotalColumns[1][rowIndex]){ + return sum + parseInt(val); + }else{ + return sum; + } }, 0); }); - $(sumColumnIndexes).each((index, value) => { - $( api.column( value ).footer() ).text( Util.formatMassValue(pageTotalColumns[index]) ); + sumColumnIndexes.forEach((colSelector, index) => { + // only "mass" column footer needs updates + if(colSelector === 'mass:name'){ + $(tableApi.column(colSelector).footer()).text( Util.formatMassValue(pageTotalColumns[index]) ); - // save mass for further reCalculation of "info" table - connectionElement.find('.' + config.connectionInfoTableCellMassLogClass).data('mass', pageTotalColumns[index]); + // save mass for further reCalculation of "info" table + connectionElement.find('.' + config.connectionInfoTableCellMassLogClass).data('mass', pageTotalColumns[index]); + } }); // calculate "info" table ----------------------------------------------------- @@ -806,14 +1000,135 @@ define([ logTable.on('order.dt search.dt', function(){ let pageInfo = logTable.page.info(); - logTable.column(0, {search:'applied', order:'applied'}).nodes().each((cell, i) => { - let content = (pageInfo.recordsTotal - i) + '.  '; + logTable.column('index:name', {search:'applied', order:'applied'}).nodes().each((cell, i) => { + let content = (pageInfo.recordsTotal - i) + '.'; $(cell).html(content); }); }); } }; + /** + * + * @param context + */ + let requestAlways = (context) => { + context.connectionElement.find('table').hideLoadingAnimation(); + }; + + /** + * show jump log dialog + * @param moduleElement + * @param connectionElement + * @param connectionData + * @param logData + */ + let showLogDialog = (moduleElement, connectionElement, connectionData, logData = {}) => { + + let data = { + id: config.connectionDialogId, + typeSelectId: config.typeSelectId, + shipMassId: config.shipMassId, + characterSelectId: config.characterSelectId, + logData: logData, + massFormat: () => { + return (val, render) => { + return (parseInt(render(val) || 0) / 1000) || ''; + }; + } + }; + + requirejs(['text!templates/dialog/connection_log.html', 'mustache'], (template, Mustache) => { + let content = Mustache.render(template, data); + + let connectionDialog = bootbox.dialog({ + title: 'Jump log', + message: content, + show: false, + buttons: { + close: { + label: 'cancel', + className: 'btn-default' + }, + success: { + label: ' save', + className: 'btn-success', + callback: function(){ + let form = this.find('form'); + + // validate form + form.validator('validate'); + + // check whether the form is valid + let formValid = form.isValidForm(); + + if(formValid){ + // get form data + let formData = form.getFormValues(); + formData.id = Util.getObjVal(logData, 'id') || 0; + formData.connectionId = Util.getObjVal(connectionData, 'id') || 0; + formData.shipTypeId = Util.getObjVal(formData, 'shipTypeId') || 0; + formData.shipMass = parseInt((Util.getObjVal(formData, 'shipMass') || 0) * 1000); + formData.characterId = Util.getObjVal(formData, 'characterId') || 0; + + // we need some "additional" form data from the Select2 dropdown + // -> data is required on the backend side + let formDataShip = form.find('#' + config.typeSelectId).select2('data'); + let formDataCharacter = form.find('#' + config.characterSelectId).select2('data'); + formData.shipTypeName = formDataShip.length ? formDataShip[0].text : ''; + formData.characterName = formDataCharacter.length ? formDataCharacter[0].text : ''; + + let method = formData.id ? 'PATCH' : 'PUT'; + + Util.request(method, 'log', formData.id, formData, { + connectionElement: connectionElement, + formElement: form + }, requestAlways) + .then( + payload => { + addConnectionsData(moduleElement, enrichConnectionsData([connectionData], payload.data)); + this.modal('hide'); + }, + Util.handleAjaxErrorResponse + ); + + } + + return false; + } + } + } + }); + + connectionDialog.on('show.bs.modal', function(e){ + let modalContent = $('#' + config.connectionDialogId); + + // init type select live search + let selectElementType = modalContent.find('#' + config.typeSelectId); + selectElementType.initUniverseTypeSelect({ + categoryIds: [6], + maxSelectionLength: 1, + selected: [Util.getObjVal(logData, 'ship.id')] + }).on('select2:select select2:unselecting', function(e){ + // get ship mass from selected ship type and update mass input field + let shipMass = e.params.data ? e.params.data.mass / 1000 : ''; + modalContent.find('#' + config.shipMassId).val(shipMass); + }); + + // init character select live search + let selectElementCharacter = modalContent.find('#' + config.characterSelectId); + selectElementCharacter.initUniverseSearch({ + categoryNames: ['character'], + maxSelectionLength: 1 + }); + + }); + + // show dialog + connectionDialog.modal('show'); + }); + }; + /** * remove connection Panel from moduleElement * @param connectionElement diff --git a/js/app/ui/module/system_info.js b/js/app/ui/module/system_info.js index 81562a36..e5c84146 100644 --- a/js/app/ui/module/system_info.js +++ b/js/app/ui/module/system_info.js @@ -51,44 +51,6 @@ define([ // max character length for system description let maxDescriptionLength = 9000; - /** - * save system (description) - * @param requestData - * @param context - * @param callback - */ - let saveSystem = (requestData, context, callback) => { - context.descriptionArea.showLoadingAnimation(); - - $.ajax({ - type: 'POST', - url: Init.path.saveSystem, - data: requestData, - dataType: 'json', - context: context - }).done(function(responseData){ - let newSystemData = responseData.systemData; - - if( !$.isEmptyObject(newSystemData) ){ - callback(newSystemData); - } - - if( - responseData.error && - responseData.error.length > 0 - ){ - for(let error of responseData.error){ - Util.showNotify({title: error.field + ' error', text: 'System: ' + error.message, type: error.type}); - } - } - }).fail(function(jqXHR, status, error){ - let reason = status + ' ' + error; - Util.showNotify({title: jqXHR.status + ': saveSystem', text: reason, type: 'warning'}); - }).always(function(){ - this.descriptionArea.hideLoadingAnimation(); - }); - }; - /** * update trigger function for this module * compare data and update module @@ -307,21 +269,22 @@ define([ if(validDescription){ // ... valid -> save() - saveSystem({ - mapData: { - id: mapId - }, - systemData: { - id: systemData.id, - description: description - } + descriptionArea.showLoadingAnimation(); + + Util.request('PATCH', 'system', systemData.id, { + description: description }, { descriptionArea: descriptionArea - }, (systemData) => { - // .. save callback - context.$note.summernote('destroy'); - updateModule(moduleElement, systemData); - }); + }, context => { + // always do + context.descriptionArea.hideLoadingAnimation(); + }).then( + payload => { + context.$note.summernote('destroy'); + updateModule(moduleElement, payload.data); + }, + Util.handleAjaxErrorResponse + ); } }else{ // ... no changes -> no save() diff --git a/js/app/ui/module/system_intel.js b/js/app/ui/module/system_intel.js index 64dcc35b..62b5caae 100644 --- a/js/app/ui/module/system_intel.js +++ b/js/app/ui/module/system_intel.js @@ -337,7 +337,7 @@ define([ data: statusData }); - // init character counter + // init char counter let textarea = modalContent.find('#' + config.descriptionTextareaId); let charCounter = modalContent.find('.' + config.descriptionTextareaCharCounter); Util.updateCounter(textarea, charCounter, maxDescriptionLength); diff --git a/js/app/ui/module/system_killboard.js b/js/app/ui/module/system_killboard.js index eb568d7f..3a0d220e 100644 --- a/js/app/ui/module/system_killboard.js +++ b/js/app/ui/module/system_killboard.js @@ -32,7 +32,6 @@ define([ systemKillboardListImgCorp: 'pf-system-killboard-img-corp', // class for all corp logos labelRecentKillsClass: 'pf-system-killboard-label-recent', // class for "recent kills" label - dynamicAreaClass: 'pf-dynamic-area', // class for "dynamic" areas controlAreaClass: 'pf-module-control-area', // class for "control" areas minCountKills: 5, @@ -292,7 +291,7 @@ define([ */ let getControlElement = () => { let controlElement = $('
', { - class: [config.dynamicAreaClass, config.controlAreaClass, config.moduleHeadlineIconClass].join(' '), + class: [Util.config.dynamicAreaClass, config.controlAreaClass, config.moduleHeadlineIconClass].join(' '), html: '  load more' }); return controlElement; diff --git a/js/app/ui/module/system_signature.js b/js/app/ui/module/system_signature.js index c48866a2..13821487 100644 --- a/js/app/ui/module/system_signature.js +++ b/js/app/ui/module/system_signature.js @@ -405,6 +405,32 @@ define([ let newSelectOptions = []; let connectionOptions = []; + /** + * get option data for a single connection + * @param type + * @param connectionData + * @param systemData + * @returns {{value: *, text: string, metaData: {type: *}}} + */ + let getOption = (type, connectionData, systemData) => { + let text = 'UNKNOWN'; + if(type === 'source'){ + text = connectionData.sourceAlias + ' - ' + systemData.security; + }else if(type === 'target'){ + text = connectionData.targetAlias + ' - ' + systemData.security; + } + + let option = { + value: connectionData.id, + text: text, + metaData: { + type: connectionData.type + } + }; + + return option; + }; + for(let systemConnection of systemConnections){ let connectionData = MapUtil.getDataByConnection(systemConnection); @@ -416,19 +442,13 @@ define([ let targetSystemData = MapUtil.getSystemData(mapId, connectionData.target); if(targetSystemData){ // take target... - connectionOptions.push({ - value: connectionData.id, - text: connectionData.targetAlias + ' - ' + targetSystemData.security - }); + connectionOptions.push(getOption('target', connectionData, targetSystemData)); } }else if(systemData.id !== connectionData.source){ let sourceSystemData = MapUtil.getSystemData(mapId, connectionData.source); if(sourceSystemData){ // take source... - connectionOptions.push({ - value: connectionData.id, - text: connectionData.sourceAlias + ' - ' + sourceSystemData.security - }); + connectionOptions.push(getOption('source', connectionData, sourceSystemData)); } } } @@ -1119,7 +1139,14 @@ define([ let editableConnectionOnShown = cell => { $(cell).on('shown', function(e, editable){ let inputField = editable.input.$input; - inputField.addClass('pf-select2').initSignatureConnectionSelect(); + + // Select2 init would work without passing select options as "data", Select2 would grap data from DOM + // -> We want to pass "meta" data for each option into Select2 for formatting + let options = { + data: Util.convertXEditableOptionsToSelect2(editable) + }; + + inputField.addClass('pf-select2').initSignatureConnectionSelect(options); }); }; @@ -1534,7 +1561,10 @@ define([ let selected = $.fn.editableutils.itemsByValue(value, sourceData); if(selected.length && selected[0].value > 0){ let errorIcon = ' '; - $(this).html(FormElement.formatSignatureConnectionSelectionData({text: selected[0].text})).prepend(errorIcon); + $(this).html(FormElement.formatSignatureConnectionSelectionData({ + text: selected[0].text, + metaData: selected[0].metaData + })).prepend(errorIcon); }else{ $(this).empty(); } @@ -2150,7 +2180,7 @@ define([ // xEditable field should not open -> on 'click' // -> therefore disable "pointer-events" on "td" for some ms -> 'click' event is not triggered $(this).css('pointer-events', 'none'); - $(e.target.parentNode).toggleClass('selected'); + $(e.target).closest('tr').toggleClass('selected'); // check delete button checkDeleteSignaturesButton(e.data.tableApi); diff --git a/js/app/util.js b/js/app/util.js index 4df9a415..47f8f240 100644 --- a/js/app/util.js +++ b/js/app/util.js @@ -8,6 +8,7 @@ define([ 'conf/signature_type', 'bootbox', 'localForage', + 'lazyload', 'velocity', 'velocityUI', 'customScrollbar', @@ -65,10 +66,12 @@ define([ mapClass: 'pf-map' , // class for all maps // util - userStatusClass: 'pf-user-status', // class for player status + userStatusClass: 'pf-user-status', // class for player status + dynamicAreaClass: 'pf-dynamic-area', // class for "dynamic" areas // select2 select2Class: 'pf-select2', // class for all "Select2" ",e.querySelectorAll("[msallowcapture^='']").length&&g.push("[*^$]="+N+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||g.push("\\["+N+"*(?:value|"+L+")"),e.querySelectorAll("[id~="+w+"-]").length||g.push("~="),e.querySelectorAll(":checked").length||g.push(":checked"),e.querySelectorAll("a#"+w+"+*").length||g.push(".#.+[+~]")}),le(function(e){e.innerHTML="";var t=p.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&g.push("name"+N+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&g.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(n.matchesSelector=K.test(y=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&le(function(e){n.disconnectedMatch=y.call(e,"*"),y.call(e,"[s!='']:x"),v.push("!=",B)}),g=g.length&&new RegExp(g.join("|")),v=v.length&&new RegExp(v.join("|")),t=K.test(h.compareDocumentPosition),b=t||K.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},I=t?function(e,t){if(e===t)return d=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===p||e.ownerDocument===x&&b(x,e)?-1:t===p||t.ownerDocument===x&&b(x,t)?1:u?R(u,e)-R(u,t):0:4&r?-1:1)}:function(e,t){if(e===t)return d=!0,0;var n,r=0,o=e.parentNode,a=t.parentNode,i=[e],s=[t];if(!o||!a)return e===p?-1:t===p?1:o?-1:a?1:u?R(u,e)-R(u,t):0;if(o===a)return ue(e,t);for(n=e;n=n.parentNode;)i.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;i[r]===s[r];)r++;return r?ue(i[r],s[r]):i[r]===x?-1:s[r]===x?1:0},p):p},ae.matches=function(e,t){return ae(e,null,null,t)},ae.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&f(e),t=t.replace(W,"='$1']"),n.matchesSelector&&m&&!D[t+" "]&&(!v||!v.test(t))&&(!g||!g.test(t)))try{var r=y.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return ae(t,p,null,[e]).length>0},ae.contains=function(e,t){return(e.ownerDocument||e)!==p&&f(e),b(e,t)},ae.attr=function(e,t){(e.ownerDocument||e)!==p&&f(e);var o=r.attrHandle[t.toLowerCase()],a=o&&k.call(r.attrHandle,t.toLowerCase())?o(e,t,!m):void 0;return void 0!==a?a:n.attributes||!m?e.getAttribute(t):(a=e.getAttributeNode(t))&&a.specified?a.value:null},ae.escape=function(e){return(e+"").replace(te,ne)},ae.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},ae.uniqueSort=function(e){var t,r=[],o=0,a=0;if(d=!n.detectDuplicates,u=!n.sortStable&&e.slice(0),e.sort(I),d){for(;t=e[a++];)t===e[a]&&(o=r.push(a));for(;o--;)e.splice(r[o],1)}return u=null,e},o=ae.getText=function(e){var t,n="",r=0,a=e.nodeType;if(a){if(1===a||9===a||11===a){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===a||4===a)return e.nodeValue}else for(;t=e[r++];)n+=o(t);return n},(r=ae.selectors={cacheLength:50,createPseudo:se,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(J,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(J,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ae.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ae.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return X.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&z.test(n)&&(t=i(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(J,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=T[e+" "];return t||(t=new RegExp("(^|"+N+")"+e+"("+N+"|$)"))&&T(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var o=ae.attr(r,e);return null==o?"!="===t:!t||(o+="","="===t?o===n:"!="===t?o!==n:"^="===t?n&&0===o.indexOf(n):"*="===t?n&&o.indexOf(n)>-1:"$="===t?n&&o.slice(-n.length)===n:"~="===t?(" "+o.replace(M," ")+" ").indexOf(n)>-1:"|="===t&&(o===n||o.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,o){var a="nth"!==e.slice(0,3),i="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===o?function(e){return!!e.parentNode}:function(t,n,l){var c,u,d,f,p,h,m=a!==i?"nextSibling":"previousSibling",g=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!l&&!s,b=!1;if(g){if(a){for(;m;){for(f=t;f=f[m];)if(s?f.nodeName.toLowerCase()===v:1===f.nodeType)return!1;h=m="only"===e&&!h&&"nextSibling"}return!0}if(h=[i?g.firstChild:g.lastChild],i&&y){for(b=(p=(c=(u=(d=(f=g)[w]||(f[w]={}))[f.uniqueID]||(d[f.uniqueID]={}))[e]||[])[0]===C&&c[1])&&c[2],f=p&&g.childNodes[p];f=++p&&f&&f[m]||(b=p=0)||h.pop();)if(1===f.nodeType&&++b&&f===t){u[e]=[C,p,b];break}}else if(y&&(b=p=(c=(u=(d=(f=t)[w]||(f[w]={}))[f.uniqueID]||(d[f.uniqueID]={}))[e]||[])[0]===C&&c[1]),!1===b)for(;(f=++p&&f&&f[m]||(b=p=0)||h.pop())&&((s?f.nodeName.toLowerCase()!==v:1!==f.nodeType)||!++b||(y&&((u=(d=f[w]||(f[w]={}))[f.uniqueID]||(d[f.uniqueID]={}))[e]=[C,b]),f!==t)););return(b-=o)===r||b%r==0&&b/r>=0}}},PSEUDO:function(e,t){var n,o=r.pseudos[e]||r.setFilters[e.toLowerCase()]||ae.error("unsupported pseudo: "+e);return o[w]?o(t):o.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){for(var r,a=o(e,t),i=a.length;i--;)e[r=R(e,a[i])]=!(n[r]=a[i])}):function(e){return o(e,0,n)}):o}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(H,"$1"));return r[w]?se(function(e,t,n,o){for(var a,i=r(e,null,o,[]),s=e.length;s--;)(a=i[s])&&(e[s]=!(t[s]=a))}):function(e,o,a){return t[0]=e,r(t,null,a,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return ae(e,t).length>0}}),contains:se(function(e){return e=e.replace(J,ee),function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:se(function(e){return V.test(e||"")||ae.error("unsupported lang: "+e),e=e.replace(J,ee).toLowerCase(),function(t){var n;do{if(n=m?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:pe(!1),disabled:pe(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return G.test(e.nodeName)},input:function(e){return Y.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){for(var o=e.length;o--;)if(!e[o](t,n,r))return!1;return!0}:e[0]}function we(e,t,n,r,o){for(var a,i=[],s=0,l=e.length,c=null!=t;s-1&&(a[c]=!(i[c]=d))}}else v=we(v===i?v.splice(h,v.length):v),o?o(null,i,v,l):P.apply(i,v)})}function Ce(e){for(var t,n,o,a=e.length,i=r.relative[e[0].type],s=i||r.relative[" "],l=i?1:0,u=ye(function(e){return e===t},s,!0),d=ye(function(e){return R(t,e)>-1},s,!0),f=[function(e,n,r){var o=!i&&(r||n!==c)||((t=n).nodeType?u(e,n,r):d(e,n,r));return t=null,o}];l1&&be(f),l>1&&ve(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(H,"$1"),n,l0,o=e.length>0,a=function(a,i,s,l,u){var d,h,g,v=0,y="0",b=a&&[],w=[],x=c,S=a||o&&r.find.TAG("*",u),T=C+=null==x?1:Math.random()||.1,_=S.length;for(u&&(c=i===p||i||u);y!==_&&null!=(d=S[y]);y++){if(o&&d){for(h=0,i||d.ownerDocument===p||(f(d),s=!m);g=e[h++];)if(g(d,i||p,s)){l.push(d);break}u&&(C=T)}n&&((d=!g&&d)&&v--,a&&b.push(d))}if(v+=y,n&&y!==v){for(h=0;g=t[h++];)g(b,w,i,s);if(a){if(v>0)for(;y--;)b[y]||w[y]||(w[y]=O.call(l));w=we(w)}P.apply(l,w),u&&!a&&w.length>0&&v+t.length>1&&ae.uniqueSort(l)}return u&&(C=T,c=x),b};return n?se(a):a}return ge.prototype=r.filters=r.pseudos,r.setFilters=new ge,i=ae.tokenize=function(e,t){var n,o,a,i,s,l,c,u=_[e+" "];if(u)return t?0:u.slice(0);for(s=e,l=[],c=r.preFilter;s;){for(i in n&&!(o=q.exec(s))||(o&&(s=s.slice(o[0].length)||s),l.push(a=[])),n=!1,(o=U.exec(s))&&(n=o.shift(),a.push({value:n,type:o[0].replace(H," ")}),s=s.slice(n.length)),r.filter)!(o=X[i].exec(s))||c[i]&&!(o=c[i](o))||(n=o.shift(),a.push({value:n,type:i,matches:o}),s=s.slice(n.length));if(!n)break}return t?s.length:s?ae.error(e):_(e,l).slice(0)},s=ae.compile=function(e,t){var n,r=[],o=[],a=D[e+" "];if(!a){for(t||(t=i(e)),n=t.length;n--;)(a=Ce(t[n]))[w]?r.push(a):o.push(a);(a=D(e,Se(o,r))).selector=e}return a},l=ae.select=function(e,t,n,o){var a,l,c,u,d,f="function"==typeof e&&e,p=!o&&i(e=f.selector||e);if(n=n||[],1===p.length){if((l=p[0]=p[0].slice(0)).length>2&&"ID"===(c=l[0]).type&&9===t.nodeType&&m&&r.relative[l[1].type]){if(!(t=(r.find.ID(c.matches[0].replace(J,ee),t)||[])[0]))return n;f&&(t=t.parentNode),e=e.slice(l.shift().value.length)}for(a=X.needsContext.test(e)?0:l.length;a--&&(c=l[a],!r.relative[u=c.type]);)if((d=r.find[u])&&(o=d(c.matches[0].replace(J,ee),Z.test(l[0].type)&&me(t.parentNode)||t))){if(l.splice(a,1),!(e=o.length&&ve(l)))return P.apply(n,o),n;break}}return(f||s(e,p))(o,t,!m,n,!t||Z.test(e)&&me(t.parentNode)||t),n},n.sortStable=w.split("").sort(I).join("")===w,n.detectDuplicates=!!d,f(),n.sortDetached=le(function(e){return 1&e.compareDocumentPosition(p.createElement("fieldset"))}),le(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||ce("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&le(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ce("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),le(function(e){return null==e.getAttribute("disabled")})||ce(L,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),ae}(e);w.find=S,w.expr=S.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=S.uniqueSort,w.text=S.getText,w.isXMLDoc=S.isXML,w.contains=S.contains,w.escapeSelector=S.escape;var T=function(e,t,n){for(var r=[],o=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(o&&w(e).is(n))break;r.push(e)}return r},_=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function I(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var k=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function A(e,t,n){return m(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return l.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,o=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(A(this,e||[],!1))},not:function(e){return this.pushStack(A(this,e||[],!0))},is:function(e){return!!A(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var O,E=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var o,a;if(!e)return this;if(n=n||O,"string"==typeof e){if(!(o="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:E.exec(e))||!o[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(o[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(o[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),k.test(o[1])&&w.isPlainObject(t))for(o in t)m(this[o])?this[o](t[o]):this.attr(o,t[o]);return this}return(a=r.getElementById(o[2]))&&(this[0]=a,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,O=w(r);var P=/^(?:parents|prev(?:Until|All))/,F={children:!0,contents:!0,next:!0,prev:!0};function R(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){a.push(n);break}return this.pushStack(a.length>1?w.uniqueSort(a):a)},index:function(e){return e?"string"==typeof e?l.call(w(e),this[0]):l.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return T(e,"parentNode")},parentsUntil:function(e,t,n){return T(e,"parentNode",n)},next:function(e){return R(e,"nextSibling")},prev:function(e){return R(e,"previousSibling")},nextAll:function(e){return T(e,"nextSibling")},prevAll:function(e){return T(e,"previousSibling")},nextUntil:function(e,t,n){return T(e,"nextSibling",n)},prevUntil:function(e,t,n){return T(e,"previousSibling",n)},siblings:function(e){return _((e.parentNode||{}).firstChild,e)},children:function(e){return _(e.firstChild)},contents:function(e){return I(e,"iframe")?e.contentDocument:(I(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var o=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(o=w.filter(r,o)),this.length>1&&(F[e]||w.uniqueSort(o),P.test(e)&&o.reverse()),this.pushStack(o)}});var L=/[^\x20\t\r\n\f]+/g;function N(e){return e}function $(e){throw e}function j(e,t,n,r){var o;try{e&&m(o=e.promise)?o.call(e).done(t).fail(n):e&&m(o=e.then)?o.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.Callbacks=function(e){e="string"==typeof e?function(e){var t={};return w.each(e.match(L)||[],function(e,n){t[n]=!0}),t}(e):w.extend({},e);var t,n,r,o,a=[],i=[],s=-1,l=function(){for(o=o||e.once,r=t=!0;i.length;s=-1)for(n=i.shift();++s-1;)a.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,a)>-1:a.length>0},empty:function(){return a&&(a=[]),this},disable:function(){return o=i=[],a=n="",this},disabled:function(){return!a},lock:function(){return o=i=[],n||t||(a=n=""),this},locked:function(){return!!o},fireWith:function(e,n){return o||(n=[e,(n=n||[]).slice?n.slice():n],i.push(n),t||l()),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",o={state:function(){return r},always:function(){return a.done(arguments).fail(arguments),this},catch:function(e){return o.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var o=m(e[r[4]])&&e[r[4]];a[r[1]](function(){var e=o&&o.apply(this,arguments);e&&m(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,o?[e]:arguments)})}),e=null}).promise()},then:function(t,r,o){var a=0;function i(t,n,r,o){return function(){var s=this,l=arguments,c=function(){var e,c;if(!(t=a&&(r!==$&&(s=void 0,l=[e]),n.rejectWith(s,l))}};t?u():(w.Deferred.getStackHook&&(u.stackTrace=w.Deferred.getStackHook()),e.setTimeout(u))}}return w.Deferred(function(e){n[0][3].add(i(0,e,m(o)?o:N,e.notifyWith)),n[1][3].add(i(0,e,m(t)?t:N)),n[2][3].add(i(0,e,m(r)?r:$))}).promise()},promise:function(e){return null!=e?w.extend(e,o):o}},a={};return w.each(n,function(e,t){var i=t[2],s=t[5];o[t[1]]=i.add,s&&i.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),i.add(t[3].fire),a[t[0]]=function(){return a[t[0]+"With"](this===a?void 0:this,arguments),this},a[t[0]+"With"]=i.fireWith}),o.promise(a),t&&t.call(a,a),a},when:function(e){var t=arguments.length,n=t,r=Array(n),o=a.call(arguments),i=w.Deferred(),s=function(e){return function(n){r[e]=this,o[e]=arguments.length>1?a.call(arguments):n,--t||i.resolveWith(r,o)}};if(t<=1&&(j(e,i.done(s(n)).resolve,i.reject,!t),"pending"===i.state()||m(o[n]&&o[n].then)))return i.then();for(;n--;)j(o[n],s(n),i.reject);return i.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var M=w.Deferred();function H(){r.removeEventListener("DOMContentLoaded",H),e.removeEventListener("load",H),w.ready()}w.fn.ready=function(e){return M.then(e).catch(function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||M.resolveWith(r,[w]))}}),w.ready.then=M.then,"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",H),e.addEventListener("load",H));var q=function(e,t,n,r,o,a,i){var s=0,l=e.length,c=null==n;if("object"===b(n))for(s in o=!0,n)q(e,t,s,n[s],!0,a,i);else if(void 0!==r&&(o=!0,m(r)||(i=!0),c&&(i?(t.call(e,r),t=null):(c=t,t=function(e,t,n){return c.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=G.get(e,t),n&&(!r||Array.isArray(n)?r=G.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,o=n.shift(),a=w._queueHooks(e,t);"inprogress"===o&&(o=n.shift(),r--),o&&("fx"===t&&n.unshift("inprogress"),delete a.stop,o.call(e,function(){w.dequeue(e,t)},a)),!r&&a&&a.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return G.get(e,n)||G.access(e,n,{empty:w.Callbacks("once memory").add(function(){G.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,de=/^$|^module$|\/(?:java|ecma)script/i,fe={option:[1,""],thead:[1,"
","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function pe(e,t){var n;return n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&I(e,t)?w.merge([e],n):n}function he(e,t){for(var n=0,r=e.length;n-1)o&&o.push(a);else if(c=w.contains(a.ownerDocument,a),i=pe(d.appendChild(a),"script"),c&&he(i),n)for(u=0;a=i[u++];)de.test(a.type||"")&&n.push(a);return d}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var ve=r.documentElement,ye=/^key/,be=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,we=/^([^.]*)(?:\.(.+)|)/;function xe(){return!0}function Ce(){return!1}function Se(){try{return r.activeElement}catch(e){}}function Te(e,t,n,r,o,a){var i,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Te(e,s,n,r,t[s],a);return e}if(null==r&&null==o?(o=n,r=n=void 0):null==o&&("string"==typeof n?(o=r,r=void 0):(o=r,r=n,n=void 0)),!1===o)o=Ce;else if(!o)return e;return 1===a&&(i=o,(o=function(e){return w().off(e),i.apply(this,arguments)}).guid=i.guid||(i.guid=w.guid++)),e.each(function(){w.event.add(this,t,o,r,n)})}w.event={global:{},add:function(e,t,n,r,o){var a,i,s,l,c,u,d,f,p,h,m,g=G.get(e);if(g)for(n.handler&&(n=(a=n).handler,o=a.selector),o&&w.find.matchesSelector(ve,o),n.guid||(n.guid=w.guid++),(l=g.events)||(l=g.events={}),(i=g.handle)||(i=g.handle=function(t){return void 0!==w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),c=(t=(t||"").match(L)||[""]).length;c--;)p=m=(s=we.exec(t[c])||[])[1],h=(s[2]||"").split(".").sort(),p&&(d=w.event.special[p]||{},p=(o?d.delegateType:d.bindType)||p,d=w.event.special[p]||{},u=w.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:o,needsContext:o&&w.expr.match.needsContext.test(o),namespace:h.join(".")},a),(f=l[p])||((f=l[p]=[]).delegateCount=0,d.setup&&!1!==d.setup.call(e,r,h,i)||e.addEventListener&&e.addEventListener(p,i)),d.add&&(d.add.call(e,u),u.handler.guid||(u.handler.guid=n.guid)),o?f.splice(f.delegateCount++,0,u):f.push(u),w.event.global[p]=!0)},remove:function(e,t,n,r,o){var a,i,s,l,c,u,d,f,p,h,m,g=G.hasData(e)&&G.get(e);if(g&&(l=g.events)){for(c=(t=(t||"").match(L)||[""]).length;c--;)if(p=m=(s=we.exec(t[c])||[])[1],h=(s[2]||"").split(".").sort(),p){for(d=w.event.special[p]||{},f=l[p=(r?d.delegateType:d.bindType)||p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=a=f.length;a--;)u=f[a],!o&&m!==u.origType||n&&n.guid!==u.guid||s&&!s.test(u.namespace)||r&&r!==u.selector&&("**"!==r||!u.selector)||(f.splice(a,1),u.selector&&f.delegateCount--,d.remove&&d.remove.call(e,u));i&&!f.length&&(d.teardown&&!1!==d.teardown.call(e,h,g.handle)||w.removeEvent(e,p,g.handle),delete l[p])}else for(p in l)w.event.remove(e,p+t[c],n,r,!0);w.isEmptyObject(l)&&G.remove(e,"handle events")}},dispatch:function(e){var t,n,r,o,a,i,s=w.event.fix(e),l=new Array(arguments.length),c=(G.get(this,"events")||{})[s.type]||[],u=w.event.special[s.type]||{};for(l[0]=s,t=1;t=1))for(;c!==this;c=c.parentNode||this)if(1===c.nodeType&&("click"!==e.type||!0!==c.disabled)){for(a=[],i={},n=0;n-1:w.find(o,this,null,[c]).length),i[o]&&a.push(r);a.length&&s.push({elem:c,handlers:a})}return c=this,l\x20\t\r\n\f]*)[^>]*)\/>/gi,De=/\s*$/g;function Ae(e,t){return I(e,"table")&&I(11!==t.nodeType?t:t.firstChild,"tr")&&w(e).children("tbody")[0]||e}function Oe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Ee(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,o,a,i,s,l,c;if(1===t.nodeType){if(G.hasData(e)&&(a=G.access(e),i=G.set(t,a),c=a.events))for(o in delete i.handle,i.events={},c)for(n=0,r=c[o].length;n1&&"string"==typeof g&&!h.checkClone&&Ie.test(g))return e.each(function(o){var a=e.eq(o);v&&(t[0]=g.call(this,o,a.html())),Re(a,t,n,r)});if(f&&(a=(o=ge(t,e[0].ownerDocument,!1,e,r)).firstChild,1===o.childNodes.length&&(o=a),a||r)){for(l=(s=w.map(pe(o,"script"),Oe)).length;d")},clone:function(e,t,n){var r,o,a,i,s=e.cloneNode(!0),l=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(i=pe(s),r=0,o=(a=pe(e)).length;r0&&he(i,!l&&pe(e,"script")),s},cleanData:function(e){for(var t,n,r,o=w.event.special,a=0;void 0!==(n=e[a]);a++)if(X(n)){if(t=n[G.expando]){if(t.events)for(r in t.events)o[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[G.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Le(this,e,!0)},remove:function(e){return Le(this,e)},text:function(e){return q(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Ae(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Ae(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(pe(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return q(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!De.test(e)&&!fe[(ue.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(l+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-a-l-s-.5))),l}function Ke(e,t,n){var r=$e(e),o=Be(e,t,r),a="border-box"===w.css(e,"boxSizing",!1,r),i=a;if(Ne.test(o)){if(!n)return o;o="auto"}return i=i&&(h.boxSizingReliable()||o===e.style[t]),("auto"===o||!parseFloat(o)&&"inline"===w.css(e,"display",!1,r))&&(o=e["offset"+t[0].toUpperCase()+t.slice(1)],i=!0),(o=parseFloat(o)||0)+Ge(e,t,n||(a?"border":"content"),i,r,o)+"px"}function Qe(e,t,n,r,o){return new Qe.prototype.init(e,t,n,r,o)}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Be(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,i,s=V(t),l=qe.test(t),c=e.style;if(l||(t=Xe(s)),i=w.cssHooks[t]||w.cssHooks[s],void 0===n)return i&&"get"in i&&void 0!==(o=i.get(e,!1,r))?o:c[t];"string"==(a=typeof n)&&(o=te.exec(n))&&o[1]&&(n=ae(e,t,o),a="number"),null!=n&&n==n&&("number"===a&&(n+=o&&o[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(c[t]="inherit"),i&&"set"in i&&void 0===(n=i.set(e,n,r))||(l?c.setProperty(t,n):c[t]=n))}},css:function(e,t,n,r){var o,a,i,s=V(t);return qe.test(t)||(t=Xe(s)),(i=w.cssHooks[t]||w.cssHooks[s])&&"get"in i&&(o=i.get(e,!0,n)),void 0===o&&(o=Be(e,t,r)),"normal"===o&&t in We&&(o=We[t]),""===n||n?(a=parseFloat(o),!0===n||isFinite(a)?a||0:o):o}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!He.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?Ke(e,t,r):oe(e,Ue,function(){return Ke(e,t,r)})},set:function(e,n,r){var o,a=$e(e),i="border-box"===w.css(e,"boxSizing",!1,a),s=r&&Ge(e,t,r,i,a);return i&&h.scrollboxSize()===a.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(a[t])-Ge(e,t,"border",!1,a)-.5)),s&&(o=te.exec(n))&&"px"!==(o[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ye(0,n,s)}}}),w.cssHooks.marginLeft=Me(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Be(e,"marginLeft"))||e.getBoundingClientRect().left-oe(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,o={},a="string"==typeof n?n.split(" "):[n];r<4;r++)o[e+ne[r]+t]=a[r]||a[r-2]||a[0];return o}},"margin"!==e&&(w.cssHooks[e+t].set=Ye)}),w.fn.extend({css:function(e,t){return q(this,function(e,t,n){var r,o,a={},i=0;if(Array.isArray(t)){for(r=$e(e),o=t.length;i1)}}),w.Tween=Qe,Qe.prototype={constructor:Qe,init:function(e,t,n,r,o,a){this.elem=e,this.prop=n,this.easing=o||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=a||(w.cssNumber[n]?"":"px")},cur:function(){var e=Qe.propHooks[this.prop];return e&&e.get?e.get(this):Qe.propHooks._default.get(this)},run:function(e){var t,n=Qe.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Qe.propHooks._default.set(this),this}},Qe.prototype.init.prototype=Qe.prototype,Qe.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},Qe.propHooks.scrollTop=Qe.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=Qe.prototype.init,w.fx.step={};var Ze,Je,et=/^(?:toggle|show|hide)$/,tt=/queueHooks$/;function nt(){Je&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(nt):e.setTimeout(nt,w.fx.interval),w.fx.tick())}function rt(){return e.setTimeout(function(){Ze=void 0}),Ze=Date.now()}function ot(e,t){var n,r=0,o={height:e};for(t=t?1:0;r<4;r+=2-t)o["margin"+(n=ne[r])]=o["padding"+n]=e;return t&&(o.opacity=o.width=e),o}function at(e,t,n){for(var r,o=(it.tweeners[t]||[]).concat(it.tweeners["*"]),a=0,i=o.length;a1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,o,a=e.nodeType;if(3!==a&&8!==a&&2!==a)return void 0===e.getAttribute?w.prop(e,t,n):(1===a&&w.isXMLDoc(e)||(o=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?st:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):o&&"set"in o&&void 0!==(r=o.set(e,n,t))?r:(e.setAttribute(t,n+""),n):o&&"get"in o&&null!==(r=o.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&I(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,o=t&&t.match(L);if(o&&1===e.nodeType)for(;n=o[r++];)e.removeAttribute(n)}}),st={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=lt[t]||w.find.attr;lt[t]=function(e,t,r){var o,a,i=t.toLowerCase();return r||(a=lt[i],lt[i]=o,o=null!=n(e,t,r)?i:null,lt[i]=a),o}});var ct=/^(?:input|select|textarea|button)$/i,ut=/^(?:a|area)$/i;function dt(e){return(e.match(L)||[]).join(" ")}function ft(e){return e.getAttribute&&e.getAttribute("class")||""}function pt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(L)||[]}w.fn.extend({prop:function(e,t){return q(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,o,a=e.nodeType;if(3!==a&&8!==a&&2!==a)return 1===a&&w.isXMLDoc(e)||(t=w.propFix[t]||t,o=w.propHooks[t]),void 0!==n?o&&"set"in o&&void 0!==(r=o.set(e,n,t))?r:e[t]=n:o&&"get"in o&&null!==(r=o.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):ct.test(e.nodeName)||ut.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this}),w.fn.extend({addClass:function(e){var t,n,r,o,a,i,s,l=0;if(m(e))return this.each(function(t){w(this).addClass(e.call(this,t,ft(this)))});if((t=pt(e)).length)for(;n=this[l++];)if(o=ft(n),r=1===n.nodeType&&" "+dt(o)+" "){for(i=0;a=t[i++];)r.indexOf(" "+a+" ")<0&&(r+=a+" ");o!==(s=dt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,o,a,i,s,l=0;if(m(e))return this.each(function(t){w(this).removeClass(e.call(this,t,ft(this)))});if(!arguments.length)return this.attr("class","");if((t=pt(e)).length)for(;n=this[l++];)if(o=ft(n),r=1===n.nodeType&&" "+dt(o)+" "){for(i=0;a=t[i++];)for(;r.indexOf(" "+a+" ")>-1;)r=r.replace(" "+a+" "," ");o!==(s=dt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):m(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,ft(this),t),t)}):this.each(function(){var t,o,a,i;if(r)for(o=0,a=w(this),i=pt(e);t=i[o++];)a.hasClass(t)?a.removeClass(t):a.addClass(t);else void 0!==e&&"boolean"!==n||((t=ft(this))&&G.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":G.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;for(t=" "+e+" ";n=this[r++];)if(1===n.nodeType&&(" "+dt(ft(n))+" ").indexOf(t)>-1)return!0;return!1}});var ht=/\r/g;w.fn.extend({val:function(e){var t,n,r,o=this[0];return arguments.length?(r=m(e),this.each(function(n){var o;1===this.nodeType&&(null==(o=r?e.call(this,n,w(this).val()):e)?o="":"number"==typeof o?o+="":Array.isArray(o)&&(o=w.map(o,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,o,"value")||(this.value=o))})):o?(t=w.valHooks[o.type]||w.valHooks[o.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(o,"value"))?n:"string"==typeof(n=o.value)?n.replace(ht,""):null==n?"":n:void 0}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:dt(w.text(e))}},select:{get:function(e){var t,n,r,o=e.options,a=e.selectedIndex,i="select-one"===e.type,s=i?null:[],l=i?a+1:o.length;for(r=a<0?l:i?a:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),a}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var mt=/^(?:focusinfocus|focusoutblur)$/,gt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,o,a){var i,s,l,c,u,f,p,h,v=[o||r],y=d.call(t,"type")?t.type:t,b=d.call(t,"namespace")?t.namespace.split("."):[];if(s=h=l=o=o||r,3!==o.nodeType&&8!==o.nodeType&&!mt.test(y+w.event.triggered)&&(y.indexOf(".")>-1&&(y=(b=y.split(".")).shift(),b.sort()),u=y.indexOf(":")<0&&"on"+y,(t=t[w.expando]?t:new w.Event(y,"object"==typeof t&&t)).isTrigger=a?2:3,t.namespace=b.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=o),n=null==n?[t]:w.makeArray(n,[t]),p=w.event.special[y]||{},a||!p.trigger||!1!==p.trigger.apply(o,n))){if(!a&&!p.noBubble&&!g(o)){for(c=p.delegateType||y,mt.test(c+y)||(s=s.parentNode);s;s=s.parentNode)v.push(s),l=s;l===(o.ownerDocument||r)&&v.push(l.defaultView||l.parentWindow||e)}for(i=0;(s=v[i++])&&!t.isPropagationStopped();)h=s,t.type=i>1?c:p.bindType||y,(f=(G.get(s,"events")||{})[t.type]&&G.get(s,"handle"))&&f.apply(s,n),(f=u&&s[u])&&f.apply&&X(s)&&(t.result=f.apply(s,n),!1===t.result&&t.preventDefault());return t.type=y,a||t.isDefaultPrevented()||p._default&&!1!==p._default.apply(v.pop(),n)||!X(o)||u&&m(o[y])&&!g(o)&&((l=o[u])&&(o[u]=null),w.event.triggered=y,t.isPropagationStopped()&&h.addEventListener(y,gt),o[y](),t.isPropagationStopped()&&h.removeEventListener(y,gt),w.event.triggered=void 0,l&&(o[u]=l)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,o=G.access(r,t);o||r.addEventListener(e,n,!0),G.access(r,t,(o||0)+1)},teardown:function(){var r=this.ownerDocument||this,o=G.access(r,t)-1;o?G.access(r,t,o):(r.removeEventListener(e,n,!0),G.remove(r,t))}}});var vt=e.location,yt=Date.now(),bt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var wt=/\[\]$/,xt=/\r?\n/g,Ct=/^(?:submit|button|image|reset|file)$/i,St=/^(?:input|select|textarea|keygen)/i;function Tt(e,t,n,r){var o;if(Array.isArray(t))w.each(t,function(t,o){n||wt.test(e)?r(e,o):Tt(e+"["+("object"==typeof o&&null!=o?t:"")+"]",o,n,r)});else if(n||"object"!==b(t))r(e,t);else for(o in t)Tt(e+"["+o+"]",t[o],n,r)}w.param=function(e,t){var n,r=[],o=function(e,t){var n=m(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){o(this.name,this.value)});else for(n in e)Tt(n,e[n],t,o);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&St.test(this.nodeName)&&!Ct.test(e)&&(this.checked||!ce.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(xt,"\r\n")}}):{name:t.name,value:n.replace(xt,"\r\n")}}).get()}});var _t=/%20/g,Dt=/#.*$/,It=/([?&])_=[^&]*/,kt=/^(.*?):[ \t]*([^\r\n]*)$/gm,At=/^(?:GET|HEAD)$/,Ot=/^\/\//,Et={},Pt={},Ft="*/".concat("*"),Rt=r.createElement("a");function Lt(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,o=0,a=t.toLowerCase().match(L)||[];if(m(n))for(;r=a[o++];)"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function Nt(e,t,n,r){var o={},a=e===Pt;function i(s){var l;return o[s]=!0,w.each(e[s]||[],function(e,s){var c=s(t,n,r);return"string"!=typeof c||a||o[c]?a?!(l=c):void 0:(t.dataTypes.unshift(c),i(c),!1)}),l}return i(t.dataTypes[0])||!o["*"]&&i("*")}function $t(e,t){var n,r,o=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((o[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}Rt.href=vt.href,w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:vt.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(vt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Ft,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?$t($t(e,w.ajaxSettings),t):$t(w.ajaxSettings,e)},ajaxPrefilter:Lt(Et),ajaxTransport:Lt(Pt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var o,a,i,s,l,c,u,d,f,p,h=w.ajaxSetup({},n),m=h.context||h,g=h.context&&(m.nodeType||m.jquery)?w(m):w.event,v=w.Deferred(),y=w.Callbacks("once memory"),b=h.statusCode||{},x={},C={},S="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(u){if(!s)for(s={};t=kt.exec(i);)s[t[1].toLowerCase()]=t[2];t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return u?i:null},setRequestHeader:function(e,t){return null==u&&(e=C[e.toLowerCase()]=C[e.toLowerCase()]||e,x[e]=t),this},overrideMimeType:function(e){return null==u&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(u)T.always(e[T.status]);else for(t in e)b[t]=[b[t],e[t]];return this},abort:function(e){var t=e||S;return o&&o.abort(t),_(0,t),this}};if(v.promise(T),h.url=((t||h.url||vt.href)+"").replace(Ot,vt.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(L)||[""],null==h.crossDomain){c=r.createElement("a");try{c.href=h.url,c.href=c.href,h.crossDomain=Rt.protocol+"//"+Rt.host!=c.protocol+"//"+c.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),Nt(Et,h,n,T),u)return T;for(f in(d=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!At.test(h.type),a=h.url.replace(Dt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(_t,"+")):(p=h.url.slice(a.length),h.data&&(h.processData||"string"==typeof h.data)&&(a+=(bt.test(a)?"&":"?")+h.data,delete h.data),!1===h.cache&&(a=a.replace(It,"$1"),p=(bt.test(a)?"&":"?")+"_="+yt+++p),h.url=a+p),h.ifModified&&(w.lastModified[a]&&T.setRequestHeader("If-Modified-Since",w.lastModified[a]),w.etag[a]&&T.setRequestHeader("If-None-Match",w.etag[a])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&T.setRequestHeader("Content-Type",h.contentType),T.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+Ft+"; q=0.01":""):h.accepts["*"]),h.headers)T.setRequestHeader(f,h.headers[f]);if(h.beforeSend&&(!1===h.beforeSend.call(m,T,h)||u))return T.abort();if(S="abort",y.add(h.complete),T.done(h.success),T.fail(h.error),o=Nt(Pt,h,n,T)){if(T.readyState=1,d&&g.trigger("ajaxSend",[T,h]),u)return T;h.async&&h.timeout>0&&(l=e.setTimeout(function(){T.abort("timeout")},h.timeout));try{u=!1,o.send(x,_)}catch(e){if(u)throw e;_(-1,e)}}else _(-1,"No Transport");function _(t,n,r,s){var c,f,p,x,C,S=n;u||(u=!0,l&&e.clearTimeout(l),o=void 0,i=s||"",T.readyState=t>0?4:0,c=t>=200&&t<300||304===t,r&&(x=function(e,t,n){for(var r,o,a,i,s=e.contents,l=e.dataTypes;"*"===l[0];)l.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(o in s)if(s[o]&&s[o].test(r)){l.unshift(o);break}if(l[0]in n)a=l[0];else{for(o in n){if(!l[0]||e.converters[o+" "+l[0]]){a=o;break}i||(i=o)}a=a||i}if(a)return a!==l[0]&&l.unshift(a),n[a]}(h,T,r)),x=function(e,t,n,r){var o,a,i,s,l,c={},u=e.dataTypes.slice();if(u[1])for(i in e.converters)c[i.toLowerCase()]=e.converters[i];for(a=u.shift();a;)if(e.responseFields[a]&&(n[e.responseFields[a]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=a,a=u.shift())if("*"===a)a=l;else if("*"!==l&&l!==a){if(!(i=c[l+" "+a]||c["* "+a]))for(o in c)if((s=o.split(" "))[1]===a&&(i=c[l+" "+s[0]]||c["* "+s[0]])){!0===i?i=c[o]:!0!==c[o]&&(a=s[0],u.unshift(s[1]));break}if(!0!==i)if(i&&e.throws)t=i(t);else try{t=i(t)}catch(e){return{state:"parsererror",error:i?e:"No conversion from "+l+" to "+a}}}return{state:"success",data:t}}(h,x,T,c),c?(h.ifModified&&((C=T.getResponseHeader("Last-Modified"))&&(w.lastModified[a]=C),(C=T.getResponseHeader("etag"))&&(w.etag[a]=C)),204===t||"HEAD"===h.type?S="nocontent":304===t?S="notmodified":(S=x.state,f=x.data,c=!(p=x.error))):(p=S,!t&&S||(S="error",t<0&&(t=0))),T.status=t,T.statusText=(n||S)+"",c?v.resolveWith(m,[f,S,T]):v.rejectWith(m,[T,S,p]),T.statusCode(b),b=void 0,d&&g.trigger(c?"ajaxSuccess":"ajaxError",[T,h,c?f:p]),y.fireWith(m,[T,S]),d&&(g.trigger("ajaxComplete",[T,h]),--w.active||w.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,o){return m(n)&&(o=o||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:o,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,throws:!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(m(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return m(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=m(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var jt={0:200,1223:204},Bt=w.ajaxSettings.xhr();h.cors=!!Bt&&"withCredentials"in Bt,h.ajax=Bt=!!Bt,w.ajaxTransport(function(t){var n,r;if(h.cors||Bt&&!t.crossDomain)return{send:function(o,a){var i,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(i in t.xhrFields)s[i]=t.xhrFields[i];for(i in t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||o["X-Requested-With"]||(o["X-Requested-With"]="XMLHttpRequest"),o)s.setRequestHeader(i,o[i]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?a(0,"error"):a(s.status,s.statusText):a(jt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){var t,n;if(e.crossDomain)return{send:function(o,a){t=w("