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

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