mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-12-04 15:39:34 +01:00
Compare commits
4 Commits
copilot/fi
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af5261d093 | ||
|
|
b0f90f3705 | ||
|
|
b60b2420c9 | ||
|
|
44366feda7 |
@@ -334,7 +334,6 @@ QList<QString> Entry::autoTypeSequences(const QString& windowTitle) const
|
|||||||
};
|
};
|
||||||
|
|
||||||
QList<QString> sequenceList;
|
QList<QString> sequenceList;
|
||||||
QList<QString> emptyWindowSequences; // Store sequences with empty window titles as fallback
|
|
||||||
|
|
||||||
// Add window association matches
|
// Add window association matches
|
||||||
const auto assocList = autoTypeAssociations()->getAll();
|
const auto assocList = autoTypeAssociations()->getAll();
|
||||||
@@ -346,13 +345,6 @@ QList<QString> Entry::autoTypeSequences(const QString& windowTitle) const
|
|||||||
} else {
|
} else {
|
||||||
sequenceList << effectiveAutoTypeSequence();
|
sequenceList << effectiveAutoTypeSequence();
|
||||||
}
|
}
|
||||||
} else if (assoc.window.isEmpty()) {
|
|
||||||
// Store empty window title associations as fallback
|
|
||||||
if (!assoc.sequence.isEmpty()) {
|
|
||||||
emptyWindowSequences << assoc.sequence;
|
|
||||||
} else {
|
|
||||||
emptyWindowSequences << effectiveAutoTypeSequence();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,12 +358,6 @@ QList<QString> Entry::autoTypeSequences(const QString& windowTitle) const
|
|||||||
sequenceList << effectiveAutoTypeSequence();
|
sequenceList << effectiveAutoTypeSequence();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no associations, title, or URL matched, use empty window title associations as fallback
|
|
||||||
// Only use fallback when title matching is enabled to avoid interfering with existing behavior
|
|
||||||
if (sequenceList.isEmpty() && config()->get(Config::AutoTypeEntryTitleMatch).toBool()) {
|
|
||||||
sequenceList << emptyWindowSequences;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sequenceList;
|
return sequenceList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1383,6 +1369,9 @@ void Entry::setGroup(Group* group, bool trackPrevious)
|
|||||||
setPreviousParentGroup(nullptr);
|
setPreviousParentGroup(nullptr);
|
||||||
m_group->database()->addDeletedObject(m_uuid);
|
m_group->database()->addDeletedObject(m_uuid);
|
||||||
|
|
||||||
|
// Resolve references before moving to a different database
|
||||||
|
resolveReferencesBeforeDatabaseMove();
|
||||||
|
|
||||||
// copy custom icon to the new database
|
// copy custom icon to the new database
|
||||||
if (!iconUuid().isNull() && group->database() && m_group->database()->metadata()->hasCustomIcon(iconUuid())
|
if (!iconUuid().isNull() && group->database() && m_group->database()->metadata()->hasCustomIcon(iconUuid())
|
||||||
&& !group->database()->metadata()->hasCustomIcon(iconUuid())) {
|
&& !group->database()->metadata()->hasCustomIcon(iconUuid())) {
|
||||||
@@ -1425,6 +1414,44 @@ Database* Entry::database()
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Entry::resolveReferencesBeforeDatabaseMove()
|
||||||
|
{
|
||||||
|
if (!m_group || !m_group->database()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve references in all default attributes
|
||||||
|
for (const QString& key : EntryAttributes::DefaultAttributes) {
|
||||||
|
if (m_attributes->contains(key) && m_attributes->isReference(key)) {
|
||||||
|
QString originalValue = m_attributes->value(key);
|
||||||
|
QString resolvedValue = resolveMultiplePlaceholdersRecursive(originalValue, 10);
|
||||||
|
|
||||||
|
// Only replace if the resolution produced a different value and it's not empty
|
||||||
|
// Empty resolution means the reference couldn't be resolved, so keep original
|
||||||
|
if (!resolvedValue.isEmpty() && resolvedValue != originalValue) {
|
||||||
|
bool isProtected = m_attributes->isProtected(key);
|
||||||
|
m_attributes->set(key, resolvedValue, isProtected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve references in custom attributes
|
||||||
|
const QList<QString> customKeys = m_attributes->customKeys();
|
||||||
|
for (const QString& key : customKeys) {
|
||||||
|
if (m_attributes->isReference(key)) {
|
||||||
|
QString originalValue = m_attributes->value(key);
|
||||||
|
QString resolvedValue = resolveMultiplePlaceholdersRecursive(originalValue, 10);
|
||||||
|
|
||||||
|
// Only replace if the resolution produced a different value and it's not empty
|
||||||
|
// Empty resolution means the reference couldn't be resolved, so keep original
|
||||||
|
if (!resolvedValue.isEmpty() && resolvedValue != originalValue) {
|
||||||
|
bool isProtected = m_attributes->isProtected(key);
|
||||||
|
m_attributes->set(key, resolvedValue, isProtected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString Entry::maskPasswordPlaceholders(const QString& str) const
|
QString Entry::maskPasswordPlaceholders(const QString& str) const
|
||||||
{
|
{
|
||||||
return QString{str}.replace(QStringLiteral("{PASSWORD}"), QStringLiteral("******"), Qt::CaseInsensitive);
|
return QString{str}.replace(QStringLiteral("{PASSWORD}"), QStringLiteral("******"), Qt::CaseInsensitive);
|
||||||
|
|||||||
@@ -273,6 +273,8 @@ public:
|
|||||||
bool canUpdateTimeinfo() const;
|
bool canUpdateTimeinfo() const;
|
||||||
void setUpdateTimeinfo(bool value);
|
void setUpdateTimeinfo(bool value);
|
||||||
|
|
||||||
|
void resolveReferencesBeforeDatabaseMove();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
/**
|
/**
|
||||||
* Emitted when a default attribute has been changed.
|
* Emitted when a default attribute has been changed.
|
||||||
|
|||||||
@@ -125,15 +125,6 @@ void TestAutoType::init()
|
|||||||
m_entry5->setPassword("example5");
|
m_entry5->setPassword("example5");
|
||||||
m_entry5->setTitle("some title");
|
m_entry5->setTitle("some title");
|
||||||
m_entry5->setUrl("http://example.org");
|
m_entry5->setUrl("http://example.org");
|
||||||
|
|
||||||
// Entry with empty window title (should act as fallback)
|
|
||||||
m_entry6 = new Entry();
|
|
||||||
m_entry6->setGroup(m_group);
|
|
||||||
m_entry6->setPassword("empty_window_test");
|
|
||||||
m_entry6->setTitle("Entry for Empty Window Test");
|
|
||||||
association.window = ""; // Empty window title
|
|
||||||
association.sequence = "empty_window_sequence";
|
|
||||||
m_entry6->autoTypeAssociations()->add(association);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestAutoType::cleanup()
|
void TestAutoType::cleanup()
|
||||||
@@ -289,31 +280,6 @@ void TestAutoType::testGlobalAutoTypeRegExp()
|
|||||||
m_test->clearActions();
|
m_test->clearActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestAutoType::testGlobalAutoTypeEmptyWindow()
|
|
||||||
{
|
|
||||||
// Enable title matching for this test since our fallback logic requires it
|
|
||||||
config()->set(Config::AutoTypeEntryTitleMatch, true);
|
|
||||||
|
|
||||||
// Test that empty window title associations work as fallback when no other associations match
|
|
||||||
// This should use the empty window association from m_entry6 when no specific window matches
|
|
||||||
m_test->setActiveWindowTitle("no_matching_window_title");
|
|
||||||
emit osUtils->globalShortcutTriggered("autotype");
|
|
||||||
m_autoType->performGlobalAutoType(m_dbList);
|
|
||||||
QCOMPARE(m_test->actionChars(), QString("empty_window_sequence"));
|
|
||||||
m_test->clearActions();
|
|
||||||
|
|
||||||
// Test that empty window title associations do NOT match when other associations exist and match
|
|
||||||
// This entry has window associations that should take precedence over empty window title
|
|
||||||
m_test->setActiveWindowTitle("custom window"); // This should match m_entry1 association
|
|
||||||
emit osUtils->globalShortcutTriggered("autotype");
|
|
||||||
m_autoType->performGlobalAutoType(m_dbList);
|
|
||||||
QCOMPARE(m_test->actionChars(), QString("myuserassociationmypass")); // Should be from m_entry1, not empty window
|
|
||||||
m_test->clearActions();
|
|
||||||
|
|
||||||
// Reset title matching to default state
|
|
||||||
config()->set(Config::AutoTypeEntryTitleMatch, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestAutoType::testAutoTypeResults()
|
void TestAutoType::testAutoTypeResults()
|
||||||
{
|
{
|
||||||
QScopedPointer<Entry> entry(new Entry());
|
QScopedPointer<Entry> entry(new Entry());
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ private slots:
|
|||||||
void testGlobalAutoTypeUrlSubdomainMatch();
|
void testGlobalAutoTypeUrlSubdomainMatch();
|
||||||
void testGlobalAutoTypeTitleMatchDisabled();
|
void testGlobalAutoTypeTitleMatchDisabled();
|
||||||
void testGlobalAutoTypeRegExp();
|
void testGlobalAutoTypeRegExp();
|
||||||
void testGlobalAutoTypeEmptyWindow();
|
|
||||||
void testAutoTypeResults();
|
void testAutoTypeResults();
|
||||||
void testAutoTypeResults_data();
|
void testAutoTypeResults_data();
|
||||||
void testAutoTypeSyntaxChecks();
|
void testAutoTypeSyntaxChecks();
|
||||||
@@ -65,7 +64,6 @@ private:
|
|||||||
Entry* m_entry3;
|
Entry* m_entry3;
|
||||||
Entry* m_entry4;
|
Entry* m_entry4;
|
||||||
Entry* m_entry5;
|
Entry* m_entry5;
|
||||||
Entry* m_entry6;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KEEPASSX_TESTAUTOTYPE_H
|
#endif // KEEPASSX_TESTAUTOTYPE_H
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
|
|
||||||
#include "TestEntry.h"
|
#include "TestEntry.h"
|
||||||
#include "core/Clock.h"
|
#include "core/Clock.h"
|
||||||
|
#include "core/Database.h"
|
||||||
|
#include "core/Entry.h"
|
||||||
|
#include "core/EntryAttributes.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
#include "core/Metadata.h"
|
#include "core/Metadata.h"
|
||||||
#include "core/TimeInfo.h"
|
#include "core/TimeInfo.h"
|
||||||
@@ -672,6 +675,85 @@ void TestEntry::testResolveClonedEntry()
|
|||||||
QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->password()), original->password());
|
QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->password()), original->password());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestEntry::testCrossDatabaseReferences()
|
||||||
|
{
|
||||||
|
// Test that references are resolved when moving entries between databases
|
||||||
|
Database db1;
|
||||||
|
auto* root1 = db1.rootGroup();
|
||||||
|
Database db2;
|
||||||
|
auto* root2 = db2.rootGroup();
|
||||||
|
|
||||||
|
// Create original entry in database 1
|
||||||
|
auto* originalEntry = new Entry();
|
||||||
|
originalEntry->setGroup(root1);
|
||||||
|
originalEntry->setUuid(QUuid::createUuid());
|
||||||
|
originalEntry->setTitle("OriginalTitle");
|
||||||
|
originalEntry->setUsername("OriginalUsername");
|
||||||
|
originalEntry->setPassword("OriginalPassword");
|
||||||
|
originalEntry->setUrl("http://original.com");
|
||||||
|
originalEntry->setNotes("OriginalNotes");
|
||||||
|
|
||||||
|
// Create entry with references to original entry in database 1
|
||||||
|
auto* refEntry = new Entry();
|
||||||
|
refEntry->setGroup(root1);
|
||||||
|
refEntry->setUuid(QUuid::createUuid());
|
||||||
|
refEntry->setTitle(QString("{REF:T@I:%1}").arg(originalEntry->uuidToHex()));
|
||||||
|
refEntry->setUsername(QString("{REF:U@I:%1}").arg(originalEntry->uuidToHex()));
|
||||||
|
refEntry->setPassword(QString("{REF:P@I:%1}").arg(originalEntry->uuidToHex()));
|
||||||
|
refEntry->setUrl(QString("{REF:A@I:%1}").arg(originalEntry->uuidToHex()));
|
||||||
|
refEntry->setNotes(QString("{REF:N@I:%1}").arg(originalEntry->uuidToHex()));
|
||||||
|
|
||||||
|
// Add custom attribute with reference
|
||||||
|
refEntry->attributes()->set("CustomRef", QString("{REF:T@I:%1}").arg(originalEntry->uuidToHex()));
|
||||||
|
|
||||||
|
// Verify references work within same database
|
||||||
|
QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->title()), QString("OriginalTitle"));
|
||||||
|
QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->username()), QString("OriginalUsername"));
|
||||||
|
QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->password()), QString("OriginalPassword"));
|
||||||
|
QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->url()), QString("http://original.com"));
|
||||||
|
QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->notes()), QString("OriginalNotes"));
|
||||||
|
QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->attributes()->value("CustomRef")),
|
||||||
|
QString("OriginalTitle"));
|
||||||
|
|
||||||
|
// Verify the attributes still contain references (not yet resolved)
|
||||||
|
QVERIFY(refEntry->attributes()->isReference(EntryAttributes::TitleKey));
|
||||||
|
QVERIFY(refEntry->attributes()->isReference(EntryAttributes::UserNameKey));
|
||||||
|
QVERIFY(refEntry->attributes()->isReference(EntryAttributes::PasswordKey));
|
||||||
|
QVERIFY(refEntry->attributes()->isReference(EntryAttributes::URLKey));
|
||||||
|
QVERIFY(refEntry->attributes()->isReference(EntryAttributes::NotesKey));
|
||||||
|
QVERIFY(refEntry->attributes()->isReference("CustomRef"));
|
||||||
|
|
||||||
|
// Move the referenced entry to database 2
|
||||||
|
// This should resolve the references before the move
|
||||||
|
refEntry->setGroup(root2);
|
||||||
|
|
||||||
|
// After move, the entry should have resolved values instead of references
|
||||||
|
QCOMPARE(refEntry->title(), QString("OriginalTitle"));
|
||||||
|
QCOMPARE(refEntry->username(), QString("OriginalUsername"));
|
||||||
|
QCOMPARE(refEntry->password(), QString("OriginalPassword"));
|
||||||
|
QCOMPARE(refEntry->url(), QString("http://original.com"));
|
||||||
|
QCOMPARE(refEntry->notes(), QString("OriginalNotes"));
|
||||||
|
QCOMPARE(refEntry->attributes()->value("CustomRef"), QString("OriginalTitle"));
|
||||||
|
|
||||||
|
// Verify that the references have been replaced with actual values
|
||||||
|
QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::TitleKey));
|
||||||
|
QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::UserNameKey));
|
||||||
|
QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::PasswordKey));
|
||||||
|
QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::URLKey));
|
||||||
|
QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::NotesKey));
|
||||||
|
QVERIFY(!refEntry->attributes()->isReference("CustomRef"));
|
||||||
|
|
||||||
|
// Test case where original entry doesn't exist (should keep the reference string)
|
||||||
|
auto* orphanEntry = new Entry();
|
||||||
|
orphanEntry->setGroup(root1);
|
||||||
|
orphanEntry->setUuid(QUuid::createUuid());
|
||||||
|
orphanEntry->setTitle("{REF:T@I:NONEXISTENTUUID}");
|
||||||
|
|
||||||
|
// Move orphan entry - the unresolvable reference should remain unchanged
|
||||||
|
orphanEntry->setGroup(root2);
|
||||||
|
QCOMPARE(orphanEntry->title(), QString("{REF:T@I:NONEXISTENTUUID}"));
|
||||||
|
}
|
||||||
|
|
||||||
void TestEntry::testIsRecycled()
|
void TestEntry::testIsRecycled()
|
||||||
{
|
{
|
||||||
auto entry = new Entry();
|
auto entry = new Entry();
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ private slots:
|
|||||||
void testResolveConversionPlaceholders();
|
void testResolveConversionPlaceholders();
|
||||||
void testResolveReplacePlaceholders();
|
void testResolveReplacePlaceholders();
|
||||||
void testResolveClonedEntry();
|
void testResolveClonedEntry();
|
||||||
|
void testCrossDatabaseReferences();
|
||||||
void testIsRecycled();
|
void testIsRecycled();
|
||||||
void testMoveUpDown();
|
void testMoveUpDown();
|
||||||
void testPreviousParentGroup();
|
void testPreviousParentGroup();
|
||||||
|
|||||||
Reference in New Issue
Block a user