File indexing completed on 2024-04-28 15:28:56

0001 /*
0002     knewstuff3/engine.cpp
0003     SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org>
0004     SPDX-FileCopyrightText: 2007-2010 Frederik Gladhorn <gladhorn@kde.org>
0005     SPDX-FileCopyrightText: 2009 Jeremy Whiting <jpwhiting@kde.org>
0006     SPDX-FileCopyrightText: 2010 Matthias Fuchs <mat69@gmx.net>
0007 
0008     SPDX-License-Identifier: LGPL-2.1-or-later
0009 */
0010 
0011 #include "engine.h"
0012 
0013 #include "commentsmodel.h"
0014 #include "imageloader_p.h"
0015 #include "installation.h"
0016 #include "question.h"
0017 #include "xmlloader.h"
0018 
0019 #include <KConfig>
0020 #include <KConfigGroup>
0021 #include <KFileUtils>
0022 #include <KFormat>
0023 #include <KLocalizedString>
0024 #include <KShell>
0025 #include <QDesktopServices>
0026 #include <knewstuffcore_debug.h>
0027 
0028 #include <QDir>
0029 #include <QDirIterator>
0030 #include <QProcess>
0031 #include <QThreadStorage>
0032 #include <QTimer>
0033 #include <QUrlQuery>
0034 #include <qdom.h>
0035 
0036 #if defined(Q_OS_WIN)
0037 #include <shlobj.h>
0038 #include <windows.h>
0039 #endif
0040 
0041 // libattica
0042 #include <attica/providermanager.h>
0043 #include <qstandardpaths.h>
0044 
0045 // own
0046 #include "../attica/atticaprovider_p.h"
0047 #include "../staticxml/staticxmlprovider_p.h"
0048 #ifdef SYNDICATION_FOUND
0049 #include "../opds/opdsprovider_p.h"
0050 #endif
0051 #include "cache.h"
0052 
0053 using namespace KNSCore;
0054 
0055 typedef QHash<QString, XmlLoader *> EngineProviderLoaderHash;
0056 Q_GLOBAL_STATIC(QThreadStorage<EngineProviderLoaderHash>, s_engineProviderLoaders)
0057 
0058 class EnginePrivate
0059 {
0060 public:
0061     QString getAdoptionCommand(const QString &command, const KNSCore::EntryInternal &entry, Installation *inst)
0062     {
0063         auto adoption = command;
0064         if (adoption.isEmpty()) {
0065             return {};
0066         }
0067 
0068         const QLatin1String dirReplace("%d");
0069         if (adoption.contains(dirReplace)) {
0070             QString installPath = sharedDir(entry.installedFiles(), inst->targetInstallationPath()).path();
0071             adoption.replace(dirReplace, KShell::quoteArg(installPath));
0072         }
0073 
0074         const QLatin1String fileReplace("%f");
0075         if (adoption.contains(fileReplace)) {
0076             if (entry.installedFiles().isEmpty()) {
0077                 qCWarning(KNEWSTUFFCORE) << "no installed files to adopt";
0078                 return {};
0079             } else if (entry.installedFiles().count() != 1) {
0080                 qCWarning(KNEWSTUFFCORE) << "can only adopt one file, will be using the first" << entry.installedFiles().at(0);
0081             }
0082 
0083             adoption.replace(fileReplace, KShell::quoteArg(entry.installedFiles().at(0)));
0084         }
0085         return adoption;
0086     }
0087     /**
0088      * we look for the directory where all the resources got installed.
0089      * assuming it was extracted into a directory
0090      */
0091     static QDir sharedDir(QStringList dirs, QString rootPath)
0092     {
0093         // Ensure that rootPath definitely is a clean path with a slash at the end
0094         rootPath = QDir::cleanPath(rootPath) + QStringLiteral("/");
0095         qCInfo(KNEWSTUFFCORE) << Q_FUNC_INFO << dirs << rootPath;
0096         while (!dirs.isEmpty()) {
0097             QString thisDir(dirs.takeLast());
0098             if (thisDir.endsWith(QStringLiteral("*"))) {
0099                 qCInfo(KNEWSTUFFCORE) << "Directory entry" << thisDir
0100                                       << "ends in a *, indicating this was installed from an archive - see Installation::archiveEntries";
0101                 thisDir.chop(1);
0102             }
0103 
0104             const QString currentPath = QDir::cleanPath(thisDir);
0105             qCInfo(KNEWSTUFFCORE) << "Current path is" << currentPath;
0106             if (!currentPath.startsWith(rootPath)) {
0107                 qCInfo(KNEWSTUFFCORE) << "Current path" << currentPath << "does not start with" << rootPath << "and should be ignored";
0108                 continue;
0109             }
0110 
0111             const QFileInfo current(currentPath);
0112             qCInfo(KNEWSTUFFCORE) << "Current file info is" << current;
0113             if (!current.isDir()) {
0114                 qCInfo(KNEWSTUFFCORE) << "Current path" << currentPath << "is not a directory, and should be ignored";
0115                 continue;
0116             }
0117 
0118             const QDir dir(currentPath);
0119             if (dir.path() == (rootPath + dir.dirName())) {
0120                 qCDebug(KNEWSTUFFCORE) << "Found directory" << dir;
0121                 return dir;
0122             }
0123         }
0124         qCWarning(KNEWSTUFFCORE) << "Failed to locate any shared installed directory in" << dirs << "and this is almost certainly very bad.";
0125         return {};
0126     }
0127 
0128     QList<Provider::CategoryMetadata> categoriesMetadata;
0129     QList<Provider::SearchPreset> searchPresets;
0130     Attica::ProviderManager *m_atticaProviderManager = nullptr;
0131     QStringList tagFilter;
0132     QStringList downloadTagFilter;
0133     bool configLocationFallback = true; // TODO KF6 remove old location
0134     QString name;
0135     QMap<EntryInternal, CommentsModel *> commentsModels;
0136     bool shouldRemoveDeletedEntries = false;
0137     KNSCore::Provider::SearchRequest storedRequest;
0138 
0139     // Used for updating purposes - we ought to be saving this information, but we also have to deal with old stuff, and so... this will have to do for now
0140     // TODO KF6: Installed state needs to move onto a per-downloadlink basis rather than per-entry
0141     QMap<EntryInternal, QStringList> payloads;
0142     QMap<EntryInternal, QString> payloadToIdentify;
0143     Engine::BusyState busyState;
0144     QString busyMessage;
0145     QString useLabel;
0146     bool uploadEnabled = false;
0147     QString configFileName;
0148 };
0149 
0150 Engine::Engine(QObject *parent)
0151     : QObject(parent)
0152     , m_installation(new Installation)
0153     , m_cache()
0154     , m_searchTimer(new QTimer)
0155     , d(new EnginePrivate)
0156     , m_currentPage(-1)
0157     , m_pageSize(20)
0158     , m_numDataJobs(0)
0159     , m_numPictureJobs(0)
0160     , m_numInstallJobs(0)
0161     , m_initialized(false)
0162 {
0163     m_searchTimer->setSingleShot(true);
0164     m_searchTimer->setInterval(1000);
0165     connect(m_searchTimer, &QTimer::timeout, this, &Engine::slotSearchTimerExpired);
0166     connect(m_installation, &Installation::signalInstallationFinished, this, &Engine::slotInstallationFinished);
0167     connect(m_installation, &Installation::signalInstallationFailed, this, &Engine::slotInstallationFailed);
0168     connect(m_installation, &Installation::signalInstallationError, this, [this](const QString &message) {
0169         Q_EMIT signalErrorCode(ErrorCode::InstallationError, i18n("An error occurred during the installation process:\n%1", message), QVariant());
0170     });
0171 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 53)
0172     // Pass along old error signal for compatibility
0173     connect(this, &Engine::signalErrorCode, this, [this](const KNSCore::ErrorCode &, const QString &msg, const QVariant &) {
0174         Q_EMIT signalError(msg);
0175     });
0176 #endif
0177 
0178 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 77)
0179     connect(this, &Engine::signalEntryEvent, this, [this](const EntryInternal &entry, EntryInternal::EntryEvent event) {
0180         if (event == EntryInternal::StatusChangedEvent) {
0181             Q_EMIT signalEntryChanged(entry);
0182         } else if (event == EntryInternal::DetailsLoadedEvent) {
0183             Q_EMIT signalEntryDetailsLoaded(entry);
0184         }
0185     });
0186 #endif
0187 }
0188 
0189 Engine::~Engine()
0190 {
0191     if (m_cache) {
0192         m_cache->writeRegistry();
0193     }
0194     delete d->m_atticaProviderManager;
0195     delete m_searchTimer;
0196     delete m_installation;
0197 }
0198 
0199 bool Engine::init(const QString &configfile)
0200 {
0201     qCDebug(KNEWSTUFFCORE) << "Initializing KNSCore::Engine from '" << configfile << "'";
0202 
0203     setBusy(BusyOperation::Initializing, i18n("Initializing"));
0204 
0205     QScopedPointer<KConfig> conf;
0206     QFileInfo configFileInfo(configfile);
0207     // TODO KF6: This is fallback logic for an old location for the knsrc files. This is deprecated in KF5 and should be removed in KF6
0208     bool isRelativeConfig = configFileInfo.isRelative();
0209     QString actualConfig;
0210     if (isRelativeConfig) {
0211         if (configfile.contains(QStringLiteral("/"))) {
0212             // If this is the case, then we've been given an /actual/ relative path, not just the name of a knsrc file
0213             actualConfig = configFileInfo.canonicalFilePath();
0214         } else {
0215             // Don't do the expensive search unless the config is relative
0216             actualConfig = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("knsrcfiles/%1").arg(configfile));
0217         }
0218     }
0219     // We need to always have a full path in some cases, and the given config name in others, so let's just
0220     // store this in a variable with a useful name
0221     QString configFullPath = actualConfig.isEmpty() ? configfile : actualConfig;
0222     QString configFileName{configfile};
0223     if (isRelativeConfig && d->configLocationFallback && actualConfig.isEmpty()) {
0224         conf.reset(new KConfig(configfile));
0225         qCWarning(KNEWSTUFFCORE) << "Using a deprecated location for the knsrc file" << configfile
0226                                  << " - please contact the author of the software which provides this file to get it updated to use the new location";
0227         configFileName = QFileInfo(configfile).baseName();
0228     } else if (isRelativeConfig && actualConfig.isEmpty()) {
0229         configFileName = QFileInfo(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("knsrcfiles/%1").arg(configfile))).baseName();
0230         conf.reset(new KConfig(QStringLiteral("knsrcfiles/%1").arg(configfile), KConfig::FullConfig, QStandardPaths::GenericDataLocation));
0231     } else if (isRelativeConfig) {
0232         configFileName = configFileInfo.baseName();
0233         conf.reset(new KConfig(actualConfig));
0234     } else {
0235         configFileName = configFileInfo.baseName();
0236         conf.reset(new KConfig(configfile));
0237     }
0238     d->configFileName = configFileName;
0239 
0240     if (conf->accessMode() == KConfig::NoAccess) {
0241         Q_EMIT signalErrorCode(KNSCore::ConfigFileError, i18n("Configuration file exists, but cannot be opened: \"%1\"", configfile), configfile);
0242         qCCritical(KNEWSTUFFCORE) << "The knsrc file '" << configfile << "' was found but could not be opened.";
0243         return false;
0244     }
0245 
0246     KConfigGroup group;
0247     if (conf->hasGroup("KNewStuff3")) {
0248         qCDebug(KNEWSTUFFCORE) << "Loading KNewStuff3 config: " << configfile;
0249         group = conf->group("KNewStuff3");
0250     } else if (conf->hasGroup("KNewStuff2")) {
0251         qCDebug(KNEWSTUFFCORE) << "Loading KNewStuff2 config: " << configfile;
0252         group = conf->group("KNewStuff2");
0253     } else {
0254         Q_EMIT signalErrorCode(KNSCore::ConfigFileError, i18n("Configuration file is invalid: \"%1\"", configfile), configfile);
0255         qCCritical(KNEWSTUFFCORE) << configfile << " doesn't contain a KNewStuff3 section.";
0256         return false;
0257     }
0258 
0259     d->name = group.readEntry("Name");
0260     m_categories = group.readEntry("Categories", QStringList());
0261     qCDebug(KNEWSTUFFCORE) << "Categories: " << m_categories;
0262     m_adoptionCommand = group.readEntry("AdoptionCommand");
0263     d->useLabel = group.readEntry("UseLabel", i18n("Use"));
0264     Q_EMIT useLabelChanged();
0265     d->uploadEnabled = group.readEntry("UploadEnabled", true);
0266     Q_EMIT uploadEnabledChanged();
0267 
0268     m_providerFileUrl = group.readEntry("ProvidersUrl", QStringLiteral("https://autoconfig.kde.org/ocs/providers.xml"));
0269     if (m_providerFileUrl == QLatin1String("https://download.kde.org/ocs/providers.xml")) {
0270         m_providerFileUrl = QStringLiteral("https://autoconfig.kde.org/ocs/providers.xml");
0271         qCWarning(KNEWSTUFFCORE) << "Please make sure" << configfile << "has ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml";
0272     }
0273     if (group.readEntry("UseLocalProvidersFile", "false").toLower() == QLatin1String{"true"}) {
0274         // The local providers file is called "appname.providers", to match "appname.knsrc"
0275         m_providerFileUrl = QUrl::fromLocalFile(QLatin1String("%1.providers").arg(configFullPath.left(configFullPath.length() - 6))).toString();
0276     }
0277 
0278     d->tagFilter = group.readEntry("TagFilter", QStringList(QStringLiteral("ghns_excluded!=1")));
0279     d->downloadTagFilter = group.readEntry("DownloadTagFilter", QStringList());
0280 
0281     // Make sure that config is valid
0282     if (!m_installation->readConfig(group)) {
0283         Q_EMIT signalErrorCode(ErrorCode::ConfigFileError,
0284                                i18n("Could not initialise the installation handler for %1\n"
0285                                     "This is a critical error and should be reported to the application author",
0286                                     configfile),
0287                                configfile);
0288         return false;
0289     }
0290 
0291     connect(m_installation, &Installation::signalEntryChanged, this, &Engine::slotEntryChanged);
0292 
0293     m_cache = Cache::getCache(configFileName);
0294     qCDebug(KNEWSTUFFCORE) << "Cache is" << m_cache << "for" << configFileName;
0295     connect(this, &Engine::signalEntryEvent, m_cache.data(), [this](const EntryInternal &entry, EntryInternal::EntryEvent event) {
0296         if (event == EntryInternal::StatusChangedEvent) {
0297             m_cache->registerChangedEntry(entry);
0298         }
0299     });
0300     connect(m_cache.data(), &Cache::entryChanged, this, &Engine::slotEntryChanged);
0301     m_cache->readRegistry();
0302 
0303     // Cache cleanup option, to help work around people deleting files from underneath KNewStuff (this
0304     // happens a lot with e.g. wallpapers and icons)
0305     if (m_installation->uncompressionSetting() == Installation::UseKPackageUncompression) {
0306         d->shouldRemoveDeletedEntries = true;
0307     }
0308 
0309     d->shouldRemoveDeletedEntries = group.readEntry("RemoveDeadEntries", d->shouldRemoveDeletedEntries);
0310     if (d->shouldRemoveDeletedEntries) {
0311         m_cache->removeDeletedEntries();
0312     }
0313 
0314     m_initialized = true;
0315 
0316     // load the providers
0317     loadProviders();
0318 
0319     return true;
0320 }
0321 
0322 QString KNSCore::Engine::name() const
0323 {
0324     return d->name;
0325 }
0326 
0327 QStringList Engine::categories() const
0328 {
0329     return m_categories;
0330 }
0331 
0332 QStringList Engine::categoriesFilter() const
0333 {
0334     return m_currentRequest.categories;
0335 }
0336 
0337 QList<Provider::CategoryMetadata> Engine::categoriesMetadata()
0338 {
0339     return d->categoriesMetadata;
0340 }
0341 
0342 QList<Provider::SearchPreset> Engine::searchPresets()
0343 {
0344     return d->searchPresets;
0345 }
0346 
0347 void Engine::loadProviders()
0348 {
0349     if (m_providerFileUrl.isEmpty()) {
0350         // it would be nicer to move the attica stuff into its own class
0351         qCDebug(KNEWSTUFFCORE) << "Using OCS default providers";
0352         delete d->m_atticaProviderManager;
0353         d->m_atticaProviderManager = new Attica::ProviderManager;
0354         connect(d->m_atticaProviderManager, &Attica::ProviderManager::providerAdded, this, &Engine::atticaProviderLoaded);
0355         connect(d->m_atticaProviderManager, &Attica::ProviderManager::failedToLoad, this, &Engine::slotProvidersFailed);
0356         d->m_atticaProviderManager->loadDefaultProviders();
0357     } else {
0358         qCDebug(KNEWSTUFFCORE) << "loading providers from " << m_providerFileUrl;
0359         setBusy(BusyOperation::LoadingData, i18n("Loading provider information"));
0360 
0361         XmlLoader *loader = s_engineProviderLoaders()->localData().value(m_providerFileUrl);
0362         if (!loader) {
0363             qCDebug(KNEWSTUFFCORE) << "No xml loader for this url yet, so create one and temporarily store that" << m_providerFileUrl;
0364             loader = new XmlLoader(this);
0365             s_engineProviderLoaders()->localData().insert(m_providerFileUrl, loader);
0366             connect(loader, &XmlLoader::signalLoaded, this, [this]() {
0367                 s_engineProviderLoaders()->localData().remove(m_providerFileUrl);
0368             });
0369             connect(loader, &XmlLoader::signalFailed, this, [this]() {
0370                 s_engineProviderLoaders()->localData().remove(m_providerFileUrl);
0371             });
0372             connect(loader, &XmlLoader::signalHttpError, this, [this](int status, QList<QNetworkReply::RawHeaderPair> rawHeaders) {
0373                 if (status == 503) { // Temporarily Unavailable
0374                     QDateTime retryAfter;
0375                     static const QByteArray retryAfterKey{"Retry-After"};
0376                     for (const QNetworkReply::RawHeaderPair &headerPair : rawHeaders) {
0377                         if (headerPair.first == retryAfterKey) {
0378                             // Retry-After is not a known header, so we need to do a bit of running around to make that work
0379                             // Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that
0380                             // So, simple workaround, just pass it through a dummy request and get a formatted date out (the
0381                             // cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...)
0382                             QNetworkRequest dummyRequest;
0383                             dummyRequest.setRawHeader(QByteArray{"Last-Modified"}, headerPair.second);
0384                             retryAfter = dummyRequest.header(QNetworkRequest::LastModifiedHeader).toDateTime();
0385                             break;
0386                         }
0387                     }
0388                     QTimer::singleShot(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch(), this, &Engine::loadProviders);
0389                     // if it's a matter of a human moment's worth of seconds, just reload
0390                     if (retryAfter.toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch() > 2) {
0391                         // more than that, spit out TryAgainLaterError to let the user know what we're doing with their time
0392                         static const KFormat formatter;
0393                         Q_EMIT signalErrorCode(KNSCore::TryAgainLaterError,
0394                                                i18n("The service is currently undergoing maintenance and is expected to be back in %1.",
0395                                                     formatter.formatSpelloutDuration(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch())),
0396                                                {retryAfter});
0397                     }
0398                 }
0399             });
0400             loader->load(QUrl(m_providerFileUrl));
0401         }
0402         connect(loader, &XmlLoader::signalLoaded, this, &Engine::slotProviderFileLoaded);
0403         connect(loader, &XmlLoader::signalFailed, this, &Engine::slotProvidersFailed);
0404     }
0405 }
0406 
0407 void Engine::slotProviderFileLoaded(const QDomDocument &doc)
0408 {
0409     qCDebug(KNEWSTUFFCORE) << "slotProvidersLoaded";
0410 
0411     bool isAtticaProviderFile = false;
0412 
0413     // get each provider element, and create a provider object from it
0414     QDomElement providers = doc.documentElement();
0415 
0416     if (providers.tagName() == QLatin1String("providers")) {
0417         isAtticaProviderFile = true;
0418     } else if (providers.tagName() != QLatin1String("ghnsproviders") && providers.tagName() != QLatin1String("knewstuffproviders")) {
0419         qWarning() << "No document in providers.xml.";
0420         Q_EMIT signalErrorCode(KNSCore::ProviderError, i18n("Could not load get hot new stuff providers from file: %1", m_providerFileUrl), m_providerFileUrl);
0421         return;
0422     }
0423 
0424     QDomElement n = providers.firstChildElement(QStringLiteral("provider"));
0425     while (!n.isNull()) {
0426         qCDebug(KNEWSTUFFCORE) << "Provider attributes: " << n.attribute(QStringLiteral("type"));
0427 
0428         QSharedPointer<KNSCore::Provider> provider;
0429         if (isAtticaProviderFile || n.attribute(QStringLiteral("type")).toLower() == QLatin1String("rest")) {
0430             provider.reset(new AtticaProvider(m_categories, d->configFileName));
0431             connect(provider.data(), &Provider::categoriesMetadataLoded, this, [this](const QList<Provider::CategoryMetadata> &categories) {
0432                 d->categoriesMetadata = categories;
0433                 Q_EMIT signalCategoriesMetadataLoded(categories);
0434             });
0435 #ifdef SYNDICATION_FOUND
0436         } else if (n.attribute(QStringLiteral("type")).toLower() == QLatin1String("opds")) {
0437             provider.reset(new OPDSProvider);
0438             connect(provider.data(), &Provider::searchPresetsLoaded, this, [this](const QList<Provider::SearchPreset> &presets) {
0439                 d->searchPresets = presets;
0440                 Q_EMIT signalSearchPresetsLoaded(presets);
0441             });
0442 #endif
0443         } else {
0444             provider.reset(new StaticXmlProvider);
0445         }
0446 
0447         if (provider->setProviderXML(n)) {
0448             addProvider(provider);
0449         } else {
0450             Q_EMIT signalErrorCode(KNSCore::ProviderError, i18n("Error initializing provider."), m_providerFileUrl);
0451         }
0452         n = n.nextSiblingElement();
0453     }
0454     setBusy(BusyOperation::LoadingData, i18n("Loading data"));
0455 }
0456 
0457 void Engine::atticaProviderLoaded(const Attica::Provider &atticaProvider)
0458 {
0459     qCDebug(KNEWSTUFFCORE) << "atticaProviderLoaded called";
0460     if (!atticaProvider.hasContentService()) {
0461         qCDebug(KNEWSTUFFCORE) << "Found provider: " << atticaProvider.baseUrl() << " but it does not support content";
0462         return;
0463     }
0464     QSharedPointer<KNSCore::Provider> provider = QSharedPointer<KNSCore::Provider>(new AtticaProvider(atticaProvider, m_categories, d->configFileName));
0465     connect(provider.data(), &Provider::categoriesMetadataLoded, this, [this](const QList<Provider::CategoryMetadata> &categories) {
0466         d->categoriesMetadata = categories;
0467         Q_EMIT signalCategoriesMetadataLoded(categories);
0468     });
0469     addProvider(provider);
0470 }
0471 
0472 void Engine::addProvider(QSharedPointer<KNSCore::Provider> provider)
0473 {
0474     qCDebug(KNEWSTUFFCORE) << "Engine addProvider called with provider with id " << provider->id();
0475     m_providers.insert(provider->id(), provider);
0476     provider->setTagFilter(d->tagFilter);
0477     provider->setDownloadTagFilter(d->downloadTagFilter);
0478     connect(provider.data(), &Provider::providerInitialized, this, &Engine::providerInitialized);
0479     connect(provider.data(), &Provider::loadingFinished, this, &Engine::slotEntriesLoaded);
0480     connect(provider.data(), &Provider::entryDetailsLoaded, this, &Engine::slotEntryDetailsLoaded);
0481     connect(provider.data(), &Provider::payloadLinkLoaded, this, &Engine::downloadLinkLoaded);
0482 
0483     connect(provider.data(), &Provider::signalError, this, [this, provider](const QString &msg) {
0484         Q_EMIT signalErrorCode(ErrorCode::ProviderError, msg, m_providerFileUrl);
0485     });
0486     connect(provider.data(), &Provider::signalErrorCode, this, &Engine::signalErrorCode);
0487     connect(provider.data(), &Provider::signalInformation, this, [this](const QString &message) {
0488         Q_EMIT signalMessage(message);
0489     });
0490     connect(provider.data(), &Provider::basicsLoaded, this, &Engine::providersChanged);
0491     Q_EMIT providersChanged();
0492 }
0493 
0494 void Engine::providerJobStarted(KJob *job)
0495 {
0496     Q_EMIT jobStarted(job, i18n("Loading data from provider"));
0497 }
0498 
0499 void Engine::slotProvidersFailed()
0500 {
0501     Q_EMIT signalErrorCode(KNSCore::ProviderError, i18n("Loading of providers from file: %1 failed", m_providerFileUrl), m_providerFileUrl);
0502 }
0503 
0504 void Engine::providerInitialized(Provider *p)
0505 {
0506     qCDebug(KNEWSTUFFCORE) << "providerInitialized" << p->name();
0507     p->setCachedEntries(m_cache->registryForProvider(p->id()));
0508     updateStatus();
0509 
0510     for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(m_providers)) {
0511         if (!p->isInitialized()) {
0512             return;
0513         }
0514     }
0515     Q_EMIT signalProvidersLoaded();
0516 }
0517 
0518 void Engine::slotEntriesLoaded(const KNSCore::Provider::SearchRequest &request, KNSCore::EntryInternal::List entries)
0519 {
0520     m_currentPage = qMax<int>(request.page, m_currentPage);
0521     qCDebug(KNEWSTUFFCORE) << "loaded page " << request.page << "current page" << m_currentPage << "count:" << entries.count();
0522 
0523     if (request.filter == Provider::Updates) {
0524         Q_EMIT signalUpdateableEntriesLoaded(entries);
0525     } else {
0526         m_cache->insertRequest(request, entries);
0527         Q_EMIT signalEntriesLoaded(entries);
0528     }
0529 
0530     --m_numDataJobs;
0531     updateStatus();
0532 }
0533 
0534 void Engine::reloadEntries()
0535 {
0536     Q_EMIT signalResetView();
0537     m_currentPage = -1;
0538     m_currentRequest.pageSize = m_pageSize;
0539     m_currentRequest.page = 0;
0540     m_numDataJobs = 0;
0541 
0542     for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(m_providers)) {
0543         if (p->isInitialized()) {
0544             if (m_currentRequest.filter == Provider::Installed) {
0545                 // when asking for installed entries, never use the cache
0546                 p->loadEntries(m_currentRequest);
0547             } else {
0548                 // take entries from cache until there are no more
0549                 EntryInternal::List cache;
0550                 EntryInternal::List lastCache = m_cache->requestFromCache(m_currentRequest);
0551                 while (!lastCache.isEmpty()) {
0552                     qCDebug(KNEWSTUFFCORE) << "From cache";
0553                     cache << lastCache;
0554 
0555                     m_currentPage = m_currentRequest.page;
0556                     ++m_currentRequest.page;
0557                     lastCache = m_cache->requestFromCache(m_currentRequest);
0558                 }
0559 
0560                 // Since the cache has no more pages, reset the request's page
0561                 if (m_currentPage >= 0) {
0562                     m_currentRequest.page = m_currentPage;
0563                 }
0564 
0565                 if (!cache.isEmpty()) {
0566                     Q_EMIT signalEntriesLoaded(cache);
0567                 } else {
0568                     qCDebug(KNEWSTUFFCORE) << "From provider";
0569                     p->loadEntries(m_currentRequest);
0570 
0571                     ++m_numDataJobs;
0572                     updateStatus();
0573                 }
0574             }
0575         }
0576     }
0577 }
0578 
0579 void Engine::setCategoriesFilter(const QStringList &categories)
0580 {
0581     m_currentRequest.categories = categories;
0582     reloadEntries();
0583 }
0584 
0585 void Engine::setSortMode(Provider::SortMode mode)
0586 {
0587     if (m_currentRequest.sortMode != mode) {
0588         m_currentRequest.page = -1;
0589     }
0590     m_currentRequest.sortMode = mode;
0591     reloadEntries();
0592 }
0593 
0594 Provider::SortMode KNSCore::Engine::sortMode() const
0595 {
0596     return m_currentRequest.sortMode;
0597 }
0598 
0599 void KNSCore::Engine::setFilter(Provider::Filter filter)
0600 {
0601     if (m_currentRequest.filter != filter) {
0602         m_currentRequest.page = -1;
0603     }
0604     m_currentRequest.filter = filter;
0605     reloadEntries();
0606 }
0607 
0608 Provider::Filter KNSCore::Engine::filter() const
0609 {
0610     return m_currentRequest.filter;
0611 }
0612 
0613 void KNSCore::Engine::fetchEntryById(const QString &id)
0614 {
0615     m_searchTimer->stop();
0616     m_currentRequest = KNSCore::Provider::SearchRequest(KNSCore::Provider::Newest, KNSCore::Provider::ExactEntryId, id);
0617     m_currentRequest.pageSize = m_pageSize;
0618 
0619     EntryInternal::List cache = m_cache->requestFromCache(m_currentRequest);
0620     if (!cache.isEmpty()) {
0621         reloadEntries();
0622     } else {
0623         m_searchTimer->start();
0624     }
0625 }
0626 
0627 void KNSCore::Engine::restoreSearch()
0628 {
0629     m_searchTimer->stop();
0630     m_currentRequest = d->storedRequest;
0631     if (m_cache) {
0632         EntryInternal::List cache = m_cache->requestFromCache(m_currentRequest);
0633         if (!cache.isEmpty()) {
0634             reloadEntries();
0635         } else {
0636             m_searchTimer->start();
0637         }
0638     } else {
0639         qCWarning(KNEWSTUFFCORE) << "Attempted to call restoreSearch() without a correctly initialized engine. You will likely get unexpected behaviour.";
0640     }
0641 }
0642 
0643 void KNSCore::Engine::storeSearch()
0644 {
0645     d->storedRequest = m_currentRequest;
0646 }
0647 
0648 void Engine::setSearchTerm(const QString &searchString)
0649 {
0650     m_searchTimer->stop();
0651     m_currentRequest.searchTerm = searchString;
0652     EntryInternal::List cache = m_cache->requestFromCache(m_currentRequest);
0653     if (!cache.isEmpty()) {
0654         reloadEntries();
0655     } else {
0656         m_searchTimer->start();
0657     }
0658 }
0659 
0660 QString KNSCore::Engine::searchTerm() const
0661 {
0662     return m_currentRequest.searchTerm;
0663 }
0664 
0665 void Engine::setTagFilter(const QStringList &filter)
0666 {
0667     d->tagFilter = filter;
0668     for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(m_providers)) {
0669         p->setTagFilter(d->tagFilter);
0670     }
0671 }
0672 
0673 QStringList Engine::tagFilter() const
0674 {
0675     return d->tagFilter;
0676 }
0677 
0678 void KNSCore::Engine::addTagFilter(const QString &filter)
0679 {
0680     d->tagFilter << filter;
0681     for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(m_providers)) {
0682         p->setTagFilter(d->tagFilter);
0683     }
0684 }
0685 
0686 void Engine::setDownloadTagFilter(const QStringList &filter)
0687 {
0688     d->downloadTagFilter = filter;
0689     for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(m_providers)) {
0690         p->setDownloadTagFilter(d->downloadTagFilter);
0691     }
0692 }
0693 
0694 QStringList Engine::downloadTagFilter() const
0695 {
0696     return d->downloadTagFilter;
0697 }
0698 
0699 void Engine::addDownloadTagFilter(const QString &filter)
0700 {
0701     d->downloadTagFilter << filter;
0702     for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(m_providers)) {
0703         p->setDownloadTagFilter(d->downloadTagFilter);
0704     }
0705 }
0706 
0707 void Engine::slotSearchTimerExpired()
0708 {
0709     reloadEntries();
0710 }
0711 
0712 void Engine::requestMoreData()
0713 {
0714     qCDebug(KNEWSTUFFCORE) << "Get more data! current page: " << m_currentPage << " requested: " << m_currentRequest.page;
0715 
0716     if (m_currentPage < m_currentRequest.page) {
0717         return;
0718     }
0719 
0720     m_currentRequest.page++;
0721     doRequest();
0722 }
0723 
0724 void Engine::requestData(int page, int pageSize)
0725 {
0726     m_currentRequest.page = page;
0727     m_currentRequest.pageSize = pageSize;
0728     doRequest();
0729 }
0730 
0731 void Engine::doRequest()
0732 {
0733     for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(m_providers)) {
0734         if (p->isInitialized()) {
0735             p->loadEntries(m_currentRequest);
0736             ++m_numDataJobs;
0737             updateStatus();
0738         }
0739     }
0740 }
0741 
0742 void Engine::install(KNSCore::EntryInternal entry, int linkId)
0743 {
0744     if (entry.downloadLinkCount() == 0 && entry.payload().isEmpty()) {
0745         // Turns out this happens sometimes, so we should deal with that and spit out an error
0746         qCDebug(KNEWSTUFFCORE) << "There were no downloadlinks defined in the entry we were just asked to update: " << entry.uniqueId() << "on provider"
0747                                << entry.providerId();
0748         Q_EMIT signalErrorCode(KNSCore::InstallationError,
0749                                i18n("Could not perform an installation of the entry %1 as it does not have any downloadable items defined. Please contact the "
0750                                     "author so they can fix this.",
0751                                     entry.name()),
0752                                entry.uniqueId());
0753     } else {
0754         if (entry.status() == KNS3::Entry::Updateable) {
0755             entry.setStatus(KNS3::Entry::Updating);
0756         } else {
0757             entry.setStatus(KNS3::Entry::Installing);
0758         }
0759         Q_EMIT signalEntryEvent(entry, EntryInternal::StatusChangedEvent);
0760 
0761         qCDebug(KNEWSTUFFCORE) << "Install " << entry.name() << " from: " << entry.providerId();
0762         QSharedPointer<Provider> p = m_providers.value(entry.providerId());
0763         if (p) {
0764             // If linkId is -1, assume that it's an update and that we don't know what to update
0765             if (entry.status() == KNS3::Entry::Updating && linkId == -1) {
0766                 if (entry.downloadLinkCount() == 1 || !entry.payload().isEmpty()) {
0767                     // If there is only one downloadable item (which also includes a predefined payload name), then we can fairly safely assume that's what
0768                     // we're wanting to update, meaning we can bypass some of the more expensive operations in downloadLinkLoaded
0769                     qCDebug(KNEWSTUFFCORE) << "Just the one download link, so let's use that";
0770                     d->payloadToIdentify[entry] = QString{};
0771                     linkId = 1;
0772                 } else {
0773                     qCDebug(KNEWSTUFFCORE) << "Try and identify a download link to use from a total of" << entry.downloadLinkCount();
0774                     // While this seems silly, the payload gets reset when fetching the new download link information
0775                     d->payloadToIdentify[entry] = entry.payload();
0776                     // Drop a fresh list in place so we've got something to work with when we get the links
0777                     d->payloads[entry] = QStringList{};
0778                     linkId = 1;
0779                 }
0780             } else {
0781                 qCDebug(KNEWSTUFFCORE) << "Link ID already known" << linkId;
0782                 // If there is no payload to identify, we will assume the payload is already known and just use that
0783                 d->payloadToIdentify[entry] = QString{};
0784             }
0785 
0786             p->loadPayloadLink(entry, linkId);
0787 
0788             ++m_numInstallJobs;
0789             updateStatus();
0790         }
0791     }
0792 }
0793 
0794 void Engine::slotInstallationFinished()
0795 {
0796     --m_numInstallJobs;
0797     updateStatus();
0798 }
0799 
0800 void Engine::slotInstallationFailed(const QString &message)
0801 {
0802     --m_numInstallJobs;
0803     Q_EMIT signalErrorCode(KNSCore::InstallationError, message, QVariant());
0804 }
0805 
0806 void Engine::slotEntryDetailsLoaded(const KNSCore::EntryInternal &entry)
0807 {
0808     --m_numDataJobs;
0809     updateStatus();
0810     Q_EMIT signalEntryEvent(entry, EntryInternal::DetailsLoadedEvent);
0811 }
0812 
0813 void Engine::downloadLinkLoaded(const KNSCore::EntryInternal &entry)
0814 {
0815     if (entry.status() == KNS3::Entry::Updating) {
0816         if (d->payloadToIdentify[entry].isEmpty()) {
0817             // If there's nothing to identify, and we've arrived here, then we know what the payload is
0818             qCDebug(KNEWSTUFFCORE) << "If there's nothing to identify, and we've arrived here, then we know what the payload is";
0819             m_installation->install(entry);
0820             d->payloadToIdentify.remove(entry);
0821         } else if (d->payloads[entry].count() < entry.downloadLinkCount()) {
0822             // We've got more to get before we can attempt to identify anything, so fetch the next one...
0823             qCDebug(KNEWSTUFFCORE) << "We've got more to get before we can attempt to identify anything, so fetch the next one...";
0824             QStringList payloads = d->payloads[entry];
0825             payloads << entry.payload();
0826             d->payloads[entry] = payloads;
0827             QSharedPointer<Provider> p = m_providers.value(entry.providerId());
0828             if (p) {
0829                 // ok, so this should definitely always work, but... safety first, kids!
0830                 p->loadPayloadLink(entry, payloads.count());
0831             }
0832         } else {
0833             // We now have all the links, so let's try and identify the correct one...
0834             qCDebug(KNEWSTUFFCORE) << "We now have all the links, so let's try and identify the correct one...";
0835             QString identifiedLink;
0836             const QString payloadToIdentify = d->payloadToIdentify[entry];
0837             const QList<EntryInternal::DownloadLinkInformation> downloadLinks = entry.downloadLinkInformationList();
0838             const QStringList &payloads = d->payloads[entry];
0839 
0840             if (payloads.contains(payloadToIdentify)) {
0841                 // Simplest option, the link hasn't changed at all
0842                 qCDebug(KNEWSTUFFCORE) << "Simplest option, the link hasn't changed at all";
0843                 identifiedLink = payloadToIdentify;
0844             } else {
0845                 // Next simplest option, filename is the same but in a different folder
0846                 qCDebug(KNEWSTUFFCORE) << "Next simplest option, filename is the same but in a different folder";
0847                 const QString fileName = payloadToIdentify.split(QChar::fromLatin1('/')).last();
0848                 for (const QString &payload : payloads) {
0849                     if (payload.endsWith(fileName)) {
0850                         identifiedLink = payload;
0851                         break;
0852                     }
0853                 }
0854 
0855                 // Possibly the payload itself is named differently (by a CDN, for example), but the link identifier is the same...
0856                 qCDebug(KNEWSTUFFCORE) << "Possibly the payload itself is named differently (by a CDN, for example), but the link identifier is the same...";
0857                 QStringList payloadNames;
0858                 for (const EntryInternal::DownloadLinkInformation &downloadLink : downloadLinks) {
0859                     qCDebug(KNEWSTUFFCORE) << "Download link" << downloadLink.name << downloadLink.id << downloadLink.size << downloadLink.descriptionLink;
0860                     payloadNames << downloadLink.name;
0861                     if (downloadLink.name == fileName) {
0862                         identifiedLink = payloads[payloadNames.count() - 1];
0863                         qCDebug(KNEWSTUFFCORE) << "Found a suitable download link for" << fileName << "which should match" << identifiedLink;
0864                     }
0865                 }
0866 
0867                 if (identifiedLink.isEmpty()) {
0868                     // Least simple option, no match - ask the user to pick (and if we still haven't got one... that's us done, no installation)
0869                     qCDebug(KNEWSTUFFCORE)
0870                         << "Least simple option, no match - ask the user to pick (and if we still haven't got one... that's us done, no installation)";
0871                     auto question = std::make_unique<Question>(Question::SelectFromListQuestion);
0872                     question->setTitle(i18n("Pick Update Item"));
0873                     question->setQuestion(
0874                         i18n("Please pick the item from the list below which should be used to apply this update. We were unable to identify which item to "
0875                              "select, based on the original item, which was named %1",
0876                              fileName));
0877                     question->setList(payloadNames);
0878                     if (question->ask() == Question::OKResponse) {
0879                         identifiedLink = payloads.value(payloadNames.indexOf(question->response()));
0880                     }
0881                 }
0882             }
0883             if (!identifiedLink.isEmpty()) {
0884                 KNSCore::EntryInternal theEntry(entry);
0885                 theEntry.setPayload(identifiedLink);
0886                 m_installation->install(theEntry);
0887             } else {
0888                 qCWarning(KNEWSTUFFCORE) << "We failed to identify a good link for updating" << entry.name() << "and are unable to perform the update";
0889                 KNSCore::EntryInternal theEntry(entry);
0890                 theEntry.setStatus(KNS3::Entry::Updateable);
0891                 Q_EMIT signalEntryEvent(theEntry, EntryInternal::StatusChangedEvent);
0892                 Q_EMIT signalErrorCode(ErrorCode::InstallationError,
0893                                        i18n("We failed to identify a good link for updating %1, and are unable to perform the update", entry.name()),
0894                                        {entry.uniqueId()});
0895             }
0896             // As the serverside data may change before next time this is called, even in the same session,
0897             // let's not make assumptions, and just get rid of this
0898             d->payloads.remove(entry);
0899             d->payloadToIdentify.remove(entry);
0900         }
0901     } else {
0902         m_installation->install(entry);
0903     }
0904 }
0905 
0906 void Engine::uninstall(KNSCore::EntryInternal entry)
0907 {
0908     const KNSCore::EntryInternal::List list = m_cache->registryForProvider(entry.providerId());
0909     // we have to use the cached entry here, not the entry from the provider
0910     // since that does not contain the list of installed files
0911     KNSCore::EntryInternal actualEntryForUninstall;
0912     for (const KNSCore::EntryInternal &eInt : list) {
0913         if (eInt.uniqueId() == entry.uniqueId()) {
0914             actualEntryForUninstall = eInt;
0915             break;
0916         }
0917     }
0918     if (!actualEntryForUninstall.isValid()) {
0919         qCDebug(KNEWSTUFFCORE) << "could not find a cached entry with following id:" << entry.uniqueId() << " ->  using the non-cached version";
0920         actualEntryForUninstall = entry;
0921     }
0922 
0923     entry.setStatus(KNS3::Entry::Installing);
0924     actualEntryForUninstall.setStatus(KNS3::Entry::Installing);
0925     Q_EMIT signalEntryEvent(entry, EntryInternal::StatusChangedEvent);
0926 
0927     qCDebug(KNEWSTUFFCORE) << "about to uninstall entry " << entry.uniqueId();
0928     m_installation->uninstall(actualEntryForUninstall);
0929 
0930     entry.setStatus(actualEntryForUninstall.status());
0931     Q_EMIT signalEntryEvent(entry, EntryInternal::StatusChangedEvent);
0932 }
0933 
0934 void Engine::loadDetails(const KNSCore::EntryInternal &entry)
0935 {
0936     QSharedPointer<Provider> p = m_providers.value(entry.providerId());
0937     p->loadEntryDetails(entry);
0938 }
0939 
0940 void Engine::loadPreview(const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type)
0941 {
0942     qCDebug(KNEWSTUFFCORE) << "START  preview: " << entry.name() << type;
0943     ImageLoader *l = new ImageLoader(entry, type, this);
0944     connect(l, &ImageLoader::signalPreviewLoaded, this, &Engine::slotPreviewLoaded);
0945     connect(l, &ImageLoader::signalError, this, [this](const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type, const QString &errorText) {
0946         Q_EMIT signalErrorCode(KNSCore::ImageError, errorText, QVariantList() << entry.name() << type);
0947         qCDebug(KNEWSTUFFCORE) << "ERROR preview: " << errorText << entry.name() << type;
0948         --m_numPictureJobs;
0949         updateStatus();
0950     });
0951     l->start();
0952     ++m_numPictureJobs;
0953     updateStatus();
0954 }
0955 
0956 void Engine::slotPreviewLoaded(const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type)
0957 {
0958     qCDebug(KNEWSTUFFCORE) << "FINISH preview: " << entry.name() << type;
0959     Q_EMIT signalEntryPreviewLoaded(entry, type);
0960     --m_numPictureJobs;
0961     updateStatus();
0962 }
0963 
0964 void Engine::contactAuthor(const EntryInternal &entry)
0965 {
0966     if (!entry.author().email().isEmpty()) {
0967         // invoke mail with the address of the author
0968         QUrl mailUrl;
0969         mailUrl.setScheme(QStringLiteral("mailto"));
0970         mailUrl.setPath(entry.author().email());
0971         QUrlQuery query;
0972         query.addQueryItem(QStringLiteral("subject"), i18n("Re: %1", entry.name()));
0973         mailUrl.setQuery(query);
0974         QDesktopServices::openUrl(mailUrl);
0975     } else if (!entry.author().homepage().isEmpty()) {
0976         QDesktopServices::openUrl(QUrl(entry.author().homepage()));
0977     }
0978 }
0979 
0980 void Engine::slotEntryChanged(const KNSCore::EntryInternal &entry)
0981 {
0982     Q_EMIT signalEntryEvent(entry, EntryInternal::StatusChangedEvent);
0983 }
0984 
0985 bool Engine::userCanVote(const EntryInternal &entry)
0986 {
0987     QSharedPointer<Provider> p = m_providers.value(entry.providerId());
0988     return p->userCanVote();
0989 }
0990 
0991 void Engine::vote(const EntryInternal &entry, uint rating)
0992 {
0993     QSharedPointer<Provider> p = m_providers.value(entry.providerId());
0994     p->vote(entry, rating);
0995 }
0996 
0997 bool Engine::userCanBecomeFan(const EntryInternal &entry)
0998 {
0999     QSharedPointer<Provider> p = m_providers.value(entry.providerId());
1000     return p->userCanBecomeFan();
1001 }
1002 
1003 void Engine::becomeFan(const EntryInternal &entry)
1004 {
1005     QSharedPointer<Provider> p = m_providers.value(entry.providerId());
1006     p->becomeFan(entry);
1007 }
1008 
1009 void Engine::updateStatus()
1010 {
1011     BusyState state;
1012     QString busyMessage;
1013     if (m_numInstallJobs > 0) {
1014         busyMessage = i18n("Installing");
1015         state |= BusyOperation::InstallingEntry;
1016     }
1017     if (m_numPictureJobs > 0) {
1018         busyMessage = i18np("Loading one preview", "Loading %1 previews", m_numPictureJobs);
1019         state |= BusyOperation::LoadingPreview;
1020     }
1021     if (m_numDataJobs > 0) {
1022         busyMessage = i18n("Loading data");
1023         state |= BusyOperation::LoadingPreview;
1024     }
1025     setBusy(state, busyMessage);
1026 }
1027 
1028 void Engine::checkForUpdates()
1029 {
1030     for (const QSharedPointer<KNSCore::Provider> &p : std::as_const(m_providers)) {
1031         Provider::SearchRequest request(KNSCore::Provider::Newest, KNSCore::Provider::Updates);
1032         p->loadEntries(request);
1033     }
1034 }
1035 
1036 void KNSCore::Engine::checkForInstalled()
1037 {
1038     EntryInternal::List entries = m_cache->registry();
1039     std::remove_if(entries.begin(), entries.end(), [](const auto &entry) {
1040         return entry.status() != KNS3::Entry::Installed && entry.status() != KNS3::Entry::Updateable;
1041     });
1042     Q_EMIT signalEntriesLoaded(entries);
1043 }
1044 
1045 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 77)
1046 QString Engine::adoptionCommand(const KNSCore::EntryInternal &entry) const
1047 {
1048     return d->getAdoptionCommand(m_adoptionCommand, entry, m_installation);
1049 }
1050 #endif
1051 
1052 bool KNSCore::Engine::hasAdoptionCommand() const
1053 {
1054     return !m_adoptionCommand.isEmpty();
1055 }
1056 
1057 void KNSCore::Engine::setPageSize(int pageSize)
1058 {
1059     m_pageSize = pageSize;
1060 }
1061 
1062 int KNSCore::Engine::pageSize() const
1063 {
1064     return m_pageSize;
1065 }
1066 
1067 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 83)
1068 QStringList KNSCore::Engine::configSearchLocations(bool includeFallbackLocations)
1069 {
1070     QStringList ret;
1071     if (includeFallbackLocations) {
1072         ret += QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation);
1073     }
1074     const QStringList paths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
1075     for (const QString &path : paths) {
1076         ret << QString::fromLocal8Bit("%1/knsrcfiles").arg(path);
1077     }
1078     return ret;
1079 }
1080 void KNSCore::Engine::setConfigLocationFallback(bool enableFallback)
1081 {
1082     d->configLocationFallback = enableFallback;
1083 }
1084 #endif
1085 
1086 QStringList KNSCore::Engine::availableConfigFiles()
1087 {
1088     QStringList configSearchLocations;
1089     configSearchLocations << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, //
1090                                                        QStringLiteral("knsrcfiles"),
1091                                                        QStandardPaths::LocateDirectory);
1092     configSearchLocations << QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation);
1093     return KFileUtils::findAllUniqueFiles(configSearchLocations, {QStringLiteral("*.knsrc")});
1094 }
1095 
1096 QSharedPointer<KNSCore::Provider> KNSCore::Engine::provider(const QString &providerId) const
1097 {
1098     return m_providers.value(providerId);
1099 }
1100 
1101 QSharedPointer<KNSCore::Provider> KNSCore::Engine::defaultProvider() const
1102 {
1103     if (m_providers.count() > 0) {
1104         return m_providers.constBegin().value();
1105     }
1106     return nullptr;
1107 }
1108 
1109 QStringList Engine::providerIDs() const
1110 {
1111     return m_providers.keys();
1112 }
1113 
1114 KNSCore::CommentsModel *KNSCore::Engine::commentsForEntry(const KNSCore::EntryInternal &entry)
1115 {
1116     CommentsModel *model = d->commentsModels[entry];
1117     if (!model) {
1118         model = new CommentsModel(this);
1119         model->setEntry(entry);
1120         connect(model, &QObject::destroyed, this, [=]() {
1121             d->commentsModels.remove(entry);
1122         });
1123         d->commentsModels[entry] = model;
1124     }
1125     return model;
1126 }
1127 
1128 QString Engine::busyMessage() const
1129 {
1130     return d->busyMessage;
1131 }
1132 
1133 void Engine::setBusyMessage(const QString &busyMessage)
1134 {
1135     if (busyMessage != d->busyMessage) {
1136         d->busyMessage = busyMessage;
1137         Q_EMIT busyMessageChanged();
1138     }
1139 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 74)
1140     // Emit old signals for compatibility
1141     if (busyMessage.isEmpty()) {
1142         Q_EMIT signalIdle({});
1143     } else {
1144         Q_EMIT signalBusy(busyMessage);
1145     }
1146 #endif
1147 }
1148 
1149 Engine::BusyState Engine::busyState() const
1150 {
1151     return d->busyState;
1152 }
1153 
1154 void Engine::setBusyState(Engine::BusyState state)
1155 {
1156     if (d->busyState != state) {
1157         d->busyState = state;
1158         Q_EMIT busyStateChanged();
1159     }
1160 }
1161 
1162 void Engine::setBusy(Engine::BusyState state, const QString &busyMessage)
1163 {
1164     setBusyState(state);
1165     setBusyMessage(busyMessage);
1166 }
1167 
1168 QSharedPointer<KNSCore::Cache> KNSCore::Engine::cache() const
1169 {
1170     return m_cache;
1171 }
1172 
1173 void KNSCore::Engine::revalidateCacheEntries()
1174 {
1175     // This gets called from QML, because in QtQuick we reuse the engine, BUG: 417985
1176     // We can't handle this in the cache, because it can't access the configuration of the engine
1177     if (m_cache && d->shouldRemoveDeletedEntries) {
1178         for (const auto &provider : std::as_const(m_providers)) {
1179             if (provider && provider->isInitialized()) {
1180                 const EntryInternal::List cacheBefore = m_cache->registryForProvider(provider->id());
1181                 m_cache->removeDeletedEntries();
1182                 const EntryInternal::List cacheAfter = m_cache->registryForProvider(provider->id());
1183                 // If the user has deleted them in the background we have to update the state to deleted
1184                 for (const auto &oldCachedEntry : cacheBefore) {
1185                     if (!cacheAfter.contains(oldCachedEntry)) {
1186                         EntryInternal removedEntry = oldCachedEntry;
1187                         removedEntry.setStatus(KNS3::Entry::Deleted);
1188                         Q_EMIT signalEntryEvent(removedEntry, EntryInternal::StatusChangedEvent);
1189                     }
1190                 }
1191             }
1192         }
1193     }
1194 }
1195 
1196 void Engine::adoptEntry(const EntryInternal &entry)
1197 {
1198     if (!hasAdoptionCommand()) {
1199         qCWarning(KNEWSTUFFCORE) << "no adoption command specified";
1200         return;
1201     }
1202     const QString command = d->getAdoptionCommand(m_adoptionCommand, entry, m_installation);
1203     QStringList split = KShell::splitArgs(command);
1204     QProcess *process = new QProcess(this);
1205     process->setProgram(split.takeFirst());
1206     process->setArguments(split);
1207 
1208     QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
1209     // The debug output is too talkative to be useful
1210     env.insert(QStringLiteral("QT_LOGGING_RULES"), QStringLiteral("*.debug=false"));
1211     process->setProcessEnvironment(env);
1212 
1213     process->start();
1214 
1215     connect(process, &QProcess::finished, this, [this, process, entry, command](int exitCode) {
1216         if (exitCode == 0) {
1217             Q_EMIT signalEntryEvent(entry, EntryInternal::EntryEvent::AdoptedEvent);
1218 
1219             // Handle error output as warnings if the process hasn't crashed
1220             const QString stdErr = QString::fromLocal8Bit(process->readAllStandardError());
1221             if (!stdErr.isEmpty()) {
1222                 Q_EMIT signalMessage(stdErr);
1223             }
1224         } else {
1225             const QString errorMsg = i18n("Failed to adopt '%1'\n%2", entry.name(), QString::fromLocal8Bit(process->readAllStandardError()));
1226             Q_EMIT signalErrorCode(KNSCore::AdoptionError, errorMsg, QVariantList{command});
1227         }
1228     });
1229 }
1230 
1231 QString Engine::useLabel() const
1232 {
1233     return d->useLabel;
1234 }
1235 
1236 bool KNSCore::Engine::uploadEnabled() const
1237 {
1238     return d->uploadEnabled;
1239 }
1240 
1241 QVector<Attica::Provider *> Engine::atticaProviders() const
1242 {
1243     QVector<Attica::Provider *> ret;
1244     ret.reserve(m_providers.size());
1245     for (const auto &p : m_providers) {
1246         const auto atticaProvider = p.dynamicCast<AtticaProvider>();
1247         if (atticaProvider) {
1248             ret += atticaProvider->provider();
1249         }
1250     }
1251     return ret;
1252 }
1253 
1254 #include "moc_engine.cpp"