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"