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 <QJsonArray>
9#include <QJsonDocument>
10#include <QJsonObject>
11#include <QNetworkReply>
12#include <QNetworkRequest>
13#include <QSslError>
14#include <QSysInfo>
15#include <QVersionNumber>
16
17#include "App/Core/Settings.h"
18#include "App/Versions.h"
19
20static constexpr auto k_apiUrl = "https://api.github.com/repos/gibis-unifesp/wiredpanda/releases/latest";
21
24static QString currentPlatform()
25{
26#if defined(Q_OS_WIN)
27 return "Windows";
28#elif defined(Q_OS_MACOS)
29 return "macOS";
30#elif defined(Q_OS_LINUX)
31 return "Linux";
32#else
33 return {};
34#endif
35}
36
37bool isMatchingReleaseAsset(const QString &name, const QString &platform, const QString &arch)
38{
39 if (platform == "Linux") {
40 return name.endsWith(".AppImage") && name.contains(arch, Qt::CaseInsensitive);
41 }
42 if (platform == "Windows") {
43 return name.contains("Windows") && name.endsWith(".zip") && name.contains(arch, Qt::CaseInsensitive);
44 }
45 if (platform == "macOS") {
46 // A single universal DMG serves both architectures, so it carries no arch token.
47 return name.contains("macOS") && name.endsWith(".dmg");
48 }
49 return false;
50}
51
52bool shouldOfferUpdate(const QString &tagName, const QVersionNumber &currentVersion, const QString &skippedVersion)
53{
54 const QVersionNumber latest = QVersionNumber::fromString(tagName).normalized();
55 if (latest.isNull() || latest <= currentVersion) {
56 return false;
57 }
58
59 // Respect the user's per-version suppression.
60 return latest.toString() != skippedVersion;
61}
62
63bool isSafeGitHubUrl(const QUrl &url)
64{
65 return url.isValid() && url.scheme() == QLatin1String("https") && url.host() == QLatin1String("github.com");
66}
67
69 : QObject(parent)
70{
71 // QNetworkAccessManager::sslErrors doesn't exist at all when Qt is built with QT_NO_SSL --
72 // the case for every Qt-for-WebAssembly build, since WASM has no native TLS backend and
73 // routes network access through the browser instead (any TLS failure there surfaces as a
74 // generic QNetworkReply error, not through this signal).
75#ifndef QT_NO_SSL
76 connect(&m_network, &QNetworkAccessManager::sslErrors, this, [](QNetworkReply *reply, const QList<QSslError> &errors) {
77 qWarning() << "UpdateChecker: SSL errors, aborting reply:" << errors;
78 reply->abort();
79 });
80#endif
81}
82
84{
85 // Honour the global opt-out (offline/managed installs).
87 return;
88 }
89
90 // Skip if we already checked today.
91 const QString today = QDate::currentDate().toString(Qt::ISODate);
92 if (Settings::updateCheckLastDate() == today) {
93 return;
94 }
95
96 QNetworkRequest request = QNetworkRequest{QUrl(k_apiUrl)};
97 request.setHeader(QNetworkRequest::UserAgentHeader, "wiRedPanda/" APP_VERSION);
98 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
99 QNetworkRequest::NoLessSafeRedirectPolicy);
100 request.setTransferTimeout(10000);
101
102 QNetworkReply *reply = m_network.get(request);
103 // The GitHub API response is realistically tens of KB; cap well above that
104 // as defense-in-depth against a hostile/corrupted endpoint buffering unbounded
105 // bytes into memory before onReplyFinished ever gets a chance to react.
106 reply->setReadBufferSize(1024 * 1024 + 1);
107 connect(reply, &QNetworkReply::downloadProgress, this, [reply](qint64 received, qint64) {
108 if (received > 1024 * 1024) {
109 reply->abort();
110 }
111 });
112 connect(reply, &QNetworkReply::finished, this, [this, reply] { onReplyFinished(reply); });
113}
114
115void UpdateChecker::onReplyFinished(QNetworkReply *reply)
116{
117 reply->deleteLater();
118
119 // Also covers QNetworkReply::OperationCanceledError produced by the
120 // read-buffer size-cap abort in checkForUpdates().
121 if (reply->error() != QNetworkReply::NoError) {
122 return;
123 }
124
125 const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
126 if (doc.isNull() || !doc.isObject()) {
127 return;
128 }
129
130 // A successful, parseable reply IS the daily check — record it here, not
131 // when a dialog is shown: with no newer release (the common case) the
132 // date was never written and the API was hit on every launch. Network
133 // failures above intentionally don't record, so the check retries.
134 Settings::setUpdateCheckLastDate(QDate::currentDate().toString(Qt::ISODate));
135
136 const QString tagName = doc.object().value("tag_name").toString();
138 return;
139 }
140 const QVersionNumber latest = QVersionNumber::fromString(tagName).normalized();
141
142 QUrl downloadUrl;
143 const QString platform = currentPlatform();
144 const QString arch = QSysInfo::buildCpuArchitecture(); // "x86_64" / "arm64"
145 const QJsonArray assets = doc.object().value("assets").toArray();
146 for (const QJsonValue &assetVal : assets) {
147 const QJsonObject asset = assetVal.toObject();
148 const QString name = asset.value("name").toString();
149 if (isMatchingReleaseAsset(name, platform, arch)) {
150 downloadUrl = QUrl(asset.value("browser_download_url").toString());
151 break;
152 }
153 }
154
155 const QUrl releaseUrl = QUrl(doc.object().value("html_url").toString());
156 if (!isSafeGitHubUrl(releaseUrl)) {
157 qWarning() << "UpdateChecker: release URL has unexpected scheme/host, ignoring update notification:" << releaseUrl;
158 return;
159 }
160 if (!downloadUrl.isEmpty() && !isSafeGitHubUrl(downloadUrl)) {
161 qWarning() << "UpdateChecker: download URL has unexpected scheme/host, falling back to release page:" << downloadUrl;
162 downloadUrl.clear();
163 }
164
165 emit updateAvailable(latest.toString(), downloadUrl, releaseUrl);
166}
Typed wrappers around QSettings for all application preferences.
static constexpr auto k_apiUrl
bool isSafeGitHubUrl(const QUrl &url)
True when url is safe to download from or hand to the OS's URL handler.
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()
bool isMatchingReleaseAsset(const QString &name, const QString &platform, const QString &arch)
True when a GitHub release asset filename is the binary for the given platform ("Windows"/"macOS"/"Li...
Checks the GitHub Releases API for newer versions of wiRedPanda.
bool isSafeGitHubUrl(const QUrl &url)
True when url is safe to download from or hand to the OS's URL handler.
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 ...
bool isMatchingReleaseAsset(const QString &name, const QString &platform, const QString &arch)
True when a GitHub release asset filename is the binary for the given platform ("Windows"/"macOS"/"Li...
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