1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include "miscellaneous/application.h"
4 
5 #include "3rd-party/boolinq/boolinq.h"
6 #include "dynamic-shortcuts/dynamicshortcuts.h"
7 #include "exceptions/applicationexception.h"
8 #include "gui/dialogs/formabout.h"
9 #include "gui/dialogs/formmain.h"
10 #include "gui/feedmessageviewer.h"
11 #include "gui/feedsview.h"
12 #include "gui/messagebox.h"
13 #include "gui/toolbars/statusbar.h"
14 #include "miscellaneous/feedreader.h"
15 #include "miscellaneous/iconfactory.h"
16 #include "miscellaneous/iofactory.h"
17 #include "miscellaneous/mutex.h"
18 #include "miscellaneous/notificationfactory.h"
19 #include "network-web/webfactory.h"
20 #include "services/abstract/serviceroot.h"
21 #include "services/owncloud/owncloudserviceentrypoint.h"
22 #include "services/standard/standardserviceentrypoint.h"
23 #include "services/standard/standardserviceroot.h"
24 #include "services/tt-rss/ttrssserviceentrypoint.h"
25 
26 #include <iostream>
27 
28 #include <QProcess>
29 #include <QSessionManager>
30 #include <QSslSocket>
31 #include <QTimer>
32 
33 #if defined(Q_OS_UNIX)
34 #include <QDBusConnection>
35 #include <QDBusMessage>
36 #endif
37 
38 #if defined(USE_WEBENGINE)
39 #include "network-web/adblock/adblockicon.h"
40 #include "network-web/adblock/adblockmanager.h"
41 #include "network-web/networkurlinterceptor.h"
42 
43 #include <QWebEngineDownloadItem>
44 #include <QWebEngineProfile>
45 #endif
46 
Application(const QString & id,int & argc,char ** argv)47 Application::Application(const QString& id, int& argc, char** argv)
48   : SingleApplication(id, argc, argv), m_updateFeedsLock(new Mutex()) {
49   parseCmdArgumentsFromMyInstance();
50   qInstallMessageHandler(performLogging);
51 
52   m_feedReader = nullptr;
53   m_quitLogicDone = false;
54   m_mainForm = nullptr;
55   m_trayIcon = nullptr;
56   m_settings = Settings::setupSettings(this);
57   m_webFactory = new WebFactory(this);
58   m_system = new SystemFactory(this);
59   m_skins = new SkinFactory(this);
60   m_localization = new Localization(this);
61   m_icons = new IconFactory(this);
62   m_database = new DatabaseFactory(this);
63   m_downloadManager = nullptr;
64   m_notifications = new NotificationFactory(this);
65   m_shouldRestart = false;
66 
67   determineFirstRuns();
68 
69   //: Abbreviation of language, e.g. en.
70   //: Use ISO 639-1 code here combined with ISO 3166-1 (alpha-2) code.
71   //: Examples: "cs", "en", "it", "cs_CZ", "en_GB", "en_US".
72   QObject::tr("LANG_ABBREV");
73 
74   //: Name of translator - optional.
75   QObject::tr("LANG_AUTHOR");
76 
77   connect(this, &Application::aboutToQuit, this, &Application::onAboutToQuit);
78   connect(this, &Application::commitDataRequest, this, &Application::onCommitData);
79   connect(this, &Application::saveStateRequest, this, &Application::onSaveState);
80 
81 #if defined(Q_OS_UNIX)
82   QString app_dir = QString::fromLocal8Bit(qgetenv("APPDIR"));
83 
84   if (!app_dir.isEmpty()) {
85     bool success = qputenv("GST_PLUGIN_SYSTEM_PATH_1_0",
86                            QSL("%1/usr/lib/gstreamer-1.0:%2").arg(app_dir,
87                                                                   QString::fromLocal8Bit(qgetenv("GST_PLUGIN_SYSTEM_PATH_1_0"))).toLocal8Bit());
88     success = qputenv("GST_PLUGIN_SCANNER_1_0",
89                       QSL("%1/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner").arg(app_dir).toLocal8Bit()) && success;
90     if (!success) {
91       qWarningNN << LOGSEC_CORE << "Unable to set up GStreamer environment.";
92     }
93   }
94 #endif
95 
96 #if defined(USE_WEBENGINE)
97   m_webFactory->urlIinterceptor()->load();
98 
99   connect(QWebEngineProfile::defaultProfile(), &QWebEngineProfile::downloadRequested, this, &Application::downloadRequested);
100   connect(m_webFactory->adBlock(), &AdBlockManager::processTerminated, this, &Application::onAdBlockFailure);
101 
102   QTimer::singleShot(3000, this, [=]() {
103     try {
104       m_webFactory->adBlock()->setEnabled(qApp->settings()->value(GROUP(AdBlock), SETTING(AdBlock::AdBlockEnabled)).toBool());
105     }
106     catch (...) {
107       onAdBlockFailure();
108     }
109   });
110 #endif
111 
112   m_webFactory->updateProxy();
113 
114   if (isFirstRun()) {
115     m_notifications->save({
116       Notification(Notification::Event::GeneralEvent, true),
117       Notification(Notification::Event::NewUnreadArticlesFetched, true,
118                    QSL("%1/notify.wav").arg(SOUNDS_BUILTIN_DIRECTORY)),
119       Notification(Notification::Event::NewAppVersionAvailable, true),
120       Notification(Notification::Event::LoginFailure, true)
121     }, settings());
122   }
123   else {
124     m_notifications->load(settings());
125   }
126 
127   QTimer::singleShot(1000, system(), &SystemFactory::checkForUpdatesOnStartup);
128 
129   qDebugNN << LOGSEC_CORE
130            << "OpenSSL version:"
131            << QUOTE_W_SPACE_DOT(QSslSocket::sslLibraryVersionString());
132 
133   qDebugNN << LOGSEC_CORE
134            << "OpenSSL supported:"
135            << QUOTE_W_SPACE_DOT(QSslSocket::supportsSsl());
136 }
137 
~Application()138 Application::~Application() {
139   qDebugNN << LOGSEC_CORE << "Destroying Application instance.";
140 }
141 
142 QString s_customLogFile = QString();
143 bool s_disableDebug = false;
144 
performLogging(QtMsgType type,const QMessageLogContext & context,const QString & msg)145 void Application::performLogging(QtMsgType type, const QMessageLogContext& context, const QString& msg) {
146 #ifndef QT_NO_DEBUG_OUTPUT
147   QString console_message = qFormatLogMessage(type, context, msg);
148 
149   if (!s_disableDebug) {
150     std::cerr << console_message.toStdString() << std::endl;
151   }
152 
153   if (!s_customLogFile.isEmpty()) {
154     QFile log_file(s_customLogFile);
155 
156     if (log_file.open(QFile::OpenModeFlag::Append | QFile::OpenModeFlag::Unbuffered)) {
157       log_file.write(console_message.toUtf8());
158       log_file.write(QSL("\r\n").toUtf8());
159       log_file.close();
160     }
161   }
162 
163   if (type == QtMsgType::QtFatalMsg) {
164     qApp->exit(EXIT_FAILURE);
165   }
166 #else
167   Q_UNUSED(type)
168   Q_UNUSED(context)
169   Q_UNUSED(msg)
170 #endif
171 }
172 
reactOnForeignNotifications()173 void Application::reactOnForeignNotifications() {
174   connect(this, &Application::messageReceived, this, &Application::parseCmdArgumentsFromOtherInstance);
175 }
176 
hideOrShowMainForm()177 void Application::hideOrShowMainForm() {
178   // Display main window.
179   if (qApp->settings()->value(GROUP(GUI), SETTING(GUI::MainWindowStartsHidden)).toBool() &&
180       SystemTrayIcon::isSystemTrayDesired() &&
181       SystemTrayIcon::isSystemTrayAreaAvailable()) {
182     qDebugNN << LOGSEC_CORE << "Hiding the main window when the application is starting.";
183     mainForm()->switchVisibility(true);
184   }
185   else {
186     qDebugNN << LOGSEC_CORE << "Showing the main window when the application is starting.";
187     mainForm()->show();
188   }
189 }
190 
loadDynamicShortcuts()191 void Application::loadDynamicShortcuts() {
192   DynamicShortcuts::load(userActions());
193 }
194 
showPolls() const195 void Application::showPolls() const {
196   if(isFirstRunCurrentVersion()) {
197     qApp->showGuiMessage(Notification::Event::NewAppVersionAvailable,
198                          tr("RSS Guard has Discord server!"),
199                          tr("You can visit it now! Click me!"),
200                          QSystemTrayIcon::MessageIcon::Information,
201                          true,
202                          {},
203                          tr("Go to Discord!"),
204                          [this]() {
205       web()->openUrlInExternalBrowser(QSL("https://discord.gg/7xbVMPPNqH"));
206     });
207   }
208 }
209 
offerChanges() const210 void Application::offerChanges() const {
211   if (isFirstRunCurrentVersion()) {
212     qApp->showGuiMessage(Notification::Event::GeneralEvent,
213                          QSL(APP_NAME),
214                          QObject::tr("Welcome to %1.\n\nPlease, check NEW stuff included in this\n"
215                                      "version by clicking this popup notification.").arg(QSL(APP_LONG_NAME)),
216                          QSystemTrayIcon::MessageIcon::NoIcon, {}, {}, tr("Go to changelog"), [] {
217       FormAbout(qApp->mainForm()).exec();
218     });
219   }
220 }
221 
isAlreadyRunning()222 bool Application::isAlreadyRunning() {
223   return m_allowMultipleInstances
224       ? false
225       : sendMessage((QStringList() << QSL("-%1").arg(QSL(CLI_IS_RUNNING))
226                                    << Application::arguments().mid(1)).join(QSL(ARGUMENTS_LIST_SEPARATOR)));
227 }
228 
builtinSounds() const229 QStringList Application::builtinSounds() const {
230   auto builtin_sounds = QDir(QSL(SOUNDS_BUILTIN_DIRECTORY)).entryInfoList(QDir::Filter::Files, QDir::SortFlag::Name);
231   auto iter = boolinq::from(builtin_sounds).select([](const QFileInfo& i) {
232     return i.absoluteFilePath();
233   }).toStdList();
234   auto descs = FROM_STD_LIST(QStringList, iter);
235 
236   return descs;
237 }
238 
feedReader()239 FeedReader* Application::feedReader() {
240   return m_feedReader;
241 }
242 
userActions()243 QList<QAction*> Application::userActions() {
244   if (m_mainForm != nullptr && m_userActions.isEmpty()) {
245     m_userActions = m_mainForm->allActions();
246 
247 #if defined(USE_WEBENGINE)
248     m_userActions.append(m_webFactory->adBlock()->adBlockIcon());
249 #endif
250   }
251 
252   return m_userActions;
253 }
254 
isFirstRun() const255 bool Application::isFirstRun() const {
256   return m_firstRunEver;
257 }
258 
isFirstRunCurrentVersion() const259 bool Application::isFirstRunCurrentVersion() const {
260   return m_firstRunCurrentVersion;
261 }
262 
cmdParser()263 QCommandLineParser* Application::cmdParser() {
264   return &m_cmdParser;
265 }
266 
web() const267 WebFactory* Application::web() const {
268   return m_webFactory;
269 }
270 
system()271 SystemFactory* Application::system() {
272   return m_system;
273 }
274 
skins()275 SkinFactory* Application::skins() {
276   return m_skins;
277 }
278 
localization()279 Localization* Application::localization() {
280   return m_localization;
281 }
282 
database()283 DatabaseFactory* Application::database() {
284   return m_database;
285 }
286 
eliminateFirstRuns()287 void Application::eliminateFirstRuns() {
288   settings()->setValue(GROUP(General), General::FirstRun, false);
289   settings()->setValue(GROUP(General), QString(General::FirstRun) + QL1C('_') + APP_VERSION, false);
290 }
291 
notifications() const292 NotificationFactory* Application::notifications() const {
293   return m_notifications;
294 }
295 
setFeedReader(FeedReader * feed_reader)296 void Application::setFeedReader(FeedReader* feed_reader) {
297   m_feedReader = feed_reader;
298 
299   connect(m_feedReader, &FeedReader::feedUpdatesFinished, this, &Application::onFeedUpdatesFinished);
300   connect(m_feedReader->feedsModel(), &FeedsModel::messageCountsChanged, this, &Application::showMessagesNumber);
301 }
302 
icons()303 IconFactory* Application::icons() {
304   return m_icons;
305 }
306 
downloadManager()307 DownloadManager* Application::downloadManager() {
308   if (m_downloadManager == nullptr) {
309     m_downloadManager = new DownloadManager();
310     connect(m_downloadManager, &DownloadManager::downloadFinished, mainForm()->statusBar(), &StatusBar::clearProgressDownload);
311     connect(m_downloadManager, &DownloadManager::downloadProgressed, mainForm()->statusBar(), &StatusBar::showProgressDownload);
312   }
313 
314   return m_downloadManager;
315 }
316 
settings() const317 Settings* Application::settings() const {
318   return m_settings;
319 }
320 
feedUpdateLock()321 Mutex* Application::feedUpdateLock() {
322   return m_updateFeedsLock.data();
323 }
324 
mainForm()325 FormMain* Application::mainForm() {
326   return m_mainForm;
327 }
328 
mainFormWidget()329 QWidget* Application::mainFormWidget() {
330   return m_mainForm;
331 }
332 
setMainForm(FormMain * main_form)333 void Application::setMainForm(FormMain* main_form) {
334   m_mainForm = main_form;
335 }
336 
configFolder() const337 QString Application::configFolder() const {
338   return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::GenericConfigLocation);
339 }
340 
userDataAppFolder() const341 QString Application::userDataAppFolder() const {
342   // In "app" folder, we would like to separate all user data into own subfolder,
343   // therefore stick to "data" folder in this mode.
344   return applicationDirPath() + QDir::separator() + QSL("data4");
345 }
346 
userDataFolder()347 QString Application::userDataFolder() {
348   if (settings()->type() == SettingsProperties::SettingsType::Custom) {
349     return customDataFolder();
350   }
351   else if (settings()->type() == SettingsProperties::SettingsType::Portable) {
352     return userDataAppFolder();
353   }
354   else {
355     return userDataHomeFolder();
356   }
357 }
358 
replaceDataUserDataFolderPlaceholder(QString text) const359 QString Application::replaceDataUserDataFolderPlaceholder(QString text) const {
360   auto user_data_folder = qApp->userDataFolder();
361 
362   return text.replace(QSL(USER_DATA_PLACEHOLDER), user_data_folder);
363 }
364 
replaceDataUserDataFolderPlaceholder(QStringList texts) const365 QStringList Application::replaceDataUserDataFolderPlaceholder(QStringList texts) const {
366   auto user_data_folder = qApp->userDataFolder();
367 
368   return texts.replaceInStrings(QSL(USER_DATA_PLACEHOLDER), user_data_folder);
369 }
370 
userDataHomeFolder() const371 QString Application::userDataHomeFolder() const {
372 #if defined(Q_OS_ANDROID)
373   return IOFactory::getSystemFolder(QStandardPaths::GenericDataLocation) + QDir::separator() + QSL(APP_NAME) + QSL(" 4");
374 #else
375   return configFolder() + QDir::separator() + QSL(APP_NAME) + QSL(" 4");
376 #endif
377 }
378 
tempFolder() const379 QString Application::tempFolder() const {
380   return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::TempLocation);
381 }
382 
documentsFolder() const383 QString Application::documentsFolder() const {
384   return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::DocumentsLocation);
385 }
386 
homeFolder() const387 QString Application::homeFolder() const {
388 #if defined(Q_OS_ANDROID)
389   return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::GenericDataLocation);
390 #else
391   return IOFactory::getSystemFolder(QStandardPaths::StandardLocation::HomeLocation);
392 #endif
393 }
394 
backupDatabaseSettings(bool backup_database,bool backup_settings,const QString & target_path,const QString & backup_name)395 void Application::backupDatabaseSettings(bool backup_database, bool backup_settings,
396                                          const QString& target_path, const QString& backup_name) {
397   if (!QFileInfo(target_path).isWritable()) {
398     throw ApplicationException(tr("Output directory is not writable."));
399   }
400 
401   if (backup_settings) {
402     settings()->sync();
403 
404     if (!IOFactory::copyFile(settings()->fileName(), target_path + QDir::separator() + backup_name + BACKUP_SUFFIX_SETTINGS)) {
405       throw ApplicationException(tr("Settings file not copied to output directory successfully."));
406     }
407   }
408 
409   if (backup_database) {
410     // We need to save the database first.
411     database()->driver()->saveDatabase();
412     database()->driver()->backupDatabase(target_path, backup_name);
413   }
414 }
415 
restoreDatabaseSettings(bool restore_database,bool restore_settings,const QString & source_database_file_path,const QString & source_settings_file_path)416 void Application::restoreDatabaseSettings(bool restore_database, bool restore_settings,
417                                           const QString& source_database_file_path, const QString& source_settings_file_path) {
418   if (restore_database) {
419     if (!qApp->database()->driver()->initiateRestoration(source_database_file_path)) {
420       throw ApplicationException(tr("Database restoration was not initiated. Make sure that output directory is writable."));
421     }
422   }
423 
424   if (restore_settings) {
425     if (!qApp->settings()->initiateRestoration(source_settings_file_path)) {
426       throw ApplicationException(tr("Settings restoration was not initiated. Make sure that output directory is writable."));
427     }
428   }
429 }
430 
trayIcon()431 SystemTrayIcon* Application::trayIcon() {
432   if (m_trayIcon == nullptr) {
433     if (qApp->settings()->value(GROUP(GUI), SETTING(GUI::MonochromeTrayIcon)).toBool()) {
434       m_trayIcon = new SystemTrayIcon(APP_ICON_MONO_PATH, APP_ICON_MONO_PLAIN_PATH, m_mainForm);
435     }
436     else {
437       m_trayIcon = new SystemTrayIcon(APP_ICON_PATH, APP_ICON_PLAIN_PATH, m_mainForm);
438     }
439 
440     connect(m_trayIcon, &SystemTrayIcon::shown, m_feedReader->feedsModel(), &FeedsModel::notifyWithCounts);
441   }
442 
443   return m_trayIcon;
444 }
445 
desktopAwareIcon() const446 QIcon Application::desktopAwareIcon() const {
447   auto from_theme = m_icons->fromTheme(QSL(APP_LOW_NAME));
448 
449   if (!from_theme.isNull()) {
450     return from_theme;
451   }
452   else {
453     return QIcon(APP_ICON_PATH);
454   }
455 }
456 
showTrayIcon()457 void Application::showTrayIcon() {
458   // Display tray icon if it is enabled and available.
459   if (SystemTrayIcon::isSystemTrayDesired()) {
460 #if !defined(Q_OS_UNIX)
461     if (!SystemTrayIcon::isSystemTrayAreaAvailable()) {
462       qWarningNN << LOGSEC_GUI << "Tray icon area is not available.";
463       return;
464     }
465 #endif
466 
467     qDebugNN << LOGSEC_GUI << "Showing tray icon.";
468     trayIcon()->show();
469   }
470   else {
471     m_feedReader->feedsModel()->notifyWithCounts();
472   }
473 }
474 
deleteTrayIcon()475 void Application::deleteTrayIcon() {
476   if (m_trayIcon != nullptr) {
477     qDebugNN << LOGSEC_CORE << "Disabling tray icon, deleting it and raising main application window.";
478     m_mainForm->display();
479     delete m_trayIcon;
480     m_trayIcon = nullptr;
481 
482     // Make sure that application quits when last window is closed.
483     setQuitOnLastWindowClosed(true);
484   }
485 }
486 
showGuiMessage(Notification::Event event,const QString & title,const QString & message,QSystemTrayIcon::MessageIcon message_type,bool show_at_least_msgbox,QWidget * parent,const QString & functor_heading,std::function<void ()> functor)487 void Application::showGuiMessage(Notification::Event event, const QString& title,
488                                  const QString& message, QSystemTrayIcon::MessageIcon message_type, bool show_at_least_msgbox,
489                                  QWidget* parent, const QString& functor_heading, std::function<void()> functor) {
490 
491   if (SystemTrayIcon::areNotificationsEnabled()) {
492     auto notification = m_notifications->notificationForEvent(event);
493 
494     notification.playSound(this);
495 
496     if (SystemTrayIcon::isSystemTrayDesired() &&
497         SystemTrayIcon::isSystemTrayAreaAvailable() &&
498         notification.balloonEnabled()) {
499       trayIcon()->showMessage(title, message, message_type, TRAY_ICON_BUBBLE_TIMEOUT, std::move(functor));
500 
501       return;
502     }
503   }
504 
505   if (show_at_least_msgbox) {
506     // Tray icon or OSD is not available, display simple text box.
507     MessageBox::show(parent == nullptr ? mainFormWidget() : parent, QMessageBox::Icon(message_type), title, message,
508                      {}, {}, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok, {}, functor_heading, functor);
509   }
510   else {
511     qDebugNN << LOGSEC_CORE << "Silencing GUI message:" << QUOTE_W_SPACE_DOT(message);
512   }
513 }
514 
onCommitData(QSessionManager & manager)515 void Application::onCommitData(QSessionManager& manager) {
516   qDebugNN << LOGSEC_CORE << "OS asked application to commit its data.";
517 
518   onAboutToQuit();
519 
520   manager.setRestartHint(QSessionManager::RestartHint::RestartNever);
521   manager.release();
522 }
523 
onSaveState(QSessionManager & manager)524 void Application::onSaveState(QSessionManager& manager) {
525   qDebugNN << LOGSEC_CORE << "OS asked application to save its state.";
526 
527   manager.setRestartHint(QSessionManager::RestartHint::RestartNever);
528   manager.release();
529 }
530 
onAboutToQuit()531 void Application::onAboutToQuit() {
532   if (m_quitLogicDone) {
533     qWarningNN << LOGSEC_CORE << "On-close logic is already done.";
534     return;
535   }
536 
537   m_quitLogicDone = true;
538 
539   // Make sure that we obtain close lock BEFORE even trying to quit the application.
540   const bool locked_safely = feedUpdateLock()->tryLock(4 * CLOSE_LOCK_TIMEOUT);
541 
542   processEvents();
543   qDebugNN << LOGSEC_CORE << "Cleaning up resources and saving application state.";
544 
545   if (locked_safely) {
546     // Application obtained permission to close in a safe way.
547     qDebugNN << LOGSEC_CORE << "Close lock was obtained safely.";
548 
549     // We locked the lock to exit peacefully, unlock it to avoid warnings.
550     feedUpdateLock()->unlock();
551   }
552   else {
553     // Request for write lock timed-out. This means
554     // that some critical action can be processed right now.
555     qWarningNN << LOGSEC_CORE << "Close lock timed-out.";
556   }
557 
558   qApp->feedReader()->quit();
559   database()->driver()->saveDatabase();
560 
561   if (mainForm() != nullptr) {
562     mainForm()->saveSize();
563   }
564 
565   // Now, we can check if application should just quit or restart itself.
566   if (m_shouldRestart) {
567     finish();
568     qDebugNN << LOGSEC_CORE << "Killing local peer connection to allow another instance to start.";
569 
570     if (QProcess::startDetached(QDir::toNativeSeparators(applicationFilePath()), {})) {
571       qDebugNN << LOGSEC_CORE << "New application instance was started.";
572     }
573     else {
574       qCriticalNN << LOGSEC_CORE << "New application instance was not started successfully.";
575     }
576   }
577 }
578 
showMessagesNumber(int unread_messages,bool any_feed_has_unread_messages)579 void Application::showMessagesNumber(int unread_messages, bool any_feed_has_unread_messages) {
580   if (m_trayIcon != nullptr) {
581     m_trayIcon->setNumber(unread_messages, any_feed_has_unread_messages);
582   }
583 
584 #if defined(Q_OS_UNIX)
585   QDBusMessage signal = QDBusMessage::createSignal(QSL("/"),
586                                                    QSL("com.canonical.Unity.LauncherEntry"),
587                                                    QSL("Update"));
588 
589   signal << QSL("application://%1").arg(APP_DESKTOP_ENTRY_FILE);
590 
591   QVariantMap setProperty;
592 
593   setProperty.insert("count", qint64(unread_messages));
594   setProperty.insert("count-visible", unread_messages > 0);
595 
596   signal << setProperty;
597 
598   QDBusConnection::sessionBus().send(signal);
599 #endif
600 }
601 
restart()602 void Application::restart() {
603   m_shouldRestart = true;
604   quit();
605 }
606 
607 #if defined(USE_WEBENGINE)
608 
downloadRequested(QWebEngineDownloadItem * download_item)609 void Application::downloadRequested(QWebEngineDownloadItem* download_item) {
610   downloadManager()->download(download_item->url());
611   download_item->cancel();
612   download_item->deleteLater();
613 }
614 
onAdBlockFailure()615 void Application::onAdBlockFailure() {
616   qApp->showGuiMessage(Notification::Event::GeneralEvent,
617                        tr("AdBlock needs to be configured"),
618                        tr("AdBlock component is not configured properly."),
619                        QSystemTrayIcon::MessageIcon::Critical,
620                        true,
621                        {},
622                        tr("Configure now"),
623                        [=]() {
624     m_webFactory->adBlock()->showDialog();
625   });
626 
627   qApp->settings()->setValue(GROUP(AdBlock), AdBlock::AdBlockEnabled, false);
628 }
629 
630 #endif
631 
onFeedUpdatesFinished(const FeedDownloadResults & results)632 void Application::onFeedUpdatesFinished(const FeedDownloadResults& results) {
633   if (!results.updatedFeeds().isEmpty()) {
634     // Now, inform about results via GUI message/notification.
635     qApp->showGuiMessage(Notification::Event::NewUnreadArticlesFetched,
636                          tr("Unread articles fetched"),
637                          results.overview(10),
638                          QSystemTrayIcon::MessageIcon::NoIcon);
639   }
640 }
641 
setupCustomDataFolder(const QString & data_folder)642 void Application::setupCustomDataFolder(const QString& data_folder) {
643   if (!QDir().mkpath(data_folder)) {
644     qCriticalNN << LOGSEC_CORE
645                 << "Failed to create custom data path"
646                 << QUOTE_W_SPACE(data_folder)
647                 << "thus falling back to standard setup.";
648     m_customDataFolder = QString();
649     return;
650   }
651 
652   // Disable single instance mode.
653   m_allowMultipleInstances = true;
654 
655   // Save custom data folder.
656   m_customDataFolder = data_folder;
657 }
658 
determineFirstRuns()659 void Application::determineFirstRuns() {
660   m_firstRunEver = settings()->value(GROUP(General),
661                                      SETTING(General::FirstRun)).toBool();
662   m_firstRunCurrentVersion = settings()->value(GROUP(General),
663                                                QString(General::FirstRun) + QL1C('_') + APP_VERSION,
664                                                true).toBool();
665 
666   eliminateFirstRuns();
667 }
668 
parseCmdArgumentsFromOtherInstance(const QString & message)669 void Application::parseCmdArgumentsFromOtherInstance(const QString& message) {
670   if (message.isEmpty()) {
671     qDebugNN << LOGSEC_CORE << "No execution message received from other app instances.";
672     return;
673   }
674 
675   qDebugNN << LOGSEC_CORE
676            << "Received"
677            << QUOTE_W_SPACE(message)
678            << "execution message.";
679 
680 #if QT_VERSION >= 0x050F00 // Qt >= 5.15.0
681   QStringList messages = message.split(QSL(ARGUMENTS_LIST_SEPARATOR), Qt::SplitBehaviorFlags::SkipEmptyParts);
682 #else
683   QStringList messages = message.split(QSL(ARGUMENTS_LIST_SEPARATOR), QString::SplitBehavior::SkipEmptyParts);
684 #endif
685 
686   QCommandLineParser cmd_parser;
687 
688   messages.prepend(qApp->applicationFilePath());
689 
690   cmd_parser.addOption(QCommandLineOption({ QSL(CLI_QUIT_INSTANCE) }));
691   cmd_parser.addOption(QCommandLineOption({ QSL(CLI_IS_RUNNING) }));
692   cmd_parser.addPositionalArgument(QSL("urls"),
693                                    QSL("List of URL addresses pointing to individual online feeds which should be added."),
694                                    QSL("[url-1 ... url-n]"));
695 
696   if (!cmd_parser.parse(messages)) {
697     qCriticalNN << LOGSEC_CORE << cmd_parser.errorText();
698   }
699 
700   if (cmd_parser.isSet(QSL(CLI_QUIT_INSTANCE))) {
701     quit();
702     return;
703   }
704   else if (cmd_parser.isSet(QSL(CLI_IS_RUNNING))) {
705     showGuiMessage(Notification::Event::GeneralEvent,
706                    QSL(APP_NAME),
707                    tr("Application is already running."),
708                    QSystemTrayIcon::MessageIcon::Information);
709     mainForm()->display();
710   }
711 
712   messages = cmd_parser.positionalArguments();
713 
714   for (const QString& msg : qAsConst(messages)) {
715     // Application was running, and someone wants to add new feed.
716     ServiceRoot* rt = boolinq::from(feedReader()->feedsModel()->serviceRoots()).firstOrDefault([](ServiceRoot* root) {
717       return root->supportsFeedAdding();
718     });
719 
720     if (rt != nullptr) {
721       rt->addNewFeed(nullptr, msg);
722     }
723     else {
724       showGuiMessage(Notification::Event::GeneralEvent,
725                      tr("Cannot add feed"),
726                      tr("Feed cannot be added because there is no active account which can add feeds."),
727                      QSystemTrayIcon::MessageIcon::Warning,
728                      true);
729     }
730   }
731 }
732 
parseCmdArgumentsFromMyInstance()733 void Application::parseCmdArgumentsFromMyInstance() {
734   QCommandLineOption help({ QSL(CLI_HELP_SHORT), QSL(CLI_HELP_LONG) },
735                           QSL("Displays overview of CLI."));
736   QCommandLineOption version({ QSL(CLI_VER_SHORT), QSL(CLI_VER_LONG) },
737                              QSL("Displays version of the application."));
738   QCommandLineOption log_file({ QSL(CLI_LOG_SHORT), QSL(CLI_LOG_LONG) },
739                               QSL("Write application debug log to file. Note that logging to file may slow application down."),
740                               QSL("log-file"));
741   QCommandLineOption custom_data_folder({ QSL(CLI_DAT_SHORT), QSL(CLI_DAT_LONG) },
742                                         QSL("Use custom folder for user data and disable single instance application mode."),
743                                         QSL("user-data-folder"));
744   QCommandLineOption disable_singleinstance({ QSL(CLI_SIN_SHORT), QSL(CLI_SIN_LONG) },
745                                             QSL("Allow running of multiple application instances."));
746   QCommandLineOption disable_debug({ QSL(CLI_NDEBUG_SHORT), QSL(CLI_NDEBUG_LONG) },
747                                    QSL("Completely disable stdout/stderr outputs."));
748 
749   m_cmdParser.addOptions({ help, version, log_file, custom_data_folder, disable_singleinstance, disable_debug });
750   m_cmdParser.addPositionalArgument(QSL("urls"),
751                                     QSL("List of URL addresses pointing to individual online feeds which should be added."),
752                                     QSL("[url-1 ... url-n]"));
753   m_cmdParser.setApplicationDescription(QSL(APP_NAME));
754 
755   if (!m_cmdParser.parse(QCoreApplication::arguments())) {
756     qCriticalNN << LOGSEC_CORE << m_cmdParser.errorText();
757   }
758 
759   s_customLogFile = m_cmdParser.value(QSL(CLI_LOG_SHORT));
760 
761   if (!m_cmdParser.value(QSL(CLI_DAT_SHORT)).isEmpty()) {
762     auto data_folder = QDir::toNativeSeparators(m_cmdParser.value(QSL(CLI_DAT_SHORT)));
763 
764     qDebugNN << LOGSEC_CORE
765              << "User wants to use custom directory for user data (and disable single instance mode):"
766              << QUOTE_W_SPACE_DOT(data_folder);
767 
768     setupCustomDataFolder(data_folder);
769   }
770   else {
771     m_allowMultipleInstances = false;
772   }
773 
774   if (m_cmdParser.isSet(QSL(CLI_HELP_SHORT))) {
775     m_cmdParser.showHelp();
776   }
777   else if (m_cmdParser.isSet(QSL(CLI_VER_SHORT))) {
778     m_cmdParser.showVersion();
779   }
780 
781   if (m_cmdParser.isSet(QSL(CLI_SIN_SHORT))) {
782     m_allowMultipleInstances = true;
783     qDebugNN << LOGSEC_CORE << "Explicitly allowing this instance to run.";
784   }
785 
786   if (m_cmdParser.isSet(QSL(CLI_NDEBUG_SHORT))) {
787     s_disableDebug = true;
788     qDebugNN << LOGSEC_CORE << "Disabling any stdout/stderr outputs.";
789   }
790 }
791 
customDataFolder() const792 QString Application::customDataFolder() const {
793   return m_customDataFolder;
794 }
795