1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2017  Vladimir Golovnev <glassez@yandex.ru>
4  * Copyright (C) 2010  Christophe Dumez <chris@qbittorrent.org>
5  * Copyright (C) 2010  Arnaud Demaiziere <arnaud@qbittorrent.org>
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20  *
21  * In addition, as a special exception, the copyright holders give permission to
22  * link this program with the OpenSSL project's "OpenSSL" library (or with
23  * modified versions of it that use the same license as the "OpenSSL" library),
24  * and distribute the linked executables. You must obey the GNU General Public
25  * License in all respects for all of the code used other than "OpenSSL".  If you
26  * modify file(s), you may extend this exception to your version of the file(s),
27  * but you are not obligated to do so. If you do not wish to do so, delete this
28  * exception statement from your version.
29  */
30 
31 #include "rss_session.h"
32 
33 #include <QDebug>
34 #include <QJsonDocument>
35 #include <QJsonObject>
36 #include <QJsonValue>
37 #include <QSaveFile>
38 #include <QString>
39 #include <QThread>
40 
41 #include "../asyncfilestorage.h"
42 #include "../global.h"
43 #include "../logger.h"
44 #include "../profile.h"
45 #include "../settingsstorage.h"
46 #include "../utils/fs.h"
47 #include "rss_article.h"
48 #include "rss_feed.h"
49 #include "rss_folder.h"
50 #include "rss_item.h"
51 
52 const int MsecsPerMin = 60000;
53 const QString ConfFolderName(QStringLiteral("rss"));
54 const QString DataFolderName(QStringLiteral("rss/articles"));
55 const QString FeedsFileName(QStringLiteral("feeds.json"));
56 
57 const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/Session/EnableProcessing"));
58 const QString SettingsKey_RefreshInterval(QStringLiteral("RSS/Session/RefreshInterval"));
59 const QString SettingsKey_MaxArticlesPerFeed(QStringLiteral("RSS/Session/MaxArticlesPerFeed"));
60 
61 using namespace RSS;
62 
63 QPointer<Session> Session::m_instance = nullptr;
64 
Session()65 Session::Session()
66     : m_processingEnabled(SettingsStorage::instance()->loadValue(SettingsKey_ProcessingEnabled, false))
67     , m_workingThread(new QThread(this))
68     , m_refreshInterval(SettingsStorage::instance()->loadValue(SettingsKey_RefreshInterval, 30))
69     , m_maxArticlesPerFeed(SettingsStorage::instance()->loadValue(SettingsKey_MaxArticlesPerFeed, 50))
70 {
71     Q_ASSERT(!m_instance); // only one instance is allowed
72     m_instance = this;
73 
74     m_confFileStorage = new AsyncFileStorage(
75                 Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Config) + ConfFolderName));
76     m_confFileStorage->moveToThread(m_workingThread);
77     connect(m_workingThread, &QThread::finished, m_confFileStorage, &AsyncFileStorage::deleteLater);
78     connect(m_confFileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString)
79     {
80         Logger::instance()->addMessage(QString("Couldn't save RSS Session configuration in %1. Error: %2")
81                                        .arg(fileName, errorString), Log::WARNING);
82     });
83 
84     m_dataFileStorage = new AsyncFileStorage(
85                 Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Data) + DataFolderName));
86     m_dataFileStorage->moveToThread(m_workingThread);
87     connect(m_workingThread, &QThread::finished, m_dataFileStorage, &AsyncFileStorage::deleteLater);
88     connect(m_dataFileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString)
89     {
90         Logger::instance()->addMessage(QString("Couldn't save RSS Session data in %1. Error: %2")
91                                        .arg(fileName, errorString), Log::WARNING);
92     });
93 
94     m_itemsByPath.insert("", new Folder); // root folder
95 
96     m_workingThread->start();
97     load();
98 
99     connect(&m_refreshTimer, &QTimer::timeout, this, &Session::refresh);
100     if (m_processingEnabled)
101     {
102         m_refreshTimer.start(m_refreshInterval * MsecsPerMin);
103         refresh();
104     }
105 
106     // Remove legacy/corrupted settings
107     // (at least on Windows, QSettings is case-insensitive and it can get
108     // confused when asked about settings that differ only in their case)
109     auto settingsStorage = SettingsStorage::instance();
110     settingsStorage->removeValue("Rss/streamList");
111     settingsStorage->removeValue("Rss/streamAlias");
112     settingsStorage->removeValue("Rss/open_folders");
113     settingsStorage->removeValue("Rss/qt5/splitter_h");
114     settingsStorage->removeValue("Rss/qt5/splitterMain");
115     settingsStorage->removeValue("Rss/hosts_cookies");
116     settingsStorage->removeValue("RSS/streamList");
117     settingsStorage->removeValue("RSS/streamAlias");
118     settingsStorage->removeValue("RSS/open_folders");
119     settingsStorage->removeValue("RSS/qt5/splitter_h");
120     settingsStorage->removeValue("RSS/qt5/splitterMain");
121     settingsStorage->removeValue("RSS/hosts_cookies");
122     settingsStorage->removeValue("Rss/Session/EnableProcessing");
123     settingsStorage->removeValue("Rss/Session/RefreshInterval");
124     settingsStorage->removeValue("Rss/Session/MaxArticlesPerFeed");
125     settingsStorage->removeValue("Rss/AutoDownloader/EnableProcessing");
126 }
127 
~Session()128 Session::~Session()
129 {
130     qDebug() << "Deleting RSS Session...";
131 
132     m_workingThread->quit();
133     m_workingThread->wait();
134 
135     //store();
136     delete m_itemsByPath[""]; // deleting root folder
137 
138     qDebug() << "RSS Session deleted.";
139 }
140 
instance()141 Session *Session::instance()
142 {
143     return m_instance;
144 }
145 
addFolder(const QString & path,QString * error)146 bool Session::addFolder(const QString &path, QString *error)
147 {
148     Folder *destFolder = prepareItemDest(path, error);
149     if (!destFolder)
150         return false;
151 
152     addItem(new Folder(path), destFolder);
153     store();
154     return true;
155 }
156 
addFeed(const QString & url,const QString & path,QString * error)157 bool Session::addFeed(const QString &url, const QString &path, QString *error)
158 {
159     if (m_feedsByURL.contains(url))
160     {
161         if (error)
162             *error = tr("RSS feed with given URL already exists: %1.").arg(url);
163         return false;
164     }
165 
166     Folder *destFolder = prepareItemDest(path, error);
167     if (!destFolder)
168         return false;
169 
170     addItem(new Feed(generateUID(), url, path, this), destFolder);
171     store();
172     if (m_processingEnabled)
173         feedByURL(url)->refresh();
174     return true;
175 }
176 
moveItem(const QString & itemPath,const QString & destPath,QString * error)177 bool Session::moveItem(const QString &itemPath, const QString &destPath, QString *error)
178 {
179     if (itemPath.isEmpty())
180     {
181         if (error)
182             *error = tr("Cannot move root folder.");
183         return false;
184     }
185 
186     auto item = m_itemsByPath.value(itemPath);
187     if (!item)
188     {
189         if (error)
190             *error = tr("Item doesn't exist: %1.").arg(itemPath);
191         return false;
192     }
193 
194     return moveItem(item, destPath, error);
195 }
196 
moveItem(Item * item,const QString & destPath,QString * error)197 bool Session::moveItem(Item *item, const QString &destPath, QString *error)
198 {
199     Q_ASSERT(item);
200     Q_ASSERT(item != rootFolder());
201 
202     Folder *destFolder = prepareItemDest(destPath, error);
203     if (!destFolder)
204         return false;
205 
206     auto srcFolder = static_cast<Folder *>(m_itemsByPath.value(Item::parentPath(item->path())));
207     if (srcFolder != destFolder)
208     {
209         srcFolder->removeItem(item);
210         destFolder->addItem(item);
211     }
212     m_itemsByPath.insert(destPath, m_itemsByPath.take(item->path()));
213     item->setPath(destPath);
214     store();
215     return true;
216 }
217 
removeItem(const QString & itemPath,QString * error)218 bool Session::removeItem(const QString &itemPath, QString *error)
219 {
220     if (itemPath.isEmpty())
221     {
222         if (error)
223             *error = tr("Cannot delete root folder.");
224         return false;
225     }
226 
227     auto item = m_itemsByPath.value(itemPath);
228     if (!item)
229     {
230         if (error)
231             *error = tr("Item doesn't exist: %1.").arg(itemPath);
232         return false;
233     }
234 
235     emit itemAboutToBeRemoved(item);
236     item->cleanup();
237 
238     auto folder = static_cast<Folder *>(m_itemsByPath.value(Item::parentPath(item->path())));
239     folder->removeItem(item);
240     delete item;
241     store();
242     return true;
243 }
244 
items() const245 QList<Item *> Session::items() const
246 {
247     return m_itemsByPath.values();
248 }
249 
itemByPath(const QString & path) const250 Item *Session::itemByPath(const QString &path) const
251 {
252     return m_itemsByPath.value(path);
253 }
254 
load()255 void Session::load()
256 {
257     QFile itemsFile(m_confFileStorage->storageDir().absoluteFilePath(FeedsFileName));
258     if (!itemsFile.exists())
259     {
260         loadLegacy();
261         return;
262     }
263 
264     if (!itemsFile.open(QFile::ReadOnly))
265     {
266         Logger::instance()->addMessage(
267                     QString("Couldn't read RSS Session data from %1. Error: %2")
268                     .arg(itemsFile.fileName(), itemsFile.errorString()), Log::WARNING);
269         return;
270     }
271 
272     QJsonParseError jsonError;
273     const QJsonDocument jsonDoc = QJsonDocument::fromJson(itemsFile.readAll(), &jsonError);
274     if (jsonError.error != QJsonParseError::NoError)
275     {
276         Logger::instance()->addMessage(
277                     QString("Couldn't parse RSS Session data from %1. Error: %2")
278                     .arg(itemsFile.fileName(), jsonError.errorString()), Log::WARNING);
279         return;
280     }
281 
282     if (!jsonDoc.isObject())
283     {
284         Logger::instance()->addMessage(
285                     QString("Couldn't load RSS Session data from %1. Invalid data format.")
286                     .arg(itemsFile.fileName()), Log::WARNING);
287         return;
288     }
289 
290     loadFolder(jsonDoc.object(), rootFolder());
291 }
292 
loadFolder(const QJsonObject & jsonObj,Folder * folder)293 void Session::loadFolder(const QJsonObject &jsonObj, Folder *folder)
294 {
295     bool updated = false;
296     for (const QString &key : asConst(jsonObj.keys()))
297     {
298         const QJsonValue val {jsonObj[key]};
299         if (val.isString())
300         {
301             // previous format (reduced form) doesn't contain UID
302             QString url = val.toString();
303             if (url.isEmpty())
304                 url = key;
305             addFeedToFolder(generateUID(), url, key, folder);
306             updated = true;
307         }
308         else if (val.isObject())
309         {
310             const QJsonObject valObj {val.toObject()};
311             if (valObj.contains("url"))
312             {
313                 if (!valObj["url"].isString())
314                 {
315                     LogMsg(tr("Couldn't load RSS Feed '%1'. URL is required.")
316                            .arg(QString("%1\\%2").arg(folder->path(), key)), Log::WARNING);
317                     continue;
318                 }
319 
320                 QUuid uid;
321                 if (valObj.contains("uid"))
322                 {
323                     uid = QUuid {valObj["uid"].toString()};
324                     if (uid.isNull())
325                     {
326                         LogMsg(tr("Couldn't load RSS Feed '%1'. UID is invalid.")
327                                .arg(QString("%1\\%2").arg(folder->path(), key)), Log::WARNING);
328                         continue;
329                     }
330 
331                     if (m_feedsByUID.contains(uid))
332                     {
333                         LogMsg(tr("Duplicate RSS Feed UID: %1. Configuration seems to be corrupted.")
334                                .arg(uid.toString()), Log::WARNING);
335                         continue;
336                     }
337                 }
338                 else
339                 {
340                     // previous format doesn't contain UID
341                     uid = generateUID();
342                     updated = true;
343                 }
344 
345                 addFeedToFolder(uid, valObj["url"].toString(), key, folder);
346             }
347             else
348             {
349                 loadFolder(valObj, addSubfolder(key, folder));
350             }
351         }
352         else
353         {
354             LogMsg(tr("Couldn't load RSS Item '%1'. Invalid data format.")
355                    .arg(QString::fromLatin1("%1\\%2").arg(folder->path(), key)), Log::WARNING);
356         }
357     }
358 
359     if (updated)
360         store(); // convert to updated format
361 }
362 
loadLegacy()363 void Session::loadLegacy()
364 {
365     const auto legacyFeedPaths = SettingsStorage::instance()->loadValue<QStringList>("Rss/streamList");
366     const auto feedAliases = SettingsStorage::instance()->loadValue<QStringList>("Rss/streamAlias");
367     if (legacyFeedPaths.size() != feedAliases.size())
368     {
369         Logger::instance()->addMessage("Corrupted RSS list, not loading it.", Log::WARNING);
370         return;
371     }
372 
373     uint i = 0;
374     for (QString legacyPath : legacyFeedPaths)
375     {
376         if (Item::PathSeparator == QString(legacyPath[0]))
377             legacyPath.remove(0, 1);
378         const QString parentFolderPath = Item::parentPath(legacyPath);
379         const QString feedUrl = Item::relativeName(legacyPath);
380 
381         for (const QString &folderPath : asConst(Item::expandPath(parentFolderPath)))
382             addFolder(folderPath);
383 
384         const QString feedPath = feedAliases[i].isEmpty()
385                 ? legacyPath
386                 : Item::joinPath(parentFolderPath, feedAliases[i]);
387         addFeed(feedUrl, feedPath);
388         ++i;
389     }
390 
391     store(); // convert to new format
392 }
393 
store()394 void Session::store()
395 {
396     m_confFileStorage->store(FeedsFileName, QJsonDocument(rootFolder()->toJsonValue().toObject()).toJson());
397 }
398 
prepareItemDest(const QString & path,QString * error)399 Folder *Session::prepareItemDest(const QString &path, QString *error)
400 {
401     if (!Item::isValidPath(path))
402     {
403         if (error)
404             *error = tr("Incorrect RSS Item path: %1.").arg(path);
405         return nullptr;
406     }
407 
408     if (m_itemsByPath.contains(path))
409     {
410         if (error)
411             *error = tr("RSS item with given path already exists: %1.").arg(path);
412         return nullptr;
413     }
414 
415     const QString destFolderPath = Item::parentPath(path);
416     auto destFolder = qobject_cast<Folder *>(m_itemsByPath.value(destFolderPath));
417     if (!destFolder)
418     {
419         if (error)
420             *error = tr("Parent folder doesn't exist: %1.").arg(destFolderPath);
421         return nullptr;
422     }
423 
424     return destFolder;
425 }
426 
addSubfolder(const QString & name,Folder * parentFolder)427 Folder *Session::addSubfolder(const QString &name, Folder *parentFolder)
428 {
429     auto folder = new Folder(Item::joinPath(parentFolder->path(), name));
430     addItem(folder, parentFolder);
431     return folder;
432 }
433 
addFeedToFolder(const QUuid & uid,const QString & url,const QString & name,Folder * parentFolder)434 Feed *Session::addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder)
435 {
436     auto feed = new Feed(uid, url, Item::joinPath(parentFolder->path(), name), this);
437     addItem(feed, parentFolder);
438     return feed;
439 }
440 
addItem(Item * item,Folder * destFolder)441 void Session::addItem(Item *item, Folder *destFolder)
442 {
443     if (auto feed = qobject_cast<Feed *>(item))
444     {
445         connect(feed, &Feed::titleChanged, this, &Session::handleFeedTitleChanged);
446         connect(feed, &Feed::iconLoaded, this, &Session::feedIconLoaded);
447         connect(feed, &Feed::stateChanged, this, &Session::feedStateChanged);
448         m_feedsByUID[feed->uid()] = feed;
449         m_feedsByURL[feed->url()] = feed;
450     }
451 
452     connect(item, &Item::pathChanged, this, &Session::itemPathChanged);
453     connect(item, &Item::aboutToBeDestroyed, this, &Session::handleItemAboutToBeDestroyed);
454     m_itemsByPath[item->path()] = item;
455     destFolder->addItem(item);
456     emit itemAdded(item);
457 }
458 
isProcessingEnabled() const459 bool Session::isProcessingEnabled() const
460 {
461     return m_processingEnabled;
462 }
463 
setProcessingEnabled(bool enabled)464 void Session::setProcessingEnabled(bool enabled)
465 {
466     if (m_processingEnabled != enabled)
467     {
468         m_processingEnabled = enabled;
469         SettingsStorage::instance()->storeValue(SettingsKey_ProcessingEnabled, m_processingEnabled);
470         if (m_processingEnabled)
471         {
472             m_refreshTimer.start(m_refreshInterval * MsecsPerMin);
473             refresh();
474         }
475         else
476         {
477             m_refreshTimer.stop();
478         }
479 
480         emit processingStateChanged(m_processingEnabled);
481     }
482 }
483 
confFileStorage() const484 AsyncFileStorage *Session::confFileStorage() const
485 {
486     return m_confFileStorage;
487 }
488 
dataFileStorage() const489 AsyncFileStorage *Session::dataFileStorage() const
490 {
491     return m_dataFileStorage;
492 }
493 
rootFolder() const494 Folder *Session::rootFolder() const
495 {
496     return static_cast<Folder *>(m_itemsByPath.value(""));
497 }
498 
feeds() const499 QList<Feed *> Session::feeds() const
500 {
501     return m_feedsByURL.values();
502 }
503 
feedByURL(const QString & url) const504 Feed *Session::feedByURL(const QString &url) const
505 {
506     return m_feedsByURL.value(url);
507 }
508 
refreshInterval() const509 int Session::refreshInterval() const
510 {
511     return m_refreshInterval;
512 }
513 
setRefreshInterval(const int refreshInterval)514 void Session::setRefreshInterval(const int refreshInterval)
515 {
516     if (m_refreshInterval != refreshInterval)
517     {
518         SettingsStorage::instance()->storeValue(SettingsKey_RefreshInterval, refreshInterval);
519         m_refreshInterval = refreshInterval;
520         m_refreshTimer.start(m_refreshInterval * MsecsPerMin);
521     }
522 }
523 
workingThread() const524 QThread *Session::workingThread() const
525 {
526     return m_workingThread;
527 }
528 
handleItemAboutToBeDestroyed(Item * item)529 void Session::handleItemAboutToBeDestroyed(Item *item)
530 {
531     m_itemsByPath.remove(item->path());
532     auto feed = qobject_cast<Feed *>(item);
533     if (feed)
534     {
535         m_feedsByUID.remove(feed->uid());
536         m_feedsByURL.remove(feed->url());
537     }
538 }
539 
handleFeedTitleChanged(Feed * feed)540 void Session::handleFeedTitleChanged(Feed *feed)
541 {
542     if (feed->name() == feed->url())
543         // Now we have something better than a URL.
544         // Trying to rename feed...
545         moveItem(feed, Item::joinPath(Item::parentPath(feed->path()), feed->title()));
546 }
547 
generateUID() const548 QUuid Session::generateUID() const
549 {
550     QUuid uid = QUuid::createUuid();
551     while (m_feedsByUID.contains(uid))
552         uid = QUuid::createUuid();
553 
554     return uid;
555 }
556 
maxArticlesPerFeed() const557 int Session::maxArticlesPerFeed() const
558 {
559     return m_maxArticlesPerFeed;
560 }
561 
setMaxArticlesPerFeed(const int n)562 void Session::setMaxArticlesPerFeed(const int n)
563 {
564     if (m_maxArticlesPerFeed != n)
565     {
566         m_maxArticlesPerFeed = n;
567         SettingsStorage::instance()->storeValue(SettingsKey_MaxArticlesPerFeed, n);
568         emit maxArticlesPerFeedChanged(n);
569     }
570 }
571 
refresh()572 void Session::refresh()
573 {
574     // NOTE: Should we allow manually refreshing for disabled session?
575     rootFolder()->refresh();
576 }
577