/* * Copyright (C) 2018 KeePassXC Team * * 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 . */ #include "ShareObserver.h" #include "core/FileWatcher.h" #include "core/Group.h" #include "keeshare/KeeShare.h" #include "keeshare/ShareExport.h" #include "keeshare/ShareImport.h" #include namespace { QString resolvePath(const QString& path, QSharedPointer database) { const QFileInfo info(database->filePath()); return info.absoluteDir().absoluteFilePath(path); } constexpr int FileWatchPeriod = 30; constexpr int FileWatchSize = 5; } // End Namespace ShareObserver::ShareObserver(QSharedPointer 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, 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 imported; QMap 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::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 ShareObserver::database() { return m_db; } QList ShareObserver::exportShares() { QList results; struct Reference { KeeShareSettings::Reference config; const Group* group; }; QMap> 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; }