1 #include "UpdateWorker.h"
2 
3 #if CUTTER_UPDATE_WORKER_AVAILABLE
4 #include <QUrl>
5 #include <QFile>
6 #include <QTimer>
7 #include <QEventLoop>
8 #include <QDataStream>
9 #include <QJsonObject>
10 #include <QApplication>
11 #include <QJsonDocument>
12 #include <QDesktopServices>
13 #include <QtNetwork/QNetworkReply>
14 #include <QtNetwork/QNetworkRequest>
15 
16 #include <QProgressDialog>
17 #include <QPushButton>
18 #include <QFileDialog>
19 #include <QMessageBox>
20 #include "common/Configuration.h"
21 #include "CutterConfig.h"
22 
23 
UpdateWorker(QObject * parent)24 UpdateWorker::UpdateWorker(QObject *parent) :
25     QObject(parent), pending(false)
26 {
27     connect(&t, &QTimer::timeout, this, [this]() {
28         if (pending) {
29             disconnect(checkReply, nullptr, this, nullptr);
30             checkReply->close();
31             checkReply->deleteLater();
32             emit checkComplete(QVersionNumber(), tr("Time limit exceeded during version check. Please check your "
33                                                     "internet connection and try again."));
34         }
35     });
36 }
37 
checkCurrentVersion(time_t timeoutMs)38 void UpdateWorker::checkCurrentVersion(time_t timeoutMs)
39 {
40     QUrl url("https://api.github.com/repos/radareorg/cutter/releases/latest");
41     QNetworkRequest request;
42     request.setUrl(url);
43 
44     t.setInterval(timeoutMs);
45     t.setSingleShot(true);
46     t.start();
47 
48     checkReply = nm.get(request);
49     connect(checkReply, &QNetworkReply::finished,
50             this, &UpdateWorker::serveVersionCheckReply);
51     pending = true;
52 }
53 
download(QString filename,QString version)54 void UpdateWorker::download(QString filename, QString version)
55 {
56     downloadFile.setFileName(filename);
57     downloadFile.open(QIODevice::WriteOnly);
58 
59     QNetworkRequest request;
60     request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
61     QUrl url(QString("https://github.com/radareorg/cutter/releases/"
62                      "download/v%1/%2").arg(version).arg(getRepositoryFileName()));
63     request.setUrl(url);
64 
65     downloadReply = nm.get(request);
66     connect(downloadReply, &QNetworkReply::downloadProgress,
67             this, &UpdateWorker::process);
68     connect(downloadReply, &QNetworkReply::finished,
69             this, &UpdateWorker::serveDownloadFinish);
70 }
71 
showUpdateDialog(bool showDontCheckForUpdatesButton)72 void UpdateWorker::showUpdateDialog(bool showDontCheckForUpdatesButton)
73 {
74     QMessageBox mb;
75     mb.setWindowTitle(tr("Version control"));
76     mb.setText(tr("There is an update available for Cutter.<br/>")
77                + "<b>" + tr("Current version:") + "</b> " CUTTER_VERSION_FULL "<br/>"
78                + "<b>" + tr("Latest version:") + "</b> " + latestVersion.toString() + "<br/><br/>"
79                + tr("For update, please check the link:<br/>")
80                + QString("<a href=\"https://github.com/radareorg/cutter/releases/tag/v%1\">"
81                          "https://github.com/radareorg/cutter/releases/tag/v%1</a><br/>").arg(latestVersion.toString())
82                + tr("or click \"Download\" to download latest version of Cutter."));
83     if (showDontCheckForUpdatesButton) {
84         mb.setStandardButtons(QMessageBox::Save | QMessageBox::Reset | QMessageBox::Ok);
85         mb.button(QMessageBox::Reset)->setText(tr("Don't check for updates"));
86     } else {
87         mb.setStandardButtons(QMessageBox::Save | QMessageBox::Ok);
88     }
89     mb.button(QMessageBox::Save)->setText(tr("Download"));
90     mb.setDefaultButton(QMessageBox::Ok);
91     int ret = mb.exec();
92     if (ret == QMessageBox::Reset) {
93         Config()->setAutoUpdateEnabled(false);
94     } else if (ret == QMessageBox::Save) {
95         QString fullFileName =
96                 QFileDialog::getSaveFileName(nullptr,
97                                              tr("Choose directory for downloading"),
98                                              QStandardPaths::writableLocation(QStandardPaths::HomeLocation) +
99                                              QDir::separator() + getRepositoryFileName(),
100                                              QString("%1 (*.%1)").arg(getRepositeryExt()));
101         if (!fullFileName.isEmpty()) {
102             QProgressDialog progressDial(tr("Downloading update..."),
103                                          tr("Cancel"),
104                                          0, 100);
105             connect(this, &UpdateWorker::downloadProcess, &progressDial,
106                     [&progressDial](size_t curr, size_t total) {
107                 progressDial.setValue(100.0f * curr / total);
108             });
109             connect(&progressDial, &QProgressDialog::canceled,
110                     this, &UpdateWorker::abortDownload);
111             connect(this, &UpdateWorker::downloadFinished,
112                     &progressDial, &QProgressDialog::cancel);
113             connect(this, &UpdateWorker::downloadFinished, this,
114                     [](QString filePath){
115                 QMessageBox info(QMessageBox::Information,
116                                  tr("Download finished!"),
117                                  tr("Latest version of Cutter was succesfully downloaded!"),
118                                  QMessageBox::Yes | QMessageBox::Open | QMessageBox::Ok,
119                                  nullptr);
120                 info.button(QMessageBox::Open)->setText(tr("Open file"));
121                 info.button(QMessageBox::Yes)->setText(tr("Open download folder"));
122                 int r = info.exec();
123                 if (r == QMessageBox::Open) {
124                     QDesktopServices::openUrl(filePath);
125                 } else if (r == QMessageBox::Yes) {
126                     auto path = filePath.split('/');
127                     path.removeLast();
128                     QDesktopServices::openUrl(path.join('/'));
129                 }
130             });
131             download(fullFileName, latestVersion.toString());
132             // Calling show() before exec() is only way make dialog non-modal
133             // it seems weird, but it works
134             progressDial.show();
135             progressDial.exec();
136         }
137     }
138 }
139 
abortDownload()140 void UpdateWorker::abortDownload()
141 {
142     disconnect(downloadReply, &QNetworkReply::finished,
143                this, &UpdateWorker::serveDownloadFinish);
144     disconnect(downloadReply, &QNetworkReply::downloadProgress,
145                this, &UpdateWorker::process);
146     downloadReply->close();
147     downloadReply->deleteLater();
148     downloadFile.remove();
149 }
150 
serveVersionCheckReply()151 void UpdateWorker::serveVersionCheckReply()
152 {
153     pending = false;
154     QString versionReplyStr = "";
155     QString errStr = "";
156     if (checkReply->error()) {
157         errStr = checkReply->errorString();
158     } else {
159         versionReplyStr = QJsonDocument::fromJson(checkReply->readAll()).object().value("tag_name").toString();
160         versionReplyStr.remove('v');
161     }
162     QVersionNumber versionReply = QVersionNumber::fromString(versionReplyStr);
163     if (!versionReply.isNull()) {
164         latestVersion = versionReply;
165     }
166     checkReply->close();
167     checkReply->deleteLater();
168     emit checkComplete(versionReply, errStr);
169 }
170 
serveDownloadFinish()171 void UpdateWorker::serveDownloadFinish()
172 {
173     downloadReply->close();
174     downloadReply->deleteLater();
175     if (downloadReply->error()) {
176         emit downloadError(downloadReply->errorString());
177     } else {
178         emit downloadFinished(downloadFile.fileName());
179     }
180 }
181 
process(size_t bytesReceived,size_t bytesTotal)182 void UpdateWorker::process(size_t bytesReceived, size_t bytesTotal)
183 {
184     downloadFile.write(downloadReply->readAll());
185     emit downloadProcess(bytesReceived, bytesTotal);
186 }
187 
getRepositeryExt() const188 QString UpdateWorker::getRepositeryExt() const
189 {
190 #ifdef Q_OS_LINUX
191     return "AppImage";
192 #elif defined (Q_OS_WIN64) || defined (Q_OS_WIN32)
193     return "zip";
194 #elif defined (Q_OS_MACOS)
195     return "dmg";
196 #endif
197 }
198 
getRepositoryFileName() const199 QString UpdateWorker::getRepositoryFileName() const
200 {
201     QString downloadFileName;
202 #ifdef Q_OS_LINUX
203     downloadFileName = "r2cutter-v%1-x%2.Linux.AppImage";
204 #elif defined (Q_OS_WIN64) || defined (Q_OS_WIN32)
205     downloadFileName = "r2cutter-v%1-x%2.Windows.zip";
206 #elif defined (Q_OS_MACOS)
207     downloadFileName = "r2cutter-v%1-x%2.macOS.dmg";
208 #endif
209     downloadFileName = downloadFileName
210                        .arg(latestVersion.toString())
211                        .arg(QSysInfo::buildAbi().split('-').at(2).contains("64")
212                             ? "64"
213                             : "32");
214 
215     return downloadFileName;
216 }
217 
currentVersionNumber()218 QVersionNumber UpdateWorker::currentVersionNumber()
219 {
220     return QVersionNumber(CUTTER_VERSION_MAJOR, CUTTER_VERSION_MINOR, CUTTER_VERSION_PATCH);
221 }
222 #endif // CUTTER_UPDATE_WORKER_AVAILABLE
223