File indexing completed on 2024-07-14 03:54:58

0001 /*
0002     SPDX-FileCopyrightText: 2009-2010 Frederik Gladhorn <gladhorn@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "atticaprovider_p.h"
0008 
0009 #include "commentsmodel.h"
0010 #include "question.h"
0011 #include "tagsfilterchecker.h"
0012 
0013 #include <KFormat>
0014 #include <KLocalizedString>
0015 #include <QCollator>
0016 #include <QDomDocument>
0017 #include <knewstuffcore_debug.h>
0018 
0019 #include <attica/accountbalance.h>
0020 #include <attica/config.h>
0021 #include <attica/content.h>
0022 #include <attica/downloaditem.h>
0023 #include <attica/listjob.h>
0024 #include <attica/person.h>
0025 #include <attica/provider.h>
0026 #include <attica/providermanager.h>
0027 
0028 using namespace Attica;
0029 
0030 namespace KNSCore
0031 {
0032 AtticaProvider::AtticaProvider(const QStringList &categories, const QString &additionalAgentInformation)
0033     : mEntryJob(nullptr)
0034     , mInitialized(false)
0035 {
0036     // init categories map with invalid categories
0037     for (const QString &category : categories) {
0038         mCategoryMap.insert(category, Attica::Category());
0039     }
0040 
0041     connect(&m_providerManager, &ProviderManager::providerAdded, this, [this, additionalAgentInformation](const Attica::Provider &provider) {
0042         providerLoaded(provider);
0043         m_provider.setAdditionalAgentInformation(additionalAgentInformation);
0044     });
0045     connect(&m_providerManager, &ProviderManager::authenticationCredentialsMissing, this, &AtticaProvider::onAuthenticationCredentialsMissing);
0046 }
0047 
0048 AtticaProvider::AtticaProvider(const Attica::Provider &provider, const QStringList &categories, const QString &additionalAgentInformation)
0049     : mEntryJob(nullptr)
0050     , mInitialized(false)
0051 {
0052     // init categories map with invalid categories
0053     for (const QString &category : categories) {
0054         mCategoryMap.insert(category, Attica::Category());
0055     }
0056     providerLoaded(provider);
0057     m_provider.setAdditionalAgentInformation(additionalAgentInformation);
0058 }
0059 
0060 QString AtticaProvider::id() const
0061 {
0062     return m_providerId;
0063 }
0064 
0065 void AtticaProvider::onAuthenticationCredentialsMissing(const Attica::Provider &)
0066 {
0067     qCDebug(KNEWSTUFFCORE) << "Authentication missing!";
0068     // FIXME Show authentication dialog
0069 }
0070 
0071 bool AtticaProvider::setProviderXML(const QDomElement &xmldata)
0072 {
0073     if (xmldata.tagName() != QLatin1String("provider")) {
0074         return false;
0075     }
0076 
0077     // FIXME this is quite ugly, repackaging the xml into a string
0078     QDomDocument doc(QStringLiteral("temp"));
0079     qCDebug(KNEWSTUFFCORE) << "setting provider xml" << doc.toString();
0080 
0081     doc.appendChild(xmldata.cloneNode(true));
0082     m_providerManager.addProviderFromXml(doc.toString());
0083 
0084     if (!m_providerManager.providers().isEmpty()) {
0085         qCDebug(KNEWSTUFFCORE) << "base url of attica provider:" << m_providerManager.providers().constLast().baseUrl().toString();
0086     } else {
0087         qCCritical(KNEWSTUFFCORE) << "Could not load provider.";
0088         return false;
0089     }
0090     return true;
0091 }
0092 
0093 void AtticaProvider::setCachedEntries(const KNSCore::Entry::List &cachedEntries)
0094 {
0095     mCachedEntries = cachedEntries;
0096 }
0097 
0098 void AtticaProvider::providerLoaded(const Attica::Provider &provider)
0099 {
0100     setName(provider.name());
0101     setIcon(provider.icon());
0102     qCDebug(KNEWSTUFFCORE) << "Added provider: " << provider.name();
0103 
0104     m_provider = provider;
0105     m_provider.setAdditionalAgentInformation(name());
0106     m_providerId = provider.baseUrl().host();
0107 
0108     Attica::ListJob<Attica::Category> *job = m_provider.requestCategories();
0109     connect(job, &BaseJob::finished, this, &AtticaProvider::listOfCategoriesLoaded);
0110     job->start();
0111 }
0112 
0113 void AtticaProvider::listOfCategoriesLoaded(Attica::BaseJob *listJob)
0114 {
0115     if (!jobSuccess(listJob)) {
0116         return;
0117     }
0118 
0119     qCDebug(KNEWSTUFFCORE) << "loading categories: " << mCategoryMap.keys();
0120 
0121     auto *job = static_cast<Attica::ListJob<Attica::Category> *>(listJob);
0122     const Category::List categoryList = job->itemList();
0123 
0124     QList<CategoryMetadata> categoryMetadataList;
0125     for (const Category &category : categoryList) {
0126         if (mCategoryMap.contains(category.name())) {
0127             qCDebug(KNEWSTUFFCORE) << "Adding category: " << category.name() << category.displayName();
0128             // If there is only the placeholder category, replace it
0129             if (mCategoryMap.contains(category.name()) && !mCategoryMap.value(category.name()).isValid()) {
0130                 mCategoryMap.replace(category.name(), category);
0131             } else {
0132                 mCategoryMap.insert(category.name(), category);
0133             }
0134 
0135             CategoryMetadata categoryMetadata;
0136             categoryMetadata.id = category.id();
0137             categoryMetadata.name = category.name();
0138             categoryMetadata.displayName = category.displayName();
0139             categoryMetadataList << categoryMetadata;
0140         }
0141     }
0142     std::sort(categoryMetadataList.begin(),
0143               categoryMetadataList.end(),
0144               [](const AtticaProvider::CategoryMetadata &i, const AtticaProvider::CategoryMetadata &j) -> bool {
0145                   const QString a(i.displayName.isEmpty() ? i.name : i.displayName);
0146                   const QString b(j.displayName.isEmpty() ? j.name : j.displayName);
0147 
0148                   return (QCollator().compare(a, b) < 0);
0149               });
0150 
0151     bool correct = false;
0152     for (auto it = mCategoryMap.cbegin(), itEnd = mCategoryMap.cend(); it != itEnd; ++it) {
0153         if (!it.value().isValid()) {
0154             qCWarning(KNEWSTUFFCORE) << "Could not find category" << it.key();
0155         } else {
0156             correct = true;
0157         }
0158     }
0159 
0160     if (correct) {
0161         mInitialized = true;
0162         Q_EMIT providerInitialized(this);
0163         Q_EMIT categoriesMetadataLoded(categoryMetadataList);
0164     } else {
0165         Q_EMIT signalErrorCode(KNSCore::ErrorCode::ConfigFileError, i18n("All categories are missing"), QVariant());
0166     }
0167 }
0168 
0169 bool AtticaProvider::isInitialized() const
0170 {
0171     return mInitialized;
0172 }
0173 
0174 void AtticaProvider::loadEntries(const KNSCore::Provider::SearchRequest &request)
0175 {
0176     if (mEntryJob) {
0177         mEntryJob->abort();
0178         mEntryJob = nullptr;
0179     }
0180 
0181     mCurrentRequest = request;
0182     switch (request.filter) {
0183     case None:
0184         break;
0185     case ExactEntryId: {
0186         ItemJob<Content> *job = m_provider.requestContent(request.searchTerm);
0187         job->setProperty("providedEntryId", request.searchTerm);
0188         connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded);
0189         job->start();
0190         return;
0191     }
0192     case Installed:
0193         if (request.page == 0) {
0194             Q_EMIT loadingFinished(request, installedEntries());
0195         } else {
0196             Q_EMIT loadingFinished(request, Entry::List());
0197         }
0198         return;
0199     case Updates:
0200         checkForUpdates();
0201         return;
0202     }
0203 
0204     Attica::Provider::SortMode sorting = atticaSortMode(request.sortMode);
0205     Attica::Category::List categoriesToSearch;
0206 
0207     if (request.categories.isEmpty()) {
0208         // search in all categories
0209         categoriesToSearch = mCategoryMap.values();
0210     } else {
0211         categoriesToSearch.reserve(request.categories.size());
0212         for (const QString &categoryName : std::as_const(request.categories)) {
0213             categoriesToSearch.append(mCategoryMap.values(categoryName));
0214         }
0215     }
0216 
0217     ListJob<Content> *job = m_provider.searchContents(categoriesToSearch, request.searchTerm, sorting, request.page, request.pageSize);
0218     job->setProperty("searchRequest", QVariant::fromValue(request));
0219     connect(job, &BaseJob::finished, this, &AtticaProvider::categoryContentsLoaded);
0220 
0221     mEntryJob = job;
0222     job->start();
0223 }
0224 
0225 void AtticaProvider::checkForUpdates()
0226 {
0227     if (mCachedEntries.isEmpty()) {
0228         Q_EMIT loadingFinished(mCurrentRequest, {});
0229     }
0230 
0231     for (const Entry &e : std::as_const(mCachedEntries)) {
0232         ItemJob<Content> *job = m_provider.requestContent(e.uniqueId());
0233         connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded);
0234         m_updateJobs.insert(job);
0235         job->start();
0236         qCDebug(KNEWSTUFFCORE) << "Checking for update: " << e.name();
0237     }
0238 }
0239 
0240 void AtticaProvider::loadEntryDetails(const KNSCore::Entry &entry)
0241 {
0242     ItemJob<Content> *job = m_provider.requestContent(entry.uniqueId());
0243     connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded);
0244     job->start();
0245 }
0246 
0247 void AtticaProvider::detailsLoaded(BaseJob *job)
0248 {
0249     if (jobSuccess(job)) {
0250         auto *contentJob = static_cast<ItemJob<Content> *>(job);
0251         Content content = contentJob->result();
0252         Entry entry = entryFromAtticaContent(content);
0253         entry.setEntryRequestedId(job->property("providedEntryId").toString()); // The ResultsStream should still known that this entry was for its query
0254         Q_EMIT entryDetailsLoaded(entry);
0255         qCDebug(KNEWSTUFFCORE) << "check update finished: " << entry.name();
0256     }
0257 
0258     if (m_updateJobs.remove(job) && m_updateJobs.isEmpty()) {
0259         qCDebug(KNEWSTUFFCORE) << "check update finished.";
0260         QList<Entry> updatable;
0261         for (const Entry &entry : std::as_const(mCachedEntries)) {
0262             if (entry.status() == KNSCore::Entry::Updateable) {
0263                 updatable.append(entry);
0264             }
0265         }
0266         Q_EMIT loadingFinished(mCurrentRequest, updatable);
0267     }
0268 }
0269 
0270 void AtticaProvider::categoryContentsLoaded(BaseJob *job)
0271 {
0272     if (!jobSuccess(job)) {
0273         return;
0274     }
0275 
0276     auto *listJob = static_cast<ListJob<Content> *>(job);
0277     const Content::List contents = listJob->itemList();
0278 
0279     Entry::List entries;
0280     TagsFilterChecker checker(tagFilter());
0281     TagsFilterChecker downloadschecker(downloadTagFilter());
0282     for (const Content &content : contents) {
0283         if (!content.isValid()) {
0284             qCDebug(KNEWSTUFFCORE)
0285                 << "Filtered out an invalid entry. This suggests something is not right on the originating server. Please contact the administrators of"
0286                 << name() << "and inform them there is an issue with content in the category or categories" << mCurrentRequest.categories;
0287             continue;
0288         }
0289         if (checker.filterAccepts(content.tags())) {
0290             bool filterAcceptsDownloads = true;
0291             if (content.downloads() > 0) {
0292                 filterAcceptsDownloads = false;
0293                 const QList<Attica::DownloadDescription> descs = content.downloadUrlDescriptions();
0294                 for (const Attica::DownloadDescription &dli : descs) {
0295                     if (downloadschecker.filterAccepts(dli.tags())) {
0296                         filterAcceptsDownloads = true;
0297                         break;
0298                     }
0299                 }
0300             }
0301             if (filterAcceptsDownloads) {
0302                 mCachedContent.insert(content.id(), content);
0303                 entries.append(entryFromAtticaContent(content));
0304             } else {
0305                 qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on download filter" << downloadTagFilter();
0306             }
0307         } else {
0308             qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on entry filter" << tagFilter();
0309         }
0310     }
0311 
0312     qCDebug(KNEWSTUFFCORE) << "loaded: " << mCurrentRequest.hashForRequest() << " count: " << entries.size();
0313     Q_EMIT loadingFinished(mCurrentRequest, entries);
0314     mEntryJob = nullptr;
0315 }
0316 
0317 Attica::Provider::SortMode AtticaProvider::atticaSortMode(SortMode sortMode)
0318 {
0319     switch (sortMode) {
0320     case Newest:
0321         return Attica::Provider::Newest;
0322     case Alphabetical:
0323         return Attica::Provider::Alphabetical;
0324     case Downloads:
0325         return Attica::Provider::Downloads;
0326     default:
0327         return Attica::Provider::Rating;
0328     }
0329 }
0330 
0331 void AtticaProvider::loadPayloadLink(const KNSCore::Entry &entry, int linkId)
0332 {
0333     Attica::Content content = mCachedContent.value(entry.uniqueId());
0334     const DownloadDescription desc = content.downloadUrlDescription(linkId);
0335 
0336     if (desc.hasPrice()) {
0337         // Ask for balance, then show information...
0338         ItemJob<AccountBalance> *job = m_provider.requestAccountBalance();
0339         connect(job, &BaseJob::finished, this, &AtticaProvider::accountBalanceLoaded);
0340         mDownloadLinkJobs[job] = qMakePair(entry, linkId);
0341         job->start();
0342 
0343         qCDebug(KNEWSTUFFCORE) << "get account balance";
0344     } else {
0345         ItemJob<DownloadItem> *job = m_provider.downloadLink(entry.uniqueId(), QString::number(linkId));
0346         connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded);
0347         mDownloadLinkJobs[job] = qMakePair(entry, linkId);
0348         job->start();
0349 
0350         qCDebug(KNEWSTUFFCORE) << " link for " << entry.uniqueId();
0351     }
0352 }
0353 
0354 void AtticaProvider::loadComments(const Entry &entry, int commentsPerPage, int page)
0355 {
0356     ListJob<Attica::Comment> *job = m_provider.requestComments(Attica::Comment::ContentComment, entry.uniqueId(), QStringLiteral("0"), page, commentsPerPage);
0357     connect(job, &BaseJob::finished, this, &AtticaProvider::loadedComments);
0358     job->start();
0359 }
0360 
0361 QList<std::shared_ptr<KNSCore::Comment>> getCommentsList(const Attica::Comment::List &comments, std::shared_ptr<KNSCore::Comment> parent)
0362 {
0363     QList<std::shared_ptr<KNSCore::Comment>> knsComments;
0364     for (const Attica::Comment &comment : comments) {
0365         qCDebug(KNEWSTUFFCORE) << "Appending comment with id" << comment.id() << ", which has" << comment.childCount() << "children";
0366         auto knsComment = std::make_shared<KNSCore::Comment>();
0367         knsComment->id = comment.id();
0368         knsComment->subject = comment.subject();
0369         knsComment->text = comment.text();
0370         knsComment->childCount = comment.childCount();
0371         knsComment->username = comment.user();
0372         knsComment->date = comment.date();
0373         knsComment->score = comment.score();
0374         knsComment->parent = parent;
0375         knsComments << knsComment;
0376         if (comment.childCount() > 0) {
0377             qCDebug(KNEWSTUFFCORE) << "Getting more comments, as this one has children, and we currently have this number of comments:" << knsComments.count();
0378             knsComments << getCommentsList(comment.children(), knsComment);
0379             qCDebug(KNEWSTUFFCORE) << "After getting the children, we now have the following number of comments:" << knsComments.count();
0380         }
0381     }
0382     return knsComments;
0383 }
0384 
0385 void AtticaProvider::loadedComments(Attica::BaseJob *baseJob)
0386 {
0387     if (!jobSuccess(baseJob)) {
0388         return;
0389     }
0390 
0391     auto *job = static_cast<ListJob<Attica::Comment> *>(baseJob);
0392     Attica::Comment::List comments = job->itemList();
0393 
0394     QList<std::shared_ptr<KNSCore::Comment>> receivedComments = getCommentsList(comments, nullptr);
0395     Q_EMIT commentsLoaded(receivedComments);
0396 }
0397 
0398 void AtticaProvider::loadPerson(const QString &username)
0399 {
0400     if (m_provider.hasPersonService()) {
0401         ItemJob<Attica::Person> *job = m_provider.requestPerson(username);
0402         job->setProperty("username", username);
0403         connect(job, &BaseJob::finished, this, &AtticaProvider::loadedPerson);
0404         job->start();
0405     }
0406 }
0407 
0408 void AtticaProvider::loadedPerson(Attica::BaseJob *baseJob)
0409 {
0410     if (!jobSuccess(baseJob)) {
0411         return;
0412     }
0413 
0414     auto *job = static_cast<ItemJob<Attica::Person> *>(baseJob);
0415     Attica::Person person = job->result();
0416 
0417     auto author = std::make_shared<KNSCore::Author>();
0418     // This is a touch hack-like, but it ensures we actually have the data in case it is not returned by the server
0419     author->setId(job->property("username").toString());
0420     author->setName(QStringLiteral("%1 %2").arg(person.firstName(), person.lastName()).trimmed());
0421     author->setHomepage(person.homepage());
0422     author->setProfilepage(person.extendedAttribute(QStringLiteral("profilepage")));
0423     author->setAvatarUrl(person.avatarUrl());
0424     author->setDescription(person.extendedAttribute(QStringLiteral("description")));
0425     Q_EMIT personLoaded(author);
0426 }
0427 
0428 void AtticaProvider::loadBasics()
0429 {
0430     Attica::ItemJob<Attica::Config> *configJob = m_provider.requestConfig();
0431     connect(configJob, &BaseJob::finished, this, &AtticaProvider::loadedConfig);
0432     configJob->start();
0433 }
0434 
0435 void AtticaProvider::loadedConfig(Attica::BaseJob *baseJob)
0436 {
0437     if (jobSuccess(baseJob)) {
0438         auto *job = static_cast<ItemJob<Attica::Config> *>(baseJob);
0439         Attica::Config config = job->result();
0440         setVersion(config.version());
0441         setSupportsSsl(config.ssl());
0442         setContactEmail(config.contact());
0443         QString protocol{QStringLiteral("http")};
0444         if (config.ssl()) {
0445             protocol = QStringLiteral("https");
0446         }
0447         // There is usually no protocol in the website and host, but in case
0448         // there is, trust what's there
0449         if (config.website().contains(QLatin1String("://"))) {
0450             setWebsite(QUrl(config.website()));
0451         } else {
0452             setWebsite(QUrl(QLatin1String("%1://%2").arg(protocol).arg(config.website())));
0453         }
0454         if (config.host().contains(QLatin1String("://"))) {
0455             setHost(QUrl(config.host()));
0456         } else {
0457             setHost(QUrl(QLatin1String("%1://%2").arg(protocol).arg(config.host())));
0458         }
0459     }
0460 }
0461 
0462 void AtticaProvider::accountBalanceLoaded(Attica::BaseJob *baseJob)
0463 {
0464     if (!jobSuccess(baseJob)) {
0465         return;
0466     }
0467 
0468     auto *job = static_cast<ItemJob<AccountBalance> *>(baseJob);
0469     AccountBalance item = job->result();
0470 
0471     QPair<Entry, int> pair = mDownloadLinkJobs.take(job);
0472     Entry entry(pair.first);
0473     Content content = mCachedContent.value(entry.uniqueId());
0474     if (content.downloadUrlDescription(pair.second).priceAmount() < item.balance()) {
0475         qCDebug(KNEWSTUFFCORE) << "Your balance is greater than the price." << content.downloadUrlDescription(pair.second).priceAmount()
0476                                << " balance: " << item.balance();
0477         Question question;
0478         question.setEntry(entry);
0479         question.setQuestion(i18nc("the price of a download item, parameter 1 is the currency, 2 is the price",
0480                                    "This item costs %1 %2.\nDo you want to buy it?",
0481                                    item.currency(),
0482                                    content.downloadUrlDescription(pair.second).priceAmount()));
0483         if (question.ask() == Question::YesResponse) {
0484             ItemJob<DownloadItem> *job = m_provider.downloadLink(entry.uniqueId(), QString::number(pair.second));
0485             connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded);
0486             mDownloadLinkJobs[job] = qMakePair(entry, pair.second);
0487             job->start();
0488         } else {
0489             return;
0490         }
0491     } else {
0492         qCDebug(KNEWSTUFFCORE) << "You don't have enough money on your account!" << content.downloadUrlDescription(0).priceAmount()
0493                                << " balance: " << item.balance();
0494         Q_EMIT signalInformation(i18n("Your account balance is too low:\nYour balance: %1\nPrice: %2", //
0495                                       item.balance(),
0496                                       content.downloadUrlDescription(0).priceAmount()));
0497     }
0498 }
0499 
0500 void AtticaProvider::downloadItemLoaded(BaseJob *baseJob)
0501 {
0502     if (!jobSuccess(baseJob)) {
0503         return;
0504     }
0505 
0506     auto *job = static_cast<ItemJob<DownloadItem> *>(baseJob);
0507     DownloadItem item = job->result();
0508 
0509     Entry entry = mDownloadLinkJobs.take(job).first;
0510     entry.setPayload(QString(item.url().toString()));
0511     Q_EMIT payloadLinkLoaded(entry);
0512 }
0513 
0514 Entry::List AtticaProvider::installedEntries() const
0515 {
0516     Entry::List entries;
0517     for (const Entry &entry : std::as_const(mCachedEntries)) {
0518         if (entry.status() == KNSCore::Entry::Installed || entry.status() == KNSCore::Entry::Updateable) {
0519             entries.append(entry);
0520         }
0521     }
0522     return entries;
0523 }
0524 
0525 void AtticaProvider::vote(const Entry &entry, uint rating)
0526 {
0527     PostJob *job = m_provider.voteForContent(entry.uniqueId(), rating);
0528     connect(job, &BaseJob::finished, this, &AtticaProvider::votingFinished);
0529     job->start();
0530 }
0531 
0532 void AtticaProvider::votingFinished(Attica::BaseJob *job)
0533 {
0534     if (!jobSuccess(job)) {
0535         return;
0536     }
0537     Q_EMIT signalInformation(i18nc("voting for an item (good/bad)", "Your vote was recorded."));
0538 }
0539 
0540 void AtticaProvider::becomeFan(const Entry &entry)
0541 {
0542     PostJob *job = m_provider.becomeFan(entry.uniqueId());
0543     connect(job, &BaseJob::finished, this, &AtticaProvider::becomeFanFinished);
0544     job->start();
0545 }
0546 
0547 void AtticaProvider::becomeFanFinished(Attica::BaseJob *job)
0548 {
0549     if (!jobSuccess(job)) {
0550         return;
0551     }
0552     Q_EMIT signalInformation(i18n("You are now a fan."));
0553 }
0554 
0555 bool AtticaProvider::jobSuccess(Attica::BaseJob *job)
0556 {
0557     if (job->metadata().error() == Attica::Metadata::NoError) {
0558         return true;
0559     }
0560     qCDebug(KNEWSTUFFCORE) << "job error: " << job->metadata().error() << " status code: " << job->metadata().statusCode() << job->metadata().message();
0561 
0562     if (job->metadata().error() == Attica::Metadata::NetworkError) {
0563         if (job->metadata().statusCode() == 503) {
0564             QDateTime retryAfter;
0565             static const QByteArray retryAfterKey{"Retry-After"};
0566             for (const QNetworkReply::RawHeaderPair &headerPair : job->metadata().headers()) {
0567                 if (headerPair.first == retryAfterKey) {
0568                     // Retry-After is not a known header, so we need to do a bit of running around to make that work
0569                     // Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that
0570                     // So, simple workaround, just pass it through a dummy request and get a formatted date out (the
0571                     // cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...)
0572                     QNetworkRequest dummyRequest;
0573                     dummyRequest.setRawHeader(QByteArray{"Last-Modified"}, headerPair.second);
0574                     retryAfter = dummyRequest.header(QNetworkRequest::LastModifiedHeader).toDateTime();
0575                     break;
0576                 }
0577             }
0578             static const KFormat formatter;
0579             Q_EMIT signalErrorCode(KNSCore::ErrorCode::TryAgainLaterError,
0580                                    i18n("The service is currently undergoing maintenance and is expected to be back in %1.",
0581                                         formatter.formatSpelloutDuration(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch())),
0582                                    {retryAfter});
0583         } else {
0584             Q_EMIT signalErrorCode(KNSCore::ErrorCode::NetworkError,
0585                                    i18n("Network error %1: %2", job->metadata().statusCode(), job->metadata().statusString()),
0586                                    job->metadata().statusCode());
0587         }
0588     }
0589     if (job->metadata().error() == Attica::Metadata::OcsError) {
0590         if (job->metadata().statusCode() == 200) {
0591             Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
0592                                    i18n("Too many requests to server. Please try again in a few minutes."),
0593                                    job->metadata().statusCode());
0594         } else if (job->metadata().statusCode() == 405) {
0595             Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
0596                                    i18n("The Open Collaboration Services instance %1 does not support the attempted function.", name()),
0597                                    job->metadata().statusCode());
0598         } else {
0599             Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
0600                                    i18n("Unknown Open Collaboration Service API error. (%1)", job->metadata().statusCode()),
0601                                    job->metadata().statusCode());
0602         }
0603     }
0604 
0605     if (auto searchRequestVar = job->property("searchRequest"); searchRequestVar.isValid()) {
0606         SearchRequest req = searchRequestVar.value<SearchRequest>();
0607         Q_EMIT loadingFailed(req);
0608     }
0609     return false;
0610 }
0611 
0612 Entry AtticaProvider::entryFromAtticaContent(const Attica::Content &content)
0613 {
0614     Entry entry;
0615 
0616     entry.setProviderId(id());
0617     entry.setUniqueId(content.id());
0618     entry.setStatus(KNSCore::Entry::Downloadable);
0619     entry.setVersion(content.version());
0620     entry.setReleaseDate(content.updated().date());
0621     entry.setCategory(content.attribute(QStringLiteral("typeid")));
0622 
0623     int index = mCachedEntries.indexOf(entry);
0624     if (index >= 0) {
0625         Entry &cacheEntry = mCachedEntries[index];
0626         // check if updateable
0627         if (((cacheEntry.status() == KNSCore::Entry::Installed) || (cacheEntry.status() == KNSCore::Entry::Updateable))
0628             && ((cacheEntry.version() != entry.version()) || (cacheEntry.releaseDate() != entry.releaseDate()))) {
0629             cacheEntry.setStatus(KNSCore::Entry::Updateable);
0630             cacheEntry.setUpdateVersion(entry.version());
0631             cacheEntry.setUpdateReleaseDate(entry.releaseDate());
0632         }
0633         entry = cacheEntry;
0634     } else {
0635         mCachedEntries.append(entry);
0636     }
0637 
0638     entry.setName(content.name());
0639     entry.setHomepage(content.detailpage());
0640     entry.setRating(content.rating());
0641     entry.setNumberOfComments(content.numberOfComments());
0642     entry.setDownloadCount(content.downloads());
0643     entry.setNumberFans(content.attribute(QStringLiteral("fans")).toInt());
0644     entry.setDonationLink(content.attribute(QStringLiteral("donationpage")));
0645     entry.setKnowledgebaseLink(content.attribute(QStringLiteral("knowledgebasepage")));
0646     entry.setNumberKnowledgebaseEntries(content.attribute(QStringLiteral("knowledgebaseentries")).toInt());
0647     entry.setHomepage(content.detailpage());
0648 
0649     entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("1")), Entry::PreviewSmall1);
0650     entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("2")), Entry::PreviewSmall2);
0651     entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("3")), Entry::PreviewSmall3);
0652 
0653     entry.setPreviewUrl(content.previewPicture(QStringLiteral("1")), Entry::PreviewBig1);
0654     entry.setPreviewUrl(content.previewPicture(QStringLiteral("2")), Entry::PreviewBig2);
0655     entry.setPreviewUrl(content.previewPicture(QStringLiteral("3")), Entry::PreviewBig3);
0656 
0657     entry.setLicense(content.license());
0658     Author author;
0659     author.setId(content.author());
0660     author.setName(content.author());
0661     author.setHomepage(content.attribute(QStringLiteral("profilepage")));
0662     entry.setAuthor(author);
0663 
0664     entry.setSource(Entry::Online);
0665     entry.setSummary(content.description());
0666     entry.setShortSummary(content.summary());
0667     entry.setChangelog(content.changelog());
0668     entry.setTags(content.tags());
0669 
0670     entry.clearDownloadLinkInformation();
0671     const QList<Attica::DownloadDescription> descs = content.downloadUrlDescriptions();
0672     for (const Attica::DownloadDescription &desc : descs) {
0673         Entry::DownloadLinkInformation info;
0674         info.name = desc.name();
0675         info.priceAmount = desc.priceAmount();
0676         info.distributionType = desc.distributionType();
0677         info.descriptionLink = desc.link();
0678         info.id = desc.id();
0679         info.size = desc.size();
0680         info.isDownloadtypeLink = desc.type() == Attica::DownloadDescription::LinkDownload;
0681         info.tags = desc.tags();
0682         entry.appendDownloadLinkInformation(info);
0683     }
0684 
0685     return entry;
0686 }
0687 
0688 } // namespace
0689 
0690 #include "moc_atticaprovider_p.cpp"