1 /*
2     This file is part of Akregator.
3 
4     SPDX-FileCopyrightText: 2004 Frank Osterfeld <osterfeld@kde.org>
5 
6     SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
7 */
8 
9 #include "feedlist.h"
10 #include "article.h"
11 #include "feed.h"
12 #include "folder.h"
13 #include "storage.h"
14 #include "treenode.h"
15 #include "treenodevisitor.h"
16 
17 #include "akregator_debug.h"
18 #include "kernel.h"
19 #include "subscriptionlistjobs.h"
20 #include <KLocalizedString>
21 #include <limits>
22 
23 #include <QElapsedTimer>
24 #include <QHash>
25 #include <QRandomGenerator>
26 #include <QSet>
27 #include <qdom.h>
28 
29 using namespace Akregator;
30 class Akregator::FeedListPrivate
31 {
32     FeedList *const q;
33 
34 public:
35     FeedListPrivate(Backend::Storage *st, FeedList *qq);
36 
37     Akregator::Backend::Storage *storage;
38     QList<TreeNode *> flatList;
39     Folder *rootNode;
40     QHash<uint, TreeNode *> idMap;
41     FeedList::AddNodeVisitor *addNodeVisitor;
42     FeedList::RemoveNodeVisitor *removeNodeVisitor;
43     QHash<QString, QList<Feed *>> urlMap;
44     mutable int unreadCache;
45 };
46 
47 class FeedList::AddNodeVisitor : public TreeNodeVisitor
48 {
49 public:
AddNodeVisitor(FeedList * list)50     AddNodeVisitor(FeedList *list)
51         : m_list(list)
52     {
53     }
54 
visitFeed(Feed * node)55     bool visitFeed(Feed *node) override
56     {
57         m_list->d->idMap.insert(node->id(), node);
58         m_list->d->flatList.append(node);
59         m_list->d->urlMap[node->xmlUrl()].append(node);
60         connect(node, &Feed::fetchStarted, m_list, &FeedList::fetchStarted);
61         connect(node, &Feed::fetched, m_list, &FeedList::fetched);
62         connect(node, &Feed::fetchAborted, m_list, &FeedList::fetchAborted);
63         connect(node, &Feed::fetchError, m_list, &FeedList::fetchError);
64         connect(node, &Feed::fetchDiscovery, m_list, &FeedList::fetchDiscovery);
65 
66         visitTreeNode(node);
67         return true;
68     }
69 
visit2(TreeNode * node,bool preserveID)70     void visit2(TreeNode *node, bool preserveID)
71     {
72         m_preserveID = preserveID;
73         TreeNodeVisitor::visit(node);
74     }
75 
visitTreeNode(TreeNode * node)76     bool visitTreeNode(TreeNode *node) override
77     {
78         if (!m_preserveID) {
79             node->setId(m_list->generateID());
80         }
81         m_list->d->idMap[node->id()] = node;
82         m_list->d->flatList.append(node);
83 
84         connect(node, &TreeNode::signalDestroyed, m_list, &FeedList::slotNodeDestroyed);
85         connect(node, &TreeNode::signalChanged, m_list, &FeedList::signalNodeChanged);
86         Q_EMIT m_list->signalNodeAdded(node);
87 
88         return true;
89     }
90 
visitFolder(Folder * node)91     bool visitFolder(Folder *node) override
92     {
93         connect(node, &Folder::signalChildAdded, m_list, &FeedList::slotNodeAdded);
94         connect(node, &Folder::signalAboutToRemoveChild, m_list, &FeedList::signalAboutToRemoveNode);
95         connect(node, &Folder::signalChildRemoved, m_list, &FeedList::slotNodeRemoved);
96 
97         visitTreeNode(node);
98 
99         for (TreeNode *i = node->firstChild(); i && i != node; i = i->next()) {
100             m_list->slotNodeAdded(i);
101         }
102 
103         return true;
104     }
105 
106 private:
107     FeedList *m_list = nullptr;
108     bool m_preserveID = false;
109 };
110 
111 class FeedList::RemoveNodeVisitor : public TreeNodeVisitor
112 {
113 public:
RemoveNodeVisitor(FeedList * list)114     RemoveNodeVisitor(FeedList *list)
115         : m_list(list)
116     {
117     }
118 
visitFeed(Feed * node)119     bool visitFeed(Feed *node) override
120     {
121         visitTreeNode(node);
122         m_list->d->urlMap[node->xmlUrl()].removeAll(node);
123         return true;
124     }
125 
visitTreeNode(TreeNode * node)126     bool visitTreeNode(TreeNode *node) override
127     {
128         m_list->d->idMap.remove(node->id());
129         m_list->d->flatList.removeAll(node);
130         m_list->disconnect(node);
131         return true;
132     }
133 
visitFolder(Folder * node)134     bool visitFolder(Folder *node) override
135     {
136         visitTreeNode(node);
137 
138         return true;
139     }
140 
141 private:
142     FeedList *m_list;
143 };
144 
FeedListPrivate(Backend::Storage * st,FeedList * qq)145 FeedListPrivate::FeedListPrivate(Backend::Storage *st, FeedList *qq)
146     : q(qq)
147     , storage(st)
148     , rootNode(nullptr)
149     , addNodeVisitor(new FeedList::AddNodeVisitor(q))
150     , removeNodeVisitor(new FeedList::RemoveNodeVisitor(q))
151     , unreadCache(-1)
152 {
153     Q_ASSERT(storage);
154 }
155 
FeedList(Backend::Storage * storage)156 FeedList::FeedList(Backend::Storage *storage)
157     : QObject(nullptr)
158     , d(new FeedListPrivate(storage, this))
159 {
160     auto rootNode = new Folder(i18n("All Feeds"));
161     rootNode->setId(1);
162     setRootNode(rootNode);
163     addNode(rootNode, true);
164 }
165 
feedIds() const166 QVector<uint> FeedList::feedIds() const
167 {
168     QVector<uint> ids;
169     const auto f = feeds();
170     for (const Feed *const i : f) {
171         ids += i->id();
172     }
173     return ids;
174 }
175 
feeds() const176 QVector<const Akregator::Feed *> FeedList::feeds() const
177 {
178     QVector<const Akregator::Feed *> constList;
179     const auto rootNodeFeeds = d->rootNode->feeds();
180     for (const Akregator::Feed *const i : rootNodeFeeds) {
181         constList.append(i);
182     }
183     return constList;
184 }
185 
feeds()186 QVector<Akregator::Feed *> FeedList::feeds()
187 {
188     return d->rootNode->feeds();
189 }
190 
folders() const191 QVector<const Folder *> FeedList::folders() const
192 {
193     QVector<const Folder *> constList;
194     const auto nodeFolders = d->rootNode->folders();
195     for (const Folder *const i : nodeFolders) {
196         constList.append(i);
197     }
198     return constList;
199 }
200 
folders()201 QVector<Folder *> FeedList::folders()
202 {
203     return d->rootNode->folders();
204 }
205 
addNode(TreeNode * node,bool preserveID)206 void FeedList::addNode(TreeNode *node, bool preserveID)
207 {
208     d->addNodeVisitor->visit2(node, preserveID);
209 }
210 
removeNode(TreeNode * node)211 void FeedList::removeNode(TreeNode *node)
212 {
213     d->removeNodeVisitor->visit(node);
214 }
215 
parseChildNodes(QDomNode & node,Folder * parent)216 void FeedList::parseChildNodes(QDomNode &node, Folder *parent)
217 {
218     QDomElement e = node.toElement(); // try to convert the node to an element.
219 
220     if (!e.isNull()) {
221         // QString title = e.hasAttribute("text") ? e.attribute("text") : e.attribute("title");
222 
223         if (e.hasAttribute(QStringLiteral("xmlUrl")) || e.hasAttribute(QStringLiteral("xmlurl")) || e.hasAttribute(QStringLiteral("xmlURL"))) {
224             Feed *feed = Feed::fromOPML(e, d->storage);
225             if (feed) {
226                 if (!d->urlMap[feed->xmlUrl()].contains(feed)) {
227                     d->urlMap[feed->xmlUrl()].append(feed);
228                 }
229                 parent->appendChild(feed);
230             }
231         } else {
232             Folder *fg = Folder::fromOPML(e);
233             parent->appendChild(fg);
234 
235             if (e.hasChildNodes()) {
236                 QDomNode child = e.firstChild();
237                 while (!child.isNull()) {
238                     parseChildNodes(child, fg);
239                     child = child.nextSibling();
240                 }
241             }
242         }
243     }
244 }
245 
readFromOpml(const QDomDocument & doc)246 bool FeedList::readFromOpml(const QDomDocument &doc)
247 {
248     QDomElement root = doc.documentElement();
249 
250     qCDebug(AKREGATOR_LOG) << "loading OPML feed" << root.tagName().toLower();
251 
252     qCDebug(AKREGATOR_LOG) << "measuring startup time: START";
253     QElapsedTimer spent;
254     spent.start();
255 
256     if (root.tagName().toLower() != QLatin1String("opml")) {
257         return false;
258     }
259     QDomNode bodyNode = root.firstChild();
260 
261     while (!bodyNode.isNull() && bodyNode.toElement().tagName().toLower() != QLatin1String("body")) {
262         bodyNode = bodyNode.nextSibling();
263     }
264 
265     if (bodyNode.isNull()) {
266         qCDebug(AKREGATOR_LOG) << "Failed to acquire body node, markup broken?";
267         return false;
268     }
269 
270     QDomElement body = bodyNode.toElement();
271 
272     QDomNode i = body.firstChild();
273 
274     while (!i.isNull()) {
275         parseChildNodes(i, allFeedsFolder());
276         i = i.nextSibling();
277     }
278 
279     for (TreeNode *i = allFeedsFolder()->firstChild(); i && i != allFeedsFolder(); i = i->next()) {
280         if (i->id() == 0) {
281             uint id = generateID();
282             i->setId(id);
283             d->idMap.insert(id, i);
284         }
285     }
286 
287     qCDebug(AKREGATOR_LOG) << "measuring startup time: STOP," << spent.elapsed() << "ms";
288     qCDebug(AKREGATOR_LOG) << "Number of articles loaded:" << allFeedsFolder()->totalCount();
289     return true;
290 }
291 
~FeedList()292 FeedList::~FeedList()
293 {
294     Q_EMIT signalDestroyed(this);
295     setRootNode(nullptr);
296     delete d->addNodeVisitor;
297     delete d->removeNodeVisitor;
298 }
299 
findByURL(const QString & feedURL) const300 const Akregator::Feed *FeedList::findByURL(const QString &feedURL) const
301 {
302     if (!d->urlMap.contains(feedURL)) {
303         return nullptr;
304     }
305     const QList<Feed *> &v = d->urlMap[feedURL];
306     return !v.isEmpty() ? v.front() : nullptr;
307 }
308 
findByURL(const QString & feedURL)309 Akregator::Feed *FeedList::findByURL(const QString &feedURL)
310 {
311     if (!d->urlMap.contains(feedURL)) {
312         return nullptr;
313     }
314     const QList<Akregator::Feed *> &v = d->urlMap[feedURL];
315     return !v.isEmpty() ? v.front() : nullptr;
316 }
317 
findArticle(const QString & feedURL,const QString & guid) const318 const Article FeedList::findArticle(const QString &feedURL, const QString &guid) const
319 {
320     const Akregator::Feed *feed = findByURL(feedURL);
321     return feed ? feed->findArticle(guid) : Article();
322 }
323 
append(FeedList * list,Folder * parent,TreeNode * after)324 void FeedList::append(FeedList *list, Folder *parent, TreeNode *after)
325 {
326     if (list == this) {
327         return;
328     }
329 
330     if (!d->flatList.contains(parent)) {
331         parent = allFeedsFolder();
332     }
333 
334     QList<TreeNode *> children = list->allFeedsFolder()->children();
335 
336     QList<TreeNode *>::ConstIterator end(children.constEnd());
337     for (QList<TreeNode *>::ConstIterator it = children.constBegin(); it != end; ++it) {
338         list->allFeedsFolder()->removeChild(*it);
339         parent->insertChild(*it, after);
340         after = *it;
341     }
342 }
343 
toOpml() const344 QDomDocument FeedList::toOpml() const
345 {
346     QDomDocument doc;
347     doc.appendChild(doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")));
348 
349     QDomElement root = doc.createElement(QStringLiteral("opml"));
350     root.setAttribute(QStringLiteral("version"), QStringLiteral("1.0"));
351     doc.appendChild(root);
352 
353     QDomElement head = doc.createElement(QStringLiteral("head"));
354     root.appendChild(head);
355 
356     QDomElement ti = doc.createElement(QStringLiteral("text"));
357     head.appendChild(ti);
358 
359     QDomElement body = doc.createElement(QStringLiteral("body"));
360     root.appendChild(body);
361 
362     const auto children = allFeedsFolder()->children();
363     for (const TreeNode *const i : children) {
364         body.appendChild(i->toOPML(body, doc));
365     }
366 
367     return doc;
368 }
369 
findByID(uint id) const370 const TreeNode *FeedList::findByID(uint id) const
371 {
372     return d->idMap[id];
373 }
374 
findByID(uint id)375 TreeNode *FeedList::findByID(uint id)
376 {
377     return d->idMap[id];
378 }
379 
findByTitle(const QString & title) const380 QList<const TreeNode *> FeedList::findByTitle(const QString &title) const
381 {
382     return allFeedsFolder()->namedChildren(title);
383 }
384 
findByTitle(const QString & title)385 QList<TreeNode *> FeedList::findByTitle(const QString &title)
386 {
387     return allFeedsFolder()->namedChildren(title);
388 }
389 
allFeedsFolder() const390 const Folder *FeedList::allFeedsFolder() const
391 {
392     return d->rootNode;
393 }
394 
allFeedsFolder()395 Folder *FeedList::allFeedsFolder()
396 {
397     return d->rootNode;
398 }
399 
isEmpty() const400 bool FeedList::isEmpty() const
401 {
402     return d->rootNode->firstChild() == nullptr;
403 }
404 
rootNodeChanged()405 void FeedList::rootNodeChanged()
406 {
407     Q_ASSERT(d->rootNode);
408     const int newUnread = d->rootNode->unread();
409     if (newUnread == d->unreadCache) {
410         return;
411     }
412     d->unreadCache = newUnread;
413     Q_EMIT unreadCountChanged(newUnread);
414 }
415 
setRootNode(Folder * folder)416 void FeedList::setRootNode(Folder *folder)
417 {
418     if (folder == d->rootNode) {
419         return;
420     }
421 
422     delete d->rootNode;
423     d->rootNode = folder;
424     d->unreadCache = -1;
425 
426     if (d->rootNode) {
427         d->rootNode->setOpen(true);
428         connect(d->rootNode, &Folder::signalChildAdded, this, &FeedList::slotNodeAdded);
429         connect(d->rootNode, &Folder::signalAboutToRemoveChild, this, &FeedList::signalAboutToRemoveNode);
430         connect(d->rootNode, &Folder::signalChildRemoved, this, &FeedList::slotNodeRemoved);
431         connect(d->rootNode, &Folder::signalChanged, this, &FeedList::signalNodeChanged);
432         connect(d->rootNode, &Folder::signalChanged, this, &FeedList::rootNodeChanged);
433     }
434 }
435 
generateID() const436 uint FeedList::generateID() const
437 {
438     // The values 0 and 1 are reserved, see TreeNode::id()
439     return QRandomGenerator::global()->bounded(2u, std::numeric_limits<quint32>::max());
440 }
441 
slotNodeAdded(TreeNode * node)442 void FeedList::slotNodeAdded(TreeNode *node)
443 {
444     if (!node) {
445         return;
446     }
447 
448     Folder *parent = node->parent();
449     if (!parent || !d->flatList.contains(parent) || d->flatList.contains(node)) {
450         return;
451     }
452 
453     addNode(node, false);
454 }
455 
slotNodeDestroyed(TreeNode * node)456 void FeedList::slotNodeDestroyed(TreeNode *node)
457 {
458     if (!node || !d->flatList.contains(node)) {
459         return;
460     }
461     removeNode(node);
462 }
463 
slotNodeRemoved(Folder *,TreeNode * node)464 void FeedList::slotNodeRemoved(Folder * /*parent*/, TreeNode *node)
465 {
466     if (!node || !d->flatList.contains(node)) {
467         return;
468     }
469     removeNode(node);
470     Q_EMIT signalNodeRemoved(node);
471 }
472 
unread() const473 int FeedList::unread() const
474 {
475     if (d->unreadCache == -1) {
476         d->unreadCache = d->rootNode ? d->rootNode->unread() : 0;
477     }
478     return d->unreadCache;
479 }
480 
addToFetchQueue(FetchQueue * qu,bool intervalOnly)481 void FeedList::addToFetchQueue(FetchQueue *qu, bool intervalOnly)
482 {
483     if (d->rootNode) {
484         d->rootNode->slotAddToFetchQueue(qu, intervalOnly);
485     }
486 }
487 
createMarkAsReadJob()488 KJob *FeedList::createMarkAsReadJob()
489 {
490     return d->rootNode ? d->rootNode->createMarkAsReadJob() : nullptr;
491 }
492 
FeedListManagementImpl(const QSharedPointer<FeedList> & list)493 FeedListManagementImpl::FeedListManagementImpl(const QSharedPointer<FeedList> &list)
494     : m_feedList(list)
495 {
496 }
497 
setFeedList(const QSharedPointer<FeedList> & list)498 void FeedListManagementImpl::setFeedList(const QSharedPointer<FeedList> &list)
499 {
500     m_feedList = list;
501 }
502 
path_of_folder(const Folder * fol)503 static QString path_of_folder(const Folder *fol)
504 {
505     Q_ASSERT(fol);
506     QString path;
507     const Folder *i = fol;
508     while (i) {
509         path = QString::number(i->id()) + QLatin1Char('/') + path;
510         i = i->parent();
511     }
512     return path;
513 }
514 
categories() const515 QStringList FeedListManagementImpl::categories() const
516 {
517     if (!m_feedList) {
518         return QStringList();
519     }
520     QStringList cats;
521     const auto folders = m_feedList->folders();
522     for (const Folder *const i : folders) {
523         cats.append(path_of_folder(i));
524     }
525     return cats;
526 }
527 
feeds(const QString & catId) const528 QStringList FeedListManagementImpl::feeds(const QString &catId) const
529 {
530     if (!m_feedList) {
531         return QStringList();
532     }
533 
534     const uint lastcatid = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts).last().toUInt();
535 
536     QSet<QString> urls;
537     const auto feeds = m_feedList->feeds();
538     for (const Feed *const i : feeds) {
539         if (lastcatid == i->parent()->id()) {
540             urls.insert(i->xmlUrl());
541         }
542     }
543     return urls.values();
544 }
545 
addFeed(const QString & url,const QString & catId)546 void FeedListManagementImpl::addFeed(const QString &url, const QString &catId)
547 {
548     if (!m_feedList) {
549         return;
550     }
551 
552     qCDebug(AKREGATOR_LOG) << "Name:" << url.left(20) << "Cat:" << catId;
553     const uint folder_id = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts).last().toUInt();
554 
555     // Get the folder
556     Folder *m_folder = nullptr;
557     const QVector<Folder *> vector = m_feedList->folders();
558     for (int i = 0; i < vector.size(); ++i) {
559         if (vector.at(i)->id() == folder_id) {
560             m_folder = vector.at(i);
561             break;
562         }
563     }
564 
565     // Create new feed
566     QScopedPointer<FeedList> new_feedlist(new FeedList(Kernel::self()->storage()));
567     Feed *new_feed = new Feed(Kernel::self()->storage());
568     new_feed->setXmlUrl(url);
569     // new_feed->setTitle(url);
570     new_feedlist->allFeedsFolder()->appendChild(new_feed);
571 
572     // Get last in the folder
573     TreeNode *m_last = m_folder->childAt(m_folder->totalCount());
574 
575     // Add the feed
576     m_feedList->append(new_feedlist.data(), m_folder, m_last);
577 }
578 
removeFeed(const QString & url,const QString & catId)579 void FeedListManagementImpl::removeFeed(const QString &url, const QString &catId)
580 {
581     qCDebug(AKREGATOR_LOG) << "Name:" << url.left(20) << "Cat:" << catId;
582 
583     uint lastcatid = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts).last().toUInt();
584 
585     const auto feeds = m_feedList->feeds();
586     for (const Feed *const i : feeds) {
587         if (lastcatid == i->parent()->id()) {
588             if (i->xmlUrl().compare(url) == 0) {
589                 qCDebug(AKREGATOR_LOG) << "id:" << i->id();
590                 auto job = new DeleteSubscriptionJob;
591                 job->setSubscriptionId(i->id());
592                 job->start();
593             }
594         }
595     }
596 }
597 
getCategoryName(const QString & catId) const598 QString FeedListManagementImpl::getCategoryName(const QString &catId) const
599 {
600     QString catname;
601 
602     if (!m_feedList) {
603         return catname;
604     }
605 
606     const QStringList list = catId.split(QLatin1Char('/'), Qt::SkipEmptyParts);
607     for (int i = 0; i < list.size(); ++i) {
608         int index = list.at(i).toInt();
609         catname += m_feedList->findByID(index)->title() + QLatin1Char('/');
610     }
611 
612     return catname;
613 }
614