diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 9eb25c2780..6b273a8fe7 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -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 @@ -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 keyList = EntryAttributes::DefaultAttributes; @@ -320,6 +361,26 @@ bool Entry::hasReferences() const return false; } +bool Entry::hasReferencesTo(const QUuid& uuid) const +{ + const QList 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); diff --git a/src/core/Entry.h b/src/core/Entry.h index ae11abe4d9..ca40366d91 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -105,12 +105,16 @@ class Entry : public QObject QString username() const; QString password() const; QString notes() const; + QString attribute(const QString& key) const; QString totp() const; QSharedPointer 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 @@ class Entry : public QObject 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 settings); @@ -237,6 +242,7 @@ private slots: 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 bool set(T& property, const T& value); diff --git a/src/core/Group.cpp b/src/core/Group.cpp index ec7633b882..6c4adbad7f 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -23,6 +23,9 @@ #include "core/DatabaseIcons.h" #include "core/Global.h" #include "core/Metadata.h" +#include "core/Tools.h" + +#include 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 Group::entriesRecursive(bool includeHistoryItems) const return entryList; } +QList 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()) { diff --git a/src/core/Group.h b/src/core/Group.h index e3ef9a596b..d0074d5d3a 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -151,6 +151,7 @@ class Group : public QObject QList entries(); const QList& entries() const; Entry* findEntryRecursive(const QString& text, EntryReferenceType referenceType, Group* group = nullptr); + QList referencesRecursive(const Entry* entry) const; QList entriesRecursive(bool includeHistoryItems = false) const; QList groupsRecursive(bool includeSelf) const; QList groupsRecursive(bool includeSelf); diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 673e6a6048..67a8c5f42e 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #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 + "$"; - } + // Exact modifier + if (exactMatch) { + pattern = "^" + pattern + "$"; + } - auto regex = QRegularExpression(pattern); - if (!caseSensitive) { - regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); - } + auto regex = QRegularExpression(pattern); + if (!caseSensitive) { + regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } - return regex; -} + return regex; + } + QString uuidToHex(const QUuid& uuid) { + return QString::fromLatin1(uuid.toRfc4122().toHex()); + } } // namespace Tools diff --git a/src/core/Tools.h b/src/core/Tools.h index 984bab491d..1e8f89a016 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -23,6 +23,7 @@ #include #include +#include #include @@ -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 diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index fb2d1a9a8d..451629a083 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -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 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 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; } } diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 1c190558c8..9feac11848 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -149,7 +149,7 @@ public slots: void replaceDatabase(QSharedPointer db); void createEntry(); void cloneEntry(); - void deleteEntries(); + void deleteSelectedEntries(); void setFocus(); void copyTitle(); void copyUsername(); @@ -225,6 +225,7 @@ private slots: void setClipboardTextAndMinimize(const QString& text); void setIconFromParent(); void processAutoOpen(); + bool confirmDeleteEntries(QList entries, bool permanent); QSharedPointer m_db; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 55c261858e..2631964472 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -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()));