Improve and secure attachment handling (fixes #2400).

Externally opened attachments are now lifecycle-managed properly.

The temporary files are created with stricter permissions and entirely
random names (except for the file extension) to prevent meta data leakage.

When the database is closed, the files are overwritten with random
data and are also more reliably deleted than before.

Changes to the temporary files are monitored and the user is asked
if they want to save the changes back to the database (fixes #3130).

KeePassXC does not keep a lock on any of the temporary files, resolving
long-standing issues with applications such as Adobe Acrobat on Windows
(fixes #5950, fixes #5839).

Internally, attachments are copied less. The EntryAttachmentsWidget
now only references EntryAttachments instead of owning a separate copy
(which used to not be cleared properly under certain circumstances).
This commit is contained in:
Janek Bevendorff 2021-06-08 19:54:36 +02:00 committed by Jonathan White
parent af9eb6d6b1
commit 93f0fef1e1
11 changed files with 245 additions and 93 deletions

Binary file not shown.

View File

@ -18,14 +18,25 @@
#include "EntryAttachments.h"
#include "core/Global.h"
#include "crypto/Random.h"
#include <QDesktopServices>
#include <QDir>
#include <QProcessEnvironment>
#include <QSet>
#include <QTemporaryFile>
#include <QUrl>
EntryAttachments::EntryAttachments(QObject* parent)
: ModifiableObject(parent)
{
}
EntryAttachments::~EntryAttachments()
{
clear();
}
QList<QString> EntryAttachments::keys() const
{
return m_attachments.keys();
@ -82,6 +93,10 @@ void EntryAttachments::remove(const QString& key)
m_attachments.remove(key);
if (m_openedAttachments.contains(key)) {
disconnectAndEraseExternalFile(m_openedAttachments.value(key));
}
emit removed(key);
emitModified();
}
@ -92,20 +107,17 @@ void EntryAttachments::remove(const QStringList& keys)
return;
}
bool emitStatus = modifiedSignalEnabled();
setEmitModified(false);
bool isModified = false;
for (const QString& key : keys) {
if (!m_attachments.contains(key)) {
Q_ASSERT_X(
false, "EntryAttachments::remove", qPrintable(QString("Can't find attachment for key %1").arg(key)));
continue;
}
isModified = true;
emit aboutToBeRemoved(key);
m_attachments.remove(key);
emit removed(key);
isModified |= m_attachments.contains(key);
remove(key);
}
setEmitModified(emitStatus);
if (isModified) {
emitModified();
}
@ -133,15 +145,47 @@ void EntryAttachments::clear()
m_attachments.clear();
const auto externalPath = m_openedAttachments.values();
for (auto& path : externalPath) {
disconnectAndEraseExternalFile(path);
}
emit reset();
emitModified();
}
void EntryAttachments::disconnectAndEraseExternalFile(const QString& path)
{
if (m_openedAttachmentsInverse.contains(path)) {
m_attachmentFileWatchers.value(path)->stop();
m_attachmentFileWatchers.remove(path);
m_openedAttachments.remove(m_openedAttachmentsInverse.value(path));
m_openedAttachmentsInverse.remove(path);
}
QFile f(path);
if (f.open(QFile::ReadWrite)) {
qint64 blocks = f.size() / 128 + 1;
for (qint64 i = 0; i < blocks; ++i) {
f.write(randomGen()->randomArray(128));
}
f.close();
}
f.remove();
}
void EntryAttachments::copyDataFrom(const EntryAttachments* other)
{
if (*this != *other) {
emit aboutToBeReset();
// Reset all externally opened files
const auto externalPath = m_openedAttachments.values();
for (auto& path : externalPath) {
disconnectAndEraseExternalFile(path);
}
m_attachments = other->m_attachments;
emit reset();
@ -167,3 +211,54 @@ int EntryAttachments::attachmentsSize() const
}
return size;
}
bool EntryAttachments::openAttachment(const QString& key, QString* errorMessage)
{
if (!m_openedAttachments.contains(key)) {
const QByteArray attachmentData = value(key);
auto ext = key.contains(".") ? "." + key.split(".").last() : "";
#ifdef KEEPASSXC_DIST_SNAP
const QString tmpFileTemplate =
QString("%1/XXXXXXXXXXXX%2").arg(QProcessEnvironment::systemEnvironment().value("SNAP_USER_DATA"), ext);
#else
const QString tmpFileTemplate = QDir::temp().absoluteFilePath(QString("XXXXXXXXXXXX").append(ext));
#endif
QTemporaryFile tmpFile(tmpFileTemplate);
const bool saveOk = tmpFile.open() && tmpFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner)
&& tmpFile.write(attachmentData) == attachmentData.size() && tmpFile.flush();
if (!saveOk && errorMessage) {
*errorMessage = tr("%1 - %2").arg(key, tmpFile.errorString());
return false;
}
tmpFile.close();
tmpFile.setAutoRemove(false);
m_openedAttachments.insert(key, tmpFile.fileName());
m_openedAttachmentsInverse.insert(tmpFile.fileName(), key);
auto watcher = QSharedPointer<FileWatcher>::create();
watcher->start(tmpFile.fileName(), 5);
connect(watcher.data(), &FileWatcher::fileChanged, this, &EntryAttachments::attachmentFileModified);
m_attachmentFileWatchers.insert(tmpFile.fileName(), watcher);
}
const bool openOk = QDesktopServices::openUrl(QUrl::fromLocalFile(m_openedAttachments.value(key)));
if (!openOk && errorMessage) {
*errorMessage = tr("Cannot open file \"%1\"").arg(key);
return false;
}
return true;
}
void EntryAttachments::attachmentFileModified(const QString& path)
{
auto it = m_openedAttachmentsInverse.find(path);
if (it != m_openedAttachmentsInverse.end()) {
emit valueModifiedExternally(it.value(), path);
}
}

View File

@ -18,10 +18,13 @@
#ifndef KEEPASSX_ENTRYATTACHMENTS_H
#define KEEPASSX_ENTRYATTACHMENTS_H
#include "core/FileWatcher.h"
#include "core/ModifiableObject.h"
#include <QHash>
#include <QMap>
#include <QObject>
#include "core/ModifiableObject.h"
#include <QSharedPointer>
class QStringList;
@ -31,6 +34,7 @@ class EntryAttachments : public ModifiableObject
public:
explicit EntryAttachments(QObject* parent = nullptr);
virtual ~EntryAttachments();
QList<QString> keys() const;
bool hasKey(const QString& key) const;
QSet<QByteArray> values() const;
@ -45,9 +49,11 @@ public:
bool operator==(const EntryAttachments& other) const;
bool operator!=(const EntryAttachments& other) const;
int attachmentsSize() const;
bool openAttachment(const QString& key, QString* errorMessage = nullptr);
signals:
void keyModified(const QString& key);
void valueModifiedExternally(const QString& key, const QString& path);
void aboutToBeAdded(const QString& key);
void added(const QString& key);
void aboutToBeRemoved(const QString& key);
@ -55,8 +61,16 @@ signals:
void aboutToBeReset();
void reset();
private slots:
void attachmentFileModified(const QString& path);
private:
void disconnectAndEraseExternalFile(const QString& path);
QMap<QString, QByteArray> m_attachments;
QHash<QString, QString> m_openedAttachments;
QHash<QString, QString> m_openedAttachmentsInverse;
QHash<QString, QSharedPointer<FileWatcher>> m_attachmentFileWatchers;
};
#endif // KEEPASSX_ENTRYATTACHMENTS_H

View File

@ -329,6 +329,7 @@ void DatabaseWidget::clearAllWidgets()
m_editEntryWidget->clear();
m_historyEditEntryWidget->clear();
m_editGroupWidget->clear();
m_previewView->clear();
}
void DatabaseWidget::emitCurrentModeChanged()

View File

@ -94,6 +94,14 @@ EntryPreviewWidget::~EntryPreviewWidget()
{
}
void EntryPreviewWidget::clear()
{
hide();
m_currentEntry = nullptr;
m_currentGroup = nullptr;
m_ui->entryAttachmentsWidget->unlinkAttachments();
}
void EntryPreviewWidget::setEntry(Entry* selectedEntry)
{
if (!selectedEntry) {
@ -339,7 +347,7 @@ void EntryPreviewWidget::updateEntryAdvancedTab()
m_ui->entryAttributesTable->horizontalHeader()->setStretchLastSection(true);
m_ui->entryAttributesTable->resizeColumnsToContents();
m_ui->entryAttributesTable->resizeRowsToContents();
m_ui->entryAttachmentsWidget->setEntryAttachments(m_currentEntry->attachments());
m_ui->entryAttachmentsWidget->linkAttachments(m_currentEntry->attachments());
}
void EntryPreviewWidget::updateEntryAutotypeTab()

View File

@ -40,6 +40,7 @@ public slots:
void setEntry(Entry* selectedEntry);
void setGroup(Group* selectedGroup);
void setDatabaseMode(DatabaseWidget::Mode mode);
void clear();
signals:
void errorOccurred(const QString& error);

View File

@ -66,6 +66,7 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
, m_sshAgentUi(new Ui::EditEntryWidgetSSHAgent())
, m_historyUi(new Ui::EditEntryWidgetHistory())
, m_browserUi(new Ui::EditEntryWidgetBrowser())
, m_attachments(new EntryAttachments())
, m_customData(new CustomData())
, m_mainWidget(new QScrollArea())
, m_advancedWidget(new QWidget())
@ -537,7 +538,7 @@ void EditEntryWidget::setupSSHAgent()
connect(m_sshAgentUi->decryptButton, &QPushButton::clicked, this, &EditEntryWidget::decryptPrivateKey);
connect(m_sshAgentUi->copyToClipboardButton, &QPushButton::clicked, this, &EditEntryWidget::copyPublicKey);
connect(m_advancedUi->attachmentsWidget->entryAttachments(), &EntryAttachments::modified,
connect(m_attachments.data(), &EntryAttachments::modified,
this, &EditEntryWidget::updateSSHAgentAttachments);
// clang-format on
@ -576,7 +577,7 @@ void EditEntryWidget::updateSSHAgentAttachments()
{
// detect if KeeAgent.settings was removed by hand and reset settings
if (m_entry && KeeAgentSettings::inEntryAttachments(m_entry->attachments())
&& !KeeAgentSettings::inEntryAttachments(m_advancedUi->attachmentsWidget->entryAttachments())) {
&& !KeeAgentSettings::inEntryAttachments(m_attachments.data())) {
m_sshAgentSettings.reset();
setSSHAgentSettings();
}
@ -584,8 +585,7 @@ void EditEntryWidget::updateSSHAgentAttachments()
m_sshAgentUi->attachmentComboBox->clear();
m_sshAgentUi->attachmentComboBox->addItem("");
auto attachments = m_advancedUi->attachmentsWidget->entryAttachments();
for (const QString& fileName : attachments->keys()) {
for (const QString& fileName : m_attachments->keys()) {
if (fileName == "KeeAgent.settings") {
continue;
}
@ -698,7 +698,7 @@ bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key, bool decrypt)
if (!settings.toOpenSSHKey(m_mainUi->usernameComboBox->lineEdit()->text(),
m_mainUi->passwordEdit->text(),
m_db->filePath(),
m_advancedUi->attachmentsWidget->entryAttachments(),
m_attachments.data(),
key,
decrypt)) {
showMessage(settings.errorString(), MessageWidget::Error);
@ -828,6 +828,7 @@ void EditEntryWidget::loadEntry(Entry* entry,
void EditEntryWidget::setForms(Entry* entry, bool restore)
{
m_attachments->copyDataFrom(entry->attachments());
m_customData->copyDataFrom(entry->customData());
m_mainUi->titleEdit->setReadOnly(m_history);
@ -888,7 +889,7 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
m_mainUi->notesEdit->setPlainText(entry->notes());
m_advancedUi->attachmentsWidget->setEntryAttachments(entry->attachments());
m_advancedUi->attachmentsWidget->linkAttachments(m_attachments.data());
m_entryAttributes->copyCustomKeysFrom(entry->attributes());
if (m_attributesModel->rowCount() != 0) {
@ -1090,7 +1091,6 @@ bool EditEntryWidget::commitEntry()
}
m_historyModel->setEntries(m_entry->historyItems());
m_advancedUi->attachmentsWidget->setEntryAttachments(m_entry->attachments());
showMessage(tr("Entry updated successfully."), MessageWidget::Positive);
setModified(false);
@ -1110,7 +1110,7 @@ void EditEntryWidget::updateEntryData(Entry* entry) const
QRegularExpression newLineRegex("(?:\r?\n|\r)");
entry->attributes()->copyCustomKeysFrom(m_entryAttributes);
entry->attachments()->copyDataFrom(m_advancedUi->attachmentsWidget->entryAttachments());
entry->attachments()->copyDataFrom(m_attachments.data());
entry->customData()->copyDataFrom(m_customData.data());
entry->setTitle(m_mainUi->titleEdit->text().replace(newLineRegex, " "));
entry->setUsername(m_mainUi->usernameComboBox->lineEdit()->text().replace(newLineRegex, " "));
@ -1212,7 +1212,8 @@ void EditEntryWidget::clear()
m_mainUi->notesEdit->clear();
m_entryAttributes->clear();
m_advancedUi->attachmentsWidget->clearAttachments();
m_attachments->clear();
m_customData->clear();
m_autoTypeAssoc->clear();
m_historyModel->clear();
m_iconsWidget->reset();

View File

@ -35,6 +35,7 @@ class EditWidgetIcons;
class EditWidgetProperties;
class Entry;
class EntryAttributes;
class EntryAttachments;
class EntryAttributesModel;
class EntryHistoryModel;
class QButtonGroup;
@ -171,6 +172,7 @@ private:
const QScopedPointer<Ui::EditEntryWidgetSSHAgent> m_sshAgentUi;
const QScopedPointer<Ui::EditEntryWidgetHistory> m_historyUi;
const QScopedPointer<Ui::EditEntryWidgetBrowser> m_browserUi;
const QScopedPointer<EntryAttachments> m_attachments;
const QScopedPointer<CustomData> m_customData;
QScrollArea* const m_mainWidget;

View File

@ -19,6 +19,7 @@
#define KEEPASSX_ENTRYATTACHMENTSMODEL_H
#include <QAbstractListModel>
#include <QPointer>
class EntryAttachments;
@ -55,7 +56,7 @@ private slots:
void setReadOnly(bool readOnly);
private:
EntryAttachments* m_entryAttachments;
QPointer<EntryAttachments> m_entryAttachments;
QStringList m_headers;
bool m_readOnly = false;
};

View File

@ -1,14 +1,30 @@
/*
* 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 <QDesktopServices>
#include <QDir>
#include <QDropEvent>
#include <QMimeData>
#include <QStandardPaths>
#include <QTemporaryFile>
#include "EntryAttachmentsModel.h"
#include "config-keepassx.h"
#include "core/Config.h"
#include "core/EntryAttachments.h"
#include "core/Tools.h"
@ -18,7 +34,7 @@
EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::EntryAttachmentsWidget)
, m_entryAttachments(new EntryAttachments(this))
, m_entryAttachments(nullptr)
, m_attachmentsModel(new EntryAttachmentsModel(this))
, m_readOnly(false)
, m_buttonsVisible(true)
@ -29,7 +45,6 @@ EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
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);
@ -63,7 +78,7 @@ EntryAttachmentsWidget::~EntryAttachmentsWidget()
{
}
const EntryAttachments* EntryAttachmentsWidget::entryAttachments() const
const EntryAttachments* EntryAttachmentsWidget::attachments() const
{
return m_entryAttachments;
}
@ -78,15 +93,29 @@ bool EntryAttachmentsWidget::isButtonsVisible() const
return m_buttonsVisible;
}
void EntryAttachmentsWidget::setEntryAttachments(const EntryAttachments* attachments)
void EntryAttachmentsWidget::linkAttachments(EntryAttachments* attachments)
{
Q_ASSERT(attachments != nullptr);
m_entryAttachments->copyDataFrom(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::clearAttachments()
void EntryAttachmentsWidget::unlinkAttachments()
{
m_entryAttachments->clear();
if (m_entryAttachments) {
m_entryAttachments->disconnect(this);
m_entryAttachments = nullptr;
m_attachmentsModel->setEntryAttachments(nullptr);
}
}
void EntryAttachmentsWidget::setReadOnly(bool readOnly)
@ -109,25 +138,9 @@ void EntryAttachmentsWidget::setButtonsVisible(bool 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(m_entryAttachments);
Q_ASSERT(!isReadOnly());
if (isReadOnly()) {
return;
@ -153,6 +166,7 @@ void EntryAttachmentsWidget::insertAttachments()
void EntryAttachmentsWidget::removeSelectedAttachments()
{
Q_ASSERT(m_entryAttachments);
Q_ASSERT(!isReadOnly());
if (isReadOnly()) {
return;
@ -181,11 +195,14 @@ void EntryAttachmentsWidget::removeSelectedAttachments()
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;
@ -253,7 +270,7 @@ void EntryAttachmentsWidget::openAttachment(const QModelIndex& index)
}
QString errorMessage;
if (!openAttachment(index, errorMessage)) {
if (!m_entryAttachments->openAttachment(m_attachmentsModel->keyByIndex(index), &errorMessage)) {
errorOccurred(tr("Unable to open attachment:\n%1").arg(errorMessage));
}
}
@ -268,7 +285,7 @@ void EntryAttachmentsWidget::openSelectedAttachments()
QStringList errors;
for (const QModelIndex& index : indexes) {
QString errorMessage;
if (!openAttachment(index, 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));
};
@ -324,39 +341,6 @@ bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QSt
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));
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;
}
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 "
@ -421,3 +405,34 @@ bool EntryAttachmentsWidget::eventFilter(QObject* watched, QEvent* e)
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);
}

View File

@ -1,3 +1,20 @@
/*
* 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/>.
*/
#ifndef ENTRYATTACHMENTSWIDGET_H
#define ENTRYATTACHMENTSWIDGET_H
@ -22,17 +39,13 @@ public:
explicit EntryAttachmentsWidget(QWidget* parent = nullptr);
~EntryAttachmentsWidget();
const EntryAttachments* entryAttachments() const;
const EntryAttachments* attachments() const;
bool isReadOnly() const;
bool isButtonsVisible() const;
QByteArray getAttachment(const QString& name);
void setAttachment(const QString& name, const QByteArray& value);
void removeAttachment(const QString& name);
public slots:
void setEntryAttachments(const EntryAttachments* attachments);
void clearAttachments();
void linkAttachments(EntryAttachments* attachments);
void unlinkAttachments();
void setReadOnly(bool readOnly);
void setButtonsVisible(bool isButtonsVisible);
@ -51,10 +64,10 @@ private slots:
void openSelectedAttachments();
void updateButtonsVisible();
void updateButtonsEnabled();
void attachmentModifiedExternally(const QString& key, const QString& filePath);
private:
bool insertAttachments(const QStringList& fileNames, QString& errorMessage);
bool openAttachment(const QModelIndex& index, QString& errorMessage);
QStringList confirmLargeAttachments(const QStringList& filenames);
@ -63,6 +76,7 @@ private:
QScopedPointer<Ui::EntryAttachmentsWidget> m_ui;
QPointer<EntryAttachments> m_entryAttachments;
QPointer<EntryAttachmentsModel> m_attachmentsModel;
QStringList m_pendingChanges;
bool m_readOnly;
bool m_buttonsVisible;
};