File indexing completed on 2024-11-17 04:55:40

0001 /*
0002  *   SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 // Qt includes
0008 #include <QDebug>
0009 #include <QDir>
0010 #include <QDirIterator>
0011 #include <QFileInfo>
0012 #include <QStandardPaths>
0013 #include <QTimer>
0014 
0015 // KDE includes
0016 #include <KConfig>
0017 #include <KConfigGroup>
0018 #include <KLocalizedString>
0019 #include <KNSCore/Provider>
0020 #include <KNSCore/Question>
0021 #include <KNSCore/QuestionManager>
0022 #include <KNSCore/ResultsStream>
0023 
0024 // DiscoverCommon includes
0025 #include "Category/Category.h"
0026 #include "Transaction/Transaction.h"
0027 #include "Transaction/TransactionModel.h"
0028 
0029 // Own includes
0030 #include "KNSBackend.h"
0031 #include "KNSResource.h"
0032 #include "KNSReviews.h"
0033 #include "KNSTransaction.h"
0034 #include "utils.h"
0035 #include <resources/StandardBackendUpdater.h>
0036 
0037 using namespace Qt::StringLiterals;
0038 
0039 static const int ENGINE_PAGE_SIZE = 100;
0040 
0041 class KNSBackendFactory : public AbstractResourcesBackendFactory
0042 {
0043     Q_OBJECT
0044     Q_PLUGIN_METADATA(IID "org.kde.muon.AbstractResourcesBackendFactory")
0045     Q_INTERFACES(AbstractResourcesBackendFactory)
0046 public:
0047     KNSBackendFactory()
0048     {
0049         connect(KNSCore::QuestionManager::instance(), &KNSCore::QuestionManager::askQuestion, this, [](KNSCore::Question *question) {
0050             const auto transactions = TransactionModel::global()->transactions();
0051             for (auto t : transactions) {
0052                 const auto transaction = dynamic_cast<KNSTransaction *>(t);
0053                 if (!transaction) {
0054                     continue;
0055                 }
0056 
0057                 if (question->entry().uniqueId() == transaction->uniqueId()) {
0058                     switch (question->questionType()) {
0059                     case KNSCore::Question::ContinueCancelQuestion:
0060                         transaction->addQuestion(question);
0061                         return;
0062                     default:
0063                         transaction->passiveMessage(i18n("Unsupported question:\n%1", question->question()));
0064                         question->setResponse(KNSCore::Question::InvalidResponse);
0065                         transaction->setStatus(Transaction::CancelledStatus);
0066                         break;
0067                     }
0068                     return;
0069                 }
0070             }
0071             qWarning() << "Question for unknown transaction" << question->question() << question->questionType();
0072             question->setResponse(KNSCore::Question::InvalidResponse);
0073         });
0074     }
0075 
0076     QVector<AbstractResourcesBackend *> newInstance(QObject *parent, const QString & /*name*/) const override
0077     {
0078         QVector<AbstractResourcesBackend *> ret;
0079         const QStringList availableConfigFiles = KNSCore::EngineBase::availableConfigFiles();
0080         for (const QString &configFile : availableConfigFiles) {
0081             auto bk = new KNSBackend(parent, QStringLiteral("plasma"), configFile);
0082             if (bk->isValid())
0083                 ret += bk;
0084             else
0085                 delete bk;
0086         }
0087         return ret;
0088     }
0089 };
0090 
0091 class KNSResultsStream : public ResultsStream
0092 {
0093     Q_OBJECT
0094 public:
0095     KNSResultsStream(KNSBackend *backend, const QString &objectName)
0096         : ResultsStream(objectName)
0097         , m_backend(backend)
0098     {
0099     }
0100 
0101     void setRequest(const KNSCore::Provider::SearchRequest &request)
0102     {
0103         KNSCore::ResultsStream *job = m_backend->engine()->search(request);
0104         connect(job, &KNSCore::ResultsStream::entriesFound, this, &KNSResultsStream::addEntries);
0105         connect(job, &KNSCore::ResultsStream::finished, this, &KNSResultsStream::finish);
0106         connect(this, &ResultsStream::fetchMore, job, &KNSCore::ResultsStream::fetchMore);
0107         job->fetch();
0108     }
0109 
0110     void addEntries(const KNSCore::Entry::List &entries)
0111     {
0112         const auto res = kTransform<QList<StreamResult>>(entries, [this](const auto &entry) {
0113             return StreamResult{m_backend->resourceForEntry(entry), 0};
0114         });
0115         Q_EMIT resourcesFound(res);
0116     }
0117 
0118 private:
0119     KNSBackend *const m_backend;
0120 };
0121 
0122 KNSBackend::KNSBackend(QObject *parent, const QString &iconName, const QString &knsrc)
0123     : AbstractResourcesBackend(parent)
0124     , m_fetching(false)
0125     , m_isValid(true)
0126     , m_reviews(new KNSReviews(this))
0127     , m_name(knsrc)
0128     , m_iconName(iconName)
0129     , m_updater(new StandardBackendUpdater(this))
0130 {
0131     const QString fileName = QFileInfo(m_name).fileName();
0132     setName(fileName);
0133     setObjectName(knsrc);
0134 
0135     const KConfig conf(m_name, KConfig::SimpleConfig);
0136     const bool hasVersionlessGrp = conf.hasGroup(u"KNewStuff"_s);
0137     if (!conf.hasGroup(u"KNewStuff3"_s) && !hasVersionlessGrp) {
0138         markInvalid(QStringLiteral("Config group not found! Check your KNSCore installation."));
0139         return;
0140     }
0141 
0142     m_categories = QStringList{fileName};
0143 
0144     const KConfigGroup group = hasVersionlessGrp ? conf.group(u"KNewStuff"_s) : conf.group(u"KNewStuff3"_s);
0145     m_extends = group.readEntry("Extends", QStringList());
0146 
0147     setFetching(true);
0148 
0149     // This ensures we have something to track when checking after the initialization timeout
0150     connect(this, &KNSBackend::initialized, this, [this]() {
0151         m_initialized = true;
0152     });
0153     // If we have not initialized in 60 seconds, consider this KNS backend invalid
0154     QTimer::singleShot(60000, this, [this]() {
0155         if (!m_initialized) {
0156             markInvalid(i18n("Backend %1 took too long to initialize", m_displayName));
0157         }
0158     });
0159 
0160     const CategoryFilter filter = {CategoryFilter::CategoryNameFilter, fileName};
0161     const QSet<QString> backendName = {name()};
0162     m_displayName = group.readEntry("Name", QString());
0163     if (m_displayName.isEmpty()) {
0164         m_displayName = fileName.mid(0, fileName.indexOf(QLatin1Char('.')));
0165         m_displayName[0] = m_displayName[0].toUpper();
0166     }
0167     m_hasApplications = group.readEntry<bool>("X-Discover-HasApplications", false);
0168 
0169     const QStringList cats = group.readEntry<QStringList>("Categories", QStringList{});
0170     QVector<Category *> categories;
0171     if (cats.count() > 1) {
0172         m_categories += cats;
0173         for (const auto &cat : cats) {
0174             if (m_hasApplications)
0175                 categories << new Category(cat, QStringLiteral("applications-other"), {CategoryFilter::CategoryNameFilter, cat}, backendName, {}, true);
0176             else
0177                 categories << new Category(cat, QStringLiteral("plasma"), {CategoryFilter::CategoryNameFilter, cat}, backendName, {}, true);
0178         }
0179     }
0180 
0181     QVector<Category *> topCategories{categories};
0182     for (const auto &cat : std::as_const(categories)) {
0183         const QString catName = cat->name().append(QLatin1Char('/'));
0184         for (const auto &potentialSubCat : std::as_const(categories)) {
0185             if (potentialSubCat->name().startsWith(catName)) {
0186                 cat->addSubcategory(potentialSubCat);
0187                 topCategories.removeOne(potentialSubCat);
0188             }
0189         }
0190     }
0191 
0192     m_engine = new KNSCore::EngineBase(this);
0193     connect(m_engine, &KNSCore::EngineBase::signalErrorCode, this, &KNSBackend::slotErrorCode);
0194     connect(m_engine, &KNSCore::EngineBase::providersChanged, this, [this] {
0195         setFetching(false);
0196     });
0197 
0198     connect(m_engine,
0199             &KNSCore::EngineBase::signalCategoriesMetadataLoded,
0200             this,
0201             [categories](const QList<KNSCore::Provider::CategoryMetadata> &categoryMetadatas) {
0202                 for (const KNSCore::Provider::CategoryMetadata &category : categoryMetadatas) {
0203                     for (Category *cat : std::as_const(categories)) {
0204                         if (cat->matchesCategoryName(category.name)) {
0205                             cat->setName(category.displayName);
0206                             break;
0207                         }
0208                     }
0209                 }
0210             });
0211     m_engine->init(m_name);
0212 
0213     if (m_hasApplications) {
0214         auto actualCategory = new Category(m_displayName, QStringLiteral("applications-other"), filter, backendName, topCategories, false);
0215         auto applicationCategory = new Category(i18n("Applications"), //
0216                                                 QStringLiteral("applications-internet"),
0217                                                 filter,
0218                                                 backendName,
0219                                                 {actualCategory},
0220                                                 false);
0221         const QVector<CategoryFilter> filters = {{CategoryFilter::CategoryNameFilter, QLatin1String("Application")}, filter};
0222         applicationCategory->setFilter({CategoryFilter::AndFilter, filters});
0223         m_categories.append(applicationCategory->name());
0224         m_rootCategories = {applicationCategory};
0225         // Make sure we filter out any apps which won't run on the current system architecture
0226         QStringList tagFilter = m_engine->tagFilter();
0227         if (QSysInfo::currentCpuArchitecture() == QLatin1String("arm")) {
0228             tagFilter << QLatin1String("application##architecture==armhf");
0229         } else if (QSysInfo::currentCpuArchitecture() == QLatin1String("arm64")) {
0230             tagFilter << QLatin1String("application##architecture==arm64");
0231         } else if (QSysInfo::currentCpuArchitecture() == QLatin1String("i386")) {
0232             tagFilter << QLatin1String("application##architecture==x86");
0233         } else if (QSysInfo::currentCpuArchitecture() == QLatin1String("ia64")) {
0234             tagFilter << QLatin1String("application##architecture==x86-64");
0235         } else if (QSysInfo::currentCpuArchitecture() == QLatin1String("x86_64")) {
0236             tagFilter << QLatin1String("application##architecture==x86");
0237             tagFilter << QLatin1String("application##architecture==x86-64");
0238         }
0239         m_engine->setTagFilter(tagFilter);
0240     } else {
0241         static const QSet<QString> knsrcPlasma = {
0242             QStringLiteral("aurorae.knsrc"),       QStringLiteral("icons.knsrc"),
0243             QStringLiteral("kfontinst.knsrc"),     QStringLiteral("lookandfeel.knsrc"),
0244             QStringLiteral("plasma-themes.knsrc"), QStringLiteral("plasmoids.knsrc"),
0245             QStringLiteral("wallpaper.knsrc"),     QStringLiteral("wallpaper-mobile.knsrc"),
0246             QStringLiteral("xcursor.knsrc"),
0247 
0248             QStringLiteral("cgcgtk3.knsrc"),       QStringLiteral("cgcicon.knsrc"),
0249             QStringLiteral("cgctheme.knsrc"), // GTK integration
0250             QStringLiteral("kwinswitcher.knsrc"),  QStringLiteral("kwineffect.knsrc"),
0251             QStringLiteral("kwinscripts.knsrc"), // KWin
0252             QStringLiteral("comic.knsrc"),         QStringLiteral("colorschemes.knsrc"),
0253             QStringLiteral("emoticons.knsrc"),     QStringLiteral("plymouth.knsrc"),
0254             QStringLiteral("sddmtheme.knsrc"),     QStringLiteral("wallpaperplugin.knsrc"),
0255             QStringLiteral("ksplash.knsrc"),       QStringLiteral("window-decorations.knsrc"),
0256         };
0257         const auto iconName = knsrcPlasma.contains(fileName) ? QStringLiteral("plasma") : QStringLiteral("applications-other");
0258         auto actualCategory = new Category(m_displayName, iconName, filter, backendName, categories, true);
0259         actualCategory->setParent(this);
0260 
0261         const auto topLevelName = knsrcPlasma.contains(fileName) ? i18n("Plasma Addons") : i18n("Application Addons");
0262         auto addonsCategory = new Category(topLevelName, iconName, filter, backendName, {actualCategory}, true);
0263         m_rootCategories = {addonsCategory};
0264     }
0265 
0266     connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &KNSBackend::updatesCountChanged);
0267 }
0268 
0269 KNSBackend::~KNSBackend()
0270 {
0271     qDeleteAll(m_rootCategories);
0272 }
0273 
0274 void KNSBackend::markInvalid(const QString &message)
0275 {
0276     m_rootCategories.clear();
0277     qWarning() << "invalid kns backend!" << m_name << "because:" << message;
0278     m_isValid = false;
0279     setFetching(false);
0280     Q_EMIT initialized();
0281 }
0282 
0283 void KNSBackend::checkForUpdates()
0284 {
0285     AbstractResourcesBackend::Filters filter;
0286     filter.state = AbstractResource::Upgradeable;
0287     search(filter);
0288 }
0289 
0290 void KNSBackend::setFetching(bool f)
0291 {
0292     if (m_fetching != f) {
0293         m_fetching = f;
0294         Q_EMIT fetchingChanged();
0295 
0296         if (!m_fetching) {
0297             Q_EMIT initialized();
0298         }
0299     }
0300 }
0301 
0302 bool KNSBackend::isValid() const
0303 {
0304     return m_isValid;
0305 }
0306 
0307 KNSResource *KNSBackend::resourceForEntry(const KNSCore::Entry &entry)
0308 {
0309     KNSResource *r = static_cast<KNSResource *>(m_resourcesByName.value(entry.uniqueId()));
0310     if (!r) {
0311         QStringList categories{name()};
0312         if (!m_rootCategories.isEmpty()) {
0313             categories << m_rootCategories.first()->name();
0314         }
0315         const auto cats = m_engine->categoriesMetadata();
0316         const int catIndex = kIndexOf(cats, [&entry](const KNSCore::Provider::CategoryMetadata &cat) {
0317             return entry.category() == cat.id;
0318         });
0319         if (catIndex > -1) {
0320             categories << cats.at(catIndex).name;
0321         }
0322         if (m_hasApplications) {
0323             categories << QLatin1String("Application");
0324         }
0325         r = new KNSResource(entry, categories, this);
0326         m_resourcesByName.insert(entry.uniqueId(), r);
0327     } else {
0328         r->setEntry(entry);
0329     }
0330     return r;
0331 }
0332 
0333 void KNSBackend::statusChanged(const KNSCore::Entry &entry)
0334 {
0335     resourceForEntry(entry);
0336 }
0337 
0338 void KNSBackend::slotErrorCode(const KNSCore::ErrorCode::ErrorCode &errorCode, const QString &message, const QVariant &metadata)
0339 {
0340     QString error = message;
0341     qWarning() << "KNS error in" << m_displayName << ":" << errorCode << message << metadata;
0342     bool invalidFile = false;
0343     switch (errorCode) {
0344     case KNSCore::ErrorCode::UnknownError:
0345         // This is not supposed to be hit, of course, but any error coming to this point should be non-critical and safely ignored.
0346         break;
0347     case KNSCore::ErrorCode::NetworkError:
0348         // If we have a network error, we need to tell the user about it. This is almost always fatal, so mark invalid and tell the user.
0349         error = i18n("Network error in backend %1: %2", m_displayName, metadata.toInt());
0350         markInvalid(error);
0351         invalidFile = true;
0352         break;
0353     case KNSCore::ErrorCode::OcsError:
0354         if (metadata.toInt() == 200) {
0355             // Too many requests, try again in a couple of minutes - perhaps we can simply postpone it automatically, and give a message?
0356             error = i18n("Too many requests sent to the server for backend %1. Please try again in a few minutes.", m_displayName);
0357         } else {
0358             // Unknown API error, usually something critical, mark as invalid and cry a lot
0359             error = i18n("Invalid %1 backend, contact your distributor.", m_displayName);
0360             markInvalid(error);
0361             invalidFile = true;
0362         }
0363         break;
0364     case KNSCore::ErrorCode::ConfigFileError:
0365         error = i18n("Invalid %1 backend, contact your distributor.", m_displayName);
0366         markInvalid(error);
0367         invalidFile = true;
0368         break;
0369     case KNSCore::ErrorCode::ProviderError:
0370         error = i18n("Invalid %1 backend, contact your distributor.", m_displayName);
0371         markInvalid(error);
0372         invalidFile = true;
0373         break;
0374     case KNSCore::ErrorCode::InstallationError: {
0375         KNSResource *r = static_cast<KNSResource *>(m_resourcesByName.value(metadata.toString()));
0376         if (r) {
0377             // If the following is true, then we can safely assume that the entry was
0378             // attempted updated, but the update was aborted.
0379             // Specifically, we can also likely expect that the update failed because
0380             // KNSCore::Engine was unable to deduce which payload to use (which will
0381             // happen when an entry has more than one payload, and none of those match
0382             // the name of the originally downloaded file).
0383             // We cannot complete this in Discover (as we've no way to forward that
0384             // query to the user) but we can give them an idea of how to deal with the
0385             // situation some other way.
0386             // TODO: Once Discover has a way to forward queries to the user from transactions, this likely will no longer be needed
0387             if (r->entry().status() == KNSCore::Entry::Updateable) {
0388                 error = i18n(
0389                     "Unable to complete the update of %1. You can try and perform this action through the Get Hot New Stuff dialog, which grants tighter "
0390                     "control. The reported error was:\n%2",
0391                     r->name(),
0392                     message);
0393             }
0394         }
0395         break;
0396     }
0397     case KNSCore::ErrorCode::ImageError:
0398         // Image fetching errors are not critical as such, but may lead to weird layout issues, might want handling...
0399         error = i18n("Could not fetch screenshot for the entry %1 in backend %2", metadata.toList().at(0).toString(), m_displayName);
0400         break;
0401     default:
0402         // Having handled all current error values, we should by all rights never arrive here, but for good order and future safety...
0403         error = i18n("Unhandled error in %1 backend. Contact your distributor.", m_displayName);
0404         break;
0405     }
0406     qWarning() << "kns error" << objectName() << error;
0407     if (!invalidFile)
0408         Q_EMIT passiveMessage(i18n("%1: %2", name(), error));
0409 }
0410 
0411 void KNSBackend::slotEntryEvent(const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event)
0412 {
0413     switch (event) {
0414     case KNSCore::Entry::StatusChangedEvent:
0415         statusChanged(entry);
0416         break;
0417     case KNSCore::Entry::DetailsLoadedEvent:
0418         detailsLoaded(entry);
0419         break;
0420     case KNSCore::Entry::AdoptedEvent:
0421     case KNSCore::Entry::UnknownEvent:
0422     default:
0423         break;
0424     }
0425 }
0426 
0427 Transaction *KNSBackend::removeApplication(AbstractResource *app)
0428 {
0429     auto res = qobject_cast<KNSResource *>(app);
0430     return new KNSTransaction(this, res, Transaction::RemoveRole);
0431 }
0432 
0433 Transaction *KNSBackend::installApplication(AbstractResource *app)
0434 {
0435     auto res = qobject_cast<KNSResource *>(app);
0436     return new KNSTransaction(this, res, Transaction::InstallRole);
0437 }
0438 
0439 Transaction *KNSBackend::installApplication(AbstractResource *app, const AddonList & /*addons*/)
0440 {
0441     return installApplication(app);
0442 }
0443 
0444 int KNSBackend::updatesCount() const
0445 {
0446     return m_updater->updatesCount();
0447 }
0448 
0449 AbstractReviewsBackend *KNSBackend::reviewsBackend() const
0450 {
0451     return m_reviews;
0452 }
0453 
0454 static ResultsStream *voidStream()
0455 {
0456     return new ResultsStream(QStringLiteral("KNS-void"), {});
0457 }
0458 
0459 ResultsStream *KNSBackend::search(const AbstractResourcesBackend::Filters &filter)
0460 {
0461     if (!m_isValid || (!filter.resourceUrl.isEmpty() && filter.resourceUrl.scheme() != QLatin1String("kns")) || !filter.mimetype.isEmpty())
0462         return voidStream();
0463 
0464     if (filter.resourceUrl.scheme() == QLatin1String("kns")) {
0465         return findResourceByPackageName(filter.resourceUrl);
0466     } else if (filter.state >= AbstractResource::Installed) {
0467         auto stream = new KNSResultsStream(this, QStringLiteral("KNS-installed"));
0468         const auto start = [this, stream, filter]() {
0469             if (m_isValid) {
0470                 const auto knsFilter = filter.state == AbstractResource::Installed ? KNSCore::Provider::Installed : KNSCore::Provider::Updates;
0471                 stream->setRequest(KNSCore::Provider::SearchRequest(KNSCore::Provider::Newest, knsFilter, {}, {}, -1, ENGINE_PAGE_SIZE));
0472             }
0473             stream->finish();
0474         };
0475         if (isFetching()) {
0476             connect(this, &KNSBackend::initialized, stream, start);
0477         } else {
0478             QTimer::singleShot(0, stream, start);
0479         }
0480 
0481         return stream;
0482     } else if ((!filter.category && !filter.search.isEmpty()) // Accept global searches
0483                                                               // If there /is/ a category, make sure we actually are one of those requested before searching
0484                || (filter.category && kContains(m_categories, [&filter](const QString &cat) {
0485                        return filter.category->matchesCategoryName(cat);
0486                    }))) {
0487         return searchStream(filter.search);
0488     }
0489     return voidStream();
0490 }
0491 
0492 KNSResultsStream *KNSBackend::searchStream(const QString &searchText)
0493 {
0494     auto stream = new KNSResultsStream(this, QLatin1String("KNS-search-") + name());
0495     auto start = [this, stream, searchText]() {
0496         Q_ASSERT(!isFetching());
0497         if (!m_isValid) {
0498             qWarning() << "querying an invalid backend";
0499             stream->finish();
0500             return;
0501         }
0502         KNSCore::Provider::SearchRequest p(KNSCore::Provider::Newest, KNSCore::Provider::None, searchText, {}, 0, ENGINE_PAGE_SIZE);
0503         stream->setRequest(p);
0504     };
0505     if (isFetching()) {
0506         connect(this, &KNSBackend::initialized, stream, start, Qt::QueuedConnection);
0507         connect(this, &KNSBackend::fetchingChanged, stream, start, Qt::QueuedConnection);
0508     } else {
0509         QTimer::singleShot(0, stream, start);
0510     }
0511     return stream;
0512 }
0513 
0514 ResultsStream *KNSBackend::findResourceByPackageName(const QUrl &search)
0515 {
0516     if (search.scheme() != QLatin1String("kns") || search.host() != name())
0517         return voidStream();
0518 
0519     const auto pathParts = search.path().split(QLatin1Char('/'), Qt::SkipEmptyParts);
0520     if (pathParts.size() != 2) {
0521         Q_EMIT passiveMessage(i18n("Wrong KNewStuff URI: %1", search.toString()));
0522         return voidStream();
0523     }
0524     const auto providerid = pathParts.at(0);
0525     const auto entryid = pathParts.at(1);
0526 
0527     auto stream = new KNSResultsStream(this, QLatin1String("KNS-byname-") + entryid);
0528     auto start = [entryid, stream, providerid]() {
0529         KNSCore::Provider::SearchRequest query(KNSCore::Provider::Newest, KNSCore::Provider::ExactEntryId, entryid, {}, 0, ENGINE_PAGE_SIZE);
0530         stream->setRequest(query);
0531     };
0532     if (isFetching()) {
0533         connect(this, &KNSBackend::initialized, stream, start, Qt::QueuedConnection);
0534         connect(this, &KNSBackend::fetchingChanged, stream, start, Qt::QueuedConnection);
0535     } else {
0536         QTimer::singleShot(0, stream, start);
0537     }
0538     return stream;
0539 }
0540 
0541 bool KNSBackend::isFetching() const
0542 {
0543     return m_fetching;
0544 }
0545 
0546 AbstractBackendUpdater *KNSBackend::backendUpdater() const
0547 {
0548     return m_updater;
0549 }
0550 
0551 QString KNSBackend::displayName() const
0552 {
0553     return QStringLiteral("KNewStuff");
0554 }
0555 
0556 void KNSBackend::detailsLoaded(const KNSCore::Entry &entry)
0557 {
0558     auto res = resourceForEntry(entry);
0559     Q_EMIT res->longDescriptionChanged();
0560 }
0561 
0562 #include "KNSBackend.moc"