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