keepassxc/src/gui/EditWidgetIcons.cpp

507 lines
17 KiB
C++
Raw Normal View History

2012-05-15 07:02:05 -04:00
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
2017-06-09 17:40:36 -04:00
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
2012-05-15 07:02:05 -04:00
*
* 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 "EditWidgetIcons.h"
#include "ui_EditWidgetIcons.h"
#include <QFileDialog>
2018-03-31 16:01:30 -04:00
#include <QMessageBox>
2012-05-15 07:02:05 -04:00
#include "core/Config.h"
2012-05-15 07:02:05 -04:00
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/Tools.h"
#include "gui/IconModels.h"
#include "gui/MessageBox.h"
2012-05-15 07:02:05 -04:00
#ifdef WITH_XC_NETWORKING
#include <QtNetwork>
#include <QNetworkAccessManager>
#endif
2012-05-15 07:02:05 -04:00
IconStruct::IconStruct()
2018-03-22 17:56:05 -04:00
: uuid(QUuid())
2012-05-15 07:02:05 -04:00
, number(0)
{
}
2012-05-15 12:51:45 -04:00
EditWidgetIcons::EditWidgetIcons(QWidget* parent)
2012-05-15 07:02:05 -04:00
: QWidget(parent)
, m_ui(new Ui::EditWidgetIcons())
, m_db(nullptr)
#ifdef WITH_XC_NETWORKING
, m_netMgr(new QNetworkAccessManager(this))
, m_reply(nullptr)
#endif
, m_defaultIconModel(new DefaultIconModel(this))
, m_customIconModel(new CustomIconModel(this))
2012-05-15 07:02:05 -04:00
{
m_ui->setupUi(this);
m_ui->defaultIconsView->setModel(m_defaultIconModel);
m_ui->customIconsView->setModel(m_customIconModel);
// clang-format off
2018-03-31 16:01:30 -04:00
connect(m_ui->defaultIconsView, SIGNAL(clicked(QModelIndex)), this, SLOT(updateRadioButtonDefaultIcons()));
connect(m_ui->customIconsView, SIGNAL(clicked(QModelIndex)), this, SLOT(updateRadioButtonCustomIcons()));
connect(m_ui->defaultIconsRadio, SIGNAL(toggled(bool)), this, SLOT(updateWidgetsDefaultIcons(bool)));
connect(m_ui->customIconsRadio, SIGNAL(toggled(bool)), this, SLOT(updateWidgetsCustomIcons(bool)));
connect(m_ui->addButton, SIGNAL(clicked()), SLOT(addCustomIconFromFile()));
2012-05-15 07:02:05 -04:00
connect(m_ui->deleteButton, SIGNAL(clicked()), SLOT(removeCustomIcon()));
connect(m_ui->faviconButton, SIGNAL(clicked()), SLOT(downloadFavicon()));
Grey out Apply button when there are no changes Resolves #1313 What this commit does: * Whenever the Apply button is pressed, and if the save was successful, then the Apply button is disabled. * Each subwidget used by EditEntryWidget has now a signal called `widgetUpdated` that is emitted when the widgets' internal content changes. The EditEntryWidget subscribes to that signal to know when to enable the Apply button (by calling `entryUpdated()`). * There are some views that are not isolated in their own widgets (`m_advancedUi`, for example) so in those cases I invoked `entryUpdated()` directly whenever I detected an update: * some updates occur directly in a Qt widget like when editing the text of a QLineItem, so in that case I connected the widget's signals directly to the `entryUpdated()` slot. * some updates occur in EditEntryWidget, so in those cases the invocation to `entryUpdated()` is made as soon as the change is detected (for example when the user has confirmed an action in a dialog). A known problem: there are some situations when the Apply button will get enabled even if there are no changes, this is because the app changes the value of a field by itself so it's considered an update (for example, clicking on the "Reveal" button changes the text shown in a text field). The solution to this can be a bit complicated: disabling temporarily the `entryUpdated()` whenever the app is going to do an action with such side-effects. So I preferred to let the Apply button get enabled in those cases.
2018-03-10 22:31:43 -05:00
connect(m_ui->defaultIconsRadio, SIGNAL(toggled(bool)), this, SIGNAL(widgetUpdated()));
connect(m_ui->defaultIconsRadio, SIGNAL(toggled(bool)), this, SIGNAL(widgetUpdated()));
connect(m_ui->defaultIconsView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
this, SIGNAL(widgetUpdated()));
connect(m_ui->customIconsView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
this, SIGNAL(widgetUpdated()));
// clang-format on
Grey out Apply button when there are no changes Resolves #1313 What this commit does: * Whenever the Apply button is pressed, and if the save was successful, then the Apply button is disabled. * Each subwidget used by EditEntryWidget has now a signal called `widgetUpdated` that is emitted when the widgets' internal content changes. The EditEntryWidget subscribes to that signal to know when to enable the Apply button (by calling `entryUpdated()`). * There are some views that are not isolated in their own widgets (`m_advancedUi`, for example) so in those cases I invoked `entryUpdated()` directly whenever I detected an update: * some updates occur directly in a Qt widget like when editing the text of a QLineItem, so in that case I connected the widget's signals directly to the `entryUpdated()` slot. * some updates occur in EditEntryWidget, so in those cases the invocation to `entryUpdated()` is made as soon as the change is detected (for example when the user has confirmed an action in a dialog). A known problem: there are some situations when the Apply button will get enabled even if there are no changes, this is because the app changes the value of a field by itself so it's considered an update (for example, clicking on the "Reveal" button changes the text shown in a text field). The solution to this can be a bit complicated: disabling temporarily the `entryUpdated()` whenever the app is going to do an action with such side-effects. So I preferred to let the Apply button get enabled in those cases.
2018-03-10 22:31:43 -05:00
m_ui->faviconButton->setVisible(false);
m_ui->addButton->setEnabled(true);
2012-05-15 07:02:05 -04:00
}
EditWidgetIcons::~EditWidgetIcons()
{
}
IconStruct EditWidgetIcons::state()
2012-05-15 07:02:05 -04:00
{
Q_ASSERT(m_db);
Q_ASSERT(!m_currentUuid.isNull());
2012-05-15 07:02:05 -04:00
IconStruct iconStruct;
if (m_ui->defaultIconsRadio->isChecked()) {
QModelIndex index = m_ui->defaultIconsView->currentIndex();
if (index.isValid()) {
iconStruct.number = index.row();
} else {
Q_ASSERT(false);
}
} else {
2012-05-15 07:02:05 -04:00
QModelIndex index = m_ui->customIconsView->currentIndex();
if (index.isValid()) {
iconStruct.uuid = m_customIconModel->uuidFromIndex(m_ui->customIconsView->currentIndex());
} else {
iconStruct.number = -1;
}
2012-05-15 07:02:05 -04:00
}
return iconStruct;
}
void EditWidgetIcons::reset()
{
m_db.reset();
2018-03-22 17:56:05 -04:00
m_currentUuid = QUuid();
2012-05-15 07:02:05 -04:00
}
void EditWidgetIcons::load(const QUuid& currentUuid,
QSharedPointer<Database> database,
const IconStruct& iconStruct,
const QString& url)
2012-05-15 07:02:05 -04:00
{
Q_ASSERT(database);
Q_ASSERT(!currentUuid.isNull());
m_db = database;
2012-05-15 07:02:05 -04:00
m_currentUuid = currentUuid;
setUrl(url);
2012-05-15 07:02:05 -04:00
m_customIconModel->setIcons(database->metadata()->customIconsScaledPixmaps(),
database->metadata()->customIconsOrder());
2012-05-15 07:02:05 -04:00
2018-03-22 17:56:05 -04:00
QUuid iconUuid = iconStruct.uuid;
if (iconUuid.isNull()) {
int iconNumber = iconStruct.number;
m_ui->defaultIconsView->setCurrentIndex(m_defaultIconModel->index(iconNumber, 0));
m_ui->defaultIconsRadio->setChecked(true);
} else {
QModelIndex index = m_customIconModel->indexFromUuid(iconUuid);
if (index.isValid()) {
m_ui->customIconsView->setCurrentIndex(index);
m_ui->customIconsRadio->setChecked(true);
} else {
m_ui->defaultIconsView->setCurrentIndex(m_defaultIconModel->index(0, 0));
m_ui->defaultIconsRadio->setChecked(true);
2012-05-15 07:02:05 -04:00
}
}
}
void EditWidgetIcons::setUrl(const QString& url)
{
#ifdef WITH_XC_NETWORKING
m_url = QUrl(url);
m_ui->faviconButton->setVisible(!url.isEmpty());
#else
2017-03-10 08:28:26 -05:00
Q_UNUSED(url);
m_ui->faviconButton->setVisible(false);
#endif
}
#ifdef WITH_XC_NETWORKING
namespace
{
// Try to get the 2nd level domain of the host part of a QUrl. For example,
// "foo.bar.example.com" would become "example.com", and "foo.bar.example.co.uk"
// would become "example.co.uk".
QString getSecondLevelDomain(const QUrl& url)
{
QString fqdn = url.host();
fqdn.truncate(fqdn.length() - url.topLevelDomain().length());
QStringList parts = fqdn.split('.');
QString newdom = parts.takeLast() + url.topLevelDomain();
return newdom;
}
QUrl convertVariantToUrl(const QVariant& var)
{
QUrl url;
if (var.canConvert<QUrl>())
url = var.toUrl();
return url;
}
QUrl getRedirectTarget(QNetworkReply* reply)
{
QVariant var = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
QUrl url = convertVariantToUrl(var);
return url;
}
} // namespace
#endif
void EditWidgetIcons::downloadFavicon()
{
#ifdef WITH_XC_NETWORKING
m_ui->faviconButton->setDisabled(true);
m_redirects = 0;
m_urlsToTry.clear();
QString fullyQualifiedDomain = m_url.host();
QString secondLevelDomain = getSecondLevelDomain(m_url);
// Attempt to simply load the favicon.ico file
if (fullyQualifiedDomain != secondLevelDomain) {
m_urlsToTry.append(QUrl(m_url.scheme() + "://" + fullyQualifiedDomain + "/favicon.ico"));
}
m_urlsToTry.append(QUrl(m_url.scheme() + "://" + secondLevelDomain + "/favicon.ico"));
// Try to use alternative fallback URL, if enabled
if (config()->get("security/IconDownloadFallback", false).toBool()) {
QUrl fallbackUrl = QUrl("https://icons.duckduckgo.com");
fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(fullyQualifiedDomain) + ".ico");
m_urlsToTry.append(fallbackUrl);
}
startFetchFavicon(m_urlsToTry.takeFirst());
#endif
}
void EditWidgetIcons::fetchReadyRead()
{
#ifdef WITH_XC_NETWORKING
m_bytesReceived += m_reply->readAll();
#endif
}
void EditWidgetIcons::fetchFinished()
{
#ifdef WITH_XC_NETWORKING
QImage image;
bool fallbackEnabled = config()->get("security/IconDownloadFallback", false).toBool();
bool error = (m_reply->error() != QNetworkReply::NoError);
QUrl url = m_reply->url();
QUrl redirectTarget = getRedirectTarget(m_reply);
m_reply->deleteLater();
m_reply = nullptr;
if (!error) {
if (redirectTarget.isValid()) {
// Redirected, we need to follow it, or fall through if we have
// done too many redirects already.
if (m_redirects < 5) {
m_redirects++;
if (redirectTarget.isRelative())
redirectTarget = m_fetchUrl.resolved(redirectTarget);
startFetchFavicon(redirectTarget);
return;
}
} else {
// No redirect, and we theoretically have some icon data now.
image.loadFromData(m_bytesReceived);
}
}
if (!image.isNull()) {
if (!addCustomIcon(image)) {
emit messageEditEntry(tr("Custom icon already exists"), MessageWidget::Information);
} else if (!this->isVisible()) {
// Show confirmation message if triggered from Entry tab download button
emit messageEditEntry(tr("Custom icon successfully downloaded"), MessageWidget::Positive);
}
} else if (!m_urlsToTry.empty()) {
m_redirects = 0;
startFetchFavicon(m_urlsToTry.takeFirst());
return;
} else {
if (!fallbackEnabled) {
emit messageEditEntry(
tr("Unable to fetch favicon.") + "\n"
+ tr("Hint: You can enable DuckDuckGo as a fallback under Tools>Settings>Security"),
MessageWidget::Error);
} else {
emit messageEditEntry(tr("Unable to fetch favicon."), MessageWidget::Error);
}
}
m_ui->faviconButton->setDisabled(false);
#endif
}
void EditWidgetIcons::fetchCanceled()
{
#ifdef WITH_XC_NETWORKING
if (m_reply) {
m_reply->abort();
}
#endif
}
void EditWidgetIcons::startFetchFavicon(const QUrl& url)
{
#ifdef WITH_XC_NETWORKING
m_bytesReceived.clear();
m_fetchUrl = url;
QNetworkRequest request(url);
m_reply = m_netMgr->get(request);
connect(m_reply, &QNetworkReply::finished, this, &EditWidgetIcons::fetchFinished);
connect(m_reply, &QIODevice::readyRead, this, &EditWidgetIcons::fetchReadyRead);
#else
Q_UNUSED(url);
#endif
}
void EditWidgetIcons::addCustomIconFromFile()
2012-05-15 07:02:05 -04:00
{
if (m_db) {
2018-03-31 16:01:30 -04:00
QString filter = QString("%1 (%2);;%3 (*)").arg(tr("Images"), Tools::imageReaderFilter(), tr("All files"));
2012-05-15 07:02:05 -04:00
auto filenames = QFileDialog::getOpenFileNames(this, tr("Select Image(s)"), "", filter);
if (!filenames.empty()) {
QStringList errornames;
int numexisting = 0;
for (const auto& filename : filenames) {
if (!filename.isEmpty()) {
auto icon = QImage(filename);
2018-06-09 16:56:52 -04:00
if (icon.isNull()) {
errornames << filename;
2018-06-09 16:56:52 -04:00
} else if (!addCustomIcon(icon)) {
// Icon already exists in database
++numexisting;
}
}
2012-05-15 07:02:05 -04:00
}
int numloaded = filenames.size() - errornames.size() - numexisting;
QString msg;
if (numloaded > 0) {
2018-06-09 16:56:52 -04:00
msg = tr("Successfully loaded %1 of %n icon(s)", "", filenames.size()).arg(numloaded);
} else {
msg = tr("No icons were loaded");
}
if (numexisting > 0) {
2018-06-09 16:56:52 -04:00
msg += "\n" + tr("%n icon(s) already exist in the database", "", numexisting);
}
if (!errornames.empty()) {
// Show the first 8 icons that failed to load
errornames = errornames.mid(0, 8);
emit messageEditEntry(msg + "\n" + tr("The following icon(s) failed:", "", errornames.size()) + "\n"
+ errornames.join("\n"),
MessageWidget::Error);
} else if (numloaded > 0) {
emit messageEditEntry(msg, MessageWidget::Positive);
} else {
emit messageEditEntry(msg, MessageWidget::Information);
2012-05-15 07:02:05 -04:00
}
}
}
}
bool EditWidgetIcons::addCustomIcon(const QImage& icon)
{
bool added = false;
if (m_db) {
// Don't add an icon larger than 128x128, but retain original size if smaller
auto scaledicon = icon;
if (icon.width() > 128 || icon.height() > 128) {
scaledicon = icon.scaled(128, 128);
}
QUuid uuid = m_db->metadata()->findCustomIcon(scaledicon);
if (uuid.isNull()) {
2018-03-22 17:56:05 -04:00
uuid = QUuid::createUuid();
m_db->metadata()->addCustomIcon(uuid, scaledicon);
m_customIconModel->setIcons(m_db->metadata()->customIconsScaledPixmaps(),
m_db->metadata()->customIconsOrder());
added = true;
}
// Select the new or existing icon
updateRadioButtonCustomIcons();
QModelIndex index = m_customIconModel->indexFromUuid(uuid);
m_ui->customIconsView->setCurrentIndex(index);
Grey out Apply button when there are no changes Resolves #1313 What this commit does: * Whenever the Apply button is pressed, and if the save was successful, then the Apply button is disabled. * Each subwidget used by EditEntryWidget has now a signal called `widgetUpdated` that is emitted when the widgets' internal content changes. The EditEntryWidget subscribes to that signal to know when to enable the Apply button (by calling `entryUpdated()`). * There are some views that are not isolated in their own widgets (`m_advancedUi`, for example) so in those cases I invoked `entryUpdated()` directly whenever I detected an update: * some updates occur directly in a Qt widget like when editing the text of a QLineItem, so in that case I connected the widget's signals directly to the `entryUpdated()` slot. * some updates occur in EditEntryWidget, so in those cases the invocation to `entryUpdated()` is made as soon as the change is detected (for example when the user has confirmed an action in a dialog). A known problem: there are some situations when the Apply button will get enabled even if there are no changes, this is because the app changes the value of a field by itself so it's considered an update (for example, clicking on the "Reveal" button changes the text shown in a text field). The solution to this can be a bit complicated: disabling temporarily the `entryUpdated()` whenever the app is going to do an action with such side-effects. So I preferred to let the Apply button get enabled in those cases.
2018-03-10 22:31:43 -05:00
emit widgetUpdated();
}
return added;
}
2012-05-15 07:02:05 -04:00
void EditWidgetIcons::removeCustomIcon()
{
if (m_db) {
2012-05-15 07:02:05 -04:00
QModelIndex index = m_ui->customIconsView->currentIndex();
if (index.isValid()) {
2018-03-22 17:56:05 -04:00
QUuid iconUuid = m_customIconModel->uuidFromIndex(index);
2012-05-15 07:02:05 -04:00
const QList<Entry*> allEntries = m_db->rootGroup()->entriesRecursive(true);
QList<Entry*> entriesWithSameIcon;
QList<Entry*> historyEntriesWithSameIcon;
for (Entry* entry : allEntries) {
if (iconUuid == entry->iconUuid()) {
// Check if this is a history entry (no assigned group)
if (!entry->group()) {
historyEntriesWithSameIcon << entry;
} else if (m_currentUuid != entry->uuid()) {
entriesWithSameIcon << entry;
}
2012-05-15 07:02:05 -04:00
}
}
const QList<Group*> allGroups = m_db->rootGroup()->groupsRecursive(true);
QList<Group*> groupsWithSameIcon;
for (Group* group : allGroups) {
2012-05-15 07:02:05 -04:00
if (iconUuid == group->iconUuid() && m_currentUuid != group->uuid()) {
groupsWithSameIcon << group;
2012-05-15 07:02:05 -04:00
}
}
int iconUseCount = entriesWithSameIcon.size() + groupsWithSameIcon.size();
if (iconUseCount > 0) {
2018-03-31 16:01:30 -04:00
QMessageBox::StandardButton ans =
MessageBox::question(this,
tr("Confirm Delete"),
tr("This icon is used by %n entry(s), and will be replaced "
"by the default icon. Are you sure you want to delete it?",
"",
iconUseCount),
QMessageBox::Yes | QMessageBox::No);
if (ans == QMessageBox::No) {
// Early out, nothing is changed
return;
} else {
// Revert matched entries to the default entry icon
for (Entry* entry : asConst(entriesWithSameIcon)) {
entry->setIcon(Entry::DefaultIconNumber);
}
// Revert matched groups to the default group icon
for (Group* group : asConst(groupsWithSameIcon)) {
group->setIcon(Group::DefaultIconNumber);
}
2012-05-15 07:02:05 -04:00
}
}
// Remove the icon from history entries
for (Entry* entry : asConst(historyEntriesWithSameIcon)) {
entry->setUpdateTimeinfo(false);
entry->setIcon(0);
entry->setUpdateTimeinfo(true);
}
// Remove the icon from the database
m_db->metadata()->removeCustomIcon(iconUuid);
m_customIconModel->setIcons(m_db->metadata()->customIconsScaledPixmaps(),
m_db->metadata()->customIconsOrder());
// Reset the current icon view
updateRadioButtonDefaultIcons();
if (m_db->rootGroup()->findEntryByUuid(m_currentUuid) != nullptr) {
m_ui->defaultIconsView->setCurrentIndex(m_defaultIconModel->index(Entry::DefaultIconNumber));
} else {
m_ui->defaultIconsView->setCurrentIndex(m_defaultIconModel->index(Group::DefaultIconNumber));
2012-05-15 07:02:05 -04:00
}
Grey out Apply button when there are no changes Resolves #1313 What this commit does: * Whenever the Apply button is pressed, and if the save was successful, then the Apply button is disabled. * Each subwidget used by EditEntryWidget has now a signal called `widgetUpdated` that is emitted when the widgets' internal content changes. The EditEntryWidget subscribes to that signal to know when to enable the Apply button (by calling `entryUpdated()`). * There are some views that are not isolated in their own widgets (`m_advancedUi`, for example) so in those cases I invoked `entryUpdated()` directly whenever I detected an update: * some updates occur directly in a Qt widget like when editing the text of a QLineItem, so in that case I connected the widget's signals directly to the `entryUpdated()` slot. * some updates occur in EditEntryWidget, so in those cases the invocation to `entryUpdated()` is made as soon as the change is detected (for example when the user has confirmed an action in a dialog). A known problem: there are some situations when the Apply button will get enabled even if there are no changes, this is because the app changes the value of a field by itself so it's considered an update (for example, clicking on the "Reveal" button changes the text shown in a text field). The solution to this can be a bit complicated: disabling temporarily the `entryUpdated()` whenever the app is going to do an action with such side-effects. So I preferred to let the Apply button get enabled in those cases.
2018-03-10 22:31:43 -05:00
emit widgetUpdated();
2012-05-15 07:02:05 -04:00
}
}
}
void EditWidgetIcons::updateWidgetsDefaultIcons(bool check)
{
if (check) {
QModelIndex index = m_ui->defaultIconsView->currentIndex();
if (!index.isValid()) {
m_ui->defaultIconsView->setCurrentIndex(m_defaultIconModel->index(0, 0));
} else {
2012-05-15 07:02:05 -04:00
m_ui->defaultIconsView->setCurrentIndex(index);
}
m_ui->customIconsView->selectionModel()->clearSelection();
m_ui->deleteButton->setEnabled(false);
}
}
void EditWidgetIcons::updateWidgetsCustomIcons(bool check)
{
if (check) {
QModelIndex index = m_ui->customIconsView->currentIndex();
if (!index.isValid()) {
m_ui->customIconsView->setCurrentIndex(m_customIconModel->index(0, 0));
} else {
2012-05-15 07:02:05 -04:00
m_ui->customIconsView->setCurrentIndex(index);
}
m_ui->defaultIconsView->selectionModel()->clearSelection();
m_ui->deleteButton->setEnabled(true);
}
}
void EditWidgetIcons::updateRadioButtonDefaultIcons()
{
m_ui->defaultIconsRadio->setChecked(true);
}
void EditWidgetIcons::updateRadioButtonCustomIcons()
{
m_ui->customIconsRadio->setChecked(true);
}