File indexing completed on 2024-11-17 04:55:39

0001 /*
0002  *   SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0003  *   SPDX-FileCopyrightText: 2017 Jan Grulich <jgrulich@redhat.com>
0004  *
0005  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006  */
0007 
0008 #include "FlatpakSourcesBackend.h"
0009 #include "FlatpakBackend.h"
0010 #include "FlatpakResource.h"
0011 #include <KConfigGroup>
0012 #include <KLocalizedString>
0013 #include <KSharedConfig>
0014 #include <QDebug>
0015 #include <QNetworkAccessManager>
0016 #include <QNetworkReply>
0017 
0018 #include <QStandardPaths>
0019 #include <QTemporaryFile>
0020 #include <glib.h>
0021 #include <resources/DiscoverAction.h>
0022 #include <resources/StoredResultsStream.h>
0023 
0024 using namespace Qt::StringLiterals;
0025 
0026 class FlatpakSourceItem : public QStandardItem
0027 {
0028 public:
0029     FlatpakSourceItem(const QString &text, FlatpakRemote *remote, FlatpakBackend *backend)
0030         : QStandardItem(text)
0031         , m_remote(remote)
0032         , m_backend(backend)
0033     {
0034         g_object_ref(remote);
0035     }
0036     ~FlatpakSourceItem()
0037     {
0038         g_object_unref(m_remote);
0039     }
0040 
0041     void setFlatpakInstallation(FlatpakInstallation *installation)
0042     {
0043         m_installation = installation;
0044     }
0045 
0046     FlatpakInstallation *flatpakInstallation() const
0047     {
0048         return m_installation;
0049     }
0050 
0051     void setData(const QVariant &value, int role) override
0052     {
0053         // We check isCheckable() so the initial setting of the item doesn't trigger a change
0054         if (role == Qt::CheckStateRole && isCheckable()) {
0055             const bool disabled = flatpak_remote_get_disabled(m_remote);
0056             const bool requestedDisabled = Qt::Unchecked == value;
0057             if (disabled != requestedDisabled) {
0058                 flatpak_remote_set_disabled(m_remote, requestedDisabled);
0059                 g_autoptr(GError) error = nullptr;
0060                 if (!flatpak_installation_modify_remote(m_installation, m_remote, nullptr, &error)) {
0061                     qWarning() << "set disabled failed" << error->message;
0062                     return;
0063                 }
0064 
0065                 if (requestedDisabled) {
0066                     m_backend->unloadRemote(m_installation, m_remote);
0067                 } else {
0068                     m_backend->loadRemote(m_installation, m_remote);
0069                 }
0070             }
0071         }
0072         QStandardItem::setData(value, role);
0073     }
0074 
0075     FlatpakRemote *remote() const
0076     {
0077         return m_remote;
0078     }
0079 
0080 private:
0081     FlatpakInstallation *m_installation = nullptr;
0082     FlatpakRemote *const m_remote;
0083     FlatpakBackend *const m_backend;
0084 };
0085 
0086 FlatpakSourcesBackend::FlatpakSourcesBackend(const QVector<FlatpakInstallation *> &installations, AbstractResourcesBackend *parent)
0087     : AbstractSourcesBackend(parent)
0088     , m_preferredInstallation(installations.constFirst())
0089     , m_sources(new QStandardItemModel(this))
0090     , m_flathubAction(new DiscoverAction(u"flatpak-discover"_s, i18n("Add Flathub"), this))
0091     , m_saveAction(new DiscoverAction(u"dialog-ok-apply"_s, i18n("Apply Changes"), this))
0092     , m_noSourcesItem(new QStandardItem(QStringLiteral("-")))
0093 {
0094     m_saveAction->setVisible(false);
0095     m_saveAction->setToolTip(i18n("Changes to the priority of Flatpak sources must be applied before they will take effect."));
0096     connect(m_saveAction, &DiscoverAction::triggered, this, &FlatpakSourcesBackend::save);
0097 
0098     m_flathubAction->setObjectName(QStringLiteral("flathub"));
0099     m_flathubAction->setToolTip(i18n("Makes it possible to easily install the applications listed in https://flathub.org"));
0100     connect(m_flathubAction, &DiscoverAction::triggered, this, [this]() {
0101         addSource(QStringLiteral("https://dl.flathub.org/repo/flathub.flatpakrepo"));
0102     });
0103 
0104     m_noSourcesItem->setEnabled(false);
0105     if (m_sources->rowCount() == 0) {
0106         m_sources->appendRow(m_noSourcesItem);
0107     }
0108 }
0109 
0110 FlatpakSourcesBackend::~FlatpakSourcesBackend()
0111 {
0112     QStringList ids;
0113     for (int i = 0, c = m_sources->rowCount(); i < c; ++i) {
0114         auto it = m_sources->item(i);
0115         ids << it->data(IdRole).toString();
0116     }
0117 
0118     auto conf = KSharedConfig::openConfig();
0119     KConfigGroup group = conf->group(u"FlatpakSources"_s);
0120     group.writeEntry("Sources", ids);
0121 
0122     if (!m_noSourcesItem->model()) {
0123         delete m_noSourcesItem;
0124     }
0125 }
0126 
0127 void FlatpakSourcesBackend::save()
0128 {
0129     int last = INT_MIN;
0130     for (int i = m_sources->rowCount() - 1; i >= 0; --i) {
0131         auto it = m_sources->item(i);
0132         const int prio = it->data(PrioRole).toInt();
0133         if (prio <= last) {
0134             FlatpakSourceItem *sourceItem = static_cast<FlatpakSourceItem *>(it);
0135             flatpak_remote_set_prio(sourceItem->remote(), ++last);
0136             g_autoptr(GError) error = nullptr;
0137             if (!flatpak_installation_modify_remote(sourceItem->flatpakInstallation(), sourceItem->remote(), nullptr, &error)) {
0138                 qDebug() << "failed setting priorities" << error->message;
0139             }
0140 
0141             it->setData(last, PrioRole);
0142         } else {
0143             last = prio;
0144         }
0145     }
0146     m_saveAction->setVisible(false);
0147 }
0148 
0149 QAbstractItemModel *FlatpakSourcesBackend::sources()
0150 {
0151     return m_sources;
0152 }
0153 
0154 bool FlatpakSourcesBackend::addSource(const QString &id)
0155 {
0156     auto backend = qobject_cast<FlatpakBackend *>(parent());
0157     const QUrl flatpakrepoUrl(id);
0158 
0159     if (id.isEmpty() || !flatpakrepoUrl.isValid()) {
0160         return false;
0161     }
0162 
0163     auto addSource = [=](const StreamResult &res) {
0164         if (res.resource) {
0165             backend->installApplication(res.resource);
0166         } else {
0167             Q_EMIT backend->passiveMessage(i18n("Could not add the source %1", flatpakrepoUrl.toDisplayString()));
0168         }
0169     };
0170 
0171     if (flatpakrepoUrl.isLocalFile()) {
0172         auto stream = new ResultsStream(QStringLiteral("FlatpakSource-") + flatpakrepoUrl.toDisplayString());
0173         backend->addSourceFromFlatpakRepo(flatpakrepoUrl, stream);
0174         connect(stream, &ResultsStream::resourcesFound, this, [addSource](const QVector<StreamResult> &res) {
0175             addSource(res.constFirst());
0176         });
0177     } else {
0178         AbstractResourcesBackend::Filters filter;
0179         filter.resourceUrl = flatpakrepoUrl;
0180         auto stream = new StoredResultsStream({backend->search(filter)});
0181         connect(stream, &StoredResultsStream::finished, this, [addSource, stream]() {
0182             const auto res = stream->resources();
0183             addSource(res.value(0));
0184         });
0185     }
0186     return true;
0187 }
0188 
0189 QStandardItem *FlatpakSourcesBackend::sourceById(const QString &id) const
0190 {
0191     QStandardItem *sourceIt = nullptr;
0192     for (int i = 0, c = m_sources->rowCount(); i < c; ++i) {
0193         auto it = m_sources->item(i);
0194         if (it->data(IdRole) == id) {
0195             sourceIt = it;
0196             break;
0197         }
0198     }
0199     return sourceIt;
0200 }
0201 
0202 QStandardItem *FlatpakSourcesBackend::sourceByUrl(const QString &_url) const
0203 {
0204     QUrl url(_url);
0205 
0206     QStandardItem *sourceIt = nullptr;
0207     for (int i = 0, c = m_sources->rowCount(); i < c && !sourceIt; ++i) {
0208         auto it = m_sources->item(i);
0209         if (url.matches(it->data(Qt::StatusTipRole).toUrl(), QUrl::StripTrailingSlash)) {
0210             sourceIt = it;
0211             break;
0212         }
0213     }
0214     return sourceIt;
0215 }
0216 
0217 bool FlatpakSourcesBackend::removeSource(const QString &id)
0218 {
0219     auto sourceIt = sourceById(id);
0220     if (sourceIt) {
0221         FlatpakSourceItem *sourceItem = static_cast<FlatpakSourceItem *>(sourceIt);
0222         g_autoptr(GCancellable) cancellable = g_cancellable_new();
0223         g_autoptr(GError) error = nullptr;
0224         const auto installation = sourceItem->flatpakInstallation();
0225 
0226         g_autoptr(GPtrArray) refs = flatpak_installation_list_remote_refs_sync(installation, id.toUtf8().constData(), cancellable, &error);
0227         if (refs) {
0228             QHash<QString, QStringList> toRemoveHash;
0229             toRemoveHash.reserve(refs->len);
0230             QStringList toRemoveRefs;
0231             toRemoveRefs.reserve(refs->len);
0232             FlatpakBackend *backend = qobject_cast<FlatpakBackend *>(parent());
0233             for (uint i = 0; i < refs->len; i++) {
0234                 FlatpakRef *ref = FLATPAK_REF(g_ptr_array_index(refs, i));
0235 
0236                 g_autoptr(GError) error = nullptr;
0237                 FlatpakInstalledRef *installedRef = flatpak_installation_get_installed_ref(installation,
0238                                                                                            flatpak_ref_get_kind(ref),
0239                                                                                            flatpak_ref_get_name(ref),
0240                                                                                            flatpak_ref_get_arch(ref),
0241                                                                                            flatpak_ref_get_branch(ref),
0242                                                                                            cancellable,
0243                                                                                            &error);
0244                 if (installedRef) {
0245                     auto res = backend->getAppForInstalledRef(installation, installedRef);
0246                     const auto name = QString::fromUtf8(flatpak_ref_get_name(ref));
0247                     const auto refString = QString::fromUtf8(flatpak_ref_format_ref(ref));
0248                     if (!name.endsWith(QLatin1String(".Locale"))) {
0249                         if (res) {
0250                             toRemoveHash[res->name()] << refString;
0251                         } else {
0252                             toRemoveHash[refString] << refString;
0253                         }
0254                     }
0255                     toRemoveRefs << refString;
0256                 }
0257             }
0258             QStringList toRemove;
0259             toRemove.reserve(toRemoveHash.count());
0260             for (auto it = toRemoveHash.constBegin(), itEnd = toRemoveHash.constEnd(); it != itEnd; ++it) {
0261                 if (it.value().count() > 1) {
0262                     toRemove << QStringLiteral("%1 - %2").arg(it.key(), it.value().join(QLatin1String(", ")));
0263                 } else {
0264                     toRemove << it.key();
0265                 }
0266             }
0267             toRemove.sort();
0268 
0269             if (!toRemove.isEmpty()) {
0270                 m_proceedFunctions.push([this, toRemoveRefs, installation, id] {
0271                     g_autoptr(GError) localError = nullptr;
0272                     g_autoptr(GCancellable) cancellable = g_cancellable_new();
0273                     g_autoptr(FlatpakTransaction) transaction = flatpak_transaction_new_for_installation(installation, cancellable, &localError);
0274                     for (const QString &instRef : std::as_const(toRemoveRefs)) {
0275                         const QByteArray refString = instRef.toUtf8();
0276                         flatpak_transaction_add_uninstall(transaction, refString.constData(), &localError);
0277                         if (localError) {
0278                             return;
0279                         }
0280                     }
0281 
0282                     if (flatpak_transaction_run(transaction, cancellable, &localError)) {
0283                         removeSource(id);
0284                     }
0285                 });
0286 
0287                 Q_EMIT proceedRequest(i18n("Removing '%1'", id),
0288                                       i18n("To remove this repository, the following applications must be uninstalled:<ul><li>%1</li></ul>",
0289                                            toRemove.join(QStringLiteral("</li><li>"))));
0290                 return false;
0291             }
0292         } else {
0293             qWarning() << "could not list refs in repo" << id << error->message;
0294         }
0295 
0296         g_autoptr(GError) errorRemoveRemote = nullptr;
0297         if (flatpak_installation_remove_remote(installation, id.toUtf8().constData(), cancellable, &errorRemoveRemote)) {
0298             m_sources->removeRow(sourceItem->row());
0299 
0300             if (m_sources->rowCount() == 0) {
0301                 m_sources->appendRow(m_noSourcesItem);
0302             }
0303             return true;
0304         } else {
0305             Q_EMIT passiveMessage(i18n("Failed to remove %1 remote repository: %2", id, QString::fromUtf8(errorRemoveRemote->message)));
0306             return false;
0307         }
0308     } else {
0309         Q_EMIT passiveMessage(i18n("Could not find %1", id));
0310         return false;
0311     }
0312 
0313     return false;
0314 }
0315 
0316 QVariantList FlatpakSourcesBackend::actions() const
0317 {
0318     return {QVariant::fromValue<QObject *>(m_flathubAction)};
0319 }
0320 
0321 void FlatpakSourcesBackend::addRemote(FlatpakRemote *remote, FlatpakInstallation *installation)
0322 {
0323     if (flatpak_remote_get_noenumerate(remote)) {
0324         return;
0325     }
0326     const QString id = QString::fromUtf8(flatpak_remote_get_name(remote));
0327     const QString title = QString::fromUtf8(flatpak_remote_get_title(remote));
0328     const QUrl remoteUrl(QString::fromUtf8(flatpak_remote_get_url(remote)));
0329 
0330     const auto theActions = actions();
0331     for (const auto &variant : theActions) {
0332         auto action = variant.value<DiscoverAction *>();
0333         if (action && action->objectName() == id) {
0334             action->setEnabled(false);
0335             action->setVisible(false);
0336         }
0337     }
0338 
0339     QString label = !title.isEmpty() ? title : id;
0340     if (flatpak_installation_get_is_user(installation)) {
0341         label = i18n("%1 (user)", label);
0342     }
0343 
0344     for (int i = 0, c = m_sources->rowCount(); i < c; ++i) {
0345         auto genItem = m_sources->item(i);
0346         if (genItem == m_noSourcesItem) {
0347             continue;
0348         }
0349 
0350         auto item = static_cast<FlatpakSourceItem *>(m_sources->item(i));
0351         if (item->data(Qt::StatusTipRole) == remoteUrl && item->flatpakInstallation() == installation) {
0352             qDebug() << "we already have an item for this" << remoteUrl;
0353             return;
0354         }
0355     }
0356 
0357     auto backend = qobject_cast<FlatpakBackend *>(parent());
0358     auto it = new FlatpakSourceItem(label, remote, backend);
0359     const int prio = flatpak_remote_get_prio(remote);
0360     it->setData(remoteUrl.isLocalFile() ? remoteUrl.toLocalFile() : remoteUrl.host(), Qt::ToolTipRole);
0361     it->setData(remoteUrl, Qt::StatusTipRole);
0362     it->setData(id, IdRole);
0363     it->setData(prio, PrioRole);
0364     it->setCheckState(flatpak_remote_get_disabled(remote) ? Qt::Unchecked : Qt::Checked);
0365 #if FLATPAK_CHECK_VERSION(1, 4, 0)
0366     it->setData(QString::fromUtf8(flatpak_remote_get_icon(remote)), IconUrlRole);
0367 #endif
0368     it->setCheckable(true);
0369     it->setFlatpakInstallation(installation);
0370 
0371     // Add the remotes before those with lower priorities, after the rest.
0372     // We disambiguate with internal discover settings
0373     const auto conf = KSharedConfig::openConfig();
0374     const KConfigGroup group = conf->group(u"FlatpakSources"_s);
0375     const auto ids = group.readEntry<QStringList>("Sources", QStringList());
0376     const int ourIdx = ids.indexOf(id);
0377 
0378     int idx, c;
0379     for (c = m_sources->rowCount(), idx = c; idx < c; ++idx) {
0380         const auto compIt = m_sources->item(idx);
0381         if (prio > compIt->data(PrioRole).toInt()) {
0382             break;
0383         }
0384         const int compIdx = ids.indexOf(compIt->data(IdRole).toString());
0385         if (compIdx >= ourIdx) {
0386             break;
0387         }
0388     }
0389 
0390     m_sources->insertRow(idx, it);
0391     if (m_sources->rowCount() == 1)
0392         Q_EMIT firstSourceIdChanged();
0393     Q_EMIT lastSourceIdChanged();
0394 
0395     if (m_sources->rowCount() > 0) {
0396         m_sources->takeRow(m_noSourcesItem->row());
0397     }
0398 }
0399 
0400 QString FlatpakSourcesBackend::idDescription()
0401 {
0402     return i18n("Enter a Flatpak repository URI (*.flatpakrepo):");
0403 }
0404 
0405 bool FlatpakSourcesBackend::moveSource(const QString &sourceId, int delta)
0406 {
0407     auto item = sourceById(sourceId);
0408     if (!item) {
0409         return false;
0410     }
0411     const auto row = item->row();
0412     auto prevRow = m_sources->takeRow(row);
0413     Q_ASSERT(!prevRow.isEmpty());
0414 
0415     const auto destRow = row + delta;
0416     m_sources->insertRow(destRow, prevRow);
0417     if (destRow == 0 || row == 0) {
0418         Q_EMIT firstSourceIdChanged();
0419     }
0420     if (destRow == m_sources->rowCount() - 1 || row == m_sources->rowCount() - 1) {
0421         Q_EMIT lastSourceIdChanged();
0422     }
0423     m_saveAction->setVisible(true);
0424     return true;
0425 }
0426 
0427 int FlatpakSourcesBackend::originIndex(const QString &sourceId) const
0428 {
0429     auto item = sourceById(sourceId);
0430     return item ? item->row() : INT_MAX;
0431 }
0432 
0433 void FlatpakSourcesBackend::cancel()
0434 {
0435     m_proceedFunctions.pop();
0436 }
0437 
0438 void FlatpakSourcesBackend::proceed()
0439 {
0440     m_proceedFunctions.pop()();
0441 }