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 , m_apiUrl(k_releaseDataUrl)
82{
83 // QNetworkAccessManager::sslErrors doesn't exist at all when Qt is built with QT_NO_SSL --
84 // the case for every Qt-for-WebAssembly build, since WASM has no native TLS backend and
85 // routes network access through the browser instead (any TLS failure there surfaces as a
86 // generic QNetworkReply error, not through this signal).
87#ifndef QT_NO_SSL
88 connect(&m_network, &QNetworkAccessManager::sslErrors, this, [](QNetworkReply *reply, const QList<QSslError> &errors) {
89 qWarning() << "UpdateChecker: SSL errors, aborting reply:" << errors;
90 reply->abort();
91 });
92#endif
93}
94
96{
97 // Honour the global opt-out (offline/managed installs).
99 return;
100 }
101
102 // Skip if we already checked today.
103 const QString today = QDate::currentDate().toString(Qt::ISODate);
104 if (Settings::updateCheckLastDate() == today) {
105 return;
106 }
107
108 QNetworkRequest request = QNetworkRequest{m_apiUrl};
109 request.setHeader(QNetworkRequest::UserAgentHeader, "wiRedPanda/" APP_VERSION);
110 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
111 QNetworkRequest::NoLessSafeRedirectPolicy);
112 request.setTransferTimeout(10000);
113
114 QNetworkReply *reply = m_network.get(request);
115 // The release data response is realistically under 1KB; cap well above that
116 // as defense-in-depth against a hostile/corrupted endpoint buffering unbounded
117 // bytes into memory before onReplyFinished ever gets a chance to react.
118 reply->setReadBufferSize(1024 * 1024 + 1);
119 // Empirically confirmed (a local QTcpServer streaming well past 1 MiB while
120 // nothing reads from `reply`): setReadBufferSize()'s own backpressure stops
121 // Qt from admitting more bytes once the cap is reached, so `received` never
122 // actually exceeds it here — this condition is a belt-and-suspenders check
123 // for a network-backend timing edge case (a single oversized delivery
124 // landing before backpressure engages) that a synchronous local-server test
125 // cannot reproduce.
126 connect(reply, &QNetworkReply::downloadProgress, this, [reply](qint64 received, qint64) {
127 if (received > 1024 * 1024) { // LCOV_EXCL_LINE
128 reply->abort(); // LCOV_EXCL_LINE
129 } // LCOV_EXCL_LINE
130 });
131 connect(reply, &QNetworkReply::finished, this, [this, reply] { onReplyFinished(reply); });
132}
133
134void UpdateChecker::onReplyFinished(QNetworkReply *reply)
135{
136 reply->deleteLater();
137
138 // Also covers QNetworkReply::OperationCanceledError produced by the
139 // read-buffer size-cap abort in checkForUpdates().
140 if (reply->error() != QNetworkReply::NoError) {
141 return;
142 }
143
144 const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
145 if (doc.isNull() || !doc.isObject()) {
146 return;
147 }
148
149 // A successful, parseable reply IS the daily check — record it here, not
150 // when a dialog is shown: with no newer release (the common case) the
151 // date was never written and the endpoint was hit on every launch. Network
152 // failures above intentionally don't record, so the check retries.
153 Settings::setUpdateCheckLastDate(QDate::currentDate().toString(Qt::ISODate));
154
155 const QString version = doc.object().value("version").toString();
157 return;
158 }
159 const QVersionNumber latest = QVersionNumber::fromString(version).normalized();
160
161 const QString platform = currentPlatform();
162 const QString arch = QSysInfo::buildCpuArchitecture(); // "x86_64" / "arm64"
163 const QString key = releaseAssetKey(platform, arch);
164 QUrl downloadUrl = key.isEmpty() ? QUrl{} : QUrl(doc.object().value(key).toString());
165
166 const QUrl releaseUrl = QUrl(QStringLiteral("https://github.com/GIBIS-UNIFESP/wiRedPanda/releases/tag/%1").arg(latest.toString()));
167 if (!isSafeGitHubUrl(releaseUrl)) {
168 // Unlike downloadUrl just below (built from an untrusted JSON field, and covered by
169 // testOnReplyFinishedUnsafeDownloadUrlFallsBackToReleasePage()), releaseUrl is built
170 // from a hardcoded "https://github.com/..." literal plus latest.toString() --
171 // QVersionNumber::toString() can only ever produce digits and dots (fromString()
172 // stops parsing at the first non-digit-dot character), so it can't inject a scheme,
173 // host, or path segment that would make isSafeGitHubUrl() reject this URL. Belt-and-
174 // suspenders, same class as checkForUpdates()'s downloadProgress backpressure guard
175 // above -- not reachable from a real test process. isSafeGitHubUrl() itself is
176 // directly unit-tested (testSafeGitHubUrl()).
177 qWarning() << "UpdateChecker: release URL has unexpected scheme/host, ignoring update notification:" << releaseUrl; // LCOV_EXCL_LINE
178 return; // LCOV_EXCL_LINE
179 } // LCOV_EXCL_LINE
180 if (!downloadUrl.isEmpty() && !isSafeGitHubUrl(downloadUrl)) {
181 qWarning() << "UpdateChecker: download URL has unexpected scheme/host, falling back to release page:" << downloadUrl;
182 downloadUrl.clear();
183 }
184
185 emit updateAvailable(latest.toString(), downloadUrl, releaseUrl);
186}
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