Files
keepassxc/src/gui/entry/EntryAttachmentsWidget.cpp
2021-08-22 17:09:21 -04:00

440 lines
15 KiB
C++

/*
* Copyright (C) 2021 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 "EntryAttachmentsWidget.h"
#include "ui_EntryAttachmentsWidget.h"
#include <QDir>
#include <QDropEvent>
#include <QMimeData>
#include <QStandardPaths>
#include <QTemporaryFile>
#include "EntryAttachmentsModel.h"
#include "core/Config.h"
#include "core/EntryAttachments.h"
#include "core/Tools.h"
#include "gui/FileDialog.h"
#include "gui/MessageBox.h"
EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::EntryAttachmentsWidget)
, m_entryAttachments(nullptr)
, 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_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->attachmentsView->setEditTriggers(QAbstractItemView::SelectedClicked);
connect(this, SIGNAL(buttonsVisibleChanged(bool)), this, SLOT(updateButtonsVisible()));
connect(this, SIGNAL(readOnlyChanged(bool)), SLOT(updateButtonsEnabled()));
connect(m_attachmentsModel, SIGNAL(modelReset()), SLOT(updateButtonsEnabled()));
// clang-format off
connect(m_ui->attachmentsView->selectionModel(),
SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
SLOT(updateButtonsEnabled()));
// clang-format on
connect(this, SIGNAL(readOnlyChanged(bool)), m_attachmentsModel, SLOT(setReadOnly(bool)));
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()));
connect(m_ui->renameAttachmentButton, SIGNAL(clicked()), SLOT(renameSelectedAttachments()));
updateButtonsVisible();
updateButtonsEnabled();
}
EntryAttachmentsWidget::~EntryAttachmentsWidget()
{
}
const EntryAttachments* EntryAttachmentsWidget::attachments() const
{
return m_entryAttachments;
}
bool EntryAttachmentsWidget::isReadOnly() const
{
return m_readOnly;
}
bool EntryAttachmentsWidget::isButtonsVisible() const
{
return m_buttonsVisible;
}
void EntryAttachmentsWidget::linkAttachments(EntryAttachments* attachments)
{
unlinkAttachments();
m_entryAttachments = attachments;
m_attachmentsModel->setEntryAttachments(m_entryAttachments);
if (m_entryAttachments) {
connect(m_entryAttachments,
SIGNAL(valueModifiedExternally(QString, QString)),
this,
SLOT(attachmentModifiedExternally(QString, QString)));
connect(m_entryAttachments, SIGNAL(modified()), this, SIGNAL(widgetUpdated()));
}
}
void EntryAttachmentsWidget::unlinkAttachments()
{
if (m_entryAttachments) {
m_entryAttachments->disconnect(this);
m_entryAttachments = nullptr;
m_attachmentsModel->setEntryAttachments(nullptr);
}
}
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);
}
void EntryAttachmentsWidget::insertAttachments()
{
Q_ASSERT(m_entryAttachments);
Q_ASSERT(!isReadOnly());
if (isReadOnly()) {
return;
}
QString defaultDirPath = FileDialog::getLastDir("attachments");
const auto filenames = fileDialog()->getOpenFileNames(this, tr("Select files"), defaultDirPath);
if (filenames.isEmpty()) {
return;
}
const auto confirmedFileNames = confirmLargeAttachments(filenames);
if (confirmedFileNames.isEmpty()) {
return;
}
// Save path to first filename
FileDialog::saveLastDir("attachments", filenames[0]);
QString errorMessage;
if (!insertAttachments(confirmedFileNames, errorMessage)) {
errorOccurred(errorMessage);
}
emit widgetUpdated();
}
void EntryAttachmentsWidget::removeSelectedAttachments()
{
Q_ASSERT(m_entryAttachments);
Q_ASSERT(!isReadOnly());
if (isReadOnly()) {
return;
}
const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0);
if (indexes.isEmpty()) {
return;
}
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;
for (const QModelIndex& index : indexes) {
keys.append(m_attachmentsModel->keyByIndex(index));
}
m_entryAttachments->remove(keys);
emit widgetUpdated();
}
}
void EntryAttachmentsWidget::renameSelectedAttachments()
{
Q_ASSERT(m_entryAttachments);
m_ui->attachmentsView->edit(m_ui->attachmentsView->selectionModel()->selectedIndexes().first());
}
void EntryAttachmentsWidget::saveSelectedAttachments()
{
Q_ASSERT(m_entryAttachments);
const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0);
if (indexes.isEmpty()) {
return;
}
QString defaultDirPath = FileDialog::getLastDir("attachments");
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;
}
}
FileDialog::saveLastDir("attachments", saveDirPath);
QStringList errors;
for (const QModelIndex& index : indexes) {
const QString filename = m_attachmentsModel->keyByIndex(index);
const QString attachmentPath = saveDir.absoluteFilePath(filename);
if (QFileInfo::exists(attachmentPath)) {
MessageBox::Buttons buttons = MessageBox::Overwrite | MessageBox::Cancel;
if (indexes.length() > 1) {
buttons |= MessageBox::Skip;
}
const QString questionText(
tr("Are you sure you want to overwrite the existing file \"%1\" with the attachment?"));
auto result = MessageBox::question(
this, tr("Confirm overwrite"), questionText.arg(filename), buttons, MessageBox::Cancel);
if (result == MessageBox::Skip) {
continue;
} else if (result == MessageBox::Cancel) {
return;
}
}
QFile file(attachmentPath);
const QByteArray attachmentData = m_entryAttachments->value(filename);
const bool saveOk = file.open(QIODevice::WriteOnly) && file.setPermissions(QFile::ReadUser | QFile::WriteUser)
&& 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 (!m_entryAttachments->openAttachment(m_attachmentsModel->keyByIndex(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;
for (const QModelIndex& index : indexes) {
QString errorMessage;
if (!m_entryAttachments->openAttachment(m_attachmentsModel->keyByIndex(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->renameAttachmentButton->setEnabled(hasSelection && !m_readOnly);
m_ui->saveAttachmentButton->setEnabled(hasSelection);
m_ui->openAttachmentButton->setEnabled(hasSelection);
}
void EntryAttachmentsWidget::updateButtonsVisible()
{
m_ui->addAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
m_ui->removeAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
}
bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage)
{
Q_ASSERT(!isReadOnly());
if (isReadOnly()) {
return false;
}
QStringList errors;
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();
}
QStringList EntryAttachmentsWidget::confirmLargeAttachments(const QStringList& filenames)
{
const QString confirmation(tr("%1 is a big file (%2 MB).\nYour database may get very large and reduce "
"performance.\n\nAre you sure to add this file?"));
QStringList confirmedFileNames;
for (const auto& file : filenames) {
QFileInfo fileInfo(file);
double size = fileInfo.size() / (1024.0 * 1024.0);
// Ask for confirmation before adding files over 5 MB in size
if (size > 5.0) {
auto fileName = fileInfo.fileName();
auto result = MessageBox::question(this,
tr("Confirm Attachment"),
confirmation.arg(fileName, QString::number(size, 'f', 1)),
MessageBox::Yes | MessageBox::No,
MessageBox::No);
if (result == MessageBox::Yes) {
confirmedFileNames << file;
}
} else {
confirmedFileNames << file;
}
}
return confirmedFileNames;
}
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();
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);
}
void EntryAttachmentsWidget::attachmentModifiedExternally(const QString& key, const QString& filePath)
{
if (m_pendingChanges.contains(filePath)) {
return;
}
m_pendingChanges << filePath;
auto result = MessageBox::question(
this,
tr("Attachment modified"),
tr("The attachment '%1' was modified.\nDo you want to save the changes to your database?").arg(key),
MessageBox::Save | MessageBox::Discard,
MessageBox::Save);
if (result == MessageBox::Save) {
QFile f(filePath);
if (f.open(QFile::ReadOnly)) {
m_entryAttachments->set(key, f.readAll());
f.close();
emit widgetUpdated();
} else {
MessageBox::critical(this,
tr("Saving attachment failed"),
tr("Saving updated attachment failed.\nError: %1").arg(f.errorString()));
}
}
m_pendingChanges.removeAll(filePath);
}