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