diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 3e39870e..76061ed7 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -1,606 +1,661 @@ /* 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 "installation.h" #include "xmlloader.h" #include "imageloader_p.h" #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; class EnginePrivate { public: QList categoriesMetadata; }; // BCI: Add a real d-pointer typedef QHash EnginePrivateHash; Q_GLOBAL_STATIC(EnginePrivateHash, d_func) static EnginePrivate *d(const Engine* engine) { EnginePrivate* ret = d_func()->value(engine); if (!ret) { ret = new EnginePrivate; d_func()->insert(engine, ret); } return ret; } static void delete_d(const Engine* engine) { EnginePrivate* ret = d_func()->value(engine); delete ret; d_func()->remove(engine); } Engine::Engine(QObject *parent) : QObject(parent) , m_installation(new Installation) , m_cache(nullptr) , m_searchTimer(new QTimer) , m_atticaProviderManager(nullptr) , 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); } Engine::~Engine() { if (m_cache) { m_cache->writeRegistry(); } delete m_atticaProviderManager; delete m_searchTimer; delete m_installation; delete_d(this); } bool Engine::init(const QString &configfile) { qCDebug(KNEWSTUFFCORE) << "Initializing KNSCore::Engine from '" << configfile << "'"; emit signalBusy(i18n("Initializing")); KConfig conf(configfile); if (conf.accessMode() == KConfig::NoAccess) { emit signalError(i18n("Configuration file not found: \"%1\"", configfile)); qCritical() << "No knsrc file named '" << configfile << "' was found." << endl; 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 signalError(i18n("Configuration file is invalid: \"%1\"", configfile)); qCritical() << "A knsrc file was found but it doesn't contain a KNewStuff3 section." << endl; return false; } m_categories = group.readEntry("Categories", QStringList()); + m_adoptionCommand = group.readEntry("AdoptionCommand", QString()); qCDebug(KNEWSTUFFCORE) << "Categories: " << m_categories; m_providerFileUrl = group.readEntry("ProvidersUrl", QString()); const QString configFileName = QFileInfo(QDir::isAbsolutePath(configfile) ? configfile : QStandardPaths::locate(QStandardPaths::GenericConfigLocation, configfile)).baseName(); // let installation read install specific config if (!m_installation->readConfig(group)) { return false; } connect(m_installation, &Installation::signalEntryChanged, this, &Engine::slotEntryChanged); m_cache = Cache::getCache(configFileName); connect(this, &Engine::signalEntryChanged, m_cache.data(), &Cache::registerChangedEntry); m_cache->readRegistry(); m_initialized = true; // load the providers loadProviders(); return true; } QStringList Engine::categories() const { return m_categories; } QStringList Engine::categoriesFilter() const { return m_currentRequest.categories; } QList Engine::categoriesMetadata() { return d(this)->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 m_atticaProviderManager; m_atticaProviderManager = new Attica::ProviderManager; connect(m_atticaProviderManager, &Attica::ProviderManager::providerAdded, this, &Engine::atticaProviderLoaded); m_atticaProviderManager->loadDefaultProviders(); } else { qCDebug(KNEWSTUFFCORE) << "loading providers from " << m_providerFileUrl; emit signalBusy(i18n("Loading provider information")); XmlLoader *loader = new XmlLoader(this); connect(loader, &XmlLoader::signalLoaded, this, &Engine::slotProviderFileLoaded); connect(loader, &XmlLoader::signalFailed, this, &Engine::slotProvidersFailed); loader->load(QUrl(m_providerFileUrl)); } } 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 signalError(i18n("Could not load get hot new stuff providers from file: %1", 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 = QSharedPointer (new AtticaProvider(m_categories)); connect(provider.data(), &Provider::categoriesMetadataLoded, this, [this](const QList &categories){ d(this)->categoriesMetadata = categories; emit signalCategoriesMetadataLoded(categories); }); } else { provider = QSharedPointer (new StaticXmlProvider); } if (provider->setProviderXML(n)) { addProvider(provider); } else { emit signalError(i18n("Error initializing provider.")); } 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)); connect(provider.data(), &Provider::categoriesMetadataLoded, this, [this](const QList &categories){ d(this)->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); 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::signalInformation, this, &Engine::signalIdle); } void Engine::providerJobStarted(KJob *job) { emit jobStarted(job, i18n("Loading data from provider")); } void Engine::slotProvidersFailed() { emit signalError(i18n("Loading of providers from file: %1 failed", m_providerFileUrl)); } void Engine::providerInitialized(Provider *p) { qCDebug(KNEWSTUFFCORE) << "providerInitialized" << p->name(); p->setCachedEntries(m_cache->registryForProvider(p->id())); updateStatus(); foreach (const QSharedPointer &p, 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; 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.page = 0; m_numDataJobs = 0; foreach (const QSharedPointer &p, 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 = m_cache->requestFromCache(m_currentRequest); while (!cache.isEmpty()) { qCDebug(KNEWSTUFFCORE) << "From cache"; emit signalEntriesLoaded(cache); m_currentPage = m_currentRequest.page; ++m_currentRequest.page; cache = 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 the cache was empty, request data from provider if (m_currentPage == -1) { 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(); } void KNSCore::Engine::setFilter(Provider::Filter filter) { if (m_currentRequest.filter != filter) { m_currentRequest.page = -1; } m_currentRequest.filter = filter; reloadEntries(); } void KNSCore::Engine::fetchEntryById(const QString& id) { m_searchTimer->stop(); m_currentRequest = KNSCore::Provider::SearchRequest(KNSCore::Provider::Newest, KNSCore::Provider::ExactEntryId, id); 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(); } } 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() { foreach (const QSharedPointer &p, 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) { p->loadPayloadLink(entry, linkId); ++m_numInstallJobs; updateStatus(); } } void Engine::slotInstallationFinished() { --m_numInstallJobs; updateStatus(); } void Engine::slotInstallationFailed(const QString &message) { --m_numInstallJobs; emit signalError(message); } void Engine::slotEntryDetailsLoaded(const KNSCore::EntryInternal &entry) { emit signalEntryDetailsLoaded(entry); } void Engine::downloadLinkLoaded(const KNSCore::EntryInternal &entry) { m_installation->install(entry); } void Engine::uninstall(KNSCore::EntryInternal entry) { 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; foreach (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() 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); 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() { foreach (QSharedPointer p, m_providers) { Provider::SearchRequest request(KNSCore::Provider::Newest, KNSCore::Provider::Updates); p->loadEntries(request); } } void KNSCore::Engine::checkForInstalled() { foreach (QSharedPointer p, m_providers) { Provider::SearchRequest request(KNSCore::Provider::Newest, KNSCore::Provider::Installed); request.page = 0; 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, const QString &rootPath) +{ + while(!dirs.isEmpty()) { + const QString currentPath = QDir::cleanPath(dirs.takeLast()); + if (!currentPath.startsWith(rootPath)) + continue; + + const QFileInfo current(currentPath); + if (!current.isDir()) + continue; + + const QDir dir = current.dir(); + if (dir.path()==(rootPath+dir.dirName())) { + return dir; + } + } + 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"); + QStringList ret; + 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(); +} diff --git a/src/core/engine.h b/src/core/engine.h index 5dcbe670..08f2c425 100644 --- a/src/core/engine.h +++ b/src/core/engine.h @@ -1,244 +1,246 @@ /* knewstuff3/engine.h. Copyright (c) 2007 Josef Spillner Copyright (C) 2007-2010 Frederik Gladhorn Copyright (c) 2009 Jeremy Whiting 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_ENGINE_P_H #define KNEWSTUFF3_ENGINE_P_H #include #include #include #include #include "provider.h" #include "entryinternal.h" #include "knewstuffcore_export.h" class QTimer; class KJob; namespace Attica { class ProviderManager; class Provider; } namespace KNSCore { class Cache; class Installation; /** * KNewStuff engine. * An engine keeps track of data which is available locally and remote * and offers high-level synchronization calls as well as upload and download * primitives using an underlying GHNS protocol. * * @internal */ class KNEWSTUFFCORE_EXPORT Engine : public QObject { Q_OBJECT public: /** * Constructor. */ explicit Engine(QObject *parent = nullptr); /** * Destructor. Frees up all the memory again which might be taken * by cached entries and providers. */ ~Engine(); /** * Initializes the engine. This step is application-specific and relies * on an external configuration file, which determines all the details * about the initialization. * * @param configfile KNewStuff2 configuration file (*.knsrc) * @return \b true if any valid configuration was found, \b false otherwise */ bool init(const QString &configfile); /** * 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. * * @param entry Entry to be installed * * @see signalInstallationFinished * @see signalInstallationFailed */ void install(KNSCore::EntryInternal entry, int linkId = 1); /** * Uninstalls an entry. It reverses the steps which were performed * during the installation. * * @param entry The entry to deinstall */ void uninstall(KNSCore::EntryInternal entry); void loadPreview(const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type); void loadDetails(const KNSCore::EntryInternal &entry); void setSortMode(Provider::SortMode mode); void setFilter(Provider::Filter filter); /** Set the categories that will be included in searches */ void setCategoriesFilter(const QStringList &categories); void setSearchTerm(const QString &searchString); void reloadEntries(); void requestMoreData(); void requestData(int page, int pageSize); void checkForUpdates(); void checkForInstalled(); void fetchEntryById(const QString &id); /** * Try to contact the author of the entry by email or showing their homepage. */ void contactAuthor(const EntryInternal &entry); bool userCanVote(const EntryInternal &entry); void vote(const EntryInternal &entry, uint rating); bool userCanBecomeFan(const EntryInternal &entry); void becomeFan(const EntryInternal &entry); QStringList categories() const; QStringList categoriesFilter() const; QList categoriesMetadata(); + QString adoptionCommand(const KNSCore::EntryInternal &entry) const; + bool hasAdoptionCommand() const; + Q_SIGNALS: /** * Indicates a message to be added to the ui's log, or sent to a messagebox */ void signalMessage(const QString &message); void signalProvidersLoaded(); void signalEntriesLoaded(const KNSCore::EntryInternal::List &entries); void signalUpdateableEntriesLoaded(const KNSCore::EntryInternal::List &entries); void signalEntryChanged(const KNSCore::EntryInternal &entry); void signalEntryDetailsLoaded(const KNSCore::EntryInternal &entry); // a new search result is there, clear the list of items void signalResetView(); void signalEntryPreviewLoaded(const KNSCore::EntryInternal &, KNSCore::EntryInternal::PreviewType); void signalPreviewFailed(); void signalEntryUploadFinished(); void signalEntryUploadFailed(); void signalDownloadDialogDone(KNSCore::EntryInternal::List); void jobStarted(KJob *, const QString &); void signalError(const QString &); void signalBusy(const QString &); void signalIdle(const QString &); void signalCategoriesMetadataLoded(const QList &categories); private Q_SLOTS: // the .knsrc file was loaded void slotProviderFileLoaded(const QDomDocument &doc); // instead of getting providers from knsrc, use what was configured in ocs systemsettings void atticaProviderLoaded(const Attica::Provider &provider); // loading the .knsrc file failed void slotProvidersFailed(); // called when a provider is ready to work void providerInitialized(KNSCore::Provider *); void slotEntriesLoaded(const KNSCore::Provider::SearchRequest &, KNSCore::EntryInternal::List); void slotEntryDetailsLoaded(const KNSCore::EntryInternal &entry); void slotPreviewLoaded(const KNSCore::EntryInternal &entry, KNSCore::EntryInternal::PreviewType type); void slotSearchTimerExpired(); void slotEntryChanged(const KNSCore::EntryInternal &entry); void slotInstallationFinished(); void slotInstallationFailed(const QString &message); void downloadLinkLoaded(const KNSCore::EntryInternal &entry); void providerJobStarted(KJob *); private: /** * load providers from the providersurl in the knsrc file * creates providers based on their type and adds them to the list of providers */ void loadProviders(); /** Add a provider and connect it to the right slots */ void addProvider(QSharedPointer provider); void updateStatus(); void doRequest(); //FIXME KF6: move all of this in EnginePrivate // handle installation of entries Installation *m_installation; // read/write cache of entries QSharedPointer m_cache; QTimer *m_searchTimer; // The url of the file containing information about content providers QString m_providerFileUrl; // Categories from knsrc file QStringList m_categories; QHash > m_providers; - // TODO KF6: remove - QString m_unused; + QString m_adoptionCommand; // the current request from providers Provider::SearchRequest m_currentRequest; Attica::ProviderManager *m_atticaProviderManager; // the page that is currently displayed, so it is not requested repeatedly int m_currentPage; // when requesting entries from a provider, how many to ask for int m_pageSize; int m_numDataJobs; int m_numPictureJobs; int m_numInstallJobs; // If the provider is ready to be used bool m_initialized; Q_DISABLE_COPY(Engine) }; } #endif diff --git a/src/core/installation.cpp b/src/core/installation.cpp index 9ca5bce2..c4dff9bb 100644 --- a/src/core/installation.cpp +++ b/src/core/installation.cpp @@ -1,658 +1,658 @@ /* 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 "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) { } 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")) { qCritical() << "invalid Uncompress setting chosen, must be one of: subdir, always, archive, or never" << endl; return false; } uncompression = uncompresssetting; 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 == QStringLiteral("wallpapers")) { uncompression = QStringLiteral("subdir"); } 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()) { qCritical() << "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 { qCritical() << "The checksum policy '" + checksumpolicy + "' is unknown." << endl; 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 { qCritical() << "The signature policy '" + signaturepolicy + "' is unknown." << endl; 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 { qCritical() << "The scope '" + scopeString + "' is unknown." << endl; return false; } if (scope == ScopeSystem) { if (!installPath.isEmpty()) { qCritical() << "System installation cannot be mixed with InstallPath." << endl; return false; } } } return true; } bool Installation::isRemote() const { if (!installPath.isEmpty()) { return false; } if (!targetDirectory.isEmpty()) { return false; } if (!xdgTargetDirectory.isEmpty()) { return false; } if (!absoluteInstallPath.isEmpty()) { return false; } if (!standardResourceDirectory.isEmpty()) { return false; } return true; } 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()) { qCritical() << "The entry doesn't have a payload." << endl; emit signalInstallationFailed(i18n("Download of item failed: no download URL for \"%1\".", entry.name())); return; } // FIXME no clue what this is supposed to do if (isRemote()) { // Remote resource qCDebug(KNEWSTUFFCORE) << "Relaying remote payload '" << source << "'"; install(entry, source.toDisplayString(QUrl::PreferLocalFile)); emit signalPayloadLoaded(source); // FIXME: we still need registration for eventual deletion return; } QString fileName(source.fileName()); QTemporaryFile tempFile(QDir::tempPath() + "/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 { qCritical() << "Checksum verification not possible" << endl; 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 { qCritical() << "Signature verification not possible" << endl; return false; } } else { qCDebug(KNEWSTUFFCORE) << "Verify signature..."; } } */ QString targetPath = targetInstallationPath(); QStringList installedFiles = installDownloadedFileAndUncompress(entry, downloadedFile, targetPath); 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, static_cast(&QProcess::finished), this, installationFinished); } else { installationFinished(); } } -QString Installation::targetInstallationPath() +QString Installation::targetInstallationPath() const { QString installdir; if (!isRemote()) { // installdir is the target directory // 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).last(); } pathcounter++; } if (!targetDirectory.isEmpty() && targetDirectory != "/") { 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 != "/") { 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) { qCritical() << "Wrong number of installation directories given." << endl; 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) { QString installpath(payloadfile); // Collect all files that were installed QStringList installedFiles; if (!isRemote()) { bool isarchive = true; // respect the uncompress flag in the knsrc 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 installpath = installdir; 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 { qCritical() << "Could not determine type of archive file '" << payloadfile << "'"; if (uncompression == QLatin1String("always")) { return QStringList(); } isarchive = false; } if (isarchive) { bool success = archive->open(QIODevice::ReadOnly); if (!success) { qCritical() << "Cannot open archive file '" << payloadfile << "'"; if (uncompression == QLatin1String("always")) { 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 if (uncompression == QLatin1String("subdir") && dir->entries().count() > 1) { installpath = installdir + QLatin1Char('/') + QFileInfo(archive->fileName()).baseName(); } dir->copyTo(installpath); installedFiles << archiveEntries(installpath, dir); installedFiles << installpath + QLatin1Char('/'); 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('.', -1); if (customName) { installfile = entry.name(); installfile += '-' + entry.version(); if (!ext.isEmpty()) { installfile += '.' + 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('/'); if (lastSlash >= 0) { installfile = installfile.mid(lastSlash); } } if (installfile.isEmpty()) { installfile = source.fileName(); } } installpath = installdir + QLatin1Char('/') + 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::ContinueCancelQuestion); question.setQuestion(i18n("Overwrite existing file?") + "\n'" + installpath + '\''); question.setTitle(i18n("Download File")); if(question.ask() != Question::ContinueResponse) { 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) { qCritical() << "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](int exitcode){ if (exitcode) { qCritical() << "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) { entry.setStatus(KNS3::Entry::Deleted); if (!uninstallCommand.isEmpty()) { foreach (const QString &file, entry.installedFiles()) { QFileInfo info(file); if (info.isFile()) { QString fileArg(KShell::quoteArg(file)); QString command(uninstallCommand); command.replace(QLatin1String("%f"), fileArg); int exitcode = QProcess::execute(command); if (exitcode) { qCritical() << "Command failed" << command; } else { qCDebug(KNEWSTUFFCORE) << "Command executed successfully: " << command; } } } } foreach (const QString &file, entry.installedFiles()) { if (file.endsWith('/')) { QDir dir; bool worked = dir.rmdir(file); if (!worked) { // Maybe directory contains user created files, ignore it 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()); emit signalEntryChanged(entry); } 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; foreach (const QString &entry, dir->entries()) { QString childPath = path + QLatin1Char('/') + entry; if (dir->entry(entry)->isFile()) { files << childPath; } if (dir->entry(entry)->isDirectory()) { const KArchiveDirectory *childDir = static_cast(dir->entry(entry)); files << archiveEntries(childPath, childDir); files << childPath + QLatin1Char('/'); } } return files; } diff --git a/src/core/installation.h b/src/core/installation.h index b78c1a65..e19318fd 100644 --- a/src/core/installation.h +++ b/src/core/installation.h @@ -1,176 +1,182 @@ /* 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 }; bool readConfig(const KConfigGroup &group); bool isRemote() const; 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: *
    *
  • Status will be set to Deleted *
  • 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); // TODO KF6: remove, was used with deprecated Security class. Q_DECL_DEPRECATED void slotInstallationVerification(int result); 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); void signalPayloadLoaded(QUrl payload); // FIXME: return Entry // TODO KF6: remove, was used with deprecated Security class. Q_DECL_DEPRECATED void signalInformation(const QString &) const; Q_DECL_DEPRECATED void signalError(const QString &) const; private: void install(KNSCore::EntryInternal entry, const QString &downloadedFile); - QString targetInstallationPath(); 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 diff --git a/src/ui/itemsviewbasedelegate.cpp b/src/ui/itemsviewbasedelegate.cpp index c959b68c..a03f9204 100644 --- a/src/ui/itemsviewbasedelegate.cpp +++ b/src/ui/itemsviewbasedelegate.cpp @@ -1,115 +1,118 @@ /* Copyright (C) 2008 Jeremy Whiting Copyright (C) 2010 Reza Fatahilah Shah Copyright (C) 2010 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 "itemsviewbasedelegate_p.h" #include "core/itemsmodel.h" #include "entrydetailsdialog_p.h" #include #include #include namespace KNS3 { ItemsViewBaseDelegate::ItemsViewBaseDelegate(QAbstractItemView *itemView, KNSCore::Engine *engine, QObject *parent) : KWidgetItemDelegate(itemView, parent) , m_engine(engine) , m_itemView(itemView) , m_iconInvalid(QIcon::fromTheme(QStringLiteral("dialog-error"))) , m_iconInstall(QIcon::fromTheme(QStringLiteral("dialog-ok"))) , m_iconUpdate(QIcon::fromTheme(QStringLiteral("system-software-update"))) , m_iconDelete(QIcon::fromTheme(QStringLiteral("edit-delete"))) , m_noImage(SmallIcon(QStringLiteral("image-missing"), KIconLoader::SizeLarge, KIconLoader::DisabledState)) { QString framefile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kf5/knewstuff/pics/thumb_frame.png")); m_frameImage = QPixmap(framefile); } ItemsViewBaseDelegate::~ItemsViewBaseDelegate() { } bool ItemsViewBaseDelegate::eventFilter(QObject *watched, QEvent *event) { if (event->type() == QEvent::MouseButtonDblClick) { slotDetailsClicked(); return true; } return KWidgetItemDelegate::eventFilter(watched, event); } void ItemsViewBaseDelegate::slotLinkClicked(const QString &url) { Q_UNUSED(url) QModelIndex index = focusedIndex(); Q_ASSERT(index.isValid()); KNSCore::EntryInternal entry = index.data(Qt::UserRole).value(); m_engine->contactAuthor(entry); } void ItemsViewBaseDelegate::slotInstallClicked() { QModelIndex index = focusedIndex(); if (index.isValid()) { KNSCore::EntryInternal entry = index.data(Qt::UserRole).value(); if (!entry.isValid()) { qCDebug(KNEWSTUFF) << "Invalid entry: " << entry.name(); return; } if (entry.status() == Entry::Installed) { m_engine->uninstall(entry); } else { m_engine->install(entry); } } } void ItemsViewBaseDelegate::slotInstallActionTriggered(QAction *action) { + if (action->data().isNull()) + return; + QPoint rowDownload = action->data().toPoint(); int row = rowDownload.x(); QModelIndex index = m_itemView->model()->index(row, 0); if (index.isValid()) { KNSCore::EntryInternal entry = index.data(Qt::UserRole).value(); m_engine->install(entry, rowDownload.y()); } } void ItemsViewBaseDelegate::slotDetailsClicked() { QModelIndex index = focusedIndex(); slotDetailsClicked(index); } void ItemsViewBaseDelegate::slotDetailsClicked(const QModelIndex &index) { if (index.isValid()) { KNSCore::EntryInternal entry = index.data(Qt::UserRole).value(); if (!entry.isValid()) { return; } qCDebug(KNEWSTUFF) << "Details: " << entry.name(); emit signalShowDetails(entry); } } } diff --git a/src/ui/itemsviewdelegate.cpp b/src/ui/itemsviewdelegate.cpp index f11fd696..f12034e3 100644 --- a/src/ui/itemsviewdelegate.cpp +++ b/src/ui/itemsviewdelegate.cpp @@ -1,332 +1,344 @@ /* This file is part of KNewStuff2. Copyright (C) 2008 Jeremy Whiting Copyright (C) 2010 Reza Fatahilah Shah Copyright (C) 2010 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 "itemsviewdelegate_p.h" #include #include #include #include #include +#include #include #include #include +#include #include "core/itemsmodel.h" #include "entrydetailsdialog_p.h" namespace KNS3 { enum { DelegateLabel, DelegateInstallButton, DelegateDetailsButton, DelegateRatingWidget }; ItemsViewDelegate::ItemsViewDelegate(QAbstractItemView *itemView, KNSCore::Engine *engine, QObject *parent) : ItemsViewBaseDelegate(itemView, engine, parent) { } ItemsViewDelegate::~ItemsViewDelegate() { } QList ItemsViewDelegate::createItemWidgets(const QModelIndex &index) const { Q_UNUSED(index); QList list; QLabel *infoLabel = new QLabel(); infoLabel->setOpenExternalLinks(true); // not so nice - work around constness to install the event filter ItemsViewDelegate *delegate = const_cast(this); infoLabel->installEventFilter(delegate); list << infoLabel; QToolButton *installButton = new QToolButton(); installButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - installButton->setPopupMode(QToolButton::InstantPopup); list << installButton; setBlockedEventTypes(installButton, QList() << QEvent::MouseButtonPress << QEvent::MouseButtonRelease << QEvent::MouseButtonDblClick); connect(installButton, &QAbstractButton::clicked, this, &ItemsViewDelegate::slotInstallClicked); connect(installButton, &QToolButton::triggered, this, &ItemsViewDelegate::slotInstallActionTriggered); QToolButton *detailsButton = new QToolButton(); detailsButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); list << detailsButton; setBlockedEventTypes(detailsButton, QList() << QEvent::MouseButtonPress << QEvent::MouseButtonRelease << QEvent::MouseButtonDblClick); connect(detailsButton, &QToolButton::clicked, this, static_cast(&ItemsViewDelegate::slotDetailsClicked)); KRatingWidget *rating = new KRatingWidget(); rating->setMaxRating(10); rating->setHalfStepsEnabled(true); list << rating; //connect(rating, SIGNAL(ratingChanged(uint)), this, SLOT()); return list; } void ItemsViewDelegate::updateItemWidgets(const QList widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const { const KNSCore::ItemsModel *model = qobject_cast(index.model()); if (!model) { qCDebug(KNEWSTUFF) << "WARNING - INVALID MODEL!"; return; } const KNSCore::EntryInternal entry = index.data(Qt::UserRole).value(); // setup the install button int margin = option.fontMetrics.height() / 2; int right = option.rect.width(); QToolButton *installButton = qobject_cast(widgets.at(DelegateInstallButton)); if (installButton != nullptr) { if (installButton->menu()) { QMenu *buttonMenu = installButton->menu(); buttonMenu->clear(); installButton->setMenu(nullptr); buttonMenu->deleteLater(); } bool installable = false; bool enabled = true; QString text; QIcon icon; switch (entry.status()) { case Entry::Installed: text = i18n("Uninstall"); icon = m_iconDelete; break; case Entry::Updateable: text = i18n("Update"); icon = m_iconUpdate; installable = true; break; case Entry::Installing: text = i18n("Installing"); enabled = false; icon = m_iconUpdate; break; case Entry::Updating: text = i18n("Updating"); enabled = false; icon = m_iconUpdate; break; case Entry::Downloadable: text = i18n("Install"); icon = m_iconInstall; installable = true; break; case Entry::Deleted: text = i18n("Install Again"); icon = m_iconInstall; installable = true; break; default: text = i18n("Install"); } installButton->setText(text); installButton->setEnabled(enabled); installButton->setIcon(icon); + installButton->setPopupMode(QToolButton::InstantPopup); + if (installable && entry.downloadLinkCount() > 1) { QMenu *installMenu = new QMenu(installButton); foreach (const KNSCore::EntryInternal::DownloadLinkInformation &info, entry.downloadLinkInformationList()) { QString text = info.name; if (!info.distributionType.trimmed().isEmpty()) { text += " (" + info.distributionType.trimmed() + ')'; } QAction *installAction = installMenu->addAction(m_iconInstall, text); installAction->setData(QPoint(index.row(), info.id)); } installButton->setMenu(installMenu); + } else if (entry.status() == Entry::Installed && m_engine->hasAdoptionCommand()) { + QMenu* m = new QMenu(installButton); + m->addAction(i18n("Use"), m, [this, entry](){ + QStringList args = KShell::splitArgs(m_engine->adoptionCommand(entry)); + qCDebug(KNEWSTUFF) << "executing AdoptionCommand" << args; + QProcess::startDetached(args.takeFirst(), args); + }); + installButton->setPopupMode(QToolButton::MenuButtonPopup); + installButton->setMenu(m); } } QToolButton *detailsButton = qobject_cast(widgets.at(DelegateDetailsButton)); if (detailsButton) { detailsButton->setText(i18n("Details")); detailsButton->setIcon(QIcon::fromTheme(QStringLiteral("documentinfo"))); } if (installButton && detailsButton) { if (m_buttonSize.width() < installButton->sizeHint().width()) { const_cast(m_buttonSize) = QSize( qMax(option.fontMetrics.height() * 7, qMax(installButton->sizeHint().width(), detailsButton->sizeHint().width())), installButton->sizeHint().height()); } installButton->resize(m_buttonSize); installButton->move(right - installButton->width() - margin, option.rect.height() / 2 - installButton->height() * 1.5); detailsButton->resize(m_buttonSize); detailsButton->move(right - installButton->width() - margin, option.rect.height() / 2 - installButton->height() / 2); } QLabel *infoLabel = qobject_cast(widgets.at(DelegateLabel)); infoLabel->setWordWrap(true); if (infoLabel != nullptr) { if (model->hasPreviewImages()) { // move the text right by kPreviewWidth + margin pixels to fit the preview infoLabel->move(KNSCore::PreviewWidth + margin * 2, 0); infoLabel->resize(QSize(option.rect.width() - KNSCore::PreviewWidth - (margin * 6) - m_buttonSize.width(), option.fontMetrics.height() * 7)); } else { infoLabel->move(margin, 0); infoLabel->resize(QSize(option.rect.width() - (margin * 4) - m_buttonSize.width(), option.fontMetrics.height() * 7)); } QString text = QLatin1String("\n" "

"); QUrl link = qvariant_cast(entry.homepage()); if (!link.isEmpty()) { text += "

" + entry.name() + "

\n"; } else { text += entry.name(); } text += QLatin1String("

\n"); QString authorName = entry.author().name(); QString email = entry.author().email(); QString authorPage = entry.author().homepage(); if (!authorName.isEmpty()) { if (!authorPage.isEmpty()) { text += "

" + i18nc("Show the author of this item in a list", "By %1", " " + authorName + "") + "

\n"; } else if (!email.isEmpty()) { text += "

" + i18nc("Show the author of this item in a list", "By %1", authorName) + " " + email + "

\n"; } else { text += "

" + i18nc("Show the author of this item in a list", "By %1", authorName) + "

\n"; } } QString summary = "

" + option.fontMetrics.elidedText(entry.summary(), Qt::ElideRight, infoLabel->width() * 3) + "

\n"; text += summary; unsigned int fans = entry.numberFans(); unsigned int downloads = entry.downloadCount(); QString fanString; QString downloadString; if (fans > 0) { fanString = i18ncp("fan as in supporter", "1 fan", "%1 fans", fans); } if (downloads > 0) { downloadString = i18np("1 download", "%1 downloads", downloads); } if (downloads > 0 || fans > 0) { text += "

" + downloadString; if (downloads > 0 && fans > 0) { text += QLatin1String(", "); } text += fanString + QLatin1String("

\n"); } text += QLatin1String(""); // use simplified to get rid of newlines etc text = KNSCore::replaceBBCode(text).simplified(); infoLabel->setText(text); } KRatingWidget *rating = qobject_cast(widgets.at(DelegateRatingWidget)); if (rating) { if (entry.rating() > 0) { rating->setToolTip(i18n("Rating: %1%", entry.rating())); // assume all entries come with rating 0..100 but most are in the range 20 - 80, so 20 is 0 stars, 80 is 5 stars rating->setRating((entry.rating() - 20) * 10 / 60); // put the rating label below the install button rating->move(right - installButton->width() - margin, option.rect.height() / 2 + installButton->height() / 2); rating->resize(m_buttonSize); } else { rating->setVisible(false); } } } // draws the preview void ItemsViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { int margin = option.fontMetrics.height() / 2; QStyle *style = QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr); painter->save(); if (option.state & QStyle::State_Selected) { painter->setPen(QPen(option.palette.highlightedText().color())); } else { painter->setPen(QPen(option.palette.text().color())); } const KNSCore::ItemsModel *realmodel = qobject_cast(index.model()); if (realmodel->hasPreviewImages()) { int height = option.rect.height(); QPoint point(option.rect.left() + margin, option.rect.top() + ((height - KNSCore::PreviewHeight) / 2)); KNSCore::EntryInternal entry = index.data(Qt::UserRole).value(); if (entry.previewUrl(KNSCore::EntryInternal::PreviewSmall1).isEmpty()) { // paint the no preview icon //point.setX((PreviewWidth - m_noImage.width())/2 + 5); //point.setY(option.rect.top() + ((height - m_noImage.height()) / 2)); //painter->drawPixmap(point, m_noImage); } else { QImage image = entry.previewImage(KNSCore::EntryInternal::PreviewSmall1); if (!image.isNull()) { point.setX((KNSCore::PreviewWidth - image.width()) / 2 + 5); point.setY(option.rect.top() + ((height - image.height()) / 2)); painter->drawImage(point, image); QPoint framePoint(point.x() - 5, point.y() - 5); painter->drawPixmap(framePoint, m_frameImage.scaled(image.width() + 10, image.height() + 10)); } else { QRect rect(point, QSize(KNSCore::PreviewWidth, KNSCore::PreviewHeight)); painter->drawText(rect, Qt::AlignCenter | Qt::TextWordWrap, i18n("Loading Preview")); } } } painter->restore(); } QSize ItemsViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(option); Q_UNUSED(index); QSize size; size.setWidth(option.fontMetrics.height() * 4); size.setHeight(qMax(option.fontMetrics.height() * 7, KNSCore::PreviewHeight)); // up to 6 lines of text, and two margins return size; } } // namespace