aboutsummaryrefslogtreecommitdiffstats
path: root/src/m_key_server_sync/KeyServerSettingsPage.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/m_key_server_sync/KeyServerSettingsPage.cpp')
-rw-r--r--src/m_key_server_sync/KeyServerSettingsPage.cpp288
1 files changed, 288 insertions, 0 deletions
diff --git a/src/m_key_server_sync/KeyServerSettingsPage.cpp b/src/m_key_server_sync/KeyServerSettingsPage.cpp
new file mode 100644
index 0000000..a01b405
--- /dev/null
+++ b/src/m_key_server_sync/KeyServerSettingsPage.cpp
@@ -0,0 +1,288 @@
+/**
+ * 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 "KeyServerSettingsPage.h"
+
+#include <QMessageBox>
+#include <QPointer>
+#include <QTableWidget>
+#include <QUrl>
+
+#include "GFModuleCommonUtils.hpp"
+
+//
+#include "ui_KeyServerSettingsPage.h"
+
+namespace {
+
+constexpr int kColumnDefault = 0;
+constexpr int kColumnAddress = 1;
+constexpr int kColumnHkp = 2;
+constexpr int kColumnVks = 3;
+constexpr int kColumnStatus = 4;
+constexpr int kColumnLastTested = 5;
+constexpr int kColumnCount = 6;
+
+} // namespace
+
+KeyServerSettingsPage::KeyServerSettingsPage(QWidget* parent)
+ : QWidget(parent),
+ ui_(SecureCreateSharedObject<Ui_KeyServerSettingsPage>()) {
+ ui_->setupUi(this);
+
+ ui_->keyServerListGroupBox->setTitle(tr("Key Server List"));
+ ui_->addGroupBox->setTitle(tr("Add a Key Server"));
+ ui_->operationsGroupBox->setTitle(tr("Operations"));
+
+ ui_->addKeyServerButton->setText(tr("Add"));
+ ui_->setDefaultButton->setText(tr("Set As Default"));
+ ui_->testButton->setText(tr("Test Selected"));
+ ui_->removeButton->setText(tr("Delete Selected"));
+
+ ui_->addKeyServerEdit->setPlaceholderText(tr("https://keys.example.org"));
+ ui_->tipsLabel->setText(tr(
+ "A new key server is tested against the HKP and VKS interfaces before "
+ "it is added. Searching uses HKP; publishing and refreshing use VKS."));
+
+ ui_->keyServerTable->setColumnCount(kColumnCount);
+ ui_->keyServerTable->setHorizontalHeaderLabels(
+ {tr("Default"), tr("Address"), tr("HKP"), tr("VKS"), tr("Status"),
+ tr("Last Tested")});
+
+ connect(ui_->addKeyServerButton, &QPushButton::clicked, this,
+ &KeyServerSettingsPage::slot_add);
+ // Typing an address and pressing Enter is the obvious way to add one.
+ connect(ui_->addKeyServerEdit, &QLineEdit::returnPressed, this,
+ &KeyServerSettingsPage::slot_add);
+ connect(ui_->removeButton, &QPushButton::clicked, this,
+ &KeyServerSettingsPage::slot_remove);
+ connect(ui_->setDefaultButton, &QPushButton::clicked, this,
+ &KeyServerSettingsPage::slot_set_default);
+ connect(ui_->testButton, &QPushButton::clicked, this,
+ &KeyServerSettingsPage::slot_test_selected);
+
+ switch_ui_busy(false);
+ SetSettings();
+}
+
+void KeyServerSettingsPage::SetSettings() {
+ entries_ = KeyServerList::Load();
+ default_url_ = KeyServerList::DefaultUrl();
+ refresh_table();
+}
+
+void KeyServerSettingsPage::ApplySettings() {
+ KeyServerList::Store(entries_, default_url_);
+}
+
+void KeyServerSettingsPage::refresh_table() {
+ ui_->keyServerTable->clearContents();
+ ui_->keyServerTable->setRowCount(static_cast<int>(entries_.size()));
+
+ const auto yes = tr("yes");
+ const auto no = tr("no");
+
+ int row = 0;
+ for (const auto& entry : entries_) {
+ const auto set = [this, row](int column, const QString& text) {
+ auto* item = new QTableWidgetItem(text);
+ item->setTextAlignment(Qt::AlignCenter);
+ ui_->keyServerTable->setItem(row, column, item);
+ return item;
+ };
+
+ set(kColumnDefault, entry.url == default_url_ ? "*" : QString{});
+ auto* address = set(kColumnAddress, entry.url);
+ address->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+ set(kColumnHkp, entry.hkp ? yes : no);
+ set(kColumnVks, entry.vks ? yes : no);
+
+ auto* status =
+ set(kColumnStatus, entry.verified ? tr("Verified") : tr("Unverified"));
+ // The reason a server did not verify is too long for a cell but too useful
+ // to drop, so it lives where the user can reach it on demand.
+ if (!entry.detail.isEmpty()) status->setToolTip(entry.detail);
+
+ set(kColumnLastTested,
+ entry.last_tested.isValid()
+ ? QLocale().toString(entry.last_tested.toLocalTime(),
+ QLocale::ShortFormat)
+ : tr("never"));
+
+ ++row;
+ }
+
+ for (int column = 0; column < kColumnCount; ++column) {
+ ui_->keyServerTable->resizeColumnToContents(column);
+ }
+}
+
+auto KeyServerSettingsPage::selected_row() const -> int {
+ const auto selection = ui_->keyServerTable->selectionModel()->selectedRows();
+ return selection.isEmpty() ? -1 : selection.first().row();
+}
+
+void KeyServerSettingsPage::slot_add() {
+ auto url = ui_->addKeyServerEdit->text().trimmed();
+ if (url.isEmpty()) return;
+
+ // Someone typing a bare hostname means https, and silently guessing http
+ // would downgrade them without asking.
+ if (!url.contains("://")) url.prepend("https://");
+ while (url.endsWith('/')) url.chop(1);
+
+ const QUrl parsed(url);
+ if (!parsed.isValid() || parsed.host().isEmpty()) {
+ QMessageBox::warning(this, tr("Invalid Address"),
+ tr("\"%1\" is not a valid key server address.")
+ .arg(ui_->addKeyServerEdit->text().trimmed()));
+ return;
+ }
+
+ const auto already_listed =
+ std::any_of(entries_.cbegin(), entries_.cend(),
+ [&url](const KeyServerEntry& e) { return e.url == url; });
+ if (already_listed) {
+ QMessageBox::information(
+ this, tr("Already Listed"),
+ tr("%1 is already in the key server list.").arg(url));
+ return;
+ }
+
+ if (parsed.scheme() == "http") {
+ const auto answer = QMessageBox::warning(
+ this, tr("Insecure Key Server Address"),
+ tr("%1 uses plain HTTP, so anyone on the network can see and change "
+ "what you look up or publish. Add it anyway?")
+ .arg(url),
+ QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel);
+ if (answer == QMessageBox::Cancel) return;
+ }
+
+ KeyServerEntry entry;
+ entry.url = url;
+ entries_.append(entry);
+
+ // First entry added to an emptied list becomes the default, so the list is
+ // never in a state where nothing is selected.
+ if (default_url_.isEmpty()) default_url_ = url;
+
+ ui_->addKeyServerEdit->clear();
+ refresh_table();
+ start_probe(url);
+}
+
+void KeyServerSettingsPage::slot_remove() {
+ const auto row = selected_row();
+ if (row < 0 || row >= entries_.size()) return;
+
+ const auto removed = entries_.at(row).url;
+ entries_.removeAt(row);
+
+ // Removing the default has to hand the role to somebody, or every operation
+ // would fall back to whatever happened to be first anyway — better to make
+ // that explicit and visible in the table.
+ if (default_url_ == removed) {
+ default_url_ = entries_.isEmpty() ? QString() : entries_.first().url;
+ }
+
+ refresh_table();
+}
+
+void KeyServerSettingsPage::slot_set_default() {
+ const auto row = selected_row();
+ if (row < 0 || row >= entries_.size()) return;
+
+ default_url_ = entries_.at(row).url;
+ refresh_table();
+}
+
+void KeyServerSettingsPage::slot_test_selected() {
+ const auto row = selected_row();
+ if (row < 0 || row >= entries_.size()) return;
+
+ start_probe(entries_.at(row).url);
+}
+
+void KeyServerSettingsPage::start_probe(const QString& url) {
+ ++pending_probes_;
+ switch_ui_busy(true);
+
+ auto* prober = new KeyServerProber(this);
+ QPointer<KeyServerSettingsPage> self(this);
+ connect(prober, &KeyServerProber::SignalProbeFinished, this,
+ [self, prober](const KeyServerProber::Result& result) {
+ // The dialog can be closed while a probe is still in the air.
+ if (self != nullptr) self->apply_probe_result(result);
+ prober->deleteLater();
+ });
+
+ prober->Probe(url);
+}
+
+void KeyServerSettingsPage::apply_probe_result(
+ const KeyServerProber::Result& result) {
+ --pending_probes_;
+ switch_ui_busy(pending_probes_ > 0);
+
+ const auto index = std::find_if(
+ entries_.begin(), entries_.end(),
+ [&result](const KeyServerEntry& e) { return e.url == result.url; });
+ // The row may have been deleted while its probe was running.
+ if (index == entries_.end()) return;
+
+ index->hkp = result.hkp;
+ index->vks = result.vks;
+ index->verified = result.Conforms();
+ index->detail = result.detail;
+ index->last_tested = QDateTime::currentDateTimeUtc();
+
+ refresh_table();
+
+ if (!result.Conforms()) {
+ // The entry stays. A server can be down, or behind a network that is
+ // blocking it right now, and dropping it would make the user retype the
+ // address to find out — Test Selected re-runs this whenever they want.
+ QMessageBox::warning(
+ this, tr("Key Server Not Verified"),
+ tr("%1 did not answer as a key server.\n\n%2\n\nIt has been added and "
+ "marked unverified; use Test Selected to try again.")
+ .arg(result.url, result.detail));
+ }
+}
+
+void KeyServerSettingsPage::switch_ui_busy(bool busy) {
+ ui_->progressBar->setVisible(busy);
+ ui_->addKeyServerButton->setDisabled(busy);
+ ui_->testButton->setDisabled(busy);
+}
+
+auto KeyServerSettingsPageFactory(void* data) -> void* {
+ Q_UNUSED(data);
+ return new KeyServerSettingsPage();
+}