1/*
2 SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org>
3 SPDX-FileCopyrightText: 2007-2010 Frederik Gladhorn <gladhorn@kde.org>
4 SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.1-or-later
7*/
8
9#include "enginebase.h"
10#include "enginebase_p.h"
11#include <knewstuffcore_debug.h>
12
13#include <KConfig>
14#include <KConfigGroup>
15#include <KFileUtils>
16#include <KFormat>
17#include <KLocalizedString>
18
19#include <QFileInfo>
20#include <QNetworkRequest>
21#include <QProcess>
22#include <QStandardPaths>
23#include <QThreadStorage>
24#include <QTimer>
25
26#include "attica/atticaprovider_p.h"
27#include "opds/opdsprovider_p.h"
28#include "resultsstream.h"
29#include "staticxml/staticxmlprovider_p.h"
30#include "transaction.h"
31#include "xmlloader_p.h"
32
33using namespace KNSCore;
34
35typedef QHash<QUrl, QPointer<XmlLoader>> EngineProviderLoaderHash;
36Q_GLOBAL_STATIC(QThreadStorage<EngineProviderLoaderHash>, s_engineProviderLoaders)
37
38EngineBase::EngineBase(QObject *parent)
39 : QObject(parent)
40 , d(new EngineBasePrivate)
41{
42 connect(sender: d->installation, signal: &Installation::signalInstallationError, context: this, slot: [this](const QString &message) {
43 Q_EMIT signalErrorCode(errorCode: ErrorCode::InstallationError, i18n("An error occurred during the installation process:\n%1", message), metadata: QVariant());
44 });
45}
46
47QStringList EngineBase::availableConfigFiles()
48{
49 QStringList configSearchLocations;
50 configSearchLocations << QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, //
51 QStringLiteral("knsrcfiles"),
52 options: QStandardPaths::LocateDirectory);
53 configSearchLocations << QStandardPaths::standardLocations(type: QStandardPaths::GenericConfigLocation);
54 return KFileUtils::findAllUniqueFiles(dirs: configSearchLocations, nameFilters: {QStringLiteral("*.knsrc")});
55}
56
57EngineBase::~EngineBase()
58{
59 if (d->cache) {
60 d->cache->writeRegistry();
61 }
62 delete d->atticaProviderManager;
63 delete d->installation;
64}
65
66bool EngineBase::init(const QString &configfile)
67{
68 qCDebug(KNEWSTUFFCORE) << "Initializing KNSCore::EngineBase from" << configfile;
69
70 QString resolvedConfigFilePath;
71 if (QFileInfo(configfile).isAbsolute()) {
72 resolvedConfigFilePath = configfile; // It is an absolute path
73 } else {
74 // Don't do the expensive search unless the config is relative
75 resolvedConfigFilePath = QStandardPaths::locate(type: QStandardPaths::GenericDataLocation, QStringLiteral("knsrcfiles/%1").arg(a: configfile));
76 }
77
78 if (!QFileInfo::exists(file: resolvedConfigFilePath)) {
79 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::ConfigFileError, i18n("Configuration file does not exist: \"%1\"", configfile), metadata: configfile);
80 qCCritical(KNEWSTUFFCORE) << "The knsrc file" << configfile << "does not exist";
81 return false;
82 }
83
84 const KConfig conf(resolvedConfigFilePath);
85
86 if (conf.accessMode() == KConfig::NoAccess) {
87 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::ConfigFileError, i18n("Configuration file exists, but cannot be opened: \"%1\"", configfile), metadata: configfile);
88 qCCritical(KNEWSTUFFCORE) << "The knsrc file" << configfile << "was found but could not be opened.";
89 return false;
90 }
91
92 const KConfigGroup group = conf.hasGroup(QStringLiteral("KNewStuff")) ? conf.group(QStringLiteral("KNewStuff")) : conf.group(QStringLiteral("KNewStuff3"));
93 if (!group.exists()) {
94 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::ConfigFileError, i18n("Configuration file is invalid: \"%1\"", configfile), metadata: configfile);
95 qCCritical(KNEWSTUFFCORE) << configfile << "doesn't contain a KNewStuff or KNewStuff3 section.";
96 return false;
97 }
98
99 d->name = group.readEntry(key: "Name");
100 d->categories = group.readEntry(key: "Categories", aDefault: QStringList());
101 qCDebug(KNEWSTUFFCORE) << "Categories: " << d->categories;
102 d->adoptionCommand = group.readEntry(key: "AdoptionCommand");
103 d->useLabel = group.readEntry(key: "UseLabel", i18n("Use"));
104 Q_EMIT useLabelChanged();
105 d->uploadEnabled = group.readEntry(key: "UploadEnabled", defaultValue: true);
106 Q_EMIT uploadEnabledChanged();
107
108 d->providerFileUrl = group.readEntry(key: "ProvidersUrl", defaultValue: QUrl(QStringLiteral("https://autoconfig.kde.org/ocs/providers.xml")));
109 if (group.readEntry(key: "UseLocalProvidersFile", defaultValue: false)) {
110 // The local providers file is called "appname.providers", to match "appname.knsrc"
111 d->providerFileUrl = QUrl::fromLocalFile(localfile: QLatin1String("%1.providers").arg(args: configfile.left(n: configfile.length() - 6)));
112 }
113
114 d->tagFilter = group.readEntry(key: "TagFilter", aDefault: QStringList(QStringLiteral("ghns_excluded!=1")));
115 d->downloadTagFilter = group.readEntry(key: "DownloadTagFilter", aDefault: QStringList());
116
117 QByteArray rawContentWarningType = group.readEntry(key: "ContentWarning", QByteArrayLiteral("Static"));
118 bool ok = false;
119 int value = QMetaEnum::fromType<ContentWarningType>().keyToValue(key: rawContentWarningType.constData(), ok: &ok);
120 if (ok) {
121 d->contentWarningType = static_cast<ContentWarningType>(value);
122 } else {
123 qCWarning(KNEWSTUFFCORE) << "Could not parse ContentWarning, invalid entry" << rawContentWarningType;
124 }
125
126 Q_EMIT contentWarningTypeChanged();
127
128 // Make sure that config is valid
129 QString error;
130 if (!d->installation->readConfig(group, errorMessage&: error)) {
131 Q_EMIT signalErrorCode(errorCode: ErrorCode::ConfigFileError,
132 i18n("Could not initialise the installation handler for %1:\n%2\n"
133 "This is a critical error and should be reported to the application author",
134 configfile,
135 error),
136 metadata: configfile);
137 return false;
138 }
139
140 const QString configFileBasename = QFileInfo(resolvedConfigFilePath).completeBaseName();
141 d->cache = Cache::getCache(appName: configFileBasename);
142 qCDebug(KNEWSTUFFCORE) << "Cache is" << d->cache << "for" << configFileBasename;
143 d->cache->readRegistry();
144
145 // Cache cleanup option, to help work around people deleting files from underneath KNewStuff (this
146 // happens a lot with e.g. wallpapers and icons)
147 if (d->installation->uncompressionSetting() == Installation::UseKPackageUncompression) {
148 d->shouldRemoveDeletedEntries = true;
149 }
150
151 d->shouldRemoveDeletedEntries = group.readEntry(key: "RemoveDeadEntries", defaultValue: d->shouldRemoveDeletedEntries);
152 if (d->shouldRemoveDeletedEntries) {
153 d->cache->removeDeletedEntries();
154 }
155
156 loadProviders();
157
158 return true;
159}
160
161void EngineBase::loadProviders()
162{
163 if (d->providerFileUrl.isEmpty()) {
164 // it would be nicer to move the attica stuff into its own class
165 qCDebug(KNEWSTUFFCORE) << "Using OCS default providers";
166 delete d->atticaProviderManager;
167 d->atticaProviderManager = new Attica::ProviderManager;
168 connect(sender: d->atticaProviderManager, signal: &Attica::ProviderManager::providerAdded, context: this, slot: &EngineBase::atticaProviderLoaded);
169 connect(sender: d->atticaProviderManager, signal: &Attica::ProviderManager::failedToLoad, context: this, slot: &EngineBase::slotProvidersFailed);
170 d->atticaProviderManager->loadDefaultProviders();
171 } else {
172 qCDebug(KNEWSTUFFCORE) << "loading providers from " << d->providerFileUrl;
173 Q_EMIT loadingProvider();
174
175 XmlLoader *loader = s_engineProviderLoaders()->localData().value(key: d->providerFileUrl);
176 if (!loader) {
177 qCDebug(KNEWSTUFFCORE) << "No xml loader for this url yet, so create one and temporarily store that" << d->providerFileUrl;
178 loader = new XmlLoader(this);
179 s_engineProviderLoaders()->localData().insert(key: d->providerFileUrl, value: loader);
180 connect(sender: loader, signal: &XmlLoader::signalLoaded, context: this, slot: [this]() {
181 s_engineProviderLoaders()->localData().remove(key: d->providerFileUrl);
182 });
183 connect(sender: loader, signal: &XmlLoader::signalFailed, context: this, slot: [this]() {
184 s_engineProviderLoaders()->localData().remove(key: d->providerFileUrl);
185 });
186 connect(sender: loader, signal: &XmlLoader::signalHttpError, context: this, slot: [this](int status, QList<QNetworkReply::RawHeaderPair> rawHeaders) {
187 if (status == 503) { // Temporarily Unavailable
188 QDateTime retryAfter;
189 static const QByteArray retryAfterKey{"Retry-After"};
190 for (const QNetworkReply::RawHeaderPair &headerPair : rawHeaders) {
191 if (headerPair.first == retryAfterKey) {
192 // Retry-After is not a known header, so we need to do a bit of running around to make that work
193 // Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that
194 // So, simple workaround, just pass it through a dummy request and get a formatted date out (the
195 // cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...)
196 QNetworkRequest dummyRequest;
197 dummyRequest.setRawHeader(headerName: QByteArray{"Last-Modified"}, value: headerPair.second);
198 retryAfter = dummyRequest.header(header: QNetworkRequest::LastModifiedHeader).toDateTime();
199 break;
200 }
201 }
202 QTimer::singleShot(interval: retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch(), receiver: this, slot: &EngineBase::loadProviders);
203 // if it's a matter of a human moment's worth of seconds, just reload
204 if (retryAfter.toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch() > 2) {
205 // more than that, spit out TryAgainLaterError to let the user know what we're doing with their time
206 static const KFormat formatter;
207 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::TryAgainLaterError,
208 i18n("The service is currently undergoing maintenance and is expected to be back in %1.",
209 formatter.formatSpelloutDuration(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch())),
210 metadata: {retryAfter});
211 }
212 }
213 });
214 loader->load(url: d->providerFileUrl);
215 }
216 connect(sender: loader, signal: &XmlLoader::signalLoaded, context: this, slot: &EngineBase::slotProviderFileLoaded);
217 connect(sender: loader, signal: &XmlLoader::signalFailed, context: this, slot: &EngineBase::slotProvidersFailed);
218 }
219}
220
221QString KNSCore::EngineBase::name() const
222{
223 return d->name;
224}
225
226QStringList EngineBase::categories() const
227{
228 return d->categories;
229}
230
231QList<Provider::CategoryMetadata> EngineBase::categoriesMetadata()
232{
233 return d->categoriesMetadata;
234}
235
236QList<Provider::SearchPreset> EngineBase::searchPresets()
237{
238 return d->searchPresets;
239}
240
241QString EngineBase::useLabel() const
242{
243 return d->useLabel;
244}
245
246bool EngineBase::uploadEnabled() const
247{
248 return d->uploadEnabled;
249}
250
251void EngineBase::addProvider(QSharedPointer<KNSCore::Provider> provider)
252{
253 qCDebug(KNEWSTUFFCORE) << "Engine addProvider called with provider with id " << provider->id();
254 d->providers.insert(key: provider->id(), value: provider);
255 provider->setTagFilter(d->tagFilter);
256 provider->setDownloadTagFilter(d->downloadTagFilter);
257 connect(sender: provider.data(), signal: &Provider::providerInitialized, context: this, slot: &EngineBase::providerInitialized);
258
259 connect(sender: provider.data(), signal: &Provider::signalError, context: this, slot: [this, provider](const QString &msg) {
260 Q_EMIT signalErrorCode(errorCode: ErrorCode::ProviderError, message: msg, metadata: d->providerFileUrl);
261 });
262 connect(sender: provider.data(), signal: &Provider::signalErrorCode, context: this, slot: &EngineBase::signalErrorCode);
263 connect(sender: provider.data(), signal: &Provider::signalInformation, context: this, slot: &EngineBase::signalMessage);
264 connect(sender: provider.data(), signal: &Provider::basicsLoaded, context: this, slot: &EngineBase::providersChanged);
265 Q_EMIT providersChanged();
266}
267
268void EngineBase::providerInitialized(Provider *p)
269{
270 qCDebug(KNEWSTUFFCORE) << "providerInitialized" << p->name();
271 p->setCachedEntries(d->cache->registryForProvider(providerId: p->id()));
272
273 for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(t&: d->providers)) {
274 if (!p->isInitialized()) {
275 return;
276 }
277 }
278 Q_EMIT signalProvidersLoaded();
279}
280
281void EngineBase::slotProvidersFailed()
282{
283 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::ProviderError,
284 i18n("Loading of providers from file: %1 failed", d->providerFileUrl.toString()),
285 metadata: d->providerFileUrl);
286}
287
288void EngineBase::slotProviderFileLoaded(const QDomDocument &doc)
289{
290 qCDebug(KNEWSTUFFCORE) << "slotProvidersLoaded";
291
292 bool isAtticaProviderFile = false;
293
294 // get each provider element, and create a provider object from it
295 QDomElement providers = doc.documentElement();
296
297 if (providers.tagName() == QLatin1String("providers")) {
298 isAtticaProviderFile = true;
299 } else if (providers.tagName() != QLatin1String("ghnsproviders") && providers.tagName() != QLatin1String("knewstuffproviders")) {
300 qWarning() << "No document in providers.xml.";
301 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::ProviderError,
302 i18n("Could not load get hot new stuff providers from file: %1", d->providerFileUrl.toString()),
303 metadata: d->providerFileUrl);
304 return;
305 }
306
307 QDomElement n = providers.firstChildElement(QStringLiteral("provider"));
308 while (!n.isNull()) {
309 qCDebug(KNEWSTUFFCORE) << "Provider attributes: " << n.attribute(QStringLiteral("type"));
310
311 QSharedPointer<KNSCore::Provider> provider;
312 if (isAtticaProviderFile || n.attribute(QStringLiteral("type")).toLower() == QLatin1String("rest")) {
313 provider.reset(t: new AtticaProvider(d->categories, {}));
314 connect(sender: provider.data(), signal: &Provider::categoriesMetadataLoded, context: this, slot: [this](const QList<Provider::CategoryMetadata> &categories) {
315 d->categoriesMetadata = categories;
316 Q_EMIT signalCategoriesMetadataLoded(categories);
317 });
318#ifdef SYNDICATION_FOUND
319 } else if (n.attribute(QStringLiteral("type")).toLower() == QLatin1String("opds")) {
320 provider.reset(t: new OPDSProvider);
321 connect(sender: provider.data(), signal: &Provider::searchPresetsLoaded, context: this, slot: [this](const QList<Provider::SearchPreset> &presets) {
322 d->searchPresets = presets;
323 Q_EMIT signalSearchPresetsLoaded(presets);
324 });
325#endif
326 } else {
327 provider.reset(t: new StaticXmlProvider);
328 }
329
330 if (provider->setProviderXML(n)) {
331 addProvider(provider);
332 } else {
333 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::ProviderError, i18n("Error initializing provider."), metadata: d->providerFileUrl);
334 }
335 n = n.nextSiblingElement();
336 }
337 Q_EMIT loadingProvider();
338}
339
340void EngineBase::atticaProviderLoaded(const Attica::Provider &atticaProvider)
341{
342 qCDebug(KNEWSTUFFCORE) << "atticaProviderLoaded called";
343 if (!atticaProvider.hasContentService()) {
344 qCDebug(KNEWSTUFFCORE) << "Found provider: " << atticaProvider.baseUrl() << " but it does not support content";
345 return;
346 }
347 QSharedPointer<KNSCore::Provider> provider = QSharedPointer<KNSCore::Provider>(new AtticaProvider(atticaProvider, d->categories, {}));
348 connect(sender: provider.data(), signal: &Provider::categoriesMetadataLoded, context: this, slot: [this](const QList<Provider::CategoryMetadata> &categories) {
349 d->categoriesMetadata = categories;
350 Q_EMIT signalCategoriesMetadataLoded(categories);
351 });
352 addProvider(provider);
353}
354
355QSharedPointer<Cache> EngineBase::cache() const
356{
357 return d->cache;
358}
359
360void EngineBase::setTagFilter(const QStringList &filter)
361{
362 d->tagFilter = filter;
363 for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(t&: d->providers)) {
364 p->setTagFilter(d->tagFilter);
365 }
366}
367
368QStringList EngineBase::tagFilter() const
369{
370 return d->tagFilter;
371}
372
373void KNSCore::EngineBase::addTagFilter(const QString &filter)
374{
375 d->tagFilter << filter;
376 for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(t&: d->providers)) {
377 p->setTagFilter(d->tagFilter);
378 }
379}
380
381void EngineBase::setDownloadTagFilter(const QStringList &filter)
382{
383 d->downloadTagFilter = filter;
384 for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(t&: d->providers)) {
385 p->setDownloadTagFilter(d->downloadTagFilter);
386 }
387}
388
389QStringList EngineBase::downloadTagFilter() const
390{
391 return d->downloadTagFilter;
392}
393
394void EngineBase::addDownloadTagFilter(const QString &filter)
395{
396 d->downloadTagFilter << filter;
397 for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(t&: d->providers)) {
398 p->setDownloadTagFilter(d->downloadTagFilter);
399 }
400}
401
402QList<Attica::Provider *> EngineBase::atticaProviders() const
403{
404 QList<Attica::Provider *> ret;
405 ret.reserve(asize: d->providers.size());
406 for (const auto &p : std::as_const(t&: d->providers)) {
407 const auto atticaProvider = p.dynamicCast<AtticaProvider>();
408 if (atticaProvider) {
409 ret += atticaProvider->provider();
410 }
411 }
412 return ret;
413}
414
415bool EngineBase::userCanVote(const Entry &entry)
416{
417 QSharedPointer<Provider> p = d->providers.value(key: entry.providerId());
418 return p->userCanVote();
419}
420
421void EngineBase::vote(const Entry &entry, uint rating)
422{
423 QSharedPointer<Provider> p = d->providers.value(key: entry.providerId());
424 p->vote(entry, rating);
425}
426
427bool EngineBase::userCanBecomeFan(const Entry &entry)
428{
429 QSharedPointer<Provider> p = d->providers.value(key: entry.providerId());
430 return p->userCanBecomeFan();
431}
432
433void EngineBase::becomeFan(const Entry &entry)
434{
435 QSharedPointer<Provider> p = d->providers.value(key: entry.providerId());
436 p->becomeFan(entry);
437}
438
439QSharedPointer<Provider> EngineBase::provider(const QString &providerId) const
440{
441 return d->providers.value(key: providerId);
442}
443
444QSharedPointer<Provider> EngineBase::defaultProvider() const
445{
446 if (d->providers.count() > 0) {
447 return d->providers.constBegin().value();
448 }
449 return nullptr;
450}
451
452QStringList EngineBase::providerIDs() const
453{
454 return d->providers.keys();
455}
456
457bool EngineBase::hasAdoptionCommand() const
458{
459 return !d->adoptionCommand.isEmpty();
460}
461
462void EngineBase::updateStatus()
463{
464}
465
466Installation *EngineBase::installation() const
467{
468 return d->installation;
469}
470
471ResultsStream *EngineBase::search(const Provider::SearchRequest &request)
472{
473 return new ResultsStream(request, this);
474}
475
476EngineBase::ContentWarningType EngineBase::contentWarningType() const
477{
478 return d->contentWarningType;
479}
480
481QList<QSharedPointer<Provider>> EngineBase::providers() const
482{
483 return d->providers.values();
484}
485
486#include "moc_enginebase.cpp"
487

source code of knewstuff/src/core/enginebase.cpp