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 }