File indexing completed on 2024-05-05 05:13:01

0001 /*
0002     This file is part of Akregator.
0003 
0004     SPDX-FileCopyrightText: 2004 Frank Osterfeld <osterfeld@kde.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0007 */
0008 
0009 #include "feedlist.h"
0010 #include "article.h"
0011 #include "feed.h"
0012 #include "folder.h"
0013 #include "storage/storage.h"
0014 #include "treenode.h"
0015 #include "treenodevisitor.h"
0016 
0017 #include "akregator_debug.h"
0018 #include "kernel.h"
0019 #include "subscriptionlistjobs.h"
0020 #include <KLocalizedString>
0021 #include <limits>
0022 
0023 #include <QElapsedTimer>
0024 #include <QHash>
0025 #include <QRandomGenerator>
0026 #include <QSet>
0027 #include <qdom.h>
0028 
0029 using namespace Akregator;
0030 class Akregator::FeedListPrivate
0031 {
0032     FeedList *const q;
0033 
0034 public:
0035     FeedListPrivate(Backend::Storage *st, FeedList *qq);
0036 
0037     Akregator::Backend::Storage *storage;
0038     QList<TreeNode *> flatList;
0039     Folder *rootNode;
0040     QHash<uint, TreeNode *> idMap;
0041     FeedList::AddNodeVisitor *addNodeVisitor;
0042     FeedList::RemoveNodeVisitor *removeNodeVisitor;
0043     QHash<QString, QList<Feed *>> urlMap;
0044     mutable int unreadCache;
0045 };
0046 
0047 class FeedList::AddNodeVisitor : public TreeNodeVisitor
0048 {
0049 public:
0050     AddNodeVisitor(FeedList *list)
0051         : m_list(list)
0052     {
0053     }
0054 
0055     bool visitFeed(Feed *node) override
0056     {
0057         m_list->d->idMap.insert(node->id(), node);
0058         m_list->d->flatList.append(node);
0059         m_list->d->urlMap[node->xmlUrl()].append(node);
0060         connect(node, &Feed::fetchStarted, m_list, &FeedList::fetchStarted);
0061         connect(node, &Feed::fetched, m_list, &FeedList::fetched);
0062         connect(node, &Feed::fetchAborted, m_list, &FeedList::fetchAborted);
0063         connect(node, &Feed::fetchError, m_list, &FeedList::fetchError);
0064         connect(node, &Feed::fetchDiscovery, m_list, &FeedList::fetchDiscovery);
0065 
0066         visitTreeNode(node);
0067         return true;
0068     }
0069 
0070     void visit2(TreeNode *node, bool preserveID)
0071     {
0072         m_preserveID = preserveID;
0073         TreeNodeVisitor::visit(node);
0074     }
0075 
0076     bool visitTreeNode(TreeNode *node) override
0077     {
0078         if (!m_preserveID) {
0079             node->setId(m_list->generateID());
0080         }
0081         m_list->d->idMap[node->id()] = node;
0082         m_list->d->flatList.append(node);
0083 
0084         connect(node, &TreeNode::signalDestroyed, m_list, &FeedList::slotNodeDestroyed);
0085         connect(node, &TreeNode::signalChanged, m_list, &FeedList::signalNodeChanged);
0086         Q_EMIT m_list->signalNodeAdded(node);
0087 
0088         return true;
0089     }
0090 
0091     bool visitFolder(Folder *node) override
0092     {
0093         connect(node, &Folder::signalChildAdded, m_list, &FeedList::slotNodeAdded);
0094         connect(node, &Folder::signalAboutToRemoveChild, m_list, &FeedList::signalAboutToRemoveNode);
0095         connect(node, &Folder::signalChildRemoved, m_list, &FeedList::slotNodeRemoved);
0096 
0097         visitTreeNode(node);
0098 
0099         for (TreeNode *i = node->firstChild(); i && i != node; i = i->next()) {
0100             m_list->slotNodeAdded(i);
0101         }
0102 
0103         return true;
0104     }
0105 
0106 private:
0107     FeedList *m_list = nullptr;
0108     bool m_preserveID = false;
0109 };
0110 
0111 class FeedList::RemoveNodeVisitor : public TreeNodeVisitor
0112 {
0113 public:
0114     RemoveNodeVisitor(FeedList *list)
0115         : m_list(list)
0116     {
0117     }
0118 
0119     bool visitFeed(Feed *node) override
0120     {
0121         visitTreeNode(node);
0122         m_list->d->urlMap[node->xmlUrl()].removeAll(node);
0123         return true;
0124     }
0125 
0126     bool visitTreeNode(TreeNode *node) override
0127     {
0128         m_list->d->idMap.remove(node->id());
0129         m_list->d->flatList.removeAll(node);
0130         m_list->disconnect(node);
0131         return true;
0132     }
0133 
0134     bool visitFolder(Folder *node) override
0135     {
0136         visitTreeNode(node);
0137 
0138         return true;
0139     }
0140 
0141 private:
0142     FeedList *m_list;
0143 };
0144 
0145 FeedListPrivate::FeedListPrivate(Backend::Storage *st, FeedList *qq)
0146     : q(qq)
0147     , storage(st)
0148     , rootNode(nullptr)
0149     , addNodeVisitor(new FeedList::AddNodeVisitor(q))
0150     , removeNodeVisitor(new FeedList::RemoveNodeVisitor(q))
0151     , unreadCache(-1)
0152 {
0153     Q_ASSERT(storage);
0154 }
0155 
0156 FeedList::FeedList(Backend::Storage *storage)
0157     : QObject(nullptr)
0158     , d(new FeedListPrivate(storage, this))
0159 {
0160     auto rootNode = new Folder(i18n("All Feeds"));
0161     rootNode->setId(1);
0162     setRootNode(rootNode);
0163     addNode(rootNode, true);
0164 }
0165 
0166 QList<uint> FeedList::feedIds() const
0167 {
0168     QList<uint> ids;
0169     const auto f = feeds();
0170     for (const Feed *const i : f) {
0171         ids += i->id();
0172     }
0173     return ids;
0174 }
0175 
0176 QList<const Akregator::Feed *> FeedList::feeds() const
0177 {
0178     QList<const Akregator::Feed *> constList;
0179     const auto rootNodeFeeds = d->rootNode->feeds();
0180     for (const Akregator::Feed *const i : rootNodeFeeds) {
0181         constList.append(i);
0182     }
0183     return constList;
0184 }
0185 
0186 QList<Akregator::Feed *> FeedList::feeds()
0187 {
0188     return d->rootNode->feeds();
0189 }
0190 
0191 QList<const Folder *> FeedList::folders() const
0192 {
0193     QList<const Folder *> constList;
0194     const auto nodeFolders = d->rootNode->folders();
0195     for (const Folder *const i : nodeFolders) {
0196         constList.append(i);
0197     }
0198     return constList;
0199 }
0200 
0201 QList<Folder *> FeedList::folders()
0202 {
0203     return d->rootNode->folders();
0204 }
0205 
0206 void FeedList::addNode(TreeNode *node, bool preserveID)
0207 {
0208     d->addNodeVisitor->visit2(node, preserveID);
0209 }
0210 
0211 void FeedList::removeNode(TreeNode *node)
0212 {
0213     d->removeNodeVisitor->visit(node);
0214 }
0215 
0216 void FeedList::parseChildNodes(QDomNode &node, Folder *parent)
0217 {
0218     QDomElement e = node.toElement(); // try to convert the node to an element.
0219 
0220     if (!e.isNull()) {
0221         // QString title = e.hasAttribute("text") ? e.attribute("text") : e.attribute("title");
0222 
0223         if (e.hasAttribute(QStringLiteral("xmlUrl")) || e.hasAttribute(QStringLiteral("xmlurl")) || e.hasAttribute(QStringLiteral("xmlURL"))) {
0224             Feed *feed = Feed::fromOPML(e, d->storage);
0225             if (feed) {
0226                 if (!d->urlMap[feed->xmlUrl()].contains(feed)) {
0227                     d->urlMap[feed->xmlUrl()].append(feed);
0228                 }
0229                 parent->appendChild(feed);
0230             }
0231         } else {
0232             Folder *fg = Folder::fromOPML(e);
0233             parent->appendChild(fg);
0234 
0235             if (e.hasChildNodes()) {
0236                 QDomNode child = e.firstChild();
0237                 while (!child.isNull()) {
0238                     parseChildNodes(child, fg);
0239                     child = child.nextSibling();
0240                 }
0241             }
0242         }
0243     }
0244 }
0245 
0246 bool FeedList::readFromOpml(const QDomDocument &doc)
0247 {
0248     QDomElement root = doc.documentElement();
0249 
0250     qCDebug(AKREGATOR_LOG) << "loading OPML feed" << root.tagName().toLower();
0251 
0252     qCDebug(AKREGATOR_LOG) << "measuring startup time: START";
0253     QElapsedTimer spent;
0254     spent.start();
0255 
0256     if (root.tagName().toLower() != QLatin1StringView("opml")) {
0257         return false;
0258     }
0259     QDomNode bodyNode = root.firstChild();
0260 
0261     while (!bodyNode.isNull() && bodyNode.toElement().tagName().toLower() != QLatin1StringView("body")) {
0262         bodyNode = bodyNode.nextSibling();
0263     }
0264 
0265     if (bodyNode.isNull()) {
0266         qCDebug(AKREGATOR_LOG) << "Failed to acquire body node, markup broken?";
0267         return false;
0268     }
0269 
0270     QDomElement body = bodyNode.toElement();
0271 
0272     QDomNode i = body.firstChild();
0273 
0274     while (!i.isNull()) {
0275         parseChildNodes(i, allFeedsFolder());
0276         i = i.nextSibling();
0277     }
0278 
0279     for (TreeNode *i = allFeedsFolder()->firstChild(); i && i != allFeedsFolder(); i = i->next()) {
0280         if (i->id() == 0) {
0281             uint id = generateID();
0282             i->setId(id);
0283             d->idMap.insert(id, i);
0284         }
0285     }
0286 
0287     qCDebug(AKREGATOR_LOG) << "measuring startup time: STOP," << spent.elapsed() << "ms";
0288     qCDebug(AKREGATOR_LOG) << "Number of articles loaded:" << allFeedsFolder()->totalCount();
0289     return true;
0290 }
0291 
0292 FeedList::~FeedList()
0293 {
0294     Q_EMIT signalDestroyed(this);
0295     setRootNode(nullptr);
0296     delete d->addNodeVisitor;
0297     delete d->removeNodeVisitor;
0298 }
0299 
0300 const Akregator::Feed *FeedList::findByURL(const QString &feedURL) const
0301 {
0302     if (!d->urlMap.contains(feedURL)) {
0303         return nullptr;
0304     }
0305     const QList<Feed *> &v = d->urlMap[feedURL];
0306     return !v.isEmpty() ? v.front() : nullptr;
0307 }
0308 
0309 Akregator::Feed *FeedList::findByURL(const QString &feedURL)
0310 {
0311     if (!d->urlMap.contains(feedURL)) {
0312         return nullptr;
0313     }
0314     const QList<Akregator::Feed *> &v = d->urlMap[feedURL];
0315     return !v.isEmpty() ? v.front() : nullptr;
0316 }
0317 
0318 const Article FeedList::findArticle(const QString &feedURL, const QString &guid) const
0319 {
0320     const Akregator::Feed *feed = findByURL(feedURL);
0321     return feed ? feed->findArticle(guid) : Article();
0322 }
0323 
0324 void FeedList::append(FeedList *list, Folder *parent, TreeNode *after)
0325 {
0326     if (list == this) {
0327         return;
0328     }
0329 
0330     if (!d->flatList.contains(parent)) {
0331         parent = allFeedsFolder();
0332     }
0333 
0334     QList<TreeNode *> children = list->allFeedsFolder()->children();
0335 
0336     QList<TreeNode *>::ConstIterator end(children.constEnd());
0337     for (QList<TreeNode *>::ConstIterator it = children.constBegin(); it != end; ++it) {
0338         list->allFeedsFolder()->removeChild(*it);
0339         parent->insertChild(*it, after);
0340         after = *it;
0341     }
0342 }
0343 
0344 QDomDocument FeedList::toOpml() const
0345 {
0346     QDomDocument doc;
0347     doc.appendChild(doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")));
0348 
0349     QDomElement root = doc.createElement(QStringLiteral("opml"));
0350     root.setAttribute(QStringLiteral("version"), QStringLiteral("1.0"));
0351     doc.appendChild(root);
0352 
0353     QDomElement head = doc.createElement(QStringLiteral("head"));
0354     root.appendChild(head);
0355 
0356     QDomElement ti = doc.createElement(QStringLiteral("text"));
0357     head.appendChild(ti);
0358 
0359     QDomElement body = doc.createElement(QStringLiteral("body"));
0360     root.appendChild(body);
0361 
0362     const auto children = allFeedsFolder()->children();
0363     for (const TreeNode *const i : children) {
0364         body.appendChild(i->toOPML(body, doc));
0365     }
0366 
0367     return doc;
0368 }
0369 
0370 const TreeNode *FeedList::findByID(uint id) const
0371 {
0372     return d->idMap[id];
0373 }
0374 
0375 TreeNode *FeedList::findByID(uint id)
0376 {
0377     return d->idMap[id];
0378 }
0379 
0380 QList<const TreeNode *> FeedList::findByTitle(const QString &title) const
0381 {
0382     return allFeedsFolder()->namedChildren(title);
0383 }
0384 
0385 QList<TreeNode *> FeedList::findByTitle(const QString &title)
0386 {
0387     return allFeedsFolder()->namedChildren(title);
0388 }
0389 
0390 const Folder *FeedList::allFeedsFolder() const
0391 {
0392     return d->rootNode;
0393 }
0394 
0395 Folder *FeedList::allFeedsFolder()
0396 {
0397     return d->rootNode;
0398 }
0399 
0400 bool FeedList::isEmpty() const
0401 {
0402     return d->rootNode->firstChild() == nullptr;
0403 }
0404 
0405 void FeedList::rootNodeChanged()
0406 {
0407     Q_ASSERT(d->rootNode);
0408     const int newUnread = d->rootNode->unread();
0409     if (newUnread == d->unreadCache) {
0410         return;
0411     }
0412     d->unreadCache = newUnread;
0413     Q_EMIT unreadCountChanged(newUnread);
0414 }
0415 
0416 void FeedList::setRootNode(Folder *folder)
0417 {
0418     if (folder == d->rootNode) {
0419         return;
0420     }
0421 
0422     delete d->rootNode;
0423     d->rootNode = folder;
0424     d->unreadCache = -1;
0425 
0426     if (d->rootNode) {
0427         d->rootNode->setOpen(true);
0428         connect(d->rootNode, &Folder::signalChildAdded, this, &FeedList::slotNodeAdded);
0429         connect(d->rootNode, &Folder::signalAboutToRemoveChild, this, &FeedList::signalAboutToRemoveNode);
0430         connect(d->rootNode, &Folder::signalChildRemoved, this, &FeedList::slotNodeRemoved);
0431         connect(d->rootNode, &Folder::signalChanged, this, &FeedList::signalNodeChanged);
0432         connect(d->rootNode, &Folder::signalChanged, this, &FeedList::rootNodeChanged);
0433     }
0434 }
0435 
0436 uint FeedList::generateID() const
0437 {
0438     // The values 0 and 1 are reserved, see TreeNode::id()
0439     return QRandomGenerator::global()->bounded(2u, std::numeric_limits<quint32>::max());
0440 }
0441 
0442 void FeedList::slotNodeAdded(TreeNode *node)
0443 {
0444     if (!node) {
0445         return;
0446     }
0447 
0448     Folder *parent = node->parent();
0449     if (!parent || !d->flatList.contains(parent) || d->flatList.contains(node)) {
0450         return;
0451     }
0452 
0453     addNode(node, false);
0454 }
0455 
0456 void FeedList::slotNodeDestroyed(TreeNode *node)
0457 {
0458     if (!node || !d->flatList.contains(node)) {
0459         return;
0460     }
0461     removeNode(node);
0462 }
0463 
0464 void FeedList::slotNodeRemoved(Folder * /*parent*/, TreeNode *node)
0465 {
0466     if (!node || !d->flatList.contains(node)) {
0467         return;
0468     }
0469     removeNode(node);
0470     Q_EMIT signalNodeRemoved(node);
0471 }
0472 
0473 int FeedList::unread() const
0474 {
0475     if (d->unreadCache == -1) {
0476         d->unreadCache = d->rootNode ? d->rootNode->unread() : 0;
0477     }
0478     return d->unreadCache;
0479 }
0480 
0481 void FeedList::addToFetchQueue(FetchQueue *qu, bool intervalOnly)
0482 {
0483     if (d->rootNode) {
0484         d->rootNode->slotAddToFetchQueue(qu, intervalOnly);
0485     }
0486 }
0487 
0488 KJob *FeedList::createMarkAsReadJob()
0489 {
0490     return d->rootNode ? d->rootNode->createMarkAsReadJob() : nullptr;
0491 }
0492 
0493 FeedListManagementImpl::FeedListManagementImpl(const QSharedPointer<FeedList> &list)
0494     : m_feedList(list)
0495 {
0496 }
0497 
0498 void FeedListManagementImpl::setFeedList(const QSharedPointer<FeedList> &list)
0499 {
0500     m_feedList = list;
0501 }
0502 
0503 static QString path_of_folder(const Folder *fol)
0504 {
0505     Q_ASSERT(fol);
0506     QString path;
0507     const Folder *i = fol;
0508     while (i) {
0509         path = QString::number(i->id()) + QLatin1Char('/') + path;
0510         i = i->parent();
0511     }
0512     return path;
0513 }
0514 
0515 QStringList FeedListManagementImpl::categories() const
0516 {
0517     if (!m_feedList) {
0518         return {};
0519     }
0520     QStringList cats;
0521     const auto folders = m_feedList->folders();
0522     for (const Folder *const i : folders) {
0523         cats.append(path_of_folder(i));
0524     }
0525     return cats;
0526 }
0527 
0528 QStringList FeedListManagementImpl::feeds(const QString &catId) const
0529 {
0530     if (!m_feedList) {
0531         return {};
0532     }
0533 
0534     const uint lastcatid = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts).last().toUInt();
0535 
0536     QSet<QString> urls;
0537     const auto feeds = m_feedList->feeds();
0538     for (const Feed *const i : feeds) {
0539         if (lastcatid == i->parent()->id()) {
0540             urls.insert(i->xmlUrl());
0541         }
0542     }
0543     return urls.values();
0544 }
0545 
0546 void FeedListManagementImpl::addFeed(const QString &url, const QString &catId)
0547 {
0548     if (!m_feedList) {
0549         return;
0550     }
0551 
0552     qCDebug(AKREGATOR_LOG) << "Name:" << url.left(20) << "Cat:" << catId;
0553     const uint folder_id = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts).last().toUInt();
0554 
0555     // Get the folder
0556     Folder *m_folder = nullptr;
0557     const QList<Folder *> vector = m_feedList->folders();
0558     for (int i = 0; i < vector.size(); ++i) {
0559         if (vector.at(i)->id() == folder_id) {
0560             m_folder = vector.at(i);
0561             break;
0562         }
0563     }
0564 
0565     // Create new feed
0566     QScopedPointer<FeedList> new_feedlist(new FeedList(Kernel::self()->storage()));
0567     Feed *new_feed = new Feed(Kernel::self()->storage());
0568     new_feed->setXmlUrl(url);
0569     // new_feed->setTitle(url);
0570     new_feedlist->allFeedsFolder()->appendChild(new_feed);
0571 
0572     // Get last in the folder
0573     TreeNode *m_last = m_folder->childAt(m_folder->totalCount());
0574 
0575     // Add the feed
0576     m_feedList->append(new_feedlist.data(), m_folder, m_last);
0577 }
0578 
0579 void FeedListManagementImpl::removeFeed(const QString &url, const QString &catId)
0580 {
0581     qCDebug(AKREGATOR_LOG) << "Name:" << url.left(20) << "Cat:" << catId;
0582 
0583     uint lastcatid = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts).last().toUInt();
0584 
0585     const auto feeds = m_feedList->feeds();
0586     for (const Feed *const i : feeds) {
0587         if (lastcatid == i->parent()->id()) {
0588             if (i->xmlUrl().compare(url) == 0) {
0589                 qCDebug(AKREGATOR_LOG) << "id:" << i->id();
0590                 auto job = new DeleteSubscriptionJob;
0591                 job->setSubscriptionId(i->id());
0592                 job->start();
0593             }
0594         }
0595     }
0596 }
0597 
0598 QString FeedListManagementImpl::getCategoryName(const QString &catId) const
0599 {
0600     QString catname;
0601 
0602     if (!m_feedList) {
0603         return catname;
0604     }
0605 
0606     const QStringList list = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts);
0607     for (int i = 0; i < list.size(); ++i) {
0608         int index = list.at(i).toInt();
0609         catname += m_feedList->findByID(index)->title() + QLatin1Char('/');
0610     }
0611 
0612     return catname;
0613 }
0614 
0615 #include "moc_feedlist.cpp"