diff --git a/resources/ews/ewsclient/auth/ewsoauth.cpp b/resources/ews/ewsclient/auth/ewsoauth.cpp index b0de2676f..c6f4e86cd 100644 --- a/resources/ews/ewsclient/auth/ewsoauth.cpp +++ b/resources/ews/ewsclient/auth/ewsoauth.cpp @@ -1,516 +1,521 @@ /* Copyright (C) 2018 Krzysztof Nowicki This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ewsoauth.h" #include #ifdef EWSOAUTH_UNITTEST #include "ewsoauth_ut_mock.h" using namespace Mock; #else #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_QCA #include "ewspkeyauthjob.h" #endif #endif #include #include "ewsclient_debug.h" static const auto o365AuthorizationUrl = QUrl(QStringLiteral("https://login.microsoftonline.com/common/oauth2/authorize")); static const auto o365AccessTokenUrl = QUrl(QStringLiteral("https://login.microsoftonline.com/common/oauth2/token")); static const auto o365FakeUserAgent = QStringLiteral("Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36"); static const auto o365Resource = QStringLiteral("https%3A%2F%2Foutlook.office365.com%2F"); #ifdef HAVE_QCA static const auto pkeyAuthSuffix = QStringLiteral(" PKeyAuth/1.0"); static const auto pkeyRedirectUri = QStringLiteral("urn:http-auth:PKeyAuth"); static const QString pkeyPasswordMapKey = QStringLiteral("pkey-password"); #endif static const QString accessTokenMapKey = QStringLiteral("access-token"); static const QString refreshTokenMapKey = QStringLiteral("refresh-token"); class EwsOAuthUrlSchemeHandler final : public QWebEngineUrlSchemeHandler { Q_OBJECT public: EwsOAuthUrlSchemeHandler(QObject *parent = nullptr) : QWebEngineUrlSchemeHandler(parent) { } ~EwsOAuthUrlSchemeHandler() override = default; void requestStarted(QWebEngineUrlRequestJob *request) override; Q_SIGNALS: void returnUriReceived(const QUrl &url); }; class EwsOAuthReplyHandler final : public QAbstractOAuthReplyHandler { Q_OBJECT public: EwsOAuthReplyHandler(QObject *parent, const QString &returnUri) : QAbstractOAuthReplyHandler(parent) , mReturnUri(returnUri) { } ~EwsOAuthReplyHandler() override = default; QString callback() const override { return mReturnUri; } void networkReplyFinished(QNetworkReply *reply) override; Q_SIGNALS: void replyError(const QString &error); private: const QString mReturnUri; }; class EwsOAuthRequestInterceptor final : public QWebEngineUrlRequestInterceptor { Q_OBJECT public: EwsOAuthRequestInterceptor(QObject *parent, const QString &redirectUri) : QWebEngineUrlRequestInterceptor(parent) , mRedirectUri(redirectUri) { } ~EwsOAuthRequestInterceptor() override = default; void interceptRequest(QWebEngineUrlRequestInfo &info) override; Q_SIGNALS: void redirectUriIntercepted(const QUrl &url); private: const QString mRedirectUri; }; class EwsOAuthPrivate final : public QObject { Q_OBJECT public: EwsOAuthPrivate(EwsOAuth *parent, const QString &email, const QString &appId, const QString &redirectUri); ~EwsOAuthPrivate() override = default; bool authenticate(bool interactive); void modifyParametersFunction(QAbstractOAuth::Stage stage, QVariantMap *parameters); void authorizeWithBrowser(const QUrl &url); void redirectUriIntercepted(const QUrl &url); void granted(); void error(const QString &error, const QString &errorDescription, const QUrl &uri); QVariantMap queryToVarmap(const QUrl &url); #ifdef HAVE_QCA void pkeyAuthResult(KJob *job); #endif QWebEngineView mWebView; QWebEngineProfile mWebProfile; QWebEnginePage mWebPage; QOAuth2AuthorizationCodeFlow mOAuth2; EwsOAuthReplyHandler mReplyHandler; EwsOAuthRequestInterceptor mRequestInterceptor; EwsOAuthUrlSchemeHandler mSchemeHandler; QString mToken; const QString mEmail; const QString mRedirectUri; bool mAuthenticated; QPointer mWebDialog; #ifdef HAVE_QCA QString mPKeyPassword; #endif EwsOAuth *q_ptr = nullptr; Q_DECLARE_PUBLIC(EwsOAuth) }; void EwsOAuthUrlSchemeHandler::requestStarted(QWebEngineUrlRequestJob *request) { returnUriReceived(request->requestUrl()); } void EwsOAuthReplyHandler::networkReplyFinished(QNetworkReply *reply) { - if (reply->error() != QNetworkReply::NoError) { +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + const auto networkError = reply->error(); +#else + const auto networkError = reply->networkError(); +#endif + if (networkError != QNetworkReply::NoError) { Q_EMIT replyError(reply->errorString()); return; } else if (reply->header(QNetworkRequest::ContentTypeHeader).isNull()) { Q_EMIT replyError(QStringLiteral("Empty or no Content-type header")); return; } const auto cth = reply->header(QNetworkRequest::ContentTypeHeader); const auto ct = cth.isNull() ? QStringLiteral("text/html") : cth.toString(); const auto data = reply->readAll(); if (data.isEmpty()) { Q_EMIT replyError(QStringLiteral("No data received")); return; } Q_EMIT replyDataReceived(data); QVariantMap tokens; if (ct.startsWith(QLatin1String("text/html")) || ct.startsWith(QLatin1String("application/x-www-form-urlencoded"))) { QUrlQuery q(QString::fromUtf8(data)); const auto items = q.queryItems(QUrl::FullyDecoded); for (const auto &it : items) { tokens.insert(it.first, it.second); } } else if (ct.startsWith(QLatin1String("application/json")) || ct.startsWith(QLatin1String("text/javascript"))) { const auto document = QJsonDocument::fromJson(data); if (!document.isObject()) { Q_EMIT replyError(QStringLiteral("Invalid JSON data received")); return; } const auto object = document.object(); if (object.isEmpty()) { Q_EMIT replyError(QStringLiteral("Empty JSON data received")); return; } tokens = object.toVariantMap(); } else { Q_EMIT replyError(QStringLiteral("Unknown content type")); return; } const auto error = tokens.value(QStringLiteral("error")); if (error.isValid()) { Q_EMIT replyError(QStringLiteral("Received error response: ") + error.toString()); return; } const auto accessToken = tokens.value(QStringLiteral("access_token")); if (!accessToken.isValid() || accessToken.toString().isEmpty()) { Q_EMIT replyError(QStringLiteral("Received empty or no access token")); return; } Q_EMIT tokensReceived(tokens); } void EwsOAuthRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info) { const auto url = info.requestUrl(); qCDebugNC(EWSCLI_LOG) << QStringLiteral("Intercepted browser navigation to ") << url; if ((url.toString(QUrl::RemoveQuery) == mRedirectUri) #ifdef HAVE_QCA || (url.toString(QUrl::RemoveQuery) == pkeyRedirectUri) #endif ) { qCDebug(EWSCLI_LOG) << QStringLiteral("Found redirect URI - blocking request"); redirectUriIntercepted(url); info.block(true); } } EwsOAuthPrivate::EwsOAuthPrivate(EwsOAuth *parent, const QString &email, const QString &appId, const QString &redirectUri) : QObject(nullptr) , mWebView(nullptr) , mWebProfile() , mWebPage(&mWebProfile) , mReplyHandler(this, redirectUri) , mRequestInterceptor(this, redirectUri) , mEmail(email) , mRedirectUri(redirectUri) , mAuthenticated(false) , q_ptr(parent) { mOAuth2.setReplyHandler(&mReplyHandler); mOAuth2.setAuthorizationUrl(o365AuthorizationUrl); mOAuth2.setAccessTokenUrl(o365AccessTokenUrl); mOAuth2.setClientIdentifier(appId); #if QTWEBENGINEWIDGETS_VERSION < QT_VERSION_CHECK(5, 13, 0) mWebProfile.setRequestInterceptor(&mRequestInterceptor); #else mWebProfile.setUrlRequestInterceptor(&mRequestInterceptor); #endif mWebProfile.installUrlSchemeHandler("urn", &mSchemeHandler); mWebView.setPage(&mWebPage); mOAuth2.setModifyParametersFunction([&](QAbstractOAuth::Stage stage, QVariantMap *parameters) { modifyParametersFunction(stage, parameters); }); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &EwsOAuthPrivate::authorizeWithBrowser); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::granted, this, &EwsOAuthPrivate::granted); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::error, this, &EwsOAuthPrivate::error); connect(&mRequestInterceptor, &EwsOAuthRequestInterceptor::redirectUriIntercepted, this, &EwsOAuthPrivate::redirectUriIntercepted, Qt::QueuedConnection); connect(&mReplyHandler, &EwsOAuthReplyHandler::replyError, this, [this](const QString &err) { error(QStringLiteral("Network reply error"), err, QUrl()); }); } bool EwsOAuthPrivate::authenticate(bool interactive) { Q_Q(EwsOAuth); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Starting OAuth2 authentication"); if (!mOAuth2.refreshToken().isEmpty()) { mOAuth2.refreshAccessToken(); return true; } else if (interactive) { mOAuth2.grant(); return true; } else { return false; } } void EwsOAuthPrivate::modifyParametersFunction(QAbstractOAuth::Stage stage, QVariantMap *parameters) { switch (stage) { case QAbstractOAuth::Stage::RequestingAccessToken: parameters->insert(QStringLiteral("resource"), o365Resource); break; case QAbstractOAuth::Stage::RequestingAuthorization: parameters->insert(QStringLiteral("prompt"), QStringLiteral("login")); parameters->insert(QStringLiteral("login_hint"), mEmail); parameters->insert(QStringLiteral("resource"), o365Resource); break; default: break; } } void EwsOAuthPrivate::authorizeWithBrowser(const QUrl &url) { Q_Q(EwsOAuth); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Launching browser for authentication"); /* Bad bad Microsoft... * When Conditional Access is enabled on the server the OAuth2 authentication server only supports Windows, * MacOSX, Android and iOS. No option to include Linux. Support (i.e. guarantee that it works) * is one thing, but blocking unsupported browsers completely is just wrong. * Fortunately enough this can be worked around by faking the user agent to something "supported". */ auto userAgent = o365FakeUserAgent; #ifdef HAVE_QCA if (!q->mPKeyCertFile.isNull() && !q->mPKeyKeyFile.isNull()) { qCInfoNC(EWSCLI_LOG) << QStringLiteral("Found PKeyAuth certificates"); userAgent += pkeyAuthSuffix; } else { qCInfoNC(EWSCLI_LOG) << QStringLiteral("PKeyAuth certificates not found"); } #endif mWebProfile.setHttpUserAgent(userAgent); mWebDialog = new QDialog(q->mAuthParentWidget); mWebDialog->setObjectName(QStringLiteral("Akonadi EWS Resource - Authentication")); mWebDialog->setWindowIcon(QIcon(QStringLiteral("akonadi-ews"))); mWebDialog->resize(400, 500); auto layout = new QHBoxLayout(mWebDialog); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(&mWebView); mWebView.show(); connect(mWebDialog.data(), &QDialog::rejected, this, [this]() { error(QStringLiteral("User cancellation"), QStringLiteral("The authentication browser was closed"), QUrl()); }); mWebView.load(url); mWebDialog->show(); } QVariantMap EwsOAuthPrivate::queryToVarmap(const QUrl &url) { QUrlQuery query(url); QVariantMap varmap; for (const auto &item : query.queryItems()) { varmap[item.first] = item.second; } return varmap; } void EwsOAuthPrivate::redirectUriIntercepted(const QUrl &url) { qCDebugNC(EWSCLI_LOG) << QStringLiteral("Intercepted redirect URI from browser: ") << url; mWebView.stop(); mWebDialog->hide(); #ifdef HAVE_QCA Q_Q(EwsOAuth); if (url.toString(QUrl::RemoveQuery) == pkeyRedirectUri) { qCDebugNC(EWSCLI_LOG) << QStringLiteral("Found PKeyAuth URI"); auto pkeyAuthJob = new EwsPKeyAuthJob(url, q->mPKeyCertFile, q->mPKeyKeyFile, mPKeyPassword, this); connect(pkeyAuthJob, &KJob::result, this, &EwsOAuthPrivate::pkeyAuthResult); pkeyAuthJob->start(); return; } #endif mOAuth2.authorizationCallbackReceived(queryToVarmap(url)); } #ifdef HAVE_QCA void EwsOAuthPrivate::pkeyAuthResult(KJob *j) { EwsPKeyAuthJob *job = qobject_cast(j); qCDebugNC(EWSCLI_LOG) << QStringLiteral("PKeyAuth result: %1").arg(job->error()); QVariantMap varmap; if (job->error() == 0) { varmap = queryToVarmap(job->resultUri()); } else { varmap[QStringLiteral("error")] = job->errorString(); } mOAuth2.authorizationCallbackReceived(varmap); } #endif void EwsOAuthPrivate::granted() { Q_Q(EwsOAuth); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Authentication succeeded"); mAuthenticated = true; QMap map; map[accessTokenMapKey] = mOAuth2.token(); map[refreshTokenMapKey] = mOAuth2.refreshToken(); Q_EMIT q->setWalletMap(map); Q_EMIT q->authSucceeded(); } void EwsOAuthPrivate::error(const QString &error, const QString &errorDescription, const QUrl &uri) { Q_Q(EwsOAuth); Q_UNUSED(uri); mAuthenticated = false; mOAuth2.setRefreshToken(QString()); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Authentication failed: ") << error << errorDescription; Q_EMIT q->authFailed(error); } EwsOAuth::EwsOAuth(QObject *parent, const QString &email, const QString &appId, const QString &redirectUri) : EwsAbstractAuth(parent) , d_ptr(new EwsOAuthPrivate(this, email, appId, redirectUri)) { } EwsOAuth::~EwsOAuth() { } void EwsOAuth::init() { requestWalletMap(); } bool EwsOAuth::getAuthData(QString &username, QString &password, QStringList &customHeaders) { Q_D(const EwsOAuth); Q_UNUSED(username); Q_UNUSED(password); if (d->mAuthenticated) { customHeaders.append(QStringLiteral("Authorization: Bearer ") + d->mOAuth2.token()); return true; } else { return false; } } void EwsOAuth::notifyRequestAuthFailed() { Q_D(EwsOAuth); d->mOAuth2.setToken(QString()); d->mAuthenticated = false; EwsAbstractAuth::notifyRequestAuthFailed(); } bool EwsOAuth::authenticate(bool interactive) { Q_D(EwsOAuth); return d->authenticate(interactive); } const QString &EwsOAuth::reauthPrompt() const { static const QString prompt = i18nc("@info", "Microsoft Exchange credentials for the account %1 are no longer valid. You need to authenticate in order to continue using it."); return prompt; } const QString &EwsOAuth::authFailedPrompt() const { static const QString prompt = i18nc("@info", "Failed to obtain credentials for Microsoft Exchange account %1. Please update it in the account settings page."); return prompt; } void EwsOAuth::walletPasswordRequestFinished(const QString &password) { Q_UNUSED(password); } void EwsOAuth::walletMapRequestFinished(const QMap &map) { Q_D(EwsOAuth); #ifdef HAVE_QCA if (map.contains(pkeyPasswordMapKey)) { d->mPKeyPassword = map[pkeyPasswordMapKey]; } #endif if (map.contains(refreshTokenMapKey)) { d->mOAuth2.setRefreshToken(map[refreshTokenMapKey]); } if (map.contains(accessTokenMapKey)) { d->mOAuth2.setToken(map[accessTokenMapKey]); d->mAuthenticated = true; Q_EMIT authSucceeded(); } else { Q_EMIT authFailed(QStringLiteral("Access token request failed")); } } #include "ewsoauth.moc" diff --git a/resources/ews/ewsclient/auth/ewspkeyauthjob.cpp b/resources/ews/ewsclient/auth/ewspkeyauthjob.cpp index 3db1ec966..9066bee2f 100644 --- a/resources/ews/ewsclient/auth/ewspkeyauthjob.cpp +++ b/resources/ews/ewsclient/auth/ewspkeyauthjob.cpp @@ -1,187 +1,192 @@ /* Copyright (C) 2018 Krzysztof Nowicki This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ewspkeyauthjob.h" #include #include #include #include #include static const QMap stringToKnownCertInfoType = { {QStringLiteral("CN"), QCA::CommonName}, {QStringLiteral("L"), QCA::Locality}, {QStringLiteral("ST"), QCA::State}, {QStringLiteral("O"), QCA::Organization}, {QStringLiteral("OU"), QCA::OrganizationalUnit}, {QStringLiteral("C"), QCA::Country}, {QStringLiteral("emailAddress"), QCA::EmailLegacy} }; static QMultiMap parseCertSubjectInfo(const QString &info) { QMultiMap map; for (const auto &token : info.split(QLatin1Char(','), QString::SkipEmptyParts)) { const auto keyval = token.trimmed().split(QLatin1Char('=')); if (keyval.count() == 2) { if (stringToKnownCertInfoType.contains(keyval[0])) { map.insert(stringToKnownCertInfoType[keyval[0]], keyval[1]); } } } return map; } static QString escapeSlashes(const QString &str) { QString result = str; return result.replace(QLatin1Char('/'), QStringLiteral("\\/")); } EwsPKeyAuthJob::EwsPKeyAuthJob(const QUrl &pkeyUri, const QString &certFile, const QString &keyFile, const QString &keyPassword, QObject *parent) : EwsJob(parent) , mPKeyUri(pkeyUri) , mCertFile(certFile) , mKeyFile(keyFile) , mKeyPassword(keyPassword) , mNetworkAccessManager(new QNetworkAccessManager(this)) { } EwsPKeyAuthJob::~EwsPKeyAuthJob() { } void EwsPKeyAuthJob::start() { const QUrlQuery query(mPKeyUri); QMap params; for (const auto &it : query.queryItems()) { params[it.first.toLower()] = QUrl::fromPercentEncoding(it.second.toLatin1()); } if (params.contains(QLatin1String("submiturl")) && params.contains(QLatin1String("nonce")) && params.contains(QLatin1String("certauthorities")) && params.contains(QLatin1String("context")) && params.contains(QLatin1String("version"))) { const auto respToken = buildAuthResponse(params); if (!respToken.isEmpty()) { sendAuthRequest(respToken, QUrl(params[QStringLiteral("submiturl")]), params[QStringLiteral("context")]); } else { emitResult(); } } else { setErrorMsg(QStringLiteral("Missing one or more input parameters")); emitResult(); } } void EwsPKeyAuthJob::sendAuthRequest(const QByteArray &respToken, const QUrl &submitUrl, const QString &context) { QNetworkRequest req(submitUrl); req.setRawHeader( "Authorization", QStringLiteral("PKeyAuth AuthToken=\"%1\",Context=\"%2\",Version=\"1.0\"").arg(QString::fromLatin1(respToken), context).toLatin1()); mAuthReply.reset(mNetworkAccessManager->get(req)); connect(mAuthReply.data(), &QNetworkReply::finished, this, &EwsPKeyAuthJob::authRequestFinished); } void EwsPKeyAuthJob::authRequestFinished() { - if (mAuthReply->error() == QNetworkReply::NoError) { +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + const auto networkError = mAuthReply->error(); +#else + const auto networkError = mAuthReply->networkError(); +#endif + if (networkError == QNetworkReply::NoError) { mResultUri = mAuthReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); if (!mResultUri.isValid()) { setErrorMsg(QStringLiteral("Incorrect or missing redirect URI in PKeyAuth response")); } } else { setErrorMsg(QStringLiteral("Failed to process PKeyAuth request: %1").arg(mAuthReply->errorString())); } emitResult(); } QByteArray EwsPKeyAuthJob::buildAuthResponse(const QMap ¶ms) { QCA::Initializer init; if (!QCA::isSupported("cert")) { setErrorMsg(QStringLiteral("QCA was not built with PKI certificate support")); return QByteArray(); } if (params[QStringLiteral("version")] != QLatin1String("1.0")) { setErrorMsg(QStringLiteral("Unknown version of PKey Authentication: %1").arg(params[QStringLiteral("version")])); return QByteArray(); } const auto authoritiesInfo = parseCertSubjectInfo(params[QStringLiteral("certauthorities")]); QCA::ConvertResult importResult; const QCA::CertificateCollection certs = QCA::CertificateCollection::fromFlatTextFile(mCertFile, &importResult); if (importResult != QCA::ConvertGood) { setErrorMsg(QStringLiteral("Certificate import failed")); return QByteArray(); } QCA::Certificate cert; for (const auto &c : certs.certificates()) { if (c.issuerInfo() == authoritiesInfo) { cert = c; break; } } if (cert.isNull()) { setErrorMsg(QStringLiteral("No suitable certificate found")); return QByteArray(); } QCA::PrivateKey privateKey = QCA::PrivateKey::fromPEMFile(mKeyFile, mKeyPassword.toUtf8(), &importResult); if (importResult != QCA::ConvertGood) { setErrorMsg(QStringLiteral("Private key import failed")); return QByteArray(); } const QString certStr = escapeSlashes(QString::fromLatin1(cert.toDER().toBase64())); const QString header = QStringLiteral("{\"x5c\":[\"%1\"],\"typ\":\"JWT\",\"alg\":\"RS256\"}").arg(certStr); const QString payload = QStringLiteral("{\"nonce\":\"%1\",\"iat\":\"%2\",\"aud\":\"%3\"}") .arg(params[QStringLiteral("nonce")]).arg(QDateTime::currentSecsSinceEpoch()) .arg(escapeSlashes(params[QStringLiteral("submiturl")])); const auto headerB64 = header.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); const auto payloadB64 = payload.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); QCA::SecureArray data(headerB64 + '.' + payloadB64); QByteArray sig = privateKey.signMessage(data, QCA::EMSA3_SHA256, QCA::IEEE_1363); return headerB64 + '.' + payloadB64 + '.' + sig.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); } const QUrl &EwsPKeyAuthJob::resultUri() const { return mResultUri; } diff --git a/resources/kalarm/kalarmdir/kalarmdirresource.cpp b/resources/kalarm/kalarmdir/kalarmdirresource.cpp index d245e4da5..4f4384775 100644 --- a/resources/kalarm/kalarmdir/kalarmdirresource.cpp +++ b/resources/kalarm/kalarmdir/kalarmdirresource.cpp @@ -1,1176 +1,1176 @@ /* * kalarmdirresource.cpp - Akonadi directory resource for KAlarm * Program: kalarm * Copyright © 2011-2019 David Jarvie * Copyright (c) 2008 Tobias Koenig * Copyright (c) 2008 Bertjan Broeksema * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Library General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public * License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "kalarmdirresource.h" #include "kalarmresourcecommon.h" #include "autoqpointer.h" #include "kalarmdirsettingsadaptor.h" #include "settingsdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarmdirresource_debug.h" using namespace Akonadi; using namespace KCalendarCore; using namespace Akonadi_KAlarm_Dir_Resource; using KAlarmResourceCommon::errorMessage; static const char warningFile[] = "WARNING_README.txt"; #define DEBUG_DATA(func) \ qCDebug(KALARMDIRRESOURCE_LOG)<itemFetchScope().fetchFullPayload(); changeRecorder()->fetchCollection(true); connect(KDirWatch::self(), &KDirWatch::created, this, &KAlarmDirResource::fileCreated); connect(KDirWatch::self(), &KDirWatch::dirty, this, &KAlarmDirResource::fileChanged); connect(KDirWatch::self(), &KDirWatch::deleted, this, &KAlarmDirResource::fileDeleted); // Find the collection which this resource manages CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel); job->fetchScope().setResource(identifier()); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::collectionFetchResult); QTimer::singleShot(0, this, [this] { loadFiles(); }); } KAlarmDirResource::~KAlarmDirResource() { delete mSettings; } void KAlarmDirResource::aboutToQuit() { mSettings->save(); } /****************************************************************************** * Called when the collection fetch job completes. * Check the calendar files' compatibility statuses if pending. */ void KAlarmDirResource::collectionFetchResult(KJob *j) { qCDebug(KALARMDIRRESOURCE_LOG); if (j->error()) { qCritical() << "Error: collectionFetchResult: " << j->errorString(); } else { CollectionFetchJob *job = static_cast(j); Collection::List collections = job->collections(); int count = collections.count(); qCDebug(KALARMDIRRESOURCE_LOG) << "collectionFetchResult: count:" << count; if (!count) { qCritical() << "Cannot retrieve this resource's collection"; } else { if (count > 1) { qCritical() << "Multiple collections for this resource:" << count; } Collection &c(collections[0]); qCDebug(KALARMDIRRESOURCE_LOG) << "collectionFetchResult: id:" << c.id() << ", remote id:" << c.remoteId(); if (!mCollectionFetched) { bool recreate = mSettings->path().isEmpty(); if (!recreate) { // Remote ID could be path or URL, depending on which version // of Akonadi created it. const QString rid = c.remoteId(); const QUrl url = QUrl::fromLocalFile(mSettings->path()); if (!url.isLocalFile() || (rid != url.toLocalFile() && rid != url.url() && rid != url.toDisplayString())) { qCritical() << "Collection remote ID does not match settings: changing settings"; recreate = true; } } if (recreate) { // Initialising a resource which seems to have no stored // settings config file. Recreate the settings. static const Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem; qCDebug(KALARMDIRRESOURCE_LOG) << "collectionFetchResult: Recreating config for remote id:" << c.remoteId(); mSettings->setPath(c.remoteId()); mSettings->setDisplayName(c.name()); mSettings->setAlarmTypes(c.contentMimeTypes()); mSettings->setReadOnly((c.rights() & writableRights) != writableRights); mSettings->save(); } mCollectionId = c.id(); if (recreate) { // Load items from the backend files now that their location is known loadFiles(true); } // Set collection's format compatibility flag now that the collection // and its attributes have been fetched. KAlarmResourceCommon::setCollectionCompatibility(c, mCompatibility, mVersion); } } } mCollectionFetched = true; if (mWaitingToRetrieve) { mWaitingToRetrieve = false; retrieveCollections(); } } /****************************************************************************** */ void KAlarmDirResource::configure(WId windowId) { qCDebug(KALARMDIRRESOURCE_LOG) << "configure"; // Keep note of the old configuration settings QString path = mSettings->path(); QString name = mSettings->displayName(); bool readOnly = mSettings->readOnly(); QStringList types = mSettings->alarmTypes(); // Note: mSettings->monitorFiles() can't change here // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer dlg = new SettingsDialog(windowId, mSettings); if (dlg->exec()) { if (path.isEmpty()) { // Creating a new resource clearCache(); // this deletes any existing collection loadFiles(true); synchronizeCollectionTree(); } else if (mSettings->path() != path) { // Directory path change is not allowed for existing resources Q_EMIT configurationDialogRejected(); return; } else { bool modify = false; Collection c(mCollectionId); if (mSettings->alarmTypes() != types) { // Settings have changed which might affect the alarm configuration initializeDirectory(); // should only be needed for new resource, but just in case ... CalEvent::Types newTypes = CalEvent::types(mSettings->alarmTypes()); CalEvent::Types oldTypes = CalEvent::types(types); changeAlarmTypes(~newTypes & oldTypes); c.setContentMimeTypes(mSettings->alarmTypes()); modify = true; } if (mSettings->readOnly() != readOnly || mSettings->displayName() != name) { // Need to change the collection's rights or name c.setRemoteId(directoryName()); setNameRights(c); modify = true; } if (modify) { // Update the Akonadi server with the changes CollectionModifyJob *job = new CollectionModifyJob(c); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone); } } Q_EMIT configurationDialogAccepted(); } else { Q_EMIT configurationDialogRejected(); } } /****************************************************************************** * Add/remove events to ensure that they match the changed alarm types for the * resource. */ void KAlarmDirResource::changeAlarmTypes(CalEvent::Types removed) { DEBUG_DATA("changeAlarmTypes:"); const QString dirPath = directoryName(); qCDebug(KALARMDIRRESOURCE_LOG) << "changeAlarmTypes:" << dirPath; const QDir dir(dirPath); // Read and parse each file in turn QDirIterator it(dir); while (it.hasNext()) { it.next(); int removeIfInvalid = 0; QString fileEventId; const QString file = it.fileName(); if (!isFileValid(file)) { continue; } QHash::iterator fit = mFileEventIds.find(file); if (fit != mFileEventIds.end()) { // The file is in the existing file list fileEventId = fit.value(); QHash::ConstIterator it = mEvents.constFind(fileEventId); if (it != mEvents.constEnd()) { // And its event is in the existing events list const EventFile &data = it.value(); if (data.files[0] == file) { // It's the file for a used event if (data.event.category() & removed) { // The event's type is no longer wanted, so remove it deleteItem(data.event); removeEvent(data.event.id(), false); } continue; } else { // The file's event is not currently used - load the // file and use its event if appropriate. removeIfInvalid = 0x03; // remove from mEvents and mFileEventIds } } else { // The file's event isn't in the list of current valid // events - this shouldn't ever happen removeIfInvalid = 0x01; // remove from mFileEventIds } } // Load the file and use its event if appropriate. const QString path = filePath(file); if (QFileInfo(path).isFile()) { if (createItemAndIndex(path, file)) { continue; } } // The event wasn't wanted, so remove from lists if (removeIfInvalid & 0x01) { mFileEventIds.erase(fit); } if (removeIfInvalid & 0x02) { removeEventFile(fileEventId, file); } } DEBUG_DATA("changeAlarmTypes exit:"); setCompatibility(); } /****************************************************************************** * Called when the resource settings have changed. * Update the display name if it has changed. * Stop monitoring the directory if 'monitorFiles' is now false. * Update the storage format if UpdateStorageFormat setting = true. * NOTE: no provision is made for changes to the directory path, since this is * not permitted (would need remote ID changed, plus other complications). */ void KAlarmDirResource::settingsChanged() { qCDebug(KALARMDIRRESOURCE_LOG) << "settingsChanged"; const QString display = mSettings->displayName(); if (display != name()) { setName(display); } const QString dirPath = mSettings->path(); if (!dirPath.isEmpty()) { const bool monitoring = KDirWatch::self()->contains(dirPath); if (monitoring && !mSettings->monitorFiles()) { KDirWatch::self()->removeDir(dirPath); } else if (!monitoring && mSettings->monitorFiles()) { KDirWatch::self()->addDir(dirPath, KDirWatch::WatchFiles); } #if 0 if (mSettings->monitorFiles() && !monitor) { // Settings have changed which might affect the alarm configuration qCDebug(KALARMDIRRESOURCE_LOG) << "Monitored changed"; loadFiles(true); // synchronizeCollectionTree(); } #endif } if (mSettings->updateStorageFormat()) { // This is a flag to request that the backend calendar storage format should // be updated to the current KAlarm format. KACalendar::Compat okCompat(KACalendar::Current | KACalendar::Convertible); if (mCompatibility & ~okCompat) { qCWarning(KALARMDIRRESOURCE_LOG) << "Either incompatible storage format or nothing to update"; } else if (mSettings->readOnly()) { qCWarning(KALARMDIRRESOURCE_LOG) << "Cannot update storage format for a read-only resource"; } else { // Update the backend storage format to the current KAlarm format bool ok = true; for (QHash::iterator it = mEvents.begin(); it != mEvents.end(); ++it) { KAEvent &event = it.value().event; if (event.compatibility() == KACalendar::Convertible) { if (writeToFile(event)) { event.setCompatibility(KACalendar::Current); } else { qCWarning(KALARMDIRRESOURCE_LOG) << "Error updating storage format for event id" << event.id(); ok = false; } } } if (ok) { mCompatibility = KACalendar::Current; mVersion = KACalendar::CurrentFormat; const Collection c(mCollectionId); if (c.isValid()) { KAlarmResourceCommon::setCollectionCompatibility(c, mCompatibility, mVersion); } } } mSettings->setUpdateStorageFormat(false); mSettings->save(); } } /****************************************************************************** * Load and parse data from each file in the directory. * The events are cached in mEvents. */ bool KAlarmDirResource::loadFiles(bool sync) { const QString dirPath = directoryName(); if (dirPath.isEmpty()) { return false; } qCDebug(KALARMDIRRESOURCE_LOG) << "loadFiles:" << dirPath; const QDir dir(dirPath); // Create the directory if it doesn't exist. // This should only be needed for a new resource, but just in case ... initializeDirectory(); mEvents.clear(); mFileEventIds.clear(); // Set the resource display name to the configured name, else the directory // name, if not already set. QString display = mSettings->displayName(); if (display.isEmpty() && (name().isEmpty() || name() == identifier())) { display = dir.dirName(); } if (!display.isEmpty()) { setName(display); } // Read and parse each file in turn QDirIterator it(dir); while (it.hasNext()) { it.next(); const QString file = it.fileName(); if (isFileValid(file)) { const QString path = filePath(file); if (QFileInfo(path).isFile()) { const KAEvent event = loadFile(path, file); if (event.isValid()) { addEventFile(event, file); mFileEventIds.insert(file, event.id()); } } } } DEBUG_DATA("loadFiles:"); setCompatibility(false); // don't write compatibility - no collection exists yet if (mSettings->monitorFiles()) { // Monitor the directory for changes to the files if (!KDirWatch::self()->contains(dirPath)) { KDirWatch::self()->addDir(dirPath, KDirWatch::WatchFiles); } } if (sync) { // Ensure the Akonadi server is updated with the current list of events synchronize(); } Q_EMIT status(Idle); return true; } /****************************************************************************** * Load and parse data a single file in the directory. * 'path' is the full path of 'file'. * 'file' should not contain any directory component. */ KAEvent KAlarmDirResource::loadFile(const QString &path, const QString &file) { qCDebug(KALARMDIRRESOURCE_LOG) << "loadFile:" << path; MemoryCalendar::Ptr calendar(new MemoryCalendar(QTimeZone::utc())); FileStorage::Ptr fileStorage(new FileStorage(calendar, path, new ICalFormat())); if (!fileStorage->load()) { // Don't output an error in the case of the creation of a temporary // file which triggered fileChanged() but no longer exists. if (QFileInfo::exists(path)) { qCWarning(KALARMDIRRESOURCE_LOG) << "Error loading" << path; } return KAEvent(); } const Event::List events = calendar->events(); if (events.isEmpty()) { qCDebug(KALARMDIRRESOURCE_LOG) << "Empty calendar in file" << path; return KAEvent(); } if (events.count() > 1) { qCWarning(KALARMDIRRESOURCE_LOG) << "Deleting" << events.count() - 1 << "excess events found in file" << path; for (int i = 1; i < events.count(); ++i) { calendar->deleteEvent(events[i]); } } const Event::Ptr kcalEvent(events[0]); if (kcalEvent->uid() != file) { qCWarning(KALARMDIRRESOURCE_LOG) << "File" << path << ": event id differs from file name"; } if (kcalEvent->alarms().isEmpty()) { qCWarning(KALARMDIRRESOURCE_LOG) << "File" << path << ": event contains no alarms"; return KAEvent(); } // Convert event in memory to current KAlarm format if possible int version; KACalendar::Compat compat = KAlarmResourceCommon::getCompatibility(fileStorage, version); KAEvent event(kcalEvent); const QString mime = CalEvent::mimeType(event.category()); if (mime.isEmpty()) { qCWarning(KALARMDIRRESOURCE_LOG) << "loadFile: KAEvent has no usable alarms:" << event.id(); return KAEvent(); } if (!mSettings->alarmTypes().contains(mime)) { qCWarning(KALARMDIRRESOURCE_LOG) << "loadFile: KAEvent has wrong alarm type for resource:" << mime; return KAEvent(); } event.setCompatibility(compat); return event; } /****************************************************************************** * After a file/event has been removed, load the next file in the list for the * event ID. * Reply = new event, or invalid if none. */ KAEvent KAlarmDirResource::loadNextFile(const QString &eventId, const QString &file) { QString nextFile = file; while (!nextFile.isEmpty()) { // There is another file with the same ID - load it const KAEvent event = loadFile(filePath(nextFile), nextFile); if (event.isValid()) { addEventFile(event, nextFile); mFileEventIds.insert(nextFile, event.id()); return event; } mFileEventIds.remove(nextFile); nextFile = removeEventFile(eventId, nextFile); } return KAEvent(); } /****************************************************************************** * Retrieve an event from the calendar, whose uid and Akonadi id are given by * 'item' (item.remoteId() and item.id() respectively). * Set the event into a new item's payload, and signal its retrieval by calling * itemRetrieved(newitem). */ bool KAlarmDirResource::retrieveItem(const Akonadi::Item &item, const QSet &) { const QString rid = item.remoteId(); QHash::ConstIterator it = mEvents.constFind(rid); if (it == mEvents.constEnd()) { qCWarning(KALARMDIRRESOURCE_LOG) << "retrieveItem: Event not found:" << rid; Q_EMIT error(errorMessage(KAlarmResourceCommon::UidNotFound, rid)); return false; } KAEvent event(it.value().event); const Item newItem = KAlarmResourceCommon::retrieveItem(item, event); itemRetrieved(newItem); return true; } /****************************************************************************** * Called when an item has been added to the collection. * Store the event in a file, and set its Akonadi remote ID to the KAEvent's UID. */ void KAlarmDirResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &) { qCDebug(KALARMDIRRESOURCE_LOG) << "itemAdded:" << item.id(); if (cancelIfReadOnly()) { return; } KAEvent event; if (item.hasPayload()) { event = item.payload(); } if (!event.isValid()) { changeProcessed(); return; } event.setCompatibility(KACalendar::Current); setCompatibility(); if (!writeToFile(event)) { return; } addEventFile(event, event.id()); Item newItem(item); newItem.setRemoteId(event.id()); // scheduleWrite(); //???? is this needed? changeCommitted(newItem); } /****************************************************************************** * Called when an item has been changed. * Store the changed event in a file. */ void KAlarmDirResource::itemChanged(const Akonadi::Item &item, const QSet &) { qCDebug(KALARMDIRRESOURCE_LOG) << "itemChanged:" << item.id() << ", remote ID:" << item.remoteId(); if (cancelIfReadOnly()) { return; } QHash::iterator it = mEvents.find(item.remoteId()); if (it != mEvents.end()) { if (it.value().event.isReadOnly()) { qCWarning(KALARMDIRRESOURCE_LOG) << "Event is read only:" << item.remoteId(); cancelTask(errorMessage(KAlarmResourceCommon::EventReadOnly, item.remoteId())); return; } if (it.value().event.compatibility() != KACalendar::Current) { qCWarning(KALARMDIRRESOURCE_LOG) << "Event not in current format:" << item.remoteId(); cancelTask(errorMessage(KAlarmResourceCommon::EventNotCurrentFormat, item.remoteId())); return; } } KAEvent event; if (item.hasPayload()) { event = item.payload(); } if (!event.isValid()) { changeProcessed(); return; } #if 0 QString errorMsg; KAEvent event = KAlarmResourceCommon::checkItemChanged(item, errorMsg); if (!event.isValid()) { if (errorMsg.isEmpty()) { changeProcessed(); } else { cancelTask(errorMsg); } return; } #endif event.setCompatibility(KACalendar::Current); if (mCompatibility != KACalendar::Current) { setCompatibility(); } if (!writeToFile(event)) { return; } it.value().event = event; changeCommitted(item); } /****************************************************************************** * Called when an item has been deleted. * Delete the item's file. */ void KAlarmDirResource::itemRemoved(const Akonadi::Item &item) { qCDebug(KALARMDIRRESOURCE_LOG) << "itemRemoved:" << item.id(); if (cancelIfReadOnly()) { return; } removeEvent(item.remoteId(), true); setCompatibility(); changeProcessed(); } /****************************************************************************** * Remove an event from the indexes, and optionally delete its file. */ void KAlarmDirResource::removeEvent(const QString &eventId, bool deleteFile) { QString file = eventId; QString nextFile; QHash::iterator it = mEvents.find(eventId); if (it != mEvents.end()) { file = it.value().files[0]; nextFile = removeEventFile(eventId, file); mFileEventIds.remove(file); DEBUG_DATA("removeEvent:"); } if (deleteFile) { QFile::remove(filePath(file)); } loadNextFile(eventId, nextFile); // load any other file with the same event ID } /****************************************************************************** * If the resource is read-only, cancel the task andQ_EMIT an error. * Reply = true if cancelled. */ bool KAlarmDirResource::cancelIfReadOnly() { if (mSettings->readOnly()) { qCWarning(KALARMDIRRESOURCE_LOG) << "Calendar is read-only:" << directoryName(); Q_EMIT error(i18nc("@info", "Trying to write to a read-only calendar: '%1'", directoryName())); cancelTask(); return true; } return false; } /****************************************************************************** * Write an event to a file. The file name is the event's id. */ bool KAlarmDirResource::writeToFile(const KAEvent &event) { Event::Ptr kcalEvent(new Event); event.updateKCalEvent(kcalEvent, KAEvent::UID_SET); MemoryCalendar::Ptr calendar(new MemoryCalendar(QTimeZone::utc())); KACalendar::setKAlarmVersion(calendar); // set the KAlarm custom property if (!calendar->addIncidence(kcalEvent)) { qCritical() << "Error adding event with id" << event.id(); Q_EMIT error(errorMessage(KAlarmResourceCommon::CalendarAdd, event.id())); cancelTask(); return false; } mChangedFiles += event.id(); // suppress KDirWatch processing for this write const QString path = filePath(event.id()); qCDebug(KALARMDIRRESOURCE_LOG) << "writeToFile:" << event.id() << " File:" << path; FileStorage::Ptr fileStorage(new FileStorage(calendar, path, new ICalFormat())); if (!fileStorage->save()) { Q_EMIT error(i18nc("@info", "Failed to save event file: %1", path)); cancelTask(); return false; } return true; } /****************************************************************************** * Create the resource's collection. */ void KAlarmDirResource::retrieveCollections() { QString rid = mSettings->path(); if (!mCollectionFetched && rid.isEmpty()) { // The resource config seems to be missing. Execute this function // once the collection config has been set up. mWaitingToRetrieve = true; return; } qCDebug(KALARMDIRRESOURCE_LOG) << "retrieveCollections"; Collection c; c.setParentCollection(Collection::root()); c.setRemoteId(rid); c.setContentMimeTypes(mSettings->alarmTypes()); setNameRights(c); // Don't update CollectionAttribute here, since it hasn't yet been fetched // from Akonadi database. Collection::List list; list << c; collectionsRetrieved(list); } /****************************************************************************** * Set the collection's name and rights. * It is the caller's responsibility to notify the Akonadi server. */ void KAlarmDirResource::setNameRights(Collection &c) { qCDebug(KALARMDIRRESOURCE_LOG) << "setNameRights"; const QString display = mSettings->displayName(); c.setName(display.isEmpty() ? name() : display); EntityDisplayAttribute *attr = c.attribute(Collection::AddIfMissing); attr->setDisplayName(name()); attr->setIconName(QStringLiteral("kalarm")); if (mSettings->readOnly()) { c.setRights(Collection::CanChangeCollection); } else { Collection::Rights rights = Collection::ReadOnly; rights |= Collection::CanChangeItem; rights |= Collection::CanCreateItem; rights |= Collection::CanDeleteItem; rights |= Collection::CanChangeCollection; c.setRights(rights); } qCDebug(KALARMDIRRESOURCE_LOG) << "setNameRights: end"; } /****************************************************************************** * Retrieve all events from the directory, and set each into a new item's * payload. Items are identified by their remote IDs. The Akonadi ID is not * used. * Signal the retrieval of the items by calling itemsRetrieved(items), which * updates Akonadi with any changes to the items. itemsRetrieved() compares * the new and old items, matching them on the remoteId(). If the flags or * payload have changed, or the Item has any new Attributes, the Akonadi * storage is updated. */ void KAlarmDirResource::retrieveItems(const Akonadi::Collection &collection) { mCollectionId = collection.id(); // note the one and only collection for this resource qCDebug(KALARMDIRRESOURCE_LOG) << "retrieveItems: collection" << mCollectionId; // Set the collection's compatibility status KAlarmResourceCommon::setCollectionCompatibility(collection, mCompatibility, mVersion); // Fetch the list of valid mime types const QStringList mimeTypes = mSettings->alarmTypes(); // Retrieve events Item::List items; foreach (const EventFile &data, mEvents) { const KAEvent &event = data.event; const QString mime = CalEvent::mimeType(event.category()); if (mime.isEmpty()) { qCWarning(KALARMDIRRESOURCE_LOG) << "retrieveItems: KAEvent has no alarms:" << event.id(); continue; // event has no usable alarms } if (!mimeTypes.contains(mime)) { continue; // restrict alarms returned to the defined types } Item item(mime); item.setRemoteId(event.id()); item.setPayload(event); items.append(item); } itemsRetrieved(items); } /****************************************************************************** * Called when the collection has been changed. * Set its display name if that has changed. */ void KAlarmDirResource::collectionChanged(const Akonadi::Collection &collection) { qCDebug(KALARMDIRRESOURCE_LOG) << "collectionChanged"; // If the collection has a new display name, set the resource's display // name the same, and save to the settings. const QString newName = collection.displayName(); if (!newName.isEmpty() && newName != name()) { setName(newName); } if (newName != mSettings->displayName()) { mSettings->setDisplayName(newName); mSettings->save(); } changeCommitted(collection); } /****************************************************************************** * Called when a file has been created in the directory. */ void KAlarmDirResource::fileCreated(const QString &path) { qCDebug(KALARMDIRRESOURCE_LOG) << "fileCreated:" << path; if (path == directoryName()) { // The directory has been created. Load all files in it, and // tell the Akonadi server to create an Item for each event. loadFiles(true); foreach (const EventFile &data, mEvents) { createItem(data.event); } } else { const QString file = fileName(path); int i = mChangedFiles.indexOf(file); if (i >= 0) { mChangedFiles.removeAt(i); // the file was updated by this resource } else if (isFileValid(file)) { if (createItemAndIndex(path, file)) { setCompatibility(); } DEBUG_DATA("fileCreated:"); } } } /****************************************************************************** * Called when a file has changed in the directory. */ void KAlarmDirResource::fileChanged(const QString &path) { if (path != directoryName()) { qCDebug(KALARMDIRRESOURCE_LOG) << "fileChanged:" << path; const QString file = fileName(path); int i = mChangedFiles.indexOf(file); if (i >= 0) { mChangedFiles.removeAt(i); // the file was updated by this resource } else if (isFileValid(file)) { QString nextFile, oldId; KAEvent oldEvent; const KAEvent event = loadFile(path, file); // Get the file's old event ID QHash::iterator fit = mFileEventIds.find(file); if (fit != mFileEventIds.end()) { oldId = fit.value(); if (event.id() != oldId) { // The file's event ID has changed - remove the old event nextFile = removeEventFile(oldId, file, &oldEvent); if (event.isValid()) { fit.value() = event.id(); } else { mFileEventIds.erase(fit); } } } else { // The file didn't contain an event before. if (event.isValid()) { // Save details of the new event. mFileEventIds.insert(file, event.id()); } else { // The file still doesn't contain a recognised event. return; } } addEventFile(event, file); KAEvent e = loadNextFile(oldId, nextFile); // load any other file with the same event ID setCompatibility(); // Tell the Akonadi server to amend the Item for the event if (event.id() != oldId) { if (e.isValid()) { modifyItem(e); } else { deleteItem(oldEvent); } createItem(event); // create a new Item for the new event ID } else { modifyItem(event); } DEBUG_DATA("fileChanged:"); } } } /****************************************************************************** * Called when a file has been deleted in the directory. */ void KAlarmDirResource::fileDeleted(const QString &path) { qCDebug(KALARMDIRRESOURCE_LOG) << "fileDeleted:" << path; if (path == directoryName()) { // The directory has been deleted mEvents.clear(); mFileEventIds.clear(); // Tell the Akonadi server to delete all Items in the collection Collection c(mCollectionId); ItemDeleteJob *job = new ItemDeleteJob(c); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone); } else { // A single file has been deleted const QString file = fileName(path); if (isFileValid(file)) { QHash::iterator fit = mFileEventIds.find(file); if (fit != mFileEventIds.end()) { QString eventId = fit.value(); KAEvent event; QString nextFile = removeEventFile(eventId, file, &event); mFileEventIds.erase(fit); KAEvent e = loadNextFile(eventId, nextFile); // load any other file with the same event ID setCompatibility(); if (e.isValid()) { // Tell the Akonadi server to amend the Item for the event modifyItem(e); } else { // Tell the Akonadi server to delete the Item for the event deleteItem(event); } DEBUG_DATA("fileDeleted:"); } } } } /****************************************************************************** * Tell the Akonadi server to create an Item for a given file's event, and add * it to the indexes. */ bool KAlarmDirResource::createItemAndIndex(const QString &path, const QString &file) { const KAEvent event = loadFile(path, file); if (event.isValid()) { // Tell the Akonadi server to create an Item for the event if (createItem(event)) { addEventFile(event, file); mFileEventIds.insert(file, event.id()); return true; } } return false; } /****************************************************************************** * Tell the Akonadi server to create an Item for a given event. */ bool KAlarmDirResource::createItem(const KAEvent &event) { Item item; if (!KAlarmCal::setItemPayload(item, event, mSettings->alarmTypes())) { qCWarning(KALARMDIRRESOURCE_LOG) << "createItem: Invalid mime type for collection"; return false; } Collection c(mCollectionId); item.setParentCollection(c); item.setRemoteId(event.id()); ItemCreateJob *job = new ItemCreateJob(item, c); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone); return true; } /****************************************************************************** * Tell the Akonadi server to amend the Item for a given event. */ bool KAlarmDirResource::modifyItem(const KAEvent &event) { Item item; if (!KAlarmCal::setItemPayload(item, event, mSettings->alarmTypes())) { qCWarning(KALARMDIRRESOURCE_LOG) << "modifyItem: Invalid mime type for collection"; return false; } Collection c(mCollectionId); item.setParentCollection(c); item.setRemoteId(event.id()); ItemModifyJob *job = new ItemModifyJob(item); job->disableRevisionCheck(); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone); return true; } /****************************************************************************** * Tell the Akonadi server to delete the Item for a given event. */ void KAlarmDirResource::deleteItem(const KAEvent &event) { Item item(CalEvent::mimeType(event.category())); Collection c(mCollectionId); item.setParentCollection(c); item.setRemoteId(event.id()); ItemDeleteJob *job = new ItemDeleteJob(item); connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone); } /****************************************************************************** * Called when a collection or item job has completed. * Checks for any error. */ void KAlarmDirResource::jobDone(KJob *j) { if (j->error()) { qCritical() << j->metaObject()->className() << "error:" << j->errorString(); } } /****************************************************************************** * Create the directory if it doesn't already exist, and ensure that it * contains a WARNING_README.txt file. */ void KAlarmDirResource::initializeDirectory() const { const QDir dir(directoryName()); const QString dirPath = dir.absolutePath(); qCDebug(KALARMDIRRESOURCE_LOG) << "initializeDirectory" << dirPath; // If folder does not exist, create it if (!dir.exists()) { qCDebug(KALARMDIRRESOURCE_LOG) << "initializeDirectory: creating" << dirPath; QDir::root().mkpath(dirPath); } // Check whether warning file is in place... QFile file(dirPath + QDir::separator() + QLatin1String(warningFile)); if (!file.exists()) { // ... if not, create it file.open(QIODevice::WriteOnly); file.write("Important Warning!!!\n" "Do not create or copy items inside this folder manually:\n" "they are managed by the Akonadi framework!\n"); file.close(); } } QString KAlarmDirResource::directoryName() const { return mSettings->path(); } /****************************************************************************** * Return the full path of an event file. * 'file' should not contain any directory component. */ QString KAlarmDirResource::filePath(const QString &file) const { return mSettings->path() + QDir::separator() + file; } /****************************************************************************** * Strip the directory path from a file name. */ QString KAlarmDirResource::fileName(const QString &path) const { const QFileInfo fi(path); if (fi.isDir() || fi.isBundle()) { return QString(); } if (fi.path() == mSettings->path()) { return fi.fileName(); } return path; } /****************************************************************************** * Evaluate the version compatibility status of the calendar. This is the OR of * the statuses of the individual events. */ void KAlarmDirResource::setCompatibility(bool writeAttr) { static const KACalendar::Compat AllCompat(KACalendar::Current | KACalendar::Convertible | KACalendar::Incompatible); const KACalendar::Compat oldCompatibility = mCompatibility; const int oldVersion = mVersion; if (mEvents.isEmpty()) { mCompatibility = KACalendar::Current; } else { mCompatibility = KACalendar::Unknown; foreach (const EventFile &data, mEvents) { const KAEvent &event = data.event; mCompatibility |= event.compatibility(); if ((mCompatibility & AllCompat) == AllCompat) { break; } } } mVersion = (mCompatibility == KACalendar::Current) ? KACalendar::CurrentFormat : KACalendar::MixedFormat; if (writeAttr && (mCompatibility != oldCompatibility || mVersion != oldVersion)) { const Collection c(mCollectionId); if (c.isValid()) { KAlarmResourceCommon::setCollectionCompatibility(c, mCompatibility, mVersion); } } } /****************************************************************************** * Add an event/file combination to the mEvents map. */ void KAlarmDirResource::addEventFile(const KAEvent &event, const QString &file) { if (event.isValid()) { QHash::iterator it = mEvents.find(event.id()); if (it != mEvents.end()) { EventFile &data = it.value(); data.event = event; data.files.removeAll(file); // in case it isn't the first file data.files.prepend(file); } else { mEvents.insert(event.id(), EventFile(event, QStringList(file))); } } } /****************************************************************************** * Remove an event ID/file combination from the mEvents map. * Reply = next file with the same event ID. */ QString KAlarmDirResource::removeEventFile(const QString &eventId, const QString &file, KAEvent *event) { QHash::iterator it = mEvents.find(eventId); if (it != mEvents.end()) { if (event) { *event = it.value().event; } it.value().files.removeAll(file); if (!it.value().files.isEmpty()) { return it.value().files[0]; } mEvents.erase(it); } else if (event) { *event = KAEvent(); } return QString(); } /****************************************************************************** * Check whether a file is to be ignored. * Reply = false if file is to be ignored. */ bool KAlarmDirResource::isFileValid(const QString &file) const { return !file.isEmpty() && !file.startsWith(QLatin1Char('.')) && !file.endsWith(QLatin1Char('~')) && file != QLatin1String(warningFile) && QFileInfo::exists(filePath(file)); // a temporary file may no longer exist } AKONADI_RESOURCE_MAIN(KAlarmDirResource) diff --git a/resources/tomboynotes/o2/o1.cpp b/resources/tomboynotes/o2/o1.cpp index 43ccca51f..a2debde97 100644 --- a/resources/tomboynotes/o2/o1.cpp +++ b/resources/tomboynotes/o2/o1.cpp @@ -1,414 +1,424 @@ #include #include #include #include #include #include "debug.h" #if QT_VERSION >= 0x050000 #include #endif #if QT_VERSION >= 0x050100 #include #endif #include "o2/o1.h" #include "o2/o2replyserver.h" #include "o2/o0globals.h" #include "o2/o0settingsstore.h" O1::O1(QObject *parent) : O0BaseAuth(parent) { setSignatureMethod(QLatin1String(O2_SIGNATURE_TYPE_HMAC_SHA1)); manager_ = new QNetworkAccessManager(this); replyServer_ = new O2ReplyServer(this); qRegisterMetaType("QNetworkReply::NetworkError"); connect(replyServer_, &O2ReplyServer::verificationReceived, this, &O1::onVerificationReceived); setCallbackUrl(QLatin1String(O2_CALLBACK_URL)); } QUrl O1::requestTokenUrl() const { return requestTokenUrl_; } void O1::setRequestTokenUrl(const QUrl &v) { requestTokenUrl_ = v; Q_EMIT requestTokenUrlChanged(); } QList O1::requestParameters() { return requestParameters_; } void O1::setRequestParameters(const QList &v) { requestParameters_ = v; } QString O1::callbackUrl() const { return callbackUrl_; } void O1::setCallbackUrl(const QString &v) { callbackUrl_ = v; } QUrl O1::authorizeUrl() const { return authorizeUrl_; } void O1::setAuthorizeUrl(const QUrl &value) { authorizeUrl_ = value; Q_EMIT authorizeUrlChanged(); } QUrl O1::accessTokenUrl() const { return accessTokenUrl_; } void O1::setAccessTokenUrl(const QUrl &value) { accessTokenUrl_ = value; Q_EMIT accessTokenUrlChanged(); } QString O1::signatureMethod() { return signatureMethod_; } void O1::setSignatureMethod(const QString &value) { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::setSignatureMethod: " << value; signatureMethod_ = value; } void O1::unlink() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::unlink"; setLinked(false); setToken(QString()); setTokenSecret(QString()); setExtraTokens(QVariantMap()); Q_EMIT linkingSucceeded(); } #if QT_VERSION < 0x050100 /// Calculate the HMAC variant of SHA1 hash. /// @author http://qt-project.org/wiki/HMAC-SHA1. /// @copyright Creative Commons Attribution-ShareAlike 2.5 Generic. static QByteArray hmacSha1(QByteArray key, QByteArray baseString) { int blockSize = 64; if (key.length() > blockSize) { key = QCryptographicHash::hash(key, QCryptographicHash::Sha1); } QByteArray innerPadding(blockSize, char(0x36)); QByteArray outerPadding(blockSize, char(0x5c)); for (int i = 0; i < key.length(); i++) { innerPadding[i] = innerPadding[i] ^ key.at(i); outerPadding[i] = outerPadding[i] ^ key.at(i); } QByteArray total = outerPadding; QByteArray part = innerPadding; part.append(baseString); total.append(QCryptographicHash::hash(part, QCryptographicHash::Sha1)); QByteArray hashed = QCryptographicHash::hash(total, QCryptographicHash::Sha1); return hashed.toBase64(); } #endif /// Get HTTP operation name. static QString getOperationName(QNetworkAccessManager::Operation op) { switch (op) { case QNetworkAccessManager::GetOperation: return QStringLiteral("GET"); case QNetworkAccessManager::PostOperation: return QStringLiteral("POST"); case QNetworkAccessManager::PutOperation: return QStringLiteral("PUT"); case QNetworkAccessManager::DeleteOperation: return QStringLiteral("DEL"); default: return QString(); } } /// Build a concatenated/percent-encoded string from a list of headers. QByteArray O1::encodeHeaders(const QList &headers) { return QUrl::toPercentEncoding(QString::fromLatin1(createQueryParameters(headers))); } /// Build a base string for signing. QByteArray O1::getRequestBase(const QList &oauthParams, const QList &otherParams, const QUrl &url, QNetworkAccessManager::Operation op) { QByteArray base; // Initialize base string with the operation name (e.g. "GET") and the base URL base.append(getOperationName(op).toUtf8() + "&"); base.append(QUrl::toPercentEncoding(url.toString(QUrl::RemoveQuery)) + "&"); // Append a sorted+encoded list of all request parameters to the base string QList headers(oauthParams); headers.append(otherParams); std::sort(headers.begin(), headers.end()); base.append(encodeHeaders(headers)); return base; } QByteArray O1::sign(const QList &oauthParams, const QList &otherParams, const QUrl &url, QNetworkAccessManager::Operation op, const QString &consumerSecret, const QString &tokenSecret) { QByteArray baseString = getRequestBase(oauthParams, otherParams, url, op); QByteArray secret = QUrl::toPercentEncoding(consumerSecret) + "&" + QUrl::toPercentEncoding(tokenSecret); #if QT_VERSION >= 0x050100 return QMessageAuthenticationCode::hash(baseString, secret, QCryptographicHash::Sha1).toBase64(); #else return hmacSha1(secret, baseString); #endif } QByteArray O1::buildAuthorizationHeader(const QList &oauthParams) { bool first = true; QByteArray ret("OAuth "); QList headers(oauthParams); std::sort(headers.begin(), headers.end()); for (const O0RequestParameter &h : qAsConst(headers)) { if (first) { first = false; } else { ret.append(","); } ret.append(h.name); ret.append("=\""); ret.append(QUrl::toPercentEncoding(QString::fromLatin1(h.value))); ret.append("\""); } return ret; } QByteArray O1::generateSignature(const QList &headers, const QNetworkRequest &req, const QList &signingParameters, QNetworkAccessManager::Operation operation) { QByteArray signature; if (signatureMethod() == QLatin1String(O2_SIGNATURE_TYPE_HMAC_SHA1)) { signature = sign(headers, signingParameters, req.url(), operation, clientSecret(), tokenSecret()); } else if (signatureMethod() == QLatin1String(O2_SIGNATURE_TYPE_PLAINTEXT)) { signature = clientSecret().toLatin1() + "&" + tokenSecret().toLatin1(); } return signature; } void O1::link() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::link"; if (linked()) { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::link: Linked already"; Q_EMIT linkingSucceeded(); return; } setLinked(false); setToken(QString()); setTokenSecret(QString()); setExtraTokens(QVariantMap()); // Start reply server replyServer_->listen(QHostAddress::Any, localPort()); // Get any query parameters for the request QUrlQuery requestData; O0RequestParameter param("", ""); foreach (param, requestParameters()) { requestData.addQueryItem(QString::fromLatin1(param.name), QString::fromLatin1(QUrl::toPercentEncoding(QString::fromLatin1(param.value)))); } // Get the request url and add parameters QUrl requestUrl = requestTokenUrl(); requestUrl.setQuery(requestData); // Create request QNetworkRequest request(requestUrl); // Create initial token request QList headers; headers.append(O0RequestParameter(O2_OAUTH_CALLBACK, callbackUrl().arg(replyServer_->serverPort()).toLatin1())); headers.append(O0RequestParameter(O2_OAUTH_CONSUMER_KEY, clientId().toLatin1())); headers.append(O0RequestParameter(O2_OAUTH_NONCE, nonce())); headers.append(O0RequestParameter(O2_OAUTH_TIMESTAMP, QString::number(QDateTime::currentDateTimeUtc().toSecsSinceEpoch()).toLatin1())); headers.append(O0RequestParameter(O2_OAUTH_VERSION, "1.0")); headers.append(O0RequestParameter(O2_OAUTH_SIGNATURE_METHOD, signatureMethod().toLatin1())); headers.append(O0RequestParameter(O2_OAUTH_SIGNATURE, generateSignature(headers, request, requestParameters(), QNetworkAccessManager::PostOperation))); // Clear request token requestToken_.clear(); requestTokenSecret_.clear(); // Post request request.setRawHeader(O2_HTTP_AUTHORIZATION_HEADER, buildAuthorizationHeader(headers)); request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(O2_MIME_TYPE_XFORM)); QNetworkReply *reply = manager_->post(request, QByteArray()); connect(reply, QOverload::of(&QNetworkReply::error), this, &O1::onTokenRequestError); connect(reply, &QNetworkReply::finished, this, &O1::onTokenRequestFinished); } void O1::onTokenRequestError(QNetworkReply::NetworkError error) { QNetworkReply *reply = qobject_cast(sender()); qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenRequestError:" << (int)error << reply->errorString() << reply->readAll(); Q_EMIT linkingFailed(); } void O1::onTokenRequestFinished() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenRequestFinished"; QNetworkReply *reply = qobject_cast(sender()); - reply->deleteLater(); - if (reply->error() != QNetworkReply::NoError) { + reply->deleteLater(); +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + const auto networkError = reply->error(); +#else + const auto networkError = reply->networkError(); +#endif + if (networkError != QNetworkReply::NoError) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenRequestFinished: " << reply->errorString(); return; } // Get request token and secret QByteArray data = reply->readAll(); QMap response = parseResponse(data); requestToken_ = response.value(QLatin1String(O2_OAUTH_TOKEN), QString()); requestTokenSecret_ = response.value(QLatin1String(O2_OAUTH_TOKEN_SECRET), QString()); setToken(requestToken_); setTokenSecret(requestTokenSecret_); // Checking for "oauth_callback_confirmed" is present and set to true QString oAuthCbConfirmed = response.value(QLatin1String(O2_OAUTH_CALLBACK_CONFIRMED), QStringLiteral("false")); if (requestToken_.isEmpty() || requestTokenSecret_.isEmpty() || (oAuthCbConfirmed == QLatin1String("false"))) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenRequestFinished: No oauth_token, oauth_token_secret or oauth_callback_confirmed in response :" << data; Q_EMIT linkingFailed(); return; } // Continue authorization flow in the browser QUrl url(authorizeUrl()); #if QT_VERSION < 0x050000 url.addQueryItem(O2_OAUTH_TOKEN, requestToken_); url.addQueryItem(O2_OAUTH_CALLBACK, callbackUrl().arg(replyServer_->serverPort()).toLatin1()); #else QUrlQuery query(url); query.addQueryItem(QLatin1String(O2_OAUTH_TOKEN), requestToken_); query.addQueryItem(QLatin1String(O2_OAUTH_CALLBACK), QString::fromLatin1(callbackUrl().arg(replyServer_->serverPort()).toLatin1())); url.setQuery(query); #endif Q_EMIT openBrowser(url); } void O1::onVerificationReceived(const QMap ¶ms) { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::onVerificationReceived"; Q_EMIT closeBrowser(); verifier_ = params.value(QLatin1String(O2_OAUTH_VERFIER), QString()); if (params.value(QLatin1String(O2_OAUTH_TOKEN)) == requestToken_) { // Exchange request token for access token exchangeToken(); } else { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onVerificationReceived: oauth_token missing or doesn't match"; Q_EMIT linkingFailed(); } } void O1::exchangeToken() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::exchangeToken"; // Create token exchange request QNetworkRequest request(accessTokenUrl()); QList oauthParams; oauthParams.append(O0RequestParameter(O2_OAUTH_CONSUMER_KEY, clientId().toLatin1())); oauthParams.append(O0RequestParameter(O2_OAUTH_VERSION, "1.0")); oauthParams.append(O0RequestParameter(O2_OAUTH_TIMESTAMP, QString::number(QDateTime::currentDateTimeUtc().toSecsSinceEpoch()).toLatin1())); oauthParams.append(O0RequestParameter(O2_OAUTH_NONCE, nonce())); oauthParams.append(O0RequestParameter(O2_OAUTH_TOKEN, requestToken_.toLatin1())); oauthParams.append(O0RequestParameter(O2_OAUTH_VERFIER, verifier_.toLatin1())); oauthParams.append(O0RequestParameter(O2_OAUTH_SIGNATURE_METHOD, signatureMethod().toLatin1())); oauthParams.append(O0RequestParameter(O2_OAUTH_SIGNATURE, generateSignature(oauthParams, request, QList(), QNetworkAccessManager::PostOperation))); // Post request request.setRawHeader(O2_HTTP_AUTHORIZATION_HEADER, buildAuthorizationHeader(oauthParams)); request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(O2_MIME_TYPE_XFORM)); QNetworkReply *reply = manager_->post(request, QByteArray()); connect(reply, QOverload::of(&QNetworkReply::error), this, &O1::onTokenExchangeError); connect(reply, &QNetworkReply::finished, this, &O1::onTokenExchangeFinished); } void O1::onTokenExchangeError(QNetworkReply::NetworkError error) { QNetworkReply *reply = qobject_cast(sender()); qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenExchangeError:" << (int)error << reply->errorString() << reply->readAll(); Q_EMIT linkingFailed(); } void O1::onTokenExchangeFinished() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenExchangeFinished"; QNetworkReply *reply = qobject_cast(sender()); reply->deleteLater(); - if (reply->error() != QNetworkReply::NoError) { +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + const auto networkError = reply->error(); +#else + const auto networkError = reply->networkError(); +#endif + if (networkError != QNetworkReply::NoError) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenExchangeFinished: " << reply->errorString(); return; } // Get access token and secret QByteArray data = reply->readAll(); QMap response = parseResponse(data); if (response.contains(QLatin1String(O2_OAUTH_TOKEN)) && response.contains(QLatin1String(O2_OAUTH_TOKEN_SECRET))) { setToken(response.take(QLatin1String(O2_OAUTH_TOKEN))); setTokenSecret(response.take(QLatin1String(O2_OAUTH_TOKEN_SECRET))); // Set extra tokens if any if (!response.isEmpty()) { QVariantMap extraTokens; for (const QString &key : qAsConst(response)) { extraTokens.insert(key, response.value(key)); } setExtraTokens(extraTokens); } setLinked(true); Q_EMIT linkingSucceeded(); } else { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenExchangeFinished: oauth_token or oauth_token_secret missing from response" << data; Q_EMIT linkingFailed(); } } QMap O1::parseResponse(const QByteArray &response) { QMap ret; foreach (const QByteArray ¶m, response.split('&')) { QList kv = param.split('='); if (kv.length() == 2) { ret.insert(QUrl::fromPercentEncoding(kv[0]), QUrl::fromPercentEncoding(kv[1])); } } return ret; } QByteArray O1::nonce() { static bool firstTime = true; if (firstTime) { firstTime = false; qsrand(QTime::currentTime().msec()); } QString u = QString::number(QDateTime::currentDateTimeUtc().toSecsSinceEpoch()); u.append(QString::number(qrand())); return u.toLatin1(); } diff --git a/resources/tomboynotes/o2/o2.cpp b/resources/tomboynotes/o2/o2.cpp index 0f5b91eda..b07cc7f74 100644 --- a/resources/tomboynotes/o2/o2.cpp +++ b/resources/tomboynotes/o2/o2.cpp @@ -1,456 +1,466 @@ #include #include #include #include #include #include #include #include #include #include #if QT_VERSION >= 0x050000 #include #include #else #include #include #endif #include "o2.h" #include "o2replyserver.h" #include "o0globals.h" #include "o0settingsstore.h" #include "debug.h" /// Parse JSON data into a QVariantMap static QVariantMap parseTokenResponse(const QByteArray &data) { #if QT_VERSION >= 0x050000 QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "parseTokenResponse: Failed to parse token response due to err:" << err.errorString(); return QVariantMap(); } if (!doc.isObject()) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "parseTokenResponse: Token response is not an object"; return QVariantMap(); } return doc.object().toVariantMap(); #else QScriptEngine engine; QScriptValue value = engine.evaluate("(" + QString(data) + QLatin1Char(')')); QScriptValueIterator it(value); QVariantMap map; while (it.hasNext()) { it.next(); map.insert(it.name(), it.value().toVariant()); } return map; #endif } /// Add query parameters to a query static void addQueryParametersToUrl(QUrl &url, const QList > ¶meters) { #if QT_VERSION < 0x050000 url.setQueryItems(parameters); #else QUrlQuery query(url); query.setQueryItems(parameters); url.setQuery(query); #endif } O2::O2(QObject *parent) : O0BaseAuth(parent) { manager_ = new QNetworkAccessManager(this); replyServer_ = new O2ReplyServer(this); grantFlow_ = GrantFlowAuthorizationCode; localhostPolicy_ = QLatin1String(O2_CALLBACK_URL); qRegisterMetaType("QNetworkReply::NetworkError"); connect(replyServer_, &O2ReplyServer::verificationReceived, this, &O2::onVerificationReceived); } O2::GrantFlow O2::grantFlow() const { return grantFlow_; } void O2::setGrantFlow(O2::GrantFlow value) { grantFlow_ = value; Q_EMIT grantFlowChanged(); } QString O2::username() const { return username_; } void O2::setUsername(const QString &value) { username_ = value; Q_EMIT usernameChanged(); } QString O2::password() const { return password_; } void O2::setPassword(const QString &value) { password_ = value; Q_EMIT passwordChanged(); } QString O2::scope() const { return scope_; } void O2::setScope(const QString &value) { scope_ = value; Q_EMIT scopeChanged(); } QString O2::requestUrl() const { return requestUrl_.toString(); } void O2::setRequestUrl(const QString &value) { requestUrl_ = QUrl(value); Q_EMIT requestUrlChanged(); } QString O2::tokenUrl() { return tokenUrl_.toString(); } void O2::setTokenUrl(const QString &value) { tokenUrl_ = QUrl(value); Q_EMIT tokenUrlChanged(); } QString O2::refreshTokenUrl() { return refreshTokenUrl_.toString(); } void O2::setRefreshTokenUrl(const QString &value) { refreshTokenUrl_ = QUrl(value); Q_EMIT refreshTokenUrlChanged(); } void O2::link() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::link"; if (linked()) { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::link: Linked already"; Q_EMIT linkingSucceeded(); return; } setLinked(false); setToken(QLatin1String("")); setTokenSecret(QLatin1String("")); setExtraTokens(QVariantMap()); setRefreshToken(QString()); setExpires(0); if (grantFlow_ == GrantFlowAuthorizationCode) { // Start listening to authentication replies replyServer_->listen(QHostAddress::Any, localPort_); // Save redirect URI, as we have to reuse it when requesting the access token redirectUri_ = localhostPolicy_.arg(replyServer_->serverPort()); // Assemble initial authentication URL QList > parameters; parameters.append(qMakePair(QLatin1String(O2_OAUTH2_RESPONSE_TYPE), (grantFlow_ == GrantFlowAuthorizationCode) ? QLatin1String(O2_OAUTH2_GRANT_TYPE_CODE) : QLatin1String(O2_OAUTH2_GRANT_TYPE_TOKEN))); parameters.append(qMakePair(QLatin1String(O2_OAUTH2_CLIENT_ID), clientId_)); parameters.append(qMakePair(QLatin1String(O2_OAUTH2_REDIRECT_URI), redirectUri_)); parameters.append(qMakePair(QLatin1String(O2_OAUTH2_SCOPE), scope_)); parameters.append(qMakePair(QLatin1String(O2_OAUTH2_API_KEY), apiKey_)); // Show authentication URL with a web browser QUrl url(requestUrl_); addQueryParametersToUrl(url, parameters); qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::link: Emit openBrowser" << url.toString(); Q_EMIT openBrowser(url); } else if (grantFlow_ == GrantFlowResourceOwnerPasswordCredentials) { QList parameters; parameters.append(O0RequestParameter(O2_OAUTH2_CLIENT_ID, clientId_.toUtf8())); parameters.append(O0RequestParameter(O2_OAUTH2_CLIENT_SECRET, clientSecret_.toUtf8())); parameters.append(O0RequestParameter(O2_OAUTH2_USERNAME, username_.toUtf8())); parameters.append(O0RequestParameter(O2_OAUTH2_PASSWORD, password_.toUtf8())); parameters.append(O0RequestParameter(O2_OAUTH2_GRANT_TYPE, O2_OAUTH2_GRANT_TYPE_PASSWORD)); parameters.append(O0RequestParameter(O2_OAUTH2_SCOPE, scope_.toUtf8())); parameters.append(O0RequestParameter(O2_OAUTH2_API_KEY, apiKey_.toUtf8())); QByteArray payload = O0BaseAuth::createQueryParameters(parameters); QUrl url(tokenUrl_); QNetworkRequest tokenRequest(url); tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded")); QNetworkReply *tokenReply = manager_->post(tokenRequest, payload); connect(tokenReply, &QNetworkReply::finished, this, &O2::onTokenReplyFinished, Qt::QueuedConnection); connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); } } void O2::unlink() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::unlink"; setLinked(false); setToken(QString()); setRefreshToken(QString()); setExpires(0); setExtraTokens(QVariantMap()); Q_EMIT linkingSucceeded(); } void O2::onVerificationReceived(const QMap &response) { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onVerificationReceived:" << response; qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onVerificationReceived: Emitting closeBrowser()"; Q_EMIT closeBrowser(); if (response.contains(QLatin1String("error"))) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onVerificationReceived: Verification failed: " << response; Q_EMIT linkingFailed(); return; } if (grantFlow_ == GrantFlowAuthorizationCode) { // Save access code setCode(response.value(QLatin1String(O2_OAUTH2_GRANT_TYPE_CODE))); // Exchange access code for access/refresh tokens QString query; if (!apiKey_.isEmpty()) { query = QString(QLatin1String("?") + QLatin1String(O2_OAUTH2_API_KEY) + QLatin1String("=") + apiKey_); } QNetworkRequest tokenRequest(QUrl(tokenUrl_.toString() + query)); tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(O2_MIME_TYPE_XFORM)); QMap parameters; parameters.insert(QLatin1String(O2_OAUTH2_GRANT_TYPE_CODE), code()); parameters.insert(QLatin1String(O2_OAUTH2_CLIENT_ID), clientId_); parameters.insert(QLatin1String(O2_OAUTH2_CLIENT_SECRET), clientSecret_); parameters.insert(QLatin1String(O2_OAUTH2_REDIRECT_URI), redirectUri_); parameters.insert(QLatin1String(O2_OAUTH2_GRANT_TYPE), QLatin1String(O2_AUTHORIZATION_CODE)); QByteArray data = buildRequestBody(parameters); QNetworkReply *tokenReply = manager_->post(tokenRequest, data); timedReplies_.add(tokenReply); connect(tokenReply, &QNetworkReply::finished, this, &O2::onTokenReplyFinished, Qt::QueuedConnection); connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); } else { setToken(response.value(QLatin1String(O2_OAUTH2_ACCESS_TOKEN))); setRefreshToken(response.value(QLatin1String(O2_OAUTH2_REFRESH_TOKEN))); } } QString O2::code() const { QString key = QString::fromLatin1(O2_KEY_CODE).arg(clientId_); return store_->value(key); } void O2::setCode(const QString &c) { QString key = QString::fromLatin1(O2_KEY_CODE).arg(clientId_); store_->setValue(key, c); } void O2::onTokenReplyFinished() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyFinished"; QNetworkReply *tokenReply = qobject_cast(sender()); - if (tokenReply->error() == QNetworkReply::NoError) { +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + const auto networkError = tokenReply->error(); +#else + const auto networkError = tokenReply->networkError(); +#endif + if (networkError == QNetworkReply::NoError) { QByteArray replyData = tokenReply->readAll(); QVariantMap tokens = parseTokenResponse(replyData); // Check for mandatory tokens if (tokens.contains(QLatin1String(O2_OAUTH2_ACCESS_TOKEN))) { setToken(tokens.take(QLatin1String(O2_OAUTH2_ACCESS_TOKEN)).toString()); bool ok = false; int expiresIn = tokens.take(QLatin1String(O2_OAUTH2_EXPIRES_IN)).toInt(&ok); if (ok) { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyFinished: Token expires in" << expiresIn << "seconds"; setExpires(QDateTime::currentSecsSinceEpoch() + expiresIn); } setRefreshToken(tokens.take(QLatin1String(O2_OAUTH2_REFRESH_TOKEN)).toString()); setExtraTokens(tokens); timedReplies_.remove(tokenReply); setLinked(true); Q_EMIT linkingSucceeded(); } else { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyFinished: oauth_token missing from response" << replyData; Q_EMIT linkingFailed(); } } tokenReply->deleteLater(); } void O2::onTokenReplyError(QNetworkReply::NetworkError error) { QNetworkReply *tokenReply = qobject_cast(sender()); qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyError: " << error << ": " << tokenReply->errorString(); qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyError: " << tokenReply->readAll(); setToken(QString()); setRefreshToken(QString()); timedReplies_.remove(tokenReply); Q_EMIT linkingFailed(); } QByteArray O2::buildRequestBody(const QMap ¶meters) { QByteArray body; bool first = true; for (const QString &key : parameters.keys()) { if (first) { first = false; } else { body.append("&"); } QString value = parameters.value(key); body.append(QUrl::toPercentEncoding(key) + QStringLiteral("=").toUtf8() + QUrl::toPercentEncoding(value)); } return body; } int O2::expires() { const QString key = QString::fromLatin1(O2_KEY_EXPIRES).arg(clientId_); return store_->value(key).toInt(); } void O2::setExpires(int v) { const QString key = QString::fromLatin1(O2_KEY_EXPIRES).arg(clientId_); store_->setValue(key, QString::number(v)); } QString O2::refreshToken() { const QString key = QString::fromLatin1(O2_KEY_REFRESH_TOKEN).arg(clientId_); return store_->value(key); } void O2::setRefreshToken(const QString &v) { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::setRefreshToken" << v.left(4) << "..."; QString key = QString::fromLatin1(O2_KEY_REFRESH_TOKEN).arg(clientId_); store_->setValue(key, v); } void O2::refresh() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::refresh: Token: ..." << refreshToken().right(7); if (refreshToken().isEmpty()) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::refresh: No refresh token"; onRefreshError(QNetworkReply::AuthenticationRequiredError); return; } if (refreshTokenUrl_.isEmpty()) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::refresh: Refresh token URL not set"; onRefreshError(QNetworkReply::AuthenticationRequiredError); return; } QNetworkRequest refreshRequest(refreshTokenUrl_); refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(O2_MIME_TYPE_XFORM)); QMap parameters; parameters.insert(QLatin1String(O2_OAUTH2_CLIENT_ID), clientId_); parameters.insert(QLatin1String(O2_OAUTH2_CLIENT_SECRET), clientSecret_); parameters.insert(QLatin1String(O2_OAUTH2_REFRESH_TOKEN), refreshToken()); parameters.insert(QLatin1String(O2_OAUTH2_GRANT_TYPE), QLatin1String(O2_OAUTH2_REFRESH_TOKEN)); QByteArray data = buildRequestBody(parameters); QNetworkReply *refreshReply = manager_->post(refreshRequest, data); timedReplies_.add(refreshReply); connect(refreshReply, &QNetworkReply::finished, this, &O2::onRefreshFinished, Qt::QueuedConnection); connect(refreshReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRefreshError(QNetworkReply::NetworkError)), Qt::QueuedConnection); } void O2::onRefreshFinished() { QNetworkReply *refreshReply = qobject_cast(sender()); - qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onRefreshFinished: Error" << (int)refreshReply->error() << refreshReply->errorString(); - if (refreshReply->error() == QNetworkReply::NoError) { +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + const auto networkError = refreshReply->error(); +#else + const auto networkError = refreshReply->networkError(); +#endif + qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onRefreshFinished: Error" << (int)networkError << refreshReply->errorString(); + if (networkError == QNetworkReply::NoError) { QByteArray reply = refreshReply->readAll(); QVariantMap tokens = parseTokenResponse(reply); setToken(tokens.value(QLatin1String(O2_OAUTH2_ACCESS_TOKEN)).toString()); setExpires(QDateTime::currentSecsSinceEpoch() + tokens.value(QLatin1String(O2_OAUTH2_EXPIRES_IN)).toInt()); setRefreshToken(tokens.value(QLatin1String(O2_OAUTH2_REFRESH_TOKEN)).toString()); timedReplies_.remove(refreshReply); setLinked(true); Q_EMIT linkingSucceeded(); Q_EMIT refreshFinished(QNetworkReply::NoError); qCDebug(TOMBOYNOTESRESOURCE_LOG) << " New token expires in" << expires() << "seconds"; } refreshReply->deleteLater(); } void O2::onRefreshError(QNetworkReply::NetworkError error) { QNetworkReply *refreshReply = qobject_cast(sender()); qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onRefreshError: " << error; unlink(); timedReplies_.remove(refreshReply); Q_EMIT refreshFinished(error); } QString O2::localhostPolicy() const { return localhostPolicy_; } void O2::setLocalhostPolicy(const QString &value) { localhostPolicy_ = value; } QString O2::apiKey() const { return apiKey_; } void O2::setApiKey(const QString &value) { apiKey_ = value; } QByteArray O2::replyContent() const { return replyServer_->replyContent(); } void O2::setReplyContent(const QByteArray &value) { replyServer_->setReplyContent(value); } bool O2::ignoreSslErrors() { return timedReplies_.ignoreSslErrors(); } void O2::setIgnoreSslErrors(bool ignoreSslErrors) { timedReplies_.setIgnoreSslErrors(ignoreSslErrors); } diff --git a/resources/tomboynotes/o2/o2requestor.cpp b/resources/tomboynotes/o2/o2requestor.cpp index 23dff6ff9..1992af8e0 100644 --- a/resources/tomboynotes/o2/o2requestor.cpp +++ b/resources/tomboynotes/o2/o2requestor.cpp @@ -1,207 +1,211 @@ #include "debug.h" #include #if QT_VERSION >= 0x050000 #include #endif #include "o2/o2requestor.h" #include "o2/o2.h" #include "o2/o0globals.h" O2Requestor::O2Requestor(QNetworkAccessManager *manager, O2 *authenticator, QObject *parent) : QObject(parent) , reply_(nullptr) , status_(Idle) { manager_ = manager; authenticator_ = authenticator; if (authenticator) { timedReplies_.setIgnoreSslErrors(authenticator->ignoreSslErrors()); } qRegisterMetaType("QNetworkReply::NetworkError"); connect(authenticator, &O2::refreshFinished, this, &O2Requestor::onRefreshFinished, Qt::QueuedConnection); } O2Requestor::~O2Requestor() { } int O2Requestor::get(const QNetworkRequest &req) { if (-1 == setup(req, QNetworkAccessManager::GetOperation)) { return -1; } reply_ = manager_->get(request_); timedReplies_.add(reply_); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, &QNetworkReply::finished, this, &O2Requestor::onRequestFinished, Qt::QueuedConnection); return id_; } int O2Requestor::post(const QNetworkRequest &req, const QByteArray &data) { if (-1 == setup(req, QNetworkAccessManager::PostOperation)) { return -1; } data_ = data; reply_ = manager_->post(request_, data_); timedReplies_.add(reply_); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, &QNetworkReply::finished, this, &O2Requestor::onRequestFinished, Qt::QueuedConnection); connect(reply_, &QNetworkReply::uploadProgress, this, &O2Requestor::onUploadProgress); return id_; } int O2Requestor::put(const QNetworkRequest &req, const QByteArray &data) { if (-1 == setup(req, QNetworkAccessManager::PutOperation)) { return -1; } data_ = data; reply_ = manager_->put(request_, data_); timedReplies_.add(reply_); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, &QNetworkReply::finished, this, &O2Requestor::onRequestFinished, Qt::QueuedConnection); connect(reply_, &QNetworkReply::uploadProgress, this, &O2Requestor::onUploadProgress); return id_; } void O2Requestor::onRefreshFinished(QNetworkReply::NetworkError error) { if (status_ != Requesting) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2Requestor::onRefreshFinished: No pending request"; return; } if (QNetworkReply::NoError == error) { QTimer::singleShot(100, this, &O2Requestor::retry); } else { error_ = error; QTimer::singleShot(10, this, &O2Requestor::finish); } } void O2Requestor::onRequestFinished() { QNetworkReply *senderReply = qobject_cast(sender()); - QNetworkReply::NetworkError error = senderReply->error(); +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + const auto networkError = senderReply->error(); +#else + const auto networkError = senderReply->networkError(); +#endif if (status_ == Idle) { return; } if (reply_ != senderReply) { return; } - if (error == QNetworkReply::NoError) { + if (networkError == QNetworkReply::NoError) { QTimer::singleShot(10, this, &O2Requestor::finish); } } void O2Requestor::onRequestError(QNetworkReply::NetworkError error) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2Requestor::onRequestError: Error" << (int)error; if (status_ == Idle) { return; } if (reply_ != qobject_cast(sender())) { return; } int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2Requestor::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); qCDebug(TOMBOYNOTESRESOURCE_LOG) << reply_->readAll(); if ((status_ == Requesting) && (httpStatus == 401)) { // Call O2::refresh. Note the O2 instance might live in a different thread if (QMetaObject::invokeMethod(authenticator_, "refresh")) { return; } qCCritical(TOMBOYNOTESRESOURCE_LOG) << "O2Requestor::onRequestError: Invoking remote refresh failed"; } error_ = error; QTimer::singleShot(10, this, &O2Requestor::finish); } void O2Requestor::onUploadProgress(qint64 uploaded, qint64 total) { if (status_ == Idle) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2Requestor::onUploadProgress: No pending request"; return; } if (reply_ != qobject_cast(sender())) { return; } Q_EMIT uploadProgress(id_, uploaded, total); } int O2Requestor::setup(const QNetworkRequest &req, QNetworkAccessManager::Operation operation) { static int currentId; QUrl url; if (status_ != Idle) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2Requestor::setup: Another request pending"; return -1; } request_ = req; operation_ = operation; id_ = currentId++; url_ = url = req.url(); #if QT_VERSION < 0x050000 url.addQueryItem(O2_OAUTH2_ACCESS_TOKEN, authenticator_->token()); #else QUrlQuery query(url); query.addQueryItem(QLatin1String(O2_OAUTH2_ACCESS_TOKEN), authenticator_->token()); url.setQuery(query); #endif request_.setUrl(url); status_ = Requesting; error_ = QNetworkReply::NoError; return id_; } void O2Requestor::finish() { QByteArray data; if (status_ == Idle) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2Requestor::finish: No pending request"; return; } data = reply_->readAll(); status_ = Idle; timedReplies_.remove(reply_); reply_->disconnect(this); reply_->deleteLater(); Q_EMIT finished(id_, error_, data); } void O2Requestor::retry() { if (status_ != Requesting) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2Requestor::retry: No pending request"; return; } timedReplies_.remove(reply_); reply_->disconnect(this); reply_->deleteLater(); QUrl url = url_; #if QT_VERSION < 0x050000 url.addQueryItem(O2_OAUTH2_ACCESS_TOKEN, authenticator_->token()); #else QUrlQuery query(url); query.addQueryItem(QLatin1String(O2_OAUTH2_ACCESS_TOKEN), authenticator_->token()); url.setQuery(query); #endif request_.setUrl(url); status_ = ReRequesting; switch (operation_) { case QNetworkAccessManager::GetOperation: reply_ = manager_->get(request_); break; case QNetworkAccessManager::PostOperation: reply_ = manager_->post(request_, data_); break; default: reply_ = manager_->put(request_, data_); } timedReplies_.add(reply_); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, &QNetworkReply::finished, this, &O2Requestor::onRequestFinished, Qt::QueuedConnection); connect(reply_, &QNetworkReply::uploadProgress, this, &O2Requestor::onUploadProgress); } diff --git a/resources/tomboynotes/tomboyjobbase.cpp b/resources/tomboynotes/tomboyjobbase.cpp index d2f96789d..15da2cc69 100644 --- a/resources/tomboynotes/tomboyjobbase.cpp +++ b/resources/tomboynotes/tomboyjobbase.cpp @@ -1,58 +1,63 @@ /* Copyright (c) 2016 Stefan Stäglich This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "tomboyjobbase.h" TomboyJobBase::TomboyJobBase(KIO::AccessManager *manager, QObject *parent) : KCompositeJob(parent) , mManager(manager) , mO1(new O1Tomboy(this)) , mReply(nullptr) { mRequestor = new O1Requestor(mManager, mO1, this); } void TomboyJobBase::setServerURL(const QString &apiurl, const QString &contenturl) { mO1->setBaseURL(apiurl); mApiURL = apiurl + QStringLiteral("/api/1.0"); mContentURL = contenturl; } void TomboyJobBase::setAuthentication(const QString &token, const QString &secret) { mO1->restoreAuthData(token, secret); } void TomboyJobBase::checkReplyError() { - switch (mReply->error()) { +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + const auto networkError = mReply->error(); +#else + const auto networkError = mReply->networkError(); +#endif + switch (networkError) { case QNetworkReply::NoError: setError(TomboyJobError::NoError); break; case QNetworkReply::RemoteHostClosedError: case QNetworkReply::HostNotFoundError: case QNetworkReply::TimeoutError: setError(TomboyJobError::TemporaryError); break; default: setError(TomboyJobError::PermanentError); break; } }