1 // -*- C++ -*-
2 // $Id: upgrade.cpp,v 1.26 2010-06-19 23:59:06 robertl Exp $
3 /*
4 Copyright (C) 2009, 2010 Robert Lipe, robertlipe@gpsbabel.org
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
20 */
21
22
23 #include "babeldata.h"
24 #include "format.h"
25 #include "upgrade.h"
26 #include "../gbversion.h"
27
28 #include <cstdio>
29
30 #include <QtCore/QDebug>
31 #include <QtCore/QLocale>
32 #include <QtCore/QSysInfo>
33 #include <QtCore/QUrl>
34 #include <QtCore/QVariant>
35 #include <QtCore/QVersionNumber>
36 #include <QtGui/QDesktopServices>
37 #include <QtNetwork/QNetworkAccessManager>
38 #include <QtNetwork/QNetworkReply>
39 #include <QtNetwork/QNetworkRequest>
40 #include <QtWidgets/QMessageBox>
41 #include <QtXml/QDomDocument>
42
43
44 #if 0
45 static const bool testing = true;
46 #else
47 static const bool testing = false;
48 #endif
49
UpgradeCheck(QWidget * parent,QList<Format> & formatList,BabelData & bd)50 UpgradeCheck::UpgradeCheck(QWidget* parent, QList<Format>& formatList,
51 BabelData& bd) :
52 QObject(parent),
53 manager_(nullptr),
54 replyId_(nullptr),
55 upgradeUrl_(QUrl("http://www.gpsbabel.org/upgrade_check.html")),
56 formatList_(formatList),
57 updateStatus_(updateUnknown),
58 babelData_(bd)
59 {
60 }
61
~UpgradeCheck()62 UpgradeCheck::~UpgradeCheck()
63 {
64 if (replyId_ != nullptr) {
65 replyId_->abort();
66 replyId_ = nullptr;
67 }
68 if (manager_ != nullptr) {
69 delete manager_;
70 manager_ = nullptr;
71 }
72 }
73
isTestMode()74 bool UpgradeCheck::isTestMode()
75 {
76 return testing;
77 }
78
79 // Since Qt 5.4 QSysInfo makes it easy to get the OsName,
80 // OsVersion and CpuArchitecture.
getOsName()81 QString UpgradeCheck::getOsName()
82 {
83 return QSysInfo::productType();
84 }
85
getOsVersion()86 QString UpgradeCheck::getOsVersion()
87 {
88 return QSysInfo::productVersion();
89 }
90
getCpuArchitecture()91 QString UpgradeCheck::getCpuArchitecture()
92 {
93 return QSysInfo::currentCpuArchitecture();
94 }
95
checkForUpgrade(const QString & currentVersionIn,const QDateTime & lastCheckTime,bool allowBeta)96 UpgradeCheck::updateStatus UpgradeCheck::checkForUpgrade(
97 const QString& currentVersionIn,
98 const QDateTime& lastCheckTime,
99 bool allowBeta)
100 {
101 currentVersion_ = currentVersionIn;
102 currentVersion_.remove("GPSBabel Version ");
103
104 QDateTime soonestCheckTime = lastCheckTime.addDays(1);
105 if (!testing && QDateTime::currentDateTime() < soonestCheckTime) {
106 // Not time to check yet.
107 return UpgradeCheck::updateUnknown;
108 }
109
110 manager_ = new QNetworkAccessManager;
111
112 connect(manager_, SIGNAL(finished(QNetworkReply*)),
113 this, SLOT(httpRequestFinished(QNetworkReply*)));
114
115 QNetworkRequest request = QNetworkRequest(upgradeUrl_);
116 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
117 request.setRawHeader("Accept-Encoding","identity");
118
119 QLocale locale;
120
121 QString args = "current_version=" + currentVersion_;
122 args += "¤t_gui_version=" VERSION;
123 args += "&installation=" + babelData_.installationUuid_;
124 args += "&os=" + getOsName();
125 args += "&cpu=" + getCpuArchitecture();
126 args += "&os_ver=" + getOsVersion();
127 args += QString("&beta_ok=%1").arg(static_cast<int>(allowBeta));
128 args += "&lang=" + QLocale::languageToString(locale.language());
129 args += "&last_checkin=" + lastCheckTime.toString(Qt::ISODate);
130 args += QString("&ugcb=%1").arg(babelData_.upgradeCallbacks_);
131 args += QString("&ugdec=%1").arg(babelData_.upgradeDeclines_);
132 args += QString("&ugacc=%1").arg(babelData_.upgradeAccept_);
133 args += QString("&ugoff=%1").arg(babelData_.upgradeOffers_);
134 args += QString("&ugerr=%1").arg(babelData_.upgradeErrors_);
135 args += QString("&rc=%1").arg(babelData_.runCount_);
136
137 int j = 0;
138
139 for (int i = 0; i < formatList_.size(); i++) {
140 int rc = formatList_[i].getReadUseCount();
141 int wc = formatList_[i].getWriteUseCount();
142 QString formatName = formatList_[i].getName();
143 if (rc != 0) {
144 args += QString("&uc%1=rd/%2/%3").arg(j++).arg(formatName).arg(rc);
145 }
146 if (wc != 0) {
147 args += QString("&uc%1=wr/%2/%3").arg(j++).arg(formatName).arg(wc);
148 }
149 }
150 if ((j != 0) && babelData_.reportStatistics_) {
151 args += QString("&uc=%1").arg(j);
152 }
153
154 if (false && testing) {
155 qDebug() << "Posting " << args;
156 }
157
158 replyId_ = manager_->post(request, args.toUtf8());
159
160 return UpgradeCheck::updateUnknown;
161 }
162
getUpgradeWarningTime()163 QDateTime UpgradeCheck::getUpgradeWarningTime()
164 {
165 return upgradeWarningTime_;
166 }
167
getStatus()168 UpgradeCheck::updateStatus UpgradeCheck::getStatus()
169 {
170 return updateStatus_;
171 }
172
173 // GPSBabel version numbers throughout the code mostly predate QVersionNumber
174 // and are stored as strings. They may be of the form "1.6.0-beta20200413"
175 // which, if sorted as a string, will be after "1.6.0" which is bad. Use
176 // this function to sort that out. (See what I did there? Bwaaaahah!)
suggestUpgrade(const QString & from,const QString & to)177 bool UpgradeCheck::suggestUpgrade(const QString& from, const QString& to)
178 {
179 int fromIndex = 0;
180 int toIndex = 0;
181 QVersionNumber fromVersion = QVersionNumber::fromString(from, &fromIndex);
182 QVersionNumber toVersion = QVersionNumber::fromString(to, &toIndex);
183
184 // We don't have to handle every possible range because the server won't
185 // have more than a version or two live at any time.
186 if (fromVersion < toVersion) {
187 return true;
188 }
189 // Just look for the presence of stuff (not even the contents) of the
190 // string. Shorter string (no "-betaXXX" wins)
191 if (fromVersion == toVersion) {
192 if (from.length() - fromIndex > to.length() - toIndex) {
193 return true;
194 }
195 }
196 return false;
197 }
198 // Some day when we have Gunit or equiv, add unit tests for:
199 //suggestUpgrade(updateVersion, currentVersion_);
200 //suggestUpgrade("1.6.0-beta20190413", "1.6.0");
201 //suggestUpgrade("1.6.0", "1.6.0-beta20190413");
202 //suggestUpgrade("1.6.0-beta20190413", "1.7.0");
203 //suggestUpgrade("1.7.0", "1.6.0-beta20190413");
204
httpRequestFinished(QNetworkReply * reply)205 void UpgradeCheck::httpRequestFinished(QNetworkReply* reply)
206 {
207
208 if (reply == nullptr) {
209 babelData_.upgradeErrors_++;
210 return;
211 }
212 if (reply != replyId_) {
213 QMessageBox::information(nullptr, tr("HTTP"),
214 tr("Unexpected reply."));
215 } else if (reply->error() != QNetworkReply::NoError) {
216 babelData_.upgradeErrors_++;
217 QMessageBox::information(nullptr, tr("HTTP"),
218 tr("Download failed: %1.")
219 .arg(reply->errorString()));
220 replyId_ = nullptr;
221 reply->deleteLater();
222 return;
223 }
224
225 // New post 1.4.4: Allow redirects in case a proxy server or something
226 // slightly rewrites the post.
227 // Note that adding port 80 to the url will cause a redirect, which is useful for testing.
228 // Also you use gpsbabel.org instead of www.gpsbabel.org to generate redirects.
229 QVariant attributeValue = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
230 if (!attributeValue.isNull() && attributeValue.isValid()) {
231 QUrl redirectUrl = attributeValue.toUrl();
232 if (redirectUrl.isValid()) {
233 if (testing) {
234 qDebug() << "redirect to " << redirectUrl.toString();
235 }
236 // Change the url for the next update check.
237 // TODO: kick off another update check.
238 upgradeUrl_ = redirectUrl;
239 replyId_ = nullptr;
240 reply->deleteLater();
241 return;
242 }
243 }
244
245 QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
246 if (testing) {
247 qDebug() << "http status code " << statusCode.toInt();
248 }
249 if (statusCode != 200) {
250 QVariant reason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute);
251 QMessageBox::information(nullptr, tr("HTTP"),
252 tr("Download failed: %1: %2.")
253 .arg(statusCode.toInt())
254 .arg(reason.toString()));
255 replyId_ = nullptr;
256 reply->deleteLater();
257 return;
258 }
259
260 babelData_.upgradeCallbacks_++;
261 QString oresponse(reply->readAll());
262
263 QDomDocument document;
264 int line = -1;
265 QString error_text;
266 // This shouldn't ever be seen by a user.
267 if (!document.setContent(oresponse, &error_text, &line)) {
268 QMessageBox::critical(nullptr, tr("Error"),
269 tr("Invalid return data at line %1: %2.")
270 .arg(line)
271 .arg(error_text));
272 babelData_.upgradeErrors_++;
273 replyId_ = nullptr;
274 reply->deleteLater();
275 return;
276 }
277
278 QString response;
279 QString upgradeText;
280
281 if (testing) {
282 currentVersion_ = "1.3.1"; // for testing
283 }
284
285 bool allowBeta = true; // TODO: come from prefs or current version...
286
287 QDomNodeList upgrades = document.elementsByTagName("update");
288 QUrl downloadUrl;
289 updateStatus_ = updateCurrent; // Current until proven guilty.
290
291 for (int i = 0; i < upgrades.length(); i++) {
292 QDomNode upgradeNode = upgrades.item(i);
293 QDomElement upgrade = upgradeNode.toElement();
294 QString updateVersion = upgrade.attribute("version");
295 if (upgrade.attribute("downloadURL").isEmpty()) {
296 downloadUrl = "https://www.gpsbabel.org/download.html";
297 } else {
298 downloadUrl = upgrade.attribute("downloadURL");
299 }
300 bool updateIsBeta = upgrade.attribute("type") == "beta";
301 bool updateIsMajor = upgrade.attribute("type") == "major";
302 bool updateIsMinor = upgrade.attribute("type") == "minor";
303
304 bool updateCandidate = updateIsMajor || updateIsMinor || (updateIsBeta && allowBeta);
305 upgradeText = upgrade.firstChildElement("overview").text();
306
307 // String compare, not a numeric one. Server will return "best first".
308 if (suggestUpgrade(currentVersion_, updateVersion) && updateCandidate) {
309 babelData_.upgradeOffers_++;
310 updateStatus_ = updateNeeded;
311 response = tr("A new version of GPSBabel is available.<br />"
312 "Your version is %1 <br />"
313 "The latest version is %2")
314 .arg(currentVersion_, updateVersion);
315 break;
316 }
317 }
318
319 if (response.length() != 0) {
320 QMessageBox information;
321 information.setWindowTitle(tr("Upgrade"));
322
323 information.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
324 information.setDefaultButton(QMessageBox::Yes);
325 information.setTextFormat(Qt::RichText);
326 information.setText(response);
327
328 information.setInformativeText(tr("Do you wish to download an upgrade?"));
329 // The text field can be RichText, but DetailedText can't be. Odd.
330 information.setDetailedText(upgradeText);
331
332 switch (information.exec()) {
333 case QMessageBox::Yes:
334 // downloadUrl.addQueryItem("os", getOsName());
335 QDesktopServices::openUrl(downloadUrl);
336 babelData_.upgradeAccept_++;
337 break;
338 default:
339 ;
340 babelData_.upgradeDeclines_++;
341 }
342 }
343
344 upgradeWarningTime_ = QDateTime(QDateTime::currentDateTime());
345
346 for (int i = 0; i < formatList_.size(); i++) {
347 formatList_[i].zeroUseCounts();
348 }
349 replyId_ = nullptr;
350 reply->deleteLater();
351 }
352