Warn user if deleting entries that are referenced. ()

On warning, references can be replaced with original values or ignored.
Removal process can be also skipped for each conflicting entry. Resolves .
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

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