mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2026-03-11 08:54:48 +00:00
Merge 737af8798f into d9be1b0682
This commit is contained in:
commit
96e260f94c
15 changed files with 446 additions and 39 deletions
|
|
@ -202,6 +202,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
|
|||
{Config::KeeShare_Own, {QS("KeeShare/Own"), Roaming, {}}},
|
||||
{Config::KeeShare_Foreign, {QS("KeeShare/Foreign"), Roaming, {}}},
|
||||
{Config::KeeShare_Active, {QS("KeeShare/Active"), Roaming, {}}},
|
||||
{Config::KeeShare_DeviceId, {QS("KeeShare/DeviceId"), Local, {}}},
|
||||
|
||||
// PasswordGenerator
|
||||
{Config::PasswordGenerator_LowerCase, {QS("PasswordGenerator/LowerCase"), Roaming, true}},
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ public:
|
|||
KeeShare_Own,
|
||||
KeeShare_Foreign,
|
||||
KeeShare_Active,
|
||||
KeeShare_DeviceId,
|
||||
|
||||
PasswordGenerator_LowerCase,
|
||||
PasswordGenerator_UpperCase,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@
|
|||
#include "gui/DatabaseIcons.h"
|
||||
#include "keeshare/ShareObserver.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QSysInfo>
|
||||
|
||||
namespace
|
||||
{
|
||||
static const QString KeeShare_Reference("KeeShare/Reference");
|
||||
|
|
@ -51,6 +54,39 @@ void KeeShare::init(QObject* parent)
|
|||
m_instance = new KeeShare(parent);
|
||||
}
|
||||
|
||||
QString KeeShare::deviceId()
|
||||
{
|
||||
auto id = config()->get(Config::KeeShare_DeviceId).toString();
|
||||
if (id.isEmpty()) {
|
||||
// Generate fallback from machine unique ID, truncated to 7 chars
|
||||
auto machineId = QSysInfo::machineUniqueId();
|
||||
if (!machineId.isEmpty()) {
|
||||
// machineUniqueId() on Linux returns a hex string directly from /etc/machine-id
|
||||
id = QString::fromLatin1(machineId).left(7).toUpper();
|
||||
} else {
|
||||
// Last resort: use hostname
|
||||
id = QSysInfo::machineHostName();
|
||||
}
|
||||
setDeviceId(id);
|
||||
// Re-read the sanitized value
|
||||
id = config()->get(Config::KeeShare_DeviceId).toString();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
void KeeShare::setDeviceId(const QString& id)
|
||||
{
|
||||
// All sanitization consolidated here: strip non-alphanumeric, enforce max
|
||||
// length, and fall back to DEFAULT if empty
|
||||
QString sanitized = id;
|
||||
sanitized.remove(QRegularExpression("[^A-Za-z0-9]"));
|
||||
sanitized.truncate(32);
|
||||
if (sanitized.isEmpty()) {
|
||||
sanitized = QStringLiteral("DEFAULT");
|
||||
}
|
||||
config()->set(Config::KeeShare_DeviceId, sanitized);
|
||||
}
|
||||
|
||||
KeeShareSettings::Own KeeShare::own()
|
||||
{
|
||||
// Read existing own certificate or generate a new one if none available
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ public:
|
|||
static const Group* resolveSharedGroup(const Group* group);
|
||||
static QString sharingLabel(const Group* group);
|
||||
|
||||
static QString deviceId();
|
||||
static void setDeviceId(const QString& id);
|
||||
|
||||
static KeeShareSettings::Own own();
|
||||
static void setOwn(const KeeShareSettings::Own& own);
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@
|
|||
#include "gui/DatabaseIcons.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QTextCodec>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
|
|
@ -286,6 +288,15 @@ namespace KeeShareSettings
|
|||
return (type & ImportFrom) != 0 && !path.isEmpty();
|
||||
}
|
||||
|
||||
bool Reference::isPerDeviceMode(const QDir& baseDir) const
|
||||
{
|
||||
if (path.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const QString resolvedPath = baseDir.absoluteFilePath(path);
|
||||
return QFileInfo(resolvedPath).isDir();
|
||||
}
|
||||
|
||||
bool Reference::operator<(const Reference& other) const
|
||||
{
|
||||
if (type != other.type) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
#include <QSharedPointer>
|
||||
#include <QUuid>
|
||||
|
||||
class QDir;
|
||||
|
||||
namespace Botan
|
||||
{
|
||||
class Private_Key;
|
||||
|
|
@ -133,6 +135,7 @@ namespace KeeShareSettings
|
|||
bool isValid() const;
|
||||
bool isExporting() const;
|
||||
bool isImporting() const;
|
||||
bool isPerDeviceMode(const QDir& baseDir) const;
|
||||
bool operator<(const Reference& other) const;
|
||||
bool operator==(const Reference& other) const;
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
#include "gui/MessageBox.h"
|
||||
#include "keeshare/KeeShare.h"
|
||||
|
||||
#include <QRegularExpressionValidator>
|
||||
#include <QStandardItemModel>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
|
|
@ -33,6 +34,9 @@ SettingsWidgetKeeShare::SettingsWidgetKeeShare(QWidget* parent)
|
|||
{
|
||||
m_ui->setupUi(this);
|
||||
|
||||
m_ui->deviceIdEdit->setValidator(
|
||||
new QRegularExpressionValidator(QRegularExpression("[A-Za-z0-9]{0,32}"), this));
|
||||
|
||||
connect(m_ui->ownCertificateSignerEdit, SIGNAL(textChanged(QString)), SLOT(setVerificationExporter(QString)));
|
||||
connect(m_ui->generateOwnCerticateButton, SIGNAL(clicked(bool)), SLOT(generateCertificate()));
|
||||
}
|
||||
|
|
@ -47,6 +51,8 @@ void SettingsWidgetKeeShare::loadSettings()
|
|||
m_ui->enableExportCheckBox->setChecked(active.out);
|
||||
m_ui->enableImportCheckBox->setChecked(active.in);
|
||||
|
||||
m_ui->deviceIdEdit->setText(KeeShare::deviceId());
|
||||
|
||||
m_own = KeeShare::own();
|
||||
updateOwnCertificate();
|
||||
}
|
||||
|
|
@ -68,6 +74,14 @@ void SettingsWidgetKeeShare::saveSettings()
|
|||
KeeShare::setOwn(m_own);
|
||||
KeeShare::setActive(active);
|
||||
|
||||
auto deviceId = m_ui->deviceIdEdit->text().trimmed();
|
||||
if (!deviceId.isEmpty()) {
|
||||
KeeShare::setDeviceId(deviceId);
|
||||
} else {
|
||||
// Clear stored ID so it will be auto-detected next time
|
||||
config()->set(Config::KeeShare_DeviceId, QString());
|
||||
}
|
||||
|
||||
config()->set(Config::KeeShare_QuietSuccess, m_ui->quietSuccessCheckBox->isChecked());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,48 @@
|
|||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="deviceIdentityGroupBox">
|
||||
<property name="title">
|
||||
<string>Device Identity</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="deviceIdLabel">
|
||||
<property name="text">
|
||||
<string>Device ID:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="deviceIdEdit">
|
||||
<property name="accessibleName">
|
||||
<string>Device ID field</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Unique identifier for this device in per-device sync mode (alphanumeric only)</string>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Auto-detected from system</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<spacer name="deviceIdSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="ownCertificateGroupBox">
|
||||
<property name="title">
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
#include "keeshare/ShareImport.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QTimer>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
|
@ -67,6 +68,7 @@ void ShareObserver::deinitialize()
|
|||
m_groupToReference.clear();
|
||||
m_shareToGroup.clear();
|
||||
m_fileWatchers.clear();
|
||||
m_dirWatchers.clear();
|
||||
}
|
||||
|
||||
void ShareObserver::reinitialize()
|
||||
|
|
@ -83,6 +85,7 @@ void ShareObserver::reinitialize()
|
|||
m_groupToReference.remove(group);
|
||||
m_shareToGroup.remove(oldResolvedPath);
|
||||
m_fileWatchers.remove(oldResolvedPath);
|
||||
m_dirWatchers.remove(oldResolvedPath);
|
||||
|
||||
if (newReference.isValid()) {
|
||||
m_groupToReference[group] = newReference;
|
||||
|
|
@ -99,6 +102,8 @@ void ShareObserver::reinitialize()
|
|||
QMap<QString, QStringList> imported;
|
||||
QMap<QString, QStringList> exported;
|
||||
|
||||
const QDir baseDir = QFileInfo(m_db->filePath()).absoluteDir();
|
||||
|
||||
for (const auto& share : shares) {
|
||||
auto group = share.first;
|
||||
auto& reference = share.second;
|
||||
|
|
@ -109,10 +114,23 @@ void ShareObserver::reinitialize()
|
|||
|
||||
if (!reference.path.isEmpty() && reference.type != KeeShareSettings::Inactive) {
|
||||
const auto newResolvedPath = resolvePath(reference.path, m_db);
|
||||
auto fileWatcher = QSharedPointer<FileWatcher>::create(this);
|
||||
connect(fileWatcher.data(), &FileWatcher::fileChanged, this, &ShareObserver::handleFileUpdated);
|
||||
fileWatcher->start(newResolvedPath, FileWatchPeriod, FileWatchSize);
|
||||
m_fileWatchers.insert(newResolvedPath, fileWatcher);
|
||||
|
||||
if (reference.isPerDeviceMode(baseDir)) {
|
||||
// Per-device mode: watch the directory for changes
|
||||
auto dirWatcher = QSharedPointer<QFileSystemWatcher>::create();
|
||||
if (QDir(newResolvedPath).exists()) {
|
||||
dirWatcher->addPath(newResolvedPath);
|
||||
}
|
||||
connect(dirWatcher.data(), &QFileSystemWatcher::directoryChanged,
|
||||
this, &ShareObserver::handleDirectoryUpdated);
|
||||
m_dirWatchers.insert(newResolvedPath, dirWatcher);
|
||||
} else {
|
||||
// Classic mode: watch the individual file
|
||||
auto fileWatcher = QSharedPointer<FileWatcher>::create(this);
|
||||
connect(fileWatcher.data(), &FileWatcher::fileChanged, this, &ShareObserver::handleFileUpdated);
|
||||
fileWatcher->start(newResolvedPath, FileWatchPeriod, FileWatchSize);
|
||||
m_fileWatchers.insert(newResolvedPath, fileWatcher);
|
||||
}
|
||||
}
|
||||
if (reference.isExporting()) {
|
||||
exported[reference.path] << group->name();
|
||||
|
|
@ -121,21 +139,42 @@ void ShareObserver::reinitialize()
|
|||
|
||||
if (reference.isImporting()) {
|
||||
imported[reference.path] << group->name();
|
||||
// import has to occur immediately
|
||||
const auto result = this->importShare(reference.path);
|
||||
if (!result.isValid()) {
|
||||
// tolerable result - blocked import or missing source
|
||||
continue;
|
||||
}
|
||||
const auto resolvedDir = resolvePath(reference.path, m_db);
|
||||
|
||||
if (result.isError()) {
|
||||
error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
|
||||
} else if (result.isWarning()) {
|
||||
warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
|
||||
} else if (result.isInfo()) {
|
||||
success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message);
|
||||
if (reference.isPerDeviceMode(baseDir)) {
|
||||
// Per-device mode: import from all device files in the directory
|
||||
const auto results = importPerDeviceShares(resolvedDir, reference, group);
|
||||
for (const auto& result : results) {
|
||||
if (!result.isValid()) {
|
||||
continue;
|
||||
}
|
||||
if (result.isError()) {
|
||||
error << tr("Import from %1 failed (%2)").arg(result.path, result.message);
|
||||
} else if (result.isWarning()) {
|
||||
warning << tr("Import from %1 failed (%2)").arg(result.path, result.message);
|
||||
} else if (result.isInfo()) {
|
||||
success << tr("Import from %1 successful (%2)").arg(result.path, result.message);
|
||||
} else {
|
||||
success << tr("Imported from %1").arg(result.path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
success << tr("Imported from %1").arg(result.path);
|
||||
// Classic mode: import single file
|
||||
const auto result = this->importShare(reference.path);
|
||||
if (!result.isValid()) {
|
||||
// tolerable result - blocked import or missing source
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.isError()) {
|
||||
error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
|
||||
} else if (result.isWarning()) {
|
||||
warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
|
||||
} else if (result.isInfo()) {
|
||||
success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message);
|
||||
} else {
|
||||
success << tr("Imported from %1").arg(result.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -216,6 +255,87 @@ void ShareObserver::handleFileUpdated(const QString& path)
|
|||
}
|
||||
}
|
||||
|
||||
void ShareObserver::handleDirectoryUpdated(const QString& dirPath)
|
||||
{
|
||||
auto group = m_shareToGroup.value(dirPath);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
auto reference = KeeShare::referenceOf(group);
|
||||
const QDir handleBaseDir = QFileInfo(m_db->filePath()).absoluteDir();
|
||||
if (!reference.isImporting() || !reference.isPerDeviceMode(handleBaseDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-add the directory to the watcher (Qt removes it after notification)
|
||||
auto dirWatcher = m_dirWatchers.value(dirPath);
|
||||
if (dirWatcher && dirWatcher->directories().isEmpty()) {
|
||||
dirWatcher->addPath(dirPath);
|
||||
}
|
||||
|
||||
if (!m_inDirUpdate) {
|
||||
QTimer::singleShot(100, this, [this, dirPath] {
|
||||
auto shareGroup = m_shareToGroup.value(dirPath);
|
||||
if (!shareGroup) {
|
||||
m_inDirUpdate = false;
|
||||
return;
|
||||
}
|
||||
auto shareRef = KeeShare::referenceOf(shareGroup);
|
||||
auto results = importPerDeviceShares(dirPath, shareRef, shareGroup);
|
||||
m_inDirUpdate = false;
|
||||
|
||||
QStringList success;
|
||||
QStringList warning;
|
||||
QStringList error;
|
||||
for (const auto& result : results) {
|
||||
if (!result.isValid()) {
|
||||
continue;
|
||||
}
|
||||
if (result.isError()) {
|
||||
error << tr("Import from %1 failed (%2)").arg(result.path, result.message);
|
||||
} else if (result.isWarning()) {
|
||||
warning << tr("Import from %1 failed (%2)").arg(result.path, result.message);
|
||||
} else if (result.isInfo()) {
|
||||
success << tr("Import from %1 successful (%2)").arg(result.path, result.message);
|
||||
} else {
|
||||
success << tr("Imported from %1").arg(result.path);
|
||||
}
|
||||
}
|
||||
notifyAbout(success, warning, error);
|
||||
});
|
||||
m_inDirUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
QList<ShareObserver::Result> ShareObserver::importPerDeviceShares(
|
||||
const QString& resolvedDir,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Group* targetGroup)
|
||||
{
|
||||
QList<Result> results;
|
||||
if (!KeeShare::active().in) {
|
||||
return results;
|
||||
}
|
||||
|
||||
const QString ownFile = KeeShare::deviceId() + ".kdbx";
|
||||
QDir dir(resolvedDir);
|
||||
if (!dir.exists()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
const auto files = dir.entryList({"*.kdbx"}, QDir::Files, QDir::Name);
|
||||
for (const auto& fileName : files) {
|
||||
if (fileName.compare(ownFile, Qt::CaseInsensitive) == 0) {
|
||||
continue; // Skip own device's file
|
||||
}
|
||||
const auto filePath = dir.absoluteFilePath(fileName);
|
||||
auto result = ShareImport::containerInto(filePath, reference, targetGroup);
|
||||
result.path = filePath;
|
||||
results << result;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
ShareObserver::Result ShareObserver::importShare(const QString& path)
|
||||
{
|
||||
if (!KeeShare::active().in) {
|
||||
|
|
@ -283,19 +403,50 @@ QList<ShareObserver::Result> ShareObserver::exportShares()
|
|||
return results;
|
||||
}
|
||||
|
||||
const QDir exportBaseDir = QFileInfo(m_db->filePath()).absoluteDir();
|
||||
|
||||
for (auto it = references.cbegin(); it != references.cend(); ++it) {
|
||||
auto reference = it.value().first();
|
||||
const QString resolvedPath = resolvePath(reference.config.path, m_db);
|
||||
auto watcher = m_fileWatchers.value(resolvedPath);
|
||||
if (watcher) {
|
||||
watcher->stop();
|
||||
}
|
||||
|
||||
// TODO: save new path into group settings if not saving to signed container anymore
|
||||
results << ShareExport::intoContainer(resolvedPath, reference.config, reference.group);
|
||||
if (reference.config.isPerDeviceMode(exportBaseDir)) {
|
||||
// Per-device mode: export to {directory}/{DEVICE_ID}.kdbx
|
||||
QDir dir(resolvedPath);
|
||||
if (!dir.exists()) {
|
||||
if (!dir.mkpath(".")) {
|
||||
results << Result{resolvedPath,
|
||||
Result::Error,
|
||||
tr("Could not create directory %1").arg(resolvedPath)};
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const auto deviceFile = dir.absoluteFilePath(KeeShare::deviceId() + ".kdbx");
|
||||
|
||||
if (watcher) {
|
||||
watcher->start(resolvedPath, FileWatchPeriod, FileWatchSize);
|
||||
// Pause directory watcher during export
|
||||
auto dirWatcher = m_dirWatchers.value(resolvedPath);
|
||||
if (dirWatcher) {
|
||||
dirWatcher->removePath(resolvedPath);
|
||||
}
|
||||
|
||||
results << ShareExport::intoContainer(deviceFile, reference.config, reference.group);
|
||||
|
||||
// Resume directory watcher (also add path if it was newly created)
|
||||
if (dirWatcher) {
|
||||
dirWatcher->addPath(resolvedPath);
|
||||
}
|
||||
} else {
|
||||
// Classic mode: export to the file directly
|
||||
auto watcher = m_fileWatchers.value(resolvedPath);
|
||||
if (watcher) {
|
||||
watcher->stop();
|
||||
}
|
||||
|
||||
// TODO: save new path into group settings if not saving to signed container anymore
|
||||
results << ShareExport::intoContainer(resolvedPath, reference.config, reference.group);
|
||||
|
||||
if (watcher) {
|
||||
watcher->start(resolvedPath, FileWatchPeriod, FileWatchSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
#ifndef KEEPASSXC_SHAREOBSERVER_H
|
||||
#define KEEPASSXC_SHAREOBSERVER_H
|
||||
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
|
||||
|
|
@ -68,10 +69,14 @@ private slots:
|
|||
void handleDatabaseChanged();
|
||||
void handleDatabaseSaved();
|
||||
void handleFileUpdated(const QString& path);
|
||||
void handleDirectoryUpdated(const QString& dirPath);
|
||||
|
||||
private:
|
||||
Result importShare(const QString& path);
|
||||
QList<Result> exportShares();
|
||||
QList<Result> importPerDeviceShares(const QString& resolvedDir,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Group* targetGroup);
|
||||
|
||||
void deinitialize();
|
||||
void reinitialize();
|
||||
|
|
@ -82,7 +87,9 @@ private:
|
|||
QMap<QPointer<Group>, KeeShareSettings::Reference> m_groupToReference;
|
||||
QMap<QString, QPointer<Group>> m_shareToGroup;
|
||||
QMap<QString, QSharedPointer<FileWatcher>> m_fileWatchers;
|
||||
QMap<QString, QSharedPointer<QFileSystemWatcher>> m_dirWatchers;
|
||||
bool m_inFileUpdate = false;
|
||||
bool m_inDirUpdate = false;
|
||||
bool m_enabled = true;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
#include "keeshare/KeeShare.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QStandardPaths>
|
||||
|
||||
EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent)
|
||||
|
|
@ -110,20 +111,30 @@ void EditGroupWidgetKeeShare::updateSharingState()
|
|||
|
||||
// Custom message for active KeeShare reference
|
||||
const auto reference = KeeShare::referenceOf(m_temporaryGroup);
|
||||
const QDir uiBaseDir = QFileInfo(m_database->filePath()).absoluteDir();
|
||||
if (!reference.path.isEmpty()) {
|
||||
bool supported = false;
|
||||
for (const auto& extension : supportedExtensions) {
|
||||
if (reference.path.endsWith(extension, Qt::CaseInsensitive)) {
|
||||
supported = true;
|
||||
break;
|
||||
if (reference.isPerDeviceMode(uiBaseDir)) {
|
||||
// Per-device mode: path is a directory, show info message
|
||||
m_ui->messageWidget->showMessage(
|
||||
tr("Per-device sync mode: each device writes its own container in this directory.\n"
|
||||
"Device ID: %1").arg(KeeShare::deviceId()),
|
||||
MessageWidget::Information);
|
||||
} else {
|
||||
// Classic mode: validate file extension
|
||||
bool supported = false;
|
||||
for (const auto& extension : supportedExtensions) {
|
||||
if (reference.path.endsWith(extension, Qt::CaseInsensitive)) {
|
||||
supported = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!supported) {
|
||||
m_ui->messageWidget->showMessage(tr("Your KeePassXC version does not support sharing this container type.\n"
|
||||
"Supported extensions are: %1.")
|
||||
.arg(supportedExtensions.join(", ")),
|
||||
MessageWidget::Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!supported) {
|
||||
m_ui->messageWidget->showMessage(tr("Your KeePassXC version does not support sharing this container type.\n"
|
||||
"Supported extensions are: %1.")
|
||||
.arg(supportedExtensions.join(", ")),
|
||||
MessageWidget::Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto groups = m_database->rootGroup()->groupsRecursive(true);
|
||||
|
|
@ -140,8 +151,12 @@ void EditGroupWidgetKeeShare::updateSharingState()
|
|||
}
|
||||
multipleImport |= other.isImporting() && reference.isImporting();
|
||||
conflictExport |= other.isExporting() && reference.isExporting();
|
||||
cycleImportExport |=
|
||||
(other.isImporting() && reference.isExporting()) || (other.isExporting() && reference.isImporting());
|
||||
// In per-device mode, import+export to the same directory is expected
|
||||
// (export writes own device file, import reads other devices' files)
|
||||
if (!reference.isPerDeviceMode(uiBaseDir)) {
|
||||
cycleImportExport |=
|
||||
(other.isImporting() && reference.isExporting()) || (other.isExporting() && reference.isImporting());
|
||||
}
|
||||
}
|
||||
if (conflictExport) {
|
||||
m_ui->messageWidget->showMessage(tr("%1 is already being exported by this database.").arg(reference.path),
|
||||
|
|
@ -239,6 +254,23 @@ void EditGroupWidgetKeeShare::launchPathSelectionDialog()
|
|||
if (filename.isEmpty()) {
|
||||
filename = m_temporaryGroup->name();
|
||||
}
|
||||
|
||||
// For SynchronizeWith, offer both file and directory selection
|
||||
if (reference.type == KeeShareSettings::SynchronizeWith) {
|
||||
// Try directory selection first for per-device sync
|
||||
auto dirPath = fileDialog()->getExistingDirectory(
|
||||
this, tr("Select per-device sync directory"), defaultDirPath);
|
||||
if (!dirPath.isEmpty()) {
|
||||
// Directory selected: per-device mode
|
||||
m_ui->pathEdit->setText(dirPath);
|
||||
selectPath();
|
||||
FileDialog::saveLastDir("keeshare", dirPath);
|
||||
updateSharingState();
|
||||
return;
|
||||
}
|
||||
// User cancelled directory dialog; fall through to file dialog
|
||||
}
|
||||
|
||||
switch (reference.type) {
|
||||
case KeeShareSettings::ImportFrom:
|
||||
filename = fileDialog()->getOpenFileName(this, tr("Select import source"), defaultDirPath, filters);
|
||||
|
|
|
|||
|
|
@ -124,6 +124,9 @@
|
|||
<property name="accessibleName">
|
||||
<string>Path to share file field</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>File path for classic mode, or directory path for per-device sync</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ Polkit::Polkit()
|
|||
{
|
||||
PolkitSubject::registerMetaType();
|
||||
PolkitAuthorizationResults::registerMetaType();
|
||||
qDBusRegisterMetaType<QMap<QString, QString>>();
|
||||
|
||||
/* Note we explicitly use our own dbus path here, as the ::systemBus() method could be overridden
|
||||
through an environment variable to return an alternative bus path. This bus could have an application
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
#include "TestSharing.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QTest>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
|
|
@ -35,6 +36,14 @@ Q_DECLARE_METATYPE(KeeShareSettings::Certificate)
|
|||
void TestSharing::initTestCase()
|
||||
{
|
||||
QVERIFY(Crypto::init());
|
||||
m_tempDir = new QTemporaryDir();
|
||||
QVERIFY(m_tempDir->isValid());
|
||||
}
|
||||
|
||||
void TestSharing::cleanupTestCase()
|
||||
{
|
||||
delete m_tempDir;
|
||||
m_tempDir = nullptr;
|
||||
}
|
||||
|
||||
void TestSharing::testNullObjects()
|
||||
|
|
@ -175,6 +184,92 @@ void TestSharing::testSettingsSerialization_data()
|
|||
QTest::newRow("5") << false << false << certificate0 << key0;
|
||||
}
|
||||
|
||||
void TestSharing::testPerDeviceMode()
|
||||
{
|
||||
QFETCH(QString, path);
|
||||
QFETCH(QString, baseDir);
|
||||
QFETCH(bool, expectedPerDevice);
|
||||
|
||||
KeeShareSettings::Reference reference;
|
||||
reference.path = path;
|
||||
reference.type = KeeShareSettings::SynchronizeWith;
|
||||
|
||||
QCOMPARE(reference.isPerDeviceMode(QDir(baseDir)), expectedPerDevice);
|
||||
}
|
||||
|
||||
void TestSharing::testPerDeviceMode_data()
|
||||
{
|
||||
QTest::addColumn<QString>("path");
|
||||
QTest::addColumn<QString>("baseDir");
|
||||
QTest::addColumn<bool>("expectedPerDevice");
|
||||
|
||||
auto base = m_tempDir->path();
|
||||
|
||||
// Classic mode paths (file-based — don't need to exist on disk)
|
||||
QTest::newRow("kdbx file") << "/some/path/share.kdbx" << base << false;
|
||||
QTest::newRow("kdbx.share file") << "/some/path/share.kdbx.share" << base << false;
|
||||
QTest::newRow("KDBX uppercase") << "/some/path/share.KDBX" << base << false;
|
||||
QTest::newRow("KDBX.SHARE uppercase") << "/some/path/share.KDBX.SHARE" << base << false;
|
||||
QTest::newRow("empty path") << "" << base << false;
|
||||
QTest::newRow("nonexistent path") << "/nonexistent/path/nowhere" << base << false;
|
||||
|
||||
// Per-device mode paths (real directories on disk — absolute)
|
||||
QDir(base).mkpath("syncdir");
|
||||
QTest::newRow("directory path") << base + "/syncdir" << base << true;
|
||||
|
||||
QDir(base).mkpath("syncdir_slash");
|
||||
QTest::newRow("directory trailing slash") << base + "/syncdir_slash/" << base << true;
|
||||
|
||||
QDir(base).mkpath("sub/shared");
|
||||
QTest::newRow("nested directory") << base + "/sub/shared" << base << true;
|
||||
|
||||
QDir(base).mkpath("path.d/sync");
|
||||
QTest::newRow("directory with dots") << base + "/path.d/sync" << base << true;
|
||||
|
||||
// Per-device mode with relative path (regression test: must resolve against baseDir)
|
||||
QDir(base).mkpath("reldir");
|
||||
QTest::newRow("relative directory") << "reldir" << base << true;
|
||||
QTest::newRow("relative file") << "share.kdbx" << base << false;
|
||||
}
|
||||
|
||||
void TestSharing::testPerDeviceModeImportExport()
|
||||
{
|
||||
QFETCH(QString, path);
|
||||
QFETCH(int, type);
|
||||
QFETCH(bool, expectedImporting);
|
||||
QFETCH(bool, expectedExporting);
|
||||
|
||||
KeeShareSettings::Reference reference;
|
||||
reference.path = path;
|
||||
reference.type = static_cast<KeeShareSettings::Type>(type);
|
||||
|
||||
QCOMPARE(reference.isImporting(), expectedImporting);
|
||||
QCOMPARE(reference.isExporting(), expectedExporting);
|
||||
}
|
||||
|
||||
void TestSharing::testPerDeviceModeImportExport_data()
|
||||
{
|
||||
QTest::addColumn<QString>("path");
|
||||
QTest::addColumn<int>("type");
|
||||
QTest::addColumn<bool>("expectedImporting");
|
||||
QTest::addColumn<bool>("expectedExporting");
|
||||
|
||||
auto base = m_tempDir->path();
|
||||
QDir(base).mkpath("importexport");
|
||||
auto dir = base + "/importexport";
|
||||
|
||||
QTest::newRow("per-device sync imports")
|
||||
<< dir << int(KeeShareSettings::SynchronizeWith) << true << true;
|
||||
QTest::newRow("per-device import only")
|
||||
<< dir << int(KeeShareSettings::ImportFrom) << true << false;
|
||||
QTest::newRow("per-device export only")
|
||||
<< dir << int(KeeShareSettings::ExportTo) << false << true;
|
||||
QTest::newRow("classic file sync")
|
||||
<< dir + "/share.kdbx" << int(KeeShareSettings::SynchronizeWith) << true << true;
|
||||
QTest::newRow("inactive per-device")
|
||||
<< dir << int(KeeShareSettings::Inactive) << false << false;
|
||||
}
|
||||
|
||||
const QSharedPointer<Botan::RSA_PrivateKey> TestSharing::stubkey(int index)
|
||||
{
|
||||
static QMap<int, QSharedPointer<Botan::RSA_PrivateKey>> keys;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#define KEEPASSXC_TESTSHARING_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
namespace Botan
|
||||
{
|
||||
|
|
@ -30,15 +31,21 @@ class TestSharing : public QObject
|
|||
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void cleanupTestCase();
|
||||
void testNullObjects();
|
||||
void testKeySerialization();
|
||||
void testReferenceSerialization();
|
||||
void testReferenceSerialization_data();
|
||||
void testSettingsSerialization();
|
||||
void testSettingsSerialization_data();
|
||||
void testPerDeviceMode();
|
||||
void testPerDeviceMode_data();
|
||||
void testPerDeviceModeImportExport();
|
||||
void testPerDeviceModeImportExport_data();
|
||||
|
||||
private:
|
||||
const QSharedPointer<Botan::RSA_PrivateKey> stubkey(int index = 0);
|
||||
QTemporaryDir* m_tempDir = nullptr;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_TESTSHARING_H
|
||||
|
|
|
|||
Loading…
Reference in a new issue