diff --git a/libdiscover/DiscoverBackendsFactory.cpp b/libdiscover/DiscoverBackendsFactory.cpp index ef817306..890ffef7 100644 --- a/libdiscover/DiscoverBackendsFactory.cpp +++ b/libdiscover/DiscoverBackendsFactory.cpp @@ -1,131 +1,135 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "DiscoverBackendsFactory.h" #include "resources/AbstractResourcesBackend.h" #include "resources/ResourcesModel.h" #include "utils.h" #include "libdiscover_debug.h" #include #include #include #include #include #include #include #include #include Q_GLOBAL_STATIC(QStringList, s_requestedBackends) void DiscoverBackendsFactory::setRequestedBackends(const QStringList& backends) { *s_requestedBackends = backends; } bool DiscoverBackendsFactory::hasRequestedBackends() { return !s_requestedBackends->isEmpty(); } DiscoverBackendsFactory::DiscoverBackendsFactory() {} QVector DiscoverBackendsFactory::backend(const QString& name) const { if (QDir::isAbsolutePath(name) && QStandardPaths::isTestModeEnabled()) { return backendForFile(name, QFileInfo(name).fileName()); } else { return backendForFile(name, name); } } QVector DiscoverBackendsFactory::backendForFile(const QString& libname, const QString& name) const { QPluginLoader* loader = new QPluginLoader(QLatin1String("discover/") + libname, ResourcesModel::global()); // qCDebug(LIBDISCOVER_LOG) << "trying to load plugin:" << loader->fileName(); AbstractResourcesBackendFactory* f = qobject_cast(loader->instance()); if(!f) { qCWarning(LIBDISCOVER_LOG) << "error loading" << libname << loader->errorString() << loader->metaData(); return {}; } auto instances = f->newInstance(ResourcesModel::global(), name); if(instances.isEmpty()) { qCWarning(LIBDISCOVER_LOG) << "Couldn't find the backend: " << libname << "among" << allBackendNames(false, true); return instances; } return instances; } QStringList DiscoverBackendsFactory::allBackendNames(bool whitelist, bool allowDummy) const { if (whitelist) { QStringList whitelistNames = *s_requestedBackends; if (!whitelistNames.isEmpty()) return whitelistNames; } QStringList pluginNames; foreach (const QString &dir, QCoreApplication::libraryPaths()) { QDirIterator it(dir + QStringLiteral("/discover"), QDir::Files); while (it.hasNext()) { it.next(); if (QLibrary::isLibrary(it.fileName()) && (allowDummy || it.fileName() != QLatin1String("dummy-backend.so"))) { pluginNames += it.fileInfo().baseName(); } } } pluginNames.removeDuplicates(); //will happen when discover is installed twice on the system return pluginNames; } QVector DiscoverBackendsFactory::allBackends() const { QStringList names = allBackendNames(); auto ret = kTransform>(names, [this](const QString& name) { return backend(name); }); ret.removeAll(nullptr); if(ret.isEmpty()) qCWarning(LIBDISCOVER_LOG) << "Didn't find any Discover backend!"; return ret; } int DiscoverBackendsFactory::backendsCount() const { return allBackendNames().count(); } void DiscoverBackendsFactory::setupCommandLine(QCommandLineParser* parser) { parser->addOption(QCommandLineOption(QStringLiteral("backends"), i18n("List all the backends we'll want to have loaded, separated by comma ','."), QStringLiteral("names"))); } void DiscoverBackendsFactory::processCommandLine(QCommandLineParser* parser, bool test) { +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList backends = test ? QStringList{ QStringLiteral("dummy-backend") } : parser->value(QStringLiteral("backends")).split(QLatin1Char(','), QString::SkipEmptyParts); +#else + QStringList backends = test ? QStringList{ QStringLiteral("dummy-backend") } : parser->value(QStringLiteral("backends")).split(QLatin1Char(','), Qt::SkipEmptyParts); +#endif for(auto &backend: backends) { if (!backend.endsWith(QLatin1String("-backend"))) backend.append(QLatin1String("-backend")); } *s_requestedBackends = backends; } diff --git a/libdiscover/appstream/AppStreamUtils.cpp b/libdiscover/appstream/AppStreamUtils.cpp index 94e62c79..9cf68099 100644 --- a/libdiscover/appstream/AppStreamUtils.cpp +++ b/libdiscover/appstream/AppStreamUtils.cpp @@ -1,117 +1,121 @@ /*************************************************************************** * Copyright © 2017 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "AppStreamUtils.h" #include #include #include #include #include "utils.h" #include #include #include #include #if APPSTREAM_HAS_SPDX #include #endif using namespace AppStreamUtils; QUrl AppStreamUtils::imageOfKind(const QList& images, AppStream::Image::Kind kind) { QUrl ret; Q_FOREACH (const AppStream::Image &i, images) { if (i.kind() == kind) { ret = i.url(); break; } } return ret; } QString AppStreamUtils::changelogToHtml(const AppStream::Component& appdata) { if(appdata.releases().isEmpty()) return {}; const auto release = appdata.releases().constFirst(); if (release.description().isEmpty()) return {}; QString changelog = QLatin1String("

") + release.version() + QLatin1String("

") + QStringLiteral("

") + release.description() + QStringLiteral("

"); return changelog; } QPair, QList > AppStreamUtils::fetchScreenshots(const AppStream::Component& appdata) { QList screenshots, thumbnails; Q_FOREACH (const AppStream::Screenshot &s, appdata.screenshots()) { const auto images = s.images(); const QUrl thumbnail = AppStreamUtils::imageOfKind(images, AppStream::Image::KindThumbnail); const QUrl plain = AppStreamUtils::imageOfKind(images, AppStream::Image::KindSource); if (plain.isEmpty()) qWarning() << "invalid screenshot for" << appdata.name(); screenshots << plain; thumbnails << (thumbnail.isEmpty() ? plain : thumbnail); } return {thumbnails, screenshots}; } QJsonArray AppStreamUtils::licenses(const AppStream::Component& appdata) { #if APPSTREAM_HAS_SPDX QJsonArray ret; const auto licenses = AppStream::SPDX::tokenizeLicense(appdata.projectLicense()); #if !APPSTREAM_HAS_SPDX_LICENSEURL static const QLatin1String prop ("@LicenseRef-proprietary="); #endif for (const auto &token : licenses) { QString license = token; license.remove(0, 1); //tokenize prefixes with an @ for some reason if (!AppStream::SPDX::isLicenseId(license)) continue; #if APPSTREAM_HAS_SPDX_LICENSEURL ret.append(QJsonObject{ {QStringLiteral("name"), license}, {QStringLiteral("url"), { AppStream::SPDX::licenseUrl(license) } }}); #else if (license.startsWith(prop)) ret.append(QJsonObject{ {QStringLiteral("name"), i18n("Proprietary")}, {QStringLiteral("url"), license.mid(prop.size())} }); else ret.append(QJsonObject{ {QStringLiteral("name"), license}, {QStringLiteral("url"), { QLatin1String("https://spdx.org/licenses/") + AppStream::SPDX::asSpdxId(license) + QLatin1String(".html#licenseText") } }}); #endif } return ret; #else return { QJsonObject { {QStringLiteral("name"), appdata.projectLicense() } } }; #endif } QStringList AppStreamUtils::appstreamIds(const QUrl &appstreamUrl) { QStringList ret; ret += appstreamUrl.host().isEmpty() ? appstreamUrl.path() : appstreamUrl.host(); if (appstreamUrl.hasQuery()) { QUrlQuery query(appstreamUrl); +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) ret << query.queryItemValue(QStringLiteral("alt")).split(QLatin1Char(','), QString::SkipEmptyParts); +#else + ret << query.queryItemValue(QStringLiteral("alt")).split(QLatin1Char(','), Qt::SkipEmptyParts); +#endif } return ret; } diff --git a/libdiscover/appstream/OdrsReviewsBackend.cpp b/libdiscover/appstream/OdrsReviewsBackend.cpp index 80e19049..a1ca9e11 100644 --- a/libdiscover/appstream/OdrsReviewsBackend.cpp +++ b/libdiscover/appstream/OdrsReviewsBackend.cpp @@ -1,351 +1,339 @@ /*************************************************************************** * Copyright © 2013 Aleix Pol Gonzalez * * Copyright © 2017 Jan Grulich * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "OdrsReviewsBackend.h" #include "AppStreamIntegration.h" #include "CachedNetworkAccessManager.h" #include #include #include #include #include #include #include #include #include #include "libdiscover_debug.h" #include #include #include #include #include #include #include #include #include // #define APIURL "http://127.0.0.1:5000/1.0/reviews/api" #define APIURL "https://odrs.gnome.org/1.0/reviews/api" OdrsReviewsBackend::OdrsReviewsBackend() : AbstractReviewsBackend(nullptr) , m_isFetching(false) { bool fetchRatings = false; const QUrl ratingsUrl(QStringLiteral(APIURL "/ratings")); const QUrl fileUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/ratings/ratings")); const QDir cacheDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); // Create $HOME/.cache/discover/ratings folder cacheDir.mkdir(QStringLiteral("ratings")); if (QFileInfo::exists(fileUrl.toLocalFile())) { QFileInfo file(fileUrl.toLocalFile()); // Refresh the cached ratings if they are older than one day if (file.lastModified().msecsTo(QDateTime::currentDateTime()) > 1000 * 60 * 60 * 24) { fetchRatings = true; } } else { fetchRatings = true; } if (fetchRatings) { m_isFetching = true; KIO::FileCopyJob *getJob = KIO::file_copy(ratingsUrl, fileUrl, -1, KIO::Overwrite | KIO::HideProgressInfo); connect(getJob, &KIO::FileCopyJob::result, this, &OdrsReviewsBackend::ratingsFetched); } else { parseRatings(); } } void OdrsReviewsBackend::ratingsFetched(KJob *job) { m_isFetching = false; if (job->error()) { qCWarning(LIBDISCOVER_LOG) << "Failed to fetch ratings " << job->errorString(); } else { parseRatings(); } } static QString osName() { return AppStreamIntegration::global()->osRelease()->name(); } static QString userHash() { QString machineId; QFile file(QStringLiteral("/etc/machine-id")); if (file.open(QIODevice::ReadOnly)) { machineId = QString::fromUtf8(file.readAll()); file.close(); } if (machineId.isEmpty()) { return QString(); } QString salted = QStringLiteral("gnome-software[%1:%2]").arg(KUser().loginName(), machineId); return QString::fromUtf8(QCryptographicHash::hash(salted.toUtf8(), QCryptographicHash::Sha1).toHex()); } void OdrsReviewsBackend::fetchReviews(AbstractResource *app, int page) { Q_UNUSED(page) m_isFetching = true; const QJsonDocument document(QJsonObject{ {QStringLiteral("app_id"), app->appstreamId()}, {QStringLiteral("distro"), osName()}, {QStringLiteral("user_hash"), userHash()}, {QStringLiteral("version"), app->isInstalled() ? app->installedVersion() : app->availableVersion()}, {QStringLiteral("locale"), QLocale::system().name()}, {QStringLiteral("limit"), -1} }); const auto json = document.toJson(QJsonDocument::Compact); QNetworkRequest request(QUrl(QStringLiteral(APIURL "/fetch"))); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json; charset=utf-8")); request.setHeader(QNetworkRequest::ContentLengthHeader, json.size()); // Store reference to the app for which we request reviews request.setOriginatingObject(app); auto reply = nam()->post(request, json); connect(reply, &QNetworkReply::finished, this, &OdrsReviewsBackend::reviewsFetched); } void OdrsReviewsBackend::reviewsFetched() { QNetworkReply* reply = qobject_cast(sender()); QScopedPointer replyPtr(reply); const QByteArray data = reply->readAll(); -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto networkError = reply->error(); -#else - const auto networkError = reply->networkError(); -#endif if (networkError != QNetworkReply::NoError) { qCWarning(LIBDISCOVER_LOG) << "error fetching reviews:" << reply->errorString() << data; m_isFetching = false; return; } const QJsonDocument document = QJsonDocument::fromJson(data); AbstractResource *resource = qobject_cast(reply->request().originatingObject()); Q_ASSERT(resource); parseReviews(document, resource); } Rating * OdrsReviewsBackend::ratingForApplication(AbstractResource *app) const { if (app->appstreamId().isEmpty()) { return nullptr; } return m_ratings[app->appstreamId()]; } void OdrsReviewsBackend::submitUsefulness(Review *review, bool useful) { const QJsonDocument document(QJsonObject{ {QStringLiteral("app_id"), review->applicationName()}, {QStringLiteral("user_skey"), review->getMetadata(QStringLiteral("ODRS::user_skey")).toString()}, {QStringLiteral("user_hash"), userHash()}, {QStringLiteral("distro"), osName()}, {QStringLiteral("review_id"), QJsonValue(double(review->id()))} //if we really need uint64 we should get it in QJsonValue }); QNetworkRequest request(QUrl(QStringLiteral(APIURL) + (useful ? QLatin1String("/upvote") : QLatin1String("/downvote")))); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json; charset=utf-8")); request.setHeader(QNetworkRequest::ContentLengthHeader, document.toJson().size()); auto reply = nam()->post(request, document.toJson()); connect(reply, &QNetworkReply::finished, this, &OdrsReviewsBackend::usefulnessSubmitted); } void OdrsReviewsBackend::usefulnessSubmitted() { QNetworkReply* reply = qobject_cast(sender()); -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto networkError = reply->error(); -#else - const auto networkError = reply->networkError(); -#endif if (networkError == QNetworkReply::NoError) { qCWarning(LIBDISCOVER_LOG) << "Usefulness submitted"; } else { qCWarning(LIBDISCOVER_LOG) << "Failed to submit usefulness: " << reply->errorString(); } reply->deleteLater(); } QString OdrsReviewsBackend::userName() const { return KUser().property(KUser::FullName).toString(); } void OdrsReviewsBackend::submitReview(AbstractResource *res, const QString &summary, const QString &description, const QString &rating) { QJsonObject map = {{QStringLiteral("app_id"), res->appstreamId()}, {QStringLiteral("user_skey"), res->getMetadata(QStringLiteral("ODRS::user_skey")).toString()}, {QStringLiteral("user_hash"), userHash()}, {QStringLiteral("version"), res->isInstalled() ? res->installedVersion() : res->availableVersion()}, {QStringLiteral("locale"), QLocale::system().name()}, {QStringLiteral("distro"), osName()}, {QStringLiteral("user_display"), QJsonValue::fromVariant(KUser().property(KUser::FullName))}, {QStringLiteral("summary"), summary}, {QStringLiteral("description"), description}, {QStringLiteral("rating"), rating.toInt() * 10}}; const QJsonDocument document(map); QNetworkAccessManager *accessManager = nam(); QNetworkRequest request(QUrl(QStringLiteral(APIURL "/submit"))); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json; charset=utf-8")); request.setHeader(QNetworkRequest::ContentLengthHeader, document.toJson().size()); // Store what we need so we can immediately show our review once it is submitted // Use review_id 0 for now as odrs starts numbering from 1 and once reviews are re-downloaded we get correct id map.insert(QStringLiteral("review_id"), 0); res->addMetadata(QStringLiteral("ODRS::review_map"), map); request.setOriginatingObject(res); accessManager->post(request, document.toJson()); connect(accessManager, &QNetworkAccessManager::finished, this, &OdrsReviewsBackend::reviewSubmitted); } void OdrsReviewsBackend::reviewSubmitted(QNetworkReply *reply) { -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto networkError = reply->error(); -#else - const auto networkError = reply->networkError(); -#endif if (networkError == QNetworkReply::NoError) { qCWarning(LIBDISCOVER_LOG) << "Review submitted"; AbstractResource *resource = qobject_cast(reply->request().originatingObject()); const QJsonArray array = {resource->getMetadata(QStringLiteral("ODRS::review_map")).toObject()}; const QJsonDocument document(array); // Remove local file with reviews so we can re-download it next time to get our review QFile file(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/reviews/%1.json").arg(array.first().toObject().value(QStringLiteral("app_id")).toString())); file.remove(); parseReviews(document, resource); } else { qCWarning(LIBDISCOVER_LOG) << "Failed to submit review: " << reply->errorString(); } reply->deleteLater(); } void OdrsReviewsBackend::parseRatings() { QFile ratingsDocument(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/ratings/ratings")); if (ratingsDocument.open(QIODevice::ReadOnly)) { QJsonDocument jsonDocument = QJsonDocument::fromJson(ratingsDocument.readAll()); const QJsonObject jsonObject = jsonDocument.object(); m_ratings.reserve(jsonObject.size()); for (auto it = jsonObject.begin(); it != jsonObject.end(); it++) { QJsonObject appJsonObject = it.value().toObject(); const int ratingCount = appJsonObject.value(QLatin1String("total")).toInt(); QVariantMap ratingMap = { { QStringLiteral("star0"), appJsonObject.value(QLatin1String("star0")).toInt() }, { QStringLiteral("star1"), appJsonObject.value(QLatin1String("star1")).toInt() }, { QStringLiteral("star2"), appJsonObject.value(QLatin1String("star2")).toInt() }, { QStringLiteral("star3"), appJsonObject.value(QLatin1String("star3")).toInt() }, { QStringLiteral("star4"), appJsonObject.value(QLatin1String("star4")).toInt() }, { QStringLiteral("star5"), appJsonObject.value(QLatin1String("star5")).toInt() } }; Rating *rating = new Rating(it.key(), ratingCount, ratingMap); rating->setParent(this); m_ratings.insert(it.key(), rating); } ratingsDocument.close(); Q_EMIT ratingsReady(); } } void OdrsReviewsBackend::parseReviews(const QJsonDocument &document, AbstractResource *resource) { m_isFetching = false; Q_ASSERT(resource); if (!resource) { return; } QJsonArray reviews = document.array(); if (!reviews.isEmpty()) { QVector reviewList; for (auto it = reviews.begin(); it != reviews.end(); it++) { const QJsonObject review = it->toObject(); if (!review.isEmpty()) { const int usefulFavorable = review.value(QStringLiteral("karma_up")).toInt(); const int usefulTotal = review.value(QStringLiteral("karma_down")).toInt() + usefulFavorable; QDateTime dateTime; dateTime.setSecsSinceEpoch(review.value(QStringLiteral("date_created")).toInt()); ReviewPtr r(new Review(review.value(QStringLiteral("app_id")).toString(), resource->packageName(), review.value(QStringLiteral("locale")).toString(), review.value(QStringLiteral("summary")).toString(), review.value(QStringLiteral("description")).toString(), review.value(QStringLiteral("user_display")).toString(), dateTime, true, review.value(QStringLiteral("review_id")).toInt(), review.value(QStringLiteral("rating")).toInt() / 10, usefulTotal, usefulFavorable, review.value(QStringLiteral("version")).toString())); // We can also receive just a json with app name and user info so filter these out as there is no review if (!r->summary().isEmpty() && !r->reviewText().isEmpty()) { reviewList << r; // Needed for submitting usefulness r->addMetadata(QStringLiteral("ODRS::user_skey"), review.value(QStringLiteral("user_skey")).toString()); } // We should get at least user_skey needed for posting reviews resource->addMetadata(QStringLiteral("ODRS::user_skey"), review.value(QStringLiteral("user_skey")).toString()); } } Q_EMIT reviewsReady(resource, reviewList, false); } } bool OdrsReviewsBackend::isResourceSupported(AbstractResource* res) const { return !res->appstreamId().isEmpty(); } void OdrsReviewsBackend::emitRatingFetched(AbstractResourcesBackend* b, const QList& resources) const { b->emitRatingsReady(); foreach(AbstractResource* res, resources) { if (m_ratings.contains(res->appstreamId())) { Q_EMIT res->ratingFetched(); } } } QNetworkAccessManager * OdrsReviewsBackend::nam() { if (!m_delayedNam) { m_delayedNam = new CachedNetworkAccessManager(QStringLiteral("odrs"), this); } return m_delayedNam; } diff --git a/libdiscover/backends/KNSBackend/KNSBackend.cpp b/libdiscover/backends/KNSBackend/KNSBackend.cpp index 278d5e2d..d4cf5dba 100644 --- a/libdiscover/backends/KNSBackend/KNSBackend.cpp +++ b/libdiscover/backends/KNSBackend/KNSBackend.cpp @@ -1,611 +1,615 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ // Qt includes #include #include #include #include #include #include // Attica includes #include #include // KDE includes #include #include #include #include #include #include // DiscoverCommon includes #include "Transaction/Transaction.h" #include "Transaction/TransactionModel.h" #include "Category/Category.h" // Own includes #include "KNSBackend.h" #include "KNSResource.h" #include "KNSReviews.h" #include #include "utils.h" class KNSBackendFactory : public AbstractResourcesBackendFactory { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.muon.AbstractResourcesBackendFactory") Q_INTERFACES(AbstractResourcesBackendFactory) public: KNSBackendFactory() { connect(KNSCore::QuestionManager::instance(), &KNSCore::QuestionManager::askQuestion, this, [](KNSCore::Question* q) { qWarning() << q->question() << q->questionType(); q->setResponse(KNSCore::Question::InvalidResponse); }); } QVector newInstance(QObject* parent, const QString &/*name*/) const override { QVector ret; #if KNEWSTUFFCORE_VERSION_MAJOR==5 && KNEWSTUFFCORE_VERSION_MINOR>=57 QStringList locations = KNSCore::Engine::configSearchLocations(); #else QStringList locations = QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation); #endif QSet files; for (const QString &path: locations) { QDirIterator dirIt(path, {QStringLiteral("*.knsrc")}, QDir::Files); for(; dirIt.hasNext(); ) { dirIt.next(); if (files.contains(dirIt.fileName())) continue; files << dirIt.fileName(); auto bk = new KNSBackend(parent, QStringLiteral("plasma"), dirIt.filePath()); if (bk->isValid()) ret += bk; else delete bk; } } return ret; } }; Q_DECLARE_METATYPE(KNSCore::EntryInternal) KNSBackend::KNSBackend(QObject* parent, const QString& iconName, const QString &knsrc) : AbstractResourcesBackend(parent) , m_fetching(false) , m_isValid(true) , m_reviews(new KNSReviews(this)) , m_name(knsrc) , m_iconName(iconName) , m_updater(new StandardBackendUpdater(this)) { const QString fileName = QFileInfo(m_name).fileName(); setName(fileName); setObjectName(knsrc); const KConfig conf(m_name, KConfig::SimpleConfig); if (!conf.hasGroup("KNewStuff3")) { markInvalid(QStringLiteral("Config group not found! Check your KNS3 installation.")); return; } m_categories = QStringList{ fileName }; const KConfigGroup group = conf.group("KNewStuff3"); m_extends = group.readEntry("Extends", QStringList()); m_reviews->setProviderUrl(QUrl(group.readEntry("ProvidersUrl", QString()))); setFetching(true); // This ensures we have something to track when checking after the initialization timeout connect(this, &KNSBackend::initialized, this, [this](){ m_initialized = true; }); // If we have not initialized in 60 seconds, consider this KNS backend invalid QTimer::singleShot(60000, this, [this](){ if(!m_initialized) { markInvalid(i18n("Backend %1 took too long to initialize", m_displayName)); m_responsePending = false; Q_EMIT searchFinished(); Q_EMIT availableForQueries(); } }); const QVector> filters = { {CategoryFilter, fileName } }; const QSet backendName = { name() }; m_displayName = group.readEntry("Name", QString()); if (m_displayName.isEmpty()) { m_displayName = fileName.mid(0, fileName.indexOf(QLatin1Char('.'))); m_displayName[0] = m_displayName[0].toUpper(); } m_hasApplications = group.readEntry("X-Discover-HasApplications", false); const QStringList cats = group.readEntry("Categories", QStringList{}); QVector categories; if (cats.count() > 1) { m_categories += cats; for(const auto &cat: cats) { if (m_hasApplications) categories << new Category(cat, QStringLiteral("applications-other"), { {CategoryFilter, cat } }, backendName, {}, {}, true); else categories << new Category(cat, QStringLiteral("plasma"), { {CategoryFilter, cat } }, backendName, {}, {}, true); } } QVector topCategories{categories}; for (const auto &cat: categories) { const QString catName = cat->name().append(QLatin1Char('/')); for (const auto& potentialSubCat: categories) { if(potentialSubCat->name().startsWith(catName)) { cat->addSubcategory(potentialSubCat); topCategories.removeOne(potentialSubCat); } } } m_engine = new KNSCore::Engine(this); connect(m_engine, &KNSCore::Engine::signalErrorCode, this, &KNSBackend::signalErrorCode); connect(m_engine, &KNSCore::Engine::signalEntriesLoaded, this, &KNSBackend::receivedEntries, Qt::QueuedConnection); connect(m_engine, &KNSCore::Engine::signalEntryChanged, this, &KNSBackend::statusChanged, Qt::QueuedConnection); connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, this, &KNSBackend::detailsLoaded); connect(m_engine, &KNSCore::Engine::signalProvidersLoaded, this, &KNSBackend::fetchInstalled); connect(m_engine, &KNSCore::Engine::signalCategoriesMetadataLoded, this, [categories](const QList< KNSCore::Provider::CategoryMetadata>& categoryMetadatas){ for (const KNSCore::Provider::CategoryMetadata& category : categoryMetadatas) { for (Category* cat : categories) { if (cat->orFilters().count() > 0 && cat->orFilters().first().second == category.name) { cat->setName(category.displayName); break; } } } }); m_engine->setPageSize(100); m_engine->init(m_name); if(m_hasApplications) { auto actualCategory = new Category(m_displayName, QStringLiteral("applications-other"), filters, backendName, topCategories, QUrl(), false); auto applicationCategory = new Category(i18n("Applications"), QStringLiteral("applications-internet"), filters, backendName, { actualCategory }, QUrl(), false); applicationCategory->setAndFilter({ {CategoryFilter, QLatin1String("Application")} }); m_categories.append(applicationCategory->name()); m_rootCategories = { applicationCategory }; // Make sure we filter out any apps which won't run on the current system architecture QStringList tagFilter = m_engine->tagFilter(); if(QSysInfo::currentCpuArchitecture() == QLatin1String("arm")) { tagFilter << QLatin1String("application##architecture==armhf"); } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("arm64")) { tagFilter << QLatin1String("application##architecture==arm64"); } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("i386")) { tagFilter << QLatin1String("application##architecture==x86"); } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("ia64")) { tagFilter << QLatin1String("application##architecture==x86-64"); } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("x86_64")) { tagFilter << QLatin1String("application##architecture==x86"); tagFilter << QLatin1String("application##architecture==x86-64"); } m_engine->setTagFilter(tagFilter); } else { static const QSet knsrcPlasma = { QStringLiteral("aurorae.knsrc"), QStringLiteral("icons.knsrc"), QStringLiteral("kfontinst.knsrc"), QStringLiteral("lookandfeel.knsrc"), QStringLiteral("plasma-themes.knsrc"), QStringLiteral("plasmoids.knsrc"), QStringLiteral("wallpaper.knsrc"), QStringLiteral("xcursor.knsrc"), QStringLiteral("cgcgtk3.knsrc"), QStringLiteral("cgcicon.knsrc"), QStringLiteral("cgctheme.knsrc"), //GTK integration QStringLiteral("kwinswitcher.knsrc"), QStringLiteral("kwineffect.knsrc"), QStringLiteral("kwinscripts.knsrc"), //KWin QStringLiteral("comic.knsrc"), QStringLiteral("colorschemes.knsrc"), QStringLiteral("emoticons.knsrc"), QStringLiteral("plymouth.knsrc"), QStringLiteral("sddmtheme.knsrc"), QStringLiteral("wallpaperplugin.knsrc"), QStringLiteral("ksplash.knsrc"), QStringLiteral("window-decorations.knsrc") }; const auto iconName = knsrcPlasma.contains(fileName)? QStringLiteral("plasma") : QStringLiteral("applications-other"); auto actualCategory = new Category(m_displayName, iconName, filters, backendName, categories, QUrl(), true); const auto topLevelName = knsrcPlasma.contains(fileName)? i18n("Plasma Addons") : i18n("Application Addons"); auto addonsCategory = new Category(topLevelName, iconName, filters, backendName, {actualCategory}, QUrl(), true); m_rootCategories = { addonsCategory }; } } KNSBackend::~KNSBackend() { qDeleteAll(m_rootCategories); } void KNSBackend::markInvalid(const QString &message) { m_rootCategories.clear(); qWarning() << "invalid kns backend!" << m_name << "because:" << message; m_isValid = false; setFetching(false); Q_EMIT initialized(); } void KNSBackend::fetchInstalled() { auto search = new OneTimeAction([this]() { Q_EMIT startingSearch(); m_onePage = true; m_responsePending = true; m_engine->checkForInstalled(); }, this); if (m_responsePending) { connect(this, &KNSBackend::availableForQueries, search, &OneTimeAction::trigger, Qt::QueuedConnection); } else { search->trigger(); } } void KNSBackend::setFetching(bool f) { if(m_fetching!=f) { m_fetching = f; emit fetchingChanged(); if (!m_fetching) { Q_EMIT initialized(); } } } bool KNSBackend::isValid() const { return m_isValid; } KNSResource* KNSBackend::resourceForEntry(const KNSCore::EntryInternal& entry) { KNSResource* r = static_cast(m_resourcesByName.value(entry.uniqueId())); if (!r) { QStringList categories{name(), m_rootCategories.first()->name()}; const auto cats = m_engine->categoriesMetadata(); const int catIndex = kIndexOf(cats, [&entry](const KNSCore::Provider::CategoryMetadata& cat){ return entry.category() == cat.id; }); if (catIndex > -1) { categories << cats.at(catIndex).name; } if(m_hasApplications) { categories << QLatin1String("Application"); } r = new KNSResource(entry, categories, this); m_resourcesByName.insert(entry.uniqueId(), r); } else { r->setEntry(entry); } return r; } void KNSBackend::receivedEntries(const KNSCore::EntryInternal::List& entries) { m_responsePending = false; const auto filtered = kFilter(entries, [](const KNSCore::EntryInternal& entry){ return entry.isValid(); }); const auto resources = kTransform>(filtered, [this](const KNSCore::EntryInternal& entry){ return resourceForEntry(entry); }); if (!resources.isEmpty()) { Q_EMIT receivedResources(resources); } else { Q_EMIT searchFinished(); Q_EMIT availableForQueries(); setFetching(false); return; } // qDebug() << "received" << objectName() << this << m_resourcesByName.count(); if (m_onePage) { Q_EMIT availableForQueries(); setFetching(false); } } void KNSBackend::fetchMore() { if (m_responsePending) return; // We _have_ to set this first. If we do not, we may run into a situation where the // data request will conclude immediately, causing m_responsePending to remain true // for perpetuity as the slots will be called before the function returns. m_responsePending = true; m_engine->requestMoreData(); } void KNSBackend::statusChanged(const KNSCore::EntryInternal& entry) { resourceForEntry(entry); } void KNSBackend::signalErrorCode(const KNSCore::ErrorCode& errorCode, const QString& message, const QVariant& metadata) { QString error = message; qDebug() << "KNS error in" << m_displayName << ":" << errorCode << message << metadata; bool invalidFile = false; switch(errorCode) { case KNSCore::ErrorCode::UnknownError: // This is not supposed to be hit, of course, but any error coming to this point should be non-critical and safely ignored. break; case KNSCore::ErrorCode::NetworkError: // If we have a network error, we need to tell the user about it. This is almost always fatal, so mark invalid and tell the user. error = i18n("Network error in backend %1: %2", m_displayName, metadata.toInt()); markInvalid(error); invalidFile = true; break; case KNSCore::ErrorCode::OcsError: if(metadata.toInt() == 200) { // Too many requests, try again in a couple of minutes - perhaps we can simply postpone it automatically, and give a message? error = i18n("Too many requests sent to the server for backend %1. Please try again in a few minutes.", m_displayName); } else { // Unknown API error, usually something critical, mark as invalid and cry a lot error = i18n("Invalid %1 backend, contact your distributor.", m_displayName); markInvalid(error); invalidFile = true; } break; case KNSCore::ErrorCode::ConfigFileError: error = i18n("Invalid %1 backend, contact your distributor.", m_displayName); markInvalid(error); invalidFile = true; break; case KNSCore::ErrorCode::ProviderError: error = i18n("Invalid %1 backend, contact your distributor.", m_displayName); markInvalid(error); invalidFile = true; break; case KNSCore::ErrorCode::InstallationError: // This error is handled already, by forwarding the KNS engine's installer error message. break; case KNSCore::ErrorCode::ImageError: // Image fetching errors are not critical as such, but may lead to weird layout issues, might want handling... error = i18n("Could not fetch screenshot for the entry %1 in backend %2", metadata.toList().at(0).toString(), m_displayName); break; default: // Having handled all current error values, we should by all rights never arrive here, but for good order and future safety... error = i18n("Unhandled error in %1 backend. Contact your distributor.", m_displayName); break; } m_responsePending = false; Q_EMIT searchFinished(); Q_EMIT availableForQueries(); // Setting setFetching to false when we get an error ensures we don't end up in an eternally-fetching state this->setFetching(false); qWarning() << "kns error" << objectName() << error; if (!invalidFile) Q_EMIT passiveMessage(i18n("%1: %2", name(), error)); } class KNSTransaction : public Transaction { public: KNSTransaction(QObject* parent, KNSResource* res, Transaction::Role role) : Transaction(parent, res, role) , m_id(res->entry().uniqueId()) { setCancellable(false); auto manager = res->knsBackend()->engine(); connect(manager, &KNSCore::Engine::signalEntryChanged, this, &KNSTransaction::anEntryChanged); TransactionModel::global()->addTransaction(this); std::function actionFunction; auto engine = res->knsBackend()->engine(); if(role == RemoveRole) actionFunction = [res, engine]() { engine->uninstall(res->entry()); }; else if (res->linkIds().isEmpty()) actionFunction = [res, engine]() { engine->install(res->entry()); }; else actionFunction = [res, engine]() { const auto links = res->linkIds(); for(auto i : links) engine->install(res->entry(), i); }; QTimer::singleShot(0, res, actionFunction); } void anEntryChanged(const KNSCore::EntryInternal& entry) { if (entry.uniqueId() == m_id) { switch (entry.status()) { case KNS3::Entry::Invalid: qWarning() << "invalid status for" << entry.uniqueId() << entry.status(); break; case KNS3::Entry::Installing: case KNS3::Entry::Updating: setStatus(CommittingStatus); break; case KNS3::Entry::Downloadable: case KNS3::Entry::Installed: case KNS3::Entry::Deleted: case KNS3::Entry::Updateable: if (status() != DoneStatus) { setStatus(DoneStatus); } break; } } } void cancel() override {} private: const QString m_id; }; Transaction* KNSBackend::removeApplication(AbstractResource* app) { auto res = qobject_cast(app); return new KNSTransaction(this, res, Transaction::RemoveRole); } Transaction* KNSBackend::installApplication(AbstractResource* app) { auto res = qobject_cast(app); return new KNSTransaction(this, res, Transaction::InstallRole); } Transaction* KNSBackend::installApplication(AbstractResource* app, const AddonList& /*addons*/) { return installApplication(app); } int KNSBackend::updatesCount() const { return m_updater->updatesCount(); } AbstractReviewsBackend* KNSBackend::reviewsBackend() const { return m_reviews; } static ResultsStream* voidStream() { return new ResultsStream(QStringLiteral("KNS-void"), {}); } ResultsStream* KNSBackend::search(const AbstractResourcesBackend::Filters& filter) { if (!m_isValid || (!filter.resourceUrl.isEmpty() && filter.resourceUrl.scheme() != QLatin1String("kns")) || !filter.mimetype.isEmpty()) return voidStream(); if (filter.resourceUrl.scheme() == QLatin1String("kns")) { return findResourceByPackageName(filter.resourceUrl); } else if (filter.state >= AbstractResource::Installed) { auto stream = new ResultsStream(QStringLiteral("KNS-installed")); const auto start = [this, stream, filter]() { if (m_isValid) { auto filterFunction = [&filter](AbstractResource* r) { return r->state()>=filter.state && (r->name().contains(filter.search, Qt::CaseInsensitive) || r->comment().contains(filter.search, Qt::CaseInsensitive)); }; const auto ret = kFilter>(m_resourcesByName, filterFunction); if (!ret.isEmpty()) Q_EMIT stream->resourcesFound(ret); } stream->finish(); }; if (isFetching()) { connect(this, &KNSBackend::initialized, stream, start); } else { QTimer::singleShot(0, stream, start); } return stream; } else if ((!filter.category && !filter.search.isEmpty()) // Accept global searches // If there /is/ a category, make sure we actually are one of those requested before searching || (filter.category && kContains(m_categories, [&filter](const QString& cat) { return filter.category->matchesCategoryName(cat); }))) { auto r = new ResultsStream(QLatin1String("KNS-search-")+name()); searchStream(r, filter.search); return r; } return voidStream(); } void KNSBackend::searchStream(ResultsStream* stream, const QString &searchText) { Q_EMIT startingSearch(); auto start = [this, stream, searchText]() { Q_ASSERT(!isFetching()); if (!m_isValid) { stream->finish(); return; } // No need to explicitly launch a search, setting the search term already does that for us m_engine->setSearchTerm(searchText); m_onePage = false; m_responsePending = true; connect(stream, &ResultsStream::fetchMore, this, &KNSBackend::fetchMore); connect(this, &KNSBackend::receivedResources, stream, &ResultsStream::resourcesFound); connect(this, &KNSBackend::searchFinished, stream, &ResultsStream::finish); connect(this, &KNSBackend::startingSearch, stream, &ResultsStream::finish); }; if (m_responsePending) { connect(this, &KNSBackend::availableForQueries, stream, start, Qt::QueuedConnection); } else if (isFetching()) { connect(this, &KNSBackend::initialized, stream, start); } else { QTimer::singleShot(0, stream, start); } } ResultsStream * KNSBackend::findResourceByPackageName(const QUrl& search) { if (search.scheme() != QLatin1String("kns") || search.host() != name()) return voidStream(); +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const auto pathParts = search.path().split(QLatin1Char('/'), QString::SkipEmptyParts); +#else + const auto pathParts = search.path().split(QLatin1Char('/'), Qt::SkipEmptyParts); +#endif if (pathParts.size() != 2) { Q_EMIT passiveMessage(i18n("Wrong KNewStuff URI: %1", search.toString())); return voidStream(); } const auto providerid = pathParts.at(0); const auto entryid = pathParts.at(1); auto stream = new ResultsStream(QLatin1String("KNS-byname-")+entryid); auto start = [this, entryid, stream, providerid]() { m_responsePending = true; m_engine->fetchEntryById(entryid); m_onePage = false; connect(m_engine, &KNSCore::Engine::signalErrorCode, stream, &ResultsStream::finish); connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, stream, [this, stream, entryid, providerid](const KNSCore::EntryInternal &entry) { if (entry.uniqueId() == entryid && providerid == QUrl(entry.providerId()).host()) { Q_EMIT stream->resourcesFound({resourceForEntry(entry)}); } else qWarning() << "found invalid" << entryid << entry.uniqueId() << providerid << QUrl(entry.providerId()).host(); m_responsePending = false; QTimer::singleShot(0, this, &KNSBackend::availableForQueries); stream->finish(); }); }; if (m_responsePending) { connect(this, &KNSBackend::availableForQueries, stream, start); } else { start(); } return stream; } bool KNSBackend::isFetching() const { return m_fetching; } AbstractBackendUpdater* KNSBackend::backendUpdater() const { return m_updater; } QString KNSBackend::displayName() const { return QStringLiteral("KNewStuff"); } void KNSBackend::detailsLoaded(const KNSCore::EntryInternal& entry) { auto res = resourceForEntry(entry); res->longDescriptionChanged(); } #include "KNSBackend.moc"