From dfd4a1c12ce29638d44d3e28c6d41e7aceb16970 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 9 Nov 2025 16:10:45 -0500 Subject: [PATCH] Implement Group sync for KeeShare (#11593) --------- Co-authored-by: ever Co-authored-by: Ben Kluwe --- share/translations/keepassxc_en.ts | 8 ++++ src/keeshare/KeeShareSettings.cpp | 20 ++++++-- src/keeshare/KeeShareSettings.h | 3 +- src/keeshare/ShareExport.cpp | 48 ++++++++++++++----- .../group/EditGroupWidgetKeeShare.cpp | 13 +++++ src/keeshare/group/EditGroupWidgetKeeShare.h | 1 + src/keeshare/group/EditGroupWidgetKeeShare.ui | 17 ++++++- 7 files changed, 91 insertions(+), 19 deletions(-) diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 48ae6c791..31f56c0bd 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -3630,6 +3630,14 @@ Supported extensions are: %1. Select import/export file + + Maintain group structure with shared database + + + + Keep Group Structure + + EditGroupWidgetMain diff --git a/src/keeshare/KeeShareSettings.cpp b/src/keeshare/KeeShareSettings.cpp index fa500eb5d..61ab2bb8e 100644 --- a/src/keeshare/KeeShareSettings.cpp +++ b/src/keeshare/KeeShareSettings.cpp @@ -213,7 +213,7 @@ namespace KeeShareSettings } } } else { - qWarning("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString())); + qDebug("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString())); reader.skipCurrentElement(); } } @@ -253,7 +253,7 @@ namespace KeeShareSettings } else if (reader.name() == "PublicKey") { own.certificate = Certificate::deserialize(reader); } else { - qWarning("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString())); + qDebug("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString())); reader.skipCurrentElement(); } } @@ -262,8 +262,7 @@ namespace KeeShareSettings } Reference::Reference() - : type(Inactive) - , uuid(QUuid::createUuid()) + : uuid(QUuid::createUuid()) { } @@ -320,12 +319,21 @@ namespace KeeShareSettings writer.writeStartElement("Password"); writer.writeCharacters(reference.password.toUtf8().toBase64()); writer.writeEndElement(); + writer.writeStartElement("KeepGroups"); + writer.writeCharacters(reference.keepGroups ? "True" : "False"); + writer.writeEndElement(); }); } Reference Reference::deserialize(const QString& raw) { + if (raw.isEmpty()) { + return {}; + } + Reference reference; + // If KeepGroups is not present, default to false for backward compatibility + reference.keepGroups = false; xmlDeserialize(raw, [&](QXmlStreamReader& reader) { while (!reader.error() && reader.readNextStartElement()) { if (reader.name() == "Type") { @@ -346,8 +354,10 @@ namespace KeeShareSettings reference.path = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1())); } else if (reader.name() == "Password") { reference.password = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1())); + } else if (reader.name() == "KeepGroups") { + reference.keepGroups = reader.readElementText().compare("True") == 0; } else { - qWarning("Unknown Reference element %s", qPrintable(reader.name().toString())); + qDebug("Unknown Reference element %s", qPrintable(reader.name().toString())); reader.skipCurrentElement(); } } diff --git a/src/keeshare/KeeShareSettings.h b/src/keeshare/KeeShareSettings.h index b81dc15af..667cb8a74 100644 --- a/src/keeshare/KeeShareSettings.h +++ b/src/keeshare/KeeShareSettings.h @@ -122,10 +122,11 @@ namespace KeeShareSettings struct Reference { - Type type; + Type type = Inactive; QUuid uuid; QString path; QString password; + bool keepGroups = true; Reference(); bool isNull() const; diff --git a/src/keeshare/ShareExport.cpp b/src/keeshare/ShareExport.cpp index 85f60d970..ed3aa0f11 100644 --- a/src/keeshare/ShareExport.cpp +++ b/src/keeshare/ShareExport.cpp @@ -62,6 +62,39 @@ namespace } } + void cloneIcon(Metadata* targetMetadata, const Database* sourceDb, const QUuid& iconUuid) + { + if (!iconUuid.isNull() && !targetMetadata->hasCustomIcon(iconUuid)) { + targetMetadata->addCustomIcon(iconUuid, sourceDb->metadata()->customIcon(iconUuid)); + } + } + + void cloneEntries(Metadata* targetMetadata, const Group* sourceGroup, Group* targetGroup) + { + for (const Entry* sourceEntry : sourceGroup->entries()) { + auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory); + const bool updateTimeinfoEntry = targetEntry->canUpdateTimeinfo(); + targetEntry->setUpdateTimeinfo(false); + targetEntry->setGroup(targetGroup); + targetEntry->setUpdateTimeinfo(updateTimeinfoEntry); + cloneIcon(targetMetadata, sourceEntry->database(), targetEntry->iconUuid()); + } + } + + void cloneChildren(Metadata* targetMetadata, const Group* sourceRoot, Group* targetRoot) + { + for (const Group* sourceGroup : sourceRoot->children()) { + auto* targetGroup = sourceGroup->clone(Entry::CloneNoFlags, Group::CloneNoFlags); + const bool updateTimeinfo = targetGroup->canUpdateTimeinfo(); + targetGroup->setUpdateTimeinfo(false); + targetGroup->setParent(targetRoot); + targetGroup->setUpdateTimeinfo(updateTimeinfo); + cloneIcon(targetMetadata, sourceRoot->database(), targetGroup->iconUuid()); + cloneEntries(targetMetadata, sourceGroup, targetGroup); + cloneChildren(targetMetadata, sourceGroup, targetGroup); + } + } + Database* extractIntoDatabase(const KeeShareSettings::Reference& reference, const Group* sourceRoot) { const auto* sourceDb = sourceRoot->database(); @@ -75,17 +108,10 @@ namespace targetRoot->setUpdateTimeinfo(false); KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference()); targetRoot->setUpdateTimeinfo(updateTimeinfo); - const auto sourceEntries = sourceRoot->entriesRecursive(false); - for (const Entry* sourceEntry : sourceEntries) { - auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory); - const bool updateTimeinfoEntry = targetEntry->canUpdateTimeinfo(); - targetEntry->setUpdateTimeinfo(false); - targetEntry->setGroup(targetRoot); - targetEntry->setUpdateTimeinfo(updateTimeinfoEntry); - const auto iconUuid = targetEntry->iconUuid(); - if (!iconUuid.isNull() && !targetMetadata->hasCustomIcon(iconUuid)) { - targetMetadata->addCustomIcon(iconUuid, sourceEntry->database()->metadata()->customIcon(iconUuid)); - } + cloneIcon(targetMetadata, sourceRoot->database(), targetRoot->iconUuid()); + cloneEntries(targetMetadata, sourceRoot, targetRoot); + if (reference.keepGroups) { + cloneChildren(targetMetadata, sourceRoot, targetRoot); } auto key = QSharedPointer::create(); diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index 82f8dd1c0..bea495b0a 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -43,6 +43,7 @@ EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent) connect(m_ui->pathEdit, SIGNAL(editingFinished()), SLOT(selectPath())); connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(launchPathSelectionDialog())); connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType())); + connect(m_ui->keepGroupsCheckbox, SIGNAL(toggled(bool)), SLOT(keepGroupsToggled(bool))); connect(m_ui->clearButton, SIGNAL(clicked(bool)), SLOT(clearInputs())); connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(updateSharingState())); @@ -97,6 +98,7 @@ void EditGroupWidgetKeeShare::updateSharingState() m_ui->pathEdit->setEnabled(isEnabled); m_ui->pathSelectionButton->setEnabled(isEnabled); m_ui->passwordEdit->setEnabled(isEnabled); + m_ui->keepGroupsCheckbox->setEnabled(isEnabled); if (!m_temporaryGroup || !isEnabled) { m_ui->messageWidget->hideMessage(); @@ -188,6 +190,7 @@ void EditGroupWidgetKeeShare::update() m_ui->typeComboBox->setCurrentIndex(reference.type); m_ui->passwordEdit->setText(reference.password); m_ui->pathEdit->setText(reference.path); + m_ui->keepGroupsCheckbox->setChecked(reference.keepGroups); } updateSharingState(); @@ -291,3 +294,13 @@ void EditGroupWidgetKeeShare::selectType() updateSharingState(); } + +void EditGroupWidgetKeeShare::keepGroupsToggled(bool toggled) +{ + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.keepGroups = toggled; + KeeShare::setReferenceTo(m_temporaryGroup, reference); +} diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.h b/src/keeshare/group/EditGroupWidgetKeeShare.h index aaa3ebbd3..bd08366e3 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.h +++ b/src/keeshare/group/EditGroupWidgetKeeShare.h @@ -48,6 +48,7 @@ private slots: void selectPassword(); void launchPathSelectionDialog(); void selectPath(); + void keepGroupsToggled(bool); private: QScopedPointer m_ui; diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.ui b/src/keeshare/group/EditGroupWidgetKeeShare.ui index 857ba61c8..1e8e0e1a6 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.ui +++ b/src/keeshare/group/EditGroupWidgetKeeShare.ui @@ -138,7 +138,7 @@ - + @@ -171,7 +171,7 @@ - + Qt::Vertical @@ -184,6 +184,19 @@ + + + + Maintain group structure with shared database + + + Keep Group Structure + + + true + + +