diff --git a/autotests/knewstuffentrytest.cpp b/autotests/knewstuffentrytest.cpp index 56228356..72a15127 100644 --- a/autotests/knewstuffentrytest.cpp +++ b/autotests/knewstuffentrytest.cpp @@ -1,137 +1,138 @@ /* This file is part of KNewStuff2. Copyright (c) 2008 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 . */ // unit test for entry #include #include #include "../src/entry.h" #include "../src/entry_p.h" #include const char *entryXML = " " "Name" "https://api.opendesktop.org/v1/" "testauthor" "https://testhomepage" "3" // krazy:exclude=spelling "4.0" "82" "128891" "/some/test/path.jpg" "12345" "2008-08-12" "new version 3.0" "" "Updated" "https://testpreview" "https://testpreview" "http://testpayload" "" "" "installed" "" "" +"ghns_exclude=1" ""; const QString name = QStringLiteral("Name"); const QString category = QStringLiteral("Category"); const QString summary = QStringLiteral("new version 3.0"); const QString version = QStringLiteral("4.0"); const QString license = QStringLiteral("3"); class testEntry: public QObject { Q_OBJECT private: KNS3::Entry createEntryOld(); KNS3::Entry createEntry(); private Q_SLOTS: void testProperties(); void testCopy(); void testAssignment(); void testDomImplementation(); }; KNS3::Entry testEntry::createEntryOld() { QDomDocument document; document.setContent(QString::fromLatin1(entryXML)); QDomElement node = document.documentElement(); KNSCore::EntryInternal entryInternal; bool xmlResult = entryInternal.setEntryXML(node); qCDebug(KNEWSTUFFCORE) << "Created entry from XML " << xmlResult; return KNS3::EntryPrivate::fromInternal(&entryInternal); } KNS3::Entry testEntry::createEntry() { QXmlStreamReader reader; reader.addData(entryXML); KNSCore::EntryInternal entryInternal; bool xmlResult = reader.readNextStartElement() && entryInternal.setEntryXML(reader); qCDebug(KNEWSTUFFCORE) << "Created entry from XML " << xmlResult; return KNS3::EntryPrivate::fromInternal(&entryInternal); } void testEntry::testProperties() { KNS3::Entry entry = createEntry(); QCOMPARE(entry.name(), name); QCOMPARE(entry.category(), category); QCOMPARE(entry.license(), license); QCOMPARE(entry.summary(), summary); QCOMPARE(entry.version(), version); } void testEntry::testCopy() { KNS3::Entry entry = createEntry(); KNS3::Entry entry2(entry); QCOMPARE(entry.name(), entry2.name()); QCOMPARE(entry.category(), entry2.category()); QCOMPARE(entry.license(), entry2.license()); QCOMPARE(entry.summary(), entry2.summary()); QCOMPARE(entry.version(), entry2.version()); } void testEntry::testAssignment() { KNS3::Entry entry = createEntry(); KNS3::Entry entry2 = entry; QCOMPARE(entry.name(), entry2.name()); QCOMPARE(entry.category(), entry2.category()); QCOMPARE(entry.license(), entry2.license()); QCOMPARE(entry.summary(), entry2.summary()); QCOMPARE(entry.version(), entry2.version()); } void testEntry::testDomImplementation() { KNS3::Entry entry = createEntry(); KNS3::Entry entry2 = createEntryOld(); QCOMPARE(entry.name(), entry2.name()); QCOMPARE(entry.category(), entry2.category()); QCOMPARE(entry.license(), entry2.license()); QCOMPARE(entry.summary(), entry2.summary()); QCOMPARE(entry.version(), entry2.version()); } QTEST_GUILESS_MAIN(testEntry) #include "knewstuffentrytest.moc" diff --git a/src/attica/atticaprovider.cpp b/src/attica/atticaprovider.cpp index 376e8955..338c672e 100644 --- a/src/attica/atticaprovider.cpp +++ b/src/attica/atticaprovider.cpp @@ -1,504 +1,527 @@ /* Copyright (c) 2009-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 "atticaprovider_p.h" #include "question.h" +#include "tagsfilterchecker.h" #include #include #include #include #include #include #include #include #include #include using namespace Attica; namespace KNSCore { AtticaProvider::AtticaProvider(const QStringList &categories) : mEntryJob(nullptr) , mInitialized(false) { // init categories map with invalid categories foreach (const QString &category, categories) { mCategoryMap.insert(category, Attica::Category()); } connect(&m_providerManager, &ProviderManager::providerAdded, this, &AtticaProvider::providerLoaded); connect(&m_providerManager, SIGNAL(authenticationCredentialsMissing(Provider)), SLOT(authenticationCredentialsMissing(Provider))); } AtticaProvider::AtticaProvider(const Attica::Provider &provider, const QStringList &categories) : mEntryJob(nullptr) , mInitialized(false) { // init categories map with invalid categories foreach (const QString &category, categories) { mCategoryMap.insert(category, Attica::Category()); } providerLoaded(provider); } QString AtticaProvider::id() const { return m_providerId; } void AtticaProvider::authenticationCredentialsMissing(const KNSCore::Provider &) { qCDebug(KNEWSTUFFCORE) << "Authentication missing!"; // FIXME Show autentication dialog } bool AtticaProvider::setProviderXML(const QDomElement &xmldata) { if (xmldata.tagName() != QLatin1String("provider")) { return false; } // FIXME this is quite ugly, repackaging the xml into a string QDomDocument doc(QStringLiteral("temp")); qCDebug(KNEWSTUFFCORE) << "setting provider xml" << doc.toString(); doc.appendChild(xmldata.cloneNode(true)); m_providerManager.addProviderFromXml(doc.toString()); if (!m_providerManager.providers().isEmpty()) { qCDebug(KNEWSTUFFCORE) << "base url of attica provider:" << m_providerManager.providers().constLast().baseUrl().toString(); } else { qCritical() << "Could not load provider."; return false; } return true; } void AtticaProvider::setCachedEntries(const KNSCore::EntryInternal::List &cachedEntries) { mCachedEntries = cachedEntries; } void AtticaProvider::providerLoaded(const Attica::Provider &provider) { mName = provider.name(); qCDebug(KNEWSTUFFCORE) << "Added provider: " << provider.name(); m_provider = provider; m_providerId = provider.baseUrl().toString(); Attica::ListJob *job = m_provider.requestCategories(); connect(job, &BaseJob::finished, this, &AtticaProvider::listOfCategoriesLoaded); job->start(); } void AtticaProvider::listOfCategoriesLoaded(Attica::BaseJob *listJob) { if (!jobSuccess(listJob)) { return; } qCDebug(KNEWSTUFFCORE) << "loading categories: " << mCategoryMap.keys(); Attica::ListJob *job = static_cast*>(listJob); Category::List categoryList = job->itemList(); QList categoryMetadataList; foreach (const Category &category, categoryList) { if (mCategoryMap.contains(category.name())) { qCDebug(KNEWSTUFFCORE) << "Adding category: " << category.name() << category.displayName(); //If there is only the placeholder category, replace it if (mCategoryMap.contains(category.name()) && !mCategoryMap.value(category.name()).isValid()) { mCategoryMap.insert(category.name(), category); } else { mCategoryMap.insertMulti(category.name(), category); } CategoryMetadata categoryMetadata; categoryMetadata.id = category.id(); categoryMetadata.name = category.name(); categoryMetadata.displayName = category.displayName(); categoryMetadataList << categoryMetadata; } } std::sort(categoryMetadataList.begin(), categoryMetadataList.end(), [](const AtticaProvider::CategoryMetadata &i, const AtticaProvider::CategoryMetadata &j) -> bool { const QString a(i.displayName.isEmpty() ? i.name : i.displayName); const QString b(j.displayName.isEmpty() ? j.name : j.displayName); return (QCollator().compare(a, b) < 0); }); bool correct = false; for(auto it = mCategoryMap.cbegin(), itEnd = mCategoryMap.cend(); it!=itEnd; ++it) { if (!it.value().isValid()) { qCWarning(KNEWSTUFFCORE) << "Could not find category" << it.key(); } else { correct = true; } } if (correct) { mInitialized = true; emit providerInitialized(this); emit categoriesMetadataLoded(categoryMetadataList); } else { emit signalError(i18n("All categories are missing")); } } bool AtticaProvider::isInitialized() const { return mInitialized; } void AtticaProvider::loadEntries(const KNSCore::Provider::SearchRequest &request) { if (mEntryJob) { mEntryJob->abort(); mEntryJob = nullptr; } mCurrentRequest = request; switch (request.filter) { case None: break; case ExactEntryId: { ItemJob *job = m_provider.requestContent(request.searchTerm); connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded); job->start(); return; } case Installed: if (request.page == 0) { emit loadingFinished(request, installedEntries()); } else { emit loadingFinished(request, EntryInternal::List()); } return; case Updates: checkForUpdates(); return; } Attica::Provider::SortMode sorting = atticaSortMode(request.sortMode); Attica::Category::List categoriesToSearch; if (request.categories.isEmpty()) { // search in all categories categoriesToSearch = mCategoryMap.values(); } else { categoriesToSearch.reserve(request.categories.size()); foreach (const QString &categoryName, request.categories) { categoriesToSearch.append(mCategoryMap.values(categoryName)); } } ListJob *job = m_provider.searchContents(categoriesToSearch, request.searchTerm, sorting, request.page, request.pageSize); connect(job, &BaseJob::finished, this, &AtticaProvider::categoryContentsLoaded); mEntryJob = job; job->start(); } void AtticaProvider::checkForUpdates() { foreach (const EntryInternal &e, mCachedEntries) { ItemJob *job = m_provider.requestContent(e.uniqueId()); connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded); m_updateJobs.insert(job); job->start(); qCDebug(KNEWSTUFFCORE) << "Checking for update: " << e.name(); } } void AtticaProvider::loadEntryDetails(const KNSCore::EntryInternal &entry) { ItemJob *job = m_provider.requestContent(entry.uniqueId()); connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded); job->start(); } void AtticaProvider::detailsLoaded(BaseJob *job) { if (jobSuccess(job)) { ItemJob *contentJob = static_cast*>(job); Content content = contentJob->result(); EntryInternal entry = entryFromAtticaContent(content); emit entryDetailsLoaded(entry); qCDebug(KNEWSTUFFCORE) << "check update finished: " << entry.name(); } if (m_updateJobs.remove(job) && m_updateJobs.isEmpty()) { qCDebug(KNEWSTUFFCORE) << "check update finished."; QList updatable; foreach (const EntryInternal &entry, mCachedEntries) { if (entry.status() == KNS3::Entry::Updateable) { updatable.append(entry); } } emit loadingFinished(mCurrentRequest, updatable); } } void AtticaProvider::categoryContentsLoaded(BaseJob *job) { if (!jobSuccess(job)) { return; } ListJob *listJob = static_cast*>(job); Content::List contents = listJob->itemList(); EntryInternal::List entries; + TagsFilterChecker checker(tagFilter()); + TagsFilterChecker downloadschecker(downloadTagFilter()); Q_FOREACH (const Content &content, contents) { - mCachedContent.insert(content.id(), content); - entries.append(entryFromAtticaContent(content)); + if (checker.filterAccepts(content.tags())) { + bool filterAcceptsDownloads = true; + if (content.downloads() > 0) { + filterAcceptsDownloads = false; + for (const Attica::DownloadDescription &dli : content.downloadUrlDescriptions()) { + if (downloadschecker.filterAccepts(dli.tags())) { + filterAcceptsDownloads = true; + break; + } + } + } + if (filterAcceptsDownloads) { + mCachedContent.insert(content.id(), content); + entries.append(entryFromAtticaContent(content)); + } else { + qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on download filter" << downloadTagFilter(); + } + } else { + qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on entry filter" << tagFilter(); + } } qCDebug(KNEWSTUFFCORE) << "loaded: " << mCurrentRequest.hashForRequest() << " count: " << entries.size(); emit loadingFinished(mCurrentRequest, entries); mEntryJob = nullptr; } Attica::Provider::SortMode AtticaProvider::atticaSortMode(const SortMode &sortMode) { switch(sortMode) { case Newest: return Attica::Provider::Newest; case Alphabetical: return Attica::Provider::Alphabetical; case Downloads: return Attica::Provider::Downloads; default: return Attica::Provider::Rating; } } void AtticaProvider::loadPayloadLink(const KNSCore::EntryInternal &entry, int linkId) { Attica::Content content = mCachedContent.value(entry.uniqueId()); const DownloadDescription desc = content.downloadUrlDescription(linkId); if (desc.hasPrice()) { // Ask for balance, then show information... ItemJob *job = m_provider.requestAccountBalance(); connect(job, &BaseJob::finished, this, &AtticaProvider::accountBalanceLoaded); mDownloadLinkJobs[job] = qMakePair(entry, linkId); job->start(); qCDebug(KNEWSTUFFCORE) << "get account balance"; } else { ItemJob *job = m_provider.downloadLink(entry.uniqueId(), QString::number(linkId)); connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded); mDownloadLinkJobs[job] = qMakePair(entry, linkId); job->start(); qCDebug(KNEWSTUFFCORE) << " link for " << entry.uniqueId(); } } void AtticaProvider::accountBalanceLoaded(Attica::BaseJob *baseJob) { if (!jobSuccess(baseJob)) { return; } ItemJob *job = static_cast*>(baseJob); AccountBalance item = job->result(); QPair pair = mDownloadLinkJobs.take(job); EntryInternal entry(pair.first); Content content = mCachedContent.value(entry.uniqueId()); if (content.downloadUrlDescription(pair.second).priceAmount() < item.balance()) { qCDebug(KNEWSTUFFCORE) << "Your balance is greater than the price." << content.downloadUrlDescription(pair.second).priceAmount() << " balance: " << item.balance(); Question question; question.setQuestion(i18nc("the price of a download item, parameter 1 is the currency, 2 is the price", "This item costs %1 %2.\nDo you want to buy it?", item.currency(), content.downloadUrlDescription(pair.second).priceAmount() )); if(question.ask() == Question::YesResponse) { ItemJob *job = m_provider.downloadLink(entry.uniqueId(), QString::number(pair.second)); connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded); mDownloadLinkJobs[job] = qMakePair(entry, pair.second); job->start(); } else { return; } } else { qCDebug(KNEWSTUFFCORE) << "You don't have enough money on your account!" << content.downloadUrlDescription(0).priceAmount() << " balance: " << item.balance(); emit signalInformation(i18n("Your account balance is too low:\nYour balance: %1\nPrice: %2", item.balance(), content.downloadUrlDescription(0).priceAmount())); } } void AtticaProvider::downloadItemLoaded(BaseJob *baseJob) { if (!jobSuccess(baseJob)) { return; } ItemJob *job = static_cast*>(baseJob); DownloadItem item = job->result(); EntryInternal entry = mDownloadLinkJobs.take(job).first; entry.setPayload(QString(item.url().toString())); emit payloadLinkLoaded(entry); } EntryInternal::List AtticaProvider::installedEntries() const { EntryInternal::List entries; foreach (const EntryInternal &entry, mCachedEntries) { if (entry.status() == KNS3::Entry::Installed || entry.status() == KNS3::Entry::Updateable) { entries.append(entry); } } return entries; } void AtticaProvider::vote(const EntryInternal &entry, uint rating) { PostJob *job = m_provider.voteForContent(entry.uniqueId(), rating); connect(job, &BaseJob::finished, this, &AtticaProvider::votingFinished); job->start(); } void AtticaProvider::votingFinished(Attica::BaseJob *job) { if (!jobSuccess(job)) { return; } emit signalInformation(i18nc("voting for an item (good/bad)", "Your vote was recorded.")); } void AtticaProvider::becomeFan(const EntryInternal &entry) { PostJob *job = m_provider.becomeFan(entry.uniqueId()); connect(job, &BaseJob::finished, this, &AtticaProvider::becomeFanFinished); job->start(); } void AtticaProvider::becomeFanFinished(Attica::BaseJob *job) { if (!jobSuccess(job)) { return; } emit signalInformation(i18n("You are now a fan.")); } bool AtticaProvider::jobSuccess(Attica::BaseJob *job) const { if (job->metadata().error() == Attica::Metadata::NoError) { return true; } qCDebug(KNEWSTUFFCORE) << "job error: " << job->metadata().error() << " status code: " << job->metadata().statusCode() << job->metadata().message(); if (job->metadata().error() == Attica::Metadata::NetworkError) { emit signalError(i18n("Network error %1: %2", job->metadata().statusCode(), job->metadata().error())); } if (job->metadata().error() == Attica::Metadata::OcsError) { if (job->metadata().statusCode() == 200) { emit signalError(i18n("Too many requests to server. Please try again in a few minutes.")); } else { emit signalError(i18n("Unknown Open Collaboration Service API error. (%1)", job->metadata().statusCode())); } } return false; } EntryInternal AtticaProvider::entryFromAtticaContent(const Attica::Content &content) { EntryInternal entry; entry.setProviderId(id()); entry.setUniqueId(content.id()); entry.setStatus(KNS3::Entry::Downloadable); entry.setVersion(content.version()); entry.setReleaseDate(content.updated().date()); int index = mCachedEntries.indexOf(entry); if (index >= 0) { EntryInternal &cacheEntry = mCachedEntries[index]; // check if updateable if (((cacheEntry.status() == KNS3::Entry::Installed) || (cacheEntry.status() == KNS3::Entry::Updateable)) && ((cacheEntry.version() != entry.version()) || (cacheEntry.releaseDate() != entry.releaseDate()))) { cacheEntry.setStatus(KNS3::Entry::Updateable); cacheEntry.setUpdateVersion(entry.version()); cacheEntry.setUpdateReleaseDate(entry.releaseDate()); } entry = cacheEntry; } else { mCachedEntries.append(entry); } entry.setName(content.name()); entry.setHomepage(content.detailpage()); entry.setRating(content.rating()); entry.setNumberOfComments(content.numberOfComments()); entry.setDownloadCount(content.downloads()); entry.setNumberFans(content.attribute(QStringLiteral("fans")).toInt()); entry.setDonationLink(content.attribute(QStringLiteral("donationpage"))); entry.setKnowledgebaseLink(content.attribute(QStringLiteral("knowledgebasepage"))); entry.setNumberKnowledgebaseEntries(content.attribute(QStringLiteral("knowledgebaseentries")).toInt()); entry.setHomepage(content.detailpage()); entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("1")), EntryInternal::PreviewSmall1); entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("2")), EntryInternal::PreviewSmall2); entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("3")), EntryInternal::PreviewSmall3); entry.setPreviewUrl(content.previewPicture(QStringLiteral("1")), EntryInternal::PreviewBig1); entry.setPreviewUrl(content.previewPicture(QStringLiteral("2")), EntryInternal::PreviewBig2); entry.setPreviewUrl(content.previewPicture(QStringLiteral("3")), EntryInternal::PreviewBig3); entry.setLicense(content.license()); Author author; author.setName(content.author()); author.setHomepage(content.attribute(QStringLiteral("profilepage"))); entry.setAuthor(author); entry.setSource(EntryInternal::Online); entry.setSummary(content.description()); entry.setShortSummary(content.summary()); entry.setChangelog(content.changelog()); + entry.setTags(content.tags()); entry.clearDownloadLinkInformation(); QList descs = content.downloadUrlDescriptions(); foreach (const Attica::DownloadDescription &desc, descs) { EntryInternal::DownloadLinkInformation info; info.name = desc.name(); info.priceAmount = desc.priceAmount(); info.distributionType = desc.distributionType(); info.descriptionLink = desc.link(); info.id = desc.id(); info.size = desc.size(); info.isDownloadtypeLink = desc.type() == Attica::DownloadDescription::LinkDownload; + info.tags = desc.tags(); entry.appendDownloadLinkInformation(info); } return entry; } } // namespace diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 735166e1..eac40ed2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,117 +1,119 @@ set(KNewStuffCore_SRCS author.cpp cache.cpp downloadmanager.cpp engine.cpp entryinternal.cpp imageloader.cpp installation.cpp itemsmodel.cpp provider.cpp security.cpp + tagsfilterchecker.cpp xmlloader.cpp # A system by which queries can be passed to the user, and responses # gathered, depending on implementation. See question.h for details. question.cpp questionmanager.cpp questionlistener.cpp # A set of minimal KJob based classes, designed to replace the # more powerful KIO based system in places where KIO is not available # for one reason or another. jobs/downloadjob.cpp jobs/filecopyjob.cpp jobs/filecopyworker.cpp jobs/httpjob.cpp jobs/httpworker.cpp ../attica/atticaprovider.cpp ../staticxml/staticxmlprovider.cpp ../upload/atticahelper.cpp ) ecm_qt_declare_logging_category(KNewStuffCore_SRCS HEADER knewstuffcore_debug.h IDENTIFIER KNEWSTUFFCORE CATEGORY_NAME org.kde.knewstuff.core) add_library(KF5NewStuffCore ${KNewStuffCore_SRCS} ) add_library(KF5::NewStuffCore ALIAS KF5NewStuffCore ) generate_export_header(KF5NewStuffCore BASE_NAME KNewStuffCore EXPORT_FILE_NAME knewstuffcore_export.h) # The src/ dir is needed for the entry.h header. This only happens because some # code in Core uses an enum from KNS3::Entry target_include_directories(KF5NewStuffCore PUBLIC "$" INTERFACE "$") target_link_libraries(KF5NewStuffCore PUBLIC KF5::Attica # For interacting with ocs providers, public for uploaddialog slots KF5::CoreAddons Qt5::Xml PRIVATE KF5::Archive # For decompressing archives KF5::I18n # For translations KF5::ConfigCore Qt5::Gui # For QImage ) set_target_properties(KF5NewStuffCore PROPERTIES VERSION ${KNEWSTUFF_VERSION_STRING} SOVERSION ${KNEWSTUFF_SOVERSION} EXPORT_NAME NewStuffCore ) ecm_generate_headers(KNewStuffCore_CamelCase_HEADERS HEADER_NAMES Author Cache DownloadManager Engine EntryInternal Installation ItemsModel Provider Question QuestionListener QuestionManager Security + TagsFilterChecker XmlLoader REQUIRED_HEADERS KNewStuffCore_HEADERS PREFIX KNSCore ) install(TARGETS KF5NewStuffCore EXPORT KF5NewStuffCoreTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${KNewStuffCore_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KNewStuff3/KNSCore COMPONENT Devel) install(FILES ${KNewStuffCore_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/knewstuffcore_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KNewStuff3/knscore COMPONENT Devel) if(BUILD_QCH) ecm_add_qch( KF5NewStuffCore_QCH NAME KNewStuffCore BASE_NAME KF5NewStuffCore VERSION ${KF5_VERSION} ORG_DOMAIN org.kde SOURCES ${KNewStuffCore_HEADERS} LINK_QCHS KF5Attica_QCH KF5CoreAddons_QCH BLANK_MACROS KNEWSTUFFCORE_EXPORT KNEWSTUFFCORE_DEPRECATED KNEWSTUFFCORE_DEPRECATED_EXPORT TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KNewStuffCore LIB_NAME KF5NewStuffCore DEPS "Attica" FILENAME_VAR COREPRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KNewStuff3) install(FILES ${COREPRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 3452bb51..f8a3c22f 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -1,670 +1,722 @@ /* 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 #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; }; 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); } 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")); KConfig conf(configfile); if (conf.accessMode() == KConfig::NoAccess) { emit signalError(i18n("Configuration file exists, but cannot be opened: \"%1\"", configfile)); qCritical() << "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 signalError(i18n("Configuration file is invalid: \"%1\"", configfile)); qCritical() << configfile << " doesn't contain a KNewStuff3 section."; 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()); + d->tagFilter = group.readEntry("TagFilter", QStringList()); + if (d->tagFilter.isEmpty()) { + d->tagFilter.append(QStringLiteral("ghns_exclude!=1")); + } + d->downloadTagFilter = group.readEntry("DownloadTagFilter", QStringList()); + 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->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); 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 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.reset(new AtticaProvider(m_categories)); 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 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->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::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 << "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; 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; 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(); } 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); 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(); } } +void Engine::setTagFilter(const QStringList &filter) +{ + d->tagFilter = filter; + foreach (const QSharedPointer &p, m_providers) { + p->setTagFilter(d->tagFilter); + } +} + +QStringList Engine::tagFilter() const +{ + return d->tagFilter; +} + +void KNSCore::Engine::addTagFilter(const QString &filter) +{ + d->tagFilter << filter; + foreach (const QSharedPointer &p, m_providers) { + p->setTagFilter(d->tagFilter); + } +} + +void Engine::setDownloadTagFilter(const QStringList &filter) +{ + d->downloadTagFilter = filter; + foreach (const QSharedPointer &p, m_providers) { + p->setDownloadTagFilter(d->downloadTagFilter); + } +} + +QStringList Engine::downloadTagFilter() const +{ + return d->downloadTagFilter; +} + +void Engine::addDownloadTagFilter(const QString &filter) +{ + d->downloadTagFilter << filter; + foreach (const QSharedPointer &p, 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() { 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); connect(l, &ImageLoader::signalError, this, [this](const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type, const QString &errorText) { 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() { 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; 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, 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(); } void KNSCore::Engine::setPageSize(int pageSize) { m_pageSize = pageSize; } diff --git a/src/core/engine.h b/src/core/engine.h index 8cb5019e..7e814cb5 100644 --- a/src/core/engine.h +++ b/src/core/engine.h @@ -1,408 +1,504 @@ /* 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; class EnginePrivate; namespace Attica { class ProviderManager; class Provider; } /** * Contains the core functionality for handling interaction with NewStuff providers. * The entrypoint for most things will be the creation of an instance of KNSCore::Engine * which will other classes then either use or get instantiated from directly. * * NOTE: When implementing anything on top of KNSCore, without using either KNS3 or the * Qt Quick components, you will need to implement a custom QuestionListener (see that * class for instructions) * * @see KNSCore::Engine * @see KNSCore::ItemsModel * @see KNSCore::QuestionListener */ 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. */ 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); /** * Attempt to load a specific preview for the specified entry. * * @param entry The entry to fetch a preview for * @param type The particular preview to fetch * * @see signalEntryPreviewLoaded(KNSCore::EntryInternal, KNSCore::EntryInternal::PreviewType); * @see signalPreviewFailed(); */ void loadPreview(const KNSCore::EntryInternal &entry, EntryInternal::PreviewType type); /** * Get the full details of a specific entry * * @param entry The entry to get full details for * * @see Entry::signalEntryDetailsLoaded(KNSCore::EntryInternal) */ void loadDetails(const KNSCore::EntryInternal &entry); /** * Set the order the search results are returned in. * * Search requests default to showing the newest entries first. * * Note: This will automatically launch a search, which means * you do not need to call requestData manually. * * @see KNSCore::Provider::SearchRequest * @param mode The order you want search results to come back in. */ void setSortMode(Provider::SortMode mode); /** * Set a filter for results (defaults to none), which will allow you * to show only installed entries, installed entries which have updates, * or a specific item with a specified ID. The latter further requires * the search term to be the exact ID of the entry you wish to retrieve. * * Note: This will automatically launch a search, which means * you do not need to call requestData manually. * * @see fetchEntryById(QString) * @see setSearchTerm(QString) * @param filter The type of results you wish to see */ void setFilter(Provider::Filter filter); /** * Set the categories that will be included in searches * * Note: This will automatically launch a search, which means * you do not need to call requestData manually. * * @see KNSCore::Engine::categories() * @param categories A list of strings of categories */ void setCategoriesFilter(const QStringList &categories); /** * Sets a string search term. * * Note: This will automatically launch a search, which means * you do not need to call requestData manually. * * @param searchString The search term you wish to search for */ void setSearchTerm(const QString &searchString); void reloadEntries(); void requestMoreData(); void requestData(int page, int pageSize); + /** + * Set a filter for results, which filters out all entries which do not match + * the filter, as applied to the tags for the entry. This filters only on the + * tags specified for the entry itself. To filter the downloadlinks, use + * setDownloadTagFilter(QStringList). + * + * @note The default filter if one is not set from your knsrc file will filter + * out entries marked as ghns_exclude=1. To retain this when setting a custom + * filter, add "ghns_exclude!=1" as one of the filters. + * + * @note Some tags provided by OCS do not supply a value (and are simply passed + * as a key). These will be interpreted as having the value 1 for filtering + * purposes. An example of this might be ghns_exclude, which in reality will + * generally be passed through ocs as "ghns_exclude" rather than "ghns_exclude=1" + * + * == Examples of use == + * Value for tag "tagname" must be exactly "tagdata": + * tagname==tagdata + * + * Value for tag "tagname" must be different from "tagdata": + * tagname!=tagdata + * + * == KNSRC entry == + * A tag filter line in a .knsrc file, which is a comma semarated list of + * tag/value pairs, might look like: + * + * TagFilter=ghns_exclude!=1,data##mimetype==application/cbr+zip,data##mimetype==application/cbr+rar + * which would honour the exclusion and filter out anything that does not + * include a comic book archive in either zip or rar format in one or more + * of the download items. + * Notice in particular that there are two data##mimetype entries. Use this + * for when a tag may have multiple values. + * + * TagFilter=application##architecture==x86_64 + * which would not honour the exclusion, and would filter out all entries + * which do not mark themselves as having a 64bit application binary in at + * least one download item. + * + * The value does not current suppport wildcards. The list should be considered + * a binary AND operation (that is, all filter entries must match for the data + * entry to be included in the return data) + * + * @param filter The filter in the form of a list of strings + * @see setDownloadTagFilter(QStringList) + * @since 5.51 + */ + void setTagFilter(const QStringList &filter); + /** + * Gets the current tag filter list + * @see setTagFilter(QStringList) + * @since 5.51 + */ + QStringList tagFilter() const; + /** + * Add a single filter entry to the entry tag filter. The filter should be in + * the same form as the filter lines in the list used by setTagFilter(QStringList) + * @param filter The filter in the form of a string + * @see setTagFilter(QStringList) + * @since 5.51 + */ + void addTagFilter(const QString &filter); + /** + * Sets a filter to be applied to the downloads for an entry. The logic is the + * same as used in setTagFilter(QStringList), but vitally, only one downloadlink + * is required to match the filter for the list to be valid. If you do not wish + * to show the others in your client, you must hide them yourself. + * + * For an entry to be accepted when a download tag filter is set, it must also + * be accepted by the entry filter (so, for example, while a list of downloads + * might be accepted, if the entry has ghns_exclude set, and the default entry + * filter is set, the entry will still be filtered out). + * + * In your knsrc file, set DownloadTagFilter to the filter you wish to apply, + * using the same logic as described for the entry tagfilter. + * + * @param filter The filter in the form of a list of strings + * @see setTagFilter(QStringList) + * @since 5.51 + */ + void setDownloadTagFilter(const QStringList &filter); + /** + * Gets the current downloadlink tag filter list + * @see setDownloadTagFilter(QStringList) + * @since 5.51 + */ + QStringList downloadTagFilter() const; + /** + * Add a single filter entry to the download tag filter. The filter should be in + * the same form as the filter lines in the list used by setDownloadsTagFilter(QStringList) + * @param filter The filter in the form of a string + * @see setTagFilter(QStringList) + * @see setDownloadTagFilter(QStringList) + * @since 5.51 + */ + void addDownloadTagFilter(const QString &filter); + /** * Request for packages that are installed and need update * * These will be reported through the signal @see signalUpdateableEntriesLoaded(). */ void checkForUpdates(); /** * Requests installed packages with an up to date state * * @see signalEntriesLoaded() */ void checkForInstalled(); /** * Convenience method to launch a search for one specific entry. * * @note it will reset the engine state * * @param id The ID of the entry you wish to fetch */ void fetchEntryById(const QString &id); /** * Try to contact the author of the entry by email or showing their homepage. */ void contactAuthor(const EntryInternal &entry); /** * Whether or not a user is able to vote on the passed entry. * * @param entry The entry to check votability on * @return True if the user is able to vote on the entry */ bool userCanVote(const EntryInternal &entry); /** * Cast a vote on the passed entry. * * @param entry The entry to vote on * @param rating A number from 0 to 100, 50 being neutral, 0 being most negative and 100 being most positive. */ void vote(const EntryInternal &entry, uint rating); /** * Whether or not the user is allowed to become a fan of * a particular entry. * Not all providers (and consequently entries) support the fan functionality * and you can use this function to determine this ability. * @param entry The entry the user might wish to be a fan of * @return Whether or not it is possible for the user to become a fan of that entry */ bool userCanBecomeFan(const EntryInternal &entry); /** * This will mark the user who is currently authenticated as a fan * of the entry passed to the function. * @param entry The entry the user wants to be a fan of */ void becomeFan(const EntryInternal &entry); // FIXME There is currently no exposed API to remove the fan status /** * The list of the server-side names of the categories handled by this * engine instance. This corresponds directly to the list of categories * in your knsrc file. This is not supposed to be used as user-facing * strings - @see categoriesMetadata() for that. * * @return The categories which this instance of Engine handles */ QStringList categories() const; /** * The list of categories searches will actually show results from. This * is a subset of the categories() list. * * @see KNSCore::Engine::setCategoriesFilter(QString) */ QStringList categoriesFilter() const; /** * The list of metadata for the categories handled by this engine instance. * If you wish to show the categories to the user, this is the data to use. * The category name is the string used to set categories for the filter, * and also what is returned by both categories() and categoriesFilter(). * The human-readable name is displayName, and the only thing which should * be shown to the user. * * @return The metadata for all categories handled by this engine */ QList categoriesMetadata(); /** * The adoption command can be used to allow a user to make use of an entry's * installed data. For example, this command might be used to ask the system to * switch to a wallpaper or icon theme which was installed with KNS. * * The following is how this might look in a knsrc file. The example shows how * an external tool is called on the installed file represented by %d. *
        AdoptionCommand=/usr/lib64/libexec/plasma-changeicons %d
      * 
* * @param entry The entry to return an adoption command for * @return The command to run to adopt this entry's installed data */ QString adoptionCommand(const KNSCore::EntryInternal &entry) const; /** * Whether or not an adoption command exists for this engine * * @see adoptionCommand(KNSCore::EntryInternal) * @return True if an adoption command exists */ bool hasAdoptionCommand() const; /** * Set the page size for requests not made explicitly with requestData(int,int) * @param pageSize the default number of entries to request from the provider * @see requestData(int,int) */ void setPageSize(int pageSize); 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; QString m_adoptionCommand; // the current request from providers Provider::SearchRequest m_currentRequest; EnginePrivate * const d; // 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/entryinternal.cpp b/src/core/entryinternal.cpp index ff9296cd..a5218748 100644 --- a/src/core/entryinternal.cpp +++ b/src/core/entryinternal.cpp @@ -1,759 +1,775 @@ /* This file is part of KNewStuff2. Copyright (c) 2002 Cornelius Schumacher Copyright (c) 2003 - 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 "entryinternal.h" #include #include #include #include #include "xmlloader.h" #include "../entry_p.h" // For Entry::Status ONLY! using namespace KNSCore; class EntryInternal::Private : public QSharedData { public: Private() : mReleaseDate(QDate::currentDate()) , mRating(0) , mNumberOfComments(0) , mDownloadCount(0) , mNumberFans(0) , mNumberKnowledgebaseEntries(0) , mStatus(KNS3::Entry::Invalid) , mSource(EntryInternal::Online) { qRegisterMetaType(); } bool operator==(const Private &other) const { return mUniqueId == other.mUniqueId && mProviderId == other.mProviderId; } QString mUniqueId; QString mName; QUrl mHomepage; QString mCategory; QString mLicense; QString mVersion; QDate mReleaseDate; // Version and date if a newer version is found (updateable) QString mUpdateVersion; QDate mUpdateReleaseDate; Author mAuthor; int mRating; int mNumberOfComments; int mDownloadCount; int mNumberFans; int mNumberKnowledgebaseEntries; QString mKnowledgebaseLink; QString mSummary; QString mShortSummary; QString mChangelog; QString mPayload; QStringList mInstalledFiles; QString mProviderId; QStringList mUnInstalledFiles; QString mDonationLink; + QStringList mTags; QString mChecksum; QString mSignature; KNS3::Entry::Status mStatus; EntryInternal::Source mSource; QString mPreviewUrl[6]; QImage mPreviewImage[6]; QList mDownloadLinkInformationList; }; EntryInternal::EntryInternal() : d(new Private) { } EntryInternal::EntryInternal(const EntryInternal &other) : d(other.d) { } EntryInternal &EntryInternal::operator=(const EntryInternal &other) { d = other.d; return *this; } bool EntryInternal::operator<(const KNSCore::EntryInternal &other) const { return d->mUniqueId < other.d->mUniqueId; } bool EntryInternal::operator==(const KNSCore::EntryInternal &other) const { return d->mUniqueId == other.d->mUniqueId && d->mProviderId == other.d->mProviderId; } EntryInternal::~EntryInternal() { } bool EntryInternal::isValid() const { return !d->mUniqueId.isEmpty(); } QString EntryInternal::name() const { return d->mName; } void EntryInternal::setName(const QString &name) { d->mName = name; } QString EntryInternal::uniqueId() const { return d->mUniqueId; } void EntryInternal::setUniqueId(const QString &id) { d->mUniqueId = id; } QString EntryInternal::providerId() const { return d->mProviderId; } void EntryInternal::setProviderId(const QString &id) { d->mProviderId = id; } +QStringList KNSCore::EntryInternal::tags() const +{ + return d->mTags; +} + +void KNSCore::EntryInternal::setTags(const QStringList &tags) +{ + d->mTags = tags; +} + QString EntryInternal::category() const { return d->mCategory; } void EntryInternal::setCategory(const QString &category) { d->mCategory = category; } QUrl EntryInternal::homepage() const { return d->mHomepage; } void EntryInternal::setHomepage(const QUrl &page) { d->mHomepage = page; } Author EntryInternal::author() const { return d->mAuthor; } void EntryInternal::setAuthor(const KNSCore::Author &author) { d->mAuthor = author; } QString EntryInternal::license() const { return d->mLicense; } void EntryInternal::setLicense(const QString &license) { d->mLicense = license; } QString EntryInternal::summary() const { return d->mSummary; } void EntryInternal::setSummary(const QString &summary) { d->mSummary = summary; } QString EntryInternal::shortSummary() const { return d->mShortSummary; } void EntryInternal::setShortSummary(const QString &summary) { d->mShortSummary = summary; } void EntryInternal::setChangelog(const QString &changelog) { d->mChangelog = changelog; } QString EntryInternal::changelog() const { return d->mChangelog; } QString EntryInternal::version() const { return d->mVersion; } void EntryInternal::setVersion(const QString &version) { d->mVersion = version; } QDate EntryInternal::releaseDate() const { return d->mReleaseDate; } void EntryInternal::setReleaseDate(const QDate &releasedate) { d->mReleaseDate = releasedate; } QString EntryInternal::payload() const { return d->mPayload; } void EntryInternal::setPayload(const QString &url) { d->mPayload = url; } QDate EntryInternal::updateReleaseDate() const { return d->mUpdateReleaseDate; } void EntryInternal::setUpdateReleaseDate(const QDate &releasedate) { d->mUpdateReleaseDate = releasedate; } QString EntryInternal::updateVersion() const { return d->mUpdateVersion; } void EntryInternal::setUpdateVersion(const QString &version) { d->mUpdateVersion = version; } QString EntryInternal::previewUrl(PreviewType type) const { return d->mPreviewUrl[type]; } void EntryInternal::setPreviewUrl(const QString &url, PreviewType type) { d->mPreviewUrl[type] = url; } QImage EntryInternal::previewImage(PreviewType type) const { return d->mPreviewImage[type]; } void EntryInternal::setPreviewImage(const QImage &image, PreviewType type) { d->mPreviewImage[type] = image; } int EntryInternal::rating() const { return d->mRating; } void EntryInternal::setRating(int rating) { d->mRating = rating; } int EntryInternal::numberOfComments() const { return d->mRating; } void EntryInternal::setNumberOfComments (int comments) { d->mNumberOfComments = comments; } int EntryInternal::downloadCount() const { return d->mDownloadCount; } void EntryInternal::setDownloadCount(int downloads) { d->mDownloadCount = downloads; } int EntryInternal::numberFans() const { return d->mNumberFans; } void EntryInternal::setNumberFans(int fans) { d->mNumberFans = fans; } QString EntryInternal::donationLink() const { return d->mDonationLink; } void EntryInternal::setDonationLink(const QString &link) { d->mDonationLink = link; } int EntryInternal::numberKnowledgebaseEntries() const { return d->mNumberKnowledgebaseEntries; } void EntryInternal::setNumberKnowledgebaseEntries(int num) { d->mNumberKnowledgebaseEntries = num; } QString EntryInternal::knowledgebaseLink() const { return d->mKnowledgebaseLink; } void EntryInternal::setKnowledgebaseLink(const QString &link) { d->mKnowledgebaseLink = link; } /* QString EntryInternal::checksum() const { return d->mChecksum; } QString EntryInternal::signature() const { return d->mSignature; } */ EntryInternal::Source EntryInternal::source() const { return d->mSource; } void EntryInternal::setSource(Source source) { d->mSource = source; } KNS3::Entry::Status EntryInternal::status() const { return d->mStatus; } void EntryInternal::setStatus(KNS3::Entry::Status status) { d->mStatus = status; } void KNSCore::EntryInternal::setInstalledFiles(const QStringList &files) { d->mInstalledFiles = files; } QStringList KNSCore::EntryInternal::installedFiles() const { return d->mInstalledFiles; } void KNSCore::EntryInternal::setUnInstalledFiles(const QStringList &files) { d->mUnInstalledFiles = files; } QStringList KNSCore::EntryInternal::uninstalledFiles() const { return d->mUnInstalledFiles; } int KNSCore::EntryInternal::downloadLinkCount() const { return d->mDownloadLinkInformationList.size(); } QList KNSCore::EntryInternal::downloadLinkInformationList() const { return d->mDownloadLinkInformationList; } void KNSCore::EntryInternal::appendDownloadLinkInformation(const KNSCore::EntryInternal::DownloadLinkInformation &info) { d->mDownloadLinkInformationList.append(info); } void EntryInternal::clearDownloadLinkInformation() { d->mDownloadLinkInformationList.clear(); } static QXmlStreamReader::TokenType readNextSkipComments(QXmlStreamReader* xml) { do { xml->readNext(); } while(xml->tokenType() == QXmlStreamReader::Comment || (xml->tokenType() == QXmlStreamReader::Characters && xml->text().trimmed().isEmpty())); return xml->tokenType(); } static QStringRef readText(QXmlStreamReader* xml) { Q_ASSERT(xml->tokenType() == QXmlStreamReader::StartElement); QStringRef ret; const auto token = readNextSkipComments(xml); if (token == QXmlStreamReader::Characters) { ret = xml->text(); } return ret; } static QString readStringTrimmed(QXmlStreamReader* xml) { Q_ASSERT(xml->tokenType() == QXmlStreamReader::StartElement); QString ret = readText(xml).trimmed().toString(); if (xml->tokenType() == QXmlStreamReader::Characters) readNextSkipComments(xml); Q_ASSERT(xml->tokenType() == QXmlStreamReader::EndElement); return ret; } static int readInt(QXmlStreamReader* xml) { Q_ASSERT(xml->tokenType() == QXmlStreamReader::StartElement); int ret = readText(xml).toInt(); xml->readNext(); Q_ASSERT(xml->tokenType() == QXmlStreamReader::EndElement); return ret; } bool KNSCore::EntryInternal::setEntryXML(QXmlStreamReader& reader) { if (reader.name() != QLatin1String("stuff")) { - qWarning() << "Parsing Entry from invalid XML"; + qCWarning(KNEWSTUFFCORE) << "Parsing Entry from invalid XML. Reader tag name was expected to be \"stuff\", but was found as:" << reader.name(); return false; } d->mCategory = reader.attributes().value(QStringLiteral("category")).toString(); while (!reader.atEnd()) { const auto token = readNextSkipComments(&reader); if (token == QXmlStreamReader::EndElement) break; else if (token != QXmlStreamReader::StartElement) continue; if (reader.name() == QLatin1String("name")) { // TODO maybe do something with the language attribute? QString lang = e.attribute("lang"); d->mName = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("author")) { const auto email = reader.attributes().value(QStringLiteral("email")); const auto jabber = reader.attributes().value(QStringLiteral("im")); const auto homepage = reader.attributes().value(QStringLiteral("homepage")); d->mAuthor.setName(readStringTrimmed(&reader)); d->mAuthor.setEmail(email.toString()); d->mAuthor.setJabber(jabber.toString()); d->mAuthor.setHomepage(homepage.toString()); } else if (reader.name() == QLatin1String("providerid")) { d->mProviderId = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("homepage")) { d->mHomepage = QUrl(reader.readElementText(QXmlStreamReader::SkipChildElements)); } else if (reader.name() == QLatin1String("licence")) { // krazy:exclude=spelling d->mLicense = readStringTrimmed(&reader); } else if (reader.name() == QLatin1String("summary")) { d->mSummary = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("changelog")) { d->mChangelog = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("version")) { d->mVersion = readStringTrimmed(&reader); } else if (reader.name() == QLatin1String("releasedate")) { d->mReleaseDate = QDate::fromString(readStringTrimmed(&reader), Qt::ISODate); } else if (reader.name() == QLatin1String("preview")) { // TODO support for all 6 image links d->mPreviewUrl[PreviewSmall1] = readStringTrimmed(&reader); } else if (reader.name() == QLatin1String("previewBig")) { d->mPreviewUrl[PreviewBig1] = readStringTrimmed(&reader); } else if (reader.name() == QLatin1String("payload")) { d->mPayload = readStringTrimmed(&reader); } else if (reader.name() == QLatin1String("rating")) { d->mRating = readInt(&reader); } else if (reader.name() == QLatin1String("downloads")) { d->mDownloadCount = readInt(&reader); } else if (reader.name() == QLatin1String("category")) { d->mCategory = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("signature")) { d->mSignature = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("checksum")) { d->mChecksum = reader.readElementText(QXmlStreamReader::SkipChildElements); } else if (reader.name() == QLatin1String("installedfile")) { d->mInstalledFiles.append(reader.readElementText(QXmlStreamReader::SkipChildElements)); } else if (reader.name() == QLatin1String("id")) { d->mUniqueId = reader.readElementText(QXmlStreamReader::SkipChildElements); + } else if (reader.name() == QLatin1String("tags")) { + d->mTags = reader.readElementText(QXmlStreamReader::SkipChildElements).split(QChar(',')); } else if (reader.name() == QLatin1String("status")) { const auto statusText = readText(&reader); if (statusText == QLatin1String("installed")) { qCDebug(KNEWSTUFFCORE) << "Found an installed entry in registry"; d->mStatus = KNS3::Entry::Installed; } else if (statusText == QLatin1String("updateable")) { d->mStatus = KNS3::Entry::Updateable; } if (reader.tokenType() == QXmlStreamReader::Characters) readNextSkipComments(&reader); } - Q_ASSERT(reader.tokenType() == QXmlStreamReader::EndElement); + Q_ASSERT_X(reader.tokenType() == QXmlStreamReader::EndElement, Q_FUNC_INFO, QString("token name was %1 and the type was %2").arg(reader.name().toString()).arg(reader.tokenString()).toLocal8Bit().data()); } // Validation if (d->mName.isEmpty()) { qWarning() << "Entry: no name given"; return false; } if (d->mUniqueId.isEmpty()) { if (!d->mPayload.isEmpty()) { d->mUniqueId = d->mPayload; } else { d->mUniqueId = d->mName; } } if (d->mPayload.isEmpty()) { qWarning() << "Entry: no payload URL given for: " << d->mName << " - " << d->mUniqueId; return false; } return true; } bool KNSCore::EntryInternal::setEntryXML(const QDomElement &xmldata) { if (xmldata.tagName() != QLatin1String("stuff")) { qWarning() << "Parsing Entry from invalid XML"; return false; } d->mCategory = xmldata.attribute(QStringLiteral("category")); QDomNode n; for (n = xmldata.firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement e = n.toElement(); if (e.tagName() == QLatin1String("name")) { // TODO maybe do something with the language attribute? QString lang = e.attribute("lang"); d->mName = e.text().trimmed(); } else if (e.tagName() == QLatin1String("author")) { QString email = e.attribute(QStringLiteral("email")); QString jabber = e.attribute(QStringLiteral("im")); QString homepage = e.attribute(QStringLiteral("homepage")); d->mAuthor.setName(e.text().trimmed()); d->mAuthor.setEmail(email); d->mAuthor.setJabber(jabber); d->mAuthor.setHomepage(homepage); } else if (e.tagName() == QLatin1String("providerid")) { d->mProviderId = e.text(); } else if (e.tagName() == QLatin1String("homepage")) { d->mHomepage = QUrl(e.text()); } else if (e.tagName() == QLatin1String("licence")) { // krazy:exclude=spelling d->mLicense = e.text().trimmed(); } else if (e.tagName() == QLatin1String("summary")) { d->mSummary = e.text(); } else if (e.tagName() == QLatin1String("changelog")) { d->mChangelog = e.text(); } else if (e.tagName() == QLatin1String("version")) { d->mVersion = e.text().trimmed(); } else if (e.tagName() == QLatin1String("releasedate")) { d->mReleaseDate = QDate::fromString(e.text().trimmed(), Qt::ISODate); } else if (e.tagName() == QLatin1String("preview")) { // TODO support for all 6 image links d->mPreviewUrl[PreviewSmall1] = e.text().trimmed(); } else if (e.tagName() == QLatin1String("previewBig")) { d->mPreviewUrl[PreviewBig1] = e.text().trimmed(); } else if (e.tagName() == QLatin1String("payload")) { d->mPayload = e.text().trimmed(); } else if (e.tagName() == QLatin1String("rating")) { d->mRating = e.text().toInt(); } else if (e.tagName() == QLatin1String("downloads")) { d->mDownloadCount = e.text().toInt(); } else if (e.tagName() == QLatin1String("category")) { d->mCategory = e.text(); } else if (e.tagName() == QLatin1String("signature")) { d->mSignature = e.text(); } else if (e.tagName() == QLatin1String("checksum")) { d->mChecksum = e.text(); } else if (e.tagName() == QLatin1String("installedfile")) { d->mInstalledFiles.append(e.text()); } else if (e.tagName() == QLatin1String("id")) { d->mUniqueId = e.text(); + } else if (e.tagName() == QLatin1String("tags")) { + d->mTags = e.text().split(QChar(',')); } else if (e.tagName() == QLatin1String("status")) { QString statusText = e.text(); if (statusText == QLatin1String("installed")) { qCDebug(KNEWSTUFFCORE) << "Found an installed entry in registry"; d->mStatus = KNS3::Entry::Installed; } else if (statusText == QLatin1String("updateable")) { d->mStatus = KNS3::Entry::Updateable; } } } // Validation if (d->mName.isEmpty()) { qWarning() << "Entry: no name given"; return false; } if (d->mUniqueId.isEmpty()) { if (!d->mPayload.isEmpty()) { d->mUniqueId = d->mPayload; } else { d->mUniqueId = d->mName; } } if (d->mPayload.isEmpty()) { qWarning() << "Entry: no payload URL given for: " << d->mName << " - " << d->mUniqueId; return false; } return true; } /** * get the xml string for the entry */ QDomElement KNSCore::EntryInternal::entryXML() const { Q_ASSERT(!d->mUniqueId.isEmpty()); Q_ASSERT(!d->mProviderId.isEmpty()); QDomDocument doc; QDomElement el = doc.createElement(QStringLiteral("stuff")); el.setAttribute(QStringLiteral("category"), d->mCategory); QString name = d->mName; QDomElement e; e = addElement(doc, el, QStringLiteral("name"), name); // todo: add language attribute (void)addElement(doc, el, QStringLiteral("providerid"), d->mProviderId); QDomElement author = addElement(doc, el, QStringLiteral("author"), d->mAuthor.name()); if (!d->mAuthor.email().isEmpty()) { author.setAttribute(QStringLiteral("email"), d->mAuthor.email()); } if (!d->mAuthor.homepage().isEmpty()) { author.setAttribute(QStringLiteral("homepage"), d->mAuthor.homepage()); } if (!d->mAuthor.jabber().isEmpty()) { author.setAttribute(QStringLiteral("im"), d->mAuthor.jabber()); } // FIXME: 'jabber' or 'im'? consult with kopete guys... addElement(doc, el, QStringLiteral("homepage"), d->mHomepage.url()); (void)addElement(doc, el, QStringLiteral("licence"), d->mLicense); // krazy:exclude=spelling (void)addElement(doc, el, QStringLiteral("version"), d->mVersion); if ((d->mRating > 0) || (d->mDownloadCount > 0)) { (void)addElement(doc, el, QStringLiteral("rating"), QString::number(d->mRating)); (void)addElement(doc, el, QStringLiteral("downloads"), QString::number(d->mDownloadCount)); } if (!d->mSignature.isEmpty()) { (void)addElement(doc, el, QStringLiteral("signature"), d->mSignature); } if (!d->mChecksum.isEmpty()) { (void)addElement(doc, el, QStringLiteral("checksum"), d->mChecksum); } foreach (const QString &file, d->mInstalledFiles) { (void)addElement(doc, el, QStringLiteral("installedfile"), file); } if (!d->mUniqueId.isEmpty()) { addElement(doc, el, QStringLiteral("id"), d->mUniqueId); } (void)addElement(doc, el, QStringLiteral("releasedate"), d->mReleaseDate.toString(Qt::ISODate)); e = addElement(doc, el, QStringLiteral("summary"), d->mSummary); e = addElement(doc, el, QStringLiteral("changelog"), d->mChangelog); e = addElement(doc, el, QStringLiteral("preview"), d->mPreviewUrl[PreviewSmall1]); e = addElement(doc, el, QStringLiteral("previewBig"), d->mPreviewUrl[PreviewBig1]); e = addElement(doc, el, QStringLiteral("payload"), d->mPayload); + e = addElement(doc, el, QStringLiteral("tags"), d->mTags.join(QChar(','))); if (d->mStatus == KNS3::Entry::Installed) { (void)addElement(doc, el, QStringLiteral("status"), QStringLiteral("installed")); } if (d->mStatus == KNS3::Entry::Updateable) { (void)addElement(doc, el, QStringLiteral("status"), QStringLiteral("updateable")); } return el; } KNSCore::EntryInternal EntryInternal::fromEntry(const KNS3::Entry &entry) { return entry.d->e; } QString KNSCore::replaceBBCode(const QString &unformattedText) { QString text(unformattedText); text.replace(QLatin1String("[b]"), QLatin1String("")); text.replace(QLatin1String("[/b]"), QLatin1String("")); text.replace(QLatin1String("[i]"), QLatin1String("")); text.replace(QLatin1String("[/i]"), QLatin1String("")); text.replace(QLatin1String("[u]"), QLatin1String("")); text.replace(QLatin1String("[/u]"), QLatin1String("")); text.replace(QLatin1String("\\\""), QLatin1String("\"")); text.replace(QLatin1String("\\\'"), QLatin1String("\'")); text.replace(QLatin1String("[li]"), QLatin1String("* ")); // TODO: better replacement for list elements? text.remove(QStringLiteral("[/li]")); text.remove(QStringLiteral("[url]")); text.remove(QStringLiteral("[/url]")); return text; } diff --git a/src/core/entryinternal.h b/src/core/entryinternal.h index c69dd7e4..1b01401b 100644 --- a/src/core/entryinternal.h +++ b/src/core/entryinternal.h @@ -1,555 +1,572 @@ /* knewstuff3/entry.h. Copyright (c) 2002 Cornelius Schumacher Copyright (c) 2003 - 2007 Josef Spillner Copyright (c) 2009 Jeremy Whiting 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_ENTRYINTERNAL_P_H #define KNEWSTUFF3_ENTRYINTERNAL_P_H #include #include #include #include #include "author.h" // This include only exists for the KNS3::Entry::Status enum // TODO Move the KNS3::Entry::Status enum to Core for KF6 #include "entry.h" #include "knewstuffcore_export.h" class QXmlStreamReader; namespace KNSCore { static const int PreviewWidth = 96; static const int PreviewHeight = 72; /** function to remove bb code formatting that opendesktop sends */ KNEWSTUFFCORE_EXPORT QString replaceBBCode(const QString &unformattedText); /** * @short KNewStuff data entry container. * * This class provides accessor methods to the data objects * as used by KNewStuff. * * @author Cornelius Schumacher (schumacher@kde.org) * \par Maintainer: * Jeremy Whiting (jpwhiting@kde.org) */ class KNEWSTUFFCORE_EXPORT EntryInternal { public: typedef QList List; /** * Source of the entry, A entry's data is coming from either cache, or an online provider * this helps the engine know which data to use when merging cached entries with online * entry data */ enum Source { Cache, Online, Registry }; enum PreviewType { PreviewSmall1, PreviewSmall2, PreviewSmall3, PreviewBig1, PreviewBig2, PreviewBig3 }; struct DownloadLinkInformation { QString name; QString priceAmount; QString distributionType; QString descriptionLink; int id; bool isDownloadtypeLink; quint64 size = 0; + QStringList tags; }; /** * Constructor. */ EntryInternal(); EntryInternal(const EntryInternal &other); EntryInternal &operator=(const EntryInternal &other); bool operator==(const EntryInternal &other) const; bool operator<(const EntryInternal &other) const; /** * Destructor. */ ~EntryInternal(); bool isValid() const; /** * Sets the name for this data object. */ void setName(const QString &name); /** * Retrieve the name of the data object. * * @return object name (potentially translated) */ QString name() const; /** * Set the object's unique ID. This must be unique to the provider. * * @param id The unique ID of this entry as unique to this provider * @see KNSCore::Provider */ void setUniqueId(const QString &id); /** * Get the object's unique ID. This will be unique to the provider. * This is not intended as user-facing information - though it can * be useful for certain purposes, this is supposed to only be used * for keeping track of the entry. * * @return The unique ID of this entry */ QString uniqueId() const; /** * Sets the data category, e.g. "KWin Scripts" or "Plasma Theme". */ void setCategory(const QString &category); /** * Retrieve the category of the data object. This is the category's * name or ID (as opposed to displayName). * * @see KNSCore::Provider::CategoryMetadata * @see KNSCore::Engine::categories() * @return object category */ QString category() const; /** * Set a link to a website containing information about this entry * * @param page The URL representing the entry's website */ void setHomepage(const QUrl &page); /** * A link to a website containing information about this entry * * @return The URL representing the entry's website */ QUrl homepage() const; /** * Sets the author of the object. */ void setAuthor(const Author &author); /** * Retrieve the author of the object. * * @return object author */ Author author() const; /** * Sets the license (abbreviation) applicable to the object. */ void setLicense(const QString &license); /** * Retrieve the license name of the object. * * @return object license */ QString license() const; /** * Sets a description (which can potentially be very long) */ void setSummary(const QString &summary); /** * Retrieve a short description of what the object is all about (should be very short) * * @return object license */ QString shortSummary() const; /** * Sets a short description of what the object is all about (should be very short) */ void setShortSummary(const QString &summary); /** * Retrieve a (potentially very long) description of the object. * * @return object description */ QString summary() const; /** * The user written changelog */ void setChangelog(const QString &changelog); QString changelog() const; /** * Sets the version number. */ void setVersion(const QString &version); /** * Retrieve the version string of the object. * * @return object version */ QString version() const; /** * Sets the release date. */ void setReleaseDate(const QDate &releasedate); /** * Retrieve the date of the object's publication. * * @return object release date */ QDate releaseDate() const; /** * Sets the version number that is available as update. */ void setUpdateVersion(const QString &version); /** * Retrieve the version string of the object that is available as update. * * @return object version */ QString updateVersion() const; /** * Sets the release date that is available as update. */ void setUpdateReleaseDate(const QDate &releasedate); /** * Retrieve the date of the newer version that is available as update. * * @return object release date */ QDate updateReleaseDate() const; /** * Sets the object's file. */ void setPayload(const QString &url); /** * Retrieve the file name of the object. * * @return object filename */ QString payload() const; /** * Sets the object's preview file, if available. This should be a * picture file. */ void setPreviewUrl(const QString &url, PreviewType type = PreviewSmall1); /** * Retrieve the file name of an image containing a preview of the object. * * @return object preview filename */ QString previewUrl(PreviewType type = PreviewSmall1) const; /** * This will not be loaded automatically, instead use Engine to load the actual images. */ QImage previewImage(PreviewType type = PreviewSmall1) const; void setPreviewImage(const QImage &image, PreviewType type = PreviewSmall1); /** * Set the files that have been installed by the install command. * @param files local file names */ void setInstalledFiles(const QStringList &files); /** * Retrieve the locally installed files. * @return file names */ QStringList installedFiles() const; /** * Set the files that have been uninstalled by the uninstall command. * @param files local file names * @since 4.1 */ void setUnInstalledFiles(const QStringList &files); /** * Retrieve the locally uninstalled files. * @return file names * @since 4.1 */ QStringList uninstalledFiles() const; /** * Sets the rating between 0 (worst) and 100 (best). * * @internal */ void setRating(int rating); /** * Retrieve the rating for the object, which has been determined by its * users and thus might change over time. * * @return object rating */ int rating() const; /** * Sets the number of comments in the asset * * @internal */ void setNumberOfComments(int comments); /** * @returns the number of comments against the asset */ int numberOfComments() const; /** * Sets the number of downloads. * * @internal */ void setDownloadCount(int downloads); /** * Retrieve the download count for the object, which has been determined * by its hosting sites and thus might change over time. * * @return object download count */ int downloadCount() const; /** * How many people have marked themselves as fans of this entry * * @return The number of fans this entry has * @see KNSCore::Engine::becomeFan(const EntryInternal& entry) */ int numberFans() const; /** * Sets how many people are fans. * Note: This is purely informational. To become a fan, call the * KNSCore::Engine::becomeFan function. * * @param fans The number of fans this entry has * @see KNSCore::Engine::becomeFan(const EntryInternal& entry) */ void setNumberFans(int fans); /** * The number of entries in the knowledgebase for this entry * @return The number of knowledgebase entries */ int numberKnowledgebaseEntries() const; /** * Set the number of knowledgebase entries for this entry * @param num The number of entries */ void setNumberKnowledgebaseEntries(int num); /** * The link for the knowledgebase for this entry. * @return A string version of the URL for the knowledgebase */ QString knowledgebaseLink() const; /** * Set the link for the knowledgebase. * Note: This is not checked for validity, the caller must do this. * @param link The string version of the URL for the knowledgebase */ void setKnowledgebaseLink(const QString &link); /** * The number of available download options for this entry * @return The number of download options */ int downloadLinkCount() const; /** * A list of downloadable data for this entry * @return The list of download options * @see DownloadLinkInformation */ QList downloadLinkInformationList() const; /** * Add a new download option to this entry * @param info The new download option */ void appendDownloadLinkInformation(const DownloadLinkInformation &info); /** * Remove all download options from this entry */ void clearDownloadLinkInformation(); /** * A string representing the URL for a website where the user can donate * to the author of this entry * @return The string version of the URL for the entry's donation website */ QString donationLink() const; /** * Set a string representation of the URL for the donation website for this entry. * Note: This is not checked for validity, the caller must do this. * @param link String version of the URL for the entry's donation website */ void setDonationLink(const QString &link); + /** + * The set of tags assigned specifically to this content item. This does not include + * tags for the download links. To get those, you must concatenate the lists yourself. + * @see downloadLinkInformationList() + * @see DownloadLinkInformation + * @see Engine::setTagFilter(QStringList) + * @since 5.51 + */ + QStringList tags() const; + /** + * Set the tags for the content item. + * @param tags A string list containing the tags for this entry + * @since 5.51 + */ + void setTags(const QStringList &tags); + /** The id of the provider this entry belongs to */ QString providerId() const; void setProviderId(const QString &id); /** The source of this entry can be Cache, Registry or Online - @see source */ void setSource(Source source); Source source() const; /** * set the xml for the entry * parses the xml and sets the private members accordingly * used to deserialize data loaded from provider * * @param xmldata string to load xml data from * * @returns whether or not setting the values was successful */ bool setEntryXML(QXmlStreamReader &reader); /** * set the xml for the entry * parses the xml and sets the private members accordingly * used to deserialize data loaded from provider * * @param xmldata string to load xml data from * * @returns whether or not setting the values was successful * * @deprecated since 5.36, use setEntryXML(QXmlStreamReader&) instead */ KNEWSTUFFCORE_DEPRECATED bool setEntryXML(const QDomElement &xmldata); /** * get the xml string for the entry */ QDomElement entryXML() const; /** * Returns the checksum for the entry. * * If an empty string is returned, no checksum was assigned. * * @return Checksum of this entry */ //QString checksum() const; /** * Sets the checksum of the entry. This will be a string representation * of an MD5 sum of the entry's selected payload file. * * @ref checksum Checksum for the entry */ //void setChecksum(const QString& checksum); /** * Returns the signature for the entry. * * If an empty string is returned, no signature was assigned. * * @return Signature of this entry */ //QString signature() const; /** * Sets the signature of the entry. This will be a digital signature * in OpenPGP-compliant format. * * @ref signature Signature for the entry */ //void setSignature(const QString& signature); /** * Sets the entry's status. If no status is set, the default will be * \ref Invalid. * * Note that while this enum is currently found in KNS3::Entry, * it will be moved to this class once the binary compatibility * lock is lifted for Frameworks 6. For now, you should read any * reference to the KNS3::Entry::Status enumerator as KNSCore::Entry::Status * * @param status New status of the entry */ void setStatus(KNS3::Entry::Status status); /** * Retrieves the entry's status. * * @return Current status of the entry */ KNS3::Entry::Status status() const; //void setIdNumber(int number); //int idNumber() const; static KNSCore::EntryInternal fromEntry(const KNS3::Entry &entry); private: class Private; QExplicitlySharedDataPointer d; }; inline uint qHash(const KNSCore::EntryInternal &entry) { return qHash(entry.uniqueId()); } } Q_DECLARE_METATYPE(KNSCore::EntryInternal::List) #endif diff --git a/src/core/provider.cpp b/src/core/provider.cpp index 937199a8..1b896043 100644 --- a/src/core/provider.cpp +++ b/src/core/provider.cpp @@ -1,71 +1,119 @@ /* knewstuff3/provider.cpp Copyright (c) 2002 Cornelius Schumacher Copyright (c) 2003 - 2007 Josef Spillner Copyright (c) 2009 Jeremy Whiting 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 "provider.h" #include "xmlloader.h" #include namespace KNSCore { +// BCI: Add a real d-pointer +class ProviderPrivate { +public: + QStringList tagFilter; + QStringList downloadTagFilter; +}; +typedef QHash ProviderPrivateHash; +Q_GLOBAL_STATIC(ProviderPrivateHash, d_func) + +static ProviderPrivate *d(const Provider *provider) +{ + ProviderPrivate *ret = d_func()->value(provider); + if (!ret) { + ret = new ProviderPrivate; + d_func()->insert(provider, ret); + } + return ret; +} + +static void delete_d(const Provider *provider) +{ + if (auto d = d_func()) { + delete d->take(provider); + } +} + QString Provider::SearchRequest::hashForRequest() const { return QString(QString::number((int)sortMode) + QLatin1Char(',') + searchTerm + QLatin1Char(',') + categories.join(QLatin1Char('-')) + QLatin1Char(',') + QString::number(page) + QLatin1Char(',') + QString::number(pageSize)); } Provider::Provider() {} Provider::~Provider() -{} +{ + delete_d(this); +} QString Provider::name() const { return mName; } QUrl Provider::icon() const { return mIcon; } +void Provider::setTagFilter(const QStringList &tagFilter) +{ + d(this)->tagFilter = tagFilter; +} + +QStringList Provider::tagFilter() const +{ + return d(this)->tagFilter; +} + +void Provider::setDownloadTagFilter(const QStringList &downloadTagFilter) +{ + d(this)->downloadTagFilter = downloadTagFilter; +} + +QStringList Provider::downloadTagFilter() const +{ + return d(this)->downloadTagFilter; +} + QDebug operator<<(QDebug dbg, const Provider::SearchRequest & search) { QDebugStateSaver saver(dbg); dbg.nospace(); dbg << "Provider::SearchRequest("; dbg << "searchTerm: " << search.searchTerm << ','; dbg << "categories: " << search.categories << ','; dbg << "filter: " << search.filter << ','; dbg << "page: " << search.page << ','; dbg << "pageSize: " << search.pageSize; dbg << ')'; return dbg; } } diff --git a/src/core/provider.h b/src/core/provider.h index 0a17b49e..8c354daf 100644 --- a/src/core/provider.h +++ b/src/core/provider.h @@ -1,190 +1,219 @@ /* knewstuff3/provider.h This file is part of KNewStuff2. Copyright (c) 2009 Jeremy Whiting 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_PROVIDER_P_H #define KNEWSTUFF3_PROVIDER_P_H #include #include #include #include "entryinternal.h" #include "knewstuffcore_export.h" class KJob; namespace KNSCore { /** * @short KNewStuff Base Provider class. * * This class provides accessors for the provider object. * It should not be used directly by the application. * This class is the base class and will be instantiated for * static website providers. * * @author Jeremy Whiting * * @internal */ class KNEWSTUFFCORE_EXPORT Provider: public QObject { Q_OBJECT public: typedef QList List; enum SortMode { Newest, Alphabetical, Rating, Downloads, }; Q_ENUM(SortMode) enum Filter { None, Installed, Updates, ExactEntryId }; Q_ENUM(Filter) /** * used to keep track of a search */ struct SearchRequest { SortMode sortMode; Filter filter; QString searchTerm; QStringList categories; int page; int pageSize; SearchRequest(SortMode sortMode_ = Newest, Filter filter_ = None, const QString &searchTerm_ = QString(), const QStringList &categories_ = QStringList(), int page_ = -1, int pageSize_ = 20) : sortMode(sortMode_), filter(filter_), searchTerm(searchTerm_), categories(categories_), page(page_), pageSize(pageSize_) {} QString hashForRequest() const; }; /** * Describes a category: id/name/disaplayName */ struct CategoryMetadata { QString id; QString name; QString displayName; }; /** * Constructor. */ Provider(); /** * Destructor. */ virtual ~Provider(); /** * A unique Id for this provider (the url in most cases) */ virtual QString id() const = 0; /** * Set the provider data xml, to initialize the provider. * The Provider needs to have it's ID set in this function and cannot change it from there on. */ virtual bool setProviderXML(const QDomElement &xmldata) = 0; virtual bool isInitialized() const = 0; virtual void setCachedEntries(const KNSCore::EntryInternal::List &cachedEntries) = 0; /** * Retrieves the common name of the provider. * * @return provider name */ virtual QString name() const; /** * Retrieves the icon URL for this provider. * * @return icon URL */ virtual QUrl icon() const; // FIXME use QIcon::fromTheme or pixmap? /** * load the given search and return given page * @param sortMode string to select the order in which the results are presented * @param searchstring string to search with * @param page page number to load * * Note: the engine connects to loadingFinished() signal to get the result */ virtual void loadEntries(const KNSCore::Provider::SearchRequest &request) = 0; virtual void loadEntryDetails(const KNSCore::EntryInternal &) {} virtual void loadPayloadLink(const EntryInternal &entry, int linkId) = 0; virtual bool userCanVote() { return false; } virtual void vote(const EntryInternal &entry, uint rating) { Q_UNUSED(entry) Q_UNUSED(rating) } virtual bool userCanBecomeFan() { return false; } virtual void becomeFan(const EntryInternal &entry) { Q_UNUSED(entry) } + /** + * Set the tag filter used for entries by this provider + * @param tagFilter The new list of filters + * @see Engine::setTagFilter(QStringList) + * @since 5.51 + */ + void setTagFilter(const QStringList &tagFilter); + /** + * The tag filter used for downloads by this provider + * @return The list of filters + * @see Engine::setTagFilter(QStringList) + * @since 5.51 + */ + QStringList tagFilter() const; + /** + * Set the tag filter used for download items by this provider + * @param downloadTagFilter The new list of filters + * @see Engine::setDownloadTagFilter(QStringList) + * @since 5.51 + */ + void setDownloadTagFilter(const QStringList &downloadTagFilter); + /** + * The tag filter used for downloads by this provider + * @return The list of filters + * @see Engine::setDownloadTagFilter(QStringList) + * @since 5.51 + */ + QStringList downloadTagFilter() const; + Q_SIGNALS: void providerInitialized(KNSCore::Provider *); void loadingFinished(const KNSCore::Provider::SearchRequest &, const KNSCore::EntryInternal::List &) const; void loadingFailed(const KNSCore::Provider::SearchRequest &); void entryDetailsLoaded(const KNSCore::EntryInternal &); void payloadLinkLoaded(const KNSCore::EntryInternal &); void signalInformation(const QString &) const; void signalError(const QString &) const; void categoriesMetadataLoded(const QList &categories); protected: QString mName; QUrl mIcon; private: Q_DISABLE_COPY(Provider) }; KNEWSTUFFCORE_EXPORT QDebug operator<<(QDebug, const Provider::SearchRequest &); } #endif diff --git a/src/core/tagsfilterchecker.cpp b/src/core/tagsfilterchecker.cpp new file mode 100644 index 00000000..85a7fbc0 --- /dev/null +++ b/src/core/tagsfilterchecker.cpp @@ -0,0 +1,168 @@ +/* + Copyright (c) 2018 Dan Leinir Turthra Jensen + + 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 "tagsfilterchecker.h" + +#include + +#include + +namespace KNSCore +{ + +class TagsFilterChecker::Private +{ +public: + Private() {} + ~Private() + { + qDeleteAll(validators); + } + class Validator; + // If people start using a LOT of validators (>20ish), we can always change it, but + // for now it seems reasonable that QMap is better than QHash here... + QMap validators; + + class Validator + { + public: + Validator(const QString &tag, const QString &value) + { + m_tag = tag; + if (!value.isNull()) { + m_acceptedValues << value; + } + } + virtual ~Validator() {}; + virtual bool filterAccepts(const QString &tag, const QString &value) = 0; + protected: + friend class TagsFilterChecker::Private; + QString m_tag; + QStringList m_acceptedValues; + }; + + // Will only accept entries which have one of the accepted values set for the tag key + class EqualityValidator : public Validator + { + public: + EqualityValidator(const QString &tag, const QString &value) + : Validator(tag, value) + {} + ~EqualityValidator() override {} + bool filterAccepts(const QString &tag, const QString &value) override + { + bool result = true; + if (tag == m_tag && !m_acceptedValues.contains(value)) { + result = false; + } + return result; + } + }; + + // Will only accept entries which have none of the values set for the tag key + class InequalityValidator : public Validator + { + public: + InequalityValidator(const QString &tag, const QString &value) + : Validator(tag, value) + {} + ~InequalityValidator() override {} + bool filterAccepts(const QString &tag, const QString &value) override + { + bool result = true; + if (tag == m_tag && m_acceptedValues.contains(value)) { + result = false; + } + return result; + } + }; + + void addValidator(const QString &filter) + { + int pos = 0; + if ((pos = filter.indexOf(QStringLiteral("=="))) > -1) { + QString tag = filter.left(pos); + QString value = filter.mid(tag.length() + 2); + Validator *val = validators.value(tag, nullptr); + if (!val) { + val = new EqualityValidator(tag, QString()); + validators.insert(tag, val); + } + val->m_acceptedValues << value; + qCDebug(KNEWSTUFFCORE) << "Created EqualityValidator for tag" << tag << "with value" << value; + } else if ((pos = filter.indexOf(QStringLiteral("!="))) > -1) { + QString tag = filter.left(pos); + QString value = filter.mid(tag.length() + 2); + Validator *val = validators.value(tag, nullptr); + if (!val) { + val = new InequalityValidator(tag, QString()); + validators.insert(tag, val); + } + val->m_acceptedValues << value; + qCDebug(KNEWSTUFFCORE) << "Created InequalityValidator for tag" << tag << "with value" << value; + } else { + qCDebug(KNEWSTUFFCORE) << "Critical error attempting to create tag filter validators. The filter is defined as" << filter << "which is not in the accepted formats key==value or key!=value"; + } + } +}; + +TagsFilterChecker::TagsFilterChecker(const QStringList &tagFilter) + : d(new TagsFilterChecker::Private) +{ + for (const QString &filter : tagFilter) { + d->addValidator(filter); + } +} + +TagsFilterChecker::~TagsFilterChecker() +{ + delete d; +} + +bool TagsFilterChecker::filterAccepts(const QStringList &tags) +{ + // if any tag in the content matches any of the tag filters, skip this entry + qCDebug(KNEWSTUFFCORE) << "Checking tags list" << tags << "against validators with keys" << d->validators.keys(); + for (const QString &tag : tags) { + if (tag.isEmpty()) { + // This happens when you do a split on an empty string (not an empty list, a list with one empty element... because reasons). + // Also handy for other things, i guess, though, so let's just catch it here. + continue; + } + QStringList current = tag.split(QLatin1Char('=')); + if (current.length() > 2) { + qCDebug(KNEWSTUFFCORE) << "Critical error attempting to filter tags. Entry has tag defined as" << tag << "which is not in the format \"key=value\" or \"key\"."; + return false; + } else if (current.length() == 1) { + // If the tag is defined simply as a key, we give it the value "1", just to make our filtering work simpler + current << QStringLiteral("1"); + } + QMap::const_iterator i = d->validators.constBegin(); + while (i != d->validators.constEnd()) { + if (!i.value()->filterAccepts(current.at(0), current.at(1))) { + return false; + } + ++i; + } + } + // If we have arrived here, nothing has filtered the entry + // out (by being either incorrectly tagged or a filter rejecting + // it), and consequently it is an acceptable entry. + return true; +} + +} diff --git a/src/core/tagsfilterchecker.h b/src/core/tagsfilterchecker.h new file mode 100644 index 00000000..676526c2 --- /dev/null +++ b/src/core/tagsfilterchecker.h @@ -0,0 +1,83 @@ +/* + Copyright (c) 2018 Dan Leinir Turthra Jensen + + 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 KNSCORE_TAGSFILTERCHECKER_H +#define KNSCORE_TAGSFILTERCHECKER_H + +#include +#include "knewstuffcore_export.h" + +namespace KNSCore { + +/** + * @brief Apply simple filtering logic to a list of tags + * + * == Examples of specifying tag filters == + * Value for tag "tagname" must be exactly "tagdata": + * tagname==tagdata + * + * Value for tag "tagname" must be different from "tagdata": + * tagname!=tagdata + * + * == Tag filter list == + * A tag filter list is a string list of filters as shown above, and a combination + * of which might look like: + * + * - ghns_exclude!=1 + * - data##mimetype==application/cbr+zip + * - data##mimetype==application/cbr+rar + * + * which would filter out anything which has ghns_exclude set to 1, and + * anything where the value of data##mimetype does not equal either + * "application/cbr+zip" or "application/cbr+rar". + * Notice in particular the two data##mimetype entries. Use this + * for when a tag may have multiple values. + * + * The value does not current suppport wildcards. The list should be considered + * a binary AND operation (that is, all filter entries must match for the data + * entry to be included in the return data) + * @since 5.51 + */ +class KNEWSTUFFCORE_EXPORT TagsFilterChecker { +public: + /** + * Constructs an instance of the tags filter checker, prepopulated + * with the list of tag filters in the tagFilter parameter. + * + * @param tagFilter The list of tag filters + * @since 5.51 + */ + TagsFilterChecker(const QStringList &tagFilter); + ~TagsFilterChecker(); + + /** + * Check whether the filter list accepts the passed list of tags + * + * @param tags A list of tags in the form of key=value strings + * @return True if the filter accepts the list, false if not + * @since 5.51 + */ + bool filterAccepts(const QStringList &tags); + +private: + class Private; + Private *d; +}; + +} + +#endif//KNSCORE_TAGSFILTERCHECKER_H diff --git a/src/staticxml/staticxmlprovider.cpp b/src/staticxml/staticxmlprovider.cpp index 20101a5e..5037fc18 100644 --- a/src/staticxml/staticxmlprovider.cpp +++ b/src/staticxml/staticxmlprovider.cpp @@ -1,298 +1,319 @@ /* knewstuff3/provider.cpp Copyright (c) 2002 Cornelius Schumacher Copyright (c) 2003 - 2007 Josef Spillner Copyright (c) 2009 Jeremy Whiting Copyright (C) 2009-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 "staticxmlprovider_p.h" #include "xmlloader.h" #include #include #include +#include namespace KNSCore { StaticXmlProvider::StaticXmlProvider() : mInitialized(false) { } QString StaticXmlProvider::id() const { return mId; } bool StaticXmlProvider::setProviderXML(const QDomElement &xmldata) { qCDebug(KNEWSTUFFCORE) << "setting provider xml"; if (xmldata.tagName() != QLatin1String("provider")) { return false; } mUploadUrl = QUrl(xmldata.attribute(QStringLiteral("uploadurl"))); mNoUploadUrl = QUrl(xmldata.attribute(QStringLiteral("nouploadurl"))); QString url = xmldata.attribute(QStringLiteral("downloadurl")); if (!url.isEmpty()) { mDownloadUrls.insert(QString(), QUrl(url)); } url = xmldata.attribute(QStringLiteral("downloadurl-latest")); if (!url.isEmpty()) { mDownloadUrls.insert(QStringLiteral("latest"), QUrl(url)); } url = xmldata.attribute(QStringLiteral("downloadurl-score")); if (!url.isEmpty()) { mDownloadUrls.insert(QStringLiteral("score"), QUrl(url)); } url = xmldata.attribute(QStringLiteral("downloadurl-downloads")); if (!url.isEmpty()) { mDownloadUrls.insert(QStringLiteral("downloads"), QUrl(url)); } // FIXME: this depends on freedesktop.org icon naming... introduce 'desktopicon'? QUrl iconurl(xmldata.attribute(QStringLiteral("icon"))); if (!iconurl.isValid()) { iconurl = QUrl::fromLocalFile(xmldata.attribute(QStringLiteral("icon"))); } mIcon = iconurl; QDomNode n; for (n = xmldata.firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement e = n.toElement(); if (e.tagName() == QLatin1String("title")) { //QString lang = e.attribute("lang"); mName = e.text().trimmed(); qCDebug(KNEWSTUFFCORE) << "add name for provider ("<< this << "): " << e.text(); } } // Validation if ((mNoUploadUrl.isValid()) && (mUploadUrl.isValid())) { qWarning() << "StaticXmlProvider: both uploadurl and nouploadurl given"; return false; } if ((!mNoUploadUrl.isValid()) && (!mUploadUrl.isValid())) { qWarning() << "StaticXmlProvider: neither uploadurl nor nouploadurl given"; return false; } mId = mDownloadUrls[QString()].url(); if (mId.isEmpty()) { mId = mDownloadUrls[mDownloadUrls.begin().key()].url(); } QTimer::singleShot(0, this, SLOT(slotEmitProviderInitialized())); return true; } void StaticXmlProvider::slotEmitProviderInitialized() { mInitialized = true; emit providerInitialized(this); } bool StaticXmlProvider::isInitialized() const { return mInitialized; } void StaticXmlProvider::setCachedEntries(const KNSCore::EntryInternal::List &cachedEntries) { qCDebug(KNEWSTUFFCORE) << "Set cached entries " << cachedEntries.size(); mCachedEntries.append(cachedEntries); } void StaticXmlProvider::loadEntries(const KNSCore::Provider::SearchRequest &request) { mCurrentRequest = request; // static providers only have on page containing everything if (request.page > 0) { emit loadingFinished(request, EntryInternal::List()); return; } if (request.filter == Installed) { qCDebug(KNEWSTUFFCORE) << "Installed entries: " << mId << installedEntries().size(); emit loadingFinished(request, installedEntries()); return; } QUrl url = downloadUrl(request.sortMode); if (!url.isEmpty()) { // TODO first get the entries, then filter with searchString, finally emit the finished signal... // FIXME: don't creat an endless number of xmlloaders! XmlLoader *loader = new XmlLoader(this); connect(loader, &XmlLoader::signalLoaded, this, &StaticXmlProvider::slotFeedFileLoaded); connect(loader, &XmlLoader::signalFailed, this, &StaticXmlProvider::slotFeedFailed); loader->setProperty("filter", request.filter); loader->setProperty("searchTerm", request.searchTerm); mFeedLoaders.insert(request.sortMode, loader); loader->load(url); } else { emit loadingFailed(request); } } QUrl StaticXmlProvider::downloadUrl(SortMode mode) const { QUrl url; switch (mode) { case Rating: url = mDownloadUrls.value(QStringLiteral("score")); break; case Alphabetical: url = mDownloadUrls.value(QString()); break; case Newest: url = mDownloadUrls.value(QStringLiteral("latest")); break; case Downloads: url = mDownloadUrls.value(QStringLiteral("downloads")); break; } if (url.isEmpty()) { url = mDownloadUrls.value(QString()); } return url; } void StaticXmlProvider::slotFeedFileLoaded(const QDomDocument &doc) { XmlLoader *loader = qobject_cast(sender()); if (!loader) { qWarning() << "Loader not found!"; emit loadingFailed(mCurrentRequest); return; } // load all the entries from the domdocument given EntryInternal::List entries; QDomElement element; const Provider::Filter filter = loader->property("filter").value(); const QString searchTerm = loader->property("searchTerm").toString(); + TagsFilterChecker checker(tagFilter()); + TagsFilterChecker downloadschecker(downloadTagFilter()); element = doc.documentElement(); QDomElement n; for (n = element.firstChildElement(); !n.isNull(); n = n.nextSiblingElement()) { EntryInternal entry; entry.setEntryXML(n.toElement()); entry.setStatus(KNS3::Entry::Downloadable); entry.setProviderId(mId); int index = mCachedEntries.indexOf(entry); if (index >= 0) { EntryInternal cacheEntry = mCachedEntries.takeAt(index); // check if updateable if ((cacheEntry.status() == KNS3::Entry::Installed) && ((cacheEntry.version() != entry.version()) || (cacheEntry.releaseDate() != entry.releaseDate()))) { entry.setStatus(KNS3::Entry::Updateable); entry.setUpdateVersion(entry.version()); entry.setVersion(cacheEntry.version()); entry.setUpdateReleaseDate(entry.releaseDate()); entry.setReleaseDate(cacheEntry.releaseDate()); } else { entry.setStatus(cacheEntry.status()); } cacheEntry = entry; } - mCachedEntries.append(entry); - - if (searchIncludesEntry(entry)) { - switch(filter) { - case Installed: - //This is dealth with in loadEntries separately - Q_UNREACHABLE(); - case Updates: - if (entry.status() == KNS3::Entry::Updateable) { - entries << entry; + + if (checker.filterAccepts(entry.tags())) { + bool filterAcceptsDownloads = true; + if (entry.downloadCount() > 0) { + for (const KNSCore::EntryInternal::DownloadLinkInformation &dli : entry.downloadLinkInformationList()) { + if (downloadschecker.filterAccepts(dli.tags)) { + filterAcceptsDownloads = true; + break; } - break; - case ExactEntryId: - if (entry.uniqueId() == searchTerm) { - entries << entry; + } + } + if (filterAcceptsDownloads) { + mCachedEntries.append(entry); + + if (searchIncludesEntry(entry)) { + switch(filter) { + case Installed: + //This is dealth with in loadEntries separately + Q_UNREACHABLE(); + case Updates: + if (entry.status() == KNS3::Entry::Updateable) { + entries << entry; + } + break; + case ExactEntryId: + if (entry.uniqueId() == searchTerm) { + entries << entry; + } + break; + case None: + entries << entry; + break; } - break; - case None: - entries << entry; - break; + } + } else { + qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << entry.name() << "on download filter" << downloadTagFilter(); } + } else { + qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << entry.name() << "on entry filter" << tagFilter(); } } emit loadingFinished(mCurrentRequest, entries); } void StaticXmlProvider::slotFeedFailed() { emit loadingFailed(mCurrentRequest); } bool StaticXmlProvider::searchIncludesEntry(const KNSCore::EntryInternal &entry) const { if (mCurrentRequest.filter == Updates) { if (entry.status() != KNS3::Entry::Updateable) { return false; } } if (mCurrentRequest.searchTerm.isEmpty()) { return true; } QString search = mCurrentRequest.searchTerm; if (entry.name().contains(search, Qt::CaseInsensitive) || entry.summary().contains(search, Qt::CaseInsensitive) || entry.author().name().contains(search, Qt::CaseInsensitive) ) { return true; } return false; } void StaticXmlProvider::loadPayloadLink(const KNSCore::EntryInternal &entry, int) { qCDebug(KNEWSTUFFCORE) << "Payload: " << entry.payload(); emit payloadLinkLoaded(entry); } EntryInternal::List StaticXmlProvider::installedEntries() const { EntryInternal::List entries; foreach (const EntryInternal &entry, mCachedEntries) { if (entry.status() == KNS3::Entry::Installed || entry.status() == KNS3::Entry::Updateable) { entries.append(entry); } } return entries; } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3e81a462..d9170e73 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,31 +1,33 @@ include(ECMMarkAsTest) -find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test Widgets) # Widgets for KMoreTools +find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test Widgets Gui Quick) # Widgets for KMoreTools and Quick for the interactive KNS test + +configure_file(khotnewstuff_test.knsrc.in khotnewstuff_test.knsrc @ONLY) macro(knewstuff_executable_tests) foreach(_testname ${ARGN}) - add_executable(${_testname} ${_testname}.cpp ../src/knewstuff_debug.cpp) - target_link_libraries(${_testname} KF5::NewStuffCore KF5::NewStuff KF5::I18n Qt5::Xml Qt5::Test) + add_executable(${_testname} ${_testname}.cpp ../src/knewstuff_debug.cpp ../src/core/knewstuffcore_debug.cpp ../src/staticxml/staticxmlprovider.cpp) + target_link_libraries(${_testname} KF5::NewStuffCore KF5::NewStuff KF5::I18n Qt5::Xml Qt5::Test Qt5::Quick Qt5::Gui) target_compile_definitions(${_testname} PRIVATE - KNSSRCDIR="\\"${CMAKE_CURRENT_SOURCE_DIR}/\\"" - KNSBUILDDIR="\\"${CMAKE_CURRENT_BINARY_DIR}\\"") + KNSSRCDIR="${CMAKE_CURRENT_SOURCE_DIR}/" + KNSBUILDDIR="${CMAKE_CURRENT_BINARY_DIR}") endforeach() endmacro() knewstuff_executable_tests( khotnewstuff khotnewstuff_upload + khotnewstuff_test ) # FIXME: port to new API #knewstuff_executable_tests( -# knewstuff2_test # knewstuff2_download # knewstuff2_standard # knewstuff2_cache #) # KMoreTools: add_executable(kmoretoolstest_interactive kmoretools/kmoretoolstest_interactive.cpp ../src/knewstuff_debug.cpp) ecm_mark_as_test(kmoretoolstest_interactive) target_link_libraries(kmoretoolstest_interactive Qt5::Test KF5::NewStuff KF5::I18n Qt5::Widgets) diff --git a/tests/khotnewstuff_test-ui/main.qml b/tests/khotnewstuff_test-ui/main.qml new file mode 100644 index 00000000..49f77a86 --- /dev/null +++ b/tests/khotnewstuff_test-ui/main.qml @@ -0,0 +1,67 @@ +import QtQuick 2.7 +import org.kde.kirigami 2.4 as Kirigami + +Kirigami.ApplicationWindow { + id: root; + + globalDrawer: Kirigami.GlobalDrawer { + title: "KNewStuff Test" + titleIcon: "applications-development" + drawerOpen: true; + modal: false; + + actions: [ + Kirigami.Action { + text: "Run Engine test" + onTriggered: testObject.engineTest(); + iconName: "run-build" + }, + Kirigami.Action { + text: "Test entry download as well" + onTriggered: testObject.testAll = !testObject.testAll + iconName: typeof(testObject) !== "undefined" ? (testObject.testAll ? "checkmark" : "") : "" + }, + Kirigami.Action {}, + Kirigami.Action { + text: "Run Entry test" + onTriggered: testObject.entryTest(); + iconName: "run-build" + }, + Kirigami.Action { + text: "Run Provider test" + onTriggered: testObject.providerTest(); + iconName: "run-build" + } + ] + } + contextDrawer: Kirigami.ContextDrawer { + id: contextDrawer + } + + pageStack.initialPage: mainPageComponent + + Component { + id: mainPageComponent + Kirigami.ScrollablePage { + title: "Welcome" + ListView { + id: messageView; + model: testObject.messages(); + onCountChanged: { + messageView.currentIndex = messageView.count - 1; + } + delegate: Kirigami.BasicListItem { + id: listItem + + reserveSpaceForIcon: true + label: model.display + icon: model.whatsThis + + Accessible.role: Accessible.MenuItem + onClicked: {} + highlighted: focus && ListView.isCurrentItem + } + } + } + } +} diff --git a/tests/khotnewstuff_test-ui/main.qmlc b/tests/khotnewstuff_test-ui/main.qmlc new file mode 100644 index 00000000..0ca92268 Binary files /dev/null and b/tests/khotnewstuff_test-ui/main.qmlc differ diff --git a/tests/knewstuff2_test.knsrc b/tests/khotnewstuff_test.knsrc.in similarity index 67% rename from tests/knewstuff2_test.knsrc rename to tests/khotnewstuff_test.knsrc.in index 52dd316a..e4acbf78 100644 --- a/tests/knewstuff2_test.knsrc +++ b/tests/khotnewstuff_test.knsrc.in @@ -1,10 +1,9 @@ [KNewStuff2] -#ProvidersUrl=http://edu.kde.org/kalzium/molecules.xml -ProvidersUrl=http://new.kstuff.org/provider-kalzium.xml +ProvidersUrl=file://@CMAKE_CURRENT_SOURCE_DIR@/testdata/provider.xml LocalRegistryDir=/tmp/knewstuff2.metafiles TargetDir=knewstuff2_test # For more *.knsrc configuration file options, see doc/tutorial.txt. # For a list of providers, see kstuff/hotstuff/providers in KStuff SVN. diff --git a/tests/knewstuff2_test.cpp b/tests/knewstuff2_test.cpp deleted file mode 100644 index e8bc5cba..00000000 --- a/tests/knewstuff2_test.cpp +++ /dev/null @@ -1,278 +0,0 @@ -/* - This file is part of KNewStuff2. - Copyright (c) 2007 Josef Spillner - - 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 "knewstuff2_test.h" - -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include // for exit() -#include // for stdout - -KNewStuff2Test::KNewStuff2Test() - : QObject() -{ - m_engine = NULL; - m_testall = false; -} - -void KNewStuff2Test::setTestAll(bool testall) -{ - m_testall = testall; -} - -void KNewStuff2Test::entryTest() -{ - // qCDebug(KNEWSTUFF) << "-- test kns2 entry class"; - - QDomDocument doc; - QFile f(QString("%1/testdata/entry.xml").arg(KNSSRCDIR)); - if (!f.open(QIODevice::ReadOnly)) { - // qCDebug(KNEWSTUFF) << "Error loading entry file."; - quitTest(); - } - if (!doc.setContent(&f)) { - // qCDebug(KNEWSTUFF) << "Error parsing entry file."; - f.close(); - quitTest(); - } - f.close(); - - KNS::EntryHandler eh(doc.documentElement()); - KNS::Entry e = eh.entry(); - - // qCDebug(KNEWSTUFF) << "-- xml->entry test result: " << eh.isValid(); - - KNS::EntryHandler eh2(e); - QDomElement exml = eh2.entryXML(); - - // qCDebug(KNEWSTUFF) << "-- entry->xml test result: " << eh.isValid(); - - if (!eh.isValid()) { - quitTest(); - } else { - QTextStream out(stdout); - out << exml; - } -} - -void KNewStuff2Test::providerTest() -{ - // qCDebug(KNEWSTUFF) << "-- test kns2 provider class"; - - QDomDocument doc; - QFile f(QString("%1/testdata/provider.xml").arg(KNSSRCDIR)); - if (!f.open(QIODevice::ReadOnly)) { - // qCDebug(KNEWSTUFF) << "Error loading provider file."; - quitTest(); - } - if (!doc.setContent(&f)) { - // qCDebug(KNEWSTUFF) << "Error parsing provider file."; - f.close(); - quitTest(); - } - f.close(); - - KNS::ProviderHandler ph(doc.documentElement()); - KNS::Provider p = ph.provider(); - - // qCDebug(KNEWSTUFF) << "-- xml->provider test result: " << ph.isValid(); - - KNS::ProviderHandler ph2(p); - QDomElement pxml = ph2.providerXML(); - - // qCDebug(KNEWSTUFF) << "-- provider->xml test result: " << ph.isValid(); - - if (!ph.isValid()) { - quitTest(); - } else { - QTextStream out(stdout); - out << pxml; - } -} - -void KNewStuff2Test::engineTest() -{ - // qCDebug(KNEWSTUFF) << "-- test kns2 engine"; - - m_engine = new KNS::CoreEngine(NULL); - bool ret = m_engine->init("knewstuff2_test.knsrc"); - - // qCDebug(KNEWSTUFF) << "-- engine test result: " << ret; - - if (ret) { - connect(m_engine, - &KNS::CoreEngine::signalProviderLoaded, - this, &KNewStuff2Test::slotProviderLoaded); - connect(m_engine, - &KNS::CoreEngine::signalProvidersFailed, - this, &KNewStuff2Test::slotProvidersFailed); - connect(m_engine, - &KNS::CoreEngine::signalEntryLoaded, - this, &KNewStuff2Test::slotEntryLoaded); - connect(m_engine, - &KNS::CoreEngine::signalEntriesFinished, - this, &KNewStuff2Test::slotEntriesFinished); - connect(m_engine, - &KNS::CoreEngine::signalEntriesFailed, - this, &KNewStuff2Test::slotEntriesFailed); - connect(m_engine, - &KNS::CoreEngine::signalPayloadLoaded, - this, &KNewStuff2Test::slotPayloadLoaded); - connect(m_engine, - &KNS::CoreEngine::signalPayloadFailed, - this, &KNewStuff2Test::slotPayloadFailed); - connect(m_engine, - &KNS::CoreEngine::signalInstallationFinished, - this, &KNewStuff2Test::slotInstallationFinished); - connect(m_engine, - &KNS::CoreEngine::signalInstallationFailed, - this, &KNewStuff2Test::slotInstallationFailed); - - m_engine->start(); - } else { - qWarning() << "ACHTUNG: you probably need to 'make install' the knsrc file first."; - qWarning() << "Although this is not required anymore, so something went really wrong."; - quitTest(); - } -} - -void KNewStuff2Test::slotProviderLoaded(KNS::Provider *provider) -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotProviderLoaded"; - // qCDebug(KNEWSTUFF) << "-- provider: " << provider->name().representation(); - - m_engine->loadEntries(provider); -} - -void KNewStuff2Test::slotEntryLoaded(KNS::Entry *entry, const KNS::Feed *feed, const KNS::Provider *provider) -{ - Q_UNUSED(feed); - Q_UNUSED(provider); - - // qCDebug(KNEWSTUFF) << "SLOT: slotEntryLoaded"; - // qCDebug(KNEWSTUFF) << "-- entry: " << entry->name().representation(); - - if (m_testall) { - // qCDebug(KNEWSTUFF) << "-- now, download the entry's preview and payload file"; - - if (!entry->preview().isEmpty()) { - m_engine->downloadPreview(entry); - } - if (!entry->payload().isEmpty()) { - m_engine->downloadPayload(entry); - } - } -} - -void KNewStuff2Test::slotEntriesFinished() -{ - // Wait for installation if requested - if (!m_testall) { - quitTest(); - } -} - -void KNewStuff2Test::slotPayloadLoaded(QUrl payload) -{ - // qCDebug(KNEWSTUFF) << "-- entry downloaded successfully"; - // qCDebug(KNEWSTUFF) << "-- downloaded to " << payload.prettyUrl(); - - // qCDebug(KNEWSTUFF) << "-- run installation"; - - bool ret = m_engine->install(payload.path()); - - // qCDebug(KNEWSTUFF) << "-- installation result: " << ret; - // qCDebug(KNEWSTUFF) << "-- now, wait for installation to finish..."; -} - -void KNewStuff2Test::slotPayloadFailed() -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotPayloadFailed"; - quitTest(); -} - -void KNewStuff2Test::slotProvidersFailed() -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotProvidersFailed"; - quitTest(); -} - -void KNewStuff2Test::slotEntriesFailed() -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotEntriesFailed"; - quitTest(); -} - -void KNewStuff2Test::slotInstallationFinished() -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotInstallationFinished"; - // qCDebug(KNEWSTUFF) << "-- OK, finish test"; - quitTest(); -} - -void KNewStuff2Test::slotInstallationFailed() -{ - // qCDebug(KNEWSTUFF) << "SLOT: slotInstallationFailed"; - quitTest(); -} - -void KNewStuff2Test::quitTest() -{ - // qCDebug(KNEWSTUFF) << "-- quitting now..."; - if (1 == 0) { - // this would be the soft way out... - delete m_engine; - deleteLater(); - qApp->quit(); - } else { - exit(1); - } -} - -int main(int argc, char **argv) -{ - //options.add("testall", qi18n("Downloads all previews and payloads")); - - QApplication app(argc, argv); - - // Take source directory into account - // qCDebug(KNEWSTUFF) << "-- adding source directory " << KNSSRCDIR; - // qCDebug(KNEWSTUFF) << "-- adding build directory " << KNSBUILDDIR; - KGlobal::dirs()->addResourceDir("config", KNSSRCDIR); - KGlobal::dirs()->addResourceDir("config", KNSBUILDDIR); - - KNewStuff2Test *test = new KNewStuff2Test(); - if (app.arguments().contains("--testall")) { - test->setTestAll(true); - test->entryTest(); - test->providerTest(); - } - test->engineTest(); - - return app.exec(); -} - diff --git a/tests/knewstuff2_test.h b/tests/knewstuff2_test.h deleted file mode 100644 index f32893ac..00000000 --- a/tests/knewstuff2_test.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - This file is part of KNewStuff2. - Copyright (c) 2007 Josef Spillner - - 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 KNEWSTUFF2_TEST_TEST_H -#define KNEWSTUFF2_TEST_TEST_H - -#include -#include - -#include - -namespace KNS -{ -class CoreEngine; -} - -class KNewStuff2Test : public QObject -{ - Q_OBJECT -public: - KNewStuff2Test(); - void setTestAll(bool testall); - void entryTest(); - void providerTest(); - void engineTest(); -public Q_SLOTS: - void slotProviderLoaded(KNS::Provider *provider); - void slotProvidersFailed(); - void slotEntryLoaded(KNS::Entry *entry, const KNS::Feed *feed, const KNS::Provider *provider); - void slotEntriesFailed(); - void slotEntriesFinished(); - void slotPayloadLoaded(QUrl payload); - void slotPayloadFailed(); - void slotInstallationFinished(); - void slotInstallationFailed(); -private: - void quitTest(); - KNS::CoreEngine *m_engine; - bool m_testall; -}; - -#endif diff --git a/tests/testdata/entry.xml b/tests/testdata/entry.xml index f78265f0..eaa49e3a 100644 --- a/tests/testdata/entry.xml +++ b/tests/testdata/entry.xml @@ -1,14 +1,55 @@ - - Some Name - Anonymous Guy - GPL - 1.0 - 2005-06-17 - This is what it is all about. - http://some.http.server/preview.png - http://some.http.server/coolstuff.tar.gz - 10 - 0 - - + + + Entry 1 (ghns excluded) + Anonymous Guy + GPL + 1.0 + 2005-06-17 + This is what it is all about. + http://some.http.server/preview.png + http://some.http.server/coolstuff.tar.gz + 10 + 0 + ghns_exclude=1 + + + Entry 2 (ghns included) + Anonymous Guy + GPL + 2.1git2 + 2018-06-05 + A short description in English (not ghns excluded). + http://some.http.server/preview.png + http://some.http.server/coolstuff.tar.gz + 10 + 0 + + + + Entry 3 (ghns excluded) + Anonymous Guy + GPL + 1.0 + 2005-06-17 + This is what it is all about. + http://some.http.server/preview.png + http://some.http.server/coolstuff.tar.gz + 10 + 0 + ghns_exclude=1 + + + Entry 4 (ghns included) + Anonymous Guy + GPL + 2.1git2 + 2018-06-05 + A short description in English (not ghns excluded). + http://some.http.server/preview.png + http://some.http.server/coolstuff.tar.gz + 10 + 0 + + + diff --git a/tests/testdata/provider.xml b/tests/testdata/provider.xml index cd7151e2..fe6fb4dd 100644 --- a/tests/testdata/provider.xml +++ b/tests/testdata/provider.xml @@ -1,12 +1,22 @@ - + + Some cool stuff Viele neue Dinge +