/* * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez * SPDX-FileCopyrightText: 2013 Lukas Appelhans * SPDX-FileCopyrightText: 2024 Harald Sitter * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "PackageKitResource.h" #include "PackageKitBackend.h" #include "PackageKitMessages.h" #include "appstream/AppStreamUtils.h" #include "config-paths.h" #include #include #include #include #include #include #include #include #include #include #include #if defined(WITH_MARKDOWN) extern "C" { #include } #endif using namespace Qt::StringLiterals; const QStringList PackageKitResource::s_topObjects({QStringLiteral("qrc:/qml/DependenciesButton.qml")}); const QStringList PackageKitResource::s_bottomObjects({QStringLiteral("qrc:/qml/PackageKitPermissions.qml")}); PackageKitResource::PackageKitResource(QString packageName, QString summary, PackageKitBackend *parent) : AbstractResource(parent) , m_summary(std::move(summary)) , m_name(std::move(packageName)) { setObjectName(m_name); connect(this, &AbstractResource::stateChanged, &m_dependencies, &PackageKitDependencies::setDirty); connect(&m_dependencies, &PackageKitDependencies::dependenciesChanged, this, [this] { Q_EMIT dependenciesChanged(); Q_EMIT sizeChanged(); }); } QString PackageKitResource::name() const { return m_name; } QString PackageKitResource::packageName() const { return m_name; } QStringList PackageKitResource::allPackageNames() const { return {m_name}; } QString PackageKitResource::availablePackageId() const { // First we check if it's upgradeable and use this version to display const QSet pkgids = backend()->upgradeablePackageId(this); if (!pkgids.isEmpty()) { return *pkgids.constBegin(); } const auto it = m_packages.constFind(PackageKit::Transaction::InfoAvailable); if (it != m_packages.constEnd()) { return it->first(); } return installedPackageId(); } QString PackageKitResource::installedPackageId() const { const auto installed = m_packages[PackageKit::Transaction::InfoInstalled]; return installed.isEmpty() ? QString() : installed.first(); } QString PackageKitResource::comment() { return m_summary; } QString PackageKitResource::longDescription() { fetchDetails(); return m_details.description(); } QUrl PackageKitResource::homepage() { fetchDetails(); return QUrl(m_details.url()); } QVariant PackageKitResource::icon() const { return QStringLiteral("applications-other"); } static QMap s_translation = { {u"AGPL"_s, u"AGPL-3.0"_s}, // {u"AGPL3"_s, u"AGPL-3.0"_s}, // {u"Artistic2.0"_s, u"Artistic-2.0"_s}, // {u"Apache"_s, u"Apache-2.0"_s}, // {u"APACHE"_s, u"Apache-2.0"_s}, // {u"CCPL"_s, u"CC0-1.0"_s}, // {u"GPL2"_s, u"GPL-2.0"_s}, // {u"GPL3"_s, u"GPL-3.0"_s}, // {u"FDL1.2"_s, u"GFDL-1.2-only"_s}, // {u"FDL1.3"_s, u"GFDL-1.3-only"_s}, // {u"LGPL"_s, u"LGPL-2.1"_s}, // {u"LGPL3"_s, u"LGPL-3.0"_s}, // {u"MPL"_s, u"MPL-1.1"_s}, // {u"MPL2"_s, u"MPL-2.0"_s}, // {u"PerlArtistic"_s, u"Artistic-1.0-Perl"_s}, // {u"PHP"_s, u"PHP-3.01"_s}, // {u"PSF"_s, u"Python-2.0"_s}, // {u"RUBY"_s, u"Ruby"_s}, // {u"ZPL"_s, u"ZPL-2.1"_s}, // }; QJsonArray PackageKitResource::licenses() { fetchDetails(); if (!m_details.license().isEmpty()) { QString id = m_details.license(); if (!AppStream::SPDX::isLicenseId(id)) { auto spdxId = AppStream::SPDX::asSpdxId(id); if (!spdxId.isEmpty()) { id = spdxId; } } if (!AppStream::SPDX::isLicenseId(id)) { id = s_translation.value(id, id); } return {AppStreamUtils::license(id)}; } return {}; } QList PackageKitResource::addonsInformation() { return QList(); } QString PackageKitResource::availableVersion() const { return PackageKit::Daemon::packageVersion(availablePackageId()); } QString PackageKitResource::installedVersion() const { return PackageKit::Daemon::packageVersion(installedPackageId()); } quint64 PackageKitResource::size() { fetchDetails(); return m_details.size(); } QString PackageKitResource::origin() const { auto osRelease = AppStreamIntegration::global()->osRelease(); if (PackageKit::Daemon::backendName() == QStringLiteral("apt")) { // Debian and its derivatives have a defined scheme for repository origins that we can parse, to // guess a better origin name. QString pkgid = availablePackageId(); QString dataField = PackageKit::Daemon::packageData(pkgid); // The "data" field of a package-id may contain a modifier such as "auto:" or "manual:", so // we will need to strip that in case it exists to extract the actual origin. // The data field may look like "auto:debian-bookworm-main" or "google_llc-stable-main", // so we can set the OS name if we see it as origin prefix, and otherwise need to fall back // to the origin string. int i = dataField.indexOf(QLatin1Char(':')); QString origin = i > 0 ? dataField.mid(i + 1) : dataField; if (origin.startsWith(osRelease->id().toLower() + QLatin1Char('-'))) { return osRelease->name(); } else { return origin.isEmpty() ? i18n("Unknown Source") : origin; } } // PackageKit doesn't give us enough information to be able to distinguish // between 3rd-party repos (which generally have human-readable names) and // 1st-party distro repos (which generally name nonsense jargon names) which // would allow us to substitute the distro name for only the nonsense repos; // see https://github.com/PackageKit/PackageKit/issues/607 and // https://bugs.kde.org/show_bug.cgi?id=465204. // So for now always show the distro name. return osRelease->name(); } QString PackageKitResource::section() { return QString(); } AbstractResource::State PackageKitResource::state() { if (backend()->isPackageNameUpgradeable(this)) { return Upgradeable; } else if (m_packages.contains(PackageKit::Transaction::InfoInstalled)) { return Installed; } else if (m_packages.contains(PackageKit::Transaction::InfoAvailable)) { return None; } else { return Broken; } } void PackageKitResource::addPackageId(PackageKit::Transaction::Info info, const QString &packageId, bool arch) { auto oldState = state(); if (arch) { m_packages[info].archPkgIds.append(packageId); } else { m_packages[info].nonarchPkgIds.append(packageId); } if (oldState != state()) { Q_EMIT stateChanged(); } Q_EMIT versionsChanged(); } bool PackageKitResource::hasCategory(const QString & /*category*/) const { return false; } AbstractResource::Type PackageKitResource::type() const { return System; } void PackageKitResource::fetchDetails() { const QString pkgid = availablePackageId(); if (!m_details.isEmpty() || pkgid.isEmpty()) { return; } m_details.insert(QStringLiteral("fetching"), true); // we add an entry so it's not re-fetched. backend()->fetchDetails(pkgid); } void PackageKitResource::failedFetchingDetails(PackageKit::Transaction::Error error, const QString &msg) { qWarning() << "error fetching details" << error << msg; } QList PackageKitResource::dependencies() { return m_dependencies.dependencies(); } void PackageKitResource::setDetails(const PackageKit::Details &details) { const bool ourDetails = details.packageId() == availablePackageId(); if (!ourDetails) { return; } if (m_details != details) { const auto oldState = state(); const auto oldSize = m_details.size(); const auto oldLicense = m_details.license(); const auto oldDescription = m_details.description(); m_details = details; if (oldState != state()) { Q_EMIT stateChanged(); } if (!backend()->isFetching()) { Q_EMIT backend()->resourcesChanged(this, {"size", "homepage", "license"}); } if (oldSize != uint(size())) { Q_EMIT sizeChanged(); } if (oldLicense != m_details.license()) { Q_EMIT licensesChanged(); } if (oldDescription != m_details.description()) { Q_EMIT longDescriptionChanged(); } } } void PackageKitResource::fetchChangelog() { } void PackageKitResource::fetchUpdateDetails() { const auto pkgid = availablePackageId(); if (pkgid.isEmpty()) { auto a = new OneTimeAction( [this] { fetchUpdateDetails(); }, this); connect(this, &PackageKitResource::stateChanged, a, &OneTimeAction::trigger); return; } backend()->updateDetails().add(pkgid); } static void addIfNotEmpty(const QString &title, const QString &content, QString &where) { if (!content.isEmpty()) { where += QLatin1String("

") + title + QLatin1String(" ") + QString(content).replace(QLatin1Char('\n'), QLatin1String("
")) + QLatin1String("

"); } } QString PackageKitResource::joinPackages(const QStringList &pkgids, const QString &_sep, const QString &shadowPackage) { QStringList ret; for (const QString &pkgid : pkgids) { const auto pkgname = PackageKit::Daemon::packageName(pkgid); if (pkgname == shadowPackage) { ret += PackageKit::Daemon::packageVersion(pkgid); } else { ret += i18nc("package-name (version)", "%1 (%2)", pkgname, PackageKit::Daemon::packageVersion(pkgid)); } } const QString sep = _sep.isEmpty() ? i18nc("comma separating package names", ", ") : _sep; return ret.join(sep); } static QStringList urlToLinks(const QStringList &urls) { QStringList ret; for (const auto &url : urls) { ret += QStringLiteral("%1").arg(url); } return ret; } QString PackageKitResource::verifiedMessage() const { return i18n("Software verified by %1", displayOrigin()); } bool PackageKitResource::containsPackageId(const QString &pkgid) const { return kContains(m_packages, [pkgid](const auto &x) { return x.archPkgIds.contains(pkgid) || x.nonarchPkgIds.contains(pkgid); }); } void PackageKitResource::updateDetail(const QString &packageID, const QStringList & /*updates*/, const QStringList &obsoletes, const QStringList &vendorUrls, const QStringList & /*bugzillaUrls*/, const QStringList & /*cveUrls*/, PackageKit::Transaction::Restart restart, const QString &_updateText, const QString & /*changelog*/, PackageKit::Transaction::UpdateState state, const QDateTime & /*issued*/, const QDateTime & /*updated*/) { #if defined(WITH_MARKDOWN) const QByteArray xx = _updateText.toUtf8(); MMIOT *markdownHandle = mkd_string(xx.constData(), _updateText.size(), {}); #ifdef MARKDOWN3 mkd_flag_t *flags = mkd_flags(); mkd_set_flag_num(flags, MKD_FENCEDCODE); mkd_set_flag_num(flags, MKD_GITHUBTAGS); mkd_set_flag_num(flags, MKD_AUTOLINK); if (!mkd_compile(markdownHandle, flags)) { #else if (!mkd_compile(markdownHandle, MKD_FENCEDCODE | MKD_GITHUBTAGS | MKD_AUTOLINK)) { #endif m_changelog = _updateText; } else { char *htmlDocument; const int size = mkd_document(markdownHandle, &htmlDocument); m_changelog = QString::fromUtf8(htmlDocument, size); } mkd_cleanup(markdownHandle); #ifdef MARKDOWN3 mkd_free_flags(flags); #endif #else m_changelog = _updateText; #endif const auto name = PackageKit::Daemon::packageName(packageID); QString info; addIfNotEmpty(i18n("Obsoletes:"), joinPackages(obsoletes, {}, name), info); addIfNotEmpty(i18n("Release Notes:"), changelog(), info); addIfNotEmpty(i18n("Update State:"), PackageKitMessages::updateStateMessage(state), info); addIfNotEmpty(i18n("Restart:"), PackageKitMessages::restartMessage(restart), info); if (!vendorUrls.isEmpty()) { addIfNotEmpty(i18n("Vendor:"), urlToLinks(vendorUrls).join(QLatin1String(", ")), info); } Q_EMIT changelogFetched(info); } PackageKitBackend *PackageKitResource::backend() const { return qobject_cast(parent()); } QString PackageKitResource::sizeDescription() { auto baseDescription = AbstractResource::sizeDescription(); if (state() != AbstractResource::State::None) { return baseDescription; } if (!m_dependencies.hasFetchedDependencies()) { fetchDetails(); updatePackageIdForDependencies(); return baseDescription; } const auto dependenciesCount = m_dependencies.dependencies().count(); if (dependenciesCount == 0) { return baseDescription; } return i18np("%2 (plus %1 dependency)", "%2 (plus %1 dependencies)", dependenciesCount, baseDescription); } QString PackageKitResource::sourceIcon() const { return QStringLiteral("package-x-generic"); } QStringList PackageKitResource::topObjects() const { return s_topObjects; } QStringList PackageKitResource::bottomObjects() const { return s_bottomObjects; } void PackageKitResource::updatePackageIdForDependencies() { const auto packageId = isInstalled() ? installedPackageId() : availablePackageId(); m_dependencies.setPackageId(packageId); m_dependencies.refresh(); // In case packageId didn't actually change. } bool PackageKitResource::extendsItself() const { const auto extendsResources = backend()->resourcesByPackageNames>(extends()); if (extendsResources.isEmpty()) { return false; } const auto ourPackageNames = allPackageNames(); for (auto resource : extendsResources) { auto pkResource = qobject_cast(resource); if (pkResource->allPackageNames() != ourPackageNames) { return false; } } return true; } void PackageKitResource::runService(KService::Ptr service) const { auto *job = new KIO::ApplicationLauncherJob(service); connect(job, &KJob::finished, this, [this, service](KJob *job) { if (job->error()) { Q_EMIT backend()->passiveMessage(i18n("Failed to start '%1': %2", service->name(), job->errorString())); } }); job->start(); } bool PackageKitResource::isCritical() const { return false; } #include "moc_PackageKitResource.cpp"