2
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation, either version 2 or (at your option)
7
* version 3 of the License.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details.
14
* You should have received a copy of the GNU General Public License
15
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
#include "EntryAttachmentsWidget.h"
19
#include "ui_EntryAttachmentsWidget.h"
24
#include <QStandardPaths>
25
#include <QTemporaryFile>
27
#include "EntryAttachmentsModel.h"
28
#include "core/Config.h"
29
#include "core/EntryAttachments.h"
30
#include "core/Tools.h"
31
#include "gui/FileDialog.h"
32
#include "gui/MessageBox.h"
34
EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
36
, m_ui(new Ui::EntryAttachmentsWidget)
37
, m_entryAttachments(nullptr)
38
, m_attachmentsModel(new EntryAttachmentsModel(this))
40
, m_buttonsVisible(true)
44
m_ui->attachmentsView->setAcceptDrops(false);
45
m_ui->attachmentsView->viewport()->setAcceptDrops(true);
46
m_ui->attachmentsView->viewport()->installEventFilter(this);
48
m_ui->attachmentsView->setModel(m_attachmentsModel);
49
m_ui->attachmentsView->verticalHeader()->hide();
50
m_ui->attachmentsView->horizontalHeader()->setStretchLastSection(true);
51
m_ui->attachmentsView->horizontalHeader()->resizeSection(EntryAttachmentsModel::NameColumn, 400);
52
m_ui->attachmentsView->setSelectionBehavior(QAbstractItemView::SelectRows);
53
m_ui->attachmentsView->setSelectionMode(QAbstractItemView::ExtendedSelection);
54
m_ui->attachmentsView->setEditTriggers(QAbstractItemView::SelectedClicked);
56
connect(this, SIGNAL(buttonsVisibleChanged(bool)), this, SLOT(updateButtonsVisible()));
57
connect(this, SIGNAL(readOnlyChanged(bool)), SLOT(updateButtonsEnabled()));
58
connect(m_attachmentsModel, SIGNAL(modelReset()), SLOT(updateButtonsEnabled()));
61
connect(m_ui->attachmentsView->selectionModel(),
62
SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
63
SLOT(updateButtonsEnabled()));
65
connect(this, SIGNAL(readOnlyChanged(bool)), m_attachmentsModel, SLOT(setReadOnly(bool)));
67
connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(openAttachment(QModelIndex)));
68
connect(m_ui->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments()));
69
connect(m_ui->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments()));
70
connect(m_ui->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments()));
71
connect(m_ui->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments()));
72
connect(m_ui->renameAttachmentButton, SIGNAL(clicked()), SLOT(renameSelectedAttachments()));
74
updateButtonsVisible();
75
updateButtonsEnabled();
78
EntryAttachmentsWidget::~EntryAttachmentsWidget() = default;
80
const EntryAttachments* EntryAttachmentsWidget::attachments() const
82
return m_entryAttachments;
85
bool EntryAttachmentsWidget::isReadOnly() const
90
bool EntryAttachmentsWidget::isButtonsVisible() const
92
return m_buttonsVisible;
95
void EntryAttachmentsWidget::linkAttachments(EntryAttachments* attachments)
99
m_entryAttachments = attachments;
100
m_attachmentsModel->setEntryAttachments(m_entryAttachments);
102
if (m_entryAttachments) {
103
connect(m_entryAttachments,
104
SIGNAL(valueModifiedExternally(QString, QString)),
106
SLOT(attachmentModifiedExternally(QString, QString)));
107
connect(m_entryAttachments, SIGNAL(modified()), this, SIGNAL(widgetUpdated()));
111
void EntryAttachmentsWidget::unlinkAttachments()
113
if (m_entryAttachments) {
114
m_entryAttachments->disconnect(this);
115
m_entryAttachments = nullptr;
116
m_attachmentsModel->setEntryAttachments(nullptr);
120
void EntryAttachmentsWidget::setReadOnly(bool readOnly)
122
if (m_readOnly == readOnly) {
126
m_readOnly = readOnly;
127
emit readOnlyChanged(m_readOnly);
130
void EntryAttachmentsWidget::setButtonsVisible(bool buttonsVisible)
132
if (m_buttonsVisible == buttonsVisible) {
136
m_buttonsVisible = buttonsVisible;
137
emit buttonsVisibleChanged(m_buttonsVisible);
140
void EntryAttachmentsWidget::insertAttachments()
142
Q_ASSERT(m_entryAttachments);
143
Q_ASSERT(!isReadOnly());
148
QString defaultDirPath = FileDialog::getLastDir("attachments");
149
const auto filenames = fileDialog()->getOpenFileNames(this, tr("Select files"), defaultDirPath);
150
if (filenames.isEmpty()) {
153
const auto confirmedFileNames = confirmAttachmentSelection(filenames);
154
if (confirmedFileNames.isEmpty()) {
157
// Save path to first filename
158
FileDialog::saveLastDir("attachments", filenames[0]);
159
QString errorMessage;
160
if (!insertAttachments(confirmedFileNames, errorMessage)) {
161
errorOccurred(errorMessage);
163
emit widgetUpdated();
166
void EntryAttachmentsWidget::removeSelectedAttachments()
168
Q_ASSERT(m_entryAttachments);
169
Q_ASSERT(!isReadOnly());
174
const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0);
175
if (indexes.isEmpty()) {
179
auto result = MessageBox::question(this,
180
tr("Confirm remove"),
181
tr("Are you sure you want to remove %n attachment(s)?", "", indexes.count()),
182
MessageBox::Remove | MessageBox::Cancel,
185
if (result == MessageBox::Remove) {
187
for (const QModelIndex& index : indexes) {
188
keys.append(m_attachmentsModel->keyByIndex(index));
190
m_entryAttachments->remove(keys);
191
emit widgetUpdated();
195
void EntryAttachmentsWidget::renameSelectedAttachments()
197
Q_ASSERT(m_entryAttachments);
198
m_ui->attachmentsView->edit(m_ui->attachmentsView->selectionModel()->selectedIndexes().first());
201
void EntryAttachmentsWidget::saveSelectedAttachments()
203
Q_ASSERT(m_entryAttachments);
205
const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0);
206
if (indexes.isEmpty()) {
210
QString defaultDirPath = FileDialog::getLastDir("attachments");
211
const QString saveDirPath = fileDialog()->getExistingDirectory(this, tr("Save attachments"), defaultDirPath);
212
if (saveDirPath.isEmpty()) {
216
QDir saveDir(saveDirPath);
217
if (!saveDir.exists()) {
218
if (saveDir.mkpath(saveDir.absolutePath())) {
219
errorOccurred(tr("Unable to create directory:\n%1").arg(saveDir.absolutePath()));
223
FileDialog::saveLastDir("attachments", saveDirPath);
226
for (const QModelIndex& index : indexes) {
227
const QString filename = m_attachmentsModel->keyByIndex(index);
228
const QString attachmentPath = saveDir.absoluteFilePath(filename);
230
if (QFileInfo::exists(attachmentPath)) {
232
MessageBox::Buttons buttons = MessageBox::Overwrite | MessageBox::Cancel;
233
if (indexes.length() > 1) {
234
buttons |= MessageBox::Skip;
237
const QString questionText(
238
tr("Are you sure you want to overwrite the existing file \"%1\" with the attachment?"));
240
auto result = MessageBox::question(
241
this, tr("Confirm overwrite"), questionText.arg(filename), buttons, MessageBox::Cancel);
243
if (result == MessageBox::Skip) {
245
} else if (result == MessageBox::Cancel) {
250
QFile file(attachmentPath);
251
const QByteArray attachmentData = m_entryAttachments->value(filename);
252
const bool saveOk = file.open(QIODevice::WriteOnly) && file.setPermissions(QFile::ReadUser | QFile::WriteUser)
253
&& file.write(attachmentData) == attachmentData.size();
255
errors.append(QString("%1 - %2").arg(filename, file.errorString()));
259
if (!errors.isEmpty()) {
260
errorOccurred(tr("Unable to save attachments:\n%1").arg(errors.join('\n')));
264
void EntryAttachmentsWidget::openAttachment(const QModelIndex& index)
266
Q_ASSERT(index.isValid());
267
if (!index.isValid()) {
271
QString errorMessage;
272
if (!m_entryAttachments->openAttachment(m_attachmentsModel->keyByIndex(index), &errorMessage)) {
273
errorOccurred(tr("Unable to open attachment:\n%1").arg(errorMessage));
277
void EntryAttachmentsWidget::openSelectedAttachments()
279
const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0);
280
if (indexes.isEmpty()) {
285
for (const QModelIndex& index : indexes) {
286
QString errorMessage;
287
if (!m_entryAttachments->openAttachment(m_attachmentsModel->keyByIndex(index), &errorMessage)) {
288
const QString filename = m_attachmentsModel->keyByIndex(index);
289
errors.append(QString("%1 - %2").arg(filename, errorMessage));
293
if (!errors.isEmpty()) {
294
errorOccurred(tr("Unable to open attachments:\n%1").arg(errors.join('\n')));
298
void EntryAttachmentsWidget::updateButtonsEnabled()
300
const bool hasSelection = m_ui->attachmentsView->selectionModel()->hasSelection();
302
m_ui->addAttachmentButton->setEnabled(!m_readOnly);
303
m_ui->removeAttachmentButton->setEnabled(hasSelection && !m_readOnly);
304
m_ui->renameAttachmentButton->setEnabled(hasSelection && !m_readOnly);
306
m_ui->saveAttachmentButton->setEnabled(hasSelection);
307
m_ui->openAttachmentButton->setEnabled(hasSelection);
310
void EntryAttachmentsWidget::updateButtonsVisible()
312
m_ui->addAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
313
m_ui->removeAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
314
m_ui->renameAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
317
bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage)
319
Q_ASSERT(!isReadOnly());
325
for (const QString& filename : filenames) {
327
QFile file(filename);
328
const QFileInfo fInfo(filename);
329
const bool readOk = file.open(QIODevice::ReadOnly) && Tools::readAllFromDevice(&file, data);
331
m_entryAttachments->set(fInfo.fileName(), data);
333
errors.append(QString("%1 - %2").arg(fInfo.fileName(), file.errorString()));
337
if (!errors.isEmpty()) {
338
errorMessage = tr("Unable to open file(s):\n%1", "", errors.size()).arg(errors.join('\n'));
341
return errors.isEmpty();
344
QStringList EntryAttachmentsWidget::confirmAttachmentSelection(const QStringList& filenames)
346
QStringList confirmedFileNames;
347
for (const auto& file : filenames) {
348
const QFileInfo fileInfo(file);
349
auto fileName = fileInfo.fileName();
351
// Ask for confirmation if overwriting an existing attachment
352
if (m_entryAttachments->hasKey(fileName)) {
353
auto result = MessageBox::question(this,
354
tr("Confirm Overwrite Attachment"),
355
tr("Attachment \"%1\" already exists. \n"
356
"Would you like to overwrite the existing attachment?")
358
MessageBox::Overwrite | MessageBox::No,
360
if (result == MessageBox::No) {
365
// Ask for confirmation before adding files over 5 MB in size
366
double size = fileInfo.size() / (1024.0 * 1024.0);
369
MessageBox::question(this,
370
tr("Confirm Attachment"),
371
tr("%1 is a big file (%2 MB).\nYour database may get very large and reduce "
372
"performance.\n\nAre you sure to add this file?")
373
.arg(fileName, QString::number(size, 'f', 1)),
374
MessageBox::Yes | MessageBox::No,
376
if (result == MessageBox::No) {
381
confirmedFileNames << file;
384
return confirmedFileNames;
387
bool EntryAttachmentsWidget::eventFilter(QObject* watched, QEvent* e)
389
if (watched == m_ui->attachmentsView->viewport() && !isReadOnly()) {
390
const QEvent::Type eventType = e->type();
391
if (eventType == QEvent::DragEnter || eventType == QEvent::DragMove) {
392
auto dropEv = static_cast<QDropEvent*>(e);
393
const QMimeData* mimeData = dropEv->mimeData();
394
if (mimeData->hasUrls()) {
395
dropEv->acceptProposedAction();
398
} else if (eventType == QEvent::Drop) {
399
auto dropEv = static_cast<QDropEvent*>(e);
400
const QMimeData* mimeData = dropEv->mimeData();
401
if (mimeData->hasUrls()) {
402
dropEv->acceptProposedAction();
403
QStringList filenames;
404
const QList<QUrl> urls = mimeData->urls();
405
for (const QUrl& url : urls) {
406
const QFileInfo fInfo(url.toLocalFile());
407
if (fInfo.isFile()) {
408
filenames.append(fInfo.absoluteFilePath());
412
QString errorMessage;
413
if (!insertAttachments(filenames, errorMessage)) {
414
errorOccurred(errorMessage);
422
return QWidget::eventFilter(watched, e);
425
void EntryAttachmentsWidget::attachmentModifiedExternally(const QString& key, const QString& filePath)
427
if (m_pendingChanges.contains(filePath)) {
431
m_pendingChanges << filePath;
433
auto result = MessageBox::question(
435
tr("Attachment modified"),
436
tr("The attachment '%1' was modified.\nDo you want to save the changes to your database?").arg(key),
437
MessageBox::Save | MessageBox::Discard,
440
if (result == MessageBox::Save) {
442
if (f.open(QFile::ReadOnly)) {
443
m_entryAttachments->set(key, f.readAll());
445
emit widgetUpdated();
447
MessageBox::critical(this,
448
tr("Saving attachment failed"),
449
tr("Saving updated attachment failed.\nError: %1").arg(f.errorString()));
453
m_pendingChanges.removeAll(filePath);