mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
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:
parent
af9eb6d6b1
commit
93f0fef1e1
BIN
share/demo.kdbx
BIN
share/demo.kdbx
Binary file not shown.
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -329,6 +329,7 @@ void DatabaseWidget::clearAllWidgets()
|
||||
m_editEntryWidget->clear();
|
||||
m_historyEditEntryWidget->clear();
|
||||
m_editGroupWidget->clear();
|
||||
m_previewView->clear();
|
||||
}
|
||||
|
||||
void DatabaseWidget::emitCurrentModeChanged()
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user