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