1 /*
2 This file is part of the KDE project.
3
4 SPDX-FileCopyrightText: 2018 Stefano Crocco <stefano.crocco@alice.it>
5
6 SPDX-License-Identifier: LGPL-2.1-or-later
7 */
8
9 #include "webenginepartcookiejar.h"
10 #include "settings/webenginesettings.h"
11 #include <webenginepart_debug.h>
12
13 #include <QWebEngineProfile>
14 #include <QStringList>
15 #include <QDBusReply>
16 #include <QDebug>
17 #include <QWidget>
18 #include <QDateTime>
19 #include <QTimeZone>
20 #include <QApplication>
21 #include <kio_version.h>
22
23 const QVariant WebEnginePartCookieJar::s_findCookieFields = QVariant::fromValue(QList<int>{
24 static_cast<int>(CookieDetails::domain),
25 static_cast<int>(CookieDetails::path),
26 static_cast<int>(CookieDetails::name),
27 static_cast<int>(CookieDetails::host),
28 static_cast<int>(CookieDetails::value),
29 static_cast<int>(CookieDetails::expirationDate),
30 static_cast<int>(CookieDetails::protocolVersion),
31 static_cast<int>(CookieDetails::secure)
32 }
33 );
34
CookieIdentifier(const QNetworkCookie & cookie)35 WebEnginePartCookieJar::CookieIdentifier::CookieIdentifier(const QNetworkCookie& cookie):
36 name(cookie.name()), domain(cookie.domain()), path(cookie.path())
37 {
38 }
39
CookieIdentifier(const QString & n,const QString & d,const QString & p)40 WebEnginePartCookieJar::CookieIdentifier::CookieIdentifier(const QString& n, const QString& d, const QString& p):
41 name(n), domain(d), path(p)
42 {
43 }
44
WebEnginePartCookieJar(QWebEngineProfile * prof,QObject * parent)45 WebEnginePartCookieJar::WebEnginePartCookieJar(QWebEngineProfile *prof, QObject *parent):
46 QObject(parent), m_cookieStore(prof->cookieStore()),
47 m_cookieServer("org.kde.kcookiejar5", "/modules/kcookiejar", "org.kde.KCookieServer")
48 {
49 prof->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
50 connect(qApp, &QApplication::lastWindowClosed, this, &WebEnginePartCookieJar::deleteSessionCookies);
51 connect(m_cookieStore, &QWebEngineCookieStore::cookieAdded, this, &WebEnginePartCookieJar::addCookie);
52 connect(m_cookieStore, &QWebEngineCookieStore::cookieRemoved, this, &WebEnginePartCookieJar::removeCookie);
53 if(!m_cookieServer.isValid()){
54 qCDebug(WEBENGINEPART_LOG) << "Couldn't connect to KCookieServer";
55 }
56
57 loadKIOCookies();
58
59 auto filter = [this](const QWebEngineCookieStore::FilterRequest &req){return filterCookie(req);};
60 m_cookieStore->setCookieFilter(filter);
61 }
62
~WebEnginePartCookieJar()63 WebEnginePartCookieJar::~WebEnginePartCookieJar()
64 {
65 }
66
filterCookie(const QWebEngineCookieStore::FilterRequest & req)67 bool WebEnginePartCookieJar::filterCookie(const QWebEngineCookieStore::FilterRequest& req)
68 {
69 return WebEngineSettings::self()->acceptCrossDomainCookies() || !req.thirdParty;
70 }
71
deleteSessionCookies()72 void WebEnginePartCookieJar::deleteSessionCookies()
73 {
74 if (!m_cookieServer.isValid()) {
75 return;
76 }
77 foreach(qlonglong id, m_windowsWithSessionCookies) {
78 m_cookieServer.call(QDBus::NoBlock, "deleteSessionCookies", id);
79 }
80 }
81
constructUrlForCookie(const QNetworkCookie & cookie) const82 QUrl WebEnginePartCookieJar::constructUrlForCookie(const QNetworkCookie& cookie) const
83 {
84 QUrl url;
85 QString domain = cookie.domain().startsWith(".") ? cookie.domain().mid(1) : cookie.domain();
86 if (!domain.isEmpty()) {
87 url.setScheme("http");
88 url.setHost(domain);
89 url.setPath(cookie.path());
90 } else {
91 qCDebug(WEBENGINEPART_LOG) << "EMPTY COOKIE DOMAIN for" << cookie.name();
92 }
93 return url;
94 }
95
findWinID()96 qlonglong WebEnginePartCookieJar::findWinID()
97 {
98 QWidget *mainWindow = qApp->activeWindow();
99 if (mainWindow && !mainWindow->windowFlags().testFlag(Qt::Dialog)) {
100 return mainWindow->winId();
101 } else {
102 QWidgetList windows = qApp->topLevelWidgets();
103 foreach(QWidget *w, windows){
104 if (!w->windowFlags().testFlag(Qt::Dialog)) {
105 return w->winId();
106 }
107 }
108 }
109 return 0;
110 }
111
removeCookieDomain(QNetworkCookie & cookie)112 void WebEnginePartCookieJar::removeCookieDomain(QNetworkCookie& cookie)
113 {
114 if (!cookie.domain().startsWith('.')) {
115 cookie.setDomain(QString());
116 }
117 }
118
addCookie(const QNetworkCookie & _cookie)119 void WebEnginePartCookieJar::addCookie(const QNetworkCookie& _cookie)
120 {
121 //If the added cookie is in m_cookiesLoadedFromKCookieServer, it means
122 //we're loading the cookie from KCookieServer (from the call to loadKIOCookies
123 //in the constructor (QWebEngineCookieStore::setCookie is asynchronous, though,
124 //so we're not in the constructor anymore)), so don't attempt to add
125 //the cookie back to KCookieServer; instead, remove it from the list.
126 if (m_cookiesLoadedFromKCookieServer.removeOne(_cookie)) {
127 return;
128 }
129
130 #ifdef BUILD_TESTING
131 m_testCookies.clear();
132 #endif
133
134 QNetworkCookie cookie(_cookie);
135 CookieIdentifier id(cookie);
136
137 if (!m_cookieServer.isValid()) {
138 return;
139 }
140
141 if (cookie.expirationDate().isValid()) {
142 //There's a bug in KCookieJar which causes the expiration date to be interpreted as local time
143 //instead of GMT as it should. The bug is fixed in KIO 5.50
144 }
145 QUrl url = constructUrlForCookie(cookie);
146 if (url.isEmpty()) {
147 return;
148 }
149 //NOTE: the removal of the domain (when not starting with a dot) must be done *after* creating
150 //the URL, as constructUrlForCookie needs the domain
151 removeCookieDomain(cookie);
152 QByteArray header("Set-Cookie: ");
153 header += cookie.toRawForm();
154 header += "\n";
155 qlonglong winId = findWinID();
156 if (!cookie.expirationDate().isValid()) {
157 m_windowsWithSessionCookies.insert(winId);
158 }
159 // qCDebug(WEBENGINEPART_LOG) << url;
160 QString advice = askAdvice(url);
161 if (advice == "Reject"){
162 m_pendingRejectedCookies << CookieIdentifier(_cookie);
163 m_cookieStore->deleteCookie(_cookie);
164 } else if (advice == "AcceptForSession" && !cookie.isSessionCookie()) {
165 cookie.setExpirationDate(QDateTime());
166 addCookie(cookie);
167 } else {
168 int oldTimeout = m_cookieServer.timeout();
169 if (advice == "Ask") {
170 //Give the user time (10 minutes = 600 000ms) to analyze the cookie
171 m_cookieServer.setTimeout(10*60*1000);
172 }
173 m_cookieServer.call(QDBus::Block, "addCookies", url.toString(), header, winId);
174 m_cookieServer.setTimeout(oldTimeout);
175 if (m_cookieServer.lastError().isValid()) {
176 qCDebug(WEBENGINEPART_LOG) << m_cookieServer.lastError();
177 return;
178 }
179 if (!advice.startsWith("Accept") && !cookieInKCookieJar(id, url)) {
180 m_pendingRejectedCookies << id;
181 m_cookieStore->deleteCookie(_cookie);
182 }
183 }
184 }
185
askAdvice(const QUrl & url)186 QString WebEnginePartCookieJar::askAdvice(const QUrl& url)
187 {
188 if (!m_cookieServer.isValid()) {
189 return QString();
190 }
191 QDBusReply<QString> rep = m_cookieServer.call(QDBus::Block, "getDomainAdvice", url.toString());
192 if (rep.isValid()) {
193 return rep.value();
194 } else {
195 qCDebug(WEBENGINEPART_LOG) << rep.error().message();
196 return QString();
197 }
198 }
199
cookieInKCookieJar(const WebEnginePartCookieJar::CookieIdentifier & id,const QUrl & url)200 bool WebEnginePartCookieJar::cookieInKCookieJar(const WebEnginePartCookieJar::CookieIdentifier& id, const QUrl& url)
201 {
202 if (!m_cookieServer.isValid()) {
203 return false;
204 }
205 QList<int> fields = {
206 static_cast<int>(CookieDetails::name),
207 static_cast<int>(CookieDetails::domain),
208 static_cast<int>(CookieDetails::path)
209 };
210 QDBusReply<QStringList> rep = m_cookieServer.call(QDBus::Block, "findCookies", QVariant::fromValue(fields), id.domain, url.toString(QUrl::FullyEncoded), id.path, id.name);
211 if (!rep.isValid()) {
212 qCDebug(WEBENGINEPART_LOG) << rep.error().message();
213 return false;
214 }
215 QStringList cookies = rep.value();
216 for(int i = 0; i < cookies.length()-2; i+=3){
217 if (CookieIdentifier(cookies.at(i), cookies.at(i+1), cookies.at(i+2)) == id) {
218 return true;
219 }
220 }
221 return false;
222 }
223
removeCookie(const QNetworkCookie & _cookie)224 void WebEnginePartCookieJar::removeCookie(const QNetworkCookie& _cookie)
225 {
226
227 int pos = m_pendingRejectedCookies.indexOf(CookieIdentifier(_cookie));
228 //Ignore pending cookies
229 if (pos >= 0) {
230 m_pendingRejectedCookies.takeAt(pos);
231 return;
232 }
233
234 if (!m_cookieServer.isValid()) {
235 return;
236 }
237
238 QNetworkCookie cookie(_cookie);
239 QUrl url = constructUrlForCookie(cookie);
240 if(url.isEmpty()){
241 qCDebug(WEBENGINEPART_LOG) << "Can't remove cookie" << cookie.name() << "because its URL isn't known";
242 return;
243 }
244 removeCookieDomain(cookie);
245
246 QDBusPendingCall pcall = m_cookieServer.asyncCall("deleteCookie", cookie.domain(), url.host(), cookie.path(), QString(cookie.name()));
247 QDBusPendingCallWatcher *w = new QDBusPendingCallWatcher(pcall, this);
248 connect(w, &QDBusPendingCallWatcher::finished, this, &WebEnginePartCookieJar::cookieRemovalFailed);
249 }
250
cookieRemovalFailed(QDBusPendingCallWatcher * watcher)251 void WebEnginePartCookieJar::cookieRemovalFailed(QDBusPendingCallWatcher *watcher)
252 {
253 QDBusPendingReply<> r = *watcher;
254 if (r.isError()){
255 qCDebug(WEBENGINEPART_LOG) << "DBus error:" << r.error().message();
256 }
257 watcher->deleteLater();
258 }
259
loadKIOCookies()260 void WebEnginePartCookieJar::loadKIOCookies()
261 {
262 const CookieUrlList cookies = findKIOCookies();
263 for (const CookieWithUrl& cookieWithUrl : cookies){
264 QNetworkCookie cookie = cookieWithUrl.cookie;
265 QDateTime currentTime = QDateTime::currentDateTime();
266 //Don't attempt to add expired cookies
267 if (cookie.expirationDate().isValid() && cookie.expirationDate() < currentTime) {
268 continue;
269 }
270 QNetworkCookie normalizedCookie(cookie);
271 normalizedCookie.normalize(cookieWithUrl.url);
272 m_cookiesLoadedFromKCookieServer << cookie;
273 #ifdef BUILD_TESTING
274 m_testCookies << cookie;
275 #endif
276 m_cookieStore->setCookie(cookie, cookieWithUrl.url);
277 }
278 }
279
findKIOCookies()280 WebEnginePartCookieJar::CookieUrlList WebEnginePartCookieJar::findKIOCookies()
281 {
282 CookieUrlList res;
283 if (!m_cookieServer.isValid()) {
284 return res;
285 }
286 QDBusReply<QStringList> rep = m_cookieServer.call(QDBus::Block, "findDomains");
287 if(!rep.isValid()){
288 qCDebug(WEBENGINEPART_LOG) << rep.error().message();
289 return res;
290 }
291 QStringList domains = rep.value();
292 uint fieldsCount = 8;
293 foreach( const QString &d, domains){
294 QDBusReply<QStringList> rep = m_cookieServer.call(QDBus::Block, "findCookies", s_findCookieFields, d, "", "", "");
295 if (!rep.isValid()) {
296 qCDebug(WEBENGINEPART_LOG) << rep.error().message();
297 return res;
298 }
299 QStringList data = rep.value();
300 for(int i = 0; i < data.count(); i+=fieldsCount){
301 res << parseKIOCookie(data, i);
302 }
303 }
304 return res;
305 }
306
307 //This function used to return a normalized cookie. However, doing so doesn't work correctly because QWebEngineCookieStore::setCookie
308 //in turns normalizes the cookie. One could think that calling normalize twice wouldn't be a problem, but that would be wrong. If the cookie
309 //domain is originally empty, after a call to normalize it will contain the cookie origin host. The second call to normalize will see a cookie
310 //whose domain is not empty and doesn't start with a dot and will add a dot to it.
parseKIOCookie(const QStringList & data,int start)311 WebEnginePartCookieJar::CookieWithUrl WebEnginePartCookieJar::parseKIOCookie(const QStringList& data, int start)
312 {
313 QNetworkCookie c;
314 auto extractField = [data, start](CookieDetails field){return data.at(start + static_cast<int>(field));};
315 c.setDomain(extractField(CookieDetails::domain));
316 c.setExpirationDate(QDateTime::fromSecsSinceEpoch(extractField(CookieDetails::expirationDate).toInt()));
317 c.setName(extractField(CookieDetails::name).toUtf8());
318 QString path = extractField(CookieDetails::path);
319 c.setPath(path);
320 c.setSecure(extractField(CookieDetails::secure).toInt()); //1 for true, 0 for false
321 c.setValue(extractField(CookieDetails::value).toUtf8());
322
323 QString host = extractField(CookieDetails::host);
324 QUrl url;
325 url.setScheme(c.isSecure() ? "https" : "http");
326 url.setHost(host);
327 url.setPath(path);
328 return CookieWithUrl{c, url};
329 }
330
operator <<(QDebug deb,const WebEnginePartCookieJar::CookieIdentifier & id)331 QDebug operator<<(QDebug deb, const WebEnginePartCookieJar::CookieIdentifier& id)
332 {
333 QDebugStateSaver saver(deb);
334 deb << "(" << id.name << "," << id.domain << "," << id.path << ")";
335 return deb;
336 }
337