1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2018 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 **************************************************************************/
19 
20 #include "FeedsManager.h"
21 #include "Application.h"
22 #include "BookmarksManager.h"
23 #include "Console.h"
24 #include "FeedParser.h"
25 #include "Job.h"
26 #include "LongTermTimer.h"
27 #include "NotificationsManager.h"
28 #include "SessionsManager.h"
29 #include "Utils.h"
30 
31 #include <QtCore/QFile>
32 #include <QtCore/QJsonArray>
33 #include <QtCore/QJsonDocument>
34 #include <QtCore/QJsonObject>
35 #include <QtCore/QSaveFile>
36 
37 namespace Otter
38 {
39 
Feed(const QString & title,const QUrl & url,const QIcon & icon,int updateInterval,QObject * parent)40 Feed::Feed(const QString &title, const QUrl &url, const QIcon &icon, int updateInterval, QObject *parent) : QObject(parent),
41 	m_updateTimer(nullptr),
42 	m_parser(nullptr),
43 	m_title(title),
44 	m_url(url),
45 	m_icon(icon),
46 	m_error(NoError),
47 	m_updateInterval(0),
48 	m_isUpdating(false)
49 {
50 	setUpdateInterval(updateInterval);
51 }
52 
markEntryAsRead(const QString & identifier)53 void Feed::markEntryAsRead(const QString &identifier)
54 {
55 	for (int i = 0; i < m_entries.count(); ++i)
56 	{
57 		if (m_entries.at(i).identifier == identifier)
58 		{
59 			m_entries[i].lastReadTime = QDateTime::currentDateTimeUtc();
60 
61 			emit feedModified(this);
62 
63 			break;
64 		}
65 	}
66 }
67 
markEntryAsRemoved(const QString & identifier)68 void Feed::markEntryAsRemoved(const QString &identifier)
69 {
70 	if (!m_removedEntries.contains(identifier))
71 	{
72 		for (int i = 0; i < m_entries.count(); ++i)
73 		{
74 			if (m_entries.at(i).identifier == identifier)
75 			{
76 				m_entries.removeAt(i);
77 
78 				m_removedEntries.append(identifier);
79 
80 				emit feedModified(this);
81 
82 				break;
83 			}
84 		}
85 	}
86 }
87 
setTitle(const QString & title)88 void Feed::setTitle(const QString &title)
89 {
90 	if (title != m_title)
91 	{
92 		m_title = title;
93 
94 		emit feedModified(this);
95 	}
96 }
97 
setDescription(const QString & description)98 void Feed::setDescription(const QString &description)
99 {
100 	if (description != m_description)
101 	{
102 		m_description = description;
103 
104 		emit feedModified(this);
105 	}
106 }
107 
setUrl(const QUrl & url)108 void Feed::setUrl(const QUrl &url)
109 {
110 	if (url != m_url)
111 	{
112 		m_url = url;
113 
114 		update();
115 
116 		emit feedModified(this);
117 	}
118 }
119 
setIcon(const QIcon & icon)120 void Feed::setIcon(const QIcon &icon)
121 {
122 	m_icon = icon;
123 
124 	emit feedModified(this);
125 }
126 
setLastUpdateTime(const QDateTime & time)127 void Feed::setLastUpdateTime(const QDateTime &time)
128 {
129 	m_lastUpdateTime = time;
130 }
131 
setLastSynchronizationTime(const QDateTime & time)132 void Feed::setLastSynchronizationTime(const QDateTime &time)
133 {
134 	m_lastSynchronizationTime = time;
135 }
136 
setCategories(const QMap<QString,QString> & categories)137 void Feed::setCategories(const QMap<QString, QString> &categories)
138 {
139 	m_categories = categories;
140 }
141 
setRemovedEntries(const QStringList & removedEntries)142 void Feed::setRemovedEntries(const QStringList &removedEntries)
143 {
144 	m_removedEntries = removedEntries;
145 }
146 
setEntries(const QVector<Feed::Entry> & entries)147 void Feed::setEntries(const QVector<Feed::Entry> &entries)
148 {
149 	m_entries = entries;
150 }
151 
setUpdateInterval(int interval)152 void Feed::setUpdateInterval(int interval)
153 {
154 	if (interval != m_updateInterval)
155 	{
156 		m_updateInterval = interval;
157 
158 		if (interval <= 0 && m_updateTimer)
159 		{
160 			m_updateTimer->deleteLater();
161 			m_updateTimer = nullptr;
162 		}
163 		else
164 		{
165 			if (!m_updateTimer)
166 			{
167 				m_updateTimer = new LongTermTimer(this);
168 
169 				connect(m_updateTimer, &LongTermTimer::timeout, this, &Feed::update);
170 			}
171 
172 			m_updateTimer->start(static_cast<quint64>(interval) * 60000);
173 		}
174 
175 		emit feedModified(this);
176 	}
177 }
178 
update()179 void Feed::update()
180 {
181 	if (m_parser)
182 	{
183 		return;
184 	}
185 
186 	m_error = NoError;
187 	m_isUpdating = true;
188 
189 	emit feedModified(this);
190 
191 	DataFetchJob *dataJob(new DataFetchJob(m_url, this));
192 
193 	connect(dataJob, &DataFetchJob::jobFinished, this, [=](bool isFetchSuccess)
194 	{
195 		if (isFetchSuccess)
196 		{
197 			m_parser = FeedParser::createParser(this, dataJob);
198 
199 			if (m_parser)
200 			{
201 				m_parser->moveToThread(&m_parserThread);
202 
203 				connect(m_parser, &FeedParser::parsingFinished, [&](bool isParsingSuccess)
204 				{
205 					const FeedParser::FeedInformation information(m_parser->getInformation());
206 
207 					if (!isParsingSuccess)
208 					{
209 						m_error = ParseError;
210 					}
211 
212 					if (m_icon.isNull() && information.icon.isValid())
213 					{
214 						IconFetchJob *iconJob(new IconFetchJob(information.icon, this));
215 
216 						connect(iconJob, &IconFetchJob::jobFinished, this, [=]()
217 						{
218 							setIcon(iconJob->getIcon());
219 						});
220 
221 						iconJob->start();
222 					}
223 
224 					if (m_title.isEmpty())
225 					{
226 						m_title = information.title;
227 					}
228 
229 					if (m_description.isEmpty())
230 					{
231 						m_description = information.description;
232 					}
233 
234 					if (!information.entries.isEmpty())
235 					{
236 						QStringList existingRemovedEntries;
237 						int amount(0);
238 
239 						for (int i = (information.entries.count() - 1); i >= 0; --i)
240 						{
241 							Feed::Entry entry(information.entries.at(i));
242 
243 							if (m_removedEntries.contains(entry.identifier))
244 							{
245 								existingRemovedEntries.append(entry.identifier);
246 							}
247 							else
248 							{
249 								bool hasEntry(false);
250 
251 								for (int j = 0; j < m_entries.count(); ++j)
252 								{
253 									const Feed::Entry existingEntry(m_entries.at(j));
254 
255 									if (existingEntry.identifier == entry.identifier)
256 									{
257 										if ((entry.publicationTime.isValid() && existingEntry.publicationTime != entry.publicationTime) || (entry.updateTime.isValid() && existingEntry.updateTime != entry.updateTime))
258 										{
259 											++amount;
260 										}
261 
262 										entry.lastReadTime = existingEntry.lastReadTime;
263 										entry.publicationTime = normalizeTime(entry.publicationTime);
264 
265 										if (entry.updateTime.isValid())
266 										{
267 											entry.updateTime = normalizeTime(entry.updateTime);
268 										}
269 
270 										m_entries[j] = entry;
271 
272 										hasEntry = true;
273 
274 										break;
275 									}
276 								}
277 
278 								if (!hasEntry)
279 								{
280 									++amount;
281 
282 									entry.publicationTime = normalizeTime(entry.publicationTime);
283 									entry.updateTime = normalizeTime(entry.updateTime);
284 
285 									m_entries.prepend(entry);
286 								}
287 							}
288 						}
289 
290 						m_removedEntries = existingRemovedEntries;
291 
292 						if (amount > 0)
293 						{
294 							connect(NotificationsManager::createNotification(NotificationsManager::FeedUpdatedEvent, tr("Feed updated:\n%1").arg(getTitle()), Notification::InformationLevel, this), &Notification::clicked, [&]()
295 							{
296 								Application::getInstance()->triggerAction(ActionsManager::OpenUrlAction, {{QLatin1String("url"), QUrl(QLatin1String("view-feed:") + getUrl().toDisplayString())}});
297 							});
298 						}
299 
300 						emit entriesModified(this);
301 					}
302 
303 					m_mimeType = information.mimeType;
304 					m_lastSynchronizationTime = QDateTime::currentDateTimeUtc();
305 					m_lastUpdateTime = information.lastUpdateTime;
306 					m_categories = information.categories;
307 
308 					m_parserThread.exit();
309 
310 					m_parser->deleteLater();
311 					m_parser = nullptr;
312 
313 					m_isUpdating = false;
314 
315 					emit feedModified(this);
316 				});
317 
318 				m_parserThread.start();
319 
320 				m_parser->parse(dataJob);
321 			}
322 			else
323 			{
324 				m_error = ParseError;
325 				m_isUpdating = false;
326 
327 				Console::addMessage(tr("Failed to parse feed: invalid feed type"), Console::NetworkCategory, Console::ErrorLevel, m_url.toDisplayString());
328 
329 				emit feedModified(this);
330 			}
331 		}
332 		else
333 		{
334 			m_error = DownloadError;
335 			m_isUpdating = false;
336 
337 			Console::addMessage(tr("Failed to download feed"), Console::NetworkCategory, Console::ErrorLevel, m_url.toDisplayString());
338 
339 			emit feedModified(this);
340 		}
341 	});
342 
343 	dataJob->start();
344 }
345 
getTitle() const346 QString Feed::getTitle() const
347 {
348 	return m_title;
349 }
350 
getDescription() const351 QString Feed::getDescription() const
352 {
353 	return m_description;
354 }
355 
getUrl() const356 QUrl Feed::getUrl() const
357 {
358 	return m_url;
359 }
360 
getIcon() const361 QIcon Feed::getIcon() const
362 {
363 	return m_icon;
364 }
365 
getLastUpdateTime() const366 QDateTime Feed::getLastUpdateTime() const
367 {
368 	return m_lastUpdateTime;
369 }
370 
getLastSynchronizationTime() const371 QDateTime Feed::getLastSynchronizationTime() const
372 {
373 	return m_lastSynchronizationTime;
374 }
375 
normalizeTime(const QDateTime & time) const376 QDateTime Feed::normalizeTime(const QDateTime &time) const
377 {
378 	return ((time.isValid() && time < QDateTime::currentDateTimeUtc()) ? time : QDateTime::currentDateTimeUtc());
379 }
380 
getMimeType() const381 QMimeType Feed::getMimeType() const
382 {
383 	return m_mimeType;
384 }
385 
getCategories() const386 QMap<QString, QString> Feed::getCategories() const
387 {
388 	return m_categories;
389 }
390 
getRemovedEntries() const391 QStringList Feed::getRemovedEntries() const
392 {
393 	return m_removedEntries;
394 }
395 
getEntries(const QStringList & categories) const396 QVector<Feed::Entry> Feed::getEntries(const QStringList &categories) const
397 {
398 	if (!categories.isEmpty())
399 	{
400 		QVector<Entry> entries;
401 		entries.reserve(entries.count() / 2);
402 
403 		for (int i = 0; i < m_entries.count(); ++i)
404 		{
405 			const Feed::Entry entry(m_entries.at(i));
406 
407 			if (!entry.categories.isEmpty())
408 			{
409 				for (int j = 0; j < categories.count(); ++j)
410 				{
411 					if (entry.categories.contains(categories.at(j)))
412 					{
413 						entries.append(entry);
414 
415 						break;
416 					}
417 				}
418 			}
419 		}
420 
421 		entries.squeeze();
422 
423 		return entries;
424 	}
425 
426 	return m_entries;
427 }
428 
getError() const429 Feed::FeedError Feed::getError() const
430 {
431 	return m_error;
432 }
433 
getUpdateInterval() const434 int Feed::getUpdateInterval() const
435 {
436 	return m_updateInterval;
437 }
438 
isUpdating() const439 bool Feed::isUpdating() const
440 {
441 	return m_isUpdating;
442 }
443 
444 FeedsManager* FeedsManager::m_instance(nullptr);
445 FeedsModel* FeedsManager::m_model(nullptr);
446 QVector<Feed*> FeedsManager::m_feeds;
447 bool FeedsManager::m_isInitialized(false);
448 
FeedsManager(QObject * parent)449 FeedsManager::FeedsManager(QObject *parent) : QObject(parent),
450 	m_saveTimer(0)
451 {
452 }
453 
timerEvent(QTimerEvent * event)454 void FeedsManager::timerEvent(QTimerEvent *event)
455 {
456 	if (event->timerId() == m_saveTimer)
457 	{
458 		killTimer(m_saveTimer);
459 
460 		m_saveTimer = 0;
461 
462 		if (m_model)
463 		{
464 			m_model->save(SessionsManager::getWritableDataPath(QLatin1String("feeds.opml")));
465 		}
466 
467 		if (SessionsManager::isReadOnly())
468 		{
469 			return;
470 		}
471 
472 		QSaveFile file(SessionsManager::getWritableDataPath(QLatin1String("feeds.json")));
473 
474 		if (!file.open(QIODevice::WriteOnly))
475 		{
476 			return;
477 		}
478 
479 		QJsonArray feedsArray;
480 
481 		for (int i = 0; i < m_feeds.count(); ++i)
482 		{
483 			const Feed *feed(m_feeds.at(i));
484 
485 			if (!FeedsManager::getModel()->hasFeed(feed->getUrl()) && !BookmarksManager::getModel()->hasFeed(feed->getUrl()))
486 			{
487 				continue;
488 			}
489 
490 			const QMap<QString, QString> categories(feed->getCategories());
491 			QJsonObject feedObject({{QLatin1String("title"), feed->getTitle()}, {QLatin1String("url"), feed->getUrl().toString()}, {QLatin1String("updateInterval"), QString::number(feed->getUpdateInterval())}, {QLatin1String("lastSynchronizationTime"), feed->getLastUpdateTime().toString(Qt::ISODate)}, {QLatin1String("lastUpdateTime"), feed->getLastSynchronizationTime().toString(Qt::ISODate)}});
492 
493 			if (!feed->getDescription().isEmpty())
494 			{
495 				feedObject.insert(QLatin1String("description"), feed->getDescription());
496 			}
497 
498 			if (!feed->getIcon().isNull())
499 			{
500 				feedObject.insert(QLatin1String("icon"), Utils::savePixmapAsDataUri(feed->getIcon().pixmap(feed->getIcon().availableSizes().value(0, QSize(16, 16)))));
501 			}
502 
503 			if (!categories.isEmpty())
504 			{
505 				QMap<QString, QString>::const_iterator iterator;
506 				QJsonObject categoriesObject;
507 
508 				for (iterator = categories.begin(); iterator != categories.end(); ++iterator)
509 				{
510 					categoriesObject.insert(iterator.key(), iterator.value());
511 				}
512 
513 				feedObject.insert(QLatin1String("categories"), categoriesObject);
514 			}
515 
516 			if (!feed->getRemovedEntries().isEmpty())
517 			{
518 				feedObject.insert(QLatin1String("removedEntries"), QJsonArray::fromStringList(feed->getRemovedEntries()));
519 			}
520 
521 			const QVector<Feed::Entry> entries(feed->getEntries());
522 			QJsonArray entriesArray;
523 
524 			for (int j = 0; j < entries.count(); ++j)
525 			{
526 				const Feed::Entry entry(entries.at(j));
527 				QJsonObject entryObject({{QLatin1String("identifier"), entry.identifier}, {QLatin1String("title"), entry.title}});
528 
529 				if (!entry.summary.isEmpty())
530 				{
531 					entryObject.insert(QLatin1String("summary"), entry.summary);
532 				}
533 
534 				if (!entry.content.isEmpty())
535 				{
536 					entryObject.insert(QLatin1String("content"), entry.content);
537 				}
538 
539 				if (!entry.author.isEmpty())
540 				{
541 					entryObject.insert(QLatin1String("author"), entry.author);
542 				}
543 
544 				if (!entry.email.isEmpty())
545 				{
546 					entryObject.insert(QLatin1String("email"), entry.email);
547 				}
548 
549 				if (!entry.url.isEmpty())
550 				{
551 					entryObject.insert(QLatin1String("url"), entry.url.toString());
552 				}
553 
554 				if (entry.lastReadTime.isValid())
555 				{
556 					entryObject.insert(QLatin1String("lastReadTime"), entry.lastReadTime.toString(Qt::ISODate));
557 				}
558 
559 				if (entry.publicationTime.isValid())
560 				{
561 					entryObject.insert(QLatin1String("publicationTime"), entry.publicationTime.toString(Qt::ISODate));
562 				}
563 
564 				if (entry.updateTime.isValid())
565 				{
566 					entryObject.insert(QLatin1String("updateTime"), entry.updateTime.toString(Qt::ISODate));
567 				}
568 
569 				if (!entry.categories.isEmpty())
570 				{
571 					entryObject.insert(QLatin1String("categories"), QJsonArray::fromStringList(entry.categories));
572 				}
573 
574 				entriesArray.append(entryObject);
575 			}
576 
577 			feedObject.insert(QLatin1String("entries"), entriesArray);
578 
579 			feedsArray.append(feedObject);
580 		}
581 
582 		QJsonDocument document;
583 		document.setArray(feedsArray);
584 
585 		file.write(document.toJson());
586 		file.commit();
587 	}
588 }
589 
createInstance()590 void FeedsManager::createInstance()
591 {
592 	if (!m_instance)
593 	{
594 		m_instance = new FeedsManager(QCoreApplication::instance());
595 	}
596 }
597 
ensureInitialized()598 void FeedsManager::ensureInitialized()
599 {
600 	if (m_isInitialized)
601 	{
602 		return;
603 	}
604 
605 	m_isInitialized = true;
606 
607 	QFile file(SessionsManager::getWritableDataPath(QLatin1String("feeds.json")));
608 
609 	if (file.open(QIODevice::ReadOnly))
610 	{
611 		const QJsonArray feedsArray(QJsonDocument::fromJson(file.readAll()).array());
612 
613 		for (int i = 0; i < feedsArray.count(); ++i)
614 		{
615 			const QJsonObject feedObject(feedsArray.at(i).toObject());
616 			Feed *feed(createFeed(QUrl(feedObject.value(QLatin1String("url")).toString()), feedObject.value(QLatin1String("title")).toString(), Utils::loadPixmapFromDataUri(feedObject.value(QLatin1String("icon")).toString()), feedObject.value(QLatin1String("updateInterval")).toInt()));
617 			feed->setDescription(feedObject.value(QLatin1String("description")).toString());
618 			feed->setLastUpdateTime(QDateTime::fromString(feedObject.value(QLatin1String("lastUpdateTime")).toString(), Qt::ISODate));
619 			feed->setLastSynchronizationTime(QDateTime::fromString(feedObject.value(QLatin1String("lastSynchronizationTime")).toString(), Qt::ISODate));
620 			feed->setRemovedEntries(feedObject.value(QLatin1String("removedEntries")).toVariant().toStringList());
621 
622 			if (feedObject.contains(QLatin1String("categories")))
623 			{
624 				QMap<QString, QString> categories;
625 				const QVariantMap rawCategories(feedObject.value(QLatin1String("categories")).toVariant().toMap());
626 				QVariantMap::const_iterator iterator;
627 
628 				for (iterator = rawCategories.begin(); iterator != rawCategories.end(); ++iterator)
629 				{
630 					categories[iterator.key()] = iterator.value().toString();
631 				}
632 
633 				feed->setCategories(categories);
634 			}
635 
636 			const QJsonArray entriesArray(feedObject.value(QLatin1String("entries")).toArray());
637 			QVector<Feed::Entry> entries;
638 			entries.reserve(entriesArray.count());
639 
640 			for (int j = 0; j < entriesArray.count(); ++j)
641 			{
642 				const QJsonObject entryObject(entriesArray.at(j).toObject());
643 				Feed::Entry entry;
644 				entry.identifier = entryObject.value(QLatin1String("identifier")).toString();
645 				entry.title = entryObject.value(QLatin1String("title")).toString();
646 				entry.summary = entryObject.value(QLatin1String("summary")).toString();
647 				entry.content = entryObject.value(QLatin1String("content")).toString();
648 				entry.author = entryObject.value(QLatin1String("author")).toString();
649 				entry.email = entryObject.value(QLatin1String("email")).toString();
650 				entry.url = entryObject.value(QLatin1String("url")).toString();
651 				entry.lastReadTime = QDateTime::fromString(entryObject.value(QLatin1String("lastReadTime")).toString(), Qt::ISODate);
652 				entry.publicationTime = QDateTime::fromString(entryObject.value(QLatin1String("publicationTime")).toString(), Qt::ISODate);
653 				entry.updateTime = QDateTime::fromString(entryObject.value(QLatin1String("updateTime")).toString(), Qt::ISODate);
654 				entry.categories = entryObject.value(QLatin1String("categories")).toVariant().toStringList();
655 
656 				entries.append(entry);
657 			}
658 
659 			feed->setEntries(entries);
660 		}
661 	}
662 
663 	if (!m_model)
664 	{
665 		m_model = new FeedsModel(SessionsManager::getWritableDataPath(QLatin1String("feeds.opml")), m_instance);
666 
667 		connect(m_model, &FeedsModel::modelModified, m_instance, &FeedsManager::scheduleSave);
668 	}
669 }
670 
scheduleSave()671 void FeedsManager::scheduleSave()
672 {
673 	if (m_saveTimer == 0)
674 	{
675 		m_saveTimer = startTimer(1000);
676 	}
677 }
678 
handleFeedModified(Feed * feed)679 void FeedsManager::handleFeedModified(Feed *feed)
680 {
681 	if (feed)
682 	{
683 		emit feedModified(feed->getUrl());
684 	}
685 
686 	scheduleSave();
687 }
688 
getInstance()689 FeedsManager* FeedsManager::getInstance()
690 {
691 	return m_instance;
692 }
693 
getModel()694 FeedsModel* FeedsManager::getModel()
695 {
696 	ensureInitialized();
697 
698 	return m_model;
699 }
700 
createFeed(const QUrl & url,const QString & title,const QIcon & icon,int updateInterval)701 Feed* FeedsManager::createFeed(const QUrl &url, const QString &title, const QIcon &icon, int updateInterval)
702 {
703 	ensureInitialized();
704 
705 	Feed *feed(getFeed(url));
706 
707 	if (feed)
708 	{
709 		return feed;
710 	}
711 
712 	feed = new Feed(title, url, icon, updateInterval, m_instance);
713 
714 	m_feeds.append(feed);
715 
716 	connect(feed, &Feed::feedModified, m_instance, &FeedsManager::handleFeedModified);
717 
718 	return feed;
719 }
720 
getFeed(const QUrl & url)721 Feed* FeedsManager::getFeed(const QUrl &url)
722 {
723 	ensureInitialized();
724 
725 	const QUrl normalizedUrl(Utils::normalizeUrl(url));
726 
727 	for (int i = 0; i < m_feeds.count(); ++i)
728 	{
729 		Feed *feed(m_feeds.at(i));
730 
731 		if (feed->getUrl() == url || feed->getUrl() == normalizedUrl)
732 		{
733 			return feed;
734 		}
735 	}
736 
737 	return nullptr;
738 }
739 
getFeeds()740 QVector<Feed*> FeedsManager::getFeeds()
741 {
742 	ensureInitialized();
743 
744 	return m_feeds;
745 }
746 
747 }
748