Show UI warning for invalid URLs

This commit is contained in:
varjolintu 2019-11-22 13:54:28 +02:00 committed by Jonathan White
parent 663393d994
commit c0f29cc790
11 changed files with 211 additions and 2 deletions

View File

@ -121,6 +121,7 @@ set(keepassx_SOURCES
gui/TotpDialog.cpp gui/TotpDialog.cpp
gui/TotpExportSettingsDialog.cpp gui/TotpExportSettingsDialog.cpp
gui/DatabaseOpenDialog.cpp gui/DatabaseOpenDialog.cpp
gui/URLEdit.cpp
gui/WelcomeWidget.cpp gui/WelcomeWidget.cpp
gui/csvImport/CsvImportWidget.cpp gui/csvImport/CsvImportWidget.cpp
gui/csvImport/CsvImportWizard.cpp gui/csvImport/CsvImportWizard.cpp

View File

@ -32,6 +32,7 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <QStringList> #include <QStringList>
#include <QSysInfo> #include <QSysInfo>
#include <QUrl>
#include <QUuid> #include <QUuid>
#include <cctype> #include <cctype>
@ -259,6 +260,33 @@ namespace Tools
} }
} }
bool checkUrlValid(const QString& urlField)
{
if (urlField.isEmpty()) {
return true;
}
QUrl url;
if (urlField.contains("://")) {
url = urlField;
} else {
url = QUrl::fromUserInput(urlField);
}
if (url.scheme() != "file" && url.host().isEmpty()) {
return false;
}
// Check for illegal characters. Adds also the wildcard * to the list
QRegularExpression re("[<>\\^`{|}\\*]");
auto match = re.match(urlField);
if (match.hasMatch()) {
return false;
}
return true;
}
// Escape common regex symbols except for *, ?, and | // Escape common regex symbols except for *, ?, and |
auto regexEscape = QRegularExpression(R"re(([-[\]{}()+.,\\\/^$#]))re"); auto regexEscape = QRegularExpression(R"re(([-[\]{}()+.,\\\/^$#]))re");

View File

@ -41,6 +41,7 @@ namespace Tools
bool isBase64(const QByteArray& ba); bool isBase64(const QByteArray& ba);
void sleep(int ms); void sleep(int ms);
void wait(int ms); void wait(int ms);
bool checkUrlValid(const QString& urlField);
QString uuidToHex(const QUuid& uuid); QString uuidToHex(const QUuid& uuid);
QUuid hexToUuid(const QString& uuid); QUuid hexToUuid(const QString& uuid);
QRegularExpression convertToRegex(const QString& string, QRegularExpression convertToRegex(const QString& string,

59
src/gui/URLEdit.cpp Normal file
View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2014 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* 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 "URLEdit.h"
#include <QRegularExpression>
#include "core/Config.h"
#include "core/FilePath.h"
#include "core/Tools.h"
#include "gui/Font.h"
const QColor URLEdit::ErrorColor = QColor(255, 125, 125);
URLEdit::URLEdit(QWidget* parent)
: QLineEdit(parent)
{
const QIcon errorIcon = filePath()->icon("status", "dialog-error");
m_errorAction = addAction(errorIcon, QLineEdit::TrailingPosition);
m_errorAction->setVisible(false);
m_errorAction->setToolTip(tr("Invalid URL"));
updateStylesheet();
}
void URLEdit::enableVerifyMode()
{
updateStylesheet();
connect(this, SIGNAL(textChanged(QString)), SLOT(updateStylesheet()));
}
void URLEdit::updateStylesheet()
{
const QString stylesheetTemplate("QLineEdit { background: %1; }");
if (!Tools::checkUrlValid(text())) {
setStyleSheet(stylesheetTemplate.arg(ErrorColor.name()));
m_errorAction->setVisible(true);
} else {
m_errorAction->setVisible(false);
setStyleSheet("");
}
}

43
src/gui/URLEdit.h Normal file
View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2014 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* 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 KEEPASSX_URLEDIT_H
#define KEEPASSX_URLEDIT_H
#include <QAction>
#include <QLineEdit>
#include <QPointer>
class URLEdit : public QLineEdit
{
Q_OBJECT
public:
static const QColor ErrorColor;
explicit URLEdit(QWidget* parent = nullptr);
void enableVerifyMode();
private slots:
void updateStylesheet();
private:
QPointer<QAction> m_errorAction;
};
#endif // KEEPASSX_URLEDIT_H

View File

@ -167,6 +167,7 @@ void EditEntryWidget::setupMain()
#ifdef WITH_XC_NETWORKING #ifdef WITH_XC_NETWORKING
connect(m_mainUi->fetchFaviconButton, SIGNAL(clicked()), m_iconsWidget, SLOT(downloadFavicon())); connect(m_mainUi->fetchFaviconButton, SIGNAL(clicked()), m_iconsWidget, SLOT(downloadFavicon()));
connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), m_iconsWidget, SLOT(setUrl(QString))); connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), m_iconsWidget, SLOT(setUrl(QString)));
m_mainUi->urlEdit->enableVerifyMode();
#endif #endif
connect(m_mainUi->expireCheck, SIGNAL(toggled(bool)), m_mainUi->expireDatePicker, SLOT(setEnabled(bool))); connect(m_mainUi->expireCheck, SIGNAL(toggled(bool)), m_mainUi->expireDatePicker, SLOT(setEnabled(bool)));
connect(m_mainUi->notesEnabled, SIGNAL(toggled(bool)), this, SLOT(toggleHideNotes(bool))); connect(m_mainUi->notesEnabled, SIGNAL(toggled(bool)), this, SLOT(toggleHideNotes(bool)));
@ -271,6 +272,10 @@ void EditEntryWidget::setupBrowser()
m_additionalURLsDataModel->setEntryAttributes(m_entryAttributes); m_additionalURLsDataModel->setEntryAttributes(m_entryAttributes);
m_browserUi->additionalURLsView->setModel(m_additionalURLsDataModel); m_browserUi->additionalURLsView->setModel(m_additionalURLsDataModel);
// Use a custom item delegate to align the icon to the right side
auto iconDelegate = new URLModelIconDelegate(m_browserUi->additionalURLsView);
m_browserUi->additionalURLsView->setItemDelegate(iconDelegate);
// clang-format off // clang-format off
connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified())); connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified()));
connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified())); connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified()));

View File

@ -30,7 +30,7 @@
<item row="5" column="1"> <item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6"> <layout class="QHBoxLayout" name="horizontalLayout_6">
<item> <item>
<widget class="QLineEdit" name="urlEdit"> <widget class="URLEdit" name="urlEdit">
<property name="accessibleName"> <property name="accessibleName">
<string>Url field</string> <string>Url field</string>
</property> </property>
@ -256,6 +256,12 @@
<header>gui/PasswordEdit.h</header> <header>gui/PasswordEdit.h</header>
<container>1</container> <container>1</container>
</customwidget> </customwidget>
<customwidget>
<class>URLEdit</class>
<extends>QLineEdit</extends>
<header>gui/URLEdit.h</header>
<container>1</container>
</customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>titleEdit</tabstop> <tabstop>titleEdit</tabstop>

View File

@ -19,6 +19,7 @@
#include "EntryURLModel.h" #include "EntryURLModel.h"
#include "core/Entry.h" #include "core/Entry.h"
#include "core/FilePath.h"
#include "core/Tools.h" #include "core/Tools.h"
#include <algorithm> #include <algorithm>
@ -26,6 +27,7 @@
EntryURLModel::EntryURLModel(QObject* parent) EntryURLModel::EntryURLModel(QObject* parent)
: QStandardItemModel(parent) : QStandardItemModel(parent)
, m_entryAttributes(nullptr) , m_entryAttributes(nullptr)
, m_errorIcon(filePath()->icon("status", "dialog-error"))
{ {
} }
@ -53,6 +55,33 @@ void EntryURLModel::setEntryAttributes(EntryAttributes* entryAttributes)
endResetModel(); endResetModel();
} }
QVariant EntryURLModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid()) {
return {};
}
const auto key = keyByIndex(index);
if (key.isEmpty()) {
return {};
}
const auto value = m_entryAttributes->value(key);
const auto urlValid = Tools::checkUrlValid(value);
if (role == Qt::BackgroundRole && !urlValid) {
return QColor(255, 125, 125);
} else if (role == Qt::DecorationRole && !urlValid) {
return m_errorIcon;
} else if (role == Qt::DisplayRole || role == Qt::EditRole) {
return value;
} else if (role == Qt::ToolTipRole && !urlValid) {
return tr("Invalid URL");
}
return {};
}
bool EntryURLModel::setData(const QModelIndex& index, const QVariant& value, int role) bool EntryURLModel::setData(const QModelIndex& index, const QVariant& value, int role)
{ {
if (!index.isValid() || role != Qt::EditRole || value.type() != QVariant::String || value.toString().isEmpty()) { if (!index.isValid() || role != Qt::EditRole || value.type() != QVariant::String || value.toString().isEmpty()) {

View File

@ -20,9 +20,23 @@
#define KEEPASSXC_ENTRYURLMODEL_H #define KEEPASSXC_ENTRYURLMODEL_H
#include <QStandardItemModel> #include <QStandardItemModel>
#include <QStyledItemDelegate>
class EntryAttributes; class EntryAttributes;
class URLModelIconDelegate : public QStyledItemDelegate
{
public:
using QStyledItemDelegate::QStyledItemDelegate;
protected:
void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override
{
QStyledItemDelegate::initStyleOption(option, index);
option->decorationPosition = QStyleOptionViewItem::Right;
}
};
class EntryURLModel : public QStandardItemModel class EntryURLModel : public QStandardItemModel
{ {
Q_OBJECT Q_OBJECT
@ -32,6 +46,7 @@ public:
void setEntryAttributes(EntryAttributes* entryAttributes); void setEntryAttributes(EntryAttributes* entryAttributes);
void insertRow(const QString& key, const QString& value); void insertRow(const QString& key, const QString& value);
bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
QVariant data(const QModelIndex& index, int role) const override;
QModelIndex indexByKey(const QString& key) const; QModelIndex indexByKey(const QString& key) const;
QString keyByIndex(const QModelIndex& index) const; QString keyByIndex(const QModelIndex& index) const;
@ -41,6 +56,7 @@ private slots:
private: private:
QList<QPair<QString, QString>> m_urls; QList<QPair<QString, QString>> m_urls;
EntryAttributes* m_entryAttributes; EntryAttributes* m_entryAttributes;
QIcon m_errorIcon;
}; };
#endif // KEEPASSXC_ENTRYURLMODEL_H #endif // KEEPASSXC_ENTRYURLMODEL_H

View File

@ -18,6 +18,7 @@
#include "TestBrowser.h" #include "TestBrowser.h"
#include "TestGlobal.h" #include "TestGlobal.h"
#include "browser/BrowserSettings.h" #include "browser/BrowserSettings.h"
#include "core/Tools.h"
#include "crypto/Crypto.h" #include "crypto/Crypto.h"
#include "sodium/crypto_box.h" #include "sodium/crypto_box.h"
#include <QString> #include <QString>
@ -462,3 +463,22 @@ QList<Entry*> TestBrowser::createEntries(QStringList& urls, Group* root) const
return entries; return entries;
} }
void TestBrowser::testValidURLs()
{
QHash<QString, bool> urls;
urls["https://github.com/login"] = true;
urls["https:///github.com/"] = false;
urls["http://github.com/**//*"] = false;
urls["http://*.github.com/login"] = false;
urls["//github.com"] = true;
urls["github.com/{}<>"] = false;
urls["http:/example.com"] = false;
urls["cmd://C:/Toolchains/msys2/usr/bin/mintty \"ssh jon@192.168.0.1:22\""] = true;
urls["file:///Users/testUser/Code/test.html"] = true;
QHashIterator<QString, bool> i(urls);
while (i.hasNext()) {
i.next();
QCOMPARE(Tools::checkUrlValid(i.key()), i.value());
}
}

View File

@ -47,6 +47,7 @@ private slots:
void testSubdomainsAndPaths(); void testSubdomainsAndPaths();
void testSortEntries(); void testSortEntries();
void testGetDatabaseGroups(); void testGetDatabaseGroups();
void testValidURLs();
private: private:
QList<Entry*> createEntries(QStringList& urls, Group* root) const; QList<Entry*> createEntries(QStringList& urls, Group* root) const;