From fc2e0ffe588132031cfb02058b5c04975a43f557 Mon Sep 17 00:00:00 2001 From: Exodus4D Date: Sun, 16 Jul 2017 14:16:15 +0200 Subject: [PATCH] - fixed "online" status for characters, closed #507 --- app/main/controller/admin.php | 21 ++++-- app/main/controller/ccp/sso.php | 19 +++--- app/main/controller/controller.php | 95 ++++++++++++++-------------- app/main/lib/util.php | 33 +++++++++- app/main/model/charactermodel.php | 33 +++++++--- js/app/notification.js | 20 +++--- js/app/page.js | 2 +- js/app/util.js | 23 +++++-- public/js/v1.2.4/app/notification.js | 20 +++--- public/js/v1.2.4/app/page.js | 2 +- public/js/v1.2.4/app/util.js | 23 +++++-- 11 files changed, 182 insertions(+), 109 deletions(-) diff --git a/app/main/controller/admin.php b/app/main/controller/admin.php index 3a89188f..378bfbd1 100644 --- a/app/main/controller/admin.php +++ b/app/main/controller/admin.php @@ -17,6 +17,7 @@ use lib\Config; class Admin extends Controller{ const ERROR_SSO_CHARACTER_EXISTS = 'No character found. Please login first.'; + const ERROR_SSO_CHARACTER_SCOPES = 'Additional ESI scopes are required for "%s". Use the SSO button below.'; const ERROR_SSO_CHARACTER_ROLES = 'Insufficient in-game roles. "%s" requires at least one of these corp roles: %s.'; const LOG_TEXT_KICK_BAN = '%s "%s" from corporation "%s", by "%s"'; @@ -86,13 +87,19 @@ class Admin extends Controller{ if($character->role != 'MEMBER'){ // current character is admin $adminCharacter = $character; + }elseif( !$character->hasAdminScopes() ){ + $f3->set(Sso::SESSION_KEY_SSO_ERROR, + sprintf( + self::ERROR_SSO_CHARACTER_SCOPES, + $character->name + )); }else{ $f3->set(Sso::SESSION_KEY_SSO_ERROR, sprintf( self::ERROR_SSO_CHARACTER_ROLES, $character->name, - implode(', ', CorporationModel::ADMIN_ROLES - ))); + implode(', ', CorporationModel::ADMIN_ROLES) + )); } }else{ $f3->set(Sso::SESSION_KEY_SSO_ERROR, self::ERROR_SSO_CHARACTER_EXISTS); @@ -107,9 +114,9 @@ class Admin extends Controller{ * @param CharacterModel $character */ protected function setCharacterRole(CharacterModel $character){ - $character->virtual('role', function($character){ + $character->virtual('role', function ($character){ // default role based on roleId (auto-detected) - if( ($role = array_search ($character->roleId, CharacterModel::ROLES)) === false ){ + if(($role = array_search($character->roleId, CharacterModel::ROLES)) === false){ $role = 'MEMBER'; } @@ -119,9 +126,9 @@ class Admin extends Controller{ */ if($this->getF3()->exists('PATHFINDER.ADMIN.CHARACTER', $globalAdminData)){ foreach((array)$globalAdminData as $adminData){ - if($adminData['ID'] === $character->_id){ - if( CharacterModel::ROLES[$adminData['ROLE']] ){ - $role = $adminData['ROLE']; + if($adminData[ 'ID' ] === $character->_id){ + if(CharacterModel::ROLES[ $adminData[ 'ROLE' ] ]){ + $role = $adminData[ 'ROLE' ]; } break; } diff --git a/app/main/controller/ccp/sso.php b/app/main/controller/ccp/sso.php index d8d70ff8..6e7050d8 100644 --- a/app/main/controller/ccp/sso.php +++ b/app/main/controller/ccp/sso.php @@ -55,7 +55,7 @@ class Sso extends Api\User{ public function requestAdminAuthorization($f3){ $f3->set(self::SESSION_KEY_SSO_FROM, 'admin'); - $scopes = $this->getScopesByAuthType('admin'); + $scopes = self::getScopesByAuthType('admin'); $this->rerouteAuthorization($f3, $scopes, 'admin'); } @@ -105,7 +105,7 @@ class Sso extends Api\User{ if($loginCheck){ // set "login" cookie - $this->setLoginCookie($character, $this->generateHashFromScopes($this->getScopesByAuthType()) ); + $this->setLoginCookie($character); // -> pass current character data to target page $f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_ID, $character->_id); @@ -122,7 +122,7 @@ class Sso extends Api\User{ } // redirect to CCP SSO ---------------------------------------------------------------------- - $scopes = $this->getScopesByAuthType(); + $scopes = self::getScopesByAuthType(); $this->rerouteAuthorization($f3, $scopes); } @@ -207,9 +207,10 @@ class Sso extends Api\User{ if( isset($characterData->character) ){ // add "ownerHash" and SSO tokens - $characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash; - $characterData->character['crestAccessToken'] = $accessData->accessToken; - $characterData->character['crestRefreshToken'] = $accessData->refreshToken; + $characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash; + $characterData->character['crestAccessToken'] = $accessData->accessToken; + $characterData->character['crestRefreshToken'] = $accessData->refreshToken; + $characterData->character['esiScopes'] = Lib\Util::convertScopesString($verificationCharacterData->Scopes); // add/update static character data $characterModel = $this->updateCharacter($characterData); @@ -255,7 +256,7 @@ class Sso extends Api\User{ if($loginCheck){ // set "login" cookie - $this->setLoginCookie($characterModel, $this->generateHashFromScopes( explode(' ', $verificationCharacterData->Scopes) )); + $this->setLoginCookie($characterModel); // -> pass current character data to target page $f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_ID, $characterModel->_id); @@ -569,7 +570,9 @@ class Sso extends Api\User{ */ $characterModel = Model\BasicModel::getNew('CharacterModel'); $characterModel->getById((int)$characterData->character['id'], 0); - $characterModel->copyfrom($characterData->character, ['id', 'name', 'ownerHash', 'crestAccessToken', 'crestRefreshToken', 'securityStatus']); + $characterModel->copyfrom($characterData->character, [ + 'id', 'name', 'ownerHash', 'crestAccessToken', 'crestRefreshToken', 'esiScopes', 'securityStatus' + ]); $characterModel->corporationId = $characterData->corporation; $characterModel->allianceId = $characterData->alliance; $characterModel = $characterModel->save(); diff --git a/app/main/controller/controller.php b/app/main/controller/controller.php index 3f9d4674..44d636aa 100644 --- a/app/main/controller/controller.php +++ b/app/main/controller/controller.php @@ -10,6 +10,7 @@ namespace Controller; use Controller\Api as Api; use lib\Config; use lib\Socket; +use Lib\Util; use Model; use DB; @@ -187,9 +188,8 @@ class Controller { * set/update logged in cookie by character model * -> store validation data in DB * @param Model\CharacterModel $character - * @param string $scopeHash */ - protected function setLoginCookie(Model\CharacterModel $character, $scopeHash = ''){ + protected function setLoginCookie(Model\CharacterModel $character){ if( $this->getCookieState() ){ $expireSeconds = (int) $this->getF3()->get('PATHFINDER.LOGIN.COOKIE_EXPIRE'); @@ -221,8 +221,7 @@ class Controller { 'characterId' => $character, 'selector' => $selector, 'token' => $token, - 'expires' => $expireTime->format('Y-m-d H:i:s'), - 'scopeHash' => $scopeHash + 'expires' => $expireTime->format('Y-m-d H:i:s') ]; $authenticationModel = $character->rel('characterAuthentications'); @@ -276,10 +275,6 @@ class Controller { // "expire data" and "validate token" if( !$characterAuth->dry() ){ if( - ( - $characterAuth->scopeHash === $this->generateHashFromScopes($this->getScopesByAuthType()) || - $characterAuth->scopeHash === $this->generateHashFromScopes($this->getScopesByAuthType('admin')) - ) && strtotime($characterAuth->expires) >= $currentTime->getTimestamp() && hash_equals($characterAuth->token, hash('sha256', $data[1])) ){ @@ -297,20 +292,32 @@ class Controller { $character = $characterAuth->rel('characterId'); $character->getById( $characterAuth->get('characterId', true) ); - // check if character still has user (is not the case of "ownerHash" changed - // check if character is still authorized to log in (e.g. corp/ally or config has changed - // -> do NOT remove cookie on failure. This can be a temporary problem (e.g. ESI is down,..) - if( $character->hasUserCharacter() ){ - $authStatus = $character->isAuthorized(); + // check ESI scopes + $scopeHash = Util::getHashFromScopes($character->esiScopes); - if( - $authStatus == 'OK' || - !$checkAuthorization - ){ - $character->virtual( 'authStatus', $authStatus); + if( + $scopeHash === Util::getHashFromScopes(self::getScopesByAuthType()) || + $scopeHash === Util::getHashFromScopes(self::getScopesByAuthType('admin')) + ){ + // check if character still has user (is not the case of "ownerHash" changed + // check if character is still authorized to log in (e.g. corp/ally or config has changed + // -> do NOT remove cookie on failure. This can be a temporary problem (e.g. ESI is down,..) + if( $character->hasUserCharacter() ){ + $authStatus = $character->isAuthorized(); + + if( + $authStatus == 'OK' || + !$checkAuthorization + ){ + $character->virtual( 'authStatus', $authStatus); + } + + $characters[$name] = $character; } - - $characters[$name] = $character; + }else{ + // outdated/invalid ESI scopes + $characterAuth->erase(); + $invalidCookie = true; } }else{ $invalidCookie = true; @@ -434,35 +441,6 @@ class Controller { return $user; } - /** - * get scope array by a "role" - * @param string $authType - * @return array - */ - protected function getScopesByAuthType($authType = ''){ - $scopes = (array)self::getEnvironmentData('CCP_ESI_SCOPES'); - - switch($authType){ - case 'admin': - $scopesAdmin = (array)self::getEnvironmentData('CCP_ESI_SCOPES_ADMIN'); - $scopes = array_merge($scopes, $scopesAdmin); - break; - } - sort($scopes, SORT_NUMERIC); - return $scopes; - } - - /** - * get hash from an array of ESI scopes - * @param array $scopes - * @return string - */ - protected function generateHashFromScopes($scopes){ - $scopes = (array)$scopes; - sort($scopes); - return md5(serialize( $scopes )); - } - /** * log out current character * @param \Base $f3 @@ -715,6 +693,25 @@ class Controller { return $controller; } + + /** + * get scope array by a "role" + * @param string $authType + * @return array + */ + static function getScopesByAuthType($authType = ''){ + $scopes = (array)self::getEnvironmentData('CCP_ESI_SCOPES'); + + switch($authType){ + case 'admin': + $scopesAdmin = (array)self::getEnvironmentData('CCP_ESI_SCOPES_ADMIN'); + $scopes = array_merge($scopes, $scopesAdmin); + break; + } + sort($scopes); + return $scopes; + } + /** * Helper function to return all headers because * getallheaders() is not available under nginx diff --git a/app/main/lib/util.php b/app/main/lib/util.php index 6994ba51..1001cf99 100644 --- a/app/main/lib/util.php +++ b/app/main/lib/util.php @@ -8,7 +8,6 @@ namespace Lib; - class Util { /** @@ -38,4 +37,36 @@ class Util { }, array_keys($arr)), $arr ); } + + /** + * convert a string with multiple scopes into an array + * @param string $scopes + * @return array|null + */ + static function convertScopesString($scopes){ + $scopes = array_filter( + array_map('strtolower', + (array)explode(' ', $scopes) + ) + ); + + if($scopes){ + sort($scopes); + }else{ + $scopes = null; + } + + return $scopes; + } + + /** + * get hash from an array of ESI scopes + * @param array $scopes + * @return string + */ + static function getHashFromScopes($scopes){ + $scopes = (array)$scopes; + sort($scopes); + return md5(serialize($scopes)); + } } \ No newline at end of file diff --git a/app/main/model/charactermodel.php b/app/main/model/charactermodel.php index 3a509861..88ceb308 100644 --- a/app/main/model/charactermodel.php +++ b/app/main/model/charactermodel.php @@ -11,6 +11,7 @@ namespace Model; use Controller\Ccp\Sso as Sso; use Controller\Api\User as User; use DB\SQL\Schema; +use Lib\Util; class CharacterModel extends BasicModel { @@ -90,6 +91,9 @@ class CharacterModel extends BasicModel { 'crestRefreshToken' => [ 'type' => Schema::DT_VARCHAR256 ], + 'esiScopes' => [ + 'type' => self::DT_JSON + ], 'corporationId' => [ 'type' => Schema::DT_INT, 'index' => true, @@ -611,12 +615,16 @@ class CharacterModel extends BasicModel { */ protected function requestRoles(){ $rolesData = []; - if( $accessToken = $this->getAccessToken() ){ - // check if corporation exists (should never fail) - if( $corporation = $this->getCorporation() ){ - $characterRolesData = $corporation->getCharactersRoles($accessToken); - if( !empty($characterRolesData[$this->_id]) ){ - $rolesData = $characterRolesData[$this->_id]; + + // check if character has accepted all admin scopes (one of them is required for "role" request) + if( $this->hasAdminScopes() ){ + if( $accessToken = $this->getAccessToken() ){ + // check if corporation exists (should never fail) + if( $corporation = $this->getCorporation() ){ + $characterRolesData = $corporation->getCharactersRoles($accessToken); + if( !empty($characterRolesData[$this->_id]) ){ + $rolesData = $characterRolesData[$this->_id]; + } } } } @@ -624,11 +632,19 @@ class CharacterModel extends BasicModel { return $rolesData; } + /** + * check whether this char has accepted all admin api scopes + * @return bool + */ + public function hasAdminScopes(){ + return empty( array_diff(Sso::getScopesByAuthType('admin'), $this->esiScopes) ); + } + /** * update character log (active system, ...) * -> API request for character log data * @param array $additionalOptions (optional) request options for cURL request - * @return $this + * @return CharacterModel */ public function updateLog($additionalOptions = []){ $deleteLog = false; @@ -806,8 +822,9 @@ class CharacterModel extends BasicModel { $characterData = $ssoController->getCharacterData($this->_id); if( !empty($characterData->character) ){ $characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash; + $characterData->character['esiScopes'] = Util::convertScopesString($verificationCharacterData->Scopes); - $this->copyfrom($characterData->character, ['ownerHash', 'securityStatus']); + $this->copyfrom($characterData->character, ['ownerHash', 'esiScopes', 'securityStatus']); $this->corporationId = $characterData->corporation; $this->allianceId = $characterData->alliance; $this->save(); diff --git a/js/app/notification.js b/js/app/notification.js index f5aa2fd5..1e4e0999 100644 --- a/js/app/notification.js +++ b/js/app/notification.js @@ -12,7 +12,7 @@ define([ 'use strict'; - var config = { + let config = { title: '', text: '', type: '', // 'info', 'success', error, 'warning' @@ -38,13 +38,13 @@ define([ }; // initial page title (cached) - var initialPageTitle = document.title; + let initialPageTitle = document.title; // global blink timeout cache - var blinkTimer; + let blinkTimer; // stack container for all notifications - var stack = { + let stack = { bottomRight: { stack: { dir1: 'up', @@ -76,7 +76,7 @@ define([ * @param customConfig * @param settings */ - var showNotify = function(customConfig, settings){ + let showNotify = function(customConfig, settings){ customConfig = $.extend(true, {}, config, customConfig ); @@ -140,13 +140,13 @@ define([ * change document.title and make the browsers tab blink * @param blinkTitle */ - var startTabBlink = function(blinkTitle){ - var initBlink = (function(blinkTitle){ + let startTabBlink = function(blinkTitle){ + let initBlink = (function(blinkTitle){ // count blinks if tab is currently active - var activeTabBlinkCount = 0; + let activeTabBlinkCount = 0; - var blink = function(){ + let blink = function(){ // number of "blinks" should be limited if tab is currently active if(window.isVisible){ activeTabBlinkCount++; @@ -173,7 +173,7 @@ define([ /** * stop blinking document.title */ - var stopTabBlink = function(){ + let stopTabBlink = function(){ if(blinkTimer){ clearInterval(blinkTimer); document.title = initialPageTitle; diff --git a/js/app/page.js b/js/app/page.js index aa72dc5b..1a7613af 100644 --- a/js/app/page.js +++ b/js/app/page.js @@ -1005,7 +1005,7 @@ define([ let initTabChangeObserver = function(){ // increase the timer if a user is inactive - let increaseTimer = 10000; + let increaseTimer = 5000; // timer keys let mapUpdateKey = 'UPDATE_SERVER_MAP'; diff --git a/js/app/util.js b/js/app/util.js index 83307c82..58f4045f 100644 --- a/js/app/util.js +++ b/js/app/util.js @@ -581,8 +581,9 @@ define([ e.preventDefault(); e.stopPropagation(); - let easeEffect = $(this).attr('data-easein'); - let popoverData = $(this).data('bs.popover'); + let button = $(this); + let easeEffect = button.attr('data-easein'); + let popoverData = button.data('bs.popover'); let popoverElement = null; let velocityOptions = { @@ -591,8 +592,16 @@ define([ if(popoverData === undefined){ + button.on('shown.bs.popover', function (e) { + let tmpPopupElement = $(this).data('bs.popover').tip(); + tmpPopupElement.find('.btn').on('click', function(e){ + // close popover + $('body').click(); + }); + }); + // init popover and add specific class to it (for styling) - $(this).popover({ + button.popover({ html: true, title: 'select character', trigger: 'manual', @@ -602,17 +611,17 @@ define([ animation: false }).data('bs.popover').tip().addClass('pf-popover'); - $(this).popover('show'); + button.popover('show'); - popoverElement = $(this).data('bs.popover').tip(); + popoverElement = button.data('bs.popover').tip(); popoverElement.velocity('transition.' + easeEffect, velocityOptions); popoverElement.initTooltips(); }else{ - popoverElement = $(this).data('bs.popover').tip(); + popoverElement = button.data('bs.popover').tip(); if(popoverElement.is(':visible')){ popoverElement.velocity('reverse'); }else{ - $(this).popover('show'); + button.popover('show'); popoverElement.initTooltips(); popoverElement.velocity('transition.' + easeEffect, velocityOptions); } diff --git a/public/js/v1.2.4/app/notification.js b/public/js/v1.2.4/app/notification.js index f5aa2fd5..1e4e0999 100644 --- a/public/js/v1.2.4/app/notification.js +++ b/public/js/v1.2.4/app/notification.js @@ -12,7 +12,7 @@ define([ 'use strict'; - var config = { + let config = { title: '', text: '', type: '', // 'info', 'success', error, 'warning' @@ -38,13 +38,13 @@ define([ }; // initial page title (cached) - var initialPageTitle = document.title; + let initialPageTitle = document.title; // global blink timeout cache - var blinkTimer; + let blinkTimer; // stack container for all notifications - var stack = { + let stack = { bottomRight: { stack: { dir1: 'up', @@ -76,7 +76,7 @@ define([ * @param customConfig * @param settings */ - var showNotify = function(customConfig, settings){ + let showNotify = function(customConfig, settings){ customConfig = $.extend(true, {}, config, customConfig ); @@ -140,13 +140,13 @@ define([ * change document.title and make the browsers tab blink * @param blinkTitle */ - var startTabBlink = function(blinkTitle){ - var initBlink = (function(blinkTitle){ + let startTabBlink = function(blinkTitle){ + let initBlink = (function(blinkTitle){ // count blinks if tab is currently active - var activeTabBlinkCount = 0; + let activeTabBlinkCount = 0; - var blink = function(){ + let blink = function(){ // number of "blinks" should be limited if tab is currently active if(window.isVisible){ activeTabBlinkCount++; @@ -173,7 +173,7 @@ define([ /** * stop blinking document.title */ - var stopTabBlink = function(){ + let stopTabBlink = function(){ if(blinkTimer){ clearInterval(blinkTimer); document.title = initialPageTitle; diff --git a/public/js/v1.2.4/app/page.js b/public/js/v1.2.4/app/page.js index aa72dc5b..1a7613af 100644 --- a/public/js/v1.2.4/app/page.js +++ b/public/js/v1.2.4/app/page.js @@ -1005,7 +1005,7 @@ define([ let initTabChangeObserver = function(){ // increase the timer if a user is inactive - let increaseTimer = 10000; + let increaseTimer = 5000; // timer keys let mapUpdateKey = 'UPDATE_SERVER_MAP'; diff --git a/public/js/v1.2.4/app/util.js b/public/js/v1.2.4/app/util.js index 83307c82..58f4045f 100644 --- a/public/js/v1.2.4/app/util.js +++ b/public/js/v1.2.4/app/util.js @@ -581,8 +581,9 @@ define([ e.preventDefault(); e.stopPropagation(); - let easeEffect = $(this).attr('data-easein'); - let popoverData = $(this).data('bs.popover'); + let button = $(this); + let easeEffect = button.attr('data-easein'); + let popoverData = button.data('bs.popover'); let popoverElement = null; let velocityOptions = { @@ -591,8 +592,16 @@ define([ if(popoverData === undefined){ + button.on('shown.bs.popover', function (e) { + let tmpPopupElement = $(this).data('bs.popover').tip(); + tmpPopupElement.find('.btn').on('click', function(e){ + // close popover + $('body').click(); + }); + }); + // init popover and add specific class to it (for styling) - $(this).popover({ + button.popover({ html: true, title: 'select character', trigger: 'manual', @@ -602,17 +611,17 @@ define([ animation: false }).data('bs.popover').tip().addClass('pf-popover'); - $(this).popover('show'); + button.popover('show'); - popoverElement = $(this).data('bs.popover').tip(); + popoverElement = button.data('bs.popover').tip(); popoverElement.velocity('transition.' + easeEffect, velocityOptions); popoverElement.initTooltips(); }else{ - popoverElement = $(this).data('bs.popover').tip(); + popoverElement = button.data('bs.popover').tip(); if(popoverElement.is(':visible')){ popoverElement.velocity('reverse'); }else{ - $(this).popover('show'); + button.popover('show'); popoverElement.initTooltips(); popoverElement.velocity('transition.' + easeEffect, velocityOptions); }