diff --git a/webenginepart/autotests/CMakeLists.txt b/webenginepart/autotests/CMakeLists.txt index e115f8b85..01bf10cf8 100644 --- a/webenginepart/autotests/CMakeLists.txt +++ b/webenginepart/autotests/CMakeLists.txt @@ -1,17 +1,19 @@ +add_definitions(-DBUILD_TESTING) + include(ECMAddTests) find_package(Qt5Test ${QT_MIN_VERSION} CONFIG REQUIRED) macro(webenginepart_unit_tests) foreach(_testname ${ARGN}) ecm_add_test(${_testname}.cpp TEST_NAME ${_testname} LINK_LIBRARIES kwebenginepartlib Qt5::Test) endforeach() endmacro(webenginepart_unit_tests) webenginepart_unit_tests( webengine_partapi_test webenginepartcookiejar_test ) target_link_libraries(webenginepartcookiejar_test Qt5::DBus) diff --git a/webenginepart/autotests/webenginepartcookiejar_test.cpp b/webenginepart/autotests/webenginepartcookiejar_test.cpp index a02c98dbe..97f674f18 100644 --- a/webenginepart/autotests/webenginepartcookiejar_test.cpp +++ b/webenginepart/autotests/webenginepartcookiejar_test.cpp @@ -1,276 +1,358 @@ /* * 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 +#include + +//Cookie expiration dates returned by KCookieServer always have msecs set to 0 +static QDateTime currentDateTime(){return QDateTime::fromSecsSinceEpoch(QDateTime::currentMSecsSinceEpoch()/1000);} + +namespace QTest { + template <> + char *toString(const QNetworkCookie &cookie){ + QByteArray ba = "QNetworkCookie{"; + ba += "\nname: " + cookie.name(); + ba += "\ndomain: " + (cookie.domain().isEmpty() ? "" : cookie.domain()); + ba += "\npath: " + (cookie.path().isEmpty() ? "" : cookie.path()); + ba += "\nvalue: " + cookie.value(); + ba += "\nexpiration: " + (cookie.expirationDate().isValid() ? QString::number(cookie.expirationDate().toMSecsSinceEpoch()) : ""); + ba += "\nsecure: " + QString::number(cookie.isSecure()); + ba += "\nhttp: only" + QString::number(cookie.isHttpOnly()); + return qstrdup(ba.data()); + } +} + 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 = nullptr; delete m_jar; m_jar = nullptr; m_store = nullptr; delete m_profile; m_profile = 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(); QVERIFY(!resFields.isEmpty()); QCOMPARE(fields.count(), resFields.count()); QCOMPARE(resFields.at(0), domain); QCOMPARE(resFields.at(1), path); QCOMPARE(resFields.at(2), name); 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; - - //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())); + QDBusError e = addCookieToKCookieServer(cookie, host); + QVERIFY2(!e.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, host, "", ""); QVERIFY2(reply.isValid(), qPrintable(reply.error().message())); QStringList cookies = reply.value(); 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"); } + +QDBusError TestWebEnginePartCookieJar::addCookieToKCookieServer(const QNetworkCookie& _cookie, const QString& host) +{ + QNetworkCookie cookie(_cookie); + QUrl url; + url.setHost(host); + url.setScheme(cookie.isSecure() ? "https" : "http"); + if (!cookie.domain().startsWith('.')) { + cookie.setDomain(QString()); + } + const QByteArray setCookie = "Set-Cookie: " + cookie.toRawForm(); + m_server->call(QDBus::Block, "addCookies", url.toString(), setCookie, static_cast(0)); + return m_server->lastError(); +} + +void TestWebEnginePartCookieJar::testPersistentCookiesAreAddedToStoreOnCreation() +{ + delete m_jar; + QDateTime exp = QDateTime::currentDateTime().addYears(1); + QString baseCookieName = m_cookieName + "-startup"; + QList data { + {baseCookieName + "-persistent", "test-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", currentDateTime().addYears(1), true}, + {baseCookieName + "-no-path", "test-value", ".yyy.xxx.com", "", "zzz.yyy.xxx.com", currentDateTime().addYears(1), true}, + {baseCookieName + "-no-domain", "test-value", "", "/abc/def/", "zzz.yyy.xxx.com", currentDateTime().addYears(1), true}, + {baseCookieName + "-no-secure", "test-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", currentDateTime().addYears(1), false} + }; + QList expected; + for(const CookieData &d: data){ + QNetworkCookie c = d.cookie(); + QDBusError e = addCookieToKCookieServer(c, d.host); + QVERIFY2(!e.isValid(), qPrintable(e.message())); + //In case of an empty domain, WebEnginePartCookieJar will use QNetworkCookie::normalize on the cookie + if (c.domain().isEmpty()) { + c.setDomain(d.host); + } + expected << c; + } + m_jar = new WebEnginePartCookieJar(m_profile, this); + QList cookiesInsertedIntoJar; + for(const QNetworkCookie &c: qAsConst(m_jar->m_testCookies)){ + if(QString(c.name()).startsWith(baseCookieName)) { + cookiesInsertedIntoJar << c; + } + } + + //Ensure that cookies in the two lists are in the same order before comparing them + //(the order in cookiesInsertedIntoJar depends on the order KCookieServer::findCookies + //returns them) + auto sortLambda = [](const QNetworkCookie &c1, const QNetworkCookie &c2){ + return c1.name() < c2.name(); + }; + std::sort(cookiesInsertedIntoJar.begin(), cookiesInsertedIntoJar.end(), sortLambda); + std::sort(expected.begin(), expected.end(), sortLambda); + + QCOMPARE(cookiesInsertedIntoJar, expected); +} + +void TestWebEnginePartCookieJar::testSessionCookiesAreNotAddedToStoreOnCreation() +{ + delete m_jar; + CookieData data{m_cookieName + "-startup-session", "test-value", ".yyy.xxx.com", "/abc/def", "zzz.yyy.xxx.com", QDateTime(), true}; + QDBusError e = addCookieToKCookieServer(data.cookie(), data.host); + QVERIFY2(!e.isValid(), qPrintable(e.message())); + m_jar = new WebEnginePartCookieJar(m_profile, this); + QList cookiesInsertedIntoJar; + for(const QNetworkCookie &c: qAsConst(m_jar->m_testCookies)) { + if (c.name() == data.name) { + cookiesInsertedIntoJar << c; + } + } + QVERIFY2(cookiesInsertedIntoJar.isEmpty(), "Session cookies inserted into cookie store"); +} + diff --git a/webenginepart/autotests/webenginepartcookiejar_test.h b/webenginepart/autotests/webenginepartcookiejar_test.h index 7a64a54a9..3417a0798 100644 --- a/webenginepart/autotests/webenginepartcookiejar_test.h +++ b/webenginepart/autotests/webenginepartcookiejar_test.h @@ -1,76 +1,91 @@ /* * 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 TESTWEBENGINEPARTCOOKIEJAR_H #define TESTWEBENGINEPARTCOOKIEJAR_H #include #include +#include class QWebEngineCookieStore; class WebEnginePartCookieJar; class QNetworkCookie; class QWebEngineProfile; class QDBusInterface; class TestWebEnginePartCookieJar : public QObject { Q_OBJECT private: struct CookieData { QString name; QString value; QString domain; QString path; QString host; QDateTime expiration; bool secure; QNetworkCookie cookie() const; }; private Q_SLOTS: void init(); void initTestCase(); void cleanup(); void testCookieAddedToStoreAreAddedToKCookieServer_data(); void testCookieAddedToStoreAreAddedToKCookieServer(); void testCookieRemovedFromStoreAreRemovedFromKCookieServer_data(); void testCookieRemovedFromStoreAreRemovedFromKCookieServer(); + void testPersistentCookiesAreAddedToStoreOnCreation(); + void testSessionCookiesAreNotAddedToStoreOnCreation(); private: + /** + * @brief Adds a cookie to KCookieServer + * + * The cookie is supposed to be in `QWebEngineStore` "format", that is its domain must not be empty; + * a domain not starting with a dot means that the domain field wasn't given in the `Set-Cookie` header. + * + * @param _cookie the cookie to add + * @param host the host where the cookie come from + * @return QDBusError the error returned by DBus when adding the cookie. If no error occurred, this object + * will be invalid + */ + QDBusError addCookieToKCookieServer(const QNetworkCookie &_cookie, const QString &host); void deleteCookies(const QList &cookies); QList findTestCookies(); - QString m_cookieName; + QString m_cookieName; QWebEngineCookieStore *m_store; WebEnginePartCookieJar *m_jar; QWebEngineProfile *m_profile; QDBusInterface *m_server; }; #endif // TESTWEBENGINEPARTCOOKIEJAR_H diff --git a/webenginepart/src/CMakeLists.txt b/webenginepart/src/CMakeLists.txt index e28474b56..091845db3 100644 --- a/webenginepart/src/CMakeLists.txt +++ b/webenginepart/src/CMakeLists.txt @@ -1,74 +1,78 @@ find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Wallet) add_definitions(-DTRANSLATION_DOMAIN=\"webenginepart\") +if(BUILD_TESTING) + add_definitions(-DBUILD_TESTING) +endif(BUILD_TESTING) + include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR}) set(kwebenginepartlib_LIB_SRCS webenginepart.cpp webenginepart_ext.cpp webengineview.cpp webenginepage.cpp websslinfo.cpp webhistoryinterface.cpp webenginepartdownloadmanager.cpp webenginewallet.cpp webengineparterrorschemehandler.cpp webengineparthtmlembedder.cpp webenginepartkiohandler.cpp webenginepartcookiejar.cpp settings/webenginesettings.cpp settings/webengine_filter.cpp ui/searchbar.cpp ui/passwordbar.cpp ui/featurepermissionbar.cpp ) qt5_wrap_ui(kwebenginepartlib_LIB_SRCS ui/searchbar.ui ) ecm_qt_declare_logging_category(kwebenginepartlib_LIB_SRCS HEADER webenginepart_debug.h IDENTIFIER WEBENGINEPART_LOG CATEGORY_NAME org.kde.webenginepart ) add_library(kwebenginepartlib ${kwebenginepartlib_LIB_SRCS}) generate_export_header(kwebenginepartlib) target_link_libraries(kwebenginepartlib PUBLIC Qt5::Core Qt5::DBus Qt5::Gui Qt5::Widgets Qt5::WebEngineWidgets KF5::Parts KF5::Wallet PRIVATE Qt5::PrintSupport KF5::SonnetCore KF5::IconThemes #for KIconLoader used by WebEnginePartErrorSchemeHandler KF5::WindowSystem # for KUserTimestamp ) target_include_directories(kwebenginepartlib PUBLIC "$" ) set_target_properties(kwebenginepartlib PROPERTIES OUTPUT_NAME kwebenginepart) install(TARGETS kwebenginepartlib ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) add_library(webenginepart MODULE webenginepartfactory.cpp) kcoreaddons_desktop_to_json(webenginepart webenginepart.desktop) target_link_libraries(webenginepart kwebenginepartlib) install(TARGETS webenginepart DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf5/parts) install(FILES webenginepart.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) install(FILES webenginepart.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/webenginepart) install(FILES error.html DESTINATION ${KDE_INSTALL_DATADIR}/webenginepart) diff --git a/webenginepart/src/webenginepartcookiejar.cpp b/webenginepart/src/webenginepartcookiejar.cpp index 4ce8fbabe..381c4afaa 100644 --- a/webenginepart/src/webenginepartcookiejar.cpp +++ b/webenginepart/src/webenginepartcookiejar.cpp @@ -1,347 +1,355 @@ /* * This file is part of the KDE project. * * Copyright (C) 2018 Stefano Crocco * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the * Free Software Foundation; either version 2.1 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 Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "webenginepartcookiejar.h" #include "settings/webenginesettings.h" #include #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()){ qCDebug(WEBENGINEPART_LOG) << "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 { qCDebug(WEBENGINEPART_LOG) << "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; } +#ifdef BUILD_TESTING + m_testCookies.clear(); +#endif + 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); } // qCDebug(WEBENGINEPART_LOG) << 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()) { qCDebug(WEBENGINEPART_LOG) << 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 { qCDebug(WEBENGINEPART_LOG) << rep.error().message(); return QString(); } } bool WebEnginePartCookieJar::cookieInKCookieJar(const WebEnginePartCookieJar::CookieIdentifier& id, const QUrl& url) { if (!m_cookieServer.isValid()) { return false; } 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()) { qCDebug(WEBENGINEPART_LOG) << 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) { 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()){ qCDebug(WEBENGINEPART_LOG) << "Can't remove cookie" << cookie.name() << "because its URL isn't known"; return; } removeCookieDomain(cookie); 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()){ qCDebug(WEBENGINEPART_LOG) << "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; +#ifdef BUILD_TESTING + m_testCookies << cookie; +#endif 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()){ qCDebug(WEBENGINEPART_LOG) << 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()) { qCDebug(WEBENGINEPART_LOG) << 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 8de72a430..e6e80af3b 100644 --- a/webenginepart/src/webenginepartcookiejar.h +++ b/webenginepart/src/webenginepartcookiejar.h @@ -1,342 +1,347 @@ /* * This file is part of the KDE project. * * Copyright (C) 2018 Stefano Crocco * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the * Free Software Foundation; either version 2.1 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 Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #ifndef WEBENGINEPARTCOOKIEJAR_H #define WEBENGINEPARTCOOKIEJAR_H #include #include #include #include #include #include #include #include #include #include #include #include "kwebenginepartlib_export.h" class QDebug; 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 containing the store to synchronize with * @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 = nullptr); /** * @brief Destructor */ ~WebEnginePartCookieJar() override; 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 destroyed 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 available (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 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 Removes the domain from the cookie if the domain doesn't start with a dot * * 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. * * 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 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 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; +#ifdef BUILD_TESTING + QList m_testCookies; + friend class TestWebEnginePartCookieJar; +#endif + }; /** * @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