File indexing completed on 2024-06-23 05:13:47

0001 /*
0002     conf/groupsconfigwidget.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2021 g10 Code GmbH
0006     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "groupsconfigwidget.h"
0012 
0013 #include <commands/certifygroupcommand.h>
0014 #include <commands/exportgroupscommand.h>
0015 #include <dialogs/editgroupdialog.h>
0016 
0017 #include <Libkleo/Algorithm>
0018 #include <Libkleo/Debug>
0019 #include <Libkleo/Formatting>
0020 #include <Libkleo/KeyCache>
0021 #include <Libkleo/KeyGroup>
0022 #include <Libkleo/KeyHelpers>
0023 #include <Libkleo/KeyListModel>
0024 #include <Libkleo/KeyListSortFilterProxyModel>
0025 
0026 #include <KLocalizedString>
0027 #include <KMessageBox>
0028 #include <KRandom>
0029 
0030 #include <QItemSelectionModel>
0031 #include <QLabel>
0032 #include <QLineEdit>
0033 #include <QListView>
0034 #include <QPushButton>
0035 #include <QVBoxLayout>
0036 
0037 #include "kleopatra_debug.h"
0038 
0039 using namespace Kleo;
0040 using namespace Kleo::Dialogs;
0041 
0042 Q_DECLARE_METATYPE(KeyGroup)
0043 
0044 namespace
0045 {
0046 
0047 class ListView : public QListView
0048 {
0049     Q_OBJECT
0050 public:
0051     using QListView::QListView;
0052 
0053 protected:
0054     void currentChanged(const QModelIndex &current, const QModelIndex &previous) override
0055     {
0056         // workaround bug in QListView::currentChanged which sends an accessible focus event
0057         // even if the list view doesn't have focus
0058         if (hasFocus()) {
0059             QListView::currentChanged(current, previous);
0060         } else {
0061             // skip the reimplementation of currentChanged in QListView
0062             QAbstractItemView::currentChanged(current, previous);
0063         }
0064     }
0065 
0066     void focusInEvent(QFocusEvent *event) override
0067     {
0068         QListView::focusInEvent(event);
0069         // select current item if it isn't selected
0070         if (currentIndex().isValid() && !selectionModel()->isSelected(currentIndex())) {
0071             selectionModel()->select(currentIndex(), QItemSelectionModel::ClearAndSelect);
0072         }
0073     }
0074 };
0075 
0076 class ProxyModel : public AbstractKeyListSortFilterProxyModel
0077 {
0078     Q_OBJECT
0079 public:
0080     ProxyModel(QObject *parent = nullptr)
0081         : AbstractKeyListSortFilterProxyModel(parent)
0082     {
0083     }
0084 
0085     ~ProxyModel() override = default;
0086 
0087     ProxyModel *clone() const override
0088     {
0089         // compiler-generated copy ctor is fine!
0090         return new ProxyModel(*this);
0091     }
0092 
0093     int columnCount(const QModelIndex &parent = {}) const override
0094     {
0095         Q_UNUSED(parent)
0096         // pretend that there is only one column to workaround a bug in
0097         // QAccessibleTable which provides the accessibility interface for the
0098         // list view
0099         return 1;
0100     }
0101 
0102     QVariant data(const QModelIndex &idx, int role) const override
0103     {
0104         if (!idx.isValid()) {
0105             return {};
0106         }
0107 
0108         return AbstractKeyListSortFilterProxyModel::data(index(idx.row(), KeyList::Summary), role);
0109     }
0110 };
0111 
0112 struct Selection {
0113     KeyGroup current;
0114     std::vector<KeyGroup> selected;
0115 };
0116 
0117 }
0118 
0119 class GroupsConfigWidget::Private
0120 {
0121     friend class ::Kleo::GroupsConfigWidget;
0122     GroupsConfigWidget *const q;
0123 
0124     struct {
0125         QLineEdit *groupsFilter = nullptr;
0126         QListView *groupsList = nullptr;
0127         QPushButton *newButton = nullptr;
0128         QPushButton *editButton = nullptr;
0129         QPushButton *deleteButton = nullptr;
0130         QPushButton *certifyButton = nullptr;
0131         QPushButton *exportButton = nullptr;
0132     } ui;
0133     AbstractKeyListModel *groupsModel = nullptr;
0134     ProxyModel *groupsFilterModel = nullptr;
0135 
0136 public:
0137     Private(GroupsConfigWidget *qq)
0138         : q(qq)
0139     {
0140         auto mainLayout = new QVBoxLayout(q);
0141 
0142         auto groupsLayout = new QGridLayout;
0143         groupsLayout->setContentsMargins(q->style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
0144                                          q->style()->pixelMetric(QStyle::PM_LayoutTopMargin),
0145                                          q->style()->pixelMetric(QStyle::PM_LayoutRightMargin),
0146                                          q->style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
0147         groupsLayout->setColumnStretch(0, 1);
0148         groupsLayout->setRowStretch(1, 1);
0149         int row = -1;
0150 
0151         row++;
0152         {
0153             auto hbox = new QHBoxLayout;
0154             auto label = new QLabel{i18nc("@label", "Search:")};
0155             label->setAccessibleName(i18nc("@label", "Search groups"));
0156             label->setToolTip(i18nc("@info:tooltip", "Search the list for groups matching the search term."));
0157             hbox->addWidget(label);
0158 
0159             ui.groupsFilter = new QLineEdit(q);
0160             ui.groupsFilter->setClearButtonEnabled(true);
0161             ui.groupsFilter->setAccessibleName(i18nc("@label", "Search groups"));
0162             ui.groupsFilter->setToolTip(i18nc("@info:tooltip", "Search the list for groups matching the search term."));
0163             ui.groupsFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term"));
0164             ui.groupsFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event
0165             label->setBuddy(ui.groupsFilter);
0166             hbox->addWidget(ui.groupsFilter, 1);
0167 
0168             groupsLayout->addLayout(hbox, row, 0);
0169         }
0170 
0171         row++;
0172         groupsModel = AbstractKeyListModel::createFlatKeyListModel(q);
0173         groupsFilterModel = new ProxyModel(q);
0174         groupsFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0175         groupsFilterModel->setFilterKeyColumn(KeyList::Summary);
0176         groupsFilterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
0177         groupsFilterModel->setSourceModel(groupsModel);
0178         groupsFilterModel->sort(KeyList::Summary, Qt::AscendingOrder);
0179         ui.groupsList = new ListView(q);
0180         ui.groupsList->setAccessibleName(i18nc("groups of keys", "groups"));
0181         ui.groupsList->setModel(groupsFilterModel);
0182         ui.groupsList->setSelectionBehavior(QAbstractItemView::SelectRows);
0183         ui.groupsList->setSelectionMode(QAbstractItemView::ExtendedSelection);
0184 
0185         groupsLayout->addWidget(ui.groupsList, row, 0);
0186 
0187         auto groupsButtonLayout = new QVBoxLayout;
0188 
0189         ui.newButton = new QPushButton(i18nc("@action:button", "New"), q);
0190         groupsButtonLayout->addWidget(ui.newButton);
0191 
0192         ui.editButton = new QPushButton(i18nc("@action:button", "Edit"), q);
0193         ui.editButton->setEnabled(false);
0194         groupsButtonLayout->addWidget(ui.editButton);
0195 
0196         ui.deleteButton = new QPushButton(i18nc("@action:button", "Delete"), q);
0197         ui.deleteButton->setEnabled(false);
0198         groupsButtonLayout->addWidget(ui.deleteButton);
0199 
0200         ui.certifyButton = new QPushButton{i18nc("@action:button", "Certify"), q};
0201         ui.certifyButton->setToolTip(i18nc("@info:tooltip", "Start the certification process for all certificates in the group."));
0202         ui.certifyButton->setEnabled(false);
0203         groupsButtonLayout->addWidget(ui.certifyButton);
0204 
0205         ui.exportButton = new QPushButton{i18nc("@action:button", "Export"), q};
0206         ui.exportButton->setEnabled(false);
0207         groupsButtonLayout->addWidget(ui.exportButton);
0208 
0209         groupsButtonLayout->addStretch(1);
0210 
0211         groupsLayout->addLayout(groupsButtonLayout, row, 1);
0212 
0213         mainLayout->addLayout(groupsLayout, /*stretch=*/1);
0214 
0215         connect(ui.groupsFilter, &QLineEdit::textChanged, q, [this](const auto &s) {
0216             groupsFilterModel->setFilterRegularExpression(QRegularExpression::escape(s));
0217         });
0218         connect(ui.groupsList->selectionModel(), &QItemSelectionModel::selectionChanged, q, [this]() {
0219             selectionChanged();
0220         });
0221         connect(ui.groupsList, &QListView::doubleClicked, q, [this](const QModelIndex &index) {
0222             editGroup(index);
0223         });
0224         connect(ui.newButton, &QPushButton::clicked, q, [this]() {
0225             addGroup();
0226         });
0227         connect(ui.editButton, &QPushButton::clicked, q, [this]() {
0228             editGroup();
0229         });
0230         connect(ui.deleteButton, &QPushButton::clicked, q, [this]() {
0231             deleteGroup();
0232         });
0233         connect(ui.certifyButton, &QPushButton::clicked, q, [this]() {
0234             certifyGroup();
0235         });
0236         connect(ui.exportButton, &QPushButton::clicked, q, [this]() {
0237             exportGroup();
0238         });
0239     }
0240 
0241 private:
0242     auto getGroupIndex(const KeyGroup &group)
0243     {
0244         QModelIndex index;
0245         if (const KeyListModelInterface *const klmi = dynamic_cast<KeyListModelInterface *>(ui.groupsList->model())) {
0246             index = klmi->index(group);
0247         }
0248         return index;
0249     }
0250 
0251     auto selectedRows()
0252     {
0253         return ui.groupsList->selectionModel()->selectedRows();
0254     }
0255 
0256     auto getGroup(const QModelIndex &index)
0257     {
0258         return index.isValid() ? ui.groupsList->model()->data(index, KeyList::GroupRole).value<KeyGroup>() : KeyGroup{};
0259     }
0260 
0261     auto getGroups(const QModelIndexList &indexes)
0262     {
0263         std::vector<KeyGroup> groups;
0264         std::transform(std::begin(indexes), std::end(indexes), std::back_inserter(groups), [this](const auto &index) {
0265             return getGroup(index);
0266         });
0267         return groups;
0268     }
0269 
0270     Selection saveSelection()
0271     {
0272         return {getGroup(ui.groupsList->selectionModel()->currentIndex()), getGroups(selectedRows())};
0273     }
0274 
0275     void restoreSelection(const Selection &selection)
0276     {
0277         auto selectionModel = ui.groupsList->selectionModel();
0278         selectionModel->clearSelection();
0279         for (const auto &group : selection.selected) {
0280             selectionModel->select(getGroupIndex(group), QItemSelectionModel::Select | QItemSelectionModel::Rows);
0281         }
0282         auto currentIndex = getGroupIndex(selection.current);
0283         if (currentIndex.isValid()) {
0284             // keep current item if old current group is gone
0285             selectionModel->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate);
0286         }
0287     }
0288 
0289     void selectionChanged()
0290     {
0291         const auto selectedGroups = getGroups(selectedRows());
0292         const bool allSelectedGroupsAreEditable = std::all_of(std::begin(selectedGroups), std::end(selectedGroups), [](const auto &g) {
0293             return !g.isNull() && !g.isImmutable();
0294         });
0295         ui.editButton->setEnabled(selectedGroups.size() == 1 && allSelectedGroupsAreEditable);
0296         ui.deleteButton->setEnabled(!selectedGroups.empty() && allSelectedGroupsAreEditable);
0297         ui.certifyButton->setEnabled(selectedGroups.size() == 1 //
0298                                      && !selectedGroups.front().keys().empty() //
0299                                      && allKeysHaveProtocol(selectedGroups.front().keys(), GpgME::OpenPGP));
0300         ui.exportButton->setEnabled(selectedGroups.size() == 1);
0301     }
0302 
0303     KeyGroup showEditGroupDialog(KeyGroup group, const QString &windowTitle, EditGroupDialog::FocusWidget focusWidget)
0304     {
0305         auto dialog = std::make_unique<EditGroupDialog>(q);
0306         dialog->setWindowTitle(windowTitle);
0307         dialog->setGroupName(group.name());
0308         const KeyGroup::Keys &keys = group.keys();
0309         dialog->setGroupKeys(std::vector<GpgME::Key>(keys.cbegin(), keys.cend()));
0310         dialog->setInitialFocus(focusWidget);
0311 
0312         const int result = dialog->exec();
0313         if (result == QDialog::Rejected) {
0314             return KeyGroup();
0315         }
0316 
0317         group.setName(dialog->groupName());
0318         group.setKeys(dialog->groupKeys());
0319 
0320         return group;
0321     }
0322 
0323     void addGroup()
0324     {
0325         const KeyGroup::Id newId = KRandom::randomString(8);
0326         KeyGroup group = KeyGroup(newId, i18nc("default name for new group of keys", "New Group"), {}, KeyGroup::ApplicationConfig);
0327         group.setIsImmutable(false);
0328 
0329         const KeyGroup newGroup = showEditGroupDialog(group, i18nc("@title:window a group of keys", "New Group"), EditGroupDialog::GroupName);
0330         if (newGroup.isNull()) {
0331             return;
0332         }
0333 
0334         const QModelIndex newIndex = groupsModel->addGroup(newGroup);
0335         if (!newIndex.isValid()) {
0336             qCDebug(KLEOPATRA_LOG) << "Adding group to model failed";
0337             return;
0338         }
0339 
0340         Q_EMIT q->changed();
0341     }
0342 
0343     void editGroup(const QModelIndex &index = {})
0344     {
0345         QModelIndex groupIndex;
0346         if (index.isValid()) {
0347             groupIndex = index;
0348         } else {
0349             const auto selection = selectedRows();
0350             if (selection.size() != 1) {
0351                 qCDebug(KLEOPATRA_LOG) << (selection.empty() ? "selection is empty" : "more than one group is selected");
0352                 return;
0353             }
0354             groupIndex = selection.front();
0355         }
0356         const KeyGroup group = getGroup(groupIndex);
0357         if (group.isNull()) {
0358             qCDebug(KLEOPATRA_LOG) << "selected group is null";
0359             return;
0360         }
0361         if (group.isImmutable()) {
0362             qCDebug(KLEOPATRA_LOG) << "selected group is immutable";
0363             return;
0364         }
0365 
0366         const KeyGroup updatedGroup = showEditGroupDialog(group, i18nc("@title:window a group of keys", "Edit Group"), EditGroupDialog::KeysFilter);
0367         if (updatedGroup.isNull()) {
0368             return;
0369         }
0370 
0371         // look up index of updated group; the groupIndex used above may have become invalid
0372         const auto updatedGroupIndex = getGroupIndex(updatedGroup);
0373         if (updatedGroupIndex.isValid()) {
0374             const bool success = ui.groupsList->model()->setData(updatedGroupIndex, QVariant::fromValue(updatedGroup));
0375             if (!success) {
0376                 qCDebug(KLEOPATRA_LOG) << "Updating group in model failed";
0377                 return;
0378             }
0379         } else {
0380             qCDebug(KLEOPATRA_LOG) << __func__ << "Failed to find index of group" << updatedGroup << "; maybe it was removed behind our back; re-add it";
0381             const QModelIndex newIndex = groupsModel->addGroup(updatedGroup);
0382             if (!newIndex.isValid()) {
0383                 qCDebug(KLEOPATRA_LOG) << "Re-adding group to model failed";
0384                 return;
0385             }
0386         }
0387 
0388         Q_EMIT q->changed();
0389     }
0390 
0391     bool confirmDeletion(const std::vector<KeyGroup> &groups)
0392     {
0393         QString message;
0394         QStringList groupSummaries;
0395         if (groups.size() == 1) {
0396             message = xi18nc("@info",
0397                              "<para>Do you really want to delete this group?</para>"
0398                              "<para><emphasis>%1</emphasis></para>"
0399                              "<para>Once deleted, it cannot be restored.</para>")
0400                           .arg(Formatting::summaryLine(groups.front()));
0401         } else {
0402             message = xi18ncp("@info",
0403                               "<para>Do you really want to delete this %1 group?</para>"
0404                               "<para>Once deleted, it cannot be restored.</para>",
0405                               "<para>Do you really want to delete these %1 groups?</para>"
0406                               "<para>Once deleted, they cannot be restored.</para>",
0407                               groups.size());
0408             Kleo::transform(groups, std::back_inserter(groupSummaries), [](const auto &g) {
0409                 return Formatting::summaryLine(g);
0410             });
0411         }
0412         const auto answer = KMessageBox::questionTwoActionsList(q,
0413                                                                 message,
0414                                                                 groupSummaries,
0415                                                                 i18ncp("@title:window", "Delete Group", "Delete Groups", groups.size()),
0416                                                                 KStandardGuiItem::del(),
0417                                                                 KStandardGuiItem::cancel(),
0418                                                                 {},
0419                                                                 KMessageBox::Notify | KMessageBox::Dangerous);
0420         return answer == KMessageBox::PrimaryAction;
0421     }
0422 
0423     void deleteGroup()
0424     {
0425         const auto selectedGroups = getGroups(selectedRows());
0426         if (selectedGroups.empty()) {
0427             qCDebug(KLEOPATRA_LOG) << "selection is empty";
0428             return;
0429         }
0430 
0431         if (!confirmDeletion(selectedGroups)) {
0432             return;
0433         }
0434 
0435         for (const auto &group : selectedGroups) {
0436             const bool success = groupsModel->removeGroup(group);
0437             if (!success) {
0438                 qCDebug(KLEOPATRA_LOG) << "Removing group from model failed:" << group;
0439             }
0440         }
0441 
0442         Q_EMIT q->changed();
0443     }
0444 
0445     void certifyGroup()
0446     {
0447         const auto selectedGroups = getGroups(selectedRows());
0448         if (selectedGroups.size() != 1) {
0449             qCDebug(KLEOPATRA_LOG) << __func__ << (selectedGroups.empty() ? "selection is empty" : "more than one group is selected");
0450             return;
0451         }
0452 
0453         auto cmd = new CertifyGroupCommand{selectedGroups.front()};
0454         cmd->setParentWidget(q->window());
0455         cmd->start();
0456     }
0457 
0458     void exportGroup()
0459     {
0460         const auto selectedGroups = getGroups(selectedRows());
0461         if (selectedGroups.empty()) {
0462             qCDebug(KLEOPATRA_LOG) << "selection is empty";
0463             return;
0464         }
0465 
0466         auto cmd = new ExportGroupsCommand(selectedGroups);
0467         cmd->start();
0468     }
0469 };
0470 
0471 GroupsConfigWidget::GroupsConfigWidget(QWidget *parent)
0472     : QWidget(parent)
0473     , d(new Private(this))
0474 {
0475 }
0476 
0477 GroupsConfigWidget::~GroupsConfigWidget() = default;
0478 
0479 void GroupsConfigWidget::setGroups(const std::vector<KeyGroup> &groups)
0480 {
0481     const auto selection = d->saveSelection();
0482     d->groupsModel->setGroups(groups);
0483     d->restoreSelection(selection);
0484 }
0485 
0486 std::vector<KeyGroup> GroupsConfigWidget::groups() const
0487 {
0488     std::vector<KeyGroup> result;
0489     result.reserve(d->groupsModel->rowCount());
0490     for (int row = 0; row < d->groupsModel->rowCount(); ++row) {
0491         const QModelIndex index = d->groupsModel->index(row, 0);
0492         result.push_back(d->groupsModel->group(index));
0493     }
0494     return result;
0495 }
0496 
0497 #include "groupsconfigwidget.moc"
0498 
0499 #include "moc_groupsconfigwidget.cpp"