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 }