diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 9ab1ee07..84b64273 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -1,944 +1,942 @@ /* knewstuff3/engine.cpp Copyright (c) 2007 Josef Spillner Copyright (C) 2007-2010 Frederik Gladhorn Copyright (c) 2009 Jeremy Whiting Copyright (c) 2010 Matthias Fuchs This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "engine.h" #include "../entry.h" #include "commentsmodel.h" #include "installation.h" #include "question.h" #include "xmlloader.h" #include "imageloader_p.h" #include #include #include #include #include #include #include #include #include #include #include #if defined(Q_OS_WIN) #include #include #endif // libattica #include #include // own #include "../attica/atticaprovider_p.h" #include "cache.h" #include "../staticxml/staticxmlprovider_p.h" using namespace KNSCore; typedef QHash EngineProviderLoaderHash; Q_GLOBAL_STATIC(QThreadStorage, s_engineProviderLoaders) class EnginePrivate { public: QList categoriesMetadata; Attica::ProviderManager *m_atticaProviderManager = nullptr; QStringList tagFilter; QStringList downloadTagFilter; bool configLocationFallback = true; QString name; QMap commentsModels; // 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, and so // TODO KF6: Installed state needs to move onto a per-downloadlink basis rather than per-entry QMap payloads; QMap payloadToIdentify; }; Engine::Engine(QObject *parent) : QObject(parent) , m_installation(new Installation) , m_cache() , m_searchTimer(new QTimer) , d(new EnginePrivate) , m_currentPage(-1) , m_pageSize(20) , m_numDataJobs(0) , m_numPictureJobs(0) , m_numInstallJobs(0) , m_initialized(false) { m_searchTimer->setSingleShot(true); m_searchTimer->setInterval(1000); connect(m_searchTimer, &QTimer::timeout, this, &Engine::slotSearchTimerExpired); connect(m_installation, &Installation::signalInstallationFinished, this, &Engine::slotInstallationFinished); connect(m_installation, &Installation::signalInstallationFailed, this, &Engine::slotInstallationFailed); connect(m_installation, &Installation::signalInstallationError, this, [this](const QString &message){ emit signalErrorCode(ErrorCode::InstallationError, i18n("An error occurred during the installation process:\n%1", message), QVariant()); }); // Pass along old error signal through new signal for locations which have not been updated yet connect(this, &Engine::signalError, this, [this](const QString& message){ emit signalErrorCode(ErrorCode::UnknownError, message, QVariant()); }); } Engine::~Engine() { if (m_cache) { m_cache->writeRegistry(); } delete d->m_atticaProviderManager; delete m_searchTimer; delete m_installation; delete d; } bool Engine::init(const QString &configfile) { qCDebug(KNEWSTUFFCORE) << "Initializing KNSCore::Engine from '" << configfile << "'"; emit signalBusy(i18n("Initializing")); QScopedPointer conf; /// TODO KF6: This is fallback logic for an old location for the knsrc files. This should be considered deprecated in KF5, /// and it would make a lot of sense to disable it entirely for KF6 bool isRelativeConfig = QFileInfo(configfile).isRelative(); QString actualConfig; if (isRelativeConfig) { // Don't do the expensive search unless the config is relative actualConfig = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString::fromLatin1("knsrcfiles/%1").arg(configfile)); } QString configFileName{configfile}; if (isRelativeConfig && d->configLocationFallback && actualConfig.isEmpty()) { conf.reset(new KConfig(configfile)); qCWarning(KNEWSTUFFCORE) << "Using a deprecated location for the knsrc file" << configfile << " - please contact the author of the software which provides this file to get it updated to use the new location"; } else if (isRelativeConfig) { configFileName = QFileInfo(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString::fromLatin1("knsrcfiles/%1").arg(configfile))).baseName(); conf.reset(new KConfig(QString::fromLatin1("knsrcfiles/%1").arg(configfile), KConfig::FullConfig, QStandardPaths::GenericDataLocation)); } else { configFileName = QFileInfo(configfile).baseName(); conf.reset(new KConfig(configfile)); } if (conf->accessMode() == KConfig::NoAccess) { emit signalErrorCode(KNSCore::ConfigFileError, i18n("Configuration file exists, but cannot be opened: \"%1\"", configfile), configfile); qCCritical(KNEWSTUFFCORE) << "The knsrc file '" << configfile << "' was found but could not be opened."; return false; } KConfigGroup group; if (conf->hasGroup("KNewStuff3")) { qCDebug(KNEWSTUFFCORE) << "Loading KNewStuff3 config: " << configfile; group = conf->group("KNewStuff3"); } else if (conf->hasGroup("KNewStuff2")) { qCDebug(KNEWSTUFFCORE) << "Loading KNewStuff2 config: " << configfile; group = conf->group("KNewStuff2"); } else { emit signalErrorCode(KNSCore::ConfigFileError, i18n("Configuration file is invalid: \"%1\"", configfile), configfile); qCCritical(KNEWSTUFFCORE) << configfile << " doesn't contain a KNewStuff3 section."; return false; } d->name = group.readEntry("Name", QString()); m_categories = group.readEntry("Categories", QStringList()); m_adoptionCommand = group.readEntry("AdoptionCommand", QString()); qCDebug(KNEWSTUFFCORE) << "Categories: " << m_categories; m_providerFileUrl = group.readEntry("ProvidersUrl", QString()); d->tagFilter = group.readEntry("TagFilter", QStringList()); if (d->tagFilter.isEmpty()) { d->tagFilter.append(QStringLiteral("ghns_excluded!=1")); } d->downloadTagFilter = group.readEntry("DownloadTagFilter", QStringList()); // Make sure that config is valid if (!m_installation->readConfig(group)) { Q_EMIT signalError(i18n("Could not initialise the installation handler for %1\n" "This is a critical error and should be reported to the application author", configfile)); return false; } connect(m_installation, &Installation::signalEntryChanged, this, &Engine::slotEntryChanged); m_cache = Cache::getCache(configFileName); qCDebug(KNEWSTUFFCORE) << "Cache is" << m_cache << "for" << configFileName; connect(this, &Engine::signalEntryChanged, m_cache.data(), &Cache::registerChangedEntry); m_cache->readRegistry(); if (m_installation->uncompressionSetting() == Installation::UseKPackageUncompression) { m_cache->removeDeletedEntries(); } m_initialized = true; // load the providers loadProviders(); return true; } QString KNSCore::Engine::name() const { return d->name; } QStringList Engine::categories() const { return m_categories; } QStringList Engine::categoriesFilter() const { return m_currentRequest.categories; } QList Engine::categoriesMetadata() { return d->categoriesMetadata; } void Engine::loadProviders() { if (m_providerFileUrl.isEmpty()) { // it would be nicer to move the attica stuff into its own class qCDebug(KNEWSTUFFCORE) << "Using OCS default providers"; delete d->m_atticaProviderManager; d->m_atticaProviderManager = new Attica::ProviderManager; connect(d->m_atticaProviderManager, &Attica::ProviderManager::providerAdded, this, &Engine::atticaProviderLoaded); connect(d->m_atticaProviderManager, &Attica::ProviderManager::failedToLoad, this, &Engine::slotProvidersFailed); d->m_atticaProviderManager->loadDefaultProviders(); } else { qCDebug(KNEWSTUFFCORE) << "loading providers from " << m_providerFileUrl; emit signalBusy(i18n("Loading provider information")); XmlLoader *loader = s_engineProviderLoaders()->localData().value(m_providerFileUrl); if (!loader) { qCDebug(KNEWSTUFFCORE) << "No xml loader for this url yet, so create one and temporarily store that" << m_providerFileUrl; loader = new XmlLoader(this); s_engineProviderLoaders()->localData().insert(m_providerFileUrl, loader); connect(loader, &XmlLoader::signalLoaded, this, [this](){ s_engineProviderLoaders()->localData().remove(m_providerFileUrl); }); connect(loader, &XmlLoader::signalFailed, this, [this](){ s_engineProviderLoaders()->localData().remove(m_providerFileUrl); }); loader->load(QUrl(m_providerFileUrl)); } connect(loader, &XmlLoader::signalLoaded, this, &Engine::slotProviderFileLoaded); connect(loader, &XmlLoader::signalFailed, this, &Engine::slotProvidersFailed); } } void Engine::slotProviderFileLoaded(const QDomDocument &doc) { qCDebug(KNEWSTUFFCORE) << "slotProvidersLoaded"; bool isAtticaProviderFile = false; // get each provider element, and create a provider object from it QDomElement providers = doc.documentElement(); if (providers.tagName() == QLatin1String("providers")) { isAtticaProviderFile = true; } else if (providers.tagName() != QLatin1String("ghnsproviders") && providers.tagName() != QLatin1String("knewstuffproviders")) { qWarning() << "No document in providers.xml."; emit signalErrorCode(KNSCore::ProviderError, i18n("Could not load get hot new stuff providers from file: %1", m_providerFileUrl), m_providerFileUrl); return; } QDomElement n = providers.firstChildElement(QStringLiteral("provider")); while (!n.isNull()) { qCDebug(KNEWSTUFFCORE) << "Provider attributes: " << n.attribute(QStringLiteral("type")); QSharedPointer provider; if (isAtticaProviderFile || n.attribute(QStringLiteral("type")).toLower() == QLatin1String("rest")) { provider.reset(new AtticaProvider(m_categories, d->name)); connect(provider.data(), &Provider::categoriesMetadataLoded, this, [this](const QList &categories){ d->categoriesMetadata = categories; emit signalCategoriesMetadataLoded(categories); }); } else { provider.reset(new StaticXmlProvider); } if (provider->setProviderXML(n)) { addProvider(provider); } else { emit signalErrorCode(KNSCore::ProviderError, i18n("Error initializing provider."), m_providerFileUrl); } n = n.nextSiblingElement(); } emit signalBusy(i18n("Loading data")); } void Engine::atticaProviderLoaded(const Attica::Provider &atticaProvider) { qCDebug(KNEWSTUFFCORE) << "atticaProviderLoaded called"; if (!atticaProvider.hasContentService()) { qCDebug(KNEWSTUFFCORE) << "Found provider: " << atticaProvider.baseUrl() << " but it does not support content"; return; } QSharedPointer provider = QSharedPointer (new AtticaProvider(atticaProvider, m_categories, d->name)); connect(provider.data(), &Provider::categoriesMetadataLoded, this, [this](const QList &categories){ d->categoriesMetadata = categories; emit signalCategoriesMetadataLoded(categories); }); addProvider(provider); } void Engine::addProvider(QSharedPointer provider) { qCDebug(KNEWSTUFFCORE) << "Engine addProvider called with provider with id " << provider->id(); m_providers.insert(provider->id(), provider); provider->setTagFilter(d->tagFilter); provider->setDownloadTagFilter(d->downloadTagFilter); connect(provider.data(), &Provider::providerInitialized, this, &Engine::providerInitialized); connect(provider.data(), &Provider::loadingFinished, this, &Engine::slotEntriesLoaded); connect(provider.data(), &Provider::entryDetailsLoaded, this, &Engine::slotEntryDetailsLoaded); connect(provider.data(), &Provider::payloadLinkLoaded, this, &Engine::downloadLinkLoaded); connect(provider.data(), &Provider::signalError, this, &Engine::signalError); connect(provider.data(), &Provider::signalErrorCode, this, &Engine::signalErrorCode); connect(provider.data(), &Provider::signalInformation, this, &Engine::signalIdle); } void Engine::providerJobStarted(KJob *job) { emit jobStarted(job, i18n("Loading data from provider")); } void Engine::slotProvidersFailed() { emit signalErrorCode(KNSCore::ProviderError, i18n("Loading of providers from file: %1 failed", m_providerFileUrl), m_providerFileUrl); } void Engine::providerInitialized(Provider *p) { qCDebug(KNEWSTUFFCORE) << "providerInitialized" << p->name(); p->setCachedEntries(m_cache->registryForProvider(p->id())); updateStatus(); for (const QSharedPointer &p : qAsConst(m_providers)) { if (!p->isInitialized()) { return; } } emit signalProvidersLoaded(); } void Engine::slotEntriesLoaded(const KNSCore::Provider::SearchRequest &request, KNSCore::EntryInternal::List entries) { m_currentPage = qMax(request.page, m_currentPage); qCDebug(KNEWSTUFFCORE) << "loaded page " << request.page << "current page" << m_currentPage << "count:" << entries.count(); if (request.filter == Provider::Updates) { emit signalUpdateableEntriesLoaded(entries); } else { m_cache->insertRequest(request, entries); emit signalEntriesLoaded(entries); } --m_numDataJobs; updateStatus(); } void Engine::reloadEntries() { emit signalResetView(); m_currentPage = -1; m_currentRequest.pageSize = m_pageSize; m_currentRequest.page = 0; m_numDataJobs = 0; for (const QSharedPointer &p : qAsConst(m_providers)) { if (p->isInitialized()) { if (m_currentRequest.filter == Provider::Installed) { // when asking for installed entries, never use the cache p->loadEntries(m_currentRequest); } else { // take entries from cache until there are no more EntryInternal::List cache; EntryInternal::List lastCache = m_cache->requestFromCache(m_currentRequest); while (!lastCache.isEmpty()) { qCDebug(KNEWSTUFFCORE) << "From cache"; cache << lastCache; m_currentPage = m_currentRequest.page; ++m_currentRequest.page; lastCache = m_cache->requestFromCache(m_currentRequest); } // Since the cache has no more pages, reset the request's page if (m_currentPage >= 0) { m_currentRequest.page = m_currentPage; } if (!cache.isEmpty()) { emit signalEntriesLoaded(cache); } else { qCDebug(KNEWSTUFFCORE) << "From provider"; p->loadEntries(m_currentRequest); ++m_numDataJobs; updateStatus(); } } } } } void Engine::setCategoriesFilter(const QStringList &categories) { m_currentRequest.categories = categories; reloadEntries(); } void Engine::setSortMode(Provider::SortMode mode) { if (m_currentRequest.sortMode != mode) { m_currentRequest.page = -1; } m_currentRequest.sortMode = mode; reloadEntries(); } Provider::SortMode KNSCore::Engine::sortMode() const { return m_currentRequest.sortMode; } void KNSCore::Engine::setFilter(Provider::Filter filter) { if (m_currentRequest.filter != filter) { m_currentRequest.page = -1; } m_currentRequest.filter = filter; reloadEntries(); } Provider::Filter KNSCore::Engine::filter() const { return m_currentRequest.filter; } void KNSCore::Engine::fetchEntryById(const QString &id) { m_searchTimer->stop(); m_currentRequest = KNSCore::Provider::SearchRequest(KNSCore::Provider::Newest, KNSCore::Provider::ExactEntryId, id); m_currentRequest.pageSize = m_pageSize; EntryInternal::List cache = m_cache->requestFromCache(m_currentRequest); if (!cache.isEmpty()) { reloadEntries(); } else { m_searchTimer->start(); } } void Engine::setSearchTerm(const QString &searchString) { m_searchTimer->stop(); m_currentRequest.searchTerm = searchString; EntryInternal::List cache = m_cache->requestFromCache(m_currentRequest); if (!cache.isEmpty()) { reloadEntries(); } else { m_searchTimer->start(); } } QString KNSCore::Engine::searchTerm() const { return m_currentRequest.searchTerm; } void Engine::setTagFilter(const QStringList &filter) { d->tagFilter = filter; for (const QSharedPointer &p : qAsConst(m_providers)) { p->setTagFilter(d->tagFilter); } } QStringList Engine::tagFilter() const { return d->tagFilter; } void KNSCore::Engine::addTagFilter(const QString &filter) { d->tagFilter << filter; for (const QSharedPointer &p : qAsConst(m_providers)) { p->setTagFilter(d->tagFilter); } } void Engine::setDownloadTagFilter(const QStringList &filter) { d->downloadTagFilter = filter; for (const QSharedPointer &p : qAsConst(m_providers)) { p->setDownloadTagFilter(d->downloadTagFilter); } } QStringList Engine::downloadTagFilter() const { return d->downloadTagFilter; } void Engine::addDownloadTagFilter(const QString &filter) { d->downloadTagFilter << filter; for (const QSharedPointer &p : qAsConst(m_providers)) { p->setDownloadTagFilter(d->downloadTagFilter); } } void Engine::slotSearchTimerExpired() { reloadEntries(); } void Engine::requestMoreData() { qCDebug(KNEWSTUFFCORE) << "Get more data! current page: " << m_currentPage << " requested: " << m_currentRequest.page; if (m_currentPage < m_currentRequest.page) { return; } m_currentRequest.page++; doRequest(); } void Engine::requestData(int page, int pageSize) { m_currentRequest.page = page; m_currentRequest.pageSize = pageSize; doRequest(); } void Engine::doRequest() { for (const QSharedPointer &p : qAsConst(m_providers)) { if (p->isInitialized()) { p->loadEntries(m_currentRequest); ++m_numDataJobs; updateStatus(); } } } void Engine::install(KNSCore::EntryInternal entry, int linkId) { if (entry.status() == KNS3::Entry::Updateable) { entry.setStatus(KNS3::Entry::Updating); } else { entry.setStatus(KNS3::Entry::Installing); } emit signalEntryChanged(entry); qCDebug(KNEWSTUFFCORE) << "Install " << entry.name() << " from: " << entry.providerId(); QSharedPointer p = m_providers.value(entry.providerId()); if (p) { // If linkId is -1, assume that it's an update and that we don't know what to update if (entry.status() == KNS3::Entry::Updating && linkId == -1) { if (entry.downloadLinkCount() == 1) { // If there is only one downloadable item, then we can fairly safely assume that's what we're wanting // to update, meaning we can bypass some of the more expensive operations in downloadLinkLoaded qCDebug(KNEWSTUFFCORE) << "Just the one download link, so let's use that"; d->payloadToIdentify[entry] = QString{}; linkId = 1; } else { qCDebug(KNEWSTUFFCORE) << "Try and identify a download link to use from a total of" << entry.downloadLinkCount(); // While this seems silly, the payload gets reset when fetching the new download link information d->payloadToIdentify[entry] = entry.payload(); // Drop a fresh list in place so we've got something to work with when we get the links d->payloads[entry] = QStringList{}; linkId = 1; } } else { qCDebug(KNEWSTUFFCORE) << "Link ID already known" << linkId; // If there is no payload to identify, we will assume the payload is already known and just use that d->payloadToIdentify[entry] = QString{}; } p->loadPayloadLink(entry, linkId); ++m_numInstallJobs; updateStatus(); } } void Engine::slotInstallationFinished() { --m_numInstallJobs; updateStatus(); } void Engine::slotInstallationFailed(const QString &message) { --m_numInstallJobs; emit signalErrorCode(KNSCore::InstallationError, message, QVariant()); } void Engine::slotEntryDetailsLoaded(const KNSCore::EntryInternal &entry) { emit signalEntryDetailsLoaded(entry); } void Engine::downloadLinkLoaded(const KNSCore::EntryInternal &entry) { if (entry.status() == KNS3::Entry::Updating) { if (d->payloadToIdentify.isEmpty()) { // If there's nothing to identify, and we've arrived here, then we know what the payload is qCDebug(KNEWSTUFFCORE) << "If there's nothing to identify, and we've arrived here, then we know what the payload is"; m_installation->install(entry); } else if (d->payloads[entry].count() < entry.downloadLinkCount()) { // We've got more to get before we can attempt to identify anything, so fetch the next one... qCDebug(KNEWSTUFFCORE) << "We've got more to get before we can attempt to identify anything, so fetch the next one..."; QStringList payloads = d->payloads[entry]; payloads << entry.payload(); d->payloads[entry] = payloads; QSharedPointer p = m_providers.value(entry.providerId()); if (p) { // ok, so this should definitely always work, but... safety first, kids! p->loadPayloadLink(entry, payloads.count()); } } else { // We now have all the links, so let's try and identify the correct one... qCDebug(KNEWSTUFFCORE) << "We now have all the links, so let's try and identify the correct one..."; QString identifiedLink; const QString payloadToIdentify = d->payloadToIdentify[entry]; const QList downloadLinks = entry.downloadLinkInformationList(); const QStringList &payloads = d->payloads[entry]; if (payloads.contains(payloadToIdentify)) { // Simplest option, the link hasn't changed at all qCDebug(KNEWSTUFFCORE) << "Simplest option, the link hasn't changed at all"; identifiedLink = payloadToIdentify; } else { // Next simplest option, filename is the same but in a different folder qCDebug(KNEWSTUFFCORE) << "Next simplest option, filename is the same but in a different folder"; const QStringRef fileName = payloadToIdentify.splitRef(QChar::fromLatin1('/')).last(); for (const QString &payload : payloads) { if (payload.endsWith(fileName)) { identifiedLink = payload; break; } } // Possibly the payload itself is named differently (by a CDN, for example), but the link identifier is the same... qCDebug(KNEWSTUFFCORE) << "Possibly the payload itself is named differently (by a CDN, for example), but the link identifier is the same..."; QStringList payloadNames; for (const EntryInternal::DownloadLinkInformation &downloadLink : downloadLinks) { qCDebug(KNEWSTUFFCORE) << "Download link" << downloadLink.name << downloadLink.id << downloadLink.size << downloadLink.descriptionLink; payloadNames << downloadLink.name; if (downloadLink.name == fileName) { identifiedLink = payloads[payloadNames.count() - 1]; qCDebug(KNEWSTUFFCORE) << "Found a suitable download link for" << fileName << "which should match" << identifiedLink; } } if (identifiedLink.isEmpty()) { // Least simple option, no match - ask the user to pick (and if we still haven't got one... that's us done, no installation) qCDebug(KNEWSTUFFCORE) << "Least simple option, no match - ask the user to pick (and if we still haven't got one... that's us done, no installation)"; auto question = std::make_unique(Question::SelectFromListQuestion); question->setTitle(i18n("Pick Update Item")); question->setQuestion(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 select, based on the original item, which was named %1").arg(fileName)); question->setList(payloadNames); if(question->ask() == Question::OKResponse) { identifiedLink = payloads.value(payloadNames.indexOf(question->response())); } } } if (!identifiedLink.isEmpty()) { KNSCore::EntryInternal theEntry(entry); theEntry.setPayload(identifiedLink); m_installation->install(theEntry); } else { qCWarning(KNEWSTUFFCORE) << "We failed to identify a good link for updating" << entry.name() << "and are unable to perform the update"; } // As the serverside data may change before next time this is called, even in the same session, // let's not make assumptions, and just get rid of this d->payloads.remove(entry); d->payloadToIdentify.remove(entry); } } else { m_installation->install(entry); } } void Engine::uninstall(KNSCore::EntryInternal entry) { const KNSCore::EntryInternal::List list = m_cache->registryForProvider(entry.providerId()); //we have to use the cached entry here, not the entry from the provider //since that does not contain the list of installed files KNSCore::EntryInternal actualEntryForUninstall; for (const KNSCore::EntryInternal &eInt : list) { if (eInt.uniqueId() == entry.uniqueId()) { actualEntryForUninstall = eInt; break; } } if (!actualEntryForUninstall.isValid()) { qCDebug(KNEWSTUFFCORE) << "could not find a cached entry with following id:" << entry.uniqueId() << " -> using the non-cached version"; return; } entry.setStatus(KNS3::Entry::Installing); actualEntryForUninstall.setStatus(KNS3::Entry::Installing); emit signalEntryChanged(entry); qCDebug(KNEWSTUFFCORE) << "about to uninstall entry " << entry.uniqueId(); - // FIXME: change the status? m_installation->uninstall(actualEntryForUninstall); - entry.setStatus(KNS3::Entry::Deleted); //status for actual entry gets set in m_installation->uninstall() + entry.setStatus(actualEntryForUninstall.status()); emit signalEntryChanged(entry); - } void Engine::loadDetails(const KNSCore::EntryInternal &entry) { QSharedPointer p = m_providers.value(entry.providerId()); p->loadEntryDetails(entry); } void Engine::loadPreview(const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type) { qCDebug(KNEWSTUFFCORE) << "START preview: " << entry.name() << type; ImageLoader *l = new ImageLoader(entry, type, this); connect(l, &ImageLoader::signalPreviewLoaded, this, &Engine::slotPreviewLoaded); connect(l, &ImageLoader::signalError, this, [this](const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type, const QString &errorText) { emit signalErrorCode(KNSCore::ImageError, errorText, QVariantList() << entry.name() << type); qCDebug(KNEWSTUFFCORE) << "ERROR preview: " << errorText << entry.name() << type; --m_numPictureJobs; updateStatus(); }); l->start(); ++m_numPictureJobs; updateStatus(); } void Engine::slotPreviewLoaded(const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type) { qCDebug(KNEWSTUFFCORE) << "FINISH preview: " << entry.name() << type; emit signalEntryPreviewLoaded(entry, type); --m_numPictureJobs; updateStatus(); } void Engine::contactAuthor(const EntryInternal &entry) { if (!entry.author().email().isEmpty()) { // invoke mail with the address of the author QUrl mailUrl; mailUrl.setScheme(QStringLiteral("mailto")); mailUrl.setPath(entry.author().email()); QUrlQuery query; query.addQueryItem(QStringLiteral("subject"), i18n("Re: %1", entry.name())); mailUrl.setQuery(query); QDesktopServices::openUrl(mailUrl); } else if (!entry.author().homepage().isEmpty()) { QDesktopServices::openUrl(QUrl(entry.author().homepage())); } } void Engine::slotEntryChanged(const KNSCore::EntryInternal &entry) { emit signalEntryChanged(entry); } bool Engine::userCanVote(const EntryInternal &entry) { QSharedPointer p = m_providers.value(entry.providerId()); return p->userCanVote(); } void Engine::vote(const EntryInternal &entry, uint rating) { QSharedPointer p = m_providers.value(entry.providerId()); p->vote(entry, rating); } bool Engine::userCanBecomeFan(const EntryInternal &entry) { QSharedPointer p = m_providers.value(entry.providerId()); return p->userCanBecomeFan(); } void Engine::becomeFan(const EntryInternal &entry) { QSharedPointer p = m_providers.value(entry.providerId()); p->becomeFan(entry); } void Engine::updateStatus() { if (m_numDataJobs > 0) { emit signalBusy(i18n("Loading data")); } else if (m_numPictureJobs > 0) { emit signalBusy(i18np("Loading one preview", "Loading %1 previews", m_numPictureJobs)); } else if (m_numInstallJobs > 0) { emit signalBusy(i18n("Installing")); } else { emit signalIdle(QString()); } } void Engine::checkForUpdates() { for (const QSharedPointer &p : qAsConst(m_providers)) { Provider::SearchRequest request(KNSCore::Provider::Newest, KNSCore::Provider::Updates); p->loadEntries(request); } } void KNSCore::Engine::checkForInstalled() { for (const QSharedPointer &p : qAsConst(m_providers)) { Provider::SearchRequest request(KNSCore::Provider::Newest, KNSCore::Provider::Installed); request.page = 0; request.pageSize = m_pageSize; p->loadEntries(request); } } /** * we look for the directory where all the resources got installed. * assuming it was extracted into a directory */ static QDir sharedDir(QStringList dirs, QString rootPath) { // Ensure that rootPath definitely is a clean path with a slash at the end rootPath = QDir::cleanPath(rootPath) + QStringLiteral("/"); qCInfo(KNEWSTUFFCORE) << Q_FUNC_INFO << dirs << rootPath; while(!dirs.isEmpty()) { QString thisDir(dirs.takeLast()); if (thisDir.endsWith(QStringLiteral("*"))) { qCInfo(KNEWSTUFFCORE) << "Directory entry" << thisDir << "ends in a *, indicating this was installed from an archive - see Installation::archiveEntries"; thisDir.chop(1); } const QString currentPath = QDir::cleanPath(thisDir); qCInfo(KNEWSTUFFCORE) << "Current path is" << currentPath; if (!currentPath.startsWith(rootPath)) { qCInfo(KNEWSTUFFCORE) << "Current path" << currentPath << "does not start with" << rootPath << "and should be ignored"; continue; } const QFileInfo current(currentPath); qCInfo(KNEWSTUFFCORE) << "Current file info is" << current; if (!current.isDir()) { qCInfo(KNEWSTUFFCORE) << "Current path" << currentPath << "is not a directory, and should be ignored"; continue; } const QDir dir(currentPath); if (dir.path()==(rootPath+dir.dirName())) { qCDebug(KNEWSTUFFCORE) << "Found directory" << dir; return dir; } } qCWarning(KNEWSTUFFCORE) << "Failed to locate any shared installed directory in" << dirs << "and this is almost certainly very bad."; return {}; } QString Engine::adoptionCommand(const KNSCore::EntryInternal& entry) const { auto adoption = m_adoptionCommand; if(adoption.isEmpty()) return {}; const QLatin1String dirReplace("%d"); if (adoption.contains(dirReplace)) { QString installPath = sharedDir(entry.installedFiles(), m_installation->targetInstallationPath()).path(); adoption.replace(dirReplace, installPath); } const QLatin1String fileReplace("%f"); if (adoption.contains(fileReplace)) { if (entry.installedFiles().isEmpty()) { qCWarning(KNEWSTUFFCORE) << "no installed files to adopt"; } else if (entry.installedFiles().count() != 1) { qCWarning(KNEWSTUFFCORE) << "can only adopt one file, will be using the first" << entry.installedFiles().at(0); } adoption.replace(fileReplace, entry.installedFiles().at(0)); } return adoption; } bool KNSCore::Engine::hasAdoptionCommand() const { return !m_adoptionCommand.isEmpty(); } void KNSCore::Engine::setPageSize(int pageSize) { m_pageSize = pageSize; } QStringList KNSCore::Engine::configSearchLocations(bool includeFallbackLocations) { QStringList ret; if(includeFallbackLocations) { ret += QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation); } QStringList paths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); for( const QString& path : paths) { ret << QString::fromLocal8Bit("%1/knsrcfiles").arg(path); } return ret; } void KNSCore::Engine::setConfigLocationFallback(bool enableFallback) { d->configLocationFallback = enableFallback; } QSharedPointer KNSCore::Engine::provider(const QString &providerId) const { return m_providers.value(providerId); } QSharedPointer KNSCore::Engine::defaultProvider() const { if (m_providers.count() > 0) return m_providers.constBegin().value(); return nullptr; } KNSCore::CommentsModel *KNSCore::Engine::commentsForEntry(const KNSCore::EntryInternal &entry) { CommentsModel *model = d->commentsModels[entry]; if (!model) { model = new CommentsModel(this); model->setEntry(entry); connect(model, &QObject::destroyed, this, [=](){ d->commentsModels.remove(entry); }); d->commentsModels[entry] = model; } return model; } diff --git a/src/core/installation.cpp b/src/core/installation.cpp index ad1e4679..49213fb0 100644 --- a/src/core/installation.cpp +++ b/src/core/installation.cpp @@ -1,899 +1,930 @@ /* This file is part of KNewStuff2. Copyright (c) 2007 Josef Spillner Copyright (C) 2009 Frederik Gladhorn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "installation.h" #include #include #include #include #include #include #include "qmimedatabase.h" #include "karchive.h" #include "kzip.h" #include "ktar.h" #include "krandom.h" #include "kshell.h" #include #include #include #include "jobs/kpackagejob.h" #include #include "klocalizedstring.h" #include #include "jobs/filecopyjob.h" #include "question.h" #ifdef Q_OS_WIN #include #include #endif using namespace KNSCore; Installation::Installation(QObject *parent) : QObject(parent) , checksumPolicy(Installation::CheckIfPossible) , signaturePolicy(Installation::CheckIfPossible) , scope(Installation::ScopeUser) , customName(false) , acceptHtml(false) { // TODO KF6 Make this a real property, when we can refactor this and add a proper dptr setProperty("kpackageType", QLatin1String("")); } bool Installation::readConfig(const KConfigGroup &group) { // FIXME: add support for several categories later on // FIXME: read out only when actually installing as a performance improvement? QString uncompresssetting = group.readEntry("Uncompress", QStringLiteral("never")); // support old value of true as equivalent of always if (uncompresssetting == QLatin1String("subdir")) { uncompresssetting = QStringLiteral("subdir"); } else if (uncompresssetting == QLatin1String("true")) { uncompresssetting = QStringLiteral("always"); } if (uncompresssetting != QLatin1String("always") && uncompresssetting != QLatin1String("archive") && uncompresssetting != QLatin1String("never") && uncompresssetting != QLatin1String("subdir") && uncompresssetting != QLatin1String("kpackage")) { qCCritical(KNEWSTUFFCORE) << "invalid Uncompress setting chosen, must be one of: subdir, always, archive, never, or kpackage"; return false; } uncompression = uncompresssetting; setProperty("kpackageType", group.readEntry("KPackageType", QString())); postInstallationCommand = group.readEntry("InstallationCommand", QString()); uninstallCommand = group.readEntry("UninstallCommand", QString()); standardResourceDirectory = group.readEntry("StandardResource", QString()); targetDirectory = group.readEntry("TargetDir", QString()); xdgTargetDirectory = group.readEntry("XdgTargetDir", QString()); // Provide some compatibility if (standardResourceDirectory == QLatin1String("wallpaper")) { xdgTargetDirectory = QStringLiteral("wallpapers"); } // also, ensure wallpapers are decompressed into subdirectories // this ensures that wallpapers with multiple resolutions continue to function // as expected if (xdgTargetDirectory == QLatin1String("wallpapers")) { uncompression = QStringLiteral("subdir"); } // A touch of special treatment for the various old kpackage based knsrc files, so they work // with the new, internal stuff. The result unfortunately is that all entries marked as // installed in knewstuff no longer will be, but since it never worked right anyway... we'll // simply have to live with that. if (postInstallationCommand.startsWith(QLatin1String("kpackagetool5 -t")) && postInstallationCommand.endsWith(QLatin1String("-i %f")) && uninstallCommand.startsWith(QLatin1String("kpackagetool5 -t")) && uninstallCommand.endsWith(QLatin1String("-r %f"))) { uncompression = QLatin1String("kpackage"); postInstallationCommand = QLatin1String(""); // Not clearing uninstallCommand, as this is used for the fallback situation setProperty("kpackageType", uninstallCommand.mid(17, uninstallCommand.length() - 17 - 6)); qCWarning(KNEWSTUFFCORE) << "Your configuration file uses an old version of the kpackage support, and should be converted. Please report this to the author of the software you are currently using. The package type, we assume, is" << property("kpackageType").toString(); } installPath = group.readEntry("InstallPath", QString()); absoluteInstallPath = group.readEntry("AbsoluteInstallPath", QString()); customName = group.readEntry("CustomName", false); acceptHtml = group.readEntry("AcceptHtmlDownloads", false); if (standardResourceDirectory.isEmpty() && targetDirectory.isEmpty() && xdgTargetDirectory.isEmpty() && installPath.isEmpty() && absoluteInstallPath.isEmpty()) { qCCritical(KNEWSTUFFCORE) << "No installation target set"; return false; } QString checksumpolicy = group.readEntry("ChecksumPolicy", QString()); if (!checksumpolicy.isEmpty()) { if (checksumpolicy == QLatin1String("never")) { checksumPolicy = Installation::CheckNever; } else if (checksumpolicy == QLatin1String("ifpossible")) { checksumPolicy = Installation::CheckIfPossible; } else if (checksumpolicy == QLatin1String("always")) { checksumPolicy = Installation::CheckAlways; } else { qCCritical(KNEWSTUFFCORE) << QStringLiteral("The checksum policy '") + checksumpolicy + QStringLiteral("' is unknown."); return false; } } QString signaturepolicy = group.readEntry("SignaturePolicy", QString()); if (!signaturepolicy.isEmpty()) { if (signaturepolicy == QLatin1String("never")) { signaturePolicy = Installation::CheckNever; } else if (signaturepolicy == QLatin1String("ifpossible")) { signaturePolicy = Installation::CheckIfPossible; } else if (signaturepolicy == QLatin1String("always")) { signaturePolicy = Installation::CheckAlways; } else { qCCritical(KNEWSTUFFCORE) << QStringLiteral("The signature policy '") + signaturepolicy + QStringLiteral("' is unknown."); return false; } } QString scopeString = group.readEntry("Scope", QString()); if (!scopeString.isEmpty()) { if (scopeString == QLatin1String("user")) { scope = ScopeUser; } else if (scopeString == QLatin1String("system")) { scope = ScopeSystem; } else { qCCritical(KNEWSTUFFCORE) << QStringLiteral("The scope '") + scopeString + QStringLiteral("' is unknown."); return false; } if (scope == ScopeSystem) { if (!installPath.isEmpty()) { qCCritical(KNEWSTUFFCORE) << "System installation cannot be mixed with InstallPath."; return false; } } } return true; } bool Installation::isRemote() const { return false; } void Installation::install(const EntryInternal& entry) { downloadPayload(entry); } void Installation::downloadPayload(const KNSCore::EntryInternal &entry) { if (!entry.isValid()) { emit signalInstallationFailed(i18n("Invalid item.")); return; } QUrl source = QUrl(entry.payload()); if (!source.isValid()) { qCCritical(KNEWSTUFFCORE) << "The entry doesn't have a payload."; emit signalInstallationFailed(i18n("Download of item failed: no download URL for \"%1\".", entry.name())); return; } QString fileName(source.fileName()); QTemporaryFile tempFile(QDir::tempPath() + QStringLiteral("/XXXXXX-") + fileName); if (!tempFile.open()) { return; // ERROR } QUrl destination = QUrl::fromLocalFile(tempFile.fileName()); qCDebug(KNEWSTUFFCORE) << "Downloading payload" << source << "to" << destination; // FIXME: check for validity FileCopyJob *job = FileCopyJob::file_copy(source, destination, -1, JobFlag::Overwrite | JobFlag::HideProgressInfo); connect(job, &KJob::result, this, &Installation::slotPayloadResult); entry_jobs[job] = entry; } void Installation::slotPayloadResult(KJob *job) { // for some reason this slot is getting called 3 times on one job error if (entry_jobs.contains(job)) { EntryInternal entry = entry_jobs[job]; entry_jobs.remove(job); if (job->error()) { emit signalInstallationFailed(i18n("Download of \"%1\" failed, error: %2", entry.name(), job->errorString())); } else { FileCopyJob *fcjob = static_cast(job); // check if the app likes html files - disabled by default as too many bad links have been submitted to opendesktop.org if (!acceptHtml) { QMimeDatabase db; QMimeType mimeType = db.mimeTypeForFile(fcjob->destUrl().toLocalFile()); if (mimeType.inherits(QStringLiteral("text/html")) || mimeType.inherits(QStringLiteral("application/x-php"))) { Question question; question.setQuestion(i18n("The downloaded file is a html file. This indicates a link to a website instead of the actual download. Would you like to open the site with a browser instead?")); question.setTitle(i18n("Possibly bad download link")); if(question.ask() == Question::YesResponse) { QDesktopServices::openUrl(fcjob->srcUrl()); emit signalInstallationFailed(i18n("Downloaded file was a HTML file. Opened in browser.")); entry.setStatus(KNS3::Entry::Invalid); emit signalEntryChanged(entry); return; } } } emit signalPayloadLoaded(fcjob->destUrl()); install(entry, fcjob->destUrl().toLocalFile()); } } } void KNSCore::Installation::install(KNSCore::EntryInternal entry, const QString& downloadedFile) { qCDebug(KNEWSTUFFCORE) << "Install: " << entry.name() << " from " << downloadedFile; if (entry.payload().isEmpty()) { qCDebug(KNEWSTUFFCORE) << "No payload associated with: " << entry.name(); return; } // this means check sum comparison and signature verification // signature verification might take a long time - make async?! /* if (checksumPolicy() != Installation::CheckNever) { if (entry.checksum().isEmpty()) { if (checksumPolicy() == Installation::CheckIfPossible) { qCDebug(KNEWSTUFFCORE) << "Skip checksum verification"; } else { qCCritical(KNEWSTUFFCORE) << "Checksum verification not possible"; return false; } } else { qCDebug(KNEWSTUFFCORE) << "Verify checksum..."; } } if (signaturePolicy() != Installation::CheckNever) { if (entry.signature().isEmpty()) { if (signaturePolicy() == Installation::CheckIfPossible) { qCDebug(KNEWSTUFFCORE) << "Skip signature verification"; } else { qCCritical(KNEWSTUFFCORE) << "Signature verification not possible"; return false; } } else { qCDebug(KNEWSTUFFCORE) << "Verify signature..."; } } */ QString targetPath = targetInstallationPath(); QStringList installedFiles = installDownloadedFileAndUncompress(entry, downloadedFile, targetPath); if (uncompression != QLatin1String("kpackage")) { if (installedFiles.isEmpty()) { if (entry.status() == KNS3::Entry::Installing) { entry.setStatus(KNS3::Entry::Downloadable); } else if (entry.status() == KNS3::Entry::Updating) { entry.setStatus(KNS3::Entry::Updateable); } emit signalEntryChanged(entry); emit signalInstallationFailed(i18n("Could not install \"%1\": file not found.", entry.name())); return; } entry.setInstalledFiles(installedFiles); auto installationFinished = [this, entry]() { EntryInternal newentry = entry; // update version and release date to the new ones if (newentry.status() == KNS3::Entry::Updating) { if (!newentry.updateVersion().isEmpty()) { newentry.setVersion(newentry.updateVersion()); } if (newentry.updateReleaseDate().isValid()) { newentry.setReleaseDate(newentry.updateReleaseDate()); } } newentry.setStatus(KNS3::Entry::Installed); emit signalEntryChanged(newentry); emit signalInstallationFinished(); }; if (!postInstallationCommand.isEmpty()) { QProcess* p = runPostInstallationCommand(installedFiles.size() == 1 ? installedFiles.first() : targetPath); connect(p, qOverload(&QProcess::finished), this, [entry, installationFinished, this] (int exitCode, QProcess::ExitStatus) { if (exitCode) { EntryInternal newEntry = entry; newEntry.setStatus(KNS3::Entry::Invalid); emit signalEntryChanged(newEntry); emit signalInstallationFailed(i18n("Failed to execute install script")); } else { installationFinished(); } }); } else { installationFinished(); } } } QString Installation::targetInstallationPath() const { // installdir is the target directory QString installdir; // installpath also contains the file name if it's a single file, otherwise equal to installdir int pathcounter = 0; #if 0 // not available in KF5 if (!standardResourceDirectory.isEmpty()) { if (scope == ScopeUser) { installdir = KStandardDirs::locateLocal(standardResourceDirectory.toUtf8(), "/"); } else { // system scope installdir = KStandardDirs::installPath(standardResourceDirectory.toUtf8()); } pathcounter++; } #endif /* this is a partial reimplementation of the above, it won't ensure a perfect 1:1 porting, but will make many kde4 ksnsrc files work out of the box*/ //wallpaper is already managed in the case of !xdgTargetDirectory.isEmpty() if (!standardResourceDirectory.isEmpty() && standardResourceDirectory != QLatin1String("wallpaper")) { QStandardPaths::StandardLocation location = QStandardPaths::TempLocation; //crude translation KStandardDirs names -> QStandardPaths enum if (standardResourceDirectory == QLatin1String("tmp")) { location = QStandardPaths::TempLocation; } else if (standardResourceDirectory == QLatin1String("config")) { location = QStandardPaths::ConfigLocation; } if (scope == ScopeUser) { installdir = QStandardPaths::writableLocation(location); } else { // system scope installdir = QStandardPaths::standardLocations(location).constLast(); } pathcounter++; } if (!targetDirectory.isEmpty() && targetDirectory != QLatin1String("/")) { if (scope == ScopeUser) { installdir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + targetDirectory + QLatin1Char('/'); } else { // system scope installdir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, targetDirectory, QStandardPaths::LocateDirectory) + QLatin1Char('/'); } pathcounter++; } if (!xdgTargetDirectory.isEmpty() && xdgTargetDirectory != QLatin1String("/")) { installdir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + xdgTargetDirectory + QLatin1Char('/'); pathcounter++; } if (!installPath.isEmpty()) { #if defined(Q_OS_WIN) WCHAR wPath[MAX_PATH + 1]; if (SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, wPath) == S_OK) { installdir = QString::fromUtf16((const ushort *) wPath) + QLatin1Char('/') + installPath + QLatin1Char('/'); } else { installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/'); } #else installdir = QDir::homePath() + QLatin1Char('/') + installPath + QLatin1Char('/'); #endif pathcounter++; } if (!absoluteInstallPath.isEmpty()) { installdir = absoluteInstallPath + QLatin1Char('/'); pathcounter++; } if (pathcounter != 1) { qCCritical(KNEWSTUFFCORE) << "Wrong number of installation directories given."; return QString(); } qCDebug(KNEWSTUFFCORE) << "installdir: " << installdir; // create the dir if it doesn't exist (QStandardPaths doesn't create it, unlike KStandardDirs!) QDir().mkpath(installdir); return installdir; } QStringList Installation::installDownloadedFileAndUncompress(const KNSCore::EntryInternal &entry, const QString &payloadfile, const QString installdir) { // Collect all files that were installed QStringList installedFiles; bool isarchive = true; // respect the uncompress flag in the knsrc if (uncompression == QLatin1String("kpackage")) { qCDebug(KNEWSTUFFCORE) << "Using KPackage for installation"; KPackage::PackageStructure structure; KPackage::Package package(&structure); QString serviceType; package.setPath(payloadfile); auto resetEntryStatus = [this,entry](){ KNSCore::EntryInternal changedEntry(entry); if (changedEntry.status() == KNS3::Entry::Installing) { changedEntry.setStatus(KNS3::Entry::Downloadable); } else if (changedEntry.status() == KNS3::Entry::Updating) { changedEntry.setStatus(KNS3::Entry::Updateable); } emit signalEntryChanged(changedEntry); }; if (package.isValid() && package.metadata().isValid()) { qCDebug(KNEWSTUFFCORE) << "Package metadata is valid"; serviceType = package.metadata().value(QStringLiteral("X-Plasma-ServiceType")); if (serviceType.isEmpty() && !package.metadata().serviceTypes().isEmpty()) { serviceType = package.metadata().serviceTypes().first(); } if (serviceType.isEmpty()) { serviceType = property("kpackageType").toString(); } if (!serviceType.isEmpty()) { qCDebug(KNEWSTUFFCORE) << "Service type discovered as" << serviceType; KPackage::PackageStructure *structure = KPackage::PackageLoader::self()->loadPackageStructure(serviceType); if (structure) { KPackage::Package installer = KPackage::Package(structure); if (installer.hasValidStructure()) { QString packageRoot = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + installer.defaultPackageRoot(); qCDebug(KNEWSTUFFCORE) << "About to attempt to install" << package.metadata().pluginId() << "into" << packageRoot; const QString expectedDir{packageRoot + package.metadata().pluginId()}; KJob *installJob = KPackageJob::update(payloadfile, packageRoot, serviceType); // TODO KF6 Really, i would prefer to make more functions to handle this, but as this is // an exported class, i'd rather not pollute the public namespace with internal functions, // and we don't have a pimpl, so... we'll just have to deal with it for now connect(installJob, &KJob::result, this, [this,entry,payloadfile,expectedDir,resetEntryStatus](KJob* job){ if (job->error() == KJob::NoError) { if (QFile::exists(expectedDir)) { EntryInternal newentry = entry; newentry.setInstalledFiles(QStringList{expectedDir}); // update version and release date to the new ones if (newentry.status() == KNS3::Entry::Updating) { if (!newentry.updateVersion().isEmpty()) { newentry.setVersion(newentry.updateVersion()); } if (newentry.updateReleaseDate().isValid()) { newentry.setReleaseDate(newentry.updateReleaseDate()); } } newentry.setStatus(KNS3::Entry::Installed); emit signalEntryChanged(newentry); emit signalInstallationFinished(); qCDebug(KNEWSTUFFCORE) << "Install job finished with no error and we now have files" << expectedDir; } else { emit signalInstallationFailed(i18n("The installation of %1 failed to create the expected new directory %2").arg(payloadfile).arg(expectedDir)); resetEntryStatus(); qCDebug(KNEWSTUFFCORE) << "Install job finished with no error, but we do not have the expected new directory" << expectedDir; } } else { if (job->error() == KPackage::Package::JobError::NewerVersionAlreadyInstalledError) { EntryInternal newentry = entry; newentry.setStatus(KNS3::Entry::Installed); emit signalEntryChanged(newentry); emit signalInstallationFinished(); newentry.setInstalledFiles(QStringList{expectedDir}); qCDebug(KNEWSTUFFCORE) << "Install job finished telling us this item was already installed with this version, so... let's just make a small fib and say we totally installed that, honest, and we now have files" << expectedDir; } else { emit signalInstallationFailed(i18n("Installation of %1 failed: %2", payloadfile, job->errorText())); resetEntryStatus(); qCDebug(KNEWSTUFFCORE) << "Install job finished with error state" << job->error() << "and description" << job->error(); } } }); installJob->start(); } else { emit signalInstallationFailed(i18n("The installation of %1 failed, as the service type %2 was not accepted by the system (did you forget to install the KPackage support plugin for this type of package?)", payloadfile, serviceType)); resetEntryStatus(); qCWarning(KNEWSTUFFCORE) << "Package serviceType" << serviceType << "not found"; } } else { // no package structure emit signalInstallationFailed(i18n("The installation of %1 failed, as the downloaded package does not contain a correct KPackage structure.", payloadfile)); resetEntryStatus(); qCWarning(KNEWSTUFFCORE) << "Could not load the package structure for KPackage service type" << serviceType; } } else { // no service type emit signalInstallationFailed(i18n("The installation of %1 failed, as the downloaded package does not list a service type.", payloadfile)); resetEntryStatus(); qCWarning(KNEWSTUFFCORE) << "No service type listed in" << payloadfile; } } else { // package or package metadata is invalid emit signalInstallationFailed(i18n("The installation of %1 failed, as the downloaded package does not contain any useful meta information, which means it is not a valid KPackage.", payloadfile)); resetEntryStatus(); qCWarning(KNEWSTUFFCORE) << "No valid meta information (which suggests no valid KPackage) found in" << payloadfile; } } else { if (uncompression == QLatin1String("always") || uncompression == QLatin1String("archive") || uncompression == QLatin1String("subdir")) { // this is weird but a decompression is not a single name, so take the path instead QMimeDatabase db; QMimeType mimeType = db.mimeTypeForFile(payloadfile); qCDebug(KNEWSTUFFCORE) << "Postinstallation: uncompress the file"; // FIXME: check for overwriting, malicious archive entries (../foo) etc. // FIXME: KArchive should provide "safe mode" for this! QScopedPointer archive; if (mimeType.inherits(QStringLiteral("application/zip"))) { archive.reset(new KZip(payloadfile)); } else if (mimeType.inherits(QStringLiteral("application/tar")) || mimeType.inherits(QStringLiteral("application/x-gzip")) || mimeType.inherits(QStringLiteral("application/x-bzip")) || mimeType.inherits(QStringLiteral("application/x-lzma")) || mimeType.inherits(QStringLiteral("application/x-xz")) || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || mimeType.inherits(QStringLiteral("application/x-compressed-tar"))) { archive.reset(new KTar(payloadfile)); } else { qCCritical(KNEWSTUFFCORE) << "Could not determine type of archive file '" << payloadfile << "'"; if (uncompression == QLatin1String("always")) { emit signalInstallationError(i18n("Could not determine the type of archive of the downloaded file %1", payloadfile)); return QStringList(); } isarchive = false; } if (isarchive) { bool success = archive->open(QIODevice::ReadOnly); if (!success) { qCCritical(KNEWSTUFFCORE) << "Cannot open archive file '" << payloadfile << "'"; if (uncompression == QLatin1String("always")) { emit signalInstallationError(i18n("Failed to open the archive file %1. The reported error was: %2", payloadfile, archive->errorString())); return QStringList(); } // otherwise, just copy the file isarchive = false; } if (isarchive) { const KArchiveDirectory *dir = archive->directory(); //if there is more than an item in the file, and we are requested to do so //put contents in a subdirectory with the same name as the file QString installpath; if (uncompression == QLatin1String("subdir") && dir->entries().count() > 1) { installpath = installdir + QLatin1Char('/') + QFileInfo(archive->fileName()).baseName(); } else { installpath = installdir; } if (dir->copyTo(installpath)) { installedFiles << archiveEntries(installpath, dir); installedFiles << installpath + QLatin1Char('/'); } else qCWarning(KNEWSTUFFCORE) << "could not install" << entry.name() << "to" << installpath; archive->close(); QFile::remove(payloadfile); } } } qCDebug(KNEWSTUFFCORE) << "isarchive: " << isarchive; //some wallpapers are compressed, some aren't if ((!isarchive && standardResourceDirectory == QLatin1String("wallpaper")) || (uncompression == QLatin1String("never") || (uncompression == QLatin1String("archive") && !isarchive))) { // no decompress but move to target /// @todo when using KIO::get the http header can be accessed and it contains a real file name. // FIXME: make naming convention configurable through *.knsrc? e.g. for kde-look.org image names QUrl source = QUrl(entry.payload()); qCDebug(KNEWSTUFFCORE) << "installing non-archive from " << source.url(); QString installfile; QString ext = source.fileName().section(QLatin1Char('.'), -1); if (customName) { installfile = entry.name(); installfile += QLatin1Char('-') + entry.version(); if (!ext.isEmpty()) { installfile += QLatin1Char('.') + ext; } } else { // TODO HACK This is a hack, the correct way of fixing it would be doing the KIO::get // and using the http headers if they exist to get the file name, but as discussed in // Randa this is not going to happen anytime soon (if ever) so go with the hack if (source.url().startsWith(QLatin1String("http://newstuff.kde.org/cgi-bin/hotstuff-access?file="))) { installfile = QUrlQuery(source).queryItemValue(QStringLiteral("file")); int lastSlash = installfile.lastIndexOf(QLatin1Char('/')); if (lastSlash >= 0) { installfile = installfile.mid(lastSlash); } } if (installfile.isEmpty()) { installfile = source.fileName(); } } QString installpath = QDir(installdir).filePath(installfile); qCDebug(KNEWSTUFFCORE) << "Install to file " << installpath; // FIXME: copy goes here (including overwrite checking) // FIXME: what must be done now is to update the cache *again* // in order to set the new payload filename (on root tag only) // - this might or might not need to take uncompression into account // FIXME: for updates, we might need to force an overwrite (that is, deleting before) QFile file(payloadfile); bool success = true; const bool update = ((entry.status() == KNS3::Entry::Updateable) || (entry.status() == KNS3::Entry::Updating)); if (QFile::exists(installpath) && QDir::tempPath() != installdir) { if (!update) { Question question(Question::YesNoQuestion); question.setQuestion(i18n("This file already exists on disk (possibly due to an earlier failed download attempt). Continuing means overwriting it. Do you wish to overwrite the existing file?") + QStringLiteral("\n'") + installpath + QLatin1Char('\'')); question.setTitle(i18n("Overwrite File")); if(question.ask() != Question::YesResponse) { return QStringList(); } } success = QFile::remove(installpath); } if (success) { //remove in case it's already present and in a temporary directory, so we get to actually use the path again if (installpath.startsWith(QDir::tempPath())) { file.remove(installpath); } success = file.rename(installpath); qCDebug(KNEWSTUFFCORE) << "move: " << file.fileName() << " to " << installpath; } if (!success) { emit signalInstallationError(i18n("Unable to move the file %1 to the intended destination %2", payloadfile, installpath)); qCCritical(KNEWSTUFFCORE) << "Cannot move file '" << payloadfile << "' to destination '" << installpath << "'"; return QStringList(); } installedFiles << installpath; } } return installedFiles; } QProcess* Installation::runPostInstallationCommand(const QString &installPath) { QString command(postInstallationCommand); QString fileArg(KShell::quoteArg(installPath)); command.replace(QLatin1String("%f"), fileArg); qCDebug(KNEWSTUFFCORE) << "Run command: " << command; QProcess* ret = new QProcess(this); connect(ret, static_cast(&QProcess::finished), this, [this, command, ret](int exitcode, QProcess::ExitStatus status) { const QString output{QString::fromLocal8Bit(ret->readAllStandardError())}; if (status == QProcess::CrashExit) { emit signalInstallationError(i18n("The installation failed while attempting to run the command:\n%1\n\nThe returned output was:\n%2", command, output)); qCCritical(KNEWSTUFFCORE) << "Process crashed with command: " << command; } else if (exitcode) { emit signalInstallationError(i18n("The installation failed with code %1 while attempting to run the command:\n%2\n\nThe returned output was:\n%3", exitcode, command, output)); qCCritical(KNEWSTUFFCORE) << "Command '" << command << "' failed with code" << exitcode; } sender()->deleteLater(); }); QStringList args = KShell::splitArgs(command); ret->setProgram(args.takeFirst()); ret->setArguments(args); ret->start(); return ret; } void Installation::uninstall(EntryInternal entry) { if (uncompression == QLatin1String("kpackage")) { const auto lst = entry.installedFiles(); if (lst.length() == 1) { const QString installedFile{lst.first()}; if (QFileInfo(installedFile).isDir()) { KPackage::PackageStructure structure; KPackage::Package package(&structure); package.setPath(installedFile); if (package.isValid() && package.metadata().isValid()) { QString serviceType = package.metadata().value(QStringLiteral("X-Plasma-ServiceType")); if (serviceType.isEmpty() && !package.metadata().serviceTypes().isEmpty()) { serviceType = package.metadata().serviceTypes().first(); } if (serviceType.isEmpty()) { serviceType = property("kpackageType").toString(); } if (!serviceType.isEmpty()) { KPackage::PackageStructure *structure = KPackage::PackageLoader::self()->loadPackageStructure(serviceType); if (structure) { KPackage::Package installer = KPackage::Package(structure); if (!installer.hasValidStructure()) { qWarning() << "Package serviceType" << serviceType << "not found"; } QString packageRoot = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + installer.defaultPackageRoot(); KJob *removalJob = KPackageJob::uninstall(package.metadata().pluginId(), packageRoot, serviceType); connect(removalJob, &KJob::result, this, [this,installedFile,installer,entry](KJob* job){ EntryInternal newEntry = entry; if (job->error() == KJob::NoError) { newEntry.setStatus(KNS3::Entry::Deleted); newEntry.setUnInstalledFiles(newEntry.installedFiles()); newEntry.setInstalledFiles(QStringList()); emit signalEntryChanged(newEntry); } else { emit signalInstallationFailed(i18n("Installation of %1 failed: %2", installedFile, job->errorText())); } }); removalJob->start(); } else { // no package structure emit signalInstallationFailed(i18n("The removal of %1 failed, as the installed package does not contain a correct KPackage structure.", installedFile)); } } else { // no service type emit signalInstallationFailed(i18n("The removal of %1 failed, as the installed package is not a supported type (did you forget to install the KPackage support plugin for this type of package?)", installedFile)); } } else { // package or package metadata is invalid emit signalInstallationFailed(i18n("The removal of %1 failed, as the installed package does not contain any useful meta information, which means it is not a valid KPackage.", entry.name())); } } else { QMimeDatabase db; QMimeType mimeType = db.mimeTypeForFile(installedFile); if (mimeType.inherits(QStringLiteral("application/zip")) || mimeType.inherits(QStringLiteral("application/x-compressed-tar")) || mimeType.inherits(QStringLiteral("application/x-gzip")) || mimeType.inherits(QStringLiteral("application/x-tar")) || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || mimeType.inherits(QStringLiteral("application/x-xz")) || mimeType.inherits(QStringLiteral("application/x-lzma"))) { // Then it's one of the downloaded files installed with an old version of knewstuff prior to // the native kpackage support being added, and we need to do some inspection-and-removal work... KPackage::PackageStructure structure; KPackage::Package package(&structure); const QString serviceType{property("kpackageType").toString()}; package.setPath(installedFile); if (package.isValid() && package.metadata().isValid()) { // try and load the kpackage and sniff the expected location of its installation, and ask KPackage to remove that thing, if it's there KPackage::PackageStructure *structure = KPackage::PackageLoader::self()->loadPackageStructure(serviceType); if (structure) { KPackage::Package installer = KPackage::Package(structure); if (installer.hasValidStructure()) { QString packageRoot = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + installer.defaultPackageRoot(); qCDebug(KNEWSTUFFCORE) << "About to attempt to uninstall" << package.metadata().pluginId() << "from" << packageRoot; const QString supposedInstallationDir{packageRoot + package.metadata().pluginId()}; // Frankly, we don't care whether or not this next step succeeds, and it can just fizzle if it wants // to. This is a cleanup step, and if it fails, it's just not really important. KJob *removalJob = KPackageJob::uninstall(package.metadata().pluginId(), packageRoot, serviceType); removalJob->start(); } } } // Also get rid of the downloaded file, and tell everything they've gone if (QFile::remove(installedFile)) { entry.setStatus(KNS3::Entry::Deleted); entry.setUnInstalledFiles(entry.installedFiles()); entry.setInstalledFiles(QStringList()); emit signalEntryChanged(entry); } else { emit signalInstallationFailed(i18n("The removal of %1 failed, as the downloaded file %2 could not be automatically removed.", entry.name(), installedFile)); } } else { // Not sure what's installed, but it's not a KPackage, not a lot we can do with this... emit signalInstallationFailed(i18n("The removal of %1 failed, due to the installed file not being a KPackage. The file in question was %2, and you can attempt to delete it yourself, if you are certain that it is not needed.", entry.name(), installedFile)); } } } else { emit signalInstallationFailed(i18n("The removal of %1 failed, as there seems to somehow be more than one thing installed, which is not supposed to be possible for KPackage based entries.", entry.name())); } } else { - entry.setStatus(KNS3::Entry::Deleted); + // If all the files are removed assume that the entry got manually removed + // otherwise there would be no way to delete the entry that editing the register + bool manuallyUninstalled = true; + const auto lst = entry.installedFiles(); + for (const auto &file : lst) { + if (QFile::exists(file)) { + manuallyUninstalled = false; + break; + } + } + if (manuallyUninstalled) { + entry.setStatus(KNS3::Entry::Deleted); + emit signalEntryChanged(entry); + return; + } + // If there is an uninstall script, make sure it runs without errors if (!uninstallCommand.isEmpty()) { const auto lst = entry.installedFiles(); for (const QString &file : lst) { QFileInfo info(file); if (info.isFile()) { QString fileArg(KShell::quoteArg(file)); QString command(uninstallCommand); command.replace(QLatin1String("%f"), fileArg); - QStringList args = KShell::splitArgs(command); - const QString program = args.takeFirst(); - int exitcode = QProcess::execute(program, args); - - if (exitcode) { - emit signalInstallationError(i18n("The uninstallation process failed to successfully run the command %1", command)); - qCCritical(KNEWSTUFFCORE) << "Command failed" << command; - } else { + QStringList args = KShell::splitArgs(command); + const QString program = args.takeFirst(); + QProcess process; + process.start(program, args); + process.waitForFinished(-1); + + if (process.exitCode()) { + const QString processOutput = QString::fromLocal8Bit(process.readAllStandardError()); + const QString err = i18n("The uninstallation process failed to successfully run the command %1\n" + "The output of was: \n%2\n" + "If you think this is incorrect, you can continue or cancel the uninstallation process", + KShell::quoteArg(command), processOutput); + emit signalInstallationError(err); + // Ask the user if he wants to continue, even though the script failed + Question question(Question::ContinueCancelQuestion); + question.setQuestion(err); + Question::Response response = question.ask(); + if (response == Question::CancelResponse) { + // Use can delete files manually + entry.setStatus(KNS3::Entry::Installed); + emit signalEntryChanged(entry); + return; + } + } else { qCDebug(KNEWSTUFFCORE) << "Command executed successfully: " << command; } } } } - const auto lst = entry.installedFiles(); for (const QString &file : lst) { if (file.endsWith(QLatin1Char('/'))) { QDir dir; bool worked = dir.rmdir(file); if (!worked) { // Maybe directory contains user created files, ignore it continue; } } else if (file.endsWith(QLatin1String("/*"))) { QDir dir(file.left(file.size()-2)); bool worked = dir.removeRecursively(); if (!worked) { qCWarning(KNEWSTUFFCORE) << "Couldn't remove" << dir.path(); continue; } } else { QFileInfo info(file); if (info.exists() || info.isSymLink()) { bool worked = QFile::remove(file); if (!worked) { qWarning() << "unable to delete file " << file; return; } } else { qWarning() << "unable to delete file " << file << ". file does not exist."; } } } entry.setUnInstalledFiles(entry.installedFiles()); entry.setInstalledFiles(QStringList()); + entry.setStatus(KNS3::Entry::Deleted); emit signalEntryChanged(entry); } } Installation::UncompressionOptions Installation::uncompressionSetting() const { if (uncompression == QLatin1String("always")) { return AlwaysUncompress; } else if (uncompression == QLatin1String("archive")) { return UncompressIfArchive; } else if (uncompression == QLatin1String("subdir")) { return UncompressIntoSubdir; } else if (uncompression == QLatin1String("kpackage")) { return UseKPackageUncompression; } return NeverUncompress; } void Installation::slotInstallationVerification(int result) { Q_UNUSED(result) // Deprecated, was wired up to defunct Security class. } QStringList Installation::archiveEntries(const QString &path, const KArchiveDirectory *dir) { QStringList files; const auto lst = dir->entries(); for (const QString &entry : lst) { const auto currentEntry = dir->entry(entry); const QString childPath = path + QLatin1Char('/') + entry; if (currentEntry->isFile()) { files << childPath; } else if (currentEntry->isDirectory()) { files << childPath + QStringLiteral("/*"); } } return files; } diff --git a/src/core/installation.h b/src/core/installation.h index b725fcc4..2f8f3929 100644 --- a/src/core/installation.h +++ b/src/core/installation.h @@ -1,214 +1,214 @@ /* This file is part of KNewStuff2. Copyright (c) 2007 Josef Spillner Copyright (C) 2009 Frederik Gladhorn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #ifndef KNEWSTUFF3_INSTALLATION_P_H #define KNEWSTUFF3_INSTALLATION_P_H #include #include #include #include "entryinternal.h" #include "knewstuffcore_export.h" class QProcess; class KArchiveDirectory; class KJob; namespace KNSCore { /** * @short KNewStuff entry installation. * * The installation class stores all information related to an entry's * installation. * * @author Josef Spillner (spillner@kde.org) * * @internal */ class KNEWSTUFFCORE_EXPORT Installation : public QObject { Q_OBJECT public: /** * Constructor. */ explicit Installation(QObject *parent = nullptr); enum Policy { CheckNever, CheckIfPossible, CheckAlways }; enum Scope { ScopeUser, ScopeSystem }; enum UncompressionOptions { NeverUncompress, ///@< Never attempt to decompress a file, whatever format it is. Matches "never" knsrc setting AlwaysUncompress, ///@< Assume all downloaded files are archives, and attempt to decompress them. Will cause failure if decompression fails. Matches "always" knsrc setting UncompressIfArchive, ///@< If the file is an archive, decompress it, otherwise just pass it on. Matches "archive" knsrc setting UncompressIntoSubdir, ///@< As Archive, except that if there is more than an item in the file, put contents in a subdirectory with the same name as the file. Matches "subdir" knsrc setting UseKPackageUncompression ///@< Use the internal KPackage support for installing and uninstalling the package. Matches "kpackage" knsrc setting }; bool readConfig(const KConfigGroup &group); #if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(5, 71) KNEWSTUFFCORE_DEPRECATED_VERSION(5, 71, "No longer use, feature obsolete") bool isRemote() const; #endif public Q_SLOTS: /** * Downloads a payload file. The payload file matching most closely * the current user language preferences will be downloaded. * The file will not be installed set, for this \ref install must * be called. * * @param entry Entry to download payload file for * * @see signalPayloadLoaded * @see signalPayloadFailed */ void downloadPayload(const KNSCore::EntryInternal &entry); /** * Installs an entry's payload file. This includes verification, if * necessary, as well as decompression and other steps according to the * application's *.knsrc file. * Note that this method is asynchronous and thus the return value will * only report the successful start of the installation. * Note also that while entry is const at this point, it will change later * during the actual installation (the installedFiles list will change, as * will its status) * * @param entry Entry to be installed * * @see signalInstallationFinished * @see signalInstallationFailed */ void install(const KNSCore::EntryInternal &entry); /** * Uninstalls an entry. It reverses the steps which were performed * during the installation. * - * The entry instance will be updated with any new information: + * The entry emitted by signalEntryChanged will be updated with any new information, in particular the following: *
    - *
  • Status will be set to Deleted + *
  • Status will be set to Deleted, unless the uninstall + * script exists with an error and the user chooses to cancel the uninstallation *
  • uninstalledFiles will list files which were removed during uninstallation *
  • installedFiles will become empty *
* * @param entry The entry to deinstall * - * @note FIXME: I don't believe this works yet :) */ void uninstall(KNSCore::EntryInternal entry); /** * Returns the uncompression setting, in a computer-readable format * * @return The value of this setting * @since 5.71 */ UncompressionOptions uncompressionSetting() const; // TODO KF6: remove, was used with deprecated Security class. #if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(5, 31) KNEWSTUFFCORE_DEPRECATED_VERSION(5, 31, "No longer use") void slotInstallationVerification(int result); #endif void slotPayloadResult(KJob *job); /** * @returns the installation path * * @since 5.31 */ QString targetInstallationPath() const; Q_SIGNALS: void signalEntryChanged(const KNSCore::EntryInternal &entry); void signalInstallationFinished(); void signalInstallationFailed(const QString &message); /** * An informational signal fired when a serious error occurs during the installation. * @param message The description of the error (a message intended to be human readable) * @since 5.69 */ void signalInstallationError(const QString &message); void signalPayloadLoaded(QUrl payload); // FIXME: return Entry // TODO KF6: remove, was used with deprecated Security class. #if KNEWSTUFFCORE_ENABLE_DEPRECATED_SINCE(5, 31) KNEWSTUFFCORE_DEPRECATED_VERSION(5, 31, "No longer use") void signalInformation(const QString &) const; KNEWSTUFFCORE_DEPRECATED_VERSION(5, 31, "No longer use") void signalError(const QString &) const; #endif private: void install(KNSCore::EntryInternal entry, const QString &downloadedFile); QStringList installDownloadedFileAndUncompress(const KNSCore::EntryInternal &entry, const QString &payloadfile, const QString installdir); QProcess* runPostInstallationCommand(const QString &installPath); static QStringList archiveEntries(const QString &path, const KArchiveDirectory *dir); // applications can set this if they want the installed files/directories to be piped into a shell command QString postInstallationCommand; // a custom command to run for the uninstall QString uninstallCommand; // compression policy QString uncompression; // only one of the five below can be set, that will be the target install path/file name // FIXME: check this when reading the config and make one path out of it if possible? QString standardResourceDirectory; QString targetDirectory; QString xdgTargetDirectory; QString installPath; QString absoluteInstallPath; // policies whether verification needs to be done Policy checksumPolicy; Policy signaturePolicy; // scope: install into user or system dirs Scope scope; // FIXME this throws together a file name from entry name and version - why would anyone want that? bool customName; bool acceptHtml; QMap entry_jobs; Q_DISABLE_COPY(Installation) }; } #endif