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 += "&current_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