2
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation, either version 2 or (at your option)
7
* version 3 of the License.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details.
14
* You should have received a copy of the GNU General Public License
15
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
#include "ShareObserver.h"
19
#include "core/FileWatcher.h"
20
#include "core/Group.h"
21
#include "keeshare/KeeShare.h"
22
#include "keeshare/ShareExport.h"
23
#include "keeshare/ShareImport.h"
29
QString resolvePath(const QString& path, QSharedPointer<Database> database)
31
const QFileInfo info(database->filePath());
32
return info.absoluteDir().absoluteFilePath(path);
35
constexpr int FileWatchPeriod = 30;
36
constexpr int FileWatchSize = 5;
39
ShareObserver::ShareObserver(QSharedPointer<Database> db, QObject* parent)
43
connect(KeeShare::instance(), &KeeShare::activeChanged, this, &ShareObserver::handleDatabaseChanged);
45
connect(m_db.data(), &Database::groupDataChanged, this, &ShareObserver::handleDatabaseChanged);
46
connect(m_db.data(), &Database::groupAdded, this, &ShareObserver::handleDatabaseChanged);
47
connect(m_db.data(), &Database::groupRemoved, this, &ShareObserver::handleDatabaseChanged);
49
connect(m_db.data(), &Database::modified, this, &ShareObserver::handleDatabaseChanged);
50
connect(m_db.data(), &Database::databaseSaved, this, &ShareObserver::handleDatabaseSaved);
52
handleDatabaseChanged();
55
ShareObserver::~ShareObserver()
57
m_db->disconnect(this);
60
void ShareObserver::deinitialize()
62
m_groupToReference.clear();
63
m_shareToGroup.clear();
64
m_fileWatchers.clear();
67
void ShareObserver::reinitialize()
69
QList<QPair<QPointer<Group>, KeeShareSettings::Reference>> shares;
70
for (Group* group : m_db->rootGroup()->groupsRecursive(true)) {
71
auto oldReference = m_groupToReference.value(group);
72
auto newReference = KeeShare::referenceOf(group);
73
if (oldReference == newReference) {
77
const auto oldResolvedPath = resolvePath(oldReference.path, m_db);
78
m_groupToReference.remove(group);
79
m_shareToGroup.remove(oldResolvedPath);
80
m_fileWatchers.remove(oldResolvedPath);
82
if (newReference.isValid()) {
83
m_groupToReference[group] = newReference;
84
const auto newResolvedPath = resolvePath(newReference.path, m_db);
85
m_shareToGroup[newResolvedPath] = group;
88
shares.append({group, newReference});
94
QMap<QString, QStringList> imported;
95
QMap<QString, QStringList> exported;
97
for (const auto& share : shares) {
98
auto group = share.first;
99
auto& reference = share.second;
100
// Check group validity, it may have been deleted by a merge action
105
if (!reference.path.isEmpty() && reference.type != KeeShareSettings::Inactive) {
106
const auto newResolvedPath = resolvePath(reference.path, m_db);
107
auto fileWatcher = QSharedPointer<FileWatcher>::create(this);
108
connect(fileWatcher.data(), &FileWatcher::fileChanged, this, &ShareObserver::handleFileUpdated);
109
fileWatcher->start(newResolvedPath, FileWatchPeriod, FileWatchSize);
110
m_fileWatchers.insert(newResolvedPath, fileWatcher);
112
if (reference.isExporting()) {
113
exported[reference.path] << group->name();
114
// export is only on save
117
if (reference.isImporting()) {
118
imported[reference.path] << group->name();
119
// import has to occur immediately
120
const auto result = this->importShare(reference.path);
121
if (!result.isValid()) {
122
// tolerable result - blocked import or missing source
126
if (result.isError()) {
127
error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
128
} else if (result.isWarning()) {
129
warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
130
} else if (result.isInfo()) {
131
success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message);
133
success << tr("Imported from %1").arg(result.path);
138
for (auto it = imported.cbegin(); it != imported.cend(); ++it) {
139
if (it.value().count() > 1) {
140
warning << tr("Multiple import source path to %1 in %2").arg(it.key(), it.value().join(", "));
144
for (auto it = exported.cbegin(); it != exported.cend(); ++it) {
145
if (it.value().count() > 1) {
146
error << tr("Conflicting export target path %1 in %2").arg(it.key(), it.value().join(", "));
150
notifyAbout(success, warning, error);
153
void ShareObserver::notifyAbout(const QStringList& success, const QStringList& warning, const QStringList& error)
155
QStringList messages;
156
MessageWidget::MessageType type = MessageWidget::Positive;
157
if (!(success.isEmpty() || config()->get(Config::KeeShare_QuietSuccess).toBool())) {
160
if (!warning.isEmpty()) {
161
type = MessageWidget::Warning;
164
if (!error.isEmpty()) {
165
type = MessageWidget::Error;
168
if (!messages.isEmpty()) {
169
emit sharingMessage(messages.join("\n"), type);
173
void ShareObserver::handleDatabaseChanged()
179
const auto active = KeeShare::active();
180
if (!active.out && !active.in) {
187
void ShareObserver::handleFileUpdated(const QString& path)
189
if (!m_inFileUpdate) {
190
QTimer::singleShot(100, this, [this, path] {
191
const Result result = importShare(path);
192
m_inFileUpdate = false;
193
if (!result.isValid()) {
199
if (result.isError()) {
200
error << tr("Import from %1 failed (%2)").arg(result.path, result.message);
201
} else if (result.isWarning()) {
202
warning << tr("Import from %1 failed (%2)").arg(result.path, result.message);
203
} else if (result.isInfo()) {
204
success << tr("Import from %1 successful (%2)").arg(result.path, result.message);
206
success << tr("Imported from %1").arg(result.path);
208
notifyAbout(success, warning, error);
210
m_inFileUpdate = true;
214
ShareObserver::Result ShareObserver::importShare(const QString& path)
216
if (!KeeShare::active().in) {
219
const auto changePath = resolvePath(path, m_db);
220
auto shareGroup = m_shareToGroup.value(changePath);
222
qWarning("Group for %s does not exist", qPrintable(path));
225
const auto reference = KeeShare::referenceOf(shareGroup);
226
if (reference.type == KeeShareSettings::Inactive) {
227
// changes of inactive references are ignored
230
if (reference.type == KeeShareSettings::ExportTo) {
231
// changes of export only references are ignored
235
Q_ASSERT(shareGroup->database() == m_db);
236
Q_ASSERT(shareGroup == m_db->rootGroup()->findGroupByUuid(shareGroup->uuid()));
237
const auto resolvedPath = resolvePath(reference.path, m_db);
238
return ShareImport::containerInto(resolvedPath, reference, shareGroup);
241
QSharedPointer<Database> ShareObserver::database()
246
QList<ShareObserver::Result> ShareObserver::exportShares()
248
QList<Result> results;
251
KeeShareSettings::Reference config;
255
QMap<QString, QList<Reference>> references;
256
const auto groups = m_db->rootGroup()->groupsRecursive(true);
257
for (const auto* group : groups) {
258
const auto reference = KeeShare::referenceOf(group);
259
if (!reference.isExporting()) {
262
references[reference.path] << Reference{reference, group};
265
for (auto it = references.cbegin(); it != references.cend(); ++it) {
266
if (it.value().count() != 1) {
267
const auto path = it.value().first().config.path;
268
QStringList groupnames;
269
for (const auto& reference : it.value()) {
270
groupnames << reference.group->name();
273
path, Result::Error, tr("Conflicting export target path %1 in %2").arg(path, groupnames.join(", "))};
276
if (!results.isEmpty()) {
277
// We need to block export due to config
281
for (auto it = references.cbegin(); it != references.cend(); ++it) {
282
auto reference = it.value().first();
283
const QString resolvedPath = resolvePath(reference.config.path, m_db);
284
auto watcher = m_fileWatchers.value(resolvedPath);
289
// TODO: save new path into group settings if not saving to signed container anymore
290
results << ShareExport::intoContainer(resolvedPath, reference.config, reference.group);
293
watcher->start(resolvedPath, FileWatchPeriod, FileWatchSize);
299
void ShareObserver::handleDatabaseSaved()
301
if (!KeeShare::active().out) {
308
const auto results = exportShares();
309
for (const Result& result : results) {
310
if (!result.isValid()) {
311
Q_ASSERT(result.isValid());
314
if (result.isError()) {
315
error << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message);
316
} else if (result.isWarning()) {
317
warning << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message);
318
} else if (result.isInfo()) {
319
success << tr("Export to %1 successful (%2)").arg(result.path).arg(result.message);
321
success << tr("Export to %1").arg(result.path);
324
notifyAbout(success, warning, error);
327
ShareObserver::Result::Result(const QString& path, ShareObserver::Result::Type type, const QString& message)
334
bool ShareObserver::Result::isValid() const
336
return !path.isEmpty() || !message.isEmpty();
339
bool ShareObserver::Result::isError() const
341
return !message.isEmpty() && type == Error;
344
bool ShareObserver::Result::isInfo() const
346
return !message.isEmpty() && type == Info;
349
bool ShareObserver::Result::isWarning() const
351
return !message.isEmpty() && type == Warning;