diff --git a/src/core/Database.cpp b/src/core/Database.cpp index c44df7ffd1..4db0af97e2 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -18,19 +18,8 @@ #include "Database.h" -#include -#include -#include -#include -#include -#include -#include -#include - #include "cli/Utils.h" -#include "core/Clock.h" #include "core/Group.h" -#include "core/Merger.h" #include "core/Metadata.h" #include "crypto/kdf/AesKdf.h" #include "format/KeePass2.h" @@ -38,6 +27,15 @@ #include "format/KeePass2Writer.h" #include "keys/FileKey.h" #include "keys/PasswordKey.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include QHash Database::m_uuidMap; @@ -215,6 +213,33 @@ Group* Database::findGroupRecursive(const QUuid& uuid, Group* group) return nullptr; } +QList Database::resolveReferences(const QUuid& uuid) const +{ + return resolveReferences(uuid, m_rootGroup); +} + +QList Database::resolveReferences(const QUuid& uuid, const Group* group) const +{ + auto isReference = [&uuid](const Entry* e) { return e->hasReferencesTo(uuid); }; + + QList result = QtConcurrent::blockingFiltered(group->entries(), isReference); + + for (Group* child : group->children()) { + result += resolveReferences(uuid, child); + } + return result; +} + +void Database::replaceReferencesWithValues(Entry* entry, QList references) +{ + for (Entry* reference : references) { + for (const QString& key : EntryAttributes::DefaultAttributes) { + if (reference->isAttributeReferenceOf(key, entry->uuid())) + reference->setDefaultAttribute(key, entry->attribute(key)); + } + } +} + QList Database::deletedObjects() { return m_deletedObjects; diff --git a/src/core/Database.h b/src/core/Database.h index 692444f4de..c953ab5761 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -91,6 +91,9 @@ class Database : public QObject Entry* resolveEntry(const QUuid& uuid); Entry* resolveEntry(const QString& text, EntryReferenceType referenceType); Group* resolveGroup(const QUuid& uuid); + QList resolveReferences(const QUuid& uuid) const; + QList resolveReferences(const QUuid& uuid, const Group* group) const; + void replaceReferencesWithValues(Entry* entry, QList references); QList deletedObjects(); const QList& deletedObjects() const; void addDeletedObject(const DeletedObject& delObj); @@ -110,7 +113,9 @@ class Database : public QObject void setCipher(const QUuid& cipher); void setCompressionAlgo(Database::CompressionAlgorithm algo); void setKdf(QSharedPointer kdf); - bool setKey(const QSharedPointer& key, bool updateChangedTime = true, bool updateTransformSalt = false); + bool setKey(const QSharedPointer& key, + bool updateChangedTime = true, + bool updateTransformSalt = false); bool hasKey() const; bool verifyKey(const QSharedPointer& key) const; QVariantMap& publicCustomData(); @@ -131,8 +136,10 @@ class Database : public QObject static Database* databaseByUuid(const QUuid& uuid); static Database* openDatabaseFile(const QString& fileName, QSharedPointer key); - static Database* unlockFromStdin(const QString& databaseFilename, const QString& keyFilename = {}, - FILE* outputDescriptor = stdout, FILE* errorDescriptor = stderr); + static Database* unlockFromStdin(const QString& databaseFilename, + const QString& keyFilename = {}, + FILE* outputDescriptor = stdout, + FILE* errorDescriptor = stderr); signals: void groupDataChanged(Group* group); diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index f6790da4b5..2d65112236 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 @@ -306,11 +307,26 @@ 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; + } + + const QString ref = Tools::reference(uuid, key).toUpper(); + return m_attributes->value(key) == ref; +} + bool Entry::hasReferences() const { const QList keyList = EntryAttributes::DefaultAttributes; @@ -322,6 +338,17 @@ 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; +} + EntryAttributes* Entry::attributes() { return m_attributes; @@ -498,6 +525,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) { @@ -658,12 +696,14 @@ Entry* Entry::clone(CloneFlags flags) const 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)); + entry->m_attributes->set( + EntryAttributes::UserNameKey, username.toUpper(), 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)); + entry->m_attributes->set( + EntryAttributes::PasswordKey, password.toUpper(), m_attributes->isProtected(EntryAttributes::PasswordKey)); } entry->m_autoTypeAssociations->copyDataFrom(m_autoTypeAssociations); @@ -1066,7 +1106,8 @@ QString Entry::resolveUrl(const QString& url) const // Validate the URL QUrl tempUrl = QUrl(newUrl); - if (tempUrl.isValid() && (tempUrl.scheme() == "http" || tempUrl.scheme() == "https" || tempUrl.scheme() == "file")) { + if (tempUrl.isValid() + && (tempUrl.scheme() == "http" || tempUrl.scheme() == "https" || tempUrl.scheme() == "file")) { return tempUrl.url(); } diff --git a/src/core/Entry.h b/src/core/Entry.h index 05ed30bc0f..ed6c7b5d9e 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -104,12 +104,15 @@ 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; bool hasReferences() const; + bool hasReferencesTo(const QUuid& uuid) const; EntryAttributes* attributes(); const EntryAttributes* attributes() const; EntryAttachments* attachments(); @@ -138,6 +141,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); diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index ded3a16518..a0d9b999bf 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -19,14 +19,16 @@ #include "Tools.h" #include "core/Config.h" +#include "core/EntryAttributes.h" #include "core/Translator.h" #include +#include #include #include #include #include -#include +#include #include @@ -57,146 +59,188 @@ namespace Tools { -QString humanReadableFileSize(qint64 bytes, quint32 precision) -{ - constexpr auto kibibyte = 1024; - double size = bytes; - - QStringList units = QStringList() << "B" - << "KiB" - << "MiB" - << "GiB"; - int i = 0; - int maxI = units.size() - 1; - - while ((size >= kibibyte) && (i < maxI)) { - size /= kibibyte; - i++; - } - - return QString("%1 %2").arg(QLocale().toString(size, 'f', precision), units.at(i)); -} - -bool readFromDevice(QIODevice* device, QByteArray& data, int size) -{ - QByteArray buffer; - buffer.resize(size); + QString humanReadableFileSize(qint64 bytes, quint32 precision) + { + constexpr auto kibibyte = 1024; + double size = bytes; + + QStringList units = QStringList() << "B" + << "KiB" + << "MiB" + << "GiB"; + int i = 0; + int maxI = units.size() - 1; + + while ((size >= kibibyte) && (i < maxI)) { + size /= kibibyte; + i++; + } - qint64 readResult = device->read(buffer.data(), size); - if (readResult == -1) { - return false; - } else { - buffer.resize(readResult); - data = buffer; - return true; + return QString("%1 %2").arg(QLocale().toString(size, 'f', precision), units.at(i)); } -} -bool readAllFromDevice(QIODevice* device, QByteArray& data) -{ - QByteArray result; - qint64 readBytes = 0; - qint64 readResult; - do { - result.resize(result.size() + 16384); - readResult = device->read(result.data() + readBytes, result.size() - readBytes); - if (readResult > 0) { - readBytes += readResult; + bool readFromDevice(QIODevice* device, QByteArray& data, int size) + { + QByteArray buffer; + buffer.resize(size); + + qint64 readResult = device->read(buffer.data(), size); + if (readResult == -1) { + return false; + } else { + buffer.resize(readResult); + data = buffer; + return true; } } - while (readResult > 0); - if (readResult == -1) { - return false; - } else { - result.resize(static_cast(readBytes)); - data = result; - return true; + bool readAllFromDevice(QIODevice* device, QByteArray& data) + { + QByteArray result; + qint64 readBytes = 0; + qint64 readResult; + do { + result.resize(result.size() + 16384); + readResult = device->read(result.data() + readBytes, result.size() - readBytes); + if (readResult > 0) { + readBytes += readResult; + } + } while (readResult > 0); + + if (readResult == -1) { + return false; + } else { + result.resize(static_cast(readBytes)); + data = result; + return true; + } } -} -QString imageReaderFilter() -{ - const QList formats = QImageReader::supportedImageFormats(); - QStringList formatsStringList; + QString imageReaderFilter() + { + const QList formats = QImageReader::supportedImageFormats(); + QStringList formatsStringList; - for (const QByteArray& format : formats) { - for (char codePoint : format) { - if (!QChar(codePoint).isLetterOrNumber()) { - continue; + for (const QByteArray& format : formats) { + for (char codePoint : format) { + if (!QChar(codePoint).isLetterOrNumber()) { + continue; + } } + + formatsStringList.append("*." + QString::fromLatin1(format).toLower()); } - formatsStringList.append("*." + QString::fromLatin1(format).toLower()); + return formatsStringList.join(" "); } - return formatsStringList.join(" "); -} - -bool isHex(const QByteArray& ba) -{ - for (const unsigned char c : ba) { - if (!std::isxdigit(c)) { - return false; + bool isHex(const QByteArray& ba) + { + for (const unsigned char c : ba) { + if (!std::isxdigit(c)) { + return false; + } } - } - return true; -} + return true; + } -bool isBase64(const QByteArray& ba) -{ - constexpr auto pattern = R"(^(?:[a-z0-9+]{4})*(?:[a-z0-9+]{3}=|[a-z0-9+]{2}==)?$)"; - QRegExp regexp(pattern, Qt::CaseInsensitive, QRegExp::RegExp2); + bool isBase64(const QByteArray& ba) + { + constexpr auto pattern = R"(^(?:[a-z0-9+]{4})*(?:[a-z0-9+]{3}=|[a-z0-9+]{2}==)?$)"; + QRegExp regexp(pattern, Qt::CaseInsensitive, QRegExp::RegExp2); - QString base64 = QString::fromLatin1(ba.constData(), ba.size()); + QString base64 = QString::fromLatin1(ba.constData(), ba.size()); - return regexp.exactMatch(base64); -} + return regexp.exactMatch(base64); + } -void sleep(int ms) -{ - Q_ASSERT(ms >= 0); + void sleep(int ms) + { + Q_ASSERT(ms >= 0); - if (ms == 0) { - return; - } + if (ms == 0) { + return; + } #ifdef Q_OS_WIN - Sleep(uint(ms)); + Sleep(uint(ms)); #else - timespec ts; - ts.tv_sec = ms/1000; - ts.tv_nsec = (ms%1000)*1000*1000; - nanosleep(&ts, nullptr); + timespec ts; + ts.tv_sec = ms / 1000; + ts.tv_nsec = (ms % 1000) * 1000 * 1000; + nanosleep(&ts, nullptr); #endif -} + } -void wait(int ms) -{ - Q_ASSERT(ms >= 0); + void wait(int ms) + { + Q_ASSERT(ms >= 0); + + if (ms == 0) { + return; + } - if (ms == 0) { - return; + QElapsedTimer timer; + timer.start(); + + if (ms <= 50) { + QCoreApplication::processEvents(QEventLoop::AllEvents, ms); + sleep(qMax(ms - static_cast(timer.elapsed()), 0)); + } else { + int timeLeft; + do { + timeLeft = ms - timer.elapsed(); + if (timeLeft > 0) { + QCoreApplication::processEvents(QEventLoop::AllEvents, timeLeft); + sleep(10); + } + } while (!timer.hasExpired(ms)); + } } - QElapsedTimer timer; - timer.start(); + bool hasChild(const QObject* parent, const QObject* child) + { + if (!parent || !child) { + return false; + } - if (ms <= 50) { - QCoreApplication::processEvents(QEventLoop::AllEvents, ms); - sleep(qMax(ms - static_cast(timer.elapsed()), 0)); - } else { - int timeLeft; - do { - timeLeft = ms - timer.elapsed(); - if (timeLeft > 0) { - QCoreApplication::processEvents(QEventLoop::AllEvents, timeLeft); - sleep(10); + const QObjectList children = parent->children(); + for (QObject* c : children) { + if (child == c || hasChild(c, child)) { + return true; } } - while (!timer.hasExpired(ms)); + return false; + } + + QString uuidToHex(const QUuid& uuid) + { + return QString::fromLatin1(uuid.toRfc4122().toHex()); } -} + QString reference(const QUuid& uuid, const QString& field) + { + Q_ASSERT(EntryAttributes::DefaultAttributes.count(field) > 0); + + QString uuidStr = uuidToHex(uuid); + + if (field == EntryAttributes::TitleKey) { + return "{REF:T@I:" + uuidStr + "}"; + } + if (field == EntryAttributes::UserNameKey) { + return "{REF:U@I:" + uuidStr + "}"; + } + if (field == EntryAttributes::PasswordKey) { + return "{REF:P@I:" + uuidStr + "}"; + } + if (field == EntryAttributes::URLKey) { + return "{REF:A@I:" + uuidStr + "}"; + } + if (field == EntryAttributes::NotesKey) { + return "{REF:N@I:" + uuidStr + "}"; + } + + __builtin_unreachable(); + } } // namespace Tools diff --git a/src/core/Tools.h b/src/core/Tools.h index 13d9869f7d..d98f297142 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -30,26 +30,30 @@ class QIODevice; namespace Tools { -QString humanReadableFileSize(qint64 bytes, quint32 precision = 2); -bool readFromDevice(QIODevice* device, QByteArray& data, int size = 16384); -bool readAllFromDevice(QIODevice* device, QByteArray& data); -QString imageReaderFilter(); -bool isHex(const QByteArray& ba); -bool isBase64(const QByteArray& ba); -void sleep(int ms); -void wait(int ms); - -template -RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value) -{ - RandomAccessIterator it = std::lower_bound(begin, end, value); - - if ((it == end) || (value < *it)) { - return end; - } else { - return it; + QString humanReadableFileSize(qint64 bytes, quint32 precision = 2); + bool readFromDevice(QIODevice* device, QByteArray& data, int size = 16384); + bool readAllFromDevice(QIODevice* device, QByteArray& data); + QString imageReaderFilter(); + bool isHex(const QByteArray& ba); + bool isBase64(const QByteArray& ba); + void sleep(int ms); + void wait(int ms); + + bool hasChild(const QObject* parent, const QObject* child); + QString uuidToHex(const QUuid& uuid); + QString reference(const QUuid& uuid, const QString& field); + + template + RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value) + { + RandomAccessIterator it = std::lower_bound(begin, end, value); + + if ((it == end) || (value < *it)) { + return end; + } else { + return it; + } } -} } // namespace Tools diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 388919952c..229f914fd3 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -456,43 +456,85 @@ void DatabaseWidget::deleteEntries() selectedEntries.append(m_entryView->entryFromIndex(index)); } - auto* recycleBin = m_db->metadata()->recycleBin(); - bool inRecycleBin = recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid()); + auto it = selectedEntries.begin(); + while (it != selectedEntries.end()) { + QList references = m_db->resolveReferences((*it)->uuid()); + for (const auto& el : selectedEntries) { + references.removeAll(el); + } + + if (!references.isEmpty()) { + if (handleEntryWithReferences(*it, references)) + it++; + else + it = selectedEntries.erase(it); + } else { + it++; + } + } + + if (!selectedEntries.isEmpty()) + deleteEntriesWithNoReferences(selectedEntries); + + refreshSearch(); +} + +bool DatabaseWidget::handleEntryWithReferences(Entry* entry, QList references) +{ + QMessageBox::StandardButton result = MessageBox::question( + this, + tr("Replace references to entry?"), + tr("Entry \"%1\" has %2 references. " + "You can either copy values into references, ignore them or cancel deletion of the original.") + .arg(entry->title().toHtmlEscaped()) + .arg(references.size()), + QMessageBox::Apply | QMessageBox::Ignore | QMessageBox::Cancel); + + if (result == QMessageBox::Apply) + m_db->replaceReferencesWithValues(entry, references); + + return result != QMessageBox::Cancel; +} + +void DatabaseWidget::deleteEntriesWithNoReferences(QList entries) +{ + Q_ASSERT(!entries.isEmpty()); + + bool inRecycleBin = Tools::hasChild(m_db->metadata()->recycleBin(), entries.first()); if (inRecycleBin || !m_db->metadata()->recycleBinEnabled()) { QString prompt; - 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()); } QMessageBox::StandardButton result = MessageBox::question( - this, tr("Delete entry(s)?", "", selected.size()), prompt, QMessageBox::Yes | QMessageBox::No); + this, tr("Delete entry(s)?", "", entries.size()), prompt, QMessageBox::Yes | QMessageBox::No); if (result == QMessageBox::Yes) { - for (Entry* entry : asConst(selectedEntries)) { + for (Entry* entry : asConst(entries)) { delete entry; } - refreshSearch(); } } 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()); } QMessageBox::StandardButton result = MessageBox::question( - this, tr("Move entry(s) to recycle bin?", "", selected.size()), prompt, QMessageBox::Yes | QMessageBox::No); + this, tr("Move entry(s) to recycle bin?", "", entries.size()), prompt, QMessageBox::Yes | QMessageBox::No); if (result == QMessageBox::No) { return; } - for (Entry* entry : asConst(selectedEntries)) { + for (Entry* entry : asConst(entries)) { m_db->recycleEntry(entry); } } diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 828aace51e..38a24e7449 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -23,6 +23,7 @@ #include #include #include +#include #include "gui/entry/EntryModel.h" #include "gui/MessageWidget.h" @@ -215,6 +216,8 @@ private slots: void setClipboardTextAndMinimize(const QString& text); void setIconFromParent(); void replaceDatabase(Database* db); + bool handleEntryWithReferences(Entry* entry, QList references); + void deleteEntriesWithNoReferences(QList entries); QPointer m_db; QWidget* m_mainWidget;