1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include "miscellaneous/systemfactory.h"
4 
5 #include "gui/dialogs/formmain.h"
6 #include "gui/dialogs/formupdate.h"
7 #include "miscellaneous/application.h"
8 #include "miscellaneous/systemfactory.h"
9 #include "network-web/networkfactory.h"
10 
11 #if defined(Q_OS_WIN)
12 #include <QSettings>
13 #endif
14 
15 #include <QDesktopServices>
16 #include <QDir>
17 #include <QFile>
18 #include <QFileInfo>
19 #include <QFuture>
20 #include <QFutureWatcher>
21 #include <QJsonArray>
22 #include <QJsonDocument>
23 #include <QJsonObject>
24 #include <QProcess>
25 #include <QString>
26 
27 using UpdateCheck = QPair<UpdateInfo, QNetworkReply::NetworkError>;
28 
SystemFactory(QObject * parent)29 SystemFactory::SystemFactory(QObject* parent) : QObject(parent) {}
30 
31 SystemFactory::~SystemFactory() = default;
32 
supportedUpdateFiles()33 QRegularExpression SystemFactory::supportedUpdateFiles() {
34 #if defined(Q_OS_WIN)
35   return QRegularExpression(QSL(".+win.+\\.(exe|7z)"));
36 #elif defined(Q_OS_MACOS)
37   return QRegularExpression(QSL(".dmg"));
38 #elif defined(Q_OS_UNIX)
39   return QRegularExpression(QSL(".AppImage"));
40 #else
41   return QRegularExpression(QSL(".*"));
42 #endif
43 }
44 
autoStartStatus() const45 SystemFactory::AutoStartStatus SystemFactory::autoStartStatus() const {
46   // User registry way to auto-start the application on Windows.
47 #if defined(Q_OS_WIN)
48   QSettings registry_key(QSL("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"),
49                          QSettings::Format::NativeFormat);
50   const bool autostart_enabled = registry_key.value(QSL(APP_LOW_NAME),
51                                                     QString()).toString().replace(QL1C('\\'),
52                                                                                   QL1C('/')) ==
53                                  Application::applicationFilePath();
54 
55   if (autostart_enabled) {
56     return AutoStartStatus::Enabled;
57   }
58   else {
59     return AutoStartStatus::Disabled;
60   }
61 #elif defined(Q_OS_UNIX)
62   // Use proper freedesktop.org way to auto-start the application on Linux.
63   // INFO: http://standards.freedesktop.org/autostart-spec/latest/
64   const QString desktop_file_location = autostartDesktopFileLocation();
65 
66   // No correct path was found.
67   if (desktop_file_location.isEmpty()) {
68     qWarningNN << LOGSEC_GUI << "Searching for auto-start function status failed. HOME variable not found.";
69     return AutoStartStatus::Unavailable;
70   }
71 
72   // We found correct path, now check if file exists and return correct status.
73   if (QFile::exists(desktop_file_location)) {
74     // File exists, we must read it and check if "Hidden" attribute is defined and what is its value.
75     QSettings desktop_settings(desktop_file_location, QSettings::IniFormat);
76     bool hidden_value = desktop_settings.value(QSL("Desktop Entry/Hidden"), false).toBool();
77 
78     return hidden_value ? AutoStartStatus::Disabled : AutoStartStatus::Enabled;
79   }
80   else {
81     return AutoStartStatus::Disabled;
82   }
83 #else
84   // Disable auto-start functionality on unsupported platforms.
85   return AutoStartStatus::Unavailable;
86 #endif
87 }
88 
89 #if defined(Q_OS_UNIX)
autostartDesktopFileLocation() const90 QString SystemFactory::autostartDesktopFileLocation() const {
91   const QString xdg_config_path(qgetenv("XDG_CONFIG_HOME"));
92   QString desktop_file_location;
93 
94   if (!xdg_config_path.isEmpty()) {
95     // XDG_CONFIG_HOME variable is specified. Look for .desktop file
96     // in 'autostart' subdirectory.
97     desktop_file_location = xdg_config_path + QSL("/autostart/") + APP_DESKTOP_ENTRY_FILE;
98   }
99   else {
100     // Desired variable is not set, look for the default 'autostart' subdirectory.
101     const QString home_directory(qgetenv("HOME"));
102 
103     if (!home_directory.isEmpty()) {
104       // Home directory exists. Check if target .desktop file exists and
105       // return according status.
106       desktop_file_location = home_directory + QSL("/.config/autostart/") + APP_DESKTOP_ENTRY_FILE;
107     }
108   }
109 
110   return desktop_file_location;
111 }
112 
113 #endif
114 
setAutoStartStatus(AutoStartStatus new_status)115 bool SystemFactory::setAutoStartStatus(AutoStartStatus new_status) {
116   const SystemFactory::AutoStartStatus current_status = SystemFactory::autoStartStatus();
117 
118   // Auto-start feature is not even available, exit.
119   if (current_status == AutoStartStatus::Unavailable) {
120     return false;
121   }
122 
123 #if defined(Q_OS_WIN)
124   QSettings registry_key(QSL("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"), QSettings::NativeFormat);
125 
126   switch (new_status) {
127     case AutoStartStatus::Enabled:
128       registry_key.setValue(QSL(APP_LOW_NAME),
129                             Application::applicationFilePath().replace(QL1C('/'), QL1C('\\')));
130       return true;
131 
132     case AutoStartStatus::Disabled:
133       registry_key.remove(QSL(APP_LOW_NAME));
134       return true;
135 
136     default:
137       return false;
138   }
139 #elif defined(Q_OS_UNIX)
140   // Note that we expect here that no other program uses
141   // "rssguard.desktop" desktop file.
142   const QString destination_file = autostartDesktopFileLocation();
143   const QString destination_folder = QFileInfo(destination_file).absolutePath();
144 
145   switch (new_status) {
146     case AutoStartStatus::Enabled: {
147       if (QFile::exists(destination_file)) {
148         if (!QFile::remove(destination_file)) {
149           return false;
150         }
151       }
152 
153       if (!QDir().mkpath(destination_folder)) {
154         return false;
155       }
156 
157       const QString source_autostart_desktop_file = QString(APP_DESKTOP_ENTRY_PATH) + QDir::separator() + APP_DESKTOP_SOURCE_ENTRY_FILE;
158 
159       return QFile::copy(source_autostart_desktop_file, destination_file);
160     }
161 
162     case AutoStartStatus::Disabled:
163       return QFile::remove(destination_file);
164 
165     default:
166       return false;
167   }
168 #else
169   return false;
170 #endif
171 }
172 
loggedInUser() const173 QString SystemFactory::loggedInUser() const {
174   QString name = qgetenv("USER");
175 
176   if (name.isEmpty()) {
177     name = qgetenv("USERNAME");
178   }
179 
180   if (name.isEmpty()) {
181     name = tr("anonymous");
182   }
183 
184   return name;
185 }
186 
checkForUpdates() const187 void SystemFactory::checkForUpdates() const {
188   auto* downloader = new Downloader();
189 
190   connect(downloader, &Downloader::completed, this, [this, downloader]() {
191     QPair<QList<UpdateInfo>, QNetworkReply::NetworkError> result;
192     result.second = downloader->lastOutputError();
193 
194     if (result.second == QNetworkReply::NoError) {
195       QByteArray obtained_data = downloader->lastOutputData();
196       result.first = parseUpdatesFile(obtained_data);
197     }
198 
199     emit updatesChecked(result);
200     downloader->deleteLater();
201   });
202   downloader->downloadFile(QSL(RELEASES_LIST));
203 }
204 
checkForUpdatesOnStartup()205 void SystemFactory::checkForUpdatesOnStartup() {
206   if (qApp->settings()->value(GROUP(General), SETTING(General::UpdateOnStartup)).toBool()) {
207     QObject::connect(qApp->system(), &SystemFactory::updatesChecked,
208                      this, [&](const QPair<QList<UpdateInfo>, QNetworkReply::NetworkError>& updates) {
209       QObject::disconnect(qApp->system(), &SystemFactory::updatesChecked, this, nullptr);
210 
211       if (!updates.first.isEmpty() &&
212           updates.second == QNetworkReply::NetworkError::NoError &&
213           SystemFactory::isVersionNewer(updates.first.at(0).m_availableVersion, QSL(APP_VERSION))) {
214         qApp->showGuiMessage(Notification::Event::NewAppVersionAvailable,
215                              QObject::tr("New version available"),
216                              QObject::tr("Click the bubble for more information."),
217                              QSystemTrayIcon::Information, {}, {},
218                              tr("See new version info"),
219                              [] {
220           FormUpdate(qApp->mainForm()).exec();
221         });
222       }
223     });
224     qApp->system()->checkForUpdates();
225   }
226 }
227 
isVersionNewer(const QString & new_version,const QString & base_version)228 bool SystemFactory::isVersionNewer(const QString& new_version, const QString& base_version) {
229   QStringList base_version_tkn = base_version.split(QL1C('.'));
230   QStringList new_version_tkn = new_version.split(QL1C('.'));
231 
232   while (!base_version_tkn.isEmpty() && !new_version_tkn.isEmpty()) {
233     const int base_number = base_version_tkn.takeFirst().toInt();
234     const int new_number = new_version_tkn.takeFirst().toInt();
235 
236     if (new_number > base_number) {
237       // New version is indeed higher that current version.
238       return true;
239     }
240     else if (new_number < base_number) {
241       return false;
242     }
243   }
244 
245   // Versions are either the same or they have unequal sizes.
246   if (base_version_tkn.isEmpty() && new_version_tkn.isEmpty()) {
247     // Versions are the same.
248     return false;
249   }
250   else {
251     if (new_version_tkn.isEmpty()) {
252       return false;
253     }
254     else {
255       return new_version_tkn.join(QString()).toInt() > 0;
256     }
257   }
258 }
259 
isVersionEqualOrNewer(const QString & new_version,const QString & base_version)260 bool SystemFactory::isVersionEqualOrNewer(const QString& new_version, const QString& base_version) {
261   return new_version == base_version || isVersionNewer(new_version, base_version);
262 }
263 
openFolderFile(const QString & file_path)264 bool SystemFactory::openFolderFile(const QString& file_path) {
265 #if defined(Q_OS_WIN)
266   return QProcess::startDetached(QSL("explorer.exe"),
267                                  { "/select,", QDir::toNativeSeparators(file_path) });
268 #else
269   const QString folder = QDir::toNativeSeparators(QFileInfo(file_path).absoluteDir().absolutePath());
270 
271   return QDesktopServices::openUrl(QUrl::fromLocalFile(folder));
272 #endif
273 }
274 
parseUpdatesFile(const QByteArray & updates_file) const275 QList<UpdateInfo> SystemFactory::parseUpdatesFile(const QByteArray& updates_file) const {
276   QList<UpdateInfo> updates;
277   QJsonArray document = QJsonDocument::fromJson(updates_file).array();
278 
279   for (QJsonValueRef i : document) {
280     QJsonObject release = i.toObject();
281 
282     if (release[QSL("tag_name")].toString() == QSL("devbuild")) {
283       continue;
284     }
285 
286     UpdateInfo update;
287 
288     update.m_availableVersion = release[QSL("tag_name")].toString();
289     update.m_date = QDateTime::fromString(release[QSL("published_at")].toString(), QSL("yyyy-MM-ddTHH:mm:ssZ"));
290     update.m_changes = release[QSL("body")].toString();
291     QJsonArray assets = release[QSL("assets")].toArray();
292 
293     for (QJsonValueRef j : assets) {
294       QJsonObject asset = j.toObject();
295       UpdateUrl url;
296 
297       url.m_fileUrl = asset[QSL("browser_download_url")].toString();
298       url.m_name = asset[QSL("name")].toString();
299       url.m_size = asset[QSL("size")].toVariant().toString() + tr(" bytes");
300       update.m_urls.append(url);
301     }
302 
303     updates.append(update);
304   }
305 
306   std::sort(updates.begin(), updates.end(), [](const UpdateInfo& a, const UpdateInfo& b) -> bool {
307     return a.m_date > b.m_date;
308   });
309   return updates;
310 }
311