// Copyright 2020-2024 CesiumGS, Inc. and Contributors #include "CesiumIonSession.h" #include "CesiumEditor.h" #include "CesiumEditorSettings.h" #include "CesiumIonServer.h" #include "CesiumRuntimeSettings.h" #include "CesiumSourceControl.h" #include "CesiumUtility/Uri.h" #include "FileHelpers.h" #include "HAL/PlatformProcess.h" #include "Misc/App.h" using namespace CesiumAsync; using namespace CesiumIonClient; namespace { template void logResponseErrors(const Response& response) { if (!response.errorCode.empty() && !response.errorMessage.empty()) { UE_LOG( LogCesiumEditor, Error, TEXT("%s (Code %s)"), UTF8_TO_TCHAR(response.errorMessage.c_str()), UTF8_TO_TCHAR(response.errorCode.c_str())); } else if (!response.errorCode.empty()) { UE_LOG( LogCesiumEditor, Error, TEXT("Code %s"), UTF8_TO_TCHAR(response.errorCode.c_str())); } else if (!response.errorMessage.empty()) { UE_LOG( LogCesiumEditor, Error, TEXT("%s"), UTF8_TO_TCHAR(response.errorMessage.c_str())); } } void logResponseErrors(const std::exception& exception) { UE_LOG( LogCesiumEditor, Error, TEXT("Exception: %s"), UTF8_TO_TCHAR(exception.what())); } } // namespace CesiumIonSession::CesiumIonSession( CesiumAsync::AsyncSystem& asyncSystem, const std::shared_ptr& pAssetAccessor, TWeakObjectPtr pServer) : _asyncSystem(asyncSystem), _pAssetAccessor(pAssetAccessor), _pServer(pServer), _connection(std::nullopt), _profile(std::nullopt), _assets(std::nullopt), _tokens(std::nullopt), _defaults(std::nullopt), _appData(std::nullopt), _isConnecting(false), _isResuming(false), _isLoadingProfile(false), _isLoadingAssets(false), _isLoadingTokens(false), _isLoadingDefaults(false), _loadProfileQueued(false), _loadAssetsQueued(false), _loadTokensQueued(false), _loadDefaultsQueued(false), _authorizeUrl() {} bool CesiumIonSession::isAuthenticationRequired() const { return this->_appData.has_value() ? this->_appData->needsOauthAuthentication() : true; } void CesiumIonSession::connect() { if (!this->_pServer.IsValid() || this->isConnecting() || this->isConnected() || this->isResuming()) { return; } UCesiumIonServer* pServer = this->_pServer.Get(); this->_isConnecting = true; std::string ionServerUrl = TCHAR_TO_UTF8(*pServer->ServerUrl); Future> futureApiUrl = !pServer->ApiUrl.IsEmpty() ? this->_asyncSystem.createResolvedFuture>( TCHAR_TO_UTF8(*pServer->ApiUrl)) : Connection::getApiUrl( this->_asyncSystem, this->_pAssetAccessor, ionServerUrl); std::shared_ptr thiz = this->shared_from_this(); std::move(futureApiUrl) .thenInMainThread([ionServerUrl, thiz, pServer = this->_pServer]( std::optional&& ionApiUrl) { CesiumAsync::Promise promise = thiz->_asyncSystem.createPromise(); if (!pServer.IsValid()) { promise.reject( std::runtime_error("CesiumIonServer unexpectedly nullptr")); return promise.getFuture(); } if (!ionApiUrl) { promise.reject(std::runtime_error(fmt::format( "Failed to retrieve API URL from the config.json file at the " "specified Ion server URL: {}", ionServerUrl))); return promise.getFuture(); } if (pServer->ApiUrl.IsEmpty()) { pServer->ApiUrl = UTF8_TO_TCHAR(ionApiUrl->c_str()); pServer->Modify(); UEditorLoadingAndSavingUtils::SavePackages( {pServer->GetPackage()}, true); } // Make request to /appData to learn the server's authentication mode return thiz->ensureAppDataLoaded(); }) .thenInMainThread( [ionServerUrl, thiz, pServer = this->_pServer](bool loadedAppData) { if (!loadedAppData || !thiz->_appData.has_value()) { Promise promise = thiz->_asyncSystem.createPromise(); promise.reject(std::runtime_error( "Failed to obtain _appData, can't create connection")); return promise.getFuture(); } if (thiz->_appData->needsOauthAuthentication()) { int64_t clientID = pServer->OAuth2ApplicationID; return CesiumIonClient::Connection::authorize( thiz->_asyncSystem, thiz->_pAssetAccessor, "Cesium for Unreal", clientID, "/cesium-for-unreal/oauth2/callback", {"assets:list", "assets:read", "profile:read", "tokens:read", "tokens:write", "geocode"}, [thiz](const std::string& url) { thiz->_authorizeUrl = url; thiz->_redirectUrl = CesiumUtility::Uri::getQueryValue(url, "redirect_uri"); FPlatformProcess::LaunchURL( UTF8_TO_TCHAR(thiz->_authorizeUrl.c_str()), NULL, NULL); }, thiz->_appData.value(), std::string(TCHAR_TO_UTF8(*pServer->ApiUrl)), CesiumUtility::Uri::resolve(ionServerUrl, "oauth")); } return thiz->_asyncSystem .createResolvedFuture( CesiumIonClient::Connection( thiz->_asyncSystem, thiz->_pAssetAccessor, "", thiz->_appData.value(), std::string(TCHAR_TO_UTF8(*pServer->ApiUrl)))); }) .thenInMainThread([ionServerUrl, thiz, pServer = this->_pServer]( CesiumIonClient::Connection&& connection) { thiz->_isConnecting = false; thiz->_connection = std::move(connection); UCesiumEditorSettings* pSettings = GetMutableDefault(); pSettings->UserAccessTokenMap.Add( thiz->_pServer.Get(), UTF8_TO_TCHAR(thiz->_connection.value().getAccessToken().c_str())); pSettings->Save(); thiz->ConnectionUpdated.Broadcast(); thiz->startQueuedLoads(); }) .catchInMainThread( [ionServerUrl, thiz, pServer = this->_pServer](std::exception&& e) { UE_LOG( LogCesiumEditor, Error, TEXT("Error connecting: %s"), UTF8_TO_TCHAR(e.what())); thiz->_isConnecting = false; thiz->_connection = std::nullopt; thiz->ConnectionUpdated.Broadcast(); }); } void CesiumIonSession::resume() { if (!this->_pServer.IsValid() || this->isConnecting() || this->isConnected() || this->isResuming()) { return; } const UCesiumEditorSettings* pSettings = GetDefault(); const FString* pUserAccessToken = pSettings->UserAccessTokenMap.Find(this->_pServer.Get()); if (!pUserAccessToken || pUserAccessToken->IsEmpty()) { // No existing session to resume. return; } this->_isResuming = true; std::shared_ptr thiz = this->shared_from_this(); // Verify that the connection actually works. this->ensureAppDataLoaded() .thenInMainThread([thiz, pUserAccessToken](bool loadedAppData) { if (!loadedAppData || !thiz->_appData.has_value()) { Promise promise = thiz->_asyncSystem.createPromise(); promise.reject(std::runtime_error( "Failed to obtain _appData, can't resume connection")); return promise.getFuture(); } std::shared_ptr pConnection = std::make_shared( thiz->_asyncSystem, thiz->_pAssetAccessor, TCHAR_TO_UTF8(**pUserAccessToken), *thiz->_appData, TCHAR_TO_UTF8(*thiz->_pServer->ApiUrl)); return pConnection->me().thenInMainThread( [thiz, pConnection](Response&& response) { logResponseErrors(response); if (response.value.has_value()) { thiz->_connection = std::move(*pConnection); } thiz->_isResuming = false; thiz->ConnectionUpdated.Broadcast(); thiz->startQueuedLoads(); }); }) .catchInMainThread([thiz](std::exception&& e) { logResponseErrors(e); thiz->_isResuming = false; }); } void CesiumIonSession::disconnect() { this->_connection.reset(); this->_profile.reset(); this->_assets.reset(); this->_tokens.reset(); this->_defaults.reset(); this->_appData.reset(); UCesiumEditorSettings* pSettings = GetMutableDefault(); pSettings->UserAccessTokenMap.Remove(this->_pServer.Get()); pSettings->Save(); this->ConnectionUpdated.Broadcast(); this->ProfileUpdated.Broadcast(); this->AssetsUpdated.Broadcast(); this->TokensUpdated.Broadcast(); this->DefaultsUpdated.Broadcast(); } void CesiumIonSession::refreshProfile() { if (!this->_connection || this->_isLoadingProfile) { this->_loadProfileQueued = true; return; } this->_isLoadingProfile = true; this->_loadProfileQueued = false; std::shared_ptr thiz = this->shared_from_this(); this->_connection->me() .thenInMainThread([thiz](Response&& profile) { logResponseErrors(profile); thiz->_isLoadingProfile = false; thiz->_profile = std::move(profile.value); thiz->ProfileUpdated.Broadcast(); if (thiz->_loadProfileQueued) thiz->refreshProfile(); }) .catchInMainThread([thiz](std::exception&& e) { logResponseErrors(e); thiz->_isLoadingProfile = false; thiz->_profile = std::nullopt; thiz->ProfileUpdated.Broadcast(); if (thiz->_loadProfileQueued) thiz->refreshProfile(); }); } void CesiumIonSession::refreshAssets() { if (!this->_connection || this->_isLoadingAssets) { this->_loadAssetsQueued = true; return; } this->_isLoadingAssets = true; this->_loadAssetsQueued = false; std::shared_ptr thiz = this->shared_from_this(); this->_connection->assets() .thenInMainThread([thiz](Response&& assets) { logResponseErrors(assets); thiz->_isLoadingAssets = false; thiz->_assets = std::move(assets.value); thiz->AssetsUpdated.Broadcast(); if (thiz->_loadAssetsQueued) thiz->refreshAssets(); }) .catchInMainThread([thiz](std::exception&& e) { logResponseErrors(e); thiz->_isLoadingAssets = false; thiz->_assets = std::nullopt; thiz->AssetsUpdated.Broadcast(); if (thiz->_loadAssetsQueued) thiz->refreshAssets(); }); } void CesiumIonSession::refreshTokens() { if (!this->_connection || this->_isLoadingTokens) { this->_loadTokensQueued = true; return; } this->_isLoadingTokens = true; this->_loadTokensQueued = false; std::shared_ptr thiz = this->shared_from_this(); this->_connection->tokens() .thenInMainThread([thiz](Response&& tokens) { logResponseErrors(tokens); thiz->_isLoadingTokens = false; thiz->_tokens = tokens.value ? std::make_optional(std::move(tokens.value->items)) : std::nullopt; thiz->TokensUpdated.Broadcast(); if (thiz->_loadTokensQueued) thiz->refreshTokens(); }) .catchInMainThread([thiz](std::exception&& e) { logResponseErrors(e); thiz->_isLoadingTokens = false; thiz->_tokens = std::nullopt; thiz->TokensUpdated.Broadcast(); if (thiz->_loadTokensQueued) thiz->refreshTokens(); }); } void CesiumIonSession::refreshDefaults() { if (!this->_connection || this->_isLoadingDefaults) { this->_loadDefaultsQueued = true; return; } this->_isLoadingDefaults = true; this->_loadDefaultsQueued = false; std::shared_ptr thiz = this->shared_from_this(); this->_connection->defaults() .thenInMainThread([thiz](Response&& defaults) { logResponseErrors(defaults); thiz->_isLoadingDefaults = false; thiz->_defaults = std::move(defaults.value); thiz->DefaultsUpdated.Broadcast(); if (thiz->_loadDefaultsQueued) thiz->refreshDefaults(); }) .catchInMainThread([thiz](std::exception&& e) { logResponseErrors(e); thiz->_isLoadingDefaults = false; thiz->_defaults = std::nullopt; thiz->DefaultsUpdated.Broadcast(); if (thiz->_loadDefaultsQueued) thiz->refreshDefaults(); }); } const std::optional& CesiumIonSession::getConnection() const { return this->_connection; } const CesiumIonClient::Profile& CesiumIonSession::getProfile() { static const CesiumIonClient::Profile empty{}; if (this->_profile) { return *this->_profile; } else { this->refreshProfile(); return empty; } } const CesiumIonClient::Assets& CesiumIonSession::getAssets() { static const CesiumIonClient::Assets empty; if (this->_assets) { return *this->_assets; } else { this->refreshAssets(); return empty; } } const std::vector& CesiumIonSession::getTokens() { static const std::vector empty; if (this->_tokens) { return *this->_tokens; } else { this->refreshTokens(); return empty; } } const CesiumIonClient::Defaults& CesiumIonSession::getDefaults() { static const CesiumIonClient::Defaults empty; if (this->_defaults) { return *this->_defaults; } else { this->refreshDefaults(); return empty; } } const CesiumIonClient::ApplicationData& CesiumIonSession::getAppData() { static const CesiumIonClient::ApplicationData empty{}; if (this->_appData) { return *this->_appData; } return empty; } bool CesiumIonSession::refreshProfileIfNeeded() { if (this->_loadProfileQueued || !this->_profile.has_value()) { this->refreshProfile(); } return this->isProfileLoaded(); } bool CesiumIonSession::refreshAssetsIfNeeded() { if (this->_loadAssetsQueued || !this->_assets.has_value()) { this->refreshAssets(); } return this->isAssetListLoaded(); } bool CesiumIonSession::refreshTokensIfNeeded() { if (this->_loadTokensQueued || !this->_tokens.has_value()) { this->refreshTokens(); } return this->isTokenListLoaded(); } bool CesiumIonSession::refreshDefaultsIfNeeded() { if (this->_loadDefaultsQueued || !this->_defaults.has_value()) { this->refreshDefaults(); } return this->isDefaultsLoaded(); } Future> CesiumIonSession::findToken(const FString& token) const { if (!this->_connection) { return this->getAsyncSystem().createResolvedFuture( Response(0, "NOTCONNECTED", "Not connected to Cesium ion.")); } std::string tokenString = TCHAR_TO_UTF8(*token); std::optional maybeTokenID = Connection::getIdFromToken(tokenString); if (!maybeTokenID) { return this->getAsyncSystem().createResolvedFuture( Response(0, "INVALIDTOKEN", "The token is not valid.")); } return this->_connection->token(*maybeTokenID); } namespace { Token tokenFromServer(UCesiumIonServer* pServer) { Token result; if (pServer) { result.token = TCHAR_TO_UTF8(*pServer->DefaultIonAccessToken); } return result; } Future getTokenFuture(const CesiumIonSession& session) { std::shared_ptr pSession = session.shared_from_this(); TWeakObjectPtr pServer = session.getServer(); if (pServer.IsValid() && !pServer->DefaultIonAccessTokenId.IsEmpty()) { return session.getConnection() ->token(TCHAR_TO_UTF8(*pServer->DefaultIonAccessTokenId)) .thenImmediately([pServer](Response&& tokenResponse) { if (tokenResponse.value) { return *tokenResponse.value; } else { return tokenFromServer(pServer.Get()); } }); } else if (!pServer->DefaultIonAccessToken.IsEmpty()) { return session.findToken(pServer->DefaultIonAccessToken) .thenImmediately([pServer](Response&& response) { if (response.value) { return *response.value; } else { return tokenFromServer(pServer.Get()); } }); } else { return session.getAsyncSystem().createResolvedFuture( tokenFromServer(pServer.Get())); } } } // namespace SharedFuture CesiumIonSession::getProjectDefaultTokenDetails() { if (this->_projectDefaultTokenDetailsFuture) { // If the future is resolved but its token doesn't match the designated // default token, do the request again because the user probably specified a // new token. if (this->_projectDefaultTokenDetailsFuture->isReady() && this->_projectDefaultTokenDetailsFuture->wait().token != TCHAR_TO_UTF8(*this->_pServer->DefaultIonAccessToken)) { this->_projectDefaultTokenDetailsFuture.reset(); } else { return *this->_projectDefaultTokenDetailsFuture; } } if (!this->isConnected()) { return this->getAsyncSystem() .createResolvedFuture(tokenFromServer(this->_pServer.Get())) .share(); } this->_projectDefaultTokenDetailsFuture = getTokenFuture(*this).share(); return *this->_projectDefaultTokenDetailsFuture; } void CesiumIonSession::invalidateProjectDefaultTokenDetails() { this->_projectDefaultTokenDetailsFuture.reset(); } void CesiumIonSession::startQueuedLoads() { if (this->_loadProfileQueued) this->refreshProfile(); if (this->_loadAssetsQueued) this->refreshAssets(); if (this->_loadTokensQueued) this->refreshTokens(); if (this->_loadDefaultsQueued) this->refreshDefaults(); } CesiumAsync::Future CesiumIonSession::ensureAppDataLoaded() { UCesiumIonServer* pServer = this->_pServer.Get(); std::shared_ptr thiz = this->shared_from_this(); return CesiumIonClient::Connection::appData( thiz->_asyncSystem, thiz->_pAssetAccessor, std::string(TCHAR_TO_UTF8(*pServer->ApiUrl))) .thenInMainThread( [thiz, pServer = this->_pServer]( CesiumIonClient::Response&& appData) { CesiumAsync::Promise promise = thiz->_asyncSystem.createPromise(); thiz->_appData = appData.value; if (!appData.value.has_value()) { UE_LOG( LogCesiumEditor, Error, TEXT("Failed to obtain ion server application data: %s"), UTF8_TO_TCHAR(appData.errorMessage.c_str())); promise.resolve(false); } else { promise.resolve(true); } return promise.getFuture(); }) .catchInMainThread([thiz, pServer = this->_pServer](std::exception&& e) { UE_LOG( LogCesiumEditor, Error, TEXT("Error obtaining appData: %s"), UTF8_TO_TCHAR(e.what())); return thiz->_asyncSystem.createResolvedFuture(false); }); }