keepassxc
176 строк · 5.2 Кб
1/*
2* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
3*
4* This program is free software: you can redistribute it and/or modify
5* it under the terms of the GNU General Public License as published by
6* the Free Software Foundation, either version 2 or (at your option)
7* version 3 of the License.
8*
9* This program is distributed in the hope that it will be useful,
10* but WITHOUT ANY WARRANTY; without even the implied warranty of
11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12* GNU General Public License for more details.
13*
14* You should have received a copy of the GNU General Public License
15* along with this program. If not, see <http://www.gnu.org/licenses/>.
16*/
17
18#include "UpdateChecker.h"
19
20#include "config-keepassx.h"
21#include "core/Clock.h"
22#include "core/Config.h"
23#include "core/NetworkManager.h"
24
25#include <QJsonArray>
26#include <QJsonDocument>
27#include <QJsonObject>
28#include <QNetworkReply>
29#include <QRegularExpression>
30
31const QString UpdateChecker::ErrorVersion("error");
32UpdateChecker* UpdateChecker::m_instance(nullptr);
33
34UpdateChecker::UpdateChecker(QObject* parent)
35: QObject(parent)
36, m_reply(nullptr)
37, m_isManuallyRequested(false)
38{
39}
40
41UpdateChecker::~UpdateChecker()
42{
43}
44
45void UpdateChecker::checkForUpdates(bool manuallyRequested)
46{
47// Skip update if we are already performing one
48if (m_reply) {
49return;
50}
51
52auto nextCheck = config()->get(Config::GUI_CheckForUpdatesNextCheck).toULongLong();
53m_isManuallyRequested = manuallyRequested;
54
55if (m_isManuallyRequested || Clock::currentSecondsSinceEpoch() >= nextCheck) {
56m_bytesReceived.clear();
57
58QString apiUrlStr = QString("https://api.github.com/repos/keepassxreboot/keepassxc/releases");
59
60if (!config()->get(Config::GUI_CheckForUpdatesIncludeBetas).toBool()) {
61apiUrlStr += "/latest";
62}
63
64QUrl apiUrl = QUrl(apiUrlStr);
65
66QNetworkRequest request(apiUrl);
67request.setRawHeader("Accept", "application/json");
68
69m_reply = getNetMgr()->get(request);
70
71connect(m_reply, &QNetworkReply::finished, this, &UpdateChecker::fetchFinished);
72connect(m_reply, &QIODevice::readyRead, this, &UpdateChecker::fetchReadyRead);
73}
74}
75
76void UpdateChecker::fetchReadyRead()
77{
78m_bytesReceived += m_reply->readAll();
79}
80
81void UpdateChecker::fetchFinished()
82{
83bool error = (m_reply->error() != QNetworkReply::NoError);
84bool hasNewVersion = false;
85QUrl url = m_reply->url();
86QString version = "";
87
88m_reply->deleteLater();
89m_reply = nullptr;
90
91if (!error) {
92QJsonDocument jsonResponse = QJsonDocument::fromJson(m_bytesReceived);
93QJsonObject jsonObject = jsonResponse.object();
94
95if (config()->get(Config::GUI_CheckForUpdatesIncludeBetas).toBool()) {
96QJsonArray jsonArray = jsonResponse.array();
97jsonObject = jsonArray.at(0).toObject();
98}
99
100if (!jsonObject.value("tag_name").isUndefined()) {
101version = jsonObject.value("tag_name").toString();
102hasNewVersion = compareVersions(QString(KEEPASSXC_VERSION), version);
103}
104
105// Check again in 7 days
106// TODO: change to toSecsSinceEpoch() when min Qt >= 5.8
107config()->set(Config::GUI_CheckForUpdatesNextCheck, Clock::currentDateTime().addDays(7).toTime_t());
108} else {
109version = ErrorVersion;
110}
111
112emit updateCheckFinished(hasNewVersion, version, m_isManuallyRequested);
113}
114
115bool UpdateChecker::compareVersions(const QString& localVersion, const QString& remoteVersion)
116{
117// Quick full-string equivalence check
118if (localVersion == remoteVersion) {
119return false;
120}
121
122QRegularExpression verRegex(R"(^((?:\d+\.){2}\d+)(?:-(\w+?)(\d+)?)?$)");
123
124auto lmatch = verRegex.match(localVersion);
125auto rmatch = verRegex.match(remoteVersion);
126
127auto lVersion = lmatch.captured(1).split(".");
128auto lSuffix = lmatch.captured(2);
129auto lBetaNum = lmatch.captured(3);
130
131auto rVersion = rmatch.captured(1).split(".");
132auto rSuffix = rmatch.captured(2);
133auto rBetaNum = rmatch.captured(3);
134
135if (!lVersion.isEmpty() && !rVersion.isEmpty()) {
136if (lSuffix.compare("snapshot", Qt::CaseInsensitive) == 0) {
137// Snapshots are not checked for version updates
138return false;
139}
140
141// Check "-beta[X]" versions
142if (lVersion == rVersion && !lSuffix.isEmpty()) {
143// Check if stable version has been released or new beta is available
144// otherwise the version numbers are equal
145return rSuffix.isEmpty() || lBetaNum.toInt() < rBetaNum.toInt();
146}
147
148for (int i = 0; i < 3; i++) {
149int l = lVersion[i].toInt();
150int r = rVersion[i].toInt();
151
152if (l == r) {
153continue;
154}
155
156if (l > r) {
157return false; // Installed version is newer than release
158} else {
159return true; // Installed version is outdated
160}
161}
162
163return false; // Installed version is the same
164}
165
166return false; // Invalid version string
167}
168
169UpdateChecker* UpdateChecker::instance()
170{
171if (!m_instance) {
172m_instance = new UpdateChecker();
173}
174
175return m_instance;
176}
177