keyselectioncombo.cpp 19 KB
Newer Older
1
/*  This file is part of Kleopatra, the KDE keymanager
2
    SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB
3

4
    SPDX-License-Identifier: GPL-2.0-or-later
5
6
7
8
9
10
*/

#include "keyselectioncombo.h"
#include <kleo_ui_debug.h>

#include "kleo/dn.h"
11
#include "models/keylist.h"
12
13
14
15
16
#include "models/keylistmodel.h"
#include "models/keylistsortfilterproxymodel.h"
#include "models/keycache.h"
#include "utils/formatting.h"
#include "progressbar.h"
17
#include "kleo/defaultkeyfilter.h"
18
19
20
21
22

#include <gpgme++/key.h>

#include <QSortFilterProxyModel>
#include <QVector>
23
#include <QTimer>
24
25
26

#include <KLocalizedString>

27
28
using namespace Kleo;

29
30
31
32
Q_DECLARE_METATYPE(GpgME::Key)

namespace
{
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class SortFilterProxyModel : public KeyListSortFilterProxyModel
{
    Q_OBJECT

public:
    using KeyListSortFilterProxyModel::KeyListSortFilterProxyModel;

    void setAlwaysAcceptedKey(const QString &fingerprint)
    {
        if (fingerprint == mFingerprint) {
            return;
        }
        mFingerprint = fingerprint;
        invalidate();
    }

protected:
    bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
    {
        if (!mFingerprint.isEmpty()) {
            const QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
            const auto fingerprint = sourceModel()->data(index, KeyList::FingerprintRole).toString();
            if (fingerprint == mFingerprint) {
                return true;
            }
        }

        return KeyListSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
    }

private:
    QString mFingerprint;
};

67
68
69
70
71
72
73
74
75
class ProxyModel : public QSortFilterProxyModel
{
    Q_OBJECT

private:
    struct CustomItem {
        QIcon icon;
        QString text;
        QVariant data;
76
        QString toolTip;
77
78
    };
public:
Laurent Montel's avatar
Laurent Montel committed
79
    ProxyModel(QObject *parent = nullptr)
80
81
82
83
        : QSortFilterProxyModel(parent)
    {
    }

Laurent Montel's avatar
Laurent Montel committed
84
    ~ProxyModel() override
85
86
87
88
89
    {
        qDeleteAll(mFrontItems);
        qDeleteAll(mBackItems);
    }

Andre Heinecke's avatar
Andre Heinecke committed
90
91
    bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
    {
92
93
        const auto leftKey =  sourceModel()->data(left, KeyList::KeyRole).value<GpgME::Key>();
        const auto rightKey = sourceModel()->data(right, KeyList::KeyRole).value<GpgME::Key>();
Andre Heinecke's avatar
Andre Heinecke committed
94
95
96
97
98
99
100
101
102
        if (leftKey.isNull()) {
            return false;
        }
        if (rightKey.isNull()) {
            return true;
        }
        // As we display UID(0) this is ok. We probably need a get Best UID at some point.
        const auto lUid = leftKey.userID(0);
        const auto rUid = rightKey.userID(0);
103
104
105
106
107
108
109
110
111
112
113
        if (lUid.isNull()) {
            return false;
        }
        if (rUid.isNull()) {
            return true;
        }
        int cmp = strcmp (lUid.id(), rUid.id());
        if (cmp) {
            return cmp < 0;
        }

Andre Heinecke's avatar
Andre Heinecke committed
114
115
116
        if (lUid.validity() == rUid.validity()) {
            /* Both are the same check which one is newer. */
            time_t oldTime = 0;
Laurent Montel's avatar
Laurent Montel committed
117
            for (const GpgME::Subkey &s: leftKey.subkeys()) {
Andre Heinecke's avatar
Andre Heinecke committed
118
119
120
121
122
123
124
125
                if (s.isRevoked() || s.isInvalid() || s.isDisabled()) {
                    continue;
                }
                if (s.creationTime() > oldTime) {
                    oldTime= s.creationTime();
                }
            }
            time_t newTime = 0;
Laurent Montel's avatar
Laurent Montel committed
126
            for (const GpgME::Subkey &s: rightKey.subkeys()) {
Andre Heinecke's avatar
Andre Heinecke committed
127
128
129
130
131
132
133
134
135
136
137
138
                if (s.isRevoked() || s.isInvalid() || s.isDisabled()) {
                    continue;
                }
                if (s.creationTime() > newTime) {
                    newTime = s.creationTime();
                }
            }
            return newTime < oldTime;
        }
        return lUid.validity() > rUid.validity();
    }

139
140
141
142
143
    bool isCustomItem(const int row) const
    {
        return row < mFrontItems.count() || row >= mFrontItems.count() + QSortFilterProxyModel::rowCount();
    }

144
    void prependItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip)
145
146
    {
        beginInsertRows(QModelIndex(), 0, 0);
147
        mFrontItems.push_front(new CustomItem{ icon, text, data, toolTip });
148
149
150
        endInsertRows();
    }

151
    void appendItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip)
152
153
    {
        beginInsertRows(QModelIndex(), rowCount(), rowCount());
154
        mBackItems.push_back(new CustomItem{ icon, text, data, toolTip });
155
156
157
        endInsertRows();
    }

158
159
160
161
162
163
164
165
166
167
168
169
    void removeCustomItem(const QVariant &data)
    {
        for (int i = 0; i < mFrontItems.count(); ++i) {
            if (mFrontItems[i]->data == data) {
                beginRemoveRows(QModelIndex(), i, i);
                delete mFrontItems.takeAt(i);
                endRemoveRows();
                return;
            }
        }
        for (int i = 0; i < mBackItems.count(); ++i) {
            if (mBackItems[i]->data == data) {
170
                const int index = mFrontItems.count() + QSortFilterProxyModel::rowCount() + i;
171
172
173
174
175
176
177
178
                beginRemoveRows(QModelIndex(), index, index);
                delete mBackItems.takeAt(i);
                endRemoveRows();
                return;
            }
        }
    }

179
    int rowCount(const QModelIndex &parent = QModelIndex()) const override
180
181
182
183
    {
        return mFrontItems.count() + QSortFilterProxyModel::rowCount(parent) + mBackItems.count();
    }

184
    QModelIndex mapToSource(const QModelIndex &index) const override
185
186
187
    {
        if (!isCustomItem(index.row())) {
            const int row = index.row() - mFrontItems.count();
188
189

            return sourceModel()->index(row, index.column());
190
        } else {
Laurent Montel's avatar
Laurent Montel committed
191
            return {};
192
193
194
        }
    }

195
    QModelIndex mapFromSource(const QModelIndex &source_index) const override
196
197
198
199
200
    {
        const QModelIndex idx = QSortFilterProxyModel::mapFromSource(source_index);
        return createIndex(mFrontItems.count() + idx.row(), idx.column(), idx.internalPointer());
    }

201
    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
202
203
    {
        if (row < 0 || row >= rowCount()) {
Laurent Montel's avatar
Laurent Montel committed
204
            return {};
205
206
207
208
209
210
211
212
213
214
215
        }
        if (row < mFrontItems.count()) {
            return createIndex(row, column, mFrontItems[row]);
        } else if (row >= mFrontItems.count() + QSortFilterProxyModel::rowCount()) {
            return createIndex(row, column, mBackItems[row - mFrontItems.count() - QSortFilterProxyModel::rowCount()]);
        } else {
            const QModelIndex mi = QSortFilterProxyModel::index(row - mFrontItems.count(), column, parent);
            return createIndex(row, column, mi.internalPointer());
        }
    }

216
    Qt::ItemFlags flags(const QModelIndex &index) const override
217
    {
Laurent Montel's avatar
Laurent Montel committed
218
        Q_UNUSED(index)
219
220
221
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemNeverHasChildren;
    }

222
    QModelIndex parent(const QModelIndex &) const override
223
224
    {
        // Flat list
Laurent Montel's avatar
Laurent Montel committed
225
        return {};
226
227
    }

228
    QVariant data(const QModelIndex &index, int role) const override
229
    {
230
231
232
233
        if (!index.isValid()) {
            return QVariant();
        }

234
235
        if (isCustomItem(index.row())) {
            Q_ASSERT(!mFrontItems.isEmpty() || !mBackItems.isEmpty());
236
            auto ci = static_cast<CustomItem*>(index.internalPointer());
237
238
239
240
241
242
243
            switch (role) {
            case Qt::DisplayRole:
                return ci->text;
            case Qt::DecorationRole:
                return ci->icon;
            case Qt::UserRole:
                return ci->data;
244
245
            case Qt::ToolTipRole:
                return ci->toolTip;
246
247
248
249
250
            default:
                return QVariant();
            }
        }

251
        const auto key = QSortFilterProxyModel::data(index, KeyList::KeyRole).value<GpgME::Key>();
252
253
254
255
256
257
258
259
260
261
262
        Q_ASSERT(!key.isNull());
        if (key.isNull()) {
            return QVariant();
        }

        switch (role) {
        case Qt::DisplayRole: {
            const auto userID = key.userID(0);
            QString name, email;

            if (key.protocol() == GpgME::OpenPGP) {
Laurent Montel's avatar
Laurent Montel committed
263
264
                name = QString::fromUtf8(userID.name());
                email = QString::fromUtf8(userID.email());
265
266
267
268
269
            } else {
                const Kleo::DN dn(userID.id());
                name = dn[QStringLiteral("CN")];
                email = dn[QStringLiteral("EMAIL")];
            }
270
            return i18nc("Name <email> (validity, type, created: date)", "%1 (%2, %3 created: %4)",
271
                         email.isEmpty() ? name : name.isEmpty() ? email : i18nc("Name <email>", "%1 <%2>", name, email),
272
                         Kleo::Formatting::complianceStringShort(key),
273
274
                         Kleo::KeyCache::instance()->pgpOnly() ? QString() :
                            key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP") + QLatin1Char(',') : i18n("S/MIME") + QLatin1Char(','),
275
                         Kleo::Formatting::creationDateString(key));
276
        }
277
278
279
280
281
282
283
        case Qt::ToolTipRole:
            return Kleo::Formatting::toolTip(key, Kleo::Formatting::Validity |
                                                  Kleo::Formatting::Issuer |
                                                  Kleo::Formatting::Subject |
                                                  Kleo::Formatting::Fingerprint |
                                                  Kleo::Formatting::ExpiryDates |
                                                  Kleo::Formatting::UserIDs);
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
        case Qt::DecorationRole:
            return Kleo::Formatting::iconForUid(key.userID(0));
        default:
            return QSortFilterProxyModel::data(index, role);
        }
    }

private:
    QVector<CustomItem*> mFrontItems;
    QVector<CustomItem*> mBackItems;
};


} // anonymous namespace

namespace Kleo
{
class KeySelectionComboPrivate
{
public:
    KeySelectionComboPrivate(KeySelectionCombo *parent)
305
306
        : wasEnabled(true)
        , q(parent)
307
308
309
    {
    }

310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
    /* Selects the first key with a UID addrSpec that matches
     * the mPerfectMatchMbox variable.
     *
     * The idea here is that if there are keys like:
     *
     * tom-store@abc.com
     * susi-store@abc.com
     * store@abc.com
     *
     * And the user wants to send a mail to "store@abc.com"
     * the filter should still show tom and susi (because they
     * both are part of store) but the key for "store" should
     * be preselected.
     *
     * Returns true if one was selected. False otherwise. */
    bool selectPerfectIdMatch() const
    {
        if (mPerfectMatchMbox.isEmpty()) {
            return false;
        }

        for (int i = 0; i < proxyModel->rowCount(); ++i) {
            const auto idx = proxyModel->index(i, 0, QModelIndex());
333
            const auto key = proxyModel->data(idx, KeyList::KeyRole).value<GpgME::Key>();
334
335
336
337
338
339
340
341
342
343
344
345
346
347
            if (key.isNull()) {
                // WTF?
                continue;
            }
            for (const auto &uid: key.userIDs()) {
                if (QString::fromStdString(uid.addrSpec()) == mPerfectMatchMbox) {
                    q->setCurrentIndex(i);
                    return true;
                }
            }
        }
        return false;
    }

348
349
    /* Updates the current key with the default key if the key matches
     * the current key filter. */
350
    void updateWithDefaultKey() {
351
352
353
354
355
356
357
358
359
360
361
362
363
364
        GpgME::Protocol filterProto = GpgME::UnknownProtocol;

        const auto filter = dynamic_cast<const DefaultKeyFilter*> (sortFilterProxy->keyFilter().get());
        if (filter && filter->isOpenPGP() == DefaultKeyFilter::Set) {
            filterProto = GpgME::OpenPGP;
        } else if (filter && filter->isOpenPGP() == DefaultKeyFilter::NotSet) {
            filterProto = GpgME::CMS;
        }

        QString defaultKey = defaultKeys.value (filterProto);
        if (defaultKey.isEmpty()) {
            // Fallback to unknown protocol
            defaultKey = defaultKeys.value (GpgME::UnknownProtocol);
        }
365
366
367
368
369
370
371
372
373
374
375
        // make sure that the default key is not filtered out unless it has the wrong protocol
        if (filterProto == GpgME::UnknownProtocol) {
            sortFilterProxy->setAlwaysAcceptedKey(defaultKey);
        } else {
            const auto key = KeyCache::instance()->findByFingerprint(defaultKey.toLatin1().constData());
            if (!key.isNull() && key.protocol() == filterProto) {
                sortFilterProxy->setAlwaysAcceptedKey(defaultKey);
            } else {
                sortFilterProxy->setAlwaysAcceptedKey({});
            }
        }
376
377
378
        q->setCurrentKey(defaultKey);
    }

Laurent Montel's avatar
Laurent Montel committed
379
    Kleo::AbstractKeyListModel *model = nullptr;
380
    SortFilterProxyModel *sortFilterProxy = nullptr;
Laurent Montel's avatar
Laurent Montel committed
381
    ProxyModel *proxyModel = nullptr;
382
    std::shared_ptr<Kleo::KeyCache> cache;
383
    QMap<GpgME::Protocol, QString> defaultKeys;
Laurent Montel's avatar
Laurent Montel committed
384
    bool wasEnabled = false;
385
    bool useWasEnabled = false;
386
    bool secretOnly;
387
    QString mPerfectMatchMbox;
388
389
390
391
392
393
394
395
396
397

private:
    KeySelectionCombo * const q;
};

}

using namespace Kleo;

KeySelectionCombo::KeySelectionCombo(QWidget* parent)
398
399
400
401
    : KeySelectionCombo(true, parent)
{}

KeySelectionCombo::KeySelectionCombo(bool secretOnly, QWidget* parent)
402
403
404
405
    : QComboBox(parent)
    , d(new KeySelectionComboPrivate(this))
{
    d->model = Kleo::AbstractKeyListModel::createFlatKeyListModel(this);
406
    d->secretOnly = secretOnly;
407

408
    d->sortFilterProxy = new SortFilterProxyModel(this);
409
410
411
412
413
414
    d->sortFilterProxy->setSourceModel(d->model);

    d->proxyModel = new ProxyModel(this);
    d->proxyModel->setSourceModel(d->sortFilterProxy);

    setModel(d->proxyModel);
415
    connect(this, QOverload<int>::of(&QComboBox::currentIndexChanged),
Daniel Vrátil's avatar
Daniel Vrátil committed
416
417
418
419
420
421
422
423
424
            this, [this](int row) {
                if (row >= 0 && row < d->proxyModel->rowCount()) {
                    if (d->proxyModel->isCustomItem(row)) {
                        Q_EMIT customItemSelected(d->proxyModel->index(row, 0).data(Qt::UserRole));
                    } else {
                        Q_EMIT currentKeyChanged(currentKey());
                    }
                }
            });
425
426
427
428

    d->cache = Kleo::KeyCache::mutableInstance();

    QTimer::singleShot(0, this, &KeySelectionCombo::init);
429
430
431
432
433
434
435
}

KeySelectionCombo::~KeySelectionCombo()
{
    delete d;
}

436
437
438
439
void KeySelectionCombo::init()
{
    connect(d->cache.get(), &Kleo::KeyCache::keyListingDone,
            this, [this]() {
440
                    // Set useKeyCache ensures that the cache is populated
Yuri Chornoivan's avatar
Yuri Chornoivan committed
441
                    // so this can be a blocking call if the cache is not initialized 
442
                    d->model->useKeyCache(true, d->secretOnly ? KeyList::SecretKeysOnly : KeyList::AllKeys);
443
                    d->proxyModel->removeCustomItem(QStringLiteral("-libkleo-loading-keys"));
444
445
446
447
448
449
450
451
452
453
454
455
456

                    // We use the useWasEnabled state variable to decide if we should
                    // change the enable / disable state based on the keylist done signal.
                    // If we triggered the refresh useWasEnabled is true and we want to
                    // enable / disable again after our refresh, as the refresh disabled it.
                    //
                    // But if a keyListingDone signal comes from just a generic refresh
                    // triggered by someone else we don't want to change the enable / disable
                    // state.
                    if (d->useWasEnabled) {
                        setEnabled(d->wasEnabled);
                        d->useWasEnabled = false;
                    }
457
                    Q_EMIT keyListingFinished();
458
459
            });

460
461
462
    connect(this, &KeySelectionCombo::keyListingFinished, this, [this]() {
            d->updateWithDefaultKey();
        });
463
464
465
466

    if (!d->cache->initialized()) {
        refreshKeys();
    } else {
467
        d->model->useKeyCache(true, d->secretOnly ? KeyList::SecretKeysOnly : KeyList::AllKeys);
468
        Q_EMIT keyListingFinished();
469
    }
470
471
472
473

    connect(this, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] () {
            setToolTip(currentData(Qt::ToolTipRole).toString());
        });
474
475
476
}


477
void KeySelectionCombo::setKeyFilter(const std::shared_ptr<const KeyFilter> &kf)
478
479
{
    d->sortFilterProxy->setKeyFilter(kf);
Andre Heinecke's avatar
Andre Heinecke committed
480
    d->proxyModel->sort(0);
481
    d->updateWithDefaultKey();
482
483
}

484
std::shared_ptr<const KeyFilter> KeySelectionCombo::keyFilter() const
485
486
487
488
489
490
491
{
    return d->sortFilterProxy->keyFilter();
}

void KeySelectionCombo::setIdFilter(const QString &id)
{
    d->sortFilterProxy->setFilterRegExp(id);
492
    d->mPerfectMatchMbox = id;
493
    d->updateWithDefaultKey();
494
495
496
497
498
499
500
}

QString KeySelectionCombo::idFilter() const
{
    return d->sortFilterProxy->filterRegExp().pattern();
}

501
502
GpgME::Key Kleo::KeySelectionCombo::currentKey() const
{
503
    return currentData(KeyList::KeyRole).value<GpgME::Key>();
504
505
506
507
}

void Kleo::KeySelectionCombo::setCurrentKey(const GpgME::Key &key)
{
508
    const int idx = findData(QVariant::fromValue(key), KeyList::KeyRole, Qt::MatchExactly);
509
510
    if (idx > -1) {
        setCurrentIndex(idx);
511
512
    } else {
        d->selectPerfectIdMatch();
513
    }
514
    setToolTip(currentData(Qt::ToolTipRole).toString());
515
516
}

517
518
void Kleo::KeySelectionCombo::setCurrentKey(const QString &fingerprint)
{
519
520
521
    const auto cur = currentKey();
    if (!cur.isNull() && !fingerprint.isEmpty() &&
        fingerprint == QLatin1String(cur.primaryFingerprint())) {
522
523
524
        // already set; still emit a changed signal because the current key may
        // have become the item at the current index by changes in the underlying model
        Q_EMIT currentKeyChanged(cur);
525
526
        return;
    }
527
    const int idx = findData(fingerprint, KeyList::FingerprintRole, Qt::MatchExactly);
528
529
530
    if (idx > -1) {
        setCurrentIndex(idx);
    } else if (!d->selectPerfectIdMatch()) {
531
532
        setCurrentIndex(0);
    }
533
    setToolTip(currentData(Qt::ToolTipRole).toString());
534
535
}

536
537
538
void KeySelectionCombo::refreshKeys()
{
    d->wasEnabled = isEnabled();
539
    d->useWasEnabled = true;
540
541
542
543
544
545
546
547
    setEnabled(false);
    const bool wasBlocked = blockSignals(true);
    prependCustomItem(QIcon(), i18n("Loading keys ..."), QStringLiteral("-libkleo-loading-keys"));
    setCurrentIndex(0);
    blockSignals(wasBlocked);
    d->cache->startKeyListing();
}

548
549
550
551
552
void KeySelectionCombo::appendCustomItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip)
{
    d->proxyModel->appendItem(icon, text, data, toolTip);
}

553
554
void KeySelectionCombo::appendCustomItem(const QIcon &icon, const QString &text, const QVariant &data)
{
555
556
557
558
559
560
    appendCustomItem(icon, text, data, QString());
}

void KeySelectionCombo::prependCustomItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip)
{
    d->proxyModel->prependItem(icon, text, data, toolTip);
561
562
563
564
}

void KeySelectionCombo::prependCustomItem(const QIcon &icon, const QString &text, const QVariant &data)
{
565
    prependCustomItem(icon, text, data, QString());
566
567
}

568
569
570
571
572
573
void Kleo::KeySelectionCombo::setDefaultKey(const QString &fingerprint, GpgME::Protocol proto)
{
    d->defaultKeys.insert(proto, fingerprint);
    d->updateWithDefaultKey();
}

574
575
void Kleo::KeySelectionCombo::setDefaultKey(const QString &fingerprint)
{
576
    setDefaultKey(fingerprint, GpgME::UnknownProtocol);
577
}
578

579
QString Kleo::KeySelectionCombo::defaultKey(GpgME::Protocol proto) const
580
{
581
    return d->defaultKeys.value(proto);
582
}
583

584
585
586
587
QString Kleo::KeySelectionCombo::defaultKey() const
{
    return defaultKey(GpgME::UnknownProtocol);
}
588
#include "keyselectioncombo.moc"