keepassxc/src/gui/entry/EntryAttachmentsWidget.cpp

393 lines
13 KiB
C++
Raw Normal View History

#include "EntryAttachmentsWidget.h"
#include "ui_EntryAttachmentsWidget.h"
#include <QDesktopServices>
#include <QDir>
#include <QDropEvent>
#include <QFile>
#include <QFileInfo>
#include <QMimeData>
#include <QProcessEnvironment>
#include <QTemporaryFile>
#include "EntryAttachmentsModel.h"
#include "config-keepassx.h"
#include "core/Config.h"
#include "core/EntryAttachments.h"
#include "core/Tools.h"
#include "gui/FileDialog.h"
#include "gui/MessageBox.h"
2018-03-31 16:01:30 -04:00
EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::EntryAttachmentsWidget)
, m_entryAttachments(new EntryAttachments(this))
, m_attachmentsModel(new EntryAttachmentsModel(this))
, m_readOnly(false)
, m_buttonsVisible(true)
{
m_ui->setupUi(this);
m_ui->attachmentsView->setAcceptDrops(false);
m_ui->attachmentsView->viewport()->setAcceptDrops(true);
m_ui->attachmentsView->viewport()->installEventFilter(this);
m_attachmentsModel->setEntryAttachments(m_entryAttachments);
m_ui->attachmentsView->setModel(m_attachmentsModel);
m_ui->attachmentsView->verticalHeader()->hide();
m_ui->attachmentsView->horizontalHeader()->setStretchLastSection(true);
m_ui->attachmentsView->horizontalHeader()->resizeSection(EntryAttachmentsModel::NameColumn, 400);
m_ui->attachmentsView->setSelectionBehavior(QAbstractItemView::SelectRows);
m_ui->attachmentsView->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_ui->actionsWidget->setVisible(m_buttonsVisible);
connect(this, SIGNAL(buttonsVisibleChanged(bool)), m_ui->actionsWidget, SLOT(setVisible(bool)));
connect(this, SIGNAL(readOnlyChanged(bool)), SLOT(updateButtonsEnabled()));
connect(m_attachmentsModel, SIGNAL(modelReset()), SLOT(updateButtonsEnabled()));
// clang-format off
2018-03-31 16:01:30 -04:00
connect(m_ui->attachmentsView->selectionModel(),
SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
SLOT(updateButtonsEnabled()));
// clang-format on
connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(openAttachment(QModelIndex)));
connect(m_ui->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments()));
connect(m_ui->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments()));
connect(m_ui->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments()));
connect(m_ui->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments()));
updateButtonsEnabled();
}
EntryAttachmentsWidget::~EntryAttachmentsWidget()
{
}
const EntryAttachments* EntryAttachmentsWidget::entryAttachments() const
{
return m_entryAttachments;
}
bool EntryAttachmentsWidget::isReadOnly() const
{
return m_readOnly;
}
bool EntryAttachmentsWidget::isButtonsVisible() const
{
return m_buttonsVisible;
}
void EntryAttachmentsWidget::setEntryAttachments(const EntryAttachments* attachments)
{
Q_ASSERT(attachments != nullptr);
m_entryAttachments->copyDataFrom(attachments);
}
void EntryAttachmentsWidget::clearAttachments()
{
m_entryAttachments->clear();
}
void EntryAttachmentsWidget::setReadOnly(bool readOnly)
{
if (m_readOnly == readOnly) {
return;
}
m_readOnly = readOnly;
emit readOnlyChanged(m_readOnly);
}
void EntryAttachmentsWidget::setButtonsVisible(bool buttonsVisible)
{
if (m_buttonsVisible == buttonsVisible) {
return;
}
m_buttonsVisible = buttonsVisible;
emit buttonsVisibleChanged(m_buttonsVisible);
}
QByteArray EntryAttachmentsWidget::getAttachment(const QString& name)
{
return m_entryAttachments->value(name);
}
void EntryAttachmentsWidget::setAttachment(const QString& name, const QByteArray& value)
{
m_entryAttachments->set(name, value);
}
void EntryAttachmentsWidget::removeAttachment(const QString& name)
{
if (!isReadOnly() && m_entryAttachments->hasKey(name)) {
m_entryAttachments->remove(name);
}
}
void EntryAttachmentsWidget::insertAttachments()
{
Q_ASSERT(!isReadOnly());
if (isReadOnly()) {
return;
}
QString defaultDirPath = config()->get("LastAttachmentDir").toString();
const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists();
if (!dirExists) {
defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first();
}
const QStringList filenames = fileDialog()->getOpenFileNames(this, tr("Select files"), defaultDirPath);
if (filenames.isEmpty()) {
return;
}
config()->set("LastAttachmentDir", QFileInfo(filenames.first()).absolutePath());
QString errorMessage;
if (!insertAttachments(filenames, errorMessage)) {
errorOccurred(errorMessage);
}
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();
}
void EntryAttachmentsWidget::removeSelectedAttachments()
{
Q_ASSERT(!isReadOnly());
if (isReadOnly()) {
return;
}
const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0);
if (indexes.isEmpty()) {
return;
}
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
auto result = MessageBox::question(this,
tr("Confirm remove"),
tr("Are you sure you want to remove %n attachment(s)?", "", indexes.count()),
MessageBox::Remove | MessageBox::Cancel,
MessageBox::Cancel);
if (result == MessageBox::Remove) {
QStringList keys;
2018-03-31 16:01:30 -04:00
for (const QModelIndex& index : indexes) {
keys.append(m_attachmentsModel->keyByIndex(index));
}
m_entryAttachments->remove(keys);
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();
}
}
void EntryAttachmentsWidget::saveSelectedAttachments()
{
const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0);
if (indexes.isEmpty()) {
return;
}
QString defaultDirPath = config()->get("LastAttachmentDir").toString();
2018-03-31 16:01:30 -04:00
const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists();
if (!dirExists) {
defaultDirPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
}
const QString saveDirPath = fileDialog()->getExistingDirectory(this, tr("Save attachments"), defaultDirPath);
if (saveDirPath.isEmpty()) {
return;
}
QDir saveDir(saveDirPath);
if (!saveDir.exists()) {
if (saveDir.mkpath(saveDir.absolutePath())) {
errorOccurred(tr("Unable to create directory:\n%1").arg(saveDir.absolutePath()));
return;
}
}
config()->set("LastAttachmentDir", QFileInfo(saveDir.absolutePath()).absolutePath());
QStringList errors;
2018-03-31 16:01:30 -04:00
for (const QModelIndex& index : indexes) {
const QString filename = m_attachmentsModel->keyByIndex(index);
const QString attachmentPath = saveDir.absoluteFilePath(filename);
if (QFileInfo::exists(attachmentPath)) {
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
MessageBox::Buttons buttons = MessageBox::Overwrite | MessageBox::Cancel;
if (indexes.length() > 1) {
buttons |= MessageBox::Skip;
}
const QString questionText(
2018-03-31 16:01:30 -04:00
tr("Are you sure you want to overwrite the existing file \"%1\" with the attachment?"));
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
auto result = MessageBox::question(
this, tr("Confirm overwrite"), questionText.arg(filename), buttons, MessageBox::Cancel);
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
if (result == MessageBox::Skip) {
continue;
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
} else if (result == MessageBox::Cancel) {
return;
}
}
QFile file(attachmentPath);
const QByteArray attachmentData = m_entryAttachments->value(filename);
const bool saveOk = file.open(QIODevice::WriteOnly) && file.write(attachmentData) == attachmentData.size();
if (!saveOk) {
errors.append(QString("%1 - %2").arg(filename, file.errorString()));
}
}
if (!errors.isEmpty()) {
errorOccurred(tr("Unable to save attachments:\n%1").arg(errors.join('\n')));
}
}
void EntryAttachmentsWidget::openAttachment(const QModelIndex& index)
{
Q_ASSERT(index.isValid());
if (!index.isValid()) {
return;
}
QString errorMessage;
if (!openAttachment(index, errorMessage)) {
errorOccurred(tr("Unable to open attachment:\n%1").arg(errorMessage));
}
}
void EntryAttachmentsWidget::openSelectedAttachments()
{
const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0);
if (indexes.isEmpty()) {
return;
}
QStringList errors;
2018-03-31 16:01:30 -04:00
for (const QModelIndex& index : indexes) {
QString errorMessage;
if (!openAttachment(index, errorMessage)) {
const QString filename = m_attachmentsModel->keyByIndex(index);
errors.append(QString("%1 - %2").arg(filename, errorMessage));
};
}
if (!errors.isEmpty()) {
errorOccurred(tr("Unable to open attachments:\n%1").arg(errors.join('\n')));
}
}
void EntryAttachmentsWidget::updateButtonsEnabled()
{
const bool hasSelection = m_ui->attachmentsView->selectionModel()->hasSelection();
m_ui->addAttachmentButton->setEnabled(!m_readOnly);
m_ui->removeAttachmentButton->setEnabled(hasSelection && !m_readOnly);
m_ui->saveAttachmentButton->setEnabled(hasSelection);
m_ui->openAttachmentButton->setEnabled(hasSelection);
}
bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage)
{
Q_ASSERT(!isReadOnly());
if (isReadOnly()) {
return false;
}
QStringList errors;
2018-03-31 16:01:30 -04:00
for (const QString& filename : filenames) {
QByteArray data;
QFile file(filename);
const QFileInfo fInfo(filename);
const bool readOk = file.open(QIODevice::ReadOnly) && Tools::readAllFromDevice(&file, data);
if (readOk) {
m_entryAttachments->set(fInfo.fileName(), data);
} else {
errors.append(QString("%1 - %2").arg(fInfo.fileName(), file.errorString()));
}
}
if (!errors.isEmpty()) {
errorMessage = tr("Unable to open file(s):\n%1", "", errors.size()).arg(errors.join('\n'));
}
return errors.isEmpty();
}
bool EntryAttachmentsWidget::openAttachment(const QModelIndex& index, QString& errorMessage)
{
const QString filename = m_attachmentsModel->keyByIndex(index);
const QByteArray attachmentData = m_entryAttachments->value(filename);
// tmp file will be removed once the database (or the application) has been closed
#ifdef KEEPASSXC_DIST_SNAP
const QString tmpFileTemplate =
QString("%1/XXXXXX.%2").arg(QProcessEnvironment::systemEnvironment().value("SNAP_USER_DATA"), filename);
#else
const QString tmpFileTemplate = QDir::temp().absoluteFilePath(QString("XXXXXX.").append(filename));
#endif
QScopedPointer<QTemporaryFile> tmpFile(new QTemporaryFile(tmpFileTemplate, this));
2018-03-31 16:01:30 -04:00
const bool saveOk = tmpFile->open() && tmpFile->write(attachmentData) == attachmentData.size() && tmpFile->flush();
if (!saveOk) {
errorMessage = QString("%1 - %2").arg(filename, tmpFile->errorString());
return false;
}
tmpFile->close();
const bool openOk = QDesktopServices::openUrl(QUrl::fromLocalFile(tmpFile->fileName()));
if (!openOk) {
errorMessage = QString("Can't open file \"%1\"").arg(filename);
return false;
}
// take ownership of the tmpFile pointer
tmpFile.take();
return true;
}
bool EntryAttachmentsWidget::eventFilter(QObject* watched, QEvent* e)
{
if (watched == m_ui->attachmentsView->viewport() && !isReadOnly()) {
const QEvent::Type eventType = e->type();
if (eventType == QEvent::DragEnter || eventType == QEvent::DragMove) {
QDropEvent* dropEv = static_cast<QDropEvent*>(e);
const QMimeData* mimeData = dropEv->mimeData();
if (mimeData->hasUrls()) {
dropEv->acceptProposedAction();
return true;
}
} else if (eventType == QEvent::Drop) {
QDropEvent* dropEv = static_cast<QDropEvent*>(e);
const QMimeData* mimeData = dropEv->mimeData();
if (mimeData->hasUrls()) {
dropEv->acceptProposedAction();
QStringList filenames;
const QList<QUrl> urls = mimeData->urls();
2018-03-31 16:01:30 -04:00
for (const QUrl& url : urls) {
const QFileInfo fInfo(url.toLocalFile());
if (fInfo.isFile()) {
filenames.append(fInfo.absoluteFilePath());
}
}
QString errorMessage;
if (!insertAttachments(filenames, errorMessage)) {
errorOccurred(errorMessage);
}
return true;
}
}
}
return QWidget::eventFilter(watched, e);
}