File indexing completed on 2024-05-12 05:35:30

0001 /*
0002     This file is part of KDE.
0003 
0004     SPDX-FileCopyrightText: 2009 Eckhart Wörner <ewoerner@kde.org>
0005     SPDX-FileCopyrightText: 2010 Frederik Gladhorn <gladhorn@kde.org>
0006     SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <admin@leinir.dk>
0007 
0008     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0009 
0010 */
0011 
0012 #include "kdeplatformdependent.h"
0013 
0014 #include "attica_plugin_debug.h"
0015 
0016 #include <KCMultiDialog>
0017 #include <KConfigGroup>
0018 #include <KLocalizedString>
0019 #include <QApplication>
0020 #include <QNetworkAccessManager>
0021 #include <QNetworkDiskCache>
0022 #include <QStorageInfo>
0023 
0024 #include <Accounts/AccountService>
0025 #include <Accounts/Manager>
0026 #include <KAccounts/Core>
0027 #include <KAccounts/GetCredentialsJob>
0028 
0029 using namespace Attica;
0030 
0031 KdePlatformDependent::KdePlatformDependent()
0032     : m_config(KSharedConfig::openConfig(QStringLiteral("atticarc")))
0033     , m_accessManager(nullptr)
0034 {
0035     // FIXME: Investigate how to not leak this instance without crashing.
0036     m_accessManager = new QNetworkAccessManager(nullptr);
0037 
0038     const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/attica");
0039     QNetworkDiskCache *cache = new QNetworkDiskCache(m_accessManager);
0040     QStorageInfo storageInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
0041     cache->setCacheDirectory(cacheDir);
0042     cache->setMaximumCacheSize(storageInfo.bytesTotal() / 1000);
0043     m_accessManager->setCache(cache);
0044 }
0045 
0046 KdePlatformDependent::~KdePlatformDependent()
0047 {
0048 }
0049 
0050 // TODO Cache the account (so we can call getAccount a WHOLE LOT of times without making the application super slow)
0051 // TODO Also don't just cache it forever, so reset to nullptr every so often, so we pick up potential new stuff the user's done
0052 QString KdePlatformDependent::getAccessToken(const QUrl & /*baseUrl*/) const
0053 {
0054     QString accessToken;
0055     QString idToken;
0056     Accounts::Manager *accountsManager = KAccounts::accountsManager();
0057     if (accountsManager) {
0058         static const QString serviceType{QStringLiteral("opendesktop-rating")};
0059         Accounts::AccountIdList accountIds = accountsManager->accountList(serviceType);
0060         // TODO Present the user with a choice in case there's more than one, but for now just pick the first successful one
0061         // loop through the accounts, and attempt to get them
0062         Accounts::Account *account{nullptr};
0063         for (const Accounts::AccountId &accountId : accountIds) {
0064             account = accountsManager->account(accountId);
0065             if (account) {
0066                 bool completed{false};
0067                 qCDebug(ATTICA_PLUGIN_LOG) << "Fetching data for" << accountId;
0068                 KAccounts::GetCredentialsJob *job = new KAccounts::GetCredentialsJob(accountId, accountsManager);
0069                 connect(job, &KJob::finished, [&completed, &accessToken, &idToken](KJob *kjob) {
0070                     KAccounts::GetCredentialsJob *job = qobject_cast<KAccounts::GetCredentialsJob *>(kjob);
0071                     const QVariantMap credentialsData = job->credentialsData();
0072                     accessToken = credentialsData[QStringLiteral("AccessToken")].toString();
0073                     idToken = credentialsData[QStringLiteral("IdToken")].toString();
0074                     // As this can be useful for more heavy duty debugging purposes, leaving this in so it doesn't have to be rewritten
0075                     //                     if (!accessToken.isEmpty()) {
0076                     //                         qCDebug(ATTICA_PLUGIN_LOG) << "Credentials data was retrieved";
0077                     //                         for (const QString& key : credentialsData.keys()) {
0078                     //                             qCDebug(ATTICA_PLUGIN_LOG) << key << credentialsData[key];
0079                     //                         }
0080                     //                     }
0081                     completed = true;
0082                 });
0083                 connect(job, &KJob::result, [&completed]() {
0084                     completed = true;
0085                 });
0086                 job->start();
0087                 while (!completed) {
0088                     qApp->processEvents();
0089                 }
0090                 if (!idToken.isEmpty()) {
0091                     qCDebug(ATTICA_PLUGIN_LOG) << "OpenID Access token retrieved for account" << account->id();
0092                     break;
0093                 }
0094             }
0095             if (idToken.isEmpty()) {
0096                 // If we arrived here, we did have an opendesktop account, but without the id token, which means an old version of the signon oauth2 plugin was
0097                 // used
0098                 qCWarning(ATTICA_PLUGIN_LOG) << "We got an OpenDesktop account, but it seems to be lacking the id token. This means an old SignOn OAuth2 "
0099                                                 "plugin was used for logging in. The plugin may have been upgraded in the meantime, but an account created "
0100                                                 "using the old plugin cannot be used, and you must log out and back in again.";
0101             }
0102         }
0103     } else {
0104         qCDebug(ATTICA_PLUGIN_LOG) << "No accounts manager could be fetched, so could not ask it for account details";
0105     }
0106 
0107     return idToken;
0108 }
0109 
0110 QUrl baseUrlFromRequest(const QNetworkRequest &request)
0111 {
0112     const QUrl url{request.url()};
0113     QString baseUrl = QLatin1String("%1://%2").arg(url.scheme(), url.host());
0114     int port = url.port();
0115     if (port != -1) {
0116         baseUrl.append(QString::number(port));
0117     }
0118     return url;
0119 }
0120 
0121 QNetworkRequest KdePlatformDependent::addOAuthToRequest(const QNetworkRequest &request)
0122 {
0123     QNetworkRequest notConstReq = const_cast<QNetworkRequest &>(request);
0124     const QString token{getAccessToken(baseUrlFromRequest(request))};
0125     if (!token.isEmpty()) {
0126         const QString bearer_format = QStringLiteral("Bearer %1");
0127         const QString bearer = bearer_format.arg(token);
0128         notConstReq.setRawHeader("Authorization", bearer.toUtf8());
0129     }
0130     notConstReq.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
0131 
0132     // Add cache preference in a granular fashion (we will almost certainly want more of these, but...)
0133     static const QStringList preferCacheEndpoints{QLatin1String{"/content/categories"}};
0134     for (const QString &endpoint : preferCacheEndpoints) {
0135         if (notConstReq.url().toString().endsWith(endpoint)) {
0136             QNetworkCacheMetaData cacheMeta{m_accessManager->cache()->metaData(notConstReq.url())};
0137             if (cacheMeta.isValid()) {
0138                 // If the expiration date is valid, but longer than 24 hours, don't trust that things
0139                 // haven't changed and check first, otherwise just use the cached version to relieve
0140                 // server strain and reduce network traffic.
0141                 const QDateTime tomorrow{QDateTime::currentDateTime().addDays(1)};
0142                 if (cacheMeta.expirationDate().isValid() && cacheMeta.expirationDate() < tomorrow) {
0143                     notConstReq.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
0144                 }
0145             }
0146             break;
0147         }
0148     }
0149 
0150     return notConstReq;
0151 }
0152 
0153 QNetworkReply *KdePlatformDependent::post(const QNetworkRequest &request, const QByteArray &data)
0154 {
0155     return m_accessManager->post(addOAuthToRequest(removeAuthFromRequest(request)), data);
0156 }
0157 
0158 QNetworkReply *KdePlatformDependent::post(const QNetworkRequest &request, QIODevice *data)
0159 {
0160     return m_accessManager->post(addOAuthToRequest(removeAuthFromRequest(request)), data);
0161 }
0162 
0163 QNetworkReply *KdePlatformDependent::get(const QNetworkRequest &request)
0164 {
0165     return m_accessManager->get(addOAuthToRequest(removeAuthFromRequest(request)));
0166 }
0167 
0168 QNetworkRequest KdePlatformDependent::removeAuthFromRequest(const QNetworkRequest &request)
0169 {
0170     const QStringList noauth = {QStringLiteral("no-auth-prompt"), QStringLiteral("true")};
0171     QNetworkRequest notConstReq = const_cast<QNetworkRequest &>(request);
0172     notConstReq.setAttribute(QNetworkRequest::User, noauth);
0173     return notConstReq;
0174 }
0175 
0176 bool KdePlatformDependent::saveCredentials(const QUrl & /*baseUrl*/, const QString & /*user*/, const QString & /*password*/)
0177 {
0178     qCDebug(ATTICA_PLUGIN_LOG) << "Launch the KAccounts control module";
0179     // TODO KF6 This will want replacing with a call named something that suggests calling it shows accounts (and perhaps
0180     // directly requests the accounts kcm to start adding a new account if it's not there, maybe even pre-fills the fields...)
0181 
0182     KCMultiDialog *dialog = new KCMultiDialog;
0183     dialog->addModule(KPluginMetaData(QStringLiteral("kcm_kaccounts")));
0184     dialog->setAttribute(Qt::WA_DeleteOnClose);
0185     dialog->show();
0186 
0187     return true;
0188 }
0189 
0190 bool KdePlatformDependent::hasCredentials(const QUrl &baseUrl) const
0191 {
0192     qCDebug(ATTICA_PLUGIN_LOG) << Q_FUNC_INFO;
0193     return !getAccessToken(baseUrl).isEmpty();
0194 }
0195 
0196 bool KdePlatformDependent::loadCredentials(const QUrl &baseUrl, QString &user, QString & /*password*/)
0197 {
0198     qCDebug(ATTICA_PLUGIN_LOG) << Q_FUNC_INFO;
0199     QString token = getAccessToken(baseUrl);
0200     if (!token.isEmpty()) {
0201         user = token;
0202     }
0203     return !token.isEmpty();
0204 }
0205 
0206 bool Attica::KdePlatformDependent::askForCredentials(const QUrl &baseUrl, QString &user, QString &password)
0207 {
0208     Q_UNUSED(baseUrl);
0209     Q_UNUSED(user);
0210     Q_UNUSED(password);
0211 
0212     return false;
0213 }
0214 
0215 QList<QUrl> KdePlatformDependent::getDefaultProviderFiles() const
0216 {
0217     KConfigGroup group(m_config, QStringLiteral("General"));
0218     const QStringList pathStrings = group.readPathEntry("providerFiles", QStringList(QStringLiteral("https://autoconfig.kde.org/ocs/providers.xml")));
0219     QList<QUrl> paths;
0220     for (const QString &pathString : pathStrings) {
0221         paths.append(QUrl(pathString));
0222     }
0223     qCDebug(ATTICA_PLUGIN_LOG) << "Loaded paths from config:" << paths;
0224     return paths;
0225 }
0226 
0227 void KdePlatformDependent::addDefaultProviderFile(const QUrl &url)
0228 {
0229     KConfigGroup group(m_config, QStringLiteral("General"));
0230     QStringList pathStrings = group.readPathEntry("providerFiles", QStringList(QStringLiteral("https://autoconfig.kde.org/ocs/providers.xml")));
0231     QString urlString = url.toString();
0232     if (!pathStrings.contains(urlString)) {
0233         pathStrings.append(urlString);
0234         group.writeEntry("providerFiles", pathStrings);
0235         group.sync();
0236         qCDebug(ATTICA_PLUGIN_LOG) << "wrote providers: " << pathStrings;
0237     }
0238 }
0239 
0240 void KdePlatformDependent::removeDefaultProviderFile(const QUrl &url)
0241 {
0242     KConfigGroup group(m_config, QStringLiteral("General"));
0243     QStringList pathStrings = group.readPathEntry("providerFiles", QStringList(QStringLiteral("https://autoconfig.kde.org/ocs/providers.xml")));
0244     pathStrings.removeAll(url.toString());
0245     group.writeEntry("providerFiles", pathStrings);
0246 }
0247 
0248 void KdePlatformDependent::enableProvider(const QUrl &baseUrl, bool enabled) const
0249 {
0250     KConfigGroup group(m_config, QStringLiteral("General"));
0251     QStringList pathStrings = group.readPathEntry("disabledProviders", QStringList());
0252     if (enabled) {
0253         pathStrings.removeAll(baseUrl.toString());
0254     } else {
0255         if (!pathStrings.contains(baseUrl.toString())) {
0256             pathStrings.append(baseUrl.toString());
0257         }
0258     }
0259     group.writeEntry("disabledProviders", pathStrings);
0260     group.sync();
0261 }
0262 
0263 bool KdePlatformDependent::isEnabled(const QUrl &baseUrl) const
0264 {
0265     KConfigGroup group(m_config, QStringLiteral("General"));
0266     return !group.readPathEntry("disabledProviders", QStringList()).contains(baseUrl.toString());
0267 }
0268 
0269 QNetworkAccessManager *Attica::KdePlatformDependent::nam()
0270 {
0271     return m_accessManager;
0272 }