File indexing completed on 2024-12-22 04:43:20

0001 /*
0002     This file is part of the KDE project.
0003 
0004     SPDX-FileCopyrightText: 2018 Stefano Crocco <stefano.crocco@alice.it>
0005 
0006     SPDX-License-Identifier: LGPL-2.1-or-later
0007 */
0008 
0009 #ifndef WEBENGINEPARTCOOKIEJAR_KIO_H
0010 #define WEBENGINEPARTCOOKIEJAR_KIO_H
0011 
0012 #include "qtwebengine6compat.h"
0013 
0014 #include <QObject>
0015 #include <QNetworkCookie>
0016 #include <QHash>
0017 #include <QUrl>
0018 #include <QWebEngineCookieStore>
0019 #include <QStringList>
0020 #include <QVariant>
0021 #include <QVector>
0022 #include <QDBusInterface>
0023 #include <QSet>
0024 
0025 #include "kwebenginepartlib_export.h"
0026 
0027 class QDebug;
0028 class QWidget;
0029 class QWebEngineProfile;
0030 class QDBusPendingCallWatcher;
0031 
0032 /**
0033  * @brief Class which takes care of synchronizing Chromium cookies from `QWebEngineCookieStore` with KIO
0034  */
0035 class KWEBENGINEPARTLIB_EXPORT WebEnginePartCookieJarKIO : public QObject
0036 {
0037     Q_OBJECT
0038 
0039 public:
0040     
0041     /**
0042     * @brief Constructor
0043     * 
0044     * @param [in,out] prof the profile containing the store to synchronize with
0045     * @param parent the parent object
0046     * @note The `PersistentCookiePolicy` of the given profile will be set to `NoPersistentCookies` so that, on application startup, 
0047     * only cookies from KIO will be inside the store
0048     */
0049     WebEnginePartCookieJarKIO(QWebEngineProfile* prof, QObject* parent = nullptr);
0050 
0051     /**
0052      * @brief Destructor
0053      */
0054     ~WebEnginePartCookieJarKIO() override;
0055 
0056 private slots:
0057     
0058     /**
0059     * @brief Adds a cookie to KIO
0060     * 
0061     * This slot is called in response to `QWebEngineCookieStore::cookieAdded` signal.
0062     * 
0063     * @param cookie cookie the cookie to add
0064     * 
0065     * @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
0066     * 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.
0067     * In case the cookie's domain is empty (which I believe shouldn't happen), #m_lastRequestOrigin is used.
0068     * 
0069     * This function also adds the cookie and its corresponding URL to #m_cookiesUrl
0070     */
0071     void addCookie(const QNetworkCookie &cookie);
0072     
0073     /**
0074     * @brief Removes the given cookie from KIO
0075     * 
0076     * This slot is called in response to `QWebEngineCookieStore::cookieRemoved` signal.
0077     * 
0078     * @param cookie the cookie to remove
0079     * 
0080     * @internal As for addCookie() there's a problem here, because `QWebEngineCookieStore::cookieRemoved` doesn't provide a fqdn for the cookie. This value is obtained 
0081     * from #m_cookiesUrl.
0082     */
0083     void removeCookie(const QNetworkCookie &cookie);
0084     
0085     /**
0086     * @brief Removes all session cookies from `KCookieJar`
0087     * 
0088     * This function doesn't remove cookies from the `QWebEngineCookieStore`, because it's meant to be called
0089     * when the application is closing, so those cookies will be destroyed automatically (because they're only stored
0090     * in memory)
0091     * 
0092     */
0093     void deleteSessionCookies();
0094     
0095     /**
0096      * @brief Slot called when the DBus call to `KCookieServer::deleteCookie` fails
0097      * 
0098      * It displays the error message on standard error
0099      * 
0100      * @param watcher the object describing the result of the DBus call
0101      */
0102     void cookieRemovalFailed(QDBusPendingCallWatcher *watcher);
0103     
0104 private:
0105     
0106     /**
0107     * @brief Determine the window ID to pass to `KCookieServer::addCookies`
0108     *
0109     * There's no sure way to find the window which received a cookie, so this function chooses the active window
0110     * if there's one (according to `QApplication::activeWindow`) or the first widget in `QApplication::topLevelWidgets`
0111     * for which `QWidget::isWindow` is `true` and which doesn't have a parent.
0112     * 
0113     * @return the ID of the window determined as described above or 0 if no such a window can be found
0114     */
0115     static qlonglong findWinID();
0116 
0117     struct CookieWithUrl {
0118         QNetworkCookie cookie;
0119         QUrl url;
0120     };
0121 
0122     using CookieUrlList = QVector<CookieWithUrl>;
0123     using CookieList = QVector<QNetworkCookie>;
0124 
0125     /**
0126     * @brief An identifier for a cookie
0127     * 
0128     * The identifier is made by the cookie's name, domain and path
0129     */
0130     struct CookieIdentifier{
0131         
0132         /**
0133         * @brief Default constructor
0134         */
0135         CookieIdentifier(){}
0136         
0137         /**
0138         * @brief Constructor
0139         * 
0140         * @param cookie the cookie to create the identifier for
0141         */
0142         CookieIdentifier(const QNetworkCookie &cookie);
0143         
0144         /**
0145         * @brief Constructor
0146         * 
0147         * @param n the cookie's name
0148         * @param d the cookie's domain
0149         * @param p the cookie's path
0150         */
0151         CookieIdentifier(const QString &n, const QString &d, const QString &p);
0152        
0153         /**
0154          * Comparison operator
0155          * 
0156          * Two cookies are equal if all their fields are equal
0157          * @param other the identifier to compare this identifier to
0158          * @return `true` if the two identifiers' name, domain and path are equal and `false` if at least one of them is different
0159          */
0160         bool operator== (const CookieIdentifier &other) const {return name == other.name && domain == other.domain && path == other.path;}
0161         
0162         /**
0163         * @brief The name of the cookie
0164         * 
0165         */
0166         QString name;
0167         /**
0168         * @brief The domain of the cookie
0169         * 
0170         */
0171         QString domain;
0172         
0173         /**
0174         * @brief The path of the cookie
0175         * 
0176         */
0177         QString path;
0178         
0179     };
0180     
0181     friend QDebug operator<<(QDebug, const CookieIdentifier &);
0182     
0183     using CookieIdentifierList = QList<CookieIdentifier>;
0184 
0185     /**
0186     * @brief Checks whether a cookie with the given identifier is in the KCookieJar
0187     * 
0188     * @note If the _id_'s domain doesn't start with a dot, a dot is prepended to it. This is because
0189     * `KCookieJar` always wants a dot in front of the domain.
0190     * 
0191     * @param id the identifiere of the cookie
0192     * @param url the origin of the cookie
0193     * @return `true` if a cookie with the given identifier is in the KCookieJar and `false` otherwise
0194     */
0195     bool cookieInKCookieJar(const CookieIdentifier &id, const QUrl &url);
0196     
0197     /**
0198     * @brief The advice from `KCookieJar` for a URL
0199     * 
0200     * @param url The URL to get the advice for
0201     * @return The advice for _url_. It can be one of `"Accept"`, `"AcceptForSession"`, `"Reject"`, `"Ask"`, `"Dunno"` or an empty
0202     * string if an error happens while contacting the `KCookieServer`
0203     */
0204     QString askAdvice(const QUrl &url);
0205     
0206     /**
0207     * @brief Inserts all cookies contained in `KCookieJar` to the store
0208     * 
0209     * @note this function should only be called by the constructor
0210     * @note this function is asynchronous because it calls `QWebEngineCookieStore::setCookie`
0211     */
0212     void loadKIOCookies();
0213     
0214     /**
0215     * @brief Finds all cookies stored in `KCookieJar`
0216     * @return a list of the cookies in `KCookieJar`
0217     */
0218     CookieUrlList findKIOCookies();
0219     
0220     /**
0221     * @brief Enum describing the possible fields to pas to `KCookieServer::findCookies` using DBus.
0222     * 
0223     * The values are the same as those of `CookieDetails` in `kio/src/kioslaves/http/kcookiejar/kcookieserver.cpp`
0224     */ 
0225     enum class CookieDetails {domain=0, path=1, name=2, host=3, value=4, expirationDate=5, protocolVersion=6, secure=7};
0226     
0227     /**
0228     * @brief Parses the value returned by `KCookieServer::findCookies` for a single cookie
0229     * 
0230     * This function assumes that all possible data for the cookie is available (that is, that the list returned by 
0231     * `KCookieServer::findCookies` contains an entry for each value in #CookieDetails)
0232     * 
0233     * @param data: the data returned by `KCookieServer::findCookies`. It can contain data for more than one cookie, but only one will be parsed
0234     * @param start: the position in the list where the data for the cookie starts.
0235     * @return The cookie described by the data and its host
0236     */
0237     static CookieWithUrl parseKIOCookie(const QStringList &data, int start);
0238     
0239     /**
0240     * @brief Function used to filter cookies
0241     * 
0242     * 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:
0243     * - this function doesn't have the cookies details  and they're needed for the "Ask" policy
0244     * - this function doesn't know the URL of the cookie (even if `QWebEngineCookieStore::FilterRequest::origin` could be used as a substitute
0245     * - if the policy is "Ask" and the question was asked here, it would be asked again when adding the cookie to `KCookieJar`.
0246     * 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
0247     * from the KCM will be enforced by the addCookies().
0248     * 
0249     * @param req the request to filter
0250     * @return ``false` for third party cookies if the user chose to block them in the KCM and `true` otherwise
0251     * @internal Besides filtering cookie requests, this function also stores the `origin` member of request in the #m_lastRequestOrigin.
0252     * @endinternal
0253     * @sa addCookie()
0254     */
0255     bool filterCookie(const QWebEngineCookieStore::FilterRequest &req);
0256     
0257     /**
0258     * @brief Removes the domain from the cookie if the domain doesn't start with a dot
0259     * 
0260     * 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
0261     * with a dot in case it wasn't explicitly given. `KCookieServer::addCookies` and `KCookieServer::deleteCookie`, instead, require an empty domain if the
0262     * domain wasn't explicitly given. This function transforms a cookie of the first form to one of the second form.
0263     * 
0264     * If the cookie's domain starts with a dot, this function does nothing.
0265     * 
0266     * @param cookie the cookie to remove the domain from. This cookie is modified in place
0267     */
0268     static void removeCookieDomain(QNetworkCookie &cookie);
0269     
0270     /**
0271     * @brief Tries to construct an Url for the given cookie
0272     * 
0273     * The URL is meant to be passed to functions like `KCookieServer::addCookies` and is constructed from the cookie's domain
0274     * if it's not empty. If the domain is empty or it's only a dot, an empty URL is returned
0275     * 
0276     * @param cookie the cookie to create the URL for
0277     * @return the URL for the cookie or an empty URL if the cookie's domain is empty or it's only a dot
0278     */
0279     QUrl constructUrlForCookie(const QNetworkCookie &cookie) const;
0280     
0281     /**
0282     * @brief The `QWebEngineCookieStore` to synchronize KIO with
0283     */
0284     QWebEngineCookieStore* m_cookieStore;
0285     
0286     /**
0287     * @brief The DBus interface used to connect to KCookieServer
0288     */
0289     QDBusInterface m_cookieServer;
0290     
0291     /**
0292     * @brief The fields to pass to `KCookieStore::findCookies` via DBus
0293     */
0294     static const QVariant s_findCookieFields;
0295     
0296     /**
0297     * @brief Overload of `qHash` for a CookieIdentifier
0298     * 
0299     * @param id: the other identifier
0300     * @param seed: the seed
0301     * @return The hash value of the identifier
0302     */
0303     friend qHashReturnType qHash(const CookieIdentifier &id, uint seed){return qHash(QStringList{id.name, id.domain, id.path}, seed);};
0304     
0305     /**
0306     * @brief A list of cookies which were added to the `QWebEngineCookieStore` but were rejected by KCookieJar and must
0307     *   be removed from the store 
0308     * 
0309     * When cookieRemoved() is called with one of those cookies, the cookie is removed from this list and no attempt is made to remove
0310     * the cookie from `KCookieJar` (because it's not there)
0311     */
0312     QVector<CookieIdentifier> m_pendingRejectedCookies;
0313     
0314     /**
0315     * @brief The IDs of all the windows which have session cookies
0316     */
0317     QSet<qlonglong> m_windowsWithSessionCookies;
0318     
0319     /**
0320     * @brief A list of cookies loaded from KCookieServer when this instance is created
0321     */
0322     CookieList m_cookiesLoadedFromKCookieServer;
0323     
0324 #ifdef BUILD_TESTING
0325     QList<QNetworkCookie> m_testCookies;
0326     friend class TestWebEnginePartCookieJarKIO;
0327 #endif
0328     
0329 };
0330 
0331 /**
0332 * @brief Overload of operator `<<` to allow a WebEnginePartCookieJarKIO::CookieIdentifier to be written to a `QDebug`
0333 * 
0334 * @param deb the debug object
0335 * @param id the identifier to write
0336 * @return the debug object
0337 */
0338 QDebug operator<<(QDebug deb, const WebEnginePartCookieJarKIO::CookieIdentifier &id);
0339 
0340 #endif // WEBENGINEPARTCOOKIEJAR_KIO_H