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:
Wojtek Gumuła 2018-12-25 00:15:46 +01:00 committed by Jonathan White
parent 4d4c839afa
commit c630214915
9 changed files with 205 additions and 61 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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()) {

View File

@ -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);

View File

@ -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

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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()));