mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-07-25 15:55:38 -04:00

* Remove QuaZip dependency in favor of minizip * Remove signature checks, but maintain signatures for backwards compatibility * Remove UI components related to certificates except for personal certificate for backwards compatibility * Default to unsigned containers (*.kdbx)
351 lines
11 KiB
C++
351 lines
11 KiB
C++
/*
|
|
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 or (at your option)
|
|
* version 3 of the License.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "ShareObserver.h"
|
|
#include "core/FileWatcher.h"
|
|
#include "core/Group.h"
|
|
#include "keeshare/KeeShare.h"
|
|
#include "keeshare/ShareExport.h"
|
|
#include "keeshare/ShareImport.h"
|
|
|
|
#include <QDir>
|
|
|
|
namespace
|
|
{
|
|
QString resolvePath(const QString& path, QSharedPointer<Database> database)
|
|
{
|
|
const QFileInfo info(database->filePath());
|
|
return info.absoluteDir().absoluteFilePath(path);
|
|
}
|
|
|
|
constexpr int FileWatchPeriod = 30;
|
|
constexpr int FileWatchSize = 5;
|
|
} // End Namespace
|
|
|
|
ShareObserver::ShareObserver(QSharedPointer<Database> db, QObject* parent)
|
|
: QObject(parent)
|
|
, m_db(std::move(db))
|
|
{
|
|
connect(KeeShare::instance(), &KeeShare::activeChanged, this, &ShareObserver::handleDatabaseChanged);
|
|
|
|
connect(m_db.data(), &Database::groupDataChanged, this, &ShareObserver::handleDatabaseChanged);
|
|
connect(m_db.data(), &Database::groupAdded, this, &ShareObserver::handleDatabaseChanged);
|
|
connect(m_db.data(), &Database::groupRemoved, this, &ShareObserver::handleDatabaseChanged);
|
|
|
|
connect(m_db.data(), &Database::modified, this, &ShareObserver::handleDatabaseChanged);
|
|
connect(m_db.data(), &Database::databaseSaved, this, &ShareObserver::handleDatabaseSaved);
|
|
|
|
handleDatabaseChanged();
|
|
}
|
|
|
|
ShareObserver::~ShareObserver()
|
|
{
|
|
}
|
|
|
|
void ShareObserver::deinitialize()
|
|
{
|
|
m_groupToReference.clear();
|
|
m_shareToGroup.clear();
|
|
m_fileWatchers.clear();
|
|
}
|
|
|
|
void ShareObserver::reinitialize()
|
|
{
|
|
QList<QPair<QPointer<Group>, KeeShareSettings::Reference>> shares;
|
|
for (Group* group : m_db->rootGroup()->groupsRecursive(true)) {
|
|
auto oldReference = m_groupToReference.value(group);
|
|
auto newReference = KeeShare::referenceOf(group);
|
|
if (oldReference == newReference) {
|
|
continue;
|
|
}
|
|
|
|
const auto oldResolvedPath = resolvePath(oldReference.path, m_db);
|
|
m_groupToReference.remove(group);
|
|
m_shareToGroup.remove(oldResolvedPath);
|
|
m_fileWatchers.remove(oldResolvedPath);
|
|
|
|
if (newReference.isValid()) {
|
|
m_groupToReference[group] = newReference;
|
|
const auto newResolvedPath = resolvePath(newReference.path, m_db);
|
|
m_shareToGroup[newResolvedPath] = group;
|
|
}
|
|
|
|
shares.append({group, newReference});
|
|
}
|
|
|
|
QStringList success;
|
|
QStringList warning;
|
|
QStringList error;
|
|
QMap<QString, QStringList> imported;
|
|
QMap<QString, QStringList> exported;
|
|
|
|
for (const auto& share : shares) {
|
|
auto group = share.first;
|
|
auto& reference = share.second;
|
|
// Check group validity, it may have been deleted by a merge action
|
|
if (!group) {
|
|
continue;
|
|
}
|
|
|
|
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.isExporting()) {
|
|
exported[reference.path] << group->name();
|
|
// export is only on save
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto it = imported.cbegin(); it != imported.cend(); ++it) {
|
|
if (it.value().count() > 1) {
|
|
warning << tr("Multiple import source path to %1 in %2").arg(it.key(), it.value().join(", "));
|
|
}
|
|
}
|
|
|
|
for (auto it = exported.cbegin(); it != exported.cend(); ++it) {
|
|
if (it.value().count() > 1) {
|
|
error << tr("Conflicting export target path %1 in %2").arg(it.key(), it.value().join(", "));
|
|
}
|
|
}
|
|
|
|
notifyAbout(success, warning, error);
|
|
}
|
|
|
|
void ShareObserver::notifyAbout(const QStringList& success, const QStringList& warning, const QStringList& error)
|
|
{
|
|
QStringList messages;
|
|
MessageWidget::MessageType type = MessageWidget::Positive;
|
|
if (!(success.isEmpty() || config()->get(Config::KeeShare_QuietSuccess).toBool())) {
|
|
messages += success;
|
|
}
|
|
if (!warning.isEmpty()) {
|
|
type = MessageWidget::Warning;
|
|
messages += warning;
|
|
}
|
|
if (!error.isEmpty()) {
|
|
type = MessageWidget::Error;
|
|
messages += error;
|
|
}
|
|
if (!messages.isEmpty()) {
|
|
emit sharingMessage(messages.join("\n"), type);
|
|
}
|
|
}
|
|
|
|
void ShareObserver::handleDatabaseChanged()
|
|
{
|
|
if (!m_db) {
|
|
Q_ASSERT(m_db);
|
|
return;
|
|
}
|
|
const auto active = KeeShare::active();
|
|
if (!active.out && !active.in) {
|
|
deinitialize();
|
|
} else {
|
|
reinitialize();
|
|
}
|
|
}
|
|
|
|
void ShareObserver::handleFileUpdated(const QString& path)
|
|
{
|
|
if (!m_inFileUpdate) {
|
|
QTimer::singleShot(100, this, [this, path] {
|
|
const Result result = importShare(path);
|
|
m_inFileUpdate = false;
|
|
if (!result.isValid()) {
|
|
return;
|
|
}
|
|
QStringList success;
|
|
QStringList warning;
|
|
QStringList error;
|
|
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_inFileUpdate = true;
|
|
}
|
|
}
|
|
|
|
ShareObserver::Result ShareObserver::importShare(const QString& path)
|
|
{
|
|
if (!KeeShare::active().in) {
|
|
return {};
|
|
}
|
|
const auto changePath = resolvePath(path, m_db);
|
|
auto shareGroup = m_shareToGroup.value(changePath);
|
|
if (!shareGroup) {
|
|
qWarning("Group for %s does not exist", qPrintable(path));
|
|
return {};
|
|
}
|
|
const auto reference = KeeShare::referenceOf(shareGroup);
|
|
if (reference.type == KeeShareSettings::Inactive) {
|
|
// changes of inactive references are ignored
|
|
return {};
|
|
}
|
|
if (reference.type == KeeShareSettings::ExportTo) {
|
|
// changes of export only references are ignored
|
|
return {};
|
|
}
|
|
|
|
Q_ASSERT(shareGroup->database() == m_db);
|
|
Q_ASSERT(shareGroup == m_db->rootGroup()->findGroupByUuid(shareGroup->uuid()));
|
|
const auto resolvedPath = resolvePath(reference.path, m_db);
|
|
return ShareImport::containerInto(resolvedPath, reference, shareGroup);
|
|
}
|
|
|
|
QSharedPointer<Database> ShareObserver::database()
|
|
{
|
|
return m_db;
|
|
}
|
|
|
|
QList<ShareObserver::Result> ShareObserver::exportShares()
|
|
{
|
|
QList<Result> results;
|
|
struct Reference
|
|
{
|
|
KeeShareSettings::Reference config;
|
|
const Group* group;
|
|
};
|
|
|
|
QMap<QString, QList<Reference>> references;
|
|
const auto groups = m_db->rootGroup()->groupsRecursive(true);
|
|
for (const auto* group : groups) {
|
|
const auto reference = KeeShare::referenceOf(group);
|
|
if (!reference.isExporting()) {
|
|
continue;
|
|
}
|
|
references[reference.path] << Reference{reference, group};
|
|
}
|
|
|
|
for (auto it = references.cbegin(); it != references.cend(); ++it) {
|
|
if (it.value().count() != 1) {
|
|
const auto path = it.value().first().config.path;
|
|
QStringList groupnames;
|
|
for (const auto& reference : it.value()) {
|
|
groupnames << reference.group->name();
|
|
}
|
|
results << Result{
|
|
path, Result::Error, tr("Conflicting export target path %1 in %2").arg(path, groupnames.join(", "))};
|
|
}
|
|
}
|
|
if (!results.isEmpty()) {
|
|
// We need to block export due to config
|
|
return results;
|
|
}
|
|
|
|
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 (watcher) {
|
|
watcher->start(resolvedPath, FileWatchPeriod, FileWatchSize);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
void ShareObserver::handleDatabaseSaved()
|
|
{
|
|
if (!KeeShare::active().out) {
|
|
return;
|
|
}
|
|
QStringList error;
|
|
QStringList warning;
|
|
QStringList success;
|
|
|
|
const auto results = exportShares();
|
|
for (const Result& result : results) {
|
|
if (!result.isValid()) {
|
|
Q_ASSERT(result.isValid());
|
|
continue;
|
|
}
|
|
if (result.isError()) {
|
|
error << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message);
|
|
} else if (result.isWarning()) {
|
|
warning << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message);
|
|
} else if (result.isInfo()) {
|
|
success << tr("Export to %1 successful (%2)").arg(result.path).arg(result.message);
|
|
} else {
|
|
success << tr("Export to %1").arg(result.path);
|
|
}
|
|
}
|
|
notifyAbout(success, warning, error);
|
|
}
|
|
|
|
ShareObserver::Result::Result(const QString& path, ShareObserver::Result::Type type, const QString& message)
|
|
: path(path)
|
|
, type(type)
|
|
, message(message)
|
|
{
|
|
}
|
|
|
|
bool ShareObserver::Result::isValid() const
|
|
{
|
|
return !path.isEmpty() || !message.isEmpty();
|
|
}
|
|
|
|
bool ShareObserver::Result::isError() const
|
|
{
|
|
return !message.isEmpty() && type == Error;
|
|
}
|
|
|
|
bool ShareObserver::Result::isInfo() const
|
|
{
|
|
return !message.isEmpty() && type == Info;
|
|
}
|
|
|
|
bool ShareObserver::Result::isWarning() const
|
|
{
|
|
return !message.isEmpty() && type == Warning;
|
|
}
|