FlatpakSourcesBackend.cpp 15.7 KB
Newer Older
1
2
3
4
5
6
7
/*
 *   SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
 *   SPDX-FileCopyrightText: 2017 Jan Grulich <jgrulich@redhat.com>
 *
 *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
 */

Jan Grulich's avatar
Jan Grulich committed
8
#include "FlatpakSourcesBackend.h"
9
#include "FlatpakBackend.h"
Alexander Lohnau's avatar
Alexander Lohnau committed
10
11
#include "FlatpakResource.h"
#include <KConfigGroup>
12
#include <KLocalizedString>
13
#include <KSharedConfig>
Jan Grulich's avatar
Jan Grulich committed
14
#include <QDebug>
15
16
#include <QNetworkAccessManager>
#include <QNetworkReply>
Jan Grulich's avatar
Jan Grulich committed
17

18
#include <QStandardPaths>
Alexander Lohnau's avatar
Alexander Lohnau committed
19
20
#include <QTemporaryFile>
#include <glib.h>
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
21
#include <resources/DiscoverAction.h>
22
#include <resources/StoredResultsStream.h>
Jan Grulich's avatar
Jan Grulich committed
23
24
25
26

class FlatpakSourceItem : public QStandardItem
{
public:
27
    FlatpakSourceItem(const QString &text, FlatpakRemote *remote, FlatpakBackend *backend)
Alexander Lohnau's avatar
Alexander Lohnau committed
28
        : QStandardItem(text)
29
30
        , m_remote(remote)
        , m_backend(backend)
Alexander Lohnau's avatar
Alexander Lohnau committed
31
    {
32
33
34
35
36
        g_object_ref(remote);
    }
    ~FlatpakSourceItem()
    {
        g_object_unref(m_remote);
Alexander Lohnau's avatar
Alexander Lohnau committed
37
    }
38

Alexander Lohnau's avatar
Alexander Lohnau committed
39
40
41
42
    void setFlatpakInstallation(FlatpakInstallation *installation)
    {
        m_installation = installation;
    }
43

Alexander Lohnau's avatar
Alexander Lohnau committed
44
45
46
47
    FlatpakInstallation *flatpakInstallation() const
    {
        return m_installation;
    }
Jan Grulich's avatar
Jan Grulich committed
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
    void setData(const QVariant &value, int role) override
    {
        // We check isCheckable() so the initial setting of the item doesn't trigger a change
        if (role == Qt::CheckStateRole && isCheckable()) {
            const bool disabled = flatpak_remote_get_disabled(m_remote);
            const bool requestedDisabled = Qt::Unchecked == value;
            if (disabled != requestedDisabled) {
                flatpak_remote_set_disabled(m_remote, requestedDisabled);
                g_autoptr(GError) error = nullptr;
                if (!flatpak_installation_modify_remote(m_installation, m_remote, nullptr, &error)) {
                    qWarning() << "set disabled failed" << error->message;
                    return;
                }

                if (requestedDisabled) {
                    m_backend->unloadRemote(m_installation, m_remote);
                } else {
                    m_backend->loadRemote(m_installation, m_remote);
                }
            }
        }
        QStandardItem::setData(value, role);
    }

73
74
75
76
77
    FlatpakRemote *remote() const
    {
        return m_remote;
    }

Jan Grulich's avatar
Jan Grulich committed
78
private:
79
80
81
    FlatpakInstallation *m_installation = nullptr;
    FlatpakRemote *const m_remote;
    FlatpakBackend *const m_backend;
Jan Grulich's avatar
Jan Grulich committed
82
83
};

Alexander Lohnau's avatar
Alexander Lohnau committed
84
FlatpakSourcesBackend::FlatpakSourcesBackend(const QVector<FlatpakInstallation *> &installations, AbstractResourcesBackend *parent)
Jan Grulich's avatar
Jan Grulich committed
85
    : AbstractSourcesBackend(parent)
86
    , m_preferredInstallation(installations.constFirst())
Jan Grulich's avatar
Jan Grulich committed
87
    , m_sources(new QStandardItemModel(this))
88
    , m_flathubAction(new DiscoverAction("flatpak-discover", i18n("Add Flathub"), this))
89
    , m_saveAction(new DiscoverAction("dialog-ok-apply", i18n("Apply Changes"), this))
90
    , m_noSourcesItem(new QStandardItem(QStringLiteral("-")))
Jan Grulich's avatar
Jan Grulich committed
91
{
92
    m_saveAction->setVisible(false);
93
    m_saveAction->setToolTip(i18n("Changes to the priority of Flatpak sources must be applied before they will take effect."));
94
95
    connect(m_saveAction, &DiscoverAction::triggered, this, &FlatpakSourcesBackend::save);

96
97
    m_flathubAction->setObjectName(QStringLiteral("flathub"));
    m_flathubAction->setToolTip(i18n("Makes it possible to easily install the applications listed in https://flathub.org"));
Alexander Lohnau's avatar
Alexander Lohnau committed
98
    connect(m_flathubAction, &DiscoverAction::triggered, this, [this]() {
99
100
        addSource(QStringLiteral("https://flathub.org/repo/flathub.flatpakrepo"));
    });
101
102
103
104
105

    m_noSourcesItem->setEnabled(false);
    if (m_sources->rowCount() == 0) {
        m_sources->appendRow(m_noSourcesItem);
    }
106
107
108
109
110
}

FlatpakSourcesBackend::~FlatpakSourcesBackend()
{
    QStringList ids;
Alexander Lohnau's avatar
Alexander Lohnau committed
111
    for (int i = 0, c = m_sources->rowCount(); i < c; ++i) {
112
113
114
        auto it = m_sources->item(i);
        ids << it->data(IdRole).toString();
    }
115

116
117
118
    auto conf = KSharedConfig::openConfig();
    KConfigGroup group = conf->group("FlatpakSources");
    group.writeEntry("Sources", ids);
Aleix Pol Gonzalez's avatar
--leak    
Aleix Pol Gonzalez committed
119
120
121

    if (!m_noSourcesItem->model())
        delete m_noSourcesItem;
Jan Grulich's avatar
Jan Grulich committed
122
123
}

124
125
126
127
128
129
130
131
132
void FlatpakSourcesBackend::save()
{
    int last = INT_MIN;
    for (int i = m_sources->rowCount() - 1; i >= 0; --i) {
        auto it = m_sources->item(i);
        const int prio = it->data(PrioRole).toInt();
        if (prio <= last) {
            FlatpakSourceItem *sourceItem = static_cast<FlatpakSourceItem *>(it);
            flatpak_remote_set_prio(sourceItem->remote(), ++last);
133
134
135
136
137
138
            g_autoptr(GError) error = nullptr;
            if (!flatpak_installation_modify_remote(sourceItem->flatpakInstallation(), sourceItem->remote(), nullptr, &error)) {
                qDebug() << "failed setting priorities" << error->message;
            }

            it->setData(last, PrioRole);
139
140
141
142
        } else {
            last = prio;
        }
    }
143
    m_saveAction->setVisible(false);
144
145
}

Alexander Lohnau's avatar
Alexander Lohnau committed
146
QAbstractItemModel *FlatpakSourcesBackend::sources()
Jan Grulich's avatar
Jan Grulich committed
147
148
149
150
{
    return m_sources;
}

Jan Grulich's avatar
Jan Grulich committed
151
bool FlatpakSourcesBackend::addSource(const QString &id)
Jan Grulich's avatar
Jan Grulich committed
152
{
Alexander Lohnau's avatar
Alexander Lohnau committed
153
    FlatpakBackend *backend = qobject_cast<FlatpakBackend *>(parent());
154
    const QUrl flatpakrepoUrl(id);
155
156
157
158

    if (id.isEmpty() || !flatpakrepoUrl.isValid())
        return false;

Alexander Lohnau's avatar
Alexander Lohnau committed
159
    auto addSource = [=](AbstractResource *res) {
160
161
162
        if (res)
            backend->installApplication(res);
        else
163
            Q_EMIT backend->passiveMessage(i18n("Could not add the source %1", flatpakrepoUrl.toDisplayString()));
164
165
166
    };

    if (flatpakrepoUrl.isLocalFile()) {
167
168
169
170
171
        auto stream = new ResultsStream(QStringLiteral("FlatpakSource-") + flatpakrepoUrl.toDisplayString());
        backend->addSourceFromFlatpakRepo(flatpakrepoUrl, stream);
        connect(stream, &ResultsStream::resourcesFound, this, [addSource](const QVector<AbstractResource *> &res) {
            addSource(res.constFirst());
        });
172
    } else {
173
174
        AbstractResourcesBackend::Filters filter;
        filter.resourceUrl = flatpakrepoUrl;
Alexander Lohnau's avatar
Alexander Lohnau committed
175
        auto stream = new StoredResultsStream({backend->search(filter)});
176
        connect(stream, &StoredResultsStream::finished, this, [addSource, stream]() {
177
            const auto res = stream->resources();
178
            addSource(res.value(0));
179
180
181
        });
    }
    return true;
Jan Grulich's avatar
Jan Grulich committed
182
183
}

Alexander Lohnau's avatar
Alexander Lohnau committed
184
QStandardItem *FlatpakSourcesBackend::sourceById(const QString &id) const
Jan Grulich's avatar
Jan Grulich committed
185
{
Alexander Lohnau's avatar
Alexander Lohnau committed
186
187
    QStandardItem *sourceIt = nullptr;
    for (int i = 0, c = m_sources->rowCount(); i < c; ++i) {
188
189
190
191
192
193
        auto it = m_sources->item(i);
        if (it->data(IdRole) == id) {
            sourceIt = it;
            break;
        }
    }
194
195
    return sourceIt;
}
196

Alexander Lohnau's avatar
Alexander Lohnau committed
197
QStandardItem *FlatpakSourcesBackend::sourceByUrl(const QString &_url) const
198
{
199
200
    QUrl url(_url);

Alexander Lohnau's avatar
Alexander Lohnau committed
201
202
    QStandardItem *sourceIt = nullptr;
    for (int i = 0, c = m_sources->rowCount(); i < c && !sourceIt; ++i) {
203
        auto it = m_sources->item(i);
204
        if (url.matches(it->data(Qt::StatusTipRole).toUrl(), QUrl::StripTrailingSlash)) {
205
206
207
208
209
210
211
            sourceIt = it;
            break;
        }
    }
    return sourceIt;
}

212
213
214
bool FlatpakSourcesBackend::removeSource(const QString &id)
{
    auto sourceIt = sourceById(id);
215
    if (sourceIt) {
Alexander Lohnau's avatar
Alexander Lohnau committed
216
        FlatpakSourceItem *sourceItem = static_cast<FlatpakSourceItem *>(sourceIt);
Jan Grulich's avatar
Jan Grulich committed
217
        g_autoptr(GCancellable) cancellable = g_cancellable_new();
218
        g_autoptr(GError) error = nullptr;
219
220
221
        const auto installation = sourceItem->flatpakInstallation();

        g_autoptr(GPtrArray) refs = flatpak_installation_list_remote_refs_sync(installation, id.toUtf8().constData(), cancellable, &error);
222
223
224
225
226
        if (refs) {
            QHash<QString, QStringList> toRemoveHash;
            toRemoveHash.reserve(refs->len);
            QStringList toRemoveRefs;
            toRemoveRefs.reserve(refs->len);
Alexander Lohnau's avatar
Alexander Lohnau committed
227
            FlatpakBackend *backend = qobject_cast<FlatpakBackend *>(parent());
228
            for (uint i = 0; i < refs->len; i++) {
Alexander Lohnau's avatar
Alexander Lohnau committed
229
                FlatpakRef *ref = FLATPAK_REF(g_ptr_array_index(refs, i));
230
231

                g_autoptr(GError) error = nullptr;
Alexander Lohnau's avatar
Alexander Lohnau committed
232
233
234
235
236
237
238
                FlatpakInstalledRef *installedRef = flatpak_installation_get_installed_ref(installation,
                                                                                           flatpak_ref_get_kind(ref),
                                                                                           flatpak_ref_get_name(ref),
                                                                                           flatpak_ref_get_arch(ref),
                                                                                           flatpak_ref_get_branch(ref),
                                                                                           cancellable,
                                                                                           &error);
239
240
241
242
243
244
245
246
247
248
249
                if (installedRef) {
                    auto res = backend->getAppForInstalledRef(installation, installedRef);
                    const auto name = QString::fromUtf8(flatpak_ref_get_name(ref));
                    const auto refString = QString::fromUtf8(flatpak_ref_format_ref(ref));
                    if (!name.endsWith(QLatin1String(".Locale"))) {
                        if (res)
                            toRemoveHash[res->name()] << refString;
                        else
                            toRemoveHash[refString] << refString;
                    }
                    toRemoveRefs << refString;
250
                }
251
            }
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
            QStringList toRemove;
            toRemove.reserve(toRemoveHash.count());
            for (auto it = toRemoveHash.constBegin(), itEnd = toRemoveHash.constEnd(); it != itEnd; ++it) {
                if (it.value().count() > 1)
                    toRemove << QStringLiteral("%1 - %2").arg(it.key(), it.value().join(QLatin1String(", ")));
                else
                    toRemove << it.key();
            }
            toRemove.sort();

            if (!toRemove.isEmpty()) {
                m_proceedFunctions.push([this, toRemoveRefs, installation, id] {
                    g_autoptr(GError) localError = nullptr;
                    g_autoptr(GCancellable) cancellable = g_cancellable_new();
                    g_autoptr(FlatpakTransaction) transaction = flatpak_transaction_new_for_installation(installation, cancellable, &localError);
Alexander Lohnau's avatar
Alexander Lohnau committed
267
                    for (const QString &instRef : qAsConst(toRemoveRefs)) {
268
269
270
271
272
273
274
275
276
277
278
                        const QByteArray refString = instRef.toUtf8();
                        flatpak_transaction_add_uninstall(transaction, refString.constData(), &localError);
                        if (localError)
                            return;
                    }

                    if (flatpak_transaction_run(transaction, cancellable, &localError)) {
                        removeSource(id);
                    }
                });

Alexander Lohnau's avatar
Alexander Lohnau committed
279
280
281
                Q_EMIT proceedRequest(i18n("Removing '%1'", id),
                                      i18n("To remove this repository, the following applications must be uninstalled:<ul><li>%1</li></ul>",
                                           toRemove.join(QStringLiteral("</li><li>"))));
282
283
284
285
                return false;
            }
        } else {
            qWarning() << "could not list refs in repo" << id << error->message;
286
287
        }

288
289
        g_autoptr(GError) errorRemoveRemote = nullptr;
        if (flatpak_installation_remove_remote(installation, id.toUtf8().constData(), cancellable, &errorRemoveRemote)) {
Jan Grulich's avatar
Jan Grulich committed
290
            m_sources->removeRow(sourceItem->row());
291
292
293
294

            if (m_sources->rowCount() == 0) {
                m_sources->appendRow(m_noSourcesItem);
            }
Jan Grulich's avatar
Jan Grulich committed
295
296
            return true;
        } else {
297
            Q_EMIT passiveMessage(i18n("Failed to remove %1 remote repository: %2", id, QString::fromUtf8(errorRemoveRemote->message)));
Jan Grulich's avatar
Jan Grulich committed
298
299
            return false;
        }
Jan Grulich's avatar
Jan Grulich committed
300
    } else {
301
        Q_EMIT passiveMessage(i18n("Could not find %1", id));
Jan Grulich's avatar
Jan Grulich committed
302
        return false;
Jan Grulich's avatar
Jan Grulich committed
303
    }
Jan Grulich's avatar
Jan Grulich committed
304
305

    return false;
Jan Grulich's avatar
Jan Grulich committed
306
307
}

308
QVariantList FlatpakSourcesBackend::actions() const
Jan Grulich's avatar
Jan Grulich committed
309
{
310
    return {QVariant::fromValue<QObject *>(m_flathubAction)};
Jan Grulich's avatar
Jan Grulich committed
311
312
}

313
314
void FlatpakSourcesBackend::addRemote(FlatpakRemote *remote, FlatpakInstallation *installation)
{
315
316
317
    if (flatpak_remote_get_noenumerate(remote)) {
        return;
    }
318
    const QString id = QString::fromUtf8(flatpak_remote_get_name(remote));
319
    const QString title = QString::fromUtf8(flatpak_remote_get_title(remote));
320
    const QUrl remoteUrl(QString::fromUtf8(flatpak_remote_get_url(remote)));
321

322
    const auto theActions = actions();
Alexander Lohnau's avatar
Alexander Lohnau committed
323
324
    for (const QVariant &act : theActions) {
        DiscoverAction *action = qobject_cast<DiscoverAction *>(act.value<QObject *>());
325
        if (action->objectName() == id) {
326
327
328
329
330
            action->setEnabled(false);
            action->setVisible(false);
        }
    }

331
332
333
334
    QString label = !title.isEmpty() ? title : id;
    if (flatpak_installation_get_is_user(installation)) {
        label = i18n("%1 (user)", label);
    }
335

336
337
338
339
340
341
342
343
    for (int i = 0, c = m_sources->rowCount(); i < c; ++i) {
        FlatpakSourceItem *item = static_cast<FlatpakSourceItem *>(m_sources->item(i));
        if (item->data(Qt::StatusTipRole) == remoteUrl && item->flatpakInstallation() == installation) {
            qDebug() << "we already have an item for this" << remoteUrl;
            return;
        }
    }

344
345
    FlatpakBackend *backend = qobject_cast<FlatpakBackend *>(parent());
    FlatpakSourceItem *it = new FlatpakSourceItem(label, remote, backend);
346
    const int prio = flatpak_remote_get_prio(remote);
347
    it->setData(remoteUrl.isLocalFile() ? remoteUrl.toLocalFile() : remoteUrl.host(), Qt::ToolTipRole);
348
    it->setData(remoteUrl, Qt::StatusTipRole);
349
    it->setData(id, IdRole);
350
    it->setData(prio, PrioRole);
351
    it->setCheckState(flatpak_remote_get_disabled(remote) ? Qt::Unchecked : Qt::Checked);
Alexander Lohnau's avatar
Alexander Lohnau committed
352
#if FLATPAK_CHECK_VERSION(1, 4, 0)
353
    it->setData(QString::fromUtf8(flatpak_remote_get_icon(remote)), IconUrlRole);
354
#endif
355
    it->setCheckable(true);
356
    it->setFlatpakInstallation(installation);
357

358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
    // Add the remotes before those with lower priorities, after the rest.
    // We disambiguate with internal discover settings
    const auto conf = KSharedConfig::openConfig();
    const KConfigGroup group = conf->group("FlatpakSources");
    const auto ids = group.readEntry<QStringList>("Sources", QStringList());
    const int ourIdx = ids.indexOf(id);

    int idx, c;
    for (c = m_sources->rowCount(), idx = c; idx < c; ++idx) {
        const auto compIt = m_sources->item(idx);
        if (prio > compIt->data(PrioRole).toInt()) {
            break;
        }
        const int compIdx = ids.indexOf(compIt->data(IdRole).toString());
        if (compIdx >= ourIdx) {
            break;
374
375
376
377
378
379
380
        }
    }

    m_sources->insertRow(idx, it);
    if (m_sources->rowCount() == 1)
        Q_EMIT firstSourceIdChanged();
    Q_EMIT lastSourceIdChanged();
381
382
383
384

    if (m_sources->rowCount() > 0) {
        m_sources->takeRow(m_noSourcesItem->row());
    }
385
}
386
387
388

QString FlatpakSourcesBackend::idDescription()
{
389
    return i18n("Enter a Flatpak repository URI (*.flatpakrepo):");
390
}
391

Alexander Lohnau's avatar
Alexander Lohnau committed
392
bool FlatpakSourcesBackend::moveSource(const QString &sourceId, int delta)
393
{
394
395
396
397
    auto item = sourceById(sourceId);
    if (!item)
        return false;
    const auto row = item->row();
398
399
400
    auto prevRow = m_sources->takeRow(row);
    Q_ASSERT(!prevRow.isEmpty());

401
    const auto destRow = row + delta;
402
403
404
405
406
    m_sources->insertRow(destRow, prevRow);
    if (destRow == 0 || row == 0)
        Q_EMIT firstSourceIdChanged();
    if (destRow == m_sources->rowCount() - 1 || row == m_sources->rowCount() - 1)
        Q_EMIT lastSourceIdChanged();
407
    m_saveAction->setVisible(true);
408
409
410
    return true;
}

Alexander Lohnau's avatar
Alexander Lohnau committed
411
int FlatpakSourcesBackend::originIndex(const QString &sourceId) const
412
{
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
413
414
    auto item = sourceById(sourceId);
    return item ? item->row() : INT_MAX;
415
}
416
417
418
419
420
421
422
423
424
425

void FlatpakSourcesBackend::cancel()
{
    m_proceedFunctions.pop();
}

void FlatpakSourcesBackend::proceed()
{
    m_proceedFunctions.pop()();
}