File indexing completed on 2024-05-19 05:01:24

0001 /*
0002     This file is part of the KDE project.
0003 
0004     SPDX-FileCopyrightText: 2020 Dawit Alemayehu <adawit@kde.org>
0005     SPDX-FileCopyrightText: 2020 Stefano Crocco <stefano.crocco@alice.it>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "webenginewallet.h"
0011 #include "webenginepage.h"
0012 
0013 #include <KWallet>
0014 
0015 #include <QSet>
0016 #include <QHash>
0017 #include <QScopedPointer>
0018 #include <QWebEngineScript>
0019 #include <QJsonDocument>
0020 #include <QJsonObject>
0021 #include <QJsonArray>
0022 
0023 #include <QMessageBox>
0024 
0025 #include <algorithm>
0026 
0027 class WebEngineWallet::WebEngineWalletPrivate
0028 {
0029 public:
0030     struct FormsData {
0031         QPointer<WebEnginePage> page;
0032         WebEngineWallet::WebFormList forms;
0033     };
0034 
0035     typedef std::function<void(const WebEngineWallet::WebFormList &)> WebWalletCallback;
0036 
0037     WebEngineWalletPrivate(WebEngineWallet *parent);
0038 
0039     static WebFormList parseFormDetectionResult(const QVariant &jsForms, const QUrl &pageUrl);
0040 
0041     WebFormList formsToFill(const WebFormList &allForms) const;
0042     WebFormList formsToSave(const WebFormList &allForms) const;
0043     bool hasAutoFillableFields(const WebFormList &forms) const;
0044 
0045     void fillDataFromCache(WebEngineWallet::WebFormList &formList, bool custom);
0046 
0047     /**
0048      * @brief Saves form data to the wallet
0049      *
0050      * If the wallet already contains data for this key, the data won't be saved immediately. Instead, the
0051      * WebEngineWallet::saveFormDataRequested signal will be emitted, which will cause the WebEnginePart to
0052      * ask the user what to do. In this case, this function will do nothing after emitting the signal.
0053      *
0054      * @note Callers of this function @b must remove the key from #pendingRemoveRequests if this function returns @b true.
0055      *
0056      * @param key the key corresponding to the data to save in #pendingRemoveRequests
0057      * @return @b true if the data was saved immediately and @a key must be removed from #pendingRemoveRequests and @b false
0058      * otherwise
0059      */
0060     bool saveDataToCache(const QString &key);
0061     void removeDataFromCache(const WebFormList &formList);
0062     void openWallet();
0063 
0064     static bool containsCustomForms(const QMap<QString, QString>& map);
0065     static void detectFormsInPage(WebEnginePage *page, WebWalletCallback callback, bool findLabels=false);
0066     static bool shouldFieldBeIgnored(const QString &name);
0067 
0068     // Private slots...
0069     void _k_openWalletDone(bool);
0070     void _k_walletClosed();
0071 
0072     WId wid;
0073     WebEngineWallet *q;
0074     QScopedPointer<KWallet::Wallet> wallet;
0075     WebEngineWallet::WebFormList pendingRemoveRequests;
0076     QHash<QUrl, FormsData> pendingFillRequests;
0077     QHash<QString, WebFormList> pendingSaveRequests;
0078     QSet<QUrl> confirmSaveRequestOverwrites;
0079 
0080     ///@brief A list of field names which the user is unlikely to want stored (such as search fields)
0081     static const char* s_fieldNamesToIgnore[];
0082 };
0083 
0084 const char* WebEngineWallet::WebEngineWalletPrivate::s_fieldNamesToIgnore[] = {
0085     "q", //The search field in Google and DuckDuckGo
0086     "search", "search_bar", //Other possible names for a search field
0087     "amount" //A field corresponding to a quantity
0088 };
0089 
0090 /**
0091  * Creates key used to store and retrieve form data.
0092  *
0093  */
0094 static QString walletKey(const WebEngineWallet::WebForm &form)
0095 {
0096     QString key = form.url.toString(QUrl::RemoveQuery | QUrl::RemoveFragment);
0097     key += QL1C('#');
0098     key += form.name;
0099     return key;
0100 }
0101 
0102 static QUrl urlForFrame(const QUrl &frameUrl, const QUrl &pageUrl)
0103 {
0104     return (frameUrl.isEmpty() || frameUrl.isRelative() ? pageUrl.resolved(frameUrl) : frameUrl);
0105 }
0106 
0107 WebEngineWallet::WebEngineWalletPrivate::WebEngineWalletPrivate(WebEngineWallet *parent)
0108     : wid(0), q(parent)
0109 {
0110 }
0111 
0112 bool WebEngineWallet::WebEngineWalletPrivate::shouldFieldBeIgnored(const QString& name)
0113 {
0114     QString lowerName = name.toLower();
0115     for (uint i = 0; i < sizeof(s_fieldNamesToIgnore)/sizeof(char*); ++i){
0116         if (lowerName == s_fieldNamesToIgnore[i]) {
0117             return true;
0118         }
0119     }
0120     return false;
0121 }
0122 
0123 WebEngineWallet::WebFormList WebEngineWallet::WebEngineWalletPrivate::parseFormDetectionResult(const QVariant& jsForms, const QUrl& pageUrl)
0124 {
0125     const QVariantList variantForms(jsForms.toList());
0126     WebEngineWallet::WebFormList list;
0127     for (const QVariant &v : variantForms) {
0128         QJsonObject formMap = QJsonDocument::fromJson(v.toString().toUtf8()).object();
0129         WebEngineWallet::WebForm form;
0130         form.url = urlForFrame(QUrl(formMap[QL1S("url")].toString()), pageUrl);
0131         form.name = formMap[QL1S("name")].toString();
0132         form.index = formMap[QL1S("index")].toString();
0133         form.framePath = QVariant(formMap[QL1S("framePath")].toArray().toVariantList()).toStringList().join(",");
0134         const QVariantList elements = formMap[QL1S("elements")].toArray().toVariantList();
0135         for(const QVariant &e : elements) {
0136            QVariantMap elementMap(e.toMap());
0137            WebForm::WebField field;
0138            field.type = WebForm::fieldTypeFromTypeName(elementMap[QL1S("type")].toString().toLower());
0139            if (field.type == WebForm::WebFieldType::Other) {
0140                continue;
0141            }
0142            field.name = elementMap[QL1S("name")].toString();
0143            if (shouldFieldBeIgnored(field.name)) {
0144                continue;
0145            }
0146 
0147            field.id = elementMap[QL1S("id")].toString();
0148            field.readOnly = elementMap[QL1S("readonly")].toBool();
0149            field.disabled = elementMap[QL1S("disabled")].toBool();
0150            field.autocompleteAllowed = elementMap[QL1S("autocompleteAllowed")].toBool();
0151            field.value = elementMap[QL1S("value")].toString();
0152            field.label = elementMap[QL1S("label")].toString();
0153            form.fields.append(field);
0154         }
0155         if (!form.fields.isEmpty()) {
0156             list.append(form);
0157         }
0158     }
0159     return list;
0160 }
0161 
0162 void WebEngineWallet::WebEngineWalletPrivate::detectFormsInPage(WebEnginePage* page, WebEngineWallet::WebEngineWalletPrivate::WebWalletCallback callback, bool findLabels)
0163 {
0164     if (!page) {
0165         return;
0166     }
0167     QUrl url = page->url();
0168     auto realCallBack = [callback, url](const QVariant &jsForms) {
0169         //If the page is deleted while the javascript code is still running, the callback will be called
0170         //but the page will be invalid, which will cause a crash. According to the documentation, when this
0171         //happens, the parameter passed to the callback is invalid: in that case, we do nothing.
0172         if (!jsForms.isValid()) {
0173             return;
0174         }
0175         WebFormList forms = parseFormDetectionResult(jsForms, url);
0176         callback(forms);
0177     };
0178     page->runJavaScript(QStringLiteral("findFormsInWindow(%1)").arg(findLabels ? "true" : ""), QWebEngineScript::ApplicationWorld, realCallBack);
0179 }
0180 
0181 WebEngineWallet::WebFormList WebEngineWallet::WebEngineWalletPrivate::formsToFill(const WebFormList &allForms) const
0182 {
0183     WebEngineWallet::WebFormList list;
0184     for (const WebEngineWallet::WebForm &form : allForms) {
0185         if (q->hasCachedFormData(form)) {
0186             WebEngineWallet::WebForm f(form.withAutoFillableFieldsOnly());
0187             if (!f.fields.isEmpty()) {
0188                 list.append(f);
0189             }
0190         }
0191     }
0192     return list;
0193 }
0194 
0195 WebEngineWallet::WebFormList WebEngineWallet::WebEngineWalletPrivate::formsToSave(const WebEngineWallet::WebFormList& allForms) const
0196 {
0197     WebEngineWallet::WebFormList list;
0198     std::copy_if(allForms.constBegin(), allForms.constEnd(), std::back_inserter(list), [](const WebForm &f){return f.hasPasswords();});
0199     return list;
0200 }
0201 
0202 bool WebEngineWallet::WebEngineWalletPrivate::hasAutoFillableFields(const WebEngineWallet::WebFormList& forms) const
0203 {
0204     return std::any_of(forms.constBegin(), forms.constEnd(), [](const WebForm &f){return f.hasAutoFillableFields();});
0205 }
0206 
0207 
0208 void WebEngineWallet::WebEngineWalletPrivate::fillDataFromCache(WebEngineWallet::WebFormList &formList, bool custom)
0209 {
0210     if (!wallet) {
0211         qCWarning(WEBENGINEPART_LOG) << "Unable to retrieve form data from wallet";
0212         return;
0213     }
0214 
0215     QString lastKey;
0216     QMap<QString, QString> cachedValues;
0217     QMutableVectorIterator <WebForm> formIt(formList);
0218 
0219     while (formIt.hasNext()) {
0220         WebEngineWallet::WebForm &form = formIt.next();
0221         const QString key(walletKey(form));
0222         if (key != lastKey && wallet->readMap(key, cachedValues) != 0) {
0223             qCWarning(WEBENGINEPART_LOG) << "Unable to read form data for key:" << key;
0224             continue;
0225         }
0226         if (!custom) {
0227             form = form.withAutoFillableFieldsOnly();
0228         }
0229         for (int i = 0, count = form.fields.count(); i < count; ++i) {
0230             form.fields[i].value = cachedValues.value(form.fields[i].name);
0231         }
0232         lastKey = key;
0233     }
0234 }
0235 
0236 bool WebEngineWallet::WebEngineWalletPrivate::saveDataToCache(const QString &key)
0237 {
0238     // Make sure the specified keys exists before acting on it. See BR# 270209.
0239     if (!pendingSaveRequests.contains(key)) {
0240         return false;
0241     }
0242 
0243     if (!wallet) {
0244         qCWarning(WEBENGINEPART_LOG) << "NULL Wallet instance!";
0245         return false;
0246     }
0247 
0248     bool success = false;
0249 
0250     int count = 0;
0251     const WebEngineWallet::WebFormList list = pendingSaveRequests.value(key);
0252     const QUrl url = list.first().url;
0253     QVectorIterator<WebEngineWallet::WebForm> formIt(list);
0254 
0255     while (formIt.hasNext()) {
0256         QMap<QString, QString> values, storedValues;
0257         WebEngineWallet::WebForm form = formIt.next();
0258         const QString accessKey = walletKey(form);
0259         const int status = wallet->readMap(accessKey, storedValues);
0260         if (status == 0 && !storedValues.isEmpty()) {
0261             if (confirmSaveRequestOverwrites.contains(url)) {
0262                 confirmSaveRequestOverwrites.remove(url);
0263                 if (!storedValues.isEmpty()) {
0264                     auto fieldChanged = [&storedValues](const WebForm::WebField field){
0265                         return storedValues.contains(field.name) && storedValues.value(field.name) != field.value;
0266                     };
0267                     if (std::any_of(form.fields.constBegin(), form.fields.constEnd(), fieldChanged)) {
0268                         emit q->saveFormDataRequested(key, url);
0269                         return false;
0270                     }
0271                     // If we got here it means the new credential is exactly
0272                     // the same as the one already cached ; so skip the
0273                     // re-saving part...
0274                     success = true;
0275                     continue;
0276                 }
0277             }
0278         }
0279         QVectorIterator<WebEngineWallet::WebForm::WebField> fieldIt(form.fields);
0280         while (fieldIt.hasNext()) {
0281             const WebEngineWallet::WebForm::WebField field = fieldIt.next();
0282             values.insert(field.name, field.value);
0283         }
0284 
0285         if (wallet->writeMap(accessKey, values) == 0) {
0286             count++;
0287         } else {
0288             qCWarning(WEBENGINEPART_LOG) << "Unable to write form data to wallet";
0289         }
0290     }
0291 
0292     if (list.isEmpty() || count > 0) {
0293         success = true;
0294     }
0295 
0296     emit q->saveFormDataCompleted(url, success);
0297     return true;
0298 }
0299 
0300 void WebEngineWallet::WebEngineWalletPrivate::openWallet()
0301 {
0302     if (!wallet.isNull()) {
0303         return;
0304     }
0305 
0306     wallet.reset(KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(),
0307                  wid, KWallet::Wallet::Asynchronous));
0308 
0309     if (wallet.isNull()) {
0310         return;
0311     }
0312 
0313     // FIXME: See if possible to use new Qt5 connect syntax
0314     connect(wallet.data(), SIGNAL(walletOpened(bool)), q, SLOT(_k_openWalletDone(bool)));
0315     connect(wallet.data(), SIGNAL(walletClosed()), q, SLOT(_k_walletClosed()));
0316 }
0317 
0318 void WebEngineWallet::WebEngineWalletPrivate::removeDataFromCache(const WebFormList &formList)
0319 {
0320     if (!wallet) {
0321         qCWarning(WEBENGINEPART_LOG) << "NULL Wallet instance!";
0322         return;
0323     }
0324 
0325     QVectorIterator<WebForm> formIt(formList);
0326     while (formIt.hasNext()) {
0327         wallet->removeEntry(walletKey(formIt.next()));
0328     }
0329 }
0330 
0331 void WebEngineWallet::WebEngineWalletPrivate::_k_openWalletDone(bool ok)
0332 {
0333     Q_ASSERT(wallet);
0334 
0335     if (ok &&
0336             (wallet->hasFolder(KWallet::Wallet::FormDataFolder()) ||
0337              wallet->createFolder(KWallet::Wallet::FormDataFolder())) &&
0338             wallet->setFolder(KWallet::Wallet::FormDataFolder())) {
0339 
0340         emit q->walletOpened();
0341 
0342         // Do pending fill requests...
0343         if (!pendingFillRequests.isEmpty()) {
0344             QMutableHashIterator<QUrl, FormsData> requestIt(pendingFillRequests);
0345             while (requestIt.hasNext()) {
0346                 requestIt.next();
0347                 WebEngineWallet::WebFormList list = requestIt.value().forms;
0348                 fillDataFromCache(list, WebEngineSettings::self()->hasPageCustomizedCacheableFields(customFormsKey(requestIt.key())));
0349                 q->fillWebForm(requestIt.key(), list);
0350             }
0351 
0352             pendingFillRequests.clear();
0353         }
0354 
0355         // Do pending save requests...
0356         //NOTE: don't increment the iterator inside the for because it's done inside the cycle, depending on the value returned
0357         //by saveDataToCache
0358         for (QHash<QString, WebFormList>::iterator it = pendingSaveRequests.begin(); it != pendingSaveRequests.end();) {
0359             bool removeEntry = saveDataToCache(it.key());
0360             //Only remove the entry if it could be saved without user confirmation
0361             if (removeEntry) {
0362                 it = pendingSaveRequests.erase(it);
0363             } else {
0364                 ++it;
0365             }
0366         }
0367 
0368         // Do pending remove requests...
0369         if (!pendingRemoveRequests.isEmpty()) {
0370             removeDataFromCache(pendingRemoveRequests);
0371             pendingRemoveRequests.clear();
0372         }
0373     } else {
0374         // Delete the wallet if opening the wallet failed or we were unable
0375         // to change to the folder we wanted to change to.
0376         delete wallet.take();
0377     }
0378 }
0379 
0380 void WebEngineWallet::WebEngineWalletPrivate::_k_walletClosed()
0381 {
0382     if (wallet) {
0383         wallet.take()->deleteLater();
0384     }
0385 
0386     emit q->walletClosed();
0387 }
0388