wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
UpdateChecker.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <QDate>
7#include <QDebug>
8#include <QJsonDocument>
9#include <QJsonObject>
10#include <QNetworkReply>
11#include <QNetworkRequest>
12#include <QSslError>
13#include <QSysInfo>
14#include <QVersionNumber>
15
16#include "App/Core/Settings.h"
17#include "App/Versions.h"
18
19static constexpr auto k_releaseDataUrl = "https://gibis-unifesp.github.io/wiRedPanda/latest-release.json";
20
23static QString currentPlatform()
24{
25#if defined(Q_OS_WIN)
26 return "Windows";
27#elif defined(Q_OS_MACOS)
28 return "macOS";
29#elif defined(Q_OS_LINUX)
30 return "Linux";
31#else
32 return {};
33#endif
34}
35
36QString releaseAssetKey(const QString &platform, const QString &arch)
37{
38 if (platform == "Linux") {
39 if (arch == "x86_64") {
40 return QStringLiteral("linuxX64");
41 }
42 if (arch == "arm64") {
43 return QStringLiteral("linuxArm64");
44 }
45 return {};
46 }
47 if (platform == "Windows") {
48 if (arch == "x86_64") {
49 return QStringLiteral("windowsX64");
50 }
51 if (arch == "arm64") {
52 return QStringLiteral("windowsArm64");
53 }
54 return {};
55 }
56 if (platform == "macOS") {
57 // A single universal DMG serves both architectures, so it carries no arch-specific key.
58 return QStringLiteral("macosUniversal");
59 }
60 return {};
61}
62
63bool shouldOfferUpdate(const QString &tagName, const QVersionNumber &currentVersion, const QString &skippedVersion)
64{
65 const QVersionNumber latest = QVersionNumber::fromString(tagName).normalized();
66 if (latest.isNull() || latest <= currentVersion) {
67 return false;
68 }
69
70 // Respect the user's per-version suppression.
71 return latest.toString() != skippedVersion;
72}
73
74bool isSafeGitHubUrl(const QUrl &url)
75{
76 return url.isValid() && url.scheme() == QLatin1String("https") && url.host() == QLatin1String("github.com");
77}
78
80 : QObject(parent)
81{
82 // QNetworkAccessManager::sslErrors doesn't exist at all when Qt is built with QT_NO_SSL --
83 // the case for every Qt-for-WebAssembly build, since WASM has no native TLS backend and
84 // routes network access through the browser instead (any TLS failure there surfaces as a
85 // generic QNetworkReply error, not through this signal).
86#ifndef QT_NO_SSL
87 connect(&m_network, &QNetworkAccessManager::sslErrors, this, [](QNetworkReply *reply, const QList<QSslError> &errors) {
88 qWarning() << "UpdateChecker: SSL errors, aborting reply:" << errors;
89 reply->abort();
90 });
91#endif
92}
93
95{
96 // Honour the global opt-out (offline/managed installs).
98 return;
99 }
100
101 // Skip if we already checked today.
102 const QString today = QDate::currentDate().toString(Qt::ISODate);
103 if (Settings::updateCheckLastDate() == today) {
104 return;
105 }
106
107 QNetworkRequest request = QNetworkRequest{QUrl(k_releaseDataUrl)};
108 request.setHeader(QNetworkRequest::UserAgentHeader, "wiRedPanda/" APP_VERSION);
109 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
110 QNetworkRequest::NoLessSafeRedirectPolicy);
111 request.setTransferTimeout(10000);
112
113 QNetworkReply *reply = m_network.get(request);
114 // The release data response is realistically under 1KB; cap well above that
115 // as defense-in-depth against a hostile/corrupted endpoint buffering unbounded
116 // bytes into memory before onReplyFinished ever gets a chance to react.
117 reply->setReadBufferSize(1024 * 1024 + 1);
118 connect(reply, &QNetworkReply::downloadProgress, this, [reply](qint64 received, qint64) {
119 if (received > 1024 * 1024) {
120 reply->abort();
121 }
122 });
123 connect(reply, &QNetworkReply::finished, this, [this, reply] { onReplyFinished(reply); });
124}
125
126void UpdateChecker::onReplyFinished(QNetworkReply *reply)
127{
128 reply->deleteLater();
129
130 // Also covers QNetworkReply::OperationCanceledError produced by the
131 // read-buffer size-cap abort in checkForUpdates().
132 if (reply->error() != QNetworkReply::NoError) {
133 return;
134 }
135
136 const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
137 if (doc.isNull() || !doc.isObject()) {
138 return;
139 }
140
141 // A successful, parseable reply IS the daily check — record it here, not
142 // when a dialog is shown: with no newer release (the common case) the
143 // date was never written and the endpoint was hit on every launch. Network
144 // failures above intentionally don't record, so the check retries.
145 Settings::setUpdateCheckLastDate(QDate::currentDate().toString(Qt::ISODate));
146
147 const QString version = doc.object().value("version").toString();
149 return;
150 }
151 const QVersionNumber latest = QVersionNumber::fromString(version).normalized();
152
153 const QString platform = currentPlatform();
154 const QString arch = QSysInfo::buildCpuArchitecture(); // "x86_64" / "arm64"
155 const QString key = releaseAssetKey(platform, arch);
156 QUrl downloadUrl = key.isEmpty() ? QUrl{} : QUrl(doc.object().value(key).toString());
157
158 const QUrl releaseUrl = QUrl(QStringLiteral("https://github.com/GIBIS-UNIFESP/wiRedPanda/releases/tag/%1").arg(latest.toString()));
159 if (!isSafeGitHubUrl(releaseUrl)) {
160 qWarning() << "UpdateChecker: release URL has unexpected scheme/host, ignoring update notification:" << releaseUrl;
161 return;
162 }
163 if (!downloadUrl.isEmpty() && !isSafeGitHubUrl(downloadUrl)) {
164 qWarning() << "UpdateChecker: download URL has unexpected scheme/host, falling back to release page:" << downloadUrl;
165 downloadUrl.clear();
166 }
167
168 emit updateAvailable(latest.toString(), downloadUrl, releaseUrl);
169}
Typed wrappers around QSettings for all application preferences.
bool isSafeGitHubUrl(const QUrl &url)
True when url is safe to download from or hand to the OS's URL handler.
static constexpr auto k_releaseDataUrl
QString releaseAssetKey(const QString &platform, const QString &arch)
The key in latest-release.json holding the download URL for the given platform ("Windows"/"macOS"/"Li...
bool shouldOfferUpdate(const QString &tagName, const QVersionNumber &currentVersion, const QString &skippedVersion)
True when the release tagged tagName should be offered to a user running currentVersion who may have ...
static QString currentPlatform()
Checks the wiRedPanda site's published release data for newer versions.
bool isSafeGitHubUrl(const QUrl &url)
True when url is safe to download from or hand to the OS's URL handler.
QString releaseAssetKey(const QString &platform, const QString &arch)
The key in latest-release.json holding the download URL for the given platform ("Windows"/"macOS"/"Li...
bool shouldOfferUpdate(const QString &tagName, const QVersionNumber &currentVersion, const QString &skippedVersion)
True when the release tagged tagName should be offered to a user running currentVersion who may have ...
File-format version constants and application version accessor.
static QString updateCheckLastDate()
Definition Settings.cpp:210
static bool updateChecksDisabled()
Global opt-out of update checks (for offline/managed installs); default false (enabled).
Definition Settings.cpp:130
static QString updateCheckSkippedVersion()
Definition Settings.cpp:220
static void setUpdateCheckLastDate(const QString &date)
Definition Settings.cpp:215
UpdateChecker(QObject *parent=nullptr)
void updateAvailable(const QString &latestVersion, const QUrl &downloadUrl, const QUrl &releaseUrl)
Emitted when a newer release is available and has not been suppressed.
void checkForUpdates()
Initiates an asynchronous version check.
const QVersionNumber current
Definition Versions.h:70