mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-12-04 15:39:34 +01:00
Compare commits
3 Commits
copilot/fi
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b561ff2e80 | ||
|
|
2a931231c1 | ||
|
|
aedd4ae08e |
@@ -334,6 +334,7 @@ QList<QString> Entry::autoTypeSequences(const QString& windowTitle) const
|
||||
};
|
||||
|
||||
QList<QString> sequenceList;
|
||||
QList<QString> emptyWindowSequences; // Store sequences with empty window titles as fallback
|
||||
|
||||
// Add window association matches
|
||||
const auto assocList = autoTypeAssociations()->getAll();
|
||||
@@ -345,6 +346,13 @@ QList<QString> Entry::autoTypeSequences(const QString& windowTitle) const
|
||||
} else {
|
||||
sequenceList << effectiveAutoTypeSequence();
|
||||
}
|
||||
} else if (assoc.window.isEmpty()) {
|
||||
// Store empty window title associations as fallback
|
||||
if (!assoc.sequence.isEmpty()) {
|
||||
emptyWindowSequences << assoc.sequence;
|
||||
} else {
|
||||
emptyWindowSequences << effectiveAutoTypeSequence();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +366,12 @@ QList<QString> Entry::autoTypeSequences(const QString& windowTitle) const
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -211,78 +211,6 @@ namespace
|
||||
return entry.take();
|
||||
}
|
||||
|
||||
/*!
|
||||
* Create nested folder hierarchy from a path string.
|
||||
* For example, "Socials/Forums" creates a "Socials" group with a "Forums" child group.
|
||||
* Returns the deepest (leaf) group in the hierarchy.
|
||||
*/
|
||||
Group*
|
||||
createNestedFolderHierarchy(const QString& folderPath, Group* rootGroup, QMap<QString, Group*>& createdGroups)
|
||||
{
|
||||
if (folderPath.isEmpty()) {
|
||||
return rootGroup;
|
||||
}
|
||||
|
||||
// Check if we've already created this exact path
|
||||
if (createdGroups.contains(folderPath)) {
|
||||
return createdGroups.value(folderPath);
|
||||
}
|
||||
|
||||
// Split the path by forward slashes
|
||||
QStringList pathParts = folderPath.split('/', Qt::SkipEmptyParts);
|
||||
if (pathParts.isEmpty()) {
|
||||
return rootGroup;
|
||||
}
|
||||
|
||||
Group* currentParent = rootGroup;
|
||||
QString currentPath;
|
||||
|
||||
// Create each level of the hierarchy
|
||||
for (int i = 0; i < pathParts.size(); ++i) {
|
||||
const QString& partName = pathParts[i];
|
||||
|
||||
// Build the current path (e.g., "Socials", then "Socials/Forums")
|
||||
if (currentPath.isEmpty()) {
|
||||
currentPath = partName;
|
||||
} else {
|
||||
currentPath += "/" + partName;
|
||||
}
|
||||
|
||||
// Check if this level already exists
|
||||
Group* existingGroup = createdGroups.value(currentPath);
|
||||
if (existingGroup) {
|
||||
currentParent = existingGroup;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find existing child group with this name
|
||||
existingGroup = nullptr;
|
||||
for (Group* child : currentParent->children()) {
|
||||
if (child->name() == partName) {
|
||||
existingGroup = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingGroup) {
|
||||
// Use existing group
|
||||
createdGroups.insert(currentPath, existingGroup);
|
||||
currentParent = existingGroup;
|
||||
} else {
|
||||
// Create new group
|
||||
auto newGroup = new Group();
|
||||
newGroup->setUuid(QUuid::createUuid());
|
||||
newGroup->setName(partName);
|
||||
newGroup->setParent(currentParent);
|
||||
|
||||
createdGroups.insert(currentPath, newGroup);
|
||||
currentParent = newGroup;
|
||||
}
|
||||
}
|
||||
|
||||
return currentParent;
|
||||
}
|
||||
|
||||
void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer<Database> db)
|
||||
{
|
||||
auto folderField = QString("folders");
|
||||
@@ -296,19 +224,15 @@ namespace
|
||||
return;
|
||||
}
|
||||
|
||||
// Create groups from folders and store a temporary map of id -> group
|
||||
// Create groups from folders and store a temporary map of id -> uuid
|
||||
QMap<QString, Group*> folderMap;
|
||||
QMap<QString, Group*> createdGroups; // Track created groups by path to avoid duplicates
|
||||
|
||||
for (const auto& folder : vault.value(folderField).toArray()) {
|
||||
const QString folderName = folder.toObject().value("name").toString();
|
||||
const QString folderId = folder.toObject().value("id").toString();
|
||||
auto group = new Group();
|
||||
group->setUuid(QUuid::createUuid());
|
||||
group->setName(folder.toObject().value("name").toString());
|
||||
group->setParent(db->rootGroup());
|
||||
|
||||
// Create the nested folder hierarchy
|
||||
Group* targetGroup = createNestedFolderHierarchy(folderName, db->rootGroup(), createdGroups);
|
||||
|
||||
// Map the folder ID to the target group
|
||||
folderMap.insert(folderId, targetGroup);
|
||||
folderMap.insert(folder.toObject().value("id").toString(), group);
|
||||
}
|
||||
|
||||
QString folderId;
|
||||
|
||||
@@ -125,6 +125,15 @@ void TestAutoType::init()
|
||||
m_entry5->setPassword("example5");
|
||||
m_entry5->setTitle("some title");
|
||||
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()
|
||||
@@ -280,6 +289,31 @@ void TestAutoType::testGlobalAutoTypeRegExp()
|
||||
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()
|
||||
{
|
||||
QScopedPointer<Entry> entry(new Entry());
|
||||
|
||||
@@ -47,6 +47,7 @@ private slots:
|
||||
void testGlobalAutoTypeUrlSubdomainMatch();
|
||||
void testGlobalAutoTypeTitleMatchDisabled();
|
||||
void testGlobalAutoTypeRegExp();
|
||||
void testGlobalAutoTypeEmptyWindow();
|
||||
void testAutoTypeResults();
|
||||
void testAutoTypeResults_data();
|
||||
void testAutoTypeSyntaxChecks();
|
||||
@@ -64,6 +65,7 @@ private:
|
||||
Entry* m_entry3;
|
||||
Entry* m_entry4;
|
||||
Entry* m_entry5;
|
||||
Entry* m_entry6;
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TESTAUTOTYPE_H
|
||||
|
||||
@@ -317,56 +317,6 @@ void TestImports::testBitwardenPasskey()
|
||||
QStringLiteral("aTFtdmFnOHYtS2dxVEJ0by1rSFpLWGg0enlTVC1iUVJReDZ5czJXa3c2aw"));
|
||||
}
|
||||
|
||||
void TestImports::testBitwardenNestedFolders()
|
||||
{
|
||||
auto bitwardenPath =
|
||||
QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/bitwarden_nested_export.json"));
|
||||
|
||||
BitwardenReader reader;
|
||||
auto db = reader.convert(bitwardenPath);
|
||||
QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
|
||||
QVERIFY(db);
|
||||
|
||||
// Test nested folder structure: "Socials/Forums" should create Socials -> Forums hierarchy
|
||||
auto entry = db->rootGroup()->findEntryByPath("/Socials/Forums/Reddit Account");
|
||||
QVERIFY(entry);
|
||||
QCOMPARE(entry->title(), QStringLiteral("Reddit Account"));
|
||||
QCOMPARE(entry->username(), QStringLiteral("myuser"));
|
||||
|
||||
// Test deeper nesting: "Work/Projects/Client A"
|
||||
entry = db->rootGroup()->findEntryByPath("/Work/Projects/Client A/Client Portal");
|
||||
QVERIFY(entry);
|
||||
QCOMPARE(entry->title(), QStringLiteral("Client Portal"));
|
||||
QCOMPARE(entry->username(), QStringLiteral("clientuser"));
|
||||
|
||||
// Test simple folder (no nesting): "Personal"
|
||||
entry = db->rootGroup()->findEntryByPath("/Personal/Personal Email");
|
||||
QVERIFY(entry);
|
||||
QCOMPARE(entry->title(), QStringLiteral("Personal Email"));
|
||||
QCOMPARE(entry->username(), QStringLiteral("personal@email.com"));
|
||||
|
||||
// Verify the folder hierarchy exists
|
||||
auto socialsGroup = db->rootGroup()->findGroupByPath("/Socials");
|
||||
QVERIFY(socialsGroup);
|
||||
QCOMPARE(socialsGroup->name(), QStringLiteral("Socials"));
|
||||
|
||||
auto forumsGroup = socialsGroup->findGroupByPath("Forums");
|
||||
QVERIFY(forumsGroup);
|
||||
QCOMPARE(forumsGroup->name(), QStringLiteral("Forums"));
|
||||
|
||||
auto workGroup = db->rootGroup()->findGroupByPath("/Work");
|
||||
QVERIFY(workGroup);
|
||||
QCOMPARE(workGroup->name(), QStringLiteral("Work"));
|
||||
|
||||
auto projectsGroup = workGroup->findGroupByPath("Projects");
|
||||
QVERIFY(projectsGroup);
|
||||
QCOMPARE(projectsGroup->name(), QStringLiteral("Projects"));
|
||||
|
||||
auto clientAGroup = projectsGroup->findGroupByPath("Client A");
|
||||
QVERIFY(clientAGroup);
|
||||
QCOMPARE(clientAGroup->name(), QStringLiteral("Client A"));
|
||||
}
|
||||
|
||||
void TestImports::testProtonPass()
|
||||
{
|
||||
auto protonPassPath =
|
||||
|
||||
@@ -31,7 +31,6 @@ private slots:
|
||||
void testBitwarden();
|
||||
void testBitwardenEncrypted();
|
||||
void testBitwardenPasskey();
|
||||
void testBitwardenNestedFolders();
|
||||
void testProtonPass();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"name": "Socials/Forums"
|
||||
},
|
||||
{
|
||||
"id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
|
||||
"name": "Work/Projects/Client A"
|
||||
},
|
||||
{
|
||||
"id": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
|
||||
"name": "Personal"
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa",
|
||||
"organizationId": null,
|
||||
"folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"type": 1,
|
||||
"name": "Reddit Account",
|
||||
"notes": "My reddit login",
|
||||
"favorite": false,
|
||||
"login": {
|
||||
"username": "myuser",
|
||||
"password": "mypass",
|
||||
"uris": [
|
||||
{
|
||||
"uri": "https://reddit.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||
"organizationId": null,
|
||||
"folderId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
|
||||
"type": 1,
|
||||
"name": "Client Portal",
|
||||
"notes": "Client A portal login",
|
||||
"favorite": false,
|
||||
"login": {
|
||||
"username": "clientuser",
|
||||
"password": "clientpass",
|
||||
"uris": [
|
||||
{
|
||||
"uri": "https://clienta.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
|
||||
"organizationId": null,
|
||||
"folderId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
|
||||
"type": 1,
|
||||
"name": "Personal Email",
|
||||
"notes": "My personal email",
|
||||
"favorite": false,
|
||||
"login": {
|
||||
"username": "personal@email.com",
|
||||
"password": "personalpass",
|
||||
"uris": [
|
||||
{
|
||||
"uri": "https://mail.provider.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user