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 }