LCOV - code coverage report
Current view: top level - muse/framework/cloud/internal - abstractcloudservice.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 54.9 % 273 150
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2025 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : 
      23              : #include "abstractcloudservice.h"
      24              : 
      25              : #include <QBuffer>
      26              : #include <QOAuth2AuthorizationCodeFlow>
      27              : #include <QJsonDocument>
      28              : #include <QJsonObject>
      29              : #include <QUrlQuery>
      30              : #include <QHttpMultiPart>
      31              : #include <QRandomGenerator>
      32              : #include <QEventLoop>
      33              : #include <QTimer>
      34              : 
      35              : #include "clouderrors.h"
      36              : #include "multiwindows/resourcelockguard.h"
      37              : #include "network/networkerrors.h"
      38              : #include "global/iapplication.h"
      39              : #include "global/io/ioretcodes.h"
      40              : #include "draw/types/color.h"
      41              : 
      42              : #include "oauthhttpserverreplyhandler.h"
      43              : 
      44              : #include "log.h"
      45              : 
      46              : using namespace muse;
      47              : using namespace muse::cloud;
      48              : using namespace muse::network;
      49              : using namespace muse::async;
      50              : 
      51              : const QString muse::cloud::ACCESS_TOKEN_KEY("access_token");
      52              : const QString muse::cloud::REFRESH_TOKEN_KEY("refresh_token");
      53              : 
      54              : static const std::string CLOUD_ACCESS_TOKEN_RESOURCE_NAME("CLOUD_ACCESS_TOKEN");
      55              : static const std::string STATUS_KEY("status");
      56              : 
      57           16 : static Ret retFromStatus(int status, bool isAlreadyUploaded)
      58              : {
      59           16 :     Ret ret;
      60              : 
      61           16 :     switch (status) {
      62            2 :     case 400:
      63            2 :         ret = make_ret(cloud::Err::Status400_InvalidRequest);
      64            2 :         break;
      65            2 :     case 401:
      66            2 :         ret = make_ret(cloud::Err::Status401_AuthorizationRequired);
      67            2 :         break;
      68            3 :     case 403:
      69            3 :         ret = make_ret(isAlreadyUploaded ? cloud::Err::Status403_AccountNotActivated : cloud::Err::Status403_NotOwner);
      70            3 :         break;
      71            3 :     case 404:
      72            3 :         ret = make_ret(cloud::Err::Status404_NotFound);
      73            3 :         break;
      74            0 :     case 409:
      75            0 :         ret = make_ret(cloud::Err::Status409_Conflict);
      76            0 :         break;
      77            2 :     case 422:
      78            2 :         ret = make_ret(cloud::Err::Status422_ValidationFailed);
      79            2 :         break;
      80            1 :     case 429:
      81            1 :         ret = make_ret(cloud::Err::Status429_RateLimitExceeded);
      82            1 :         break;
      83            2 :     case 500:
      84            2 :         ret = make_ret(cloud::Err::Status500_InternalServerError);
      85            2 :         break;
      86            1 :     default:
      87            1 :         ret = make_ret(cloud::Err::UnknownStatusCode);
      88            1 :         ret.setText("Unknown status code: " + std::to_string(status));
      89            1 :         break;
      90              :     }
      91              : 
      92           16 :     ret.setData(STATUS_KEY, status);
      93              : 
      94           16 :     return ret;
      95            0 : }
      96              : 
      97           57 : AbstractCloudService::AbstractCloudService(const modularity::ContextPtr& iocCtx, QObject* parent)
      98           57 :     : QObject(parent), Contextable(iocCtx)
      99              : {
     100           57 :     m_userAuthorized.val = false;
     101           57 : }
     102              : 
     103           57 : void AbstractCloudService::init()
     104              : {
     105           58 :     TRACEFUNC;
     106              : 
     107           57 :     m_serverConfig = serverConfig();
     108           57 :     m_networkManager = networkManagerCreator()->makeNetworkManager();
     109              : 
     110           57 :     multiwindowsProvider()->resourceChanged().onReceive(this, [this](const std::string& resourceName) {
     111            0 :         if (resourceName == CLOUD_ACCESS_TOKEN_RESOURCE_NAME) {
     112            0 :             readTokens();
     113              :         }
     114            0 :     });
     115              : 
     116           57 :     if (readTokens()) {
     117          151 :         executeAsyncRequest([this]() { return downloadAccountInfo(); })
     118           50 :         .onResolve(this, [](const Ret& ret) {
     119           50 :             if (!ret) {
     120            0 :                 LOGE() << ret.toString();
     121              :             }
     122           50 :         });
     123              :     }
     124           57 : }
     125              : 
     126            0 : void AbstractCloudService::initOAuthIfNecessary()
     127              : {
     128            0 :     if (m_oauth2) {
     129            0 :         return;
     130              :     }
     131              : 
     132              :     // We initialize the OAuth etc lazily, to save resources, but also so that firewall warnings
     133              :     // will only appear at the moment that the user tries to log in, not on every launch.
     134              : 
     135            0 :     m_oauth2 = new QOAuth2AuthorizationCodeFlow(this);
     136              : 
     137            0 :     m_replyHandler = new OAuthHttpServerReplyHandler(iocContext(), this);
     138            0 :     m_replyHandler->setRedirectUrl(m_serverConfig.signInSuccessUrl);
     139              : 
     140            0 :     m_oauth2->setAuthorizationUrl(m_serverConfig.authorizationUrl);
     141            0 :     m_oauth2->setTokenUrl(m_serverConfig.accessTokenUrl);
     142            0 :     m_oauth2->setPkceMethod(QOAuth2AuthorizationCodeFlow::PkceMethod::None);
     143            0 :     m_oauth2->setModifyParametersFunction([this](QAbstractOAuth::Stage, QMultiMap<QString, QVariant>* parameters) {
     144            0 :         for (const QString& key : m_serverConfig.authorizationParameters.keys()) {
     145            0 :             parameters->replace(key, m_serverConfig.authorizationParameters.value(key));
     146            0 :         }
     147            0 :     });
     148              : 
     149            0 :     m_oauth2->setReplyHandler(m_replyHandler);
     150              : 
     151            0 :     connect(m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &AbstractCloudService::openUrl);
     152            0 :     connect(m_oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, &AbstractCloudService::onUserAuthorized);
     153              : 
     154            0 :     connect(m_oauth2, &QOAuth2AuthorizationCodeFlow::serverReportedErrorOccurred,
     155            0 :             [](const QString& error, const QString& errorDescription, const QUrl& uri) {
     156            0 :         LOGE() << "Error during authorization: " << error << "\n Description: " << errorDescription << "\n URI: " << uri.toString();
     157            0 :     });
     158              : }
     159              : 
     160           57 : bool AbstractCloudService::readTokens()
     161              : {
     162           58 :     TRACEFUNC;
     163              : 
     164           57 :     io::path_t tokensPath = tokensFilePath();
     165           57 :     RetVal<ByteArray> tokensData;
     166              :     {
     167           57 :         mi::ReadResourceLockGuard resource_guard(multiwindowsProvider(), CLOUD_ACCESS_TOKEN_RESOURCE_NAME);
     168           57 :         tokensData = fileSystem()->readFile(tokensPath);
     169           57 :     }
     170              : 
     171           57 :     if (!tokensData.ret) {
     172            7 :         if (tokensData.ret.code() == (int)io::Err::FSNotExist) {
     173            7 :             LOGI() << "Could not find the tokens file: " << tokensPath;
     174              :         } else {
     175            0 :             LOGE() << tokensData.ret.toString();
     176              :         }
     177              : 
     178            7 :         return false;
     179              :     }
     180              : 
     181           50 :     QJsonParseError err;
     182           50 :     QJsonDocument tokensDoc = QJsonDocument::fromJson(tokensData.val.toQByteArrayNoCopy(), &err);
     183           50 :     if (err.error != QJsonParseError::NoError || !tokensDoc.isObject()) {
     184            0 :         LOGE() << "Error on parse tokens file: " << err.errorString();
     185            0 :         return false;
     186              :     }
     187              : 
     188           50 :     QJsonObject saveObject = tokensDoc.object();
     189              : 
     190           50 :     m_accessToken = saveObject[ACCESS_TOKEN_KEY].toString();
     191           50 :     m_refreshToken = saveObject[REFRESH_TOKEN_KEY].toString();
     192              : 
     193           50 :     return true;
     194           57 : }
     195              : 
     196            5 : Ret AbstractCloudService::saveTokens()
     197              : {
     198            6 :     TRACEFUNC;
     199              : 
     200            5 :     QJsonObject tokensObject;
     201            5 :     tokensObject[ACCESS_TOKEN_KEY] = m_accessToken;
     202            5 :     tokensObject[REFRESH_TOKEN_KEY] = m_refreshToken;
     203            5 :     QJsonDocument tokensDoc(tokensObject);
     204            5 :     QByteArray json = tokensDoc.toJson();
     205              : 
     206            5 :     Ret ret;
     207              :     {
     208            5 :         mi::WriteResourceLockGuard resource_guard(multiwindowsProvider(), CLOUD_ACCESS_TOKEN_RESOURCE_NAME);
     209            5 :         ret = fileSystem()->writeFile(tokensFilePath(), ByteArray::fromQByteArrayNoCopy(json));
     210            5 :     }
     211              : 
     212            5 :     return ret;
     213            5 : }
     214              : 
     215            0 : void AbstractCloudService::removeTokens()
     216              : {
     217              :     {
     218            0 :         mi::WriteResourceLockGuard resource_guard(multiwindowsProvider(), CLOUD_ACCESS_TOKEN_RESOURCE_NAME);
     219            0 :         Ret ret = fileSystem()->remove(tokensFilePath());
     220            0 :         if (!ret) {
     221            0 :             LOGE() << ret.toString();
     222              :         }
     223            0 :     }
     224              : 
     225            0 :     clearTokens();
     226            0 : }
     227              : 
     228            0 : void AbstractCloudService::clearTokens()
     229              : {
     230            0 :     m_accessToken.clear();
     231            0 :     m_refreshToken.clear();
     232            0 :     setAccountInfo(AccountInfo());
     233            0 : }
     234              : 
     235           62 : io::path_t AbstractCloudService::tokensFilePath() const
     236              : {
     237           62 :     return configuration()->tokensFilePath(m_serverConfig.serverCode.toStdString());
     238              : }
     239              : 
     240            0 : void AbstractCloudService::onUserAuthorized()
     241              : {
     242            0 :     TRACEFUNC;
     243              : 
     244            0 :     initOAuthIfNecessary();
     245              : 
     246            0 :     m_accessToken = m_oauth2->token();
     247            0 :     m_refreshToken = m_oauth2->refreshToken();
     248              : 
     249            0 :     LOGD() << "========== access " << m_accessToken << " ========= refresh " << m_refreshToken;
     250              : 
     251            0 :     saveTokens();
     252              : 
     253            0 :     downloadAccountInfo().onResolve(this, [](const Ret& ret) {
     254            0 :         if (!ret) {
     255            0 :             LOGE() << ret.toString();
     256              :         }
     257            0 :     });
     258            0 : }
     259              : 
     260          176 : RequestHeaders AbstractCloudService::defaultHeaders() const
     261              : {
     262          176 :     return configuration()->headers();
     263              : }
     264              : 
     265          106 : RetVal<QUrl> AbstractCloudService::prepareUrlForRequest(QUrl apiUrl, const QVariantMap& params) const
     266              : {
     267          106 :     if (m_accessToken.isEmpty()) {
     268            5 :         return make_ret(cloud::Err::AccessTokenIsEmpty);
     269              :     }
     270              : 
     271          101 :     QUrlQuery query;
     272          101 :     query.addQueryItem(ACCESS_TOKEN_KEY, m_accessToken);
     273              : 
     274          134 :     for (auto it = params.cbegin(); it != params.cend(); ++it) {
     275           33 :         query.addQueryItem(it.key(), it.value().toString());
     276              :     }
     277              : 
     278          101 :     apiUrl.setQuery(query);
     279              : 
     280          101 :     return RetVal<QUrl>::make_ok(apiUrl);
     281          101 : }
     282              : 
     283            0 : void AbstractCloudService::signIn()
     284              : {
     285            0 :     if (m_userAuthorized.val) {
     286            0 :         return;
     287              :     }
     288              : 
     289            0 :     initOAuthIfNecessary();
     290              : 
     291            0 :     m_oauth2->setAuthorizationUrl(m_serverConfig.authorizationUrl);
     292            0 :     m_oauth2->grant();
     293              : }
     294              : 
     295            0 : void AbstractCloudService::signUp()
     296              : {
     297            0 :     if (m_userAuthorized.val) {
     298            0 :         return;
     299              :     }
     300              : 
     301            0 :     initOAuthIfNecessary();
     302              : 
     303            0 :     m_oauth2->setAuthorizationUrl(m_serverConfig.signUpUrl);
     304            0 :     m_oauth2->grant();
     305              : }
     306              : 
     307            0 : void AbstractCloudService::signOut()
     308              : {
     309            0 :     if (!m_userAuthorized.val) {
     310            0 :         return;
     311              :     }
     312              : 
     313            0 :     TRACEFUNC;
     314              : 
     315            0 :     QVariantMap params;
     316            0 :     params[REFRESH_TOKEN_KEY] = m_refreshToken;
     317              : 
     318            0 :     RetVal<QUrl> signOutUrl = prepareUrlForRequest(m_serverConfig.logoutApiUrl, params);
     319            0 :     if (!signOutUrl.ret) {
     320            0 :         LOGE() << signOutUrl.ret.toString();
     321            0 :         return;
     322              :     }
     323              : 
     324            0 :     if (signOutUrl.val.isEmpty()) {
     325            0 :         removeTokens();
     326            0 :         return;
     327              :     }
     328              : 
     329            0 :     RetVal<Progress> progress = m_networkManager->get(signOutUrl.val, nullptr, m_serverConfig.headers);
     330            0 :     if (!progress.ret) {
     331            0 :         LOGE() << progress.ret.toString();
     332            0 :         removeTokens();
     333            0 :         return;
     334              :     }
     335              : 
     336            0 :     progress.val.finished().onReceive(this, [this](const ProgressResult& res) {
     337            0 :         if (!res.ret) {
     338            0 :             LOGE() << res.ret;
     339              :         }
     340              : 
     341            0 :         removeTokens();
     342            0 :     });
     343            0 : }
     344              : 
     345            4 : ValCh<bool> AbstractCloudService::userAuthorized() const
     346              : {
     347            4 :     return m_userAuthorized;
     348              : }
     349              : 
     350            4 : const AccountInfo& AbstractCloudService::accountInfo() const
     351              : {
     352            4 :     return m_accountInfo;
     353              : }
     354              : 
     355            0 : Promise<Ret> AbstractCloudService::checkCloudIsAvailable() const
     356              : {
     357              :     static constexpr int ANSWER_TIMEOUT_MS = 5000;
     358              : 
     359            0 :     return async::make_promise<Ret>([this](auto resolve) {
     360            0 :         RetVal<Progress> progress = m_networkManager->get(m_serverConfig.serverAvailabilityUrl, nullptr, m_serverConfig.headers);
     361            0 :         if (!progress.ret) {
     362            0 :             return resolve(progress.ret);
     363              :         }
     364              : 
     365            0 :         auto answered = std::make_shared<bool>(false);
     366              : 
     367            0 :         progress.val.finished().onReceive(this, [answered, resolve](const ProgressResult& res) {
     368            0 :             *answered = true;
     369            0 :             (void)resolve(res.ret);
     370              :         });
     371              : 
     372            0 :         QTimer::singleShot(ANSWER_TIMEOUT_MS, [answered, progress]() mutable {
     373            0 :             if (!*answered) {
     374            0 :                 progress.val.cancel();
     375              :             }
     376              :         });
     377              : 
     378            0 :         return Promise<Ret>::dummy_result();
     379            0 :     });
     380              : }
     381              : 
     382           50 : void AbstractCloudService::setAccountInfo(const AccountInfo& info)
     383              : {
     384           50 :     if (m_accountInfo == info) {
     385            1 :         return;
     386              :     }
     387              : 
     388           49 :     m_accountInfo = info;
     389           49 :     m_userAuthorized.set(info.isValid());
     390              : }
     391              : 
     392           90 : Promise<Ret> AbstractCloudService::executeAsyncRequest(const AsyncRequestCallback& requestCallback)
     393              : {
     394              :     //! NOTE: helps to avoid memory leak due to self-capture
     395           90 :     auto callback = new AsyncRequestCallback(requestCallback);
     396              : 
     397          180 :     return requestCallback().then<Ret>(this, [this, callback](const Ret& ret, auto resolve) {
     398           86 :         if (ret) {
     399           60 :             delete callback;
     400           60 :             return resolve(ret);
     401              :         }
     402              : 
     403              :         // Check whether tokens have expired...
     404           26 :         if (statusCode(ret) != USER_UNAUTHORIZED_STATUS_CODE) {
     405           19 :             delete callback;
     406           19 :             return resolve(ret);
     407              :         }
     408              : 
     409              :         // Update tokens and retry request
     410            7 :         updateTokens().onResolve(this, [this, callback, resolve](const Ret& ret) {
     411            7 :             if (!ret) {
     412            2 :                 delete callback;
     413            2 :                 (void)resolve(ret);
     414            3 :                 return;
     415              :             }
     416              : 
     417            5 :             Ret saveTokensRet = saveTokens();
     418            5 :             if (!saveTokensRet) {
     419            1 :                 delete callback;
     420            1 :                 (void)resolve(saveTokensRet);
     421            1 :                 return;
     422              :             }
     423              : 
     424            4 :             (*callback)().onResolve(this, [resolve, callback](const Ret& ret) {
     425            4 :                 delete callback;
     426            4 :                 (void)resolve(ret);
     427              :             });
     428            5 :         });
     429              : 
     430            7 :         return Promise<Ret>::dummy_result();
     431          180 :     });
     432              : }
     433              : 
     434           19 : Ret AbstractCloudService::uploadingDownloadingRetFromRawRet(const Ret& rawRet, bool isAlreadyUploaded) const
     435              : {
     436           19 :     if (rawRet) {
     437            1 :         return rawRet; // OK
     438              :     }
     439              : 
     440           18 :     int status = statusCode(rawRet);
     441           18 :     if (status) {
     442           16 :         return retFromStatus(status, isAlreadyUploaded);
     443              :     }
     444              : 
     445            2 :     switch (rawRet.code()) {
     446            1 :     case int(network::Err::NetworkError):
     447              :     case int(network::Err::Timeout):
     448              :     case int(network::Err::Abort):
     449            1 :         return make_ret(cloud::Err::NetworkError);
     450              :     }
     451              : 
     452            1 :     return rawRet;
     453              : }
     454              : 
     455           46 : int AbstractCloudService::statusCode(const Ret& ret) const
     456              : {
     457           46 :     return ret.data<int>(STATUS_KEY, 0);
     458              : }
     459              : 
     460           14 : void AbstractCloudService::printServerReply(const QBuffer& reply) const
     461              : {
     462           14 :     const QByteArray& data = reply.data();
     463              : 
     464           14 :     if (!data.isEmpty()) {
     465            6 :         LOGD() << "Server reply: " << data;
     466              :     }
     467           14 : }
     468              : 
     469            0 : const QString& AbstractCloudService::accessToken() const
     470              : {
     471            0 :     return m_accessToken;
     472              : }
     473              : 
     474            5 : void AbstractCloudService::setAccessToken(const QString& token)
     475              : {
     476            5 :     m_accessToken = token;
     477            5 : }
     478              : 
     479            7 : const QString& AbstractCloudService::refreshToken() const
     480              : {
     481            7 :     return m_refreshToken;
     482              : }
     483              : 
     484            5 : void AbstractCloudService::setRefreshToken(const QString& token)
     485              : {
     486            5 :     m_refreshToken = token;
     487            5 : }
     488              : 
     489            0 : void AbstractCloudService::openUrl(const QUrl& url)
     490              : {
     491            0 :     Ret ret = platformInteractive()->openUrl(url);
     492            0 :     if (!ret) {
     493            0 :         LOGE() << ret.toString();
     494              :     }
     495            0 : }
     496              : 
     497            0 : QString AbstractCloudService::logoColor() const
     498              : {
     499            0 :     const ui::ThemeList& themens = uiConfig()->themes();
     500            0 :     bool isDarkMode = uiConfig()->isDarkMode();
     501              : 
     502            0 :     for (const ui::ThemeInfo& theme : themens) {
     503            0 :         if ((isDarkMode && theme.codeKey == ui::DARK_THEME_CODE)
     504            0 :             || (!isDarkMode && theme.codeKey == ui::LIGHT_THEME_CODE)) {
     505            0 :             return theme.values[ui::FONT_PRIMARY_COLOR].toString();
     506              :         }
     507              :     }
     508              : 
     509            0 :     return QString::fromStdString(draw::Color::BLACK.toString());
     510            0 : }
        

Generated by: LCOV version 2.0-1