File indexing completed on 2024-09-15 04:49:03

0001 /*
0002  *   SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez <aleixpol@kde.org>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "AppStreamUtils.h"
0008 
0009 #include "utils.h"
0010 #include <AppStreamQt/pool.h>
0011 #include <AppStreamQt/release.h>
0012 #include <AppStreamQt/screenshot.h>
0013 #include <AppStreamQt/spdx.h>
0014 #include <AppStreamQt/version.h>
0015 #include <Category/Category.h>
0016 #include <KLocalizedString>
0017 #include <QDebug>
0018 #include <QJsonArray>
0019 #include <QJsonObject>
0020 #include <QUrlQuery>
0021 
0022 using namespace AppStreamUtils;
0023 
0024 QUrl AppStreamUtils::imageOfKind(const QList<AppStream::Image> &images, AppStream::Image::Kind kind)
0025 {
0026     QUrl ret;
0027     for (const AppStream::Image &i : images) {
0028         if (i.kind() == kind) {
0029             ret = i.url();
0030             break;
0031         }
0032     }
0033     return ret;
0034 }
0035 
0036 QString AppStreamUtils::changelogToHtml(const AppStream::Component &appdata)
0037 {
0038 #if ASQ_CHECK_VERSION(1, 0, 0)
0039     const auto releases = appdata.releasesPlain();
0040 #else
0041     const auto releases = appdata.releases();
0042 #endif
0043     if (releases.isEmpty()) {
0044         return {};
0045     }
0046 
0047 #if ASQ_CHECK_VERSION(1, 0, 0)
0048     const auto release = releases.indexSafe(0).value();
0049 #else
0050     const auto release = releases.constFirst();
0051 #endif
0052     if (release.description().isEmpty())
0053         return {};
0054 
0055     QString changelog =
0056         QLatin1String("<h3>") + release.version() + QLatin1String("</h3>") + QStringLiteral("<p>") + release.description() + QStringLiteral("</p>");
0057     return changelog;
0058 }
0059 
0060 Screenshots AppStreamUtils::fetchScreenshots(const AppStream::Component &appdata)
0061 {
0062     const auto appdataScreenshots = appdata.screenshotsAll();
0063     Screenshots ret;
0064     ret.reserve(appdataScreenshots.size());
0065     for (const AppStream::Screenshot &s : appdataScreenshots) {
0066         const auto images = s.images();
0067         const QUrl thumbnail = AppStreamUtils::imageOfKind(images, AppStream::Image::KindThumbnail);
0068         const QUrl full = AppStreamUtils::imageOfKind(images, AppStream::Image::KindSource);
0069         if (full.isEmpty()) {
0070             qWarning() << "AppStreamUtils: Invalid screenshot for" << appdata.name();
0071         }
0072         const bool isAnimated = s.mediaKind() == AppStream::Screenshot::MediaKindVideo;
0073 
0074         ret.append(Screenshot{thumbnail.isEmpty() ? full : thumbnail, full, isAnimated});
0075     }
0076     return ret;
0077 }
0078 
0079 QJsonArray AppStreamUtils::licenses(const AppStream::Component &appdata)
0080 {
0081     return licenses(appdata.projectLicense());
0082 }
0083 
0084 QJsonArray AppStreamUtils::licenses(const QString &spdx)
0085 {
0086     static const QSet<QChar> tokens = {QLatin1Char('&'), QLatin1Char('+'), QLatin1Char('|'), QLatin1Char('^'), QLatin1Char('('), QLatin1Char(')')};
0087 
0088     QJsonArray ret;
0089     const auto licenses = AppStream::SPDX::tokenizeLicense(spdx);
0090     for (const auto &token : licenses) {
0091         if (token.size() == 1 && tokens.contains(token.at(0)))
0092             continue;
0093         ret += license(token.mid(1)); // tokenize prefixes with an @ for some reason
0094     }
0095     return ret;
0096 }
0097 
0098 QJsonObject AppStreamUtils::license(const QString &license)
0099 {
0100     bool publicLicense = false;
0101     QString name = license;
0102     if (license.startsWith(QLatin1String("LicenseRef-proprietary"))) {
0103         name = i18n("Proprietary");
0104     } else if (license == QLatin1String("LicenseRef-public-domain")) {
0105         name = i18n("Public Domain");
0106         publicLicense = true;
0107     }
0108 
0109     if (license.isEmpty()) {
0110         return {
0111             {QStringLiteral("name"), i18n("Unknown")},
0112             {QStringLiteral("hasFreedom"), true}, // give it the benefit of the doubt
0113         };
0114     }
0115     if (!AppStream::SPDX::isLicenseId(license))
0116         return {
0117             {QStringLiteral("name"), name},
0118             {QStringLiteral("hasFreedom"), true}, // give it the benefit of the doubt
0119         };
0120     return {
0121         {QStringLiteral("name"), name},
0122         {QStringLiteral("url"), {AppStream::SPDX::licenseUrl(license)}},
0123         {QStringLiteral("hasFreedom"), AppStream::SPDX::isFreeLicense(license) || publicLicense},
0124     };
0125 }
0126 
0127 QStringList AppStreamUtils::appstreamIds(const QUrl &appstreamUrl)
0128 {
0129     QStringList ret;
0130     ret += appstreamUrl.host().isEmpty() ? appstreamUrl.path() : appstreamUrl.host();
0131     if (appstreamUrl.hasQuery()) {
0132         QUrlQuery query(appstreamUrl);
0133         ret << query.queryItemValue(QStringLiteral("alt")).split(QLatin1Char(','), Qt::SkipEmptyParts);
0134     }
0135     if (ret.removeDuplicates() != 0) {
0136         qDebug() << "AppStreamUtils: Received malformed url" << appstreamUrl;
0137     }
0138     return ret;
0139 }
0140 
0141 QString AppStreamUtils::versionString(const QString &version, const AppStream::Component &appdata)
0142 {
0143     Q_UNUSED(appdata);
0144 
0145     if (version.isEmpty()) {
0146         return {};
0147     }
0148 
0149     return version;
0150 }
0151 
0152 QString AppStreamUtils::contentRatingDescription(const AppStream::Component &appdata)
0153 {
0154 #if ASQ_CHECK_VERSION(0, 15, 6)
0155     const auto ratings = appdata.contentRatings();
0156     QString ret;
0157     for (const auto &r : ratings) {
0158         const auto ratingIds = r.ratingIds();
0159         for (const auto &id : ratingIds) {
0160             if (r.value(id) != AppStream::ContentRating::RatingValueNone) {
0161                 ret += QLatin1String("* ") + r.description(id) + QLatin1Char('\n');
0162             }
0163         }
0164     }
0165 
0166     return ret;
0167 #else
0168     Q_UNUSED(appdata);
0169     return {};
0170 #endif
0171 }
0172 
0173 QString AppStreamUtils::contentRatingText(const AppStream::Component &appdata)
0174 {
0175 #if ASQ_CHECK_VERSION(0, 15, 6)
0176     const auto ratings = appdata.contentRatings();
0177     AppStream::ContentRating::RatingValue intensity = AppStream::ContentRating::RatingValueUnknown;
0178     for (const auto &r : ratings) {
0179         const auto ratingIds = r.ratingIds();
0180         for (const auto &id : ratingIds) {
0181             intensity = std::max(r.value(id), intensity);
0182         }
0183     }
0184 
0185     static QStringList texts = {
0186         {},
0187         i18nc("Open Age Ratings Service (https://hughsie.github.io/oars) description of content suitable for everyone", "All Audiences"),
0188         i18nc("Open Age Ratings Service (https://hughsie.github.io/oars) description of content with relatively benign themes only unsuitable for very young "
0189               "children, such as minor cartoon violence or mild profanity",
0190               "Mild Content"),
0191         i18nc("Open Age Ratings Service (https://hughsie.github.io/oars) description of content with some intense themes, such as somewhat realistic "
0192               "violence, references to sexuality, or adult profanity",
0193               "Moderate Content"),
0194         i18nc("Open Age Ratings Service (https://hughsie.github.io/oars) description of mature content that could be quite objectionable or unsuitable for "
0195               "young audiences, such as realistic graphic violence, extreme profanity or nudity, or glorification of drug use",
0196               "Intense Content"),
0197     };
0198     return texts[intensity];
0199 #else
0200     Q_UNUSED(appdata);
0201     return {};
0202 #endif
0203 }
0204 
0205 AbstractResource::ContentIntensity AppStreamUtils::contentRatingIntensity(const AppStream::Component &appdata)
0206 {
0207 #if ASQ_CHECK_VERSION(0, 15, 6)
0208     const auto ratings = appdata.contentRatings();
0209     AppStream::ContentRating::RatingValue intensity = AppStream::ContentRating::RatingValueUnknown;
0210     for (const auto &r : ratings) {
0211         const auto ratingIds = r.ratingIds();
0212         for (const auto &id : ratingIds) {
0213             intensity = std::max(r.value(id), intensity);
0214         }
0215     }
0216 
0217     static QVector<AbstractResource::ContentIntensity> intensities = {
0218         AbstractResource::Mild,
0219         AbstractResource::Mild,
0220         AbstractResource::Mild,
0221         AbstractResource::Intense,
0222         AbstractResource::Intense,
0223     };
0224     return intensities[intensity];
0225 #else
0226     Q_UNUSED(appdata);
0227     return {};
0228 #endif
0229 }
0230 
0231 uint AppStreamUtils::contentRatingMinimumAge(const AppStream::Component &appdata)
0232 {
0233 #if ASQ_CHECK_VERSION(0, 15, 6)
0234     const auto ratings = appdata.contentRatings();
0235     uint minimumAge = 0;
0236     for (const auto &r : ratings) {
0237         minimumAge = std::max(r.minimumAge(), minimumAge);
0238     }
0239     return minimumAge;
0240 #else
0241     Q_UNUSED(appdata);
0242     return 0;
0243 #endif
0244 }
0245 
0246 static void kRemoveDuplicates(AppStream::ComponentBox &input, AppStream::Bundle::Kind kind)
0247 {
0248     QSet<QString> ret;
0249     for (auto it = input.begin(); it != input.end();) {
0250         const auto key = kind == AppStream::Bundle::KindUnknown ? it->id() : it->bundle(kind).id();
0251         if (!ret.contains(key)) {
0252             ret << key;
0253             ++it;
0254         } else {
0255             it = input.erase(it);
0256         }
0257     }
0258 }
0259 
0260 AppStream::ComponentBox AppStreamUtils::componentsByCategories(AppStream::Pool *pool, Category *cat, AppStream::Bundle::Kind kind)
0261 {
0262     AppStream::ComponentBox ret(AppStream::ComponentBox::FlagNoChecks);
0263     for (const auto &categoryName : cat->involvedCategories()) {
0264         ret += pool->componentsByCategories({categoryName});
0265     }
0266     kRemoveDuplicates(ret, kind);
0267     return ret;
0268 }