mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Warn user if deleting entries that are referenced. (#1744)
On warning, references can be replaced with original values or ignored. Removal process can be also skipped for each conflicting entry. Resolves #852.
This commit is contained in:
parent
4d4c839afa
commit
c630214915
@ -24,6 +24,7 @@
|
||||
#include "core/DatabaseIcons.h"
|
||||
#include "core/Group.h"
|
||||
#include "core/Metadata.h"
|
||||
#include "core/Tools.h"
|
||||
#include "totp/totp.h"
|
||||
|
||||
#include <QDir>
|
||||
@ -100,6 +101,32 @@ void Entry::setUpdateTimeinfo(bool value)
|
||||
m_updateTimeinfo = value;
|
||||
}
|
||||
|
||||
QString Entry::buildReference(const QUuid& uuid, const QString& field)
|
||||
{
|
||||
Q_ASSERT(EntryAttributes::DefaultAttributes.count(field) > 0);
|
||||
|
||||
QString uuidStr = Tools::uuidToHex(uuid).toUpper();
|
||||
QString shortField;
|
||||
|
||||
if (field == EntryAttributes::TitleKey) {
|
||||
shortField = "T";
|
||||
} else if (field == EntryAttributes::UserNameKey) {
|
||||
shortField = "U";
|
||||
} else if (field == EntryAttributes::PasswordKey) {
|
||||
shortField = "P";
|
||||
} else if (field == EntryAttributes::URLKey) {
|
||||
shortField = "A";
|
||||
} else if (field == EntryAttributes::NotesKey) {
|
||||
shortField = "N";
|
||||
}
|
||||
|
||||
if (shortField.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return QString("{REF:%1@I:%2}").arg(shortField, uuidStr);
|
||||
}
|
||||
|
||||
EntryReferenceType Entry::referenceType(const QString& referenceStr)
|
||||
{
|
||||
const QString referenceLowerStr = referenceStr.toLower();
|
||||
@ -130,7 +157,7 @@ const QUuid& Entry::uuid() const
|
||||
|
||||
const QString Entry::uuidToHex() const
|
||||
{
|
||||
return QString::fromLatin1(m_uuid.toRfc4122().toHex());
|
||||
return Tools::uuidToHex(m_uuid);
|
||||
}
|
||||
|
||||
QImage Entry::icon() const
|
||||
@ -304,11 +331,25 @@ QString Entry::notes() const
|
||||
return m_attributes->value(EntryAttributes::NotesKey);
|
||||
}
|
||||
|
||||
QString Entry::attribute(const QString& key) const
|
||||
{
|
||||
return m_attributes->value(key);
|
||||
}
|
||||
|
||||
bool Entry::isExpired() const
|
||||
{
|
||||
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTimeUtc();
|
||||
}
|
||||
|
||||
bool Entry::isAttributeReferenceOf(const QString& key, const QUuid& uuid) const
|
||||
{
|
||||
if (!m_attributes->isReference(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_attributes->value(key).contains(Tools::uuidToHex(uuid), Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool Entry::hasReferences() const
|
||||
{
|
||||
const QList<QString> keyList = EntryAttributes::DefaultAttributes;
|
||||
@ -320,6 +361,26 @@ bool Entry::hasReferences() const
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Entry::hasReferencesTo(const QUuid& uuid) const
|
||||
{
|
||||
const QList<QString> keyList = EntryAttributes::DefaultAttributes;
|
||||
for (const QString& key : keyList) {
|
||||
if (isAttributeReferenceOf(key, uuid)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Entry::replaceReferencesWithValues(const Entry* other)
|
||||
{
|
||||
for (const QString& key : EntryAttributes::DefaultAttributes) {
|
||||
if (isAttributeReferenceOf(key, other->uuid())) {
|
||||
setDefaultAttribute(key, other->attribute(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EntryAttributes* Entry::attributes()
|
||||
{
|
||||
return m_attributes;
|
||||
@ -496,6 +557,17 @@ void Entry::setNotes(const QString& notes)
|
||||
m_attributes->set(EntryAttributes::NotesKey, notes, m_attributes->isProtected(EntryAttributes::NotesKey));
|
||||
}
|
||||
|
||||
void Entry::setDefaultAttribute(const QString& attribute, const QString& value)
|
||||
{
|
||||
Q_ASSERT(EntryAttributes::isDefaultAttribute(attribute));
|
||||
|
||||
if (!EntryAttributes::isDefaultAttribute(attribute)) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_attributes->set(attribute, value, m_attributes->isProtected(attribute));
|
||||
}
|
||||
|
||||
void Entry::setExpires(const bool& value)
|
||||
{
|
||||
if (m_data.timeInfo.expires() != value) {
|
||||
@ -654,16 +726,17 @@ Entry* Entry::clone(CloneFlags flags) const
|
||||
entry->m_attachments->copyDataFrom(m_attachments);
|
||||
|
||||
if (flags & CloneUserAsRef) {
|
||||
// Build the username reference
|
||||
QString username = "{REF:U@I:" + uuidToHex() + "}";
|
||||
entry->m_attributes->set(
|
||||
EntryAttributes::UserNameKey, username.toUpper(), m_attributes->isProtected(EntryAttributes::UserNameKey));
|
||||
EntryAttributes::UserNameKey,
|
||||
buildReference(uuid(), EntryAttributes::UserNameKey),
|
||||
m_attributes->isProtected(EntryAttributes::UserNameKey));
|
||||
}
|
||||
|
||||
if (flags & ClonePassAsRef) {
|
||||
QString password = "{REF:P@I:" + uuidToHex() + "}";
|
||||
entry->m_attributes->set(
|
||||
EntryAttributes::PasswordKey, password.toUpper(), m_attributes->isProtected(EntryAttributes::PasswordKey));
|
||||
EntryAttributes::PasswordKey,
|
||||
buildReference(uuid(), EntryAttributes::PasswordKey),
|
||||
m_attributes->isProtected(EntryAttributes::PasswordKey));
|
||||
}
|
||||
|
||||
entry->m_autoTypeAssociations->copyDataFrom(m_autoTypeAssociations);
|
||||
|
@ -105,12 +105,16 @@ public:
|
||||
QString username() const;
|
||||
QString password() const;
|
||||
QString notes() const;
|
||||
QString attribute(const QString& key) const;
|
||||
QString totp() const;
|
||||
QSharedPointer<Totp::Settings> totpSettings() const;
|
||||
|
||||
bool hasTotp() const;
|
||||
bool isExpired() const;
|
||||
bool isAttributeReferenceOf(const QString& key, const QUuid& uuid) const;
|
||||
void replaceReferencesWithValues(const Entry* other);
|
||||
bool hasReferences() const;
|
||||
bool hasReferencesTo(const QUuid& uuid) const;
|
||||
EntryAttributes* attributes();
|
||||
const EntryAttributes* attributes() const;
|
||||
EntryAttachments* attachments();
|
||||
@ -139,6 +143,7 @@ public:
|
||||
void setUsername(const QString& username);
|
||||
void setPassword(const QString& password);
|
||||
void setNotes(const QString& notes);
|
||||
void setDefaultAttribute(const QString& attribute, const QString& value);
|
||||
void setExpires(const bool& value);
|
||||
void setExpiryTime(const QDateTime& dateTime);
|
||||
void setTotp(QSharedPointer<Totp::Settings> settings);
|
||||
@ -237,6 +242,7 @@ private:
|
||||
QString resolveReferencePlaceholderRecursive(const QString& placeholder, int maxDepth) const;
|
||||
QString referenceFieldValue(EntryReferenceType referenceType) const;
|
||||
|
||||
static QString buildReference(const QUuid& uuid, const QString& field);
|
||||
static EntryReferenceType referenceType(const QString& referenceStr);
|
||||
|
||||
template <class T> bool set(T& property, const T& value);
|
||||
|
@ -23,6 +23,9 @@
|
||||
#include "core/DatabaseIcons.h"
|
||||
#include "core/Global.h"
|
||||
#include "core/Metadata.h"
|
||||
#include "core/Tools.h"
|
||||
|
||||
#include <QtConcurrent>
|
||||
|
||||
const int Group::DefaultIconNumber = 48;
|
||||
const int Group::RecycleBinIconNumber = 43;
|
||||
@ -119,7 +122,7 @@ const QUuid& Group::uuid() const
|
||||
|
||||
const QString Group::uuidToHex() const
|
||||
{
|
||||
return QString::fromLatin1(m_uuid.toRfc4122().toHex());
|
||||
return Tools::uuidToHex(m_uuid);
|
||||
}
|
||||
|
||||
QString Group::name() const
|
||||
@ -548,6 +551,12 @@ QList<Entry*> Group::entriesRecursive(bool includeHistoryItems) const
|
||||
return entryList;
|
||||
}
|
||||
|
||||
QList<Entry*> Group::referencesRecursive(const Entry* entry) const
|
||||
{
|
||||
auto entries = entriesRecursive();
|
||||
return QtConcurrent::blockingFiltered(entries, [entry](const Entry* e) { return e->hasReferencesTo(entry->uuid()); });
|
||||
}
|
||||
|
||||
Entry* Group::findEntryByUuid(const QUuid& uuid) const
|
||||
{
|
||||
if (uuid.isNull()) {
|
||||
|
@ -151,6 +151,7 @@ public:
|
||||
QList<Entry*> entries();
|
||||
const QList<Entry*>& entries() const;
|
||||
Entry* findEntryRecursive(const QString& text, EntryReferenceType referenceType, Group* group = nullptr);
|
||||
QList<Entry*> referencesRecursive(const Entry* entry) const;
|
||||
QList<Entry*> entriesRecursive(bool includeHistoryItems = false) const;
|
||||
QList<const Group*> groupsRecursive(bool includeSelf) const;
|
||||
QList<Group*> groupsRecursive(bool includeSelf);
|
||||
|
@ -28,6 +28,7 @@
|
||||
#include <QLocale>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringList>
|
||||
#include <QUuid>
|
||||
#include <cctype>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
@ -197,31 +198,34 @@ namespace Tools
|
||||
}
|
||||
}
|
||||
|
||||
// Escape common regex symbols except for *, ?, and |
|
||||
auto regexEscape = QRegularExpression(R"re(([-[\]{}()+.,\\\/^$#]))re");
|
||||
// Escape common regex symbols except for *, ?, and |
|
||||
auto regexEscape = QRegularExpression(R"re(([-[\]{}()+.,\\\/^$#]))re");
|
||||
|
||||
QRegularExpression convertToRegex(const QString& string, bool useWildcards, bool exactMatch, bool caseSensitive)
|
||||
{
|
||||
QString pattern = string;
|
||||
QRegularExpression convertToRegex(const QString& string, bool useWildcards, bool exactMatch, bool caseSensitive)
|
||||
{
|
||||
QString pattern = string;
|
||||
|
||||
// Wildcard support (*, ?, |)
|
||||
if (useWildcards) {
|
||||
pattern.replace(regexEscape, "\\\\1");
|
||||
pattern.replace("*", ".*");
|
||||
pattern.replace("?", ".");
|
||||
// Wildcard support (*, ?, |)
|
||||
if (useWildcards) {
|
||||
pattern.replace(regexEscape, "\\\\1");
|
||||
pattern.replace("*", ".*");
|
||||
pattern.replace("?", ".");
|
||||
}
|
||||
|
||||
// Exact modifier
|
||||
if (exactMatch) {
|
||||
pattern = "^" + pattern + "$";
|
||||
}
|
||||
|
||||
auto regex = QRegularExpression(pattern);
|
||||
if (!caseSensitive) {
|
||||
regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
|
||||
return regex;
|
||||
}
|
||||
|
||||
// Exact modifier
|
||||
if (exactMatch) {
|
||||
pattern = "^" + pattern + "$";
|
||||
QString uuidToHex(const QUuid& uuid) {
|
||||
return QString::fromLatin1(uuid.toRfc4122().toHex());
|
||||
}
|
||||
|
||||
auto regex = QRegularExpression(pattern);
|
||||
if (!caseSensitive) {
|
||||
regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
|
||||
return regex;
|
||||
}
|
||||
|
||||
} // namespace Tools
|
||||
|
@ -23,6 +23,7 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUuid>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@ -39,7 +40,8 @@ namespace Tools
|
||||
bool isBase64(const QByteArray& ba);
|
||||
void sleep(int ms);
|
||||
void wait(int ms);
|
||||
QRegularExpression convertToRegex(const QString& string, bool useWildcards = false,
|
||||
QString uuidToHex(const QUuid& uuid);
|
||||
QRegularExpression convertToRegex(const QString& string, bool useWildcards = false,
|
||||
bool exactMatch = false, bool caseSensitive = false);
|
||||
|
||||
template <typename RandomAccessIterator, typename T>
|
||||
|
@ -426,68 +426,116 @@ void DatabaseWidget::setupTotp()
|
||||
setupTotpDialog->open();
|
||||
}
|
||||
|
||||
void DatabaseWidget::deleteEntries()
|
||||
void DatabaseWidget::deleteSelectedEntries()
|
||||
{
|
||||
const QModelIndexList selected = m_entryView->selectionModel()->selectedRows();
|
||||
|
||||
Q_ASSERT(!selected.isEmpty());
|
||||
if (selected.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get all entry pointers as the indexes change when removing multiple entries
|
||||
// Resolve entries from the selection model
|
||||
QList<Entry*> selectedEntries;
|
||||
for (const QModelIndex& index : selected) {
|
||||
selectedEntries.append(m_entryView->entryFromIndex(index));
|
||||
}
|
||||
|
||||
// Confirm entry removal before moving forward
|
||||
auto* recycleBin = m_db->metadata()->recycleBin();
|
||||
bool inRecycleBin = recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid());
|
||||
if (inRecycleBin || !m_db->metadata()->recycleBinEnabled()) {
|
||||
bool permanent = (recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid()))
|
||||
|| !m_db->metadata()->recycleBinEnabled();
|
||||
|
||||
if (!confirmDeleteEntries(selectedEntries, permanent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find references to selected entries and prompt for direction if necessary
|
||||
auto it = selectedEntries.begin();
|
||||
while (it != selectedEntries.end()) {
|
||||
auto references = m_db->rootGroup()->referencesRecursive(*it);
|
||||
if (!references.isEmpty()) {
|
||||
// Ignore references that are selected for deletion
|
||||
for (auto* entry : selectedEntries) {
|
||||
references.removeAll(entry);
|
||||
}
|
||||
|
||||
if (!references.isEmpty()) {
|
||||
// Prompt for reference handling
|
||||
auto result = MessageBox::question(
|
||||
this,
|
||||
tr("Replace references to entry?"),
|
||||
tr("Entry \"%1\" has %2 reference(s). "
|
||||
"Do you want to overwrite references with values, skip this entry, or delete anyway?", "",
|
||||
references.size())
|
||||
.arg((*it)->title().toHtmlEscaped())
|
||||
.arg(references.size()),
|
||||
MessageBox::Overwrite | MessageBox::Skip | MessageBox::Delete,
|
||||
MessageBox::Overwrite);
|
||||
|
||||
if (result == MessageBox::Overwrite) {
|
||||
for (auto* entry : references) {
|
||||
entry->replaceReferencesWithValues(*it);
|
||||
}
|
||||
} else if (result == MessageBox::Skip) {
|
||||
it = selectedEntries.erase(it);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it++;
|
||||
}
|
||||
|
||||
if (permanent) {
|
||||
for (auto* entry : asConst(selectedEntries)) {
|
||||
delete entry;
|
||||
}
|
||||
} else {
|
||||
for (auto* entry : asConst(selectedEntries)) {
|
||||
m_db->recycleEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
refreshSearch();
|
||||
}
|
||||
|
||||
bool DatabaseWidget::confirmDeleteEntries(QList<Entry*> entries, bool permanent)
|
||||
{
|
||||
if (entries.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (permanent) {
|
||||
QString prompt;
|
||||
refreshSearch();
|
||||
if (selected.size() == 1) {
|
||||
if (entries.size() == 1) {
|
||||
prompt = tr("Do you really want to delete the entry \"%1\" for good?")
|
||||
.arg(selectedEntries.first()->title().toHtmlEscaped());
|
||||
.arg(entries.first()->title().toHtmlEscaped());
|
||||
} else {
|
||||
prompt = tr("Do you really want to delete %n entry(s) for good?", "", selected.size());
|
||||
prompt = tr("Do you really want to delete %n entry(s) for good?", "", entries.size());
|
||||
}
|
||||
|
||||
auto answer = MessageBox::question(this,
|
||||
tr("Delete entry(s)?", "", selected.size()),
|
||||
tr("Delete entry(s)?", "", entries.size()),
|
||||
prompt,
|
||||
MessageBox::Delete | MessageBox::Cancel,
|
||||
MessageBox::Cancel);
|
||||
|
||||
if (answer == MessageBox::Delete) {
|
||||
for (Entry* entry : asConst(selectedEntries)) {
|
||||
delete entry;
|
||||
refreshSearch();
|
||||
}
|
||||
refreshSearch();
|
||||
}
|
||||
return answer == MessageBox::Delete;
|
||||
} else {
|
||||
QString prompt;
|
||||
if (selected.size() == 1) {
|
||||
if (entries.size() == 1) {
|
||||
prompt = tr("Do you really want to move entry \"%1\" to the recycle bin?")
|
||||
.arg(selectedEntries.first()->title().toHtmlEscaped());
|
||||
.arg(entries.first()->title().toHtmlEscaped());
|
||||
} else {
|
||||
prompt = tr("Do you really want to move %n entry(s) to the recycle bin?", "", selected.size());
|
||||
prompt = tr("Do you really want to move %n entry(s) to the recycle bin?", "", entries.size());
|
||||
}
|
||||
|
||||
auto answer = MessageBox::question(this,
|
||||
tr("Move entry(s) to recycle bin?", "", selected.size()),
|
||||
tr("Move entry(s) to recycle bin?", "", entries.size()),
|
||||
prompt,
|
||||
MessageBox::Move | MessageBox::Cancel,
|
||||
MessageBox::Cancel);
|
||||
|
||||
if (answer == MessageBox::Cancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Entry* entry : asConst(selectedEntries)) {
|
||||
m_db->recycleEntry(entry);
|
||||
}
|
||||
return answer == MessageBox::Move;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,7 +149,7 @@ public slots:
|
||||
void replaceDatabase(QSharedPointer<Database> db);
|
||||
void createEntry();
|
||||
void cloneEntry();
|
||||
void deleteEntries();
|
||||
void deleteSelectedEntries();
|
||||
void setFocus();
|
||||
void copyTitle();
|
||||
void copyUsername();
|
||||
@ -225,6 +225,7 @@ private:
|
||||
void setClipboardTextAndMinimize(const QString& text);
|
||||
void setIconFromParent();
|
||||
void processAutoOpen();
|
||||
bool confirmDeleteEntries(QList<Entry*> entries, bool permanent);
|
||||
|
||||
QSharedPointer<Database> m_db;
|
||||
|
||||
|
@ -310,7 +310,7 @@ MainWindow::MainWindow()
|
||||
m_actionMultiplexer.connect(m_ui->actionEntryNew, SIGNAL(triggered()), SLOT(createEntry()));
|
||||
m_actionMultiplexer.connect(m_ui->actionEntryClone, SIGNAL(triggered()), SLOT(cloneEntry()));
|
||||
m_actionMultiplexer.connect(m_ui->actionEntryEdit, SIGNAL(triggered()), SLOT(switchToEntryEdit()));
|
||||
m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), SLOT(deleteEntries()));
|
||||
m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), SLOT(deleteSelectedEntries()));
|
||||
|
||||
m_actionMultiplexer.connect(m_ui->actionEntryTotp, SIGNAL(triggered()), SLOT(showTotp()));
|
||||
m_actionMultiplexer.connect(m_ui->actionEntrySetupTotp, SIGNAL(triggered()), SLOT(setupTotp()));
|
||||
|
Loading…
Reference in New Issue
Block a user