diff options
Diffstat (limited to 'src/m_key_server_sync')
| -rw-r--r-- | src/m_key_server_sync/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | src/m_key_server_sync/KeyServerBatchLogic.cpp | 99 | ||||
| -rw-r--r-- | src/m_key_server_sync/KeyServerBatchLogic.h | 79 | ||||
| -rw-r--r-- | src/m_key_server_sync/KeyServerSyncModule.cpp | 280 | ||||
| -rw-r--r-- | src/m_key_server_sync/module.json | 1 | ||||
| -rw-r--r-- | src/m_key_server_sync/test/CMakeLists.txt | 52 | ||||
| -rw-r--r-- | src/m_key_server_sync/test/KeyServerBatchLogicTest.cpp | 97 | ||||
| -rw-r--r-- | src/m_key_server_sync/ui/main.lua | 16 |
8 files changed, 610 insertions, 18 deletions
diff --git a/src/m_key_server_sync/CMakeLists.txt b/src/m_key_server_sync/CMakeLists.txt index df12fc1..5a1e767 100644 --- a/src/m_key_server_sync/CMakeLists.txt +++ b/src/m_key_server_sync/CMakeLists.txt @@ -29,3 +29,7 @@ gf_add_module( NAME key_server_sync QT Core Widgets Network LUA_SCRIPTS ui/main.lua) + +if(GPGFRONTEND_MODULES_BUILD_TESTS) + add_subdirectory(test) +endif() diff --git a/src/m_key_server_sync/KeyServerBatchLogic.cpp b/src/m_key_server_sync/KeyServerBatchLogic.cpp new file mode 100644 index 0000000..c67f65e --- /dev/null +++ b/src/m_key_server_sync/KeyServerBatchLogic.cpp @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2021-2024 Saturneric <[email protected]> + * + * This file is part of GpgFrontend. + * + * GpgFrontend 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 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>. + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric <[email protected]> starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include "KeyServerBatchLogic.h" + +#include <QCoreApplication> +#include <QSet> + +namespace KeyServerBatchLogic { + +namespace { + +/// The failures, one per line, under the headline. +auto WithFailures(QString headline, const QStringList& failures) -> QString { + if (failures.isEmpty()) return headline; + headline += QStringLiteral("\n\n"); + headline += QCoreApplication::translate("GTrC", "Failed:"); + for (const auto& f : failures) headline += QStringLiteral("\n") + f; + return headline; +} + +} // namespace + +auto Normalise(const std::optional<BatchKey>& key, + const std::optional<QList<BatchKey>>& keys) -> QList<BatchKey> { + QList<BatchKey> all; + if (key.has_value()) all.append(*key); + if (keys.has_value()) all.append(*keys); + + QList<BatchKey> out; + QSet<QString> seen; + for (const auto& k : all) { + const auto fpr = k.fingerprint.trimmed().toUpper(); + if (fpr.isEmpty() || seen.contains(fpr)) continue; + seen.insert(fpr); + out.append(k); + } + return out; +} + +auto JoinForImport(const QList<QByteArray>& blocks) -> QByteArray { + QByteArray out; + for (const auto& b : blocks) { + if (b.trimmed().isEmpty()) continue; + out.append(b); + if (!b.endsWith('\n')) out.append('\n'); + } + return out; +} + +auto RefreshSummary(int fetched, int total, const QStringList& failures) + -> QString { + return WithFailures( + QCoreApplication::translate("GTrC", + "%1 of %2 keys were fetched from the key " + "server.") + .arg(fetched) + .arg(total), + failures); +} + +auto PublishSummary(int published, int total, const QString& host, + const QStringList& failures) -> QString { + return WithFailures( + QCoreApplication::translate("GTrC", + "%1 of %2 keys were published to the key " + "server %3.") + .arg(published) + .arg(total) + .arg(host), + failures); +} + +} // namespace KeyServerBatchLogic diff --git a/src/m_key_server_sync/KeyServerBatchLogic.h b/src/m_key_server_sync/KeyServerBatchLogic.h new file mode 100644 index 0000000..439ef87 --- /dev/null +++ b/src/m_key_server_sync/KeyServerBatchLogic.h @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2021-2024 Saturneric <[email protected]> + * + * This file is part of GpgFrontend. + * + * GpgFrontend 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 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>. + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric <[email protected]> starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#pragma once + +#include <QByteArray> +#include <QList> +#include <QString> +#include <QStringList> +#include <optional> + +/** + * @file KeyServerBatchLogic.h + * @brief The decisions a batch of key-server operations makes, without the + * network or the SDK, so they can be tested on their own. + */ +namespace KeyServerBatchLogic { + +/// One key a batch acts on: the part of the Host's key reference it needs. +struct BatchKey { + qint64 channel = 0; + QString key_id; + QString fingerprint; +}; + +/** + * @brief The keys a command was given, as one list. + * + * A command is given either one key (the key details dialog) or a selection + * (a key list). Both together are merged; a key named twice, by fingerprint, + * is acted on once, in the order it was first named. Keys without a + * fingerprint are dropped: there is nothing to fetch or publish them by. + */ +auto Normalise(const std::optional<BatchKey>& key, + const std::optional<QList<BatchKey>>& keys) -> QList<BatchKey>; + +/** + * @brief Several fetched key blocks as one import payload. + * + * One import means one dialog for the whole batch. Each block ends in a + * newline so the next one's armor header starts on a line of its own; empty + * blocks are skipped. + */ +auto JoinForImport(const QList<QByteArray>& blocks) -> QByteArray; + +/// What a finished refresh says, e.g. "3 of 4 keys fetched" plus failures. +auto RefreshSummary(int fetched, int total, const QStringList& failures) + -> QString; + +/// What a finished publish says, e.g. "2 of 2 keys published" plus failures. +auto PublishSummary(int published, int total, const QString& host, + const QStringList& failures) -> QString; + +} // namespace KeyServerBatchLogic diff --git a/src/m_key_server_sync/KeyServerSyncModule.cpp b/src/m_key_server_sync/KeyServerSyncModule.cpp index a8f0e21..0c4122b 100644 --- a/src/m_key_server_sync/KeyServerSyncModule.cpp +++ b/src/m_key_server_sync/KeyServerSyncModule.cpp @@ -38,6 +38,7 @@ #include "GFModuleIdentity.h" #include "GFSDKHostCommands.hpp" #include "GFSDKUI.h" +#include "KeyServerBatchLogic.h" #include "KeyServerList.h" #include "KeyServerSettingsPage.h" #include "PKSInterface.h" @@ -83,17 +84,19 @@ using ErrorCallback = std::function<void(const QString&, const QString&)>; * * @param by_fingerprint VKS has separate endpoints for the two handle kinds; * HKP does not care. + * @param owner when given, owns the request: deleting it aborts the request + * and guarantees neither callback runs. */ void FetchKey(const KeyServerList::Route& route, const QString& handle, bool by_fingerprint, const KeyCallback& on_key, - const ErrorCallback& on_error) { + const ErrorCallback& on_error, QObject* owner = nullptr) { + QObject* context = + owner != nullptr ? owner : static_cast<QObject*>(QThread::currentThread()); if (route.vks) { - auto* vks = new VKSInterface(route.url); - QObject::connect(vks, &VKSInterface::SignalKeyRetrieved, - QThread::currentThread(), + auto* vks = new VKSInterface(route.url, owner); + QObject::connect(vks, &VKSInterface::SignalKeyRetrieved, context, [on_key](const QString& key) { on_key(key); }); - QObject::connect(vks, &VKSInterface::SignalErrorOccurred, - QThread::currentThread(), + QObject::connect(vks, &VKSInterface::SignalErrorOccurred, context, [on_error](const QString& error, const QString& data) { on_error(error, data); }); @@ -110,10 +113,9 @@ void FetchKey(const KeyServerList::Route& route, const QString& handle, return; } - auto* pks = new PKSInterface(); + auto* pks = new PKSInterface(owner); QObject::connect( - pks, &PKSInterface::SignalKeyServerKeyLookupResult, - QThread::currentThread(), + pks, &PKSInterface::SignalKeyServerKeyLookupResult, context, [on_key, on_error](QNetworkReply::NetworkError error, const QString& error_string, const QByteArray& data) { if (error != QNetworkReply::NoError) { @@ -163,10 +165,175 @@ auto ConfirmHkpPublish(const QString& url) -> bool { QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Ok; } + +using KeyServerBatchLogic::BatchKey; + +/// Set when the module deactivates; every batch step checks it first. +std::atomic<bool> g_stopped{false}; + +/** + * @brief Refreshes or publishes several keys, one request at a time. + * + * Owns its requests: deleting it aborts them, and no callback runs after. + * Spaced out so a large selection does not hammer the server, and finished + * with one import (one dialog) and one summary rather than one per key. + */ +class KeyServerBatch : public QObject { + public: + enum class Kind { kRefresh, kPublish }; + + /// Between two requests of one batch. + static constexpr int kSpacingMs = 200; + + KeyServerBatch(Kind kind, QList<BatchKey> keys) + : kind_(kind), keys_(std::move(keys)), + route_(KeyServerList::SyncRoute()) {} + + /// @return false when the user declined, and the batch is already gone + auto Start() -> bool { + if (kind_ == Kind::kPublish && !route_.vks && + !ConfirmHkpPublish(route_.url)) { + deleteLater(); + return false; + } + Next(); + return true; + } + + private: + void Next() { + if (g_stopped.load()) return; + if (index_ >= keys_.size()) return Finish(); + const auto key = keys_[index_++]; + if (kind_ == Kind::kRefresh) { + FetchKey( + route_, key.fingerprint, true, + [this](const QString& data) { + blocks_.append(data.toUtf8()); + Schedule(); + }, + [this, key](const QString& error, const QString&) { + failures_.append(QStringLiteral("%1: %2").arg(key.fingerprint, + error)); + Schedule(); + }, + this); + return; + } + Publish(key); + } + + void Publish(const BatchKey& key) { + const auto exported = + gf::sdk::ExportKey(GFModuleSdkContext(), + static_cast<int>(key.channel), key.key_id, true); + if (exported.isEmpty()) { + failures_.append(QCoreApplication::translate( + "GTrC", "%1: the public key could not be exported") + .arg(key.fingerprint)); + return Schedule(); + } + const auto ok = [this]() { + ++done_; + Schedule(); + }; + const auto failed = [this, key](const QString& error) { + failures_.append(QStringLiteral("%1: %2").arg(key.fingerprint, error)); + Schedule(); + }; + if (route_.vks) { + auto* vks = new VKSInterface(route_.url, this); + connect(vks, &VKSInterface::SignalKeyUploaded, this, + [ok](const QString&, const QJsonObject&, const QString&) { + ok(); + }); + connect(vks, &VKSInterface::SignalErrorOccurred, this, + [failed](const QString& error, const QString&) { + failed(error); + }); + vks->UploadKey(QString::fromUtf8(exported)); + return; + } + auto* pks = new PKSInterface(this); + connect(pks, &PKSInterface::SignalKeyServerKeyUploadResult, this, + [ok, failed](QNetworkReply::NetworkError error, + const QString& error_string) { + if (error != QNetworkReply::NoError) return failed(error_string); + ok(); + }); + pks->UploadKey(route_.url, exported); + } + + void Schedule() { + if (g_stopped.load()) return; + QTimer::singleShot(kSpacingMs, this, [this]() { Next(); }); + } + + void Finish() { + const auto total = static_cast<int>(keys_.size()); + const auto severity = + failures_.isEmpty() ? Severity::kInfo : Severity::kWarning; + if (kind_ == Kind::kRefresh) { + if (!blocks_.isEmpty()) { + gf::sdk::ImportKeys(GFModuleSdkContext(), + static_cast<int>(keys_.front().channel), + KeyServerBatchLogic::JoinForImport(blocks_)); + } + Tell(severity, + QCoreApplication::translate("GTrC", "Key Refresh Finished"), + KeyServerBatchLogic::RefreshSummary( + static_cast<int>(blocks_.size()), total, failures_)); + } else { + Tell(severity, + QCoreApplication::translate("GTrC", "Key Publishing Finished"), + KeyServerBatchLogic::PublishSummary(done_, total, + QUrl(route_.url).host(), + failures_)); + } + deleteLater(); + } + + Kind kind_; + QList<BatchKey> keys_; + KeyServerList::Route route_; + qsizetype index_ = 0; + int done_ = 0; + QList<QByteArray> blocks_; + QStringList failures_; +}; + +/// The one batch that may run; a second request waits for it to finish. +QPointer<KeyServerBatch> g_batch; + +void StartBatch(KeyServerBatch::Kind kind, QList<BatchKey> keys) { + if (!g_batch.isNull()) { + Tell(Severity::kInfo, QCoreApplication::translate("GTrC", "Key Server"), + QCoreApplication::translate( + "GTrC", "A key server operation is already running. Try again " + "when it has finished.")); + return; + } + auto* batch = new KeyServerBatch(kind, std::move(keys)); + g_batch = batch; + batch->Start(); +} + } // namespace +auto OnDeactivate() -> GFResult { + // No batch step runs from here on; the batch and its requests go with the + // event loop's next turn, on the thread that owns them. + g_stopped.store(true); + if (!g_batch.isNull()) { + QMetaObject::invokeMethod(g_batch.data(), &QObject::deleteLater, + Qt::QueuedConnection); + } + return GFResult::Ok(); +} + auto OnActivate() -> GFResult { LOG_INFO("key server sync module registering"); + g_stopped.store(false); // Presentation is registered untranslated: the module translators are not // installed yet, so anything translated here would be stuck at the source @@ -340,14 +507,33 @@ auto UpdateKeyFromKeyServer(int channel, const QString& fpr) -> int { namespace { -/// Both key commands act on one key, named by the Host's context. +/// The key commands act on one key (the key details dialog) or on a +/// selection (a key list's context menu), named by the Host's context. struct KeyArgs { - gf::cmd::KeyRef key; + std::optional<gf::cmd::KeyRef> key; + std::optional<QList<gf::cmd::KeyRef>> keys; static constexpr auto Fields() { - return std::make_tuple(gf::cmd::F("key", &KeyArgs::key)); + return std::make_tuple(gf::cmd::F("key", &KeyArgs::key), + gf::cmd::F("keys", &KeyArgs::keys)); } }; +auto ToBatchKey(const gf::cmd::KeyRef& k) -> BatchKey { + return {k.channel, k.key_id, k.fingerprint}; +} + +/// Every key the arguments name, once each. +auto KeysOf(const KeyArgs& a) -> QList<BatchKey> { + std::optional<BatchKey> one; + if (a.key.has_value()) one = ToBatchKey(*a.key); + std::optional<QList<BatchKey>> many; + if (a.keys.has_value()) { + many.emplace(); + for (const auto& k : *a.keys) many->append(ToBatchKey(k)); + } + return KeyServerBatchLogic::Normalise(one, many); +} + struct PublishKey { static constexpr gf::cmd::Meta kMeta{ GF_MODULE_ID ".publish_key", GC_TR("Publish Public Key to Key Server"), @@ -366,6 +552,16 @@ struct RefreshKey { using Result = gf::cmd::Unit; }; +struct CheckPublication { + static constexpr gf::cmd::Meta kMeta{ + GF_MODULE_ID ".check_publication", + GC_TR("Check Publication Status"), + GC_TR("Ask the key server whether it has this public key"), + GC_TR("Key Server Operations"), 0, gf::cmd::kNeedsGuiThread}; + using Args = KeyArgs; + using Result = gf::cmd::Unit; +}; + struct SearchKey { static constexpr gf::cmd::Meta kMeta{ GF_MODULE_ID ".search_key", GC_TR("Key Server"), @@ -380,11 +576,23 @@ struct SearchKey { using Result = gf::cmd::Unit; }; +auto NoKey() -> gf::cmd::Outcome<gf::cmd::Unit> { + return gf::cmd::Outcome<gf::cmd::Unit>::Failure(GF_CMD_E_BAD_ARGS, + "no key was given"); +} + auto DoPublishKey(const gf::cmd::CommandContext& /*ctx*/, const KeyArgs& a) -> gf::cmd::Outcome<gf::cmd::Unit> { - // Any key with a public part can be published, mirroring the Key - // Management "Publish Key to Keyserver" action. - UploadKeyToServer(static_cast<int>(a.key.channel), a.key.key_id); + // Any key with a public part can be published. One key keeps the detailed + // per-key report; several go as one batch with one summary. + const auto keys = KeysOf(a); + if (keys.isEmpty()) return NoKey(); + if (keys.size() == 1) { + UploadKeyToServer(static_cast<int>(keys.front().channel), + keys.front().key_id); + } else { + StartBatch(KeyServerBatch::Kind::kPublish, keys); + } return gf::cmd::Outcome<gf::cmd::Unit>::Success({}); } @@ -392,7 +600,42 @@ auto DoRefreshKey(const gf::cmd::CommandContext& /*ctx*/, const KeyArgs& a) -> gf::cmd::Outcome<gf::cmd::Unit> { // Refresh re-imports the latest public key from the server; it is valid // for any key, including your own. - UpdateKeyFromKeyServer(static_cast<int>(a.key.channel), a.key.fingerprint); + const auto keys = KeysOf(a); + if (keys.isEmpty()) return NoKey(); + if (keys.size() == 1) { + UpdateKeyFromKeyServer(static_cast<int>(keys.front().channel), + keys.front().fingerprint); + } else { + StartBatch(KeyServerBatch::Kind::kRefresh, keys); + } + return gf::cmd::Outcome<gf::cmd::Unit>::Success({}); +} + +auto DoCheckPublication(const gf::cmd::CommandContext& /*ctx*/, + const KeyArgs& a) -> gf::cmd::Outcome<gf::cmd::Unit> { + // Asked for, never automatic: looking a key up tells the server which key + // the user is interested in. + const auto keys = KeysOf(a); + if (keys.isEmpty()) return NoKey(); + const auto fpr = keys.front().fingerprint; + const auto route = KeyServerList::SyncRoute(); + const auto host = QUrl(route.url).host(); + const auto title = + QCoreApplication::translate("GTrC", "Publication Status"); + FetchKey( + route, fpr, true, + [title, host](const QString&) { + Tell(Severity::kInfo, title, + QCoreApplication::translate( + "GTrC", "The public key has been published on %1.") + .arg(host)); + }, + [title, host](const QString& error, const QString&) { + Tell(Severity::kInfo, title, + QCoreApplication::translate( + "GTrC", "%1 did not return this public key.\n%2") + .arg(host, error)); + }); return gf::cmd::Outcome<gf::cmd::Unit>::Success({}); } @@ -539,9 +782,10 @@ constexpr std::array<GFEventBinding, 4> kEvents = {{ {"REQUEST_UPLOAD_PUBLIC_KEY", &OnRequestUploadPublicKey}, }}; -const std::array<gf::cmd::Binding, 3> kCommands = { +const std::array<gf::cmd::Binding, 4> kCommands = { gf::cmd::Bind<PublishKey, &DoPublishKey>(), gf::cmd::Bind<RefreshKey, &DoRefreshKey>(), + gf::cmd::Bind<CheckPublication, &DoCheckPublication>(), gf::cmd::Bind<SearchKey, &DoSearchKey>(), }; @@ -551,7 +795,7 @@ const GFModuleHooks kHooks = { GF_MODULE_VERSION, GF_MODULE_TRANSLATION_CONTEXT, &OnActivate, - nullptr, // the Host withdraws the commands, widgets and script itself + &OnDeactivate, // stops a running batch; the Host withdraws the rest &OnUnload, kEvents.data(), kEvents.size(), diff --git a/src/m_key_server_sync/module.json b/src/m_key_server_sync/module.json index c02aff7..02cbd6f 100644 --- a/src/m_key_server_sync/module.json +++ b/src/m_key_server_sync/module.json @@ -19,6 +19,7 @@ "REQUEST_UPLOAD_PUBLIC_KEY" ], "commands": [ + "com.bktus.gpgfrontend.module.key_server_sync.check_publication", "com.bktus.gpgfrontend.module.key_server_sync.publish_key", "com.bktus.gpgfrontend.module.key_server_sync.refresh_key", "com.bktus.gpgfrontend.module.key_server_sync.search_key" diff --git a/src/m_key_server_sync/test/CMakeLists.txt b/src/m_key_server_sync/test/CMakeLists.txt new file mode 100644 index 0000000..898a1f9 --- /dev/null +++ b/src/m_key_server_sync/test/CMakeLists.txt @@ -0,0 +1,52 @@ +# Copyright (C) 2021-2024 Saturneric <[email protected]> +# +# This file is part of GpgFrontend. +# +# GpgFrontend 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 3 of the License, or +# (at your option) any later version. +# +# GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>. +# +# The initial version of the source code is inherited from +# the gpg4usb project, which is under GPL-3.0-or-later. +# +# All the source code of GpgFrontend was modified and released by +# Saturneric <[email protected]> starting on May 12, 2021. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# Unit tests for the SDK-free half of the key server batch. +# +# KeyServerBatchLogic.cpp carries no GF SDK symbol, no widget and no network, +# so it compiles straight into a plain gtest binary with nothing but Qt Core +# behind it. Keep it that way, or this target stops linking. + +if(NOT TARGET GTest::gtest) + find_package(GTest QUIET) +endif() + +if(NOT TARGET GTest::gtest) + message(STATUS "GTest not found; skipping key server sync module tests") + return() +endif() + +add_executable(gf_mod_key_server_sync_test + KeyServerBatchLogicTest.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../KeyServerBatchLogic.cpp) + +target_include_directories(gf_mod_key_server_sync_test PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/..") + +target_link_libraries(gf_mod_key_server_sync_test PRIVATE + GTest::gtest GTest::gtest_main Qt::Core) + +set_target_properties(gf_mod_key_server_sync_test PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${GPGFRONTEND_MODULE_TEST_OUTPUT_DIR}") diff --git a/src/m_key_server_sync/test/KeyServerBatchLogicTest.cpp b/src/m_key_server_sync/test/KeyServerBatchLogicTest.cpp new file mode 100644 index 0000000..2404b14 --- /dev/null +++ b/src/m_key_server_sync/test/KeyServerBatchLogicTest.cpp @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2021-2024 Saturneric <[email protected]> + * + * This file is part of GpgFrontend. + * + * GpgFrontend 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 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>. + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric <[email protected]> starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include <gtest/gtest.h> + +#include "KeyServerBatchLogic.h" + +using KeyServerBatchLogic::BatchKey; + +namespace { + +auto Key(const QString& fpr, qint64 channel = 0) -> BatchKey { + return {channel, fpr.right(16), fpr}; +} + +} // namespace + +TEST(KeyServerBatchLogicTest, OneKeyOrASelectionBecomeOneList) { + const auto one = KeyServerBatchLogic::Normalise(Key("AAAA"), std::nullopt); + ASSERT_EQ(one.size(), 1); + EXPECT_EQ(one.front().fingerprint, "AAAA"); + + const auto many = KeyServerBatchLogic::Normalise( + std::nullopt, QList<BatchKey>{Key("AAAA"), Key("BBBB")}); + ASSERT_EQ(many.size(), 2); + EXPECT_EQ(many[1].fingerprint, "BBBB"); + + EXPECT_TRUE( + KeyServerBatchLogic::Normalise(std::nullopt, std::nullopt).isEmpty()); +} + +TEST(KeyServerBatchLogicTest, AKeyNamedTwiceIsActedOnOnceInFirstOrder) { + // Case and surrounding space do not make a fingerprint a different key. + const auto keys = KeyServerBatchLogic::Normalise( + Key("bbbb"), QList<BatchKey>{Key("AAAA"), Key(" BBBB "), Key("AAAA")}); + ASSERT_EQ(keys.size(), 2); + EXPECT_EQ(keys[0].fingerprint, "bbbb"); + EXPECT_EQ(keys[1].fingerprint, "AAAA"); +} + +TEST(KeyServerBatchLogicTest, AKeyWithoutAFingerprintIsDropped) { + const auto keys = KeyServerBatchLogic::Normalise( + std::nullopt, QList<BatchKey>{Key(""), Key(" "), Key("CCCC")}); + ASSERT_EQ(keys.size(), 1); + EXPECT_EQ(keys.front().fingerprint, "CCCC"); +} + +TEST(KeyServerBatchLogicTest, BlocksJoinIntoOneImportOnSeparateLines) { + const QByteArray a = "-----BEGIN PGP PUBLIC KEY BLOCK-----\nA\n" + "-----END PGP PUBLIC KEY BLOCK-----"; + const QByteArray b = "-----BEGIN PGP PUBLIC KEY BLOCK-----\nB\n" + "-----END PGP PUBLIC KEY BLOCK-----\n"; + const auto joined = KeyServerBatchLogic::JoinForImport({a, "", " \n", b}); + EXPECT_EQ(joined, a + "\n" + b); + EXPECT_TRUE(KeyServerBatchLogic::JoinForImport({}).isEmpty()); +} + +TEST(KeyServerBatchLogicTest, SummariesCountAndListFailures) { + const auto clean = KeyServerBatchLogic::RefreshSummary(3, 3, {}); + EXPECT_TRUE(clean.contains("3 of 3")); + EXPECT_FALSE(clean.contains("Failed")); + + const auto partial = + KeyServerBatchLogic::RefreshSummary(1, 2, {"BBBB: not found"}); + EXPECT_TRUE(partial.contains("1 of 2")); + EXPECT_TRUE(partial.contains("BBBB: not found")); + + const auto published = KeyServerBatchLogic::PublishSummary( + 2, 2, "keys.openpgp.org", {}); + EXPECT_TRUE(published.contains("2 of 2")); + EXPECT_TRUE(published.contains("keys.openpgp.org")); +} diff --git a/src/m_key_server_sync/ui/main.lua b/src/m_key_server_sync/ui/main.lua index 2ad2a01..5c9c1e9 100644 --- a/src/m_key_server_sync/ui/main.lua +++ b/src/m_key_server_sync/ui/main.lua @@ -6,6 +6,7 @@ local search = commands.get("com.bktus.gpgfrontend.module.key_server_sync.search_key") local publish = commands.get("com.bktus.gpgfrontend.module.key_server_sync.publish_key") local refresh = commands.get("com.bktus.gpgfrontend.module.key_server_sync.refresh_key") +local check = commands.get("com.bktus.gpgfrontend.module.key_server_sync.check_publication") ui.mount { id = "search", anchor = ui.anchor.dialog {}, widget = native.widget("search") } @@ -33,3 +34,18 @@ ui.action { id = "publish", anchor = ui.anchor("key.details.actions"), command = publish, update = for_this_key } ui.action { id = "refresh", anchor = ui.anchor("key.details.actions"), command = refresh, update = for_this_key } +ui.action { id = "check", anchor = ui.anchor("key.details.actions"), + command = check, update = for_this_key } + +-- The same operations on whatever a key list has selected: one key or many, +-- sent as one batch. +local function for_the_selection(ctx) + local keys = ctx.keys + if not keys or #keys == 0 then return { visible = false } end + return { args = { keys = keys } } +end + +ui.action { id = "publish_selected", anchor = ui.anchor("key.list.context"), + command = publish, update = for_the_selection } +ui.action { id = "refresh_selected", anchor = ui.anchor("key.list.context"), + command = refresh, update = for_the_selection } |
