diff --git a/webenginepart/autotests/webenginepartcookiejar_test.cpp b/webenginepart/autotests/webenginepartcookiejar_test.cpp index b1dcff88c..54c55be96 100644 --- a/webenginepart/autotests/webenginepartcookiejar_test.cpp +++ b/webenginepart/autotests/webenginepartcookiejar_test.cpp @@ -1,267 +1,276 @@ /* * This file is part of the KDE project * Copyright (C) 2018 Stefano Crocco * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "webenginepartcookiejar_test.h" #include #include + #include #include #include #include #include QTEST_MAIN(TestWebEnginePartCookieJar); void TestWebEnginePartCookieJar::initTestCase() { m_cookieName = "webenginepartcookiejartest"; } void TestWebEnginePartCookieJar::init() { m_server = new QDBusInterface("org.kde.kcookiejar5", "/modules/kcookiejar", "org.kde.KCookieServer"); m_profile = new QWebEngineProfile(this); m_store = m_profile->cookieStore(); m_jar = new WebEnginePartCookieJar(m_profile, this); } void TestWebEnginePartCookieJar::cleanup() { if (m_server->isValid()) { deleteCookies(findTestCookies()); } delete m_server; m_server = Q_NULLPTR; delete m_jar; m_jar = Q_NULLPTR; m_store = Q_NULLPTR; delete m_profile; m_profile = Q_NULLPTR; } QNetworkCookie TestWebEnginePartCookieJar::CookieData::cookie() const { QNetworkCookie cookie; cookie.setName(name.toUtf8()); cookie.setValue(value.toUtf8()); cookie.setPath(path); cookie.setDomain(domain); cookie.setSecure(secure); cookie.setExpirationDate(expiration); return cookie; } void TestWebEnginePartCookieJar::testCookieAddedToStoreAreAddedToKCookieServer_data() { QTest::addColumn("cookie"); QTest::addColumn("name"); QTest::addColumn("value"); QTest::addColumn("domain"); QTest::addColumn("path"); QTest::addColumn("host"); QTest::addColumn("expiration"); QTest::addColumn("secure"); const QStringList labels{ "persistent cookie with domain and path", "session cookie with domain and path", "persistent cookie with domain and no path", "persistent cookie with path and no domain", "persistent cookie without secure", }; const QList input{ {m_cookieName + "-persistent", "test-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true}, {m_cookieName + "-session", "test-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime(), true}, {m_cookieName + "-no-path", "test-value", ".yyy.xxx.com", "", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true}, {m_cookieName + "-no-domain", "test-value", "", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true}, {m_cookieName + "-no-secure", "test-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), false} }; QList expected(input); for (int i = 0; i < input.count(); ++i) { const CookieData &ex = expected.at(i); const CookieData &in = input.at(i); QNetworkCookie c = in.cookie(); if (in.domain.isEmpty()) { c.normalize(QUrl("https://" + in.host)); } QTest::newRow(labels.at(i).toLatin1()) << c << ex.name << ex.value << ex.domain << ex.path << ex.host << ex.expiration << ex.secure; } } void TestWebEnginePartCookieJar::testCookieAddedToStoreAreAddedToKCookieServer() { QFETCH(const QNetworkCookie, cookie); QFETCH(const QString, name); QFETCH(const QString, value); QFETCH(const QString, domain); QFETCH(const QString, path); QFETCH(const QString, host); QFETCH(const QDateTime, expiration); QFETCH(const bool, secure); QVERIFY2(m_server->isValid(), qPrintable(m_server->lastError().message())); // domain=0, path=1, name=2, host=3, value=4, expirationDate=5, protocolVersion=6, secure=7; const QList fields{0,1,2,3,4,5,6,7}; emit m_store->cookieAdded(cookie); const QDBusReply res = m_server->call(QDBus::Block, "findCookies", QVariant::fromValue(fields), domain, host, cookie.path(), QString(cookie.name())); QVERIFY2(!m_server->lastError().isValid(), m_server->lastError().message().toLatin1()); QStringList resFields = res.value(); - QEXPECT_FAIL("persistent cookie with path and no domain", "Handling of cookies without domain is currently broken", Abort); QVERIFY(!resFields.isEmpty()); QCOMPARE(fields.count(), resFields.count()); QCOMPARE(resFields.at(0), domain); QCOMPARE(resFields.at(1), path); QCOMPARE(resFields.at(2), name); - QEXPECT_FAIL("", "The value returned by KCookieServer strips the leftmost part of the fqdn. Why?", Continue); + if (!domain.isEmpty()){ + QEXPECT_FAIL("", "The value returned by KCookieServer strips the leftmost part of the fqdn. Why?", Continue); + } QCOMPARE(resFields.at(3), host); QCOMPARE(resFields.at(4), value); const int secsSinceEpoch = resFields.at(5).toInt(); //KCookieServer gives a session cookie an expiration time equal to epoch, while QNetworkCookie uses an invalid QDateTime if (!expiration.isValid()) { QCOMPARE(secsSinceEpoch, 0); } else { QCOMPARE(secsSinceEpoch, expiration.toSecsSinceEpoch()); } const bool sec = resFields.at(7).toInt(); QCOMPARE(sec, secure); } QList TestWebEnginePartCookieJar::findTestCookies() { QList cookies; if (!m_server->isValid()) { return cookies; } QDBusReply rep = m_server->call(QDBus::Block, "findDomains"); if (!rep.isValid()) { qDebug() << rep.error().message(); return cookies; } QStringList domains = rep.value(); //domain, path, name, host const QList fields{0,1,2,3}; foreach (const QString &d, domains){ rep = m_server->call(QDBus::Block, "findCookies", QVariant::fromValue(fields), d, "", "", ""); if (!rep.isValid()) { qDebug() << rep.error().message(); return cookies; } QStringList res = rep.value(); for (int i = 0; i < res.count(); i+= fields.count()) { if (res.at(i+2).startsWith(m_cookieName)) { CookieData d; d.name = res.at(i+2); d.domain = res.at(i); d.path = res.at(i+1); d.host = res.at(i+3); cookies.append(d); } } } return cookies; } void TestWebEnginePartCookieJar::deleteCookies(const QList &cookies) { if (!m_server->isValid()) { return; } for (const CookieData &c: cookies) { QDBusMessage deleteRep = m_server->call(QDBus::Block, "deleteCookie", c.domain, c.host, c.path, c.name); if (m_server->lastError().isValid()) { qDebug() << m_server->lastError().message(); } } } void TestWebEnginePartCookieJar::testCookieRemovedFromStoreAreRemovedFromKCookieServer_data() { QTest::addColumn("cookie"); QTest::addColumn("name"); QTest::addColumn("domain"); QTest::addColumn("host"); const QStringList labels{ "remove persistent cookie with domain and path", "remove session cookie with domain and path", "remove persistent cookie with domain and no path", "remove persistent cookie with path and no domain", "remove persistent cookie without secure", }; const QList input{ {m_cookieName + "-persistent-remove", "test-remove-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true}, {m_cookieName + "-session-remove", "test-remove-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime(), true}, {m_cookieName + "-no-path-remove", "test-remove-value", ".yyy.xxx.com", "", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true}, {m_cookieName + "-no-domain-remove", "test-remove-value", "", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true}, {m_cookieName + "-no-secure-remove", "test-remove-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), false} }; QList expected(input); for (int i = 0; i < input.count(); ++i) { const CookieData &ex = expected.at(i); const CookieData &in = input.at(i); QNetworkCookie c = in.cookie(); if (in.domain.isEmpty()) { c.normalize(QUrl("https://" + in.host)); } QTest::newRow(labels.at(i).toLatin1()) << c << ex.name << ex.domain << ex.host; }} void TestWebEnginePartCookieJar::testCookieRemovedFromStoreAreRemovedFromKCookieServer() { QFETCH(const QNetworkCookie, cookie); QFETCH(const QString, name); QFETCH(const QString, domain); QFETCH(const QString, host); const QString url = "https://" + host; - const QByteArray setCookie = "Set-Cookie: " + cookie.toRawForm(); + + //cookie is in the "format" used by QWebEngineCookieStore, which means that, if the domain should be empty, + //it is stored as a domain not starting with a dot. KCookieServer, instead, wants cookies without domains + //to actually have no domain, so we have to change it + QNetworkCookie kcookieServerCookie(cookie); + if (!kcookieServerCookie.domain().startsWith('.')) { + kcookieServerCookie.setDomain(QString()); + } + const QByteArray setCookie = "Set-Cookie: " + kcookieServerCookie.toRawForm(); //Add cookie to KCookieServer QDBusMessage rep = m_server->call(QDBus::Block, "addCookies", url, setCookie, static_cast(0)); QVERIFY2(!m_server->lastError().isValid(), qPrintable(m_server->lastError().message())); //Ensure cookie has been added to KCookieServer - QDBusReply reply = m_server->call(QDBus::Block, "findCookies", QVariant::fromValue(QList{2}), domain, "", "", ""); + QDBusReply reply = m_server->call(QDBus::Block, "findCookies", QVariant::fromValue(QList{2}), domain, host, "", ""); QVERIFY2(reply.isValid(), qPrintable(reply.error().message())); QStringList cookies = reply.value(); - QEXPECT_FAIL("remove persistent cookie with path and no domain", "Handling of cookies without domain is currently broken", Abort); QVERIFY2(cookies.contains(name), "Cookie wasn't added to server"); //Emit QWebEngineCookieStore::cookieRemoved signal and check that cookie has indeed been removed emit m_store->cookieRemoved(cookie); reply = m_server->call(QDBus::Block, "findCookies", QVariant::fromValue(QList{2}), domain, "", "", ""); QVERIFY2(reply.isValid(), qPrintable(reply.error().message())); cookies = reply.value(); QVERIFY2(!cookies.contains(name), "Cookie wasn't removed from server"); } diff --git a/webenginepart/src/webenginepartcookiejar.cpp b/webenginepart/src/webenginepartcookiejar.cpp index 618728c51..b0b0e94c3 100644 --- a/webenginepart/src/webenginepartcookiejar.cpp +++ b/webenginepart/src/webenginepartcookiejar.cpp @@ -1,335 +1,350 @@ /* * This file is part of the KDE project * Copyright (C) 2018 Stefano Crocco * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "webenginepartcookiejar.h" #include "settings/webenginesettings.h" #include #include #include #include #include #include #include #include #include #include #include const QVariant WebEnginePartCookieJar::s_findCookieFields = QVariant::fromValue(QList{ static_cast(CookieDetails::domain), static_cast(CookieDetails::path), static_cast(CookieDetails::name), static_cast(CookieDetails::host), static_cast(CookieDetails::value), static_cast(CookieDetails::expirationDate), static_cast(CookieDetails::protocolVersion), static_cast(CookieDetails::secure) } ); WebEnginePartCookieJar::CookieIdentifier::CookieIdentifier(const QNetworkCookie& cookie): name(cookie.name()), domain(cookie.domain()), path(cookie.path()) { } WebEnginePartCookieJar::CookieIdentifier::CookieIdentifier(const QString& n, const QString& d, const QString& p): name(n), domain(d), path(p) { } WebEnginePartCookieJar::WebEnginePartCookieJar(QWebEngineProfile *prof, QObject *parent): QObject(parent), m_cookieStore(prof->cookieStore()), m_cookieServer("org.kde.kcookiejar5", "/modules/kcookiejar", "org.kde.KCookieServer") { prof->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); connect(qApp, &QApplication::lastWindowClosed, this, &WebEnginePartCookieJar::deleteSessionCookies); connect(m_cookieStore, &QWebEngineCookieStore::cookieAdded, this, &WebEnginePartCookieJar::addCookie); connect(m_cookieStore, &QWebEngineCookieStore::cookieRemoved, this, &WebEnginePartCookieJar::removeCookie); if(!m_cookieServer.isValid()){ qDebug() << "Couldn't connect to KCookieServer"; } loadKIOCookies(); //QWebEngineCookieStore::setCookieFilter only exists from Qt 5.11.0 #if QTWEBENGINE_VERSION >= QT_VERSION_CHECK(5,11,0) auto filter = [this](const QWebEngineCookieStore::FilterRequest &req){return filterCookie(req);}; m_cookieStore->setCookieFilter(filter); #endif //QTWEBENGINE_VERSION >= QT_VERSION_CHECK(5,11,0) } WebEnginePartCookieJar::~WebEnginePartCookieJar() { } #if QTWEBENGINE_VERSION >= QT_VERSION_CHECK(5,11,0) bool WebEnginePartCookieJar::filterCookie(const QWebEngineCookieStore::FilterRequest& req) { return WebEngineSettings::self()->acceptCrossDomainCookies() || !req.thirdParty; } #endif //QTWEBENGINE_VERSION >= QT_VERSION_CHECK(5,11,0) void WebEnginePartCookieJar::deleteSessionCookies() { if (!m_cookieServer.isValid()) { return; } foreach(qlonglong id, m_windowsWithSessionCookies) { m_cookieServer.call(QDBus::NoBlock, "deleteSessionCookies", id); } } QUrl WebEnginePartCookieJar::constructUrlForCookie(const QNetworkCookie& cookie) const { QUrl url; QString domain = cookie.domain().startsWith(".") ? cookie.domain().mid(1) : cookie.domain(); if (!domain.isEmpty()) { url.setScheme("http"); url.setHost(domain); url.setPath(cookie.path()); } else { qDebug() << "EMPTY COOKIE DOMAIN for" << cookie.name(); } return url; } qlonglong WebEnginePartCookieJar::findWinID() { QWidget *mainWindow = qApp->activeWindow(); if (mainWindow && !(mainWindow->windowFlags() & Qt::Dialog)) { return mainWindow->winId(); } else { QWidgetList windows = qApp->topLevelWidgets(); foreach(QWidget *w, windows){ if (w->isWindow() && !(w->windowFlags() & Qt::Dialog)) { return w->winId(); } } } return 0; } +void WebEnginePartCookieJar::removeCookieDomain(QNetworkCookie& cookie) +{ + if (!cookie.domain().startsWith('.')) { + cookie.setDomain(QString()); + } +} + void WebEnginePartCookieJar::addCookie(const QNetworkCookie& _cookie) { //If the added cookie is in m_cookiesLoadedFromKCookieServer, it means //we're loading the cookie from KCookieServer (from the call to loadKIOCookies //in the constructor (QWebEngineCookieStore::setCookie is asynchronous, though, //so we're not in the constructor anymore)), so don't attempt to add //the cookie back to KCookieServer; instead, remove it from the list. if (m_cookiesLoadedFromKCookieServer.removeOne(_cookie)) { return; } QNetworkCookie cookie(_cookie); CookieIdentifier id(cookie); if (!m_cookieServer.isValid()) { return; } if (cookie.expirationDate().isValid()) { //There's a bug in KCookieJar which causes the expiration date to be interpreted as local time //instead of GMT as it should. The bug is fixed in KIO 5.50 #if KIO_VERSION < QT_VERSION_CHECK(5,50,0) QTimeZone local = QTimeZone::systemTimeZone(); int offset = local.offsetFromUtc(QDateTime::currentDateTime()); QDateTime dt = cookie.expirationDate(); dt.setTime(dt.time().addSecs(offset)); cookie.setExpirationDate(dt); #endif } QUrl url = constructUrlForCookie(cookie); if (url.isEmpty()) { return; } + //NOTE: the removal of the domain (when not starting with a dot) must be done *after* creating + //the URL, as constructUrlForCookie needs the domain + removeCookieDomain(cookie); QByteArray header("Set-Cookie: "); header += cookie.toRawForm(); header += "\n"; qlonglong winId = findWinID(); if (!cookie.expirationDate().isValid()) { m_windowsWithSessionCookies.insert(winId); } // qDebug() << url; QString advice = askAdvice(url); if (advice == "Reject"){ m_pendingRejectedCookies << CookieIdentifier(_cookie); m_cookieStore->deleteCookie(_cookie); } else if (advice == "AcceptForSession" && !cookie.isSessionCookie()) { cookie.setExpirationDate(QDateTime()); addCookie(cookie); } else { int oldTimeout = m_cookieServer.timeout(); if (advice == "Ask") { //Give the user time (10 minutes = 600 000ms) to analyze the cookie m_cookieServer.setTimeout(10*60*1000); } m_cookieServer.call(QDBus::Block, "addCookies", url.toString(), header, winId); m_cookieServer.setTimeout(oldTimeout); if (m_cookieServer.lastError().isValid()) { qDebug() << m_cookieServer.lastError(); return; } if (!advice.startsWith("Accept") && !cookieInKCookieJar(id, url)) { m_pendingRejectedCookies << id; m_cookieStore->deleteCookie(_cookie); } } } QString WebEnginePartCookieJar::askAdvice(const QUrl& url) { if (!m_cookieServer.isValid()) { return QString(); } QDBusReply rep = m_cookieServer.call(QDBus::Block, "getDomainAdvice", url.toString()); if (rep.isValid()) { return rep.value(); } else { qDebug() << rep.error().message(); return QString(); } } -bool WebEnginePartCookieJar::cookieInKCookieJar(const WebEnginePartCookieJar::CookieIdentifier& _id, const QUrl& url) +bool WebEnginePartCookieJar::cookieInKCookieJar(const WebEnginePartCookieJar::CookieIdentifier& id, const QUrl& url) { if (!m_cookieServer.isValid()) { return false; } - CookieIdentifier id(_id.name, prependDotToDomain(_id.domain), _id.path); QList fields = { static_cast(CookieDetails::name), static_cast(CookieDetails::domain), static_cast(CookieDetails::path) }; QDBusReply rep = m_cookieServer.call(QDBus::Block, "findCookies", QVariant::fromValue(fields), id.domain, url.toString(QUrl::FullyEncoded), id.path, id.name); if (!rep.isValid()) { qDebug() << rep.error().message(); return false; } QStringList cookies = rep.value(); for(int i = 0; i < cookies.length()-2; i+=3){ if (CookieIdentifier(cookies.at(i), cookies.at(i+1), cookies.at(i+2)) == id) { return true; } } return false; } -void WebEnginePartCookieJar::removeCookie(const QNetworkCookie& cookie) +void WebEnginePartCookieJar::removeCookie(const QNetworkCookie& _cookie) { - CookieIdentifier id(cookie); - int pos = m_pendingRejectedCookies.indexOf(id); + int pos = m_pendingRejectedCookies.indexOf(CookieIdentifier(_cookie)); //Ignore pending cookies if (pos >= 0) { m_pendingRejectedCookies.takeAt(pos); return; } if (!m_cookieServer.isValid()) { return; } + QNetworkCookie cookie(_cookie); QUrl url = constructUrlForCookie(cookie); if(url.isEmpty()){ qDebug() << "Can't remove cookie" << cookie.name() << "because its URL isn't known"; return; } + removeCookieDomain(cookie); - //Add leading dot to domain, if necessary - id.domain = prependDotToDomain(id.domain); - QDBusPendingCall pcall = m_cookieServer.asyncCall("deleteCookie", id.domain, constructUrlForCookie(cookie).toString(), cookie.path(), QString(cookie.name())); + QDBusPendingCall pcall = m_cookieServer.asyncCall("deleteCookie", cookie.domain(), url.toString(), cookie.path(), QString(cookie.name())); QDBusPendingCallWatcher *w = new QDBusPendingCallWatcher(pcall, this); connect(w, &QDBusPendingCallWatcher::finished, this, &WebEnginePartCookieJar::cookieRemovalFailed); } void WebEnginePartCookieJar::cookieRemovalFailed(QDBusPendingCallWatcher *watcher) { QDBusPendingReply<> r = *watcher; if (r.isError()){ qDebug() << "DBus error:" << r.error().message(); } watcher->deleteLater(); } void WebEnginePartCookieJar::loadKIOCookies() { CookieList cookies = findKIOCookies(); foreach(const QNetworkCookie& cookie, cookies){ QDateTime currentTime = QDateTime::currentDateTime(); //Don't attempt to add expired cookies if (cookie.expirationDate().isValid() && cookie.expirationDate() < currentTime) { continue; } m_cookiesLoadedFromKCookieServer << cookie; m_cookieStore->setCookie(cookie); } } WebEnginePartCookieJar::CookieList WebEnginePartCookieJar::findKIOCookies() { CookieList res; if (!m_cookieServer.isValid()) { return res; } QDBusReply rep = m_cookieServer.call(QDBus::Block, "findDomains"); if(!rep.isValid()){ qDebug() << rep.error().message(); return res; } QStringList domains = rep.value(); uint fieldsCount = 8; foreach( const QString &d, domains){ QDBusReply rep = m_cookieServer.call(QDBus::Block, "findCookies", s_findCookieFields, d, "", "", ""); if (!rep.isValid()) { qDebug() << rep.error().message(); return res; } QStringList data = rep.value(); for(int i = 0; i < data.count(); i+=fieldsCount){ res << parseKIOCookie(data, i); } } return res; } QNetworkCookie WebEnginePartCookieJar::parseKIOCookie(const QStringList& data, int start) { QNetworkCookie c; auto extractField = [data, start](CookieDetails field){return data.at(start + static_cast(field));}; c.setDomain(data.at(start+static_cast(CookieDetails::domain)).toUtf8()); c.setExpirationDate(QDateTime::fromSecsSinceEpoch(extractField(CookieDetails::expirationDate).toInt())); c.setName(extractField(CookieDetails::name).toUtf8()); c.setPath(extractField(CookieDetails::path).toUtf8()); c.setSecure(extractField(CookieDetails::secure).toInt()); //1 for true, 0 for false c.setValue(extractField(CookieDetails::value).toUtf8()); + if (c.domain().isEmpty()) { + QString host = extractField(CookieDetails::host); + QUrl url; + url.setScheme(c.isSecure() ? "https" : "http"); + url.setHost(host); + c.normalize(url); + } return c; } QDebug operator<<(QDebug deb, const WebEnginePartCookieJar::CookieIdentifier& id) { QDebugStateSaver saver(deb); deb << "(" << id.name << "," << id.domain << "," << id.path << ")"; return deb; } diff --git a/webenginepart/src/webenginepartcookiejar.h b/webenginepart/src/webenginepartcookiejar.h index ec2594243..f20b91f3a 100644 --- a/webenginepart/src/webenginepartcookiejar.h +++ b/webenginepart/src/webenginepartcookiejar.h @@ -1,345 +1,346 @@ /* * This file is part of the KDE project * Copyright (C) 2018 Stefano Crocco * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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. * */ #ifndef WEBENGINEPARTCOOKIEJAR_H #define WEBENGINEPARTCOOKIEJAR_H #include #include #include #include #include #include #include #include #include #include #include #include "kwebenginepartlib_export.h" class QWidget; class QWebEngineProfile; class QDBusPendingCallWatcher; /** * @brief Class which takes care of synchronizing Chromium cookies from `QWebEngineCookieStore` with KIO */ class KWEBENGINEPARTLIB_EXPORT WebEnginePartCookieJar : public QObject { Q_OBJECT public: /** * @brief Constructor * * @param [in,out] prof the profile containig the store to synchronize with * @param view the view associated with the store. Its only purpose is to obtain the `WinID` for the DBus call to `addCookies` * @param parent the parent object * @note The `PersistentCookiePolicy` of the given profile will be set to `NoPersistentCookies` so that, on application startup, * only cookies from KIO will be inside the store */ WebEnginePartCookieJar(QWebEngineProfile* prof, QObject* parent = Q_NULLPTR); /** * @brief Destructor */ ~WebEnginePartCookieJar(); private slots: /** * @brief Adds a cookie to KIO * * This slot is called in response to `QWebEngineCookieStore::cookieAdded` signal. * * @param cookie cookie the cookie to add * * @internal KIO requires an URL when adding a cookie; unfortunately, the `cookieAdded` signal doesn't provide one. To solve this problem, an URL is created * by using the scheme of #m_lastRequestOrigin and the cookie's domain. Looking At the source code for `KCookieJar`, an URL built this way should work. * In case the cookie's domain is empty (which I believe shouldn't happen), #m_lastRequestOrigin is used. * * This function also adds the cookie and its corresponding URL to #m_cookiesUrl */ void addCookie(const QNetworkCookie &cookie); /** * @brief Removes the given cookie from KIO * * This slot is called in response to `QWebEngineCookieStore::cookieRemoved` signal. * * @param cookie the cookie to remove * * @internal As for addCookie() there's a problem here, because `QWebEngineCookieStore::cookieRemoved` doesn't provide a fqdn for the cookie. This value is obtained * from #m_cookiesUrl. */ void removeCookie(const QNetworkCookie &cookie); /** * @brief Removes all session cookies from `KCookieJar` * * This function doesn't remove cookies from the `QWebEngineCookieStore`, because it's meant to be called * when the application is closing, so those cookies will be destroied automatically (because they're only stored * in memory) * */ void deleteSessionCookies(); /** * @brief Slot called when the DBus call to `KCookieServer::deleteCookie` fails * * It displays the error message on standard error * * @param watcher the object describing the result of the DBus call */ void cookieRemovalFailed(QDBusPendingCallWatcher *watcher); private: /** * @brief Determine the window ID to pass to `KCookieServer::addCookies` * * There's no sure way to find the window which received a cookie, so this function chooses the active window * if there's one (according to `QApplication::activeWindow`) or the first widget in `QApplication::topLevelWidgets` * for which `QWidget::isWindow` is `true` and which doesn't have a parent. * * @return the ID of the window determined as described above or 0 if no such a window can be found */ static qlonglong findWinID(); using CookieList = QList; /** * @brief An identifier for a cookie * * The identifier is made by the cookie's name, domain and path */ struct CookieIdentifier{ /** * @brief Default constructor */ CookieIdentifier(){} /** * @brief Constructor * * @param cookie the cookie to create the identifier for */ CookieIdentifier(const QNetworkCookie &cookie); /** * @brief Constructor * * @param n the cookie's name * @param d the cookie's domain * @param p the cookie's path */ CookieIdentifier(const QString &n, const QString &d, const QString &p); /** * Comparison operator * * Two cookies are equal if all their fields are equal * @param other the identifier to compare this identifier to * @return `true` if the two identifiers' name, domain and path are equal and `false` if at least one of them is different */ bool operator== (const CookieIdentifier &other) const {return name == other.name && domain == other.domain && path == other.path;} /** * @brief The name of the cookie * */ QString name; /** * @brief The domain of the cookie * */ QString domain; /** * @brief The path of the cookie * */ QString path; }; friend QDebug operator<<(QDebug, const CookieIdentifier &); using CookieIdentifierList = QList; /** * @brief Checks whether a cookie with the given identifier is in the KCookieJar * * @note If the _id_'s domain doesn't start with a dot, a dot is prepended to it. This is because * `KCookieJar` always wants a dot in front of the domain. * * @param id the identifiere of the cookie * @param url the origin of the cookie * @return `true` if a cookie with the given identifier is in the KCookieJar and `false` otherwise */ bool cookieInKCookieJar(const CookieIdentifier &id, const QUrl &url); /** * @brief The advice from `KCookieJar` for a URL * * @param url The URL to get the advice for * @return The advice for _url_. It can be one of `"Accept"`, `"AcceptForSession"`, `"Reject"`, `"Ask"`, `"Dunno"` or an empty * string if an error happens while contacting the `KCookieServer` */ QString askAdvice(const QUrl &url); /** * @brief Inserts all cookies contained in `KCookieJar` to the store * * @note this function should only be called by the constructor * @note this function is asynchronous because it calls `QWebEngineCookieStore::setCookie` */ void loadKIOCookies(); /** * @brief Finds all cookies stored in `KCookieJar` * @return a list of the cookies in `KCookieJar` */ CookieList findKIOCookies(); /** * @brief Enum describing the possible fields to pas to `KCookieServer::findCookies` using DBus. * * The values are the same as those of `CookieDetails` in `kio/src/kioslaves/http/kcookiejar/kcookieserver.cpp` */ enum class CookieDetails {domain=0, path=1, name=2, host=3, value=4, expirationDate=5, protocolVersion=6, secure=7}; /** * @brief Parses the value returned by `KCookieServer::findCookies` for a single cookie * * This function assumes that all possible data for the cookie is availlable (that is, that the list returned by * `KCookieServer::findCookies` contains an entry for each value in #CookieDetails) * * @param data: the data returned by `KCookieServer::findCookies`. It can contain data for more than one cookie, but only one will be parsed * @param start: the position in the list where the data for the cookie starts. * @return The cookie described by the data and its host */ static QNetworkCookie parseKIOCookie(const QStringList &data, int start); #if QTWEBENGINE_VERSION >= QT_VERSION_CHECK(5,11,0) /** * @brief Function used to filter cookies * * In theory, this function should use the configuration chosen by the user in the Cookies KCM. However, this can't be done for several reasons: * - this function doesn't have the cookies details availlable and they're needed for the "Ask" policy * - this function doesn't know the URL of the cookie (even if `QWebEngineCookieStore::FilterRequest::origin` could be used as a substitute * - if the policy is "Ask" and the question was asked here, it would be asked again when adding the cookie to `KCookieJar`. * Because of these reasons, the only setting from the KCM which is applied here is whether to accept and reject cross domain cookies. Other settings * from the KCM will be enforced by the addCookies(). * * @param req the request to filter * @return ``false` for third party cookies if the user chose to block them in the KCM and `true` otherwise * @internal Besides filtering cookie requests, this function also stores the `origin` member of request in the #m_lastRequestOrigin. * @endinternal * @sa addCookie() */ bool filterCookie(const QWebEngineCookieStore::FilterRequest &req); #endif //QTWEBENGINE_VERSION >= QT_VERSION_CHECK(5,11,0) /** - * @brief Adds a dot in front of a domain if it's not already there + * @brief Removes the domain from the cookie if the domain doesn't start with a dot * - * @param dom the domain - * @return `dom` if it already starts with a dot and `dom` preceded by a dot otherwise + * The cookies managed by QWebEngine never have an empty domain; instead, their domain starts with a dot if it was explicitly given and doesn't start + * with a dot in case it wasn't explicitly given. `KCookieServer::addCookies` and `KCookieServer::deleteCookie`, instead, require an empty domain if the + * domain wasn't explicitly given. This function transforms a cookie of the first form to one of the second form. * - * @internal - * This function is needed because KCookieJar prepends a dot to all domains not starting with one (according - * to `KCookieJar::makeCookies` in `kcookiejar.cpp`) + * If the cookie's domain starts with a dot, this function does nothing. + * + * @param cookie the cookie to remove the domain from. This cookie is modified in place */ - static inline QString prependDotToDomain(const QString &dom) {return dom.startsWith(".") ? dom : "." + dom;} + static void removeCookieDomain(QNetworkCookie &cookie); /** * @brief Tries to construct an Url for the given cookie * * The URL is meant to be passed to functions like `KCookieServer::addCookies` and is constructed from the cookie's domain * if it's not empty. If the domain is empty or it's only a dot, an empty URL is returned * * @param cookie the cookie to create the URL for * @return the URL for the cookie or an empty URL if the cookie's domain is empty or it's only a dot */ QUrl constructUrlForCookie(const QNetworkCookie &cookie) const; /** * @brief The `QWebEngineCookieStore` to synchronize KIO with */ QWebEngineCookieStore* m_cookieStore; /** * @brief The DBus interface used to connect to KCookieServer */ QDBusInterface m_cookieServer; /** * @brief The fields to pass to `KCookieStore::findCookies` via DBus */ static const QVariant s_findCookieFields; /** * @brief Overload of `qHash` for a #CookieIdentifier * * @param id: the other identifier * @param seed: the seed * @return The hash value of the identifier */ friend uint qHash(const CookieIdentifier &id, uint seed){return qHash(QStringList{id.name, id.domain, id.path}, seed);}; /** * @brief A list of cookies which were added to to the `QWebEngineCookieStore` but were rejected by KCookieJar and must * be removed from the store * * When cookieRemoved() is called with one of those cookies, the cookie is removed from this list and no attempt is made to remove * the cookie from `KCookieJar` (because it's not there) */ QVector m_pendingRejectedCookies; /** * @brief The IDs of all the windows which have session cookies */ QSet m_windowsWithSessionCookies; /** * @brief A list of cookies loaded from KCookieServer when this instance is created */ CookieList m_cookiesLoadedFromKCookieServer; }; /** * @brief Overload of operator `<<` to allow a WebEnginePartCookieJar::CookieIdentifier to be written to a `QDebug` * * @param deb: the debug object * @param id: the identifier to write * @return the debug object */ QDebug operator<<(QDebug deb, const WebEnginePartCookieJar::CookieIdentifier &id); #endif // WEBENGINEPARTCOOKIEJAR_H