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"