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