File indexing completed on 2024-04-28 03:56:25
0001 /* 0002 SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org> 0003 SPDX-FileCopyrightText: 2007-2010 Frederik Gladhorn <gladhorn@kde.org> 0004 SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org> 0005 0006 SPDX-License-Identifier: LGPL-2.1-or-later 0007 */ 0008 0009 #include "enginebase.h" 0010 #include "enginebase_p.h" 0011 #include <knewstuffcore_debug.h> 0012 0013 #include <KConfig> 0014 #include <KConfigGroup> 0015 #include <KFileUtils> 0016 #include <KFormat> 0017 #include <KLocalizedString> 0018 0019 #include <QFileInfo> 0020 #include <QNetworkRequest> 0021 #include <QProcess> 0022 #include <QStandardPaths> 0023 #include <QThreadStorage> 0024 #include <QTimer> 0025 0026 #include "attica/atticaprovider_p.h" 0027 #include "opds/opdsprovider_p.h" 0028 #include "resultsstream.h" 0029 #include "staticxml/staticxmlprovider_p.h" 0030 #include "transaction.h" 0031 #include "xmlloader_p.h" 0032 0033 using namespace KNSCore; 0034 0035 typedef QHash<QUrl, QPointer<XmlLoader>> EngineProviderLoaderHash; 0036 Q_GLOBAL_STATIC(QThreadStorage<EngineProviderLoaderHash>, s_engineProviderLoaders) 0037 0038 EngineBase::EngineBase(QObject *parent) 0039 : QObject(parent) 0040 , d(new EngineBasePrivate) 0041 { 0042 connect(d->installation, &Installation::signalInstallationError, this, [this](const QString &message) { 0043 Q_EMIT signalErrorCode(ErrorCode::InstallationError, i18n("An error occurred during the installation process:\n%1", message), QVariant()); 0044 }); 0045 } 0046 0047 QStringList EngineBase::availableConfigFiles() 0048 { 0049 QStringList configSearchLocations; 0050 configSearchLocations << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, // 0051 QStringLiteral("knsrcfiles"), 0052 QStandardPaths::LocateDirectory); 0053 configSearchLocations << QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation); 0054 return KFileUtils::findAllUniqueFiles(configSearchLocations, {QStringLiteral("*.knsrc")}); 0055 } 0056 0057 EngineBase::~EngineBase() 0058 { 0059 if (d->cache) { 0060 d->cache->writeRegistry(); 0061 } 0062 delete d->atticaProviderManager; 0063 delete d->installation; 0064 } 0065 0066 bool EngineBase::init(const QString &configfile) 0067 { 0068 qCDebug(KNEWSTUFFCORE) << "Initializing KNSCore::EngineBase from" << configfile; 0069 0070 QString resolvedConfigFilePath; 0071 if (QFileInfo(configfile).isAbsolute()) { 0072 resolvedConfigFilePath = configfile; // It is an absolute path 0073 } else { 0074 // Don't do the expensive search unless the config is relative 0075 resolvedConfigFilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("knsrcfiles/%1").arg(configfile)); 0076 } 0077 0078 if (!QFileInfo::exists(resolvedConfigFilePath)) { 0079 Q_EMIT signalErrorCode(KNSCore::ErrorCode::ConfigFileError, i18n("Configuration file does not exist: \"%1\"", configfile), configfile); 0080 qCCritical(KNEWSTUFFCORE) << "The knsrc file" << configfile << "does not exist"; 0081 return false; 0082 } 0083 0084 const KConfig conf(resolvedConfigFilePath); 0085 0086 if (conf.accessMode() == KConfig::NoAccess) { 0087 Q_EMIT signalErrorCode(KNSCore::ErrorCode::ConfigFileError, i18n("Configuration file exists, but cannot be opened: \"%1\"", configfile), configfile); 0088 qCCritical(KNEWSTUFFCORE) << "The knsrc file" << configfile << "was found but could not be opened."; 0089 return false; 0090 } 0091 0092 const KConfigGroup group = conf.hasGroup(QStringLiteral("KNewStuff")) ? conf.group(QStringLiteral("KNewStuff")) : conf.group(QStringLiteral("KNewStuff3")); 0093 if (!group.exists()) { 0094 Q_EMIT signalErrorCode(KNSCore::ErrorCode::ConfigFileError, i18n("Configuration file is invalid: \"%1\"", configfile), configfile); 0095 qCCritical(KNEWSTUFFCORE) << configfile << "doesn't contain a KNewStuff or KNewStuff3 section."; 0096 return false; 0097 } 0098 0099 d->name = group.readEntry("Name"); 0100 d->categories = group.readEntry("Categories", QStringList()); 0101 qCDebug(KNEWSTUFFCORE) << "Categories: " << d->categories; 0102 d->adoptionCommand = group.readEntry("AdoptionCommand"); 0103 d->useLabel = group.readEntry("UseLabel", i18n("Use")); 0104 Q_EMIT useLabelChanged(); 0105 d->uploadEnabled = group.readEntry("UploadEnabled", true); 0106 Q_EMIT uploadEnabledChanged(); 0107 0108 d->providerFileUrl = group.readEntry("ProvidersUrl", QUrl(QStringLiteral("https://autoconfig.kde.org/ocs/providers.xml"))); 0109 if (group.readEntry("UseLocalProvidersFile", false)) { 0110 // The local providers file is called "appname.providers", to match "appname.knsrc" 0111 d->providerFileUrl = QUrl::fromLocalFile(QLatin1String("%1.providers").arg(configfile.left(configfile.length() - 6))); 0112 } 0113 0114 d->tagFilter = group.readEntry("TagFilter", QStringList(QStringLiteral("ghns_excluded!=1"))); 0115 d->downloadTagFilter = group.readEntry("DownloadTagFilter", QStringList()); 0116 0117 // Make sure that config is valid 0118 QString error; 0119 if (!d->installation->readConfig(group, error)) { 0120 Q_EMIT signalErrorCode(ErrorCode::ConfigFileError, 0121 i18n("Could not initialise the installation handler for %1:\n%2\n" 0122 "This is a critical error and should be reported to the application author", 0123 configfile, 0124 error), 0125 configfile); 0126 return false; 0127 } 0128 0129 const QString configFileBasename = QFileInfo(resolvedConfigFilePath).completeBaseName(); 0130 d->cache = Cache::getCache(configFileBasename); 0131 qCDebug(KNEWSTUFFCORE) << "Cache is" << d->cache << "for" << configFileBasename; 0132 d->cache->readRegistry(); 0133 0134 // Cache cleanup option, to help work around people deleting files from underneath KNewStuff (this 0135 // happens a lot with e.g. wallpapers and icons) 0136 if (d->installation->uncompressionSetting() == Installation::UseKPackageUncompression) { 0137 d->shouldRemoveDeletedEntries = true; 0138 } 0139 0140 d->shouldRemoveDeletedEntries = group.readEntry("RemoveDeadEntries", d->shouldRemoveDeletedEntries); 0141 if (d->shouldRemoveDeletedEntries) { 0142 d->cache->removeDeletedEntries(); 0143 } 0144 0145 loadProviders(); 0146 0147 return true; 0148 } 0149 0150 void EngineBase::loadProviders() 0151 { 0152 if (d->providerFileUrl.isEmpty()) { 0153 // it would be nicer to move the attica stuff into its own class 0154 qCDebug(KNEWSTUFFCORE) << "Using OCS default providers"; 0155 delete d->atticaProviderManager; 0156 d->atticaProviderManager = new Attica::ProviderManager; 0157 connect(d->atticaProviderManager, &Attica::ProviderManager::providerAdded, this, &EngineBase::atticaProviderLoaded); 0158 connect(d->atticaProviderManager, &Attica::ProviderManager::failedToLoad, this, &EngineBase::slotProvidersFailed); 0159 d->atticaProviderManager->loadDefaultProviders(); 0160 } else { 0161 qCDebug(KNEWSTUFFCORE) << "loading providers from " << d->providerFileUrl; 0162 Q_EMIT loadingProvider(); 0163 0164 XmlLoader *loader = s_engineProviderLoaders()->localData().value(d->providerFileUrl); 0165 if (!loader) { 0166 qCDebug(KNEWSTUFFCORE) << "No xml loader for this url yet, so create one and temporarily store that" << d->providerFileUrl; 0167 loader = new XmlLoader(this); 0168 s_engineProviderLoaders()->localData().insert(d->providerFileUrl, loader); 0169 connect(loader, &XmlLoader::signalLoaded, this, [this]() { 0170 s_engineProviderLoaders()->localData().remove(d->providerFileUrl); 0171 }); 0172 connect(loader, &XmlLoader::signalFailed, this, [this]() { 0173 s_engineProviderLoaders()->localData().remove(d->providerFileUrl); 0174 }); 0175 connect(loader, &XmlLoader::signalHttpError, this, [this](int status, QList<QNetworkReply::RawHeaderPair> rawHeaders) { 0176 if (status == 503) { // Temporarily Unavailable 0177 QDateTime retryAfter; 0178 static const QByteArray retryAfterKey{"Retry-After"}; 0179 for (const QNetworkReply::RawHeaderPair &headerPair : rawHeaders) { 0180 if (headerPair.first == retryAfterKey) { 0181 // Retry-After is not a known header, so we need to do a bit of running around to make that work 0182 // Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that 0183 // So, simple workaround, just pass it through a dummy request and get a formatted date out (the 0184 // cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...) 0185 QNetworkRequest dummyRequest; 0186 dummyRequest.setRawHeader(QByteArray{"Last-Modified"}, headerPair.second); 0187 retryAfter = dummyRequest.header(QNetworkRequest::LastModifiedHeader).toDateTime(); 0188 break; 0189 } 0190 } 0191 QTimer::singleShot(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch(), this, &EngineBase::loadProviders); 0192 // if it's a matter of a human moment's worth of seconds, just reload 0193 if (retryAfter.toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch() > 2) { 0194 // more than that, spit out TryAgainLaterError to let the user know what we're doing with their time 0195 static const KFormat formatter; 0196 Q_EMIT signalErrorCode(KNSCore::ErrorCode::TryAgainLaterError, 0197 i18n("The service is currently undergoing maintenance and is expected to be back in %1.", 0198 formatter.formatSpelloutDuration(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch())), 0199 {retryAfter}); 0200 } 0201 } 0202 }); 0203 loader->load(d->providerFileUrl); 0204 } 0205 connect(loader, &XmlLoader::signalLoaded, this, &EngineBase::slotProviderFileLoaded); 0206 connect(loader, &XmlLoader::signalFailed, this, &EngineBase::slotProvidersFailed); 0207 } 0208 } 0209 0210 QString KNSCore::EngineBase::name() const 0211 { 0212 return d->name; 0213 } 0214 0215 QStringList EngineBase::categories() const 0216 { 0217 return d->categories; 0218 } 0219 0220 QList<Provider::CategoryMetadata> EngineBase::categoriesMetadata() 0221 { 0222 return d->categoriesMetadata; 0223 } 0224 0225 QList<Provider::SearchPreset> EngineBase::searchPresets() 0226 { 0227 return d->searchPresets; 0228 } 0229 0230 QString EngineBase::useLabel() const 0231 { 0232 return d->useLabel; 0233 } 0234 0235 bool EngineBase::uploadEnabled() const 0236 { 0237 return d->uploadEnabled; 0238 } 0239 0240 void EngineBase::addProvider(QSharedPointer<KNSCore::Provider> provider) 0241 { 0242 qCDebug(KNEWSTUFFCORE) << "Engine addProvider called with provider with id " << provider->id(); 0243 d->providers.insert(provider->id(), provider); 0244 provider->setTagFilter(d->tagFilter); 0245 provider->setDownloadTagFilter(d->downloadTagFilter); 0246 connect(provider.data(), &Provider::providerInitialized, this, &EngineBase::providerInitialized); 0247 0248 connect(provider.data(), &Provider::signalError, this, [this, provider](const QString &msg) { 0249 Q_EMIT signalErrorCode(ErrorCode::ProviderError, msg, d->providerFileUrl); 0250 }); 0251 connect(provider.data(), &Provider::signalErrorCode, this, &EngineBase::signalErrorCode); 0252 connect(provider.data(), &Provider::signalInformation, this, &EngineBase::signalMessage); 0253 connect(provider.data(), &Provider::basicsLoaded, this, &EngineBase::providersChanged); 0254 Q_EMIT providersChanged(); 0255 } 0256 0257 void EngineBase::providerInitialized(Provider *p) 0258 { 0259 qCDebug(KNEWSTUFFCORE) << "providerInitialized" << p->name(); 0260 p->setCachedEntries(d->cache->registryForProvider(p->id())); 0261 0262 for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(d->providers)) { 0263 if (!p->isInitialized()) { 0264 return; 0265 } 0266 } 0267 Q_EMIT signalProvidersLoaded(); 0268 } 0269 0270 void EngineBase::slotProvidersFailed() 0271 { 0272 Q_EMIT signalErrorCode(KNSCore::ErrorCode::ProviderError, 0273 i18n("Loading of providers from file: %1 failed", d->providerFileUrl.toString()), 0274 d->providerFileUrl); 0275 } 0276 0277 void EngineBase::slotProviderFileLoaded(const QDomDocument &doc) 0278 { 0279 qCDebug(KNEWSTUFFCORE) << "slotProvidersLoaded"; 0280 0281 bool isAtticaProviderFile = false; 0282 0283 // get each provider element, and create a provider object from it 0284 QDomElement providers = doc.documentElement(); 0285 0286 if (providers.tagName() == QLatin1String("providers")) { 0287 isAtticaProviderFile = true; 0288 } else if (providers.tagName() != QLatin1String("ghnsproviders") && providers.tagName() != QLatin1String("knewstuffproviders")) { 0289 qWarning() << "No document in providers.xml."; 0290 Q_EMIT signalErrorCode(KNSCore::ErrorCode::ProviderError, 0291 i18n("Could not load get hot new stuff providers from file: %1", d->providerFileUrl.toString()), 0292 d->providerFileUrl); 0293 return; 0294 } 0295 0296 QDomElement n = providers.firstChildElement(QStringLiteral("provider")); 0297 while (!n.isNull()) { 0298 qCDebug(KNEWSTUFFCORE) << "Provider attributes: " << n.attribute(QStringLiteral("type")); 0299 0300 QSharedPointer<KNSCore::Provider> provider; 0301 if (isAtticaProviderFile || n.attribute(QStringLiteral("type")).toLower() == QLatin1String("rest")) { 0302 provider.reset(new AtticaProvider(d->categories, {})); 0303 connect(provider.data(), &Provider::categoriesMetadataLoded, this, [this](const QList<Provider::CategoryMetadata> &categories) { 0304 d->categoriesMetadata = categories; 0305 Q_EMIT signalCategoriesMetadataLoded(categories); 0306 }); 0307 #ifdef SYNDICATION_FOUND 0308 } else if (n.attribute(QStringLiteral("type")).toLower() == QLatin1String("opds")) { 0309 provider.reset(new OPDSProvider); 0310 connect(provider.data(), &Provider::searchPresetsLoaded, this, [this](const QList<Provider::SearchPreset> &presets) { 0311 d->searchPresets = presets; 0312 Q_EMIT signalSearchPresetsLoaded(presets); 0313 }); 0314 #endif 0315 } else { 0316 provider.reset(new StaticXmlProvider); 0317 } 0318 0319 if (provider->setProviderXML(n)) { 0320 addProvider(provider); 0321 } else { 0322 Q_EMIT signalErrorCode(KNSCore::ErrorCode::ProviderError, i18n("Error initializing provider."), d->providerFileUrl); 0323 } 0324 n = n.nextSiblingElement(); 0325 } 0326 Q_EMIT loadingProvider(); 0327 } 0328 0329 void EngineBase::atticaProviderLoaded(const Attica::Provider &atticaProvider) 0330 { 0331 qCDebug(KNEWSTUFFCORE) << "atticaProviderLoaded called"; 0332 if (!atticaProvider.hasContentService()) { 0333 qCDebug(KNEWSTUFFCORE) << "Found provider: " << atticaProvider.baseUrl() << " but it does not support content"; 0334 return; 0335 } 0336 QSharedPointer<KNSCore::Provider> provider = QSharedPointer<KNSCore::Provider>(new AtticaProvider(atticaProvider, d->categories, {})); 0337 connect(provider.data(), &Provider::categoriesMetadataLoded, this, [this](const QList<Provider::CategoryMetadata> &categories) { 0338 d->categoriesMetadata = categories; 0339 Q_EMIT signalCategoriesMetadataLoded(categories); 0340 }); 0341 addProvider(provider); 0342 } 0343 0344 QSharedPointer<Cache> EngineBase::cache() const 0345 { 0346 return d->cache; 0347 } 0348 0349 void EngineBase::setTagFilter(const QStringList &filter) 0350 { 0351 d->tagFilter = filter; 0352 for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(d->providers)) { 0353 p->setTagFilter(d->tagFilter); 0354 } 0355 } 0356 0357 QStringList EngineBase::tagFilter() const 0358 { 0359 return d->tagFilter; 0360 } 0361 0362 void KNSCore::EngineBase::addTagFilter(const QString &filter) 0363 { 0364 d->tagFilter << filter; 0365 for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(d->providers)) { 0366 p->setTagFilter(d->tagFilter); 0367 } 0368 } 0369 0370 void EngineBase::setDownloadTagFilter(const QStringList &filter) 0371 { 0372 d->downloadTagFilter = filter; 0373 for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(d->providers)) { 0374 p->setDownloadTagFilter(d->downloadTagFilter); 0375 } 0376 } 0377 0378 QStringList EngineBase::downloadTagFilter() const 0379 { 0380 return d->downloadTagFilter; 0381 } 0382 0383 void EngineBase::addDownloadTagFilter(const QString &filter) 0384 { 0385 d->downloadTagFilter << filter; 0386 for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(d->providers)) { 0387 p->setDownloadTagFilter(d->downloadTagFilter); 0388 } 0389 } 0390 0391 QList<Attica::Provider *> EngineBase::atticaProviders() const 0392 { 0393 QList<Attica::Provider *> ret; 0394 ret.reserve(d->providers.size()); 0395 for (const auto &p : std::as_const(d->providers)) { 0396 const auto atticaProvider = p.dynamicCast<AtticaProvider>(); 0397 if (atticaProvider) { 0398 ret += atticaProvider->provider(); 0399 } 0400 } 0401 return ret; 0402 } 0403 0404 bool EngineBase::userCanVote(const Entry &entry) 0405 { 0406 QSharedPointer<Provider> p = d->providers.value(entry.providerId()); 0407 return p->userCanVote(); 0408 } 0409 0410 void EngineBase::vote(const Entry &entry, uint rating) 0411 { 0412 QSharedPointer<Provider> p = d->providers.value(entry.providerId()); 0413 p->vote(entry, rating); 0414 } 0415 0416 bool EngineBase::userCanBecomeFan(const Entry &entry) 0417 { 0418 QSharedPointer<Provider> p = d->providers.value(entry.providerId()); 0419 return p->userCanBecomeFan(); 0420 } 0421 0422 void EngineBase::becomeFan(const Entry &entry) 0423 { 0424 QSharedPointer<Provider> p = d->providers.value(entry.providerId()); 0425 p->becomeFan(entry); 0426 } 0427 0428 QSharedPointer<Provider> EngineBase::provider(const QString &providerId) const 0429 { 0430 return d->providers.value(providerId); 0431 } 0432 0433 QSharedPointer<Provider> EngineBase::defaultProvider() const 0434 { 0435 if (d->providers.count() > 0) { 0436 return d->providers.constBegin().value(); 0437 } 0438 return nullptr; 0439 } 0440 0441 QStringList EngineBase::providerIDs() const 0442 { 0443 return d->providers.keys(); 0444 } 0445 0446 bool EngineBase::hasAdoptionCommand() const 0447 { 0448 return !d->adoptionCommand.isEmpty(); 0449 } 0450 0451 void EngineBase::updateStatus() 0452 { 0453 } 0454 0455 Installation *EngineBase::installation() const 0456 { 0457 return d->installation; 0458 } 0459 0460 ResultsStream *EngineBase::search(const Provider::SearchRequest &request) 0461 { 0462 return new ResultsStream(request, this); 0463 } 0464 0465 QList<QSharedPointer<Provider>> EngineBase::providers() const 0466 { 0467 return d->providers.values(); 0468 } 0469 0470 #include "moc_enginebase.cpp"