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 | |
33 | using namespace KNSCore; |
34 | |
35 | typedef QHash<QUrl, QPointer<XmlLoader>> EngineProviderLoaderHash; |
36 | Q_GLOBAL_STATIC(QThreadStorage<EngineProviderLoaderHash>, s_engineProviderLoaders) |
37 | |
38 | EngineBase::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 | |
47 | QStringList 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 | |
57 | EngineBase::~EngineBase() |
58 | { |
59 | if (d->cache) { |
60 | d->cache->writeRegistry(); |
61 | } |
62 | delete d->atticaProviderManager; |
63 | delete d->installation; |
64 | } |
65 | |
66 | bool 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 | |
161 | void 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> ) { |
187 | if (status == 503) { // Temporarily Unavailable |
188 | QDateTime retryAfter; |
189 | static const QByteArray retryAfterKey{"Retry-After" }; |
190 | for (const QNetworkReply::RawHeaderPair & : 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 | |
221 | QString KNSCore::EngineBase::name() const |
222 | { |
223 | return d->name; |
224 | } |
225 | |
226 | QStringList EngineBase::categories() const |
227 | { |
228 | return d->categories; |
229 | } |
230 | |
231 | QList<Provider::CategoryMetadata> EngineBase::categoriesMetadata() |
232 | { |
233 | return d->categoriesMetadata; |
234 | } |
235 | |
236 | QList<Provider::SearchPreset> EngineBase::searchPresets() |
237 | { |
238 | return d->searchPresets; |
239 | } |
240 | |
241 | QString EngineBase::useLabel() const |
242 | { |
243 | return d->useLabel; |
244 | } |
245 | |
246 | bool EngineBase::uploadEnabled() const |
247 | { |
248 | return d->uploadEnabled; |
249 | } |
250 | |
251 | void 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 | |
268 | void 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 | |
281 | void 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 | |
288 | void 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 | |
340 | void 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 | |
355 | QSharedPointer<Cache> EngineBase::cache() const |
356 | { |
357 | return d->cache; |
358 | } |
359 | |
360 | void 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 | |
368 | QStringList EngineBase::tagFilter() const |
369 | { |
370 | return d->tagFilter; |
371 | } |
372 | |
373 | void 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 | |
381 | void 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 | |
389 | QStringList EngineBase::downloadTagFilter() const |
390 | { |
391 | return d->downloadTagFilter; |
392 | } |
393 | |
394 | void 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 | |
402 | QList<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 | |
415 | bool EngineBase::userCanVote(const Entry &entry) |
416 | { |
417 | QSharedPointer<Provider> p = d->providers.value(key: entry.providerId()); |
418 | return p->userCanVote(); |
419 | } |
420 | |
421 | void 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 | |
427 | bool EngineBase::userCanBecomeFan(const Entry &entry) |
428 | { |
429 | QSharedPointer<Provider> p = d->providers.value(key: entry.providerId()); |
430 | return p->userCanBecomeFan(); |
431 | } |
432 | |
433 | void EngineBase::becomeFan(const Entry &entry) |
434 | { |
435 | QSharedPointer<Provider> p = d->providers.value(key: entry.providerId()); |
436 | p->becomeFan(entry); |
437 | } |
438 | |
439 | QSharedPointer<Provider> EngineBase::provider(const QString &providerId) const |
440 | { |
441 | return d->providers.value(key: providerId); |
442 | } |
443 | |
444 | QSharedPointer<Provider> EngineBase::defaultProvider() const |
445 | { |
446 | if (d->providers.count() > 0) { |
447 | return d->providers.constBegin().value(); |
448 | } |
449 | return nullptr; |
450 | } |
451 | |
452 | QStringList EngineBase::providerIDs() const |
453 | { |
454 | return d->providers.keys(); |
455 | } |
456 | |
457 | bool EngineBase::hasAdoptionCommand() const |
458 | { |
459 | return !d->adoptionCommand.isEmpty(); |
460 | } |
461 | |
462 | void EngineBase::updateStatus() |
463 | { |
464 | } |
465 | |
466 | Installation *EngineBase::installation() const |
467 | { |
468 | return d->installation; |
469 | } |
470 | |
471 | ResultsStream *EngineBase::search(const Provider::SearchRequest &request) |
472 | { |
473 | return new ResultsStream(request, this); |
474 | } |
475 | |
476 | EngineBase::ContentWarningType EngineBase::contentWarningType() const |
477 | { |
478 | return d->contentWarningType; |
479 | } |
480 | |
481 | QList<QSharedPointer<Provider>> EngineBase::providers() const |
482 | { |
483 | return d->providers.values(); |
484 | } |
485 | |
486 | #include "moc_enginebase.cpp" |
487 | |