Commit 3c109c19 authored by Stefano Crocco's avatar Stefano Crocco

Improve integration with KWallet

Konqueror fails to save user credentials in many sites for two reasons:
- the site sets the autocomplete attribute to off for the fields (I
  can't understand whether this is an oversight or a conscious choice)
- clicking the "login" (or similar) button causes
  QWebEnginePage::acceptNavigationRequest with a type argument different
  from NavigationTypeFormSubmitted. Since Konqueror uses that value to
  decide whether credentials should be saved or not, it doesn't save
  them.

Given this situation, I believe that there's no simple way to
automatically detect when credential saving and loading should be
applied, so I decided to give the user the ability to manually choose
which fields should be saved and to manually save them. In practice, I
added three entries to the KWallet popup menu in Konqueror:
- one displays a dialog where the user can choose which of the
  fields in the current page should be saved in KWallet. This allows to
  override the autocomplete=off HTML attribute
- one allows to remove the customization described above
- one immediately saves the credentials to KWallet, allowing
  to work around the NavigationTypeFormSubmitted issue.
parent 7e30b477
......@@ -27,8 +27,13 @@ set(kwebenginepartlib_LIB_SRCS
ui/passwordbar.cpp
ui/featurepermissionbar.cpp
about/konq_aboutpage.cpp
webenginecustomizecacheablefieldsdlg.cpp
)
ki18n_wrap_ui(kwebenginepartlib_LIB_SRCS webenginecustomizecacheablefieldsdlg.ui)
qt5_add_resources(kwebenginepartlib_LIB_SRCS webenginepart.qrc)
if(NOT Qt5WebEngineWidgets_VERSION VERSION_LESS "5.12.0")
add_definitions(-DUSE_QWEBENGINE_URL_SCHEME)
add_definitions(-DDOWNLOADITEM_KNOWS_PAGE)
......@@ -86,4 +91,6 @@ install(FILES webenginepart.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/webenginep
install(FILES error.html DESTINATION ${KDE_INSTALL_DATADIR}/webenginepart)
install(FILES settings/kconf_update/webenginepart.upd DESTINATION ${KDE_INSTALL_KCONFUPDATEDIR})
add_subdirectory(about)
function labelsForIdsInFrame(frm) {
var labelList = frm.document.getElementsByTagName("label");
var res = new Object;
for (var i = 0; i < labelList.length; i++) {
var l = labelList[i];
if (l.htmlFor != '') {
var obj = frm.document.getElementById(l.htmlFor);
if (obj) {
res[obj.id] = l.innerHTML;
}
}
}
return res;
}
function findFormsRecursive(wnd, existingList, path, findLabels){
findFormsInFrame(wnd, existingList, path, findLabels);
var frameList = wnd.frames;
for(var i = 0; i < frameList.length; ++i) {
var newPath = path.concat(i);
findFormsRecursive(frameList[i], existingList, newPath, findLabels);
}
}
function findFormsInFrame(frm, existingList, path, findLabels){
var url = frm.location;
var formList;
try{ formList = frm.document.forms; }
catch(e){
return;
}
var labelsForIds;
if (findLabels) {
labelsForIds = labelsForIdsInFrame(frm);
}
if (formList.length > 0) {
for (var i = 0; i < formList.length; ++i) {
var inputList = formList[i].elements;
if (inputList.length < 1) {
continue;
}
var formObject = new Object;
formObject.url = url;
formObject.name = formList[i].name;
if (typeof(formObject.name) != 'string') {
formObject.name = String(formList[i].id);
}
formObject.index = String(i);
formObject.elements = new Array;
for (var j = 0; j < inputList.length; ++j) {
if (inputList[j].type != 'text' && inputList[j].type != 'email' && inputList[j].type != 'password') {
continue;
}
var element = new Object;
element.id = String(inputList[j].id);
element.name = inputList[j].name;
if (typeof(element.name) != 'string' ) {
element.name = element.id;
}
element.value = String(inputList[j].value);
element.type = String(inputList[j].type);
element.readonly = Boolean(inputList[j].readOnly);
element.disabled = Boolean(inputList[j].disabled);
element.autocompleteAllowed = inputList[j].autocomplete != 'off';
if (findLabels && element.id) {
var l = labelsForIds[element.id];
if (l != '') {
element.label = l;
}
}
formObject.elements.push(element);
}
if (formObject.elements.length > 0) {
formObject.framePath = path;
existingList.push(JSON.stringify(formObject));
}
}
}
}
function findFormsInWindow(findLabels){
var forms = new Array;
findFormsRecursive(window, forms, [], findLabels);
return forms;
}
//Fills a single element in a form
//Arguments:
//path: the frame path
//form: the name of the form the element to fill belongs to
//element: the name of the element to fill
//value: the value to insert
function fillFormElement(path, form, element, value){
var frm = window;
if (path === "") {
path = [];
} else {
path = [path];
}
for(var i=0; i < path.length; ++i) frm=frm.frames[i];
if (frm.document.forms[form] && frm.document.forms[form].elements[element]){
frm.document.forms[form].elements[element].value=value;
}
}
Version=5
Id=MoveNonPasswordStorableSites
File=khtml/formcompletions,konquerorrc
Group=NonPasswordStorableSites
AllKeys
......@@ -35,6 +35,18 @@
#include <QFontDatabase>
#include <QFileInfo>
QDataStream & operator<<(QDataStream& ds, const WebEngineSettings::WebFormInfo& info)
{
ds << info.name<< info.framePath << info.fields;
return ds;
}
QDataStream & operator>>(QDataStream& ds, WebEngineSettings::WebFormInfo& info)
{
ds >> info.name >> info.framePath >> info.fields;
return ds;
}
/**
* @internal
* Contains all settings which are both available globally and per-domain
......@@ -131,6 +143,7 @@ public:
QList< QPair< QString, QChar > > m_fallbackAccessKeysAssignments;
KSharedConfig::Ptr nonPasswordStorableSites;
KSharedConfig::Ptr sitesWithCustomForms;
bool m_internalPdfViewer;
};
......@@ -1185,27 +1198,25 @@ bool WebEngineSettings::acceptCrossDomainCookies() const
return d->m_acceptCrossDomainCookies;
}
// Password storage...
static KConfigGroup nonPasswordStorableSitesCg(KSharedConfig::Ptr& configPtr)
KConfigGroup WebEngineSettings::nonPasswordStorableSitesCg() const
{
if (!configPtr) {
configPtr = KSharedConfig::openConfig(QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral("khtml/formcompletions")), KConfig::NoGlobals);
if (!d->nonPasswordStorableSites) {
d->nonPasswordStorableSites = KSharedConfig::openConfig(QString(), KConfig::NoGlobals);
}
return KConfigGroup(configPtr, "NonPasswordStorableSites");
return KConfigGroup(d->nonPasswordStorableSites, "NonPasswordStorableSites");
}
bool WebEngineSettings::isNonPasswordStorableSite(const QString &host) const
{
KConfigGroup cg = nonPasswordStorableSitesCg(d->nonPasswordStorableSites);
KConfigGroup cg = nonPasswordStorableSitesCg();
const QStringList sites = cg.readEntry("Sites", QStringList());
return sites.contains(host);
}
void WebEngineSettings::addNonPasswordStorableSite(const QString &host)
{
KConfigGroup cg = nonPasswordStorableSitesCg(d->nonPasswordStorableSites);
KConfigGroup cg = nonPasswordStorableSitesCg();
QStringList sites = cg.readEntry("Sites", QStringList());
sites.append(host);
cg.writeEntry("Sites", sites);
......@@ -1214,13 +1225,57 @@ void WebEngineSettings::addNonPasswordStorableSite(const QString &host)
void WebEngineSettings::removeNonPasswordStorableSite(const QString &host)
{
KConfigGroup cg = nonPasswordStorableSitesCg(d->nonPasswordStorableSites);
KConfigGroup cg = nonPasswordStorableSitesCg();
QStringList sites = cg.readEntry("Sites", QStringList());
sites.removeOne(host);
cg.writeEntry("Sites", sites);
cg.sync();
}
KConfigGroup WebEngineSettings::pagesWithCustomizedCacheableFieldsCg() const
{
if (!d->sitesWithCustomForms) {
d->sitesWithCustomForms = KSharedConfig::openConfig(QString(), KConfig::NoGlobals);
}
return KConfigGroup(d->sitesWithCustomForms, "PagesWithCustomizedCacheableFields");
}
void WebEngineSettings::setCustomizedCacheableFieldsForPage(const QString& url, const WebFormInfoList& forms)
{
KConfigGroup cg = pagesWithCustomizedCacheableFieldsCg();
QByteArray data;
QDataStream ds(&data, QIODevice::WriteOnly);
ds << forms;
cg.writeEntry(url, data);
cg.sync();
}
bool WebEngineSettings::hasPageCustomizedCacheableFields(const QString& url) const
{
KConfigGroup cg = pagesWithCustomizedCacheableFieldsCg();
return cg.hasKey(url);
}
void WebEngineSettings::removeCacheableFieldsCustomizationForPage(const QString& url)
{
KConfigGroup cg = pagesWithCustomizedCacheableFieldsCg();
cg.deleteEntry(url);
cg.sync();
}
WebEngineSettings::WebFormInfoList WebEngineSettings::customizedCacheableFieldsForPage(const QString& url)
{
KConfigGroup cg = pagesWithCustomizedCacheableFieldsCg();
QByteArray data = cg.readEntry(url, QByteArray());
if (data.isEmpty()) {
return {};
}
QDataStream ds(data);
WebFormInfoList res;
ds >> res;
return res;
}
bool WebEngineSettings::askToSaveSitePassword() const
{
return d->m_offerToSaveWebSitePassword;
......@@ -1285,4 +1340,12 @@ WebEngineSettings* WebEngineSettings::self()
return &s_webEngineSettings;
}
QDebug operator<<(QDebug dbg, const WebEngineSettings::WebFormInfo& info)
{
QDebugStateSaver state(dbg);
dbg.nospace() << "CustomWebFormInfo{";
dbg << info.name << ", " << info.framePath << ", " << info.fields << "}";
return dbg;
}
#include "webenginesettings.moc"
......@@ -26,6 +26,9 @@ class KConfigGroup;
#include <QColor>
#include <QStringList>
#include <QPair>
#include <QDataStream>
#include <QDebug>
#include <QVector>
#include <KParts/HtmlExtension>
#include <KParts/HtmlSettingsInterface>
......@@ -52,6 +55,16 @@ public:
KSmoothScrollingEnabled
};
/**
* Contains information about which forms to save in KWallet
*/
struct WebFormInfo {
QString name;
QString framePath;
QStringList fields;
};
typedef QVector<WebFormInfo> WebFormInfoList;
/**
* Called by constructor and reparseConfiguration
*/
......@@ -120,6 +133,11 @@ public:
void addNonPasswordStorableSite(const QString &host);
void removeNonPasswordStorableSite(const QString &host);
bool askToSaveSitePassword() const;
void setCustomizedCacheableFieldsForPage(const QString &url, const QVector<WebFormInfo> &forms);
void removeCacheableFieldsCustomizationForPage(const QString &url);
bool hasPageCustomizedCacheableFields(const QString &url) const;
QVector<WebFormInfo> customizedCacheableFieldsForPage(const QString &url);
// Mixed content
bool alowActiveMixedContent() const;
......@@ -150,6 +168,9 @@ private:
KSmoothScrollingMode smoothScrolling() const;
bool zoomTextOnly() const;
KConfigGroup pagesWithCustomizedCacheableFieldsCg() const;
KConfigGroup nonPasswordStorableSitesCg() const;
// Font settings
QString stdFontName() const;
QString fixedFontName() const;
......@@ -216,4 +237,9 @@ private:
WebEngineSettingsPrivate* const d;
};
QDataStream& operator<<(QDataStream &ds, const WebEngineSettings::WebFormInfo &info);
QDataStream& operator>>(QDataStream &ds, WebEngineSettings::WebFormInfo &info);
QDebug operator<<(QDebug dbg, const WebEngineSettings::WebFormInfo &info);
#endif
......@@ -14,5 +14,9 @@ inline bool isBlankUrl(const QUrl& url)
return (url.isEmpty() || url.url() == QL1S("konq:blank"));
}
inline bool isKonqUrl(const QUrl &url){
return (url.scheme()) == QL1S("konq");
}
}
#endif // WEBENGINEPART_UTILS_H
/*
* This file is part of the KDE project.
*
* Copyright 2020 Stefano Crocco <posta@stefanocrocco.it>
*
* 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 program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "webenginecustomizecacheablefieldsdlg.h"
#include <QDialogButtonBox>
#include <QLayout>
#include <QTableView>
#include <QStandardItemModel>
#include <QStandardItem>
#include "ui_webenginecustomizecacheablefieldsdlg.h"
using WebForm = WebEngineWallet::WebForm;
using WebFormList = WebEngineWallet::WebFormList;
using WebField = WebEngineWallet::WebForm::WebField;
using WebFieldType = WebEngineWallet::WebForm::WebFieldType;
WebEnginePartPasswordDelegate::WebEnginePartPasswordDelegate(QObject* parent): QStyledItemDelegate(parent)
{
}
bool WebEnginePartPasswordDelegate::isPassword(const QModelIndex& idx)
{
return idx.data(WebEngineCustomizeCacheableFieldsDlg::PasswordRole).toBool();
}
QString WebEnginePartPasswordDelegate::passwordReplacement(const QStyleOptionViewItem& option, const QModelIndex& index)
{
const QWidget *w = option.widget;
QStyle *s = w->style();
QChar passwdChar = s->styleHint(QStyle::StyleHint::SH_LineEdit_PasswordCharacter, &option, w);
return QString(index.data().toString().length(), passwdChar);
}
void WebEnginePartPasswordDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
if (!isPassword(index)) {
QStyledItemDelegate::paint(painter, option, index);
} else {
QString str = passwordReplacement(option, index);
option.widget->style()->drawItemText(painter, option.rect, index.data(Qt::TextAlignmentRole).toInt(), option.palette, true, str);
}
}
QSize WebEnginePartPasswordDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
if (!isPassword(index)) {
return QStyledItemDelegate::sizeHint(option, index);
} else {
QString str = passwordReplacement(option, index);
return option.widget->style()->itemTextRect(option.fontMetrics, option.rect, option.displayAlignment, true, str).size();
}
}
WebEngineCustomizeCacheableFieldsDlg::WebEngineCustomizeCacheableFieldsDlg(const WebEngineWallet::WebFormList &forms, const QMap<QString, QStringList> &oldCustomization, QWidget* parent):
QDialog(parent), m_forms(forms), m_model(new QStandardItemModel(this)), m_passwordDelegate(new WebEnginePartPasswordDelegate(this)),
m_ui(new Ui::WebEngineCustomizeCacheableFieldsDlg)
{
m_ui->setupUi(this);
connect(m_ui->showPasswords, &QCheckBox::toggled, this, &WebEngineCustomizeCacheableFieldsDlg::toggleShowPasswords);
connect(m_ui->showDetails, &QCheckBox::toggled, this, &WebEngineCustomizeCacheableFieldsDlg::toggleDetails);
fillFieldTable(oldCustomization);
}
QList<QStandardItem *> WebEngineCustomizeCacheableFieldsDlg::createRowForField(const WebEngineWallet::WebForm::WebField& field)
{
QString type = WebForm::fieldNameFromType(field.type, true);
QStringList notes;
if (field.readOnly) {
notes << i18nc("web field has the readonly attribute", "read only");
}
if (!field.autocompleteAllowed) {
notes << i18nc("web field has the autocomplete attribute set to off", "auto-completion off");
}
if (field.disabled) {
notes << i18nc("web field is disabled", "disabled");
}
QString label = !field.label.isEmpty() ? field.label : field.name;
QStringList contents{QString(), label, field.value, field.name, type, field.id, notes.join(", ")};
QList<QStandardItem*> row;
row.reserve(contents.size());
auto itemFromString = [](const QString &s){
QStandardItem *it = new QStandardItem(s);
it->setTextAlignment(Qt::AlignCenter);
return it;
};
std::transform(contents.constBegin(), contents.constEnd(), std::back_inserter(row), itemFromString);
row[ValueCol]->setData(field.type == WebFieldType::Password, PasswordRole);
row[ChosenCol]->setCheckable(true);
QString toolTip = toolTipForField(field);
row[LabelCol]->setToolTip(toolTip);
row[ValueCol]->setToolTip(toolTip);
return row;
}
void WebEngineCustomizeCacheableFieldsDlg::fillFieldTable(const QMap<QString, QStringList> &oldCustomization)
{
bool autoCheck = oldCustomization.isEmpty();
m_ui->fields->setModel(m_model);
m_ui->fields->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_model->setHorizontalHeaderLabels(QStringList{"",
i18nc("Label of a web field", "Field name"),
i18nc("Value of a web field", "Field value"),
i18nc("Name attribute of a web field", "Internal field name"),
i18nc("Type of a web field", "Field type"),
i18nc("The id of a web field", "Field id"),
i18nc("Other details about a web field", "Details")});
m_ui->fields->setItemDelegateForColumn(2, m_passwordDelegate);
for (int i = 0; i < m_forms.length(); ++i) {
const WebForm &form = m_forms.at(i);
QStringList oldCustomInThisForm = oldCustomization.value(form.name);
for (int j = 0; j < form.fields.length(); ++j) {
WebField field = form.fields.at(j);
QList<QStandardItem*> row = createRowForField(field);
QStandardItem *chosen = row.at(ChosenCol);
chosen->setData(i, FormRole);
chosen->setData(j, FieldRole);
bool checked = false;
if (autoCheck) {
checked = !field.value.isEmpty() && !field.readOnly && !field.disabled && field.autocompleteAllowed;
} else {
checked = oldCustomInThisForm.contains(field.name);
}
chosen->setCheckState(checked ? Qt::Checked : Qt::Unchecked);
m_model->appendRow(row);
}
}
m_ui->fields->verticalHeader()->hide();
m_ui->fields->horizontalHeader()->setStretchLastSection(true);
m_ui->fields->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
toggleDetails(m_ui->showDetails->isChecked());
}
WebFormList WebEngineCustomizeCacheableFieldsDlg::selectedFields() const
{
QMap<int, QVector<int>> fields;
for (int i = 0; i < m_model->rowCount(); ++i) {
QStandardItem *it = m_model->item(i, ChosenCol);
if (it->checkState() == Qt::Checked) {
fields[it->data(FormRole).toInt()].append(it->data(FieldRole).toInt());
}
}
WebFormList lst;
for (QMap<int, QVector<int>>::const_iterator it = fields.constBegin(); it != fields.constEnd(); ++it) {
if (it.value().isEmpty()) {
continue;
}
const WebForm &oldForm = m_forms.at(it.key());
WebForm form(oldForm);
form.fields.clear();
for (int i : it.value()) {
form.fields.append(oldForm.fields.at(i));
}
lst.append(form);
}
return lst;
}
void WebEngineCustomizeCacheableFieldsDlg::toggleDetails (bool show)
{
for (int i = InternalNameCol; i <= DetailsCol; ++i) {
m_ui->fields->setColumnHidden(i, !show);
}
}
void WebEngineCustomizeCacheableFieldsDlg::toggleShowPasswords (bool show)
{
//Do nothing if the item delegate setting is already correct. This should never happen
if (show == (m_ui->fields->itemDelegateForColumn(ValueCol) == m_ui->fields->itemDelegate())) {
return;
}
QAbstractItemDelegate *delegate = show ? m_ui->fields->itemDelegate() : m_passwordDelegate;
m_ui->fields->setItemDelegateForColumn(ValueCol, delegate);
}
bool WebEngineCustomizeCacheableFieldsDlg::immediatelyCacheData() const
{
return m_ui->immediatelyCacheData->isChecked();
}
QString WebEngineCustomizeCacheableFieldsDlg::toolTipForField(const WebEngineWallet::WebForm::WebField &field)
{
QString type = WebForm::fieldNameFromType(field.type, true);
const QString yes = i18nc("A statement about a field is true", "yes");
const QString no = i18nc("A statement about a field is false", "no");
auto boolToYesNo = [yes, no](bool val){return val ? yes : no;};
QString toolTip = i18n(
"<ul><li><b>Field internal name: </b>%1</li>"
"<li><b>Field type: </b>%2</li>"
"<li><b>Field id: </b>%3</li>"
"<li><b>Field is read only: </b>%4</li>"
"<li><b>Field is enabled: </b>%5</li>"
"<li><b>Autocompletion is enabled: </b>%6</li>"
"</ul>",
field.name, type, field.id, boolToYesNo(field.readOnly), boolToYesNo(!field.disabled), boolToYesNo(field.autocompleteAllowed));
return toolTip;
}
/*
* This file is part of the KDE project.
*
* Copyright 2020 Stefano Crocco <posta@stefanocrocco.it>
*
* 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 program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef WEBENGINECUSTOMIZECACHEABLEFIELDSDLG_H
#define WEBENGINECUSTOMIZECACHEABLEFIELDSDLG_H
#include <QDialog>
#include <QStyledItemDelegate>
#include "webenginewallet.h"