1 #include "releasechannel.h"
2 
3 #include "version_string.h"
4 
5 #include <QJsonArray>
6 #include <QJsonDocument>
7 #include <QJsonObject>
8 #include <QMessageBox>
9 #include <QNetworkReply>
10 #include <QRegularExpression>
11 #include <QSysInfo>
12 #include <QtGlobal>
13 
14 #define STABLERELEASE_URL "https://api.github.com/repos/Cockatrice/Cockatrice/releases/latest"
15 #define STABLEMANUALDOWNLOAD_URL "https://github.com/Cockatrice/Cockatrice/releases/latest"
16 #define STABLETAG_URL "https://api.github.com/repos/Cockatrice/Cockatrice/git/refs/tags/"
17 
18 #define BETARELEASE_URL "https://api.github.com/repos/Cockatrice/Cockatrice/releases"
19 #define BETAMANUALDOWNLOAD_URL "https://github.com/Cockatrice/Cockatrice/releases/"
20 #define BETARELEASE_CHANGESURL "https://github.com/Cockatrice/Cockatrice/compare/%1...%2"
21 
22 #define GIT_SHORT_HASH_LEN 7
23 
24 int ReleaseChannel::sharedIndex = 0;
25 
ReleaseChannel()26 ReleaseChannel::ReleaseChannel() : response(nullptr), lastRelease(nullptr)
27 {
28     index = sharedIndex++;
29     netMan = new QNetworkAccessManager(this);
30 }
31 
~ReleaseChannel()32 ReleaseChannel::~ReleaseChannel()
33 {
34     netMan->deleteLater();
35 }
36 
checkForUpdates()37 void ReleaseChannel::checkForUpdates()
38 {
39     QString releaseChannelUrl = getReleaseChannelUrl();
40     qDebug() << "Searching for updates on the channel: " << releaseChannelUrl;
41     response = netMan->get(QNetworkRequest(releaseChannelUrl));
42     connect(response, SIGNAL(finished()), this, SLOT(releaseListFinished()));
43 }
44 
45 // Different release channel checking functions for different operating systems
46 #if defined(Q_OS_OSX)
downloadMatchesCurrentOS(const QString & fileName)47 bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
48 {
49     static QRegularExpression version_regex("macOS-(\\d+)\\.(\\d+)");
50     auto match = version_regex.match(fileName);
51     if (!match.hasMatch()) {
52         return false;
53     }
54 
55     // older(smaller) releases are compatible with a newer or the same system version
56     int sys_maj = QSysInfo::productVersion().split(".")[0].toInt();
57     int sys_min = QSysInfo::productVersion().split(".")[1].toInt();
58     int rel_maj = match.captured(1).toInt();
59     int rel_min = match.captured(2).toInt();
60     return rel_maj < sys_maj || (rel_maj == sys_maj && rel_min <= rel_min);
61 }
62 #elif defined(Q_OS_WIN)
downloadMatchesCurrentOS(const QString & fileName)63 bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
64 {
65 #if Q_PROCESSOR_WORDSIZE == 4
66     return fileName.contains("win32");
67 #elif Q_PROCESSOR_WORDSIZE == 8
68     return fileName.contains("win64");
69 #else
70     return false;
71 #endif
72 }
73 #else
downloadMatchesCurrentOS(const QString &)74 bool ReleaseChannel::downloadMatchesCurrentOS(const QString &)
75 {
76     // If the OS doesn't fit one of the above #defines, then it will never match
77     return false;
78 }
79 #endif
80 
getManualDownloadUrl() const81 QString StableReleaseChannel::getManualDownloadUrl() const
82 {
83     return QString(STABLEMANUALDOWNLOAD_URL);
84 }
85 
getName() const86 QString StableReleaseChannel::getName() const
87 {
88     return tr("Stable Releases");
89 }
90 
getReleaseChannelUrl() const91 QString StableReleaseChannel::getReleaseChannelUrl() const
92 {
93     return QString(STABLERELEASE_URL);
94 }
95 
releaseListFinished()96 void StableReleaseChannel::releaseListFinished()
97 {
98     auto *reply = static_cast<QNetworkReply *>(sender());
99     QJsonParseError parseError{};
100     QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
101     reply->deleteLater();
102     if (parseError.error != QJsonParseError::NoError) {
103         qWarning() << "No reply received from the release update server.";
104         emit error(tr("No reply received from the release update server."));
105         return;
106     }
107 
108     QVariantMap resultMap = jsonResponse.toVariant().toMap();
109     if (!(resultMap.contains("name") && resultMap.contains("html_url") && resultMap.contains("tag_name") &&
110           resultMap.contains("published_at"))) {
111         qWarning() << "Invalid received from the release update server.";
112         emit error(tr("Invalid reply received from the release update server."));
113         return;
114     }
115 
116     if (!lastRelease)
117         lastRelease = new Release;
118 
119     lastRelease->setName(resultMap["name"].toString());
120     lastRelease->setDescriptionUrl(resultMap["html_url"].toString());
121     lastRelease->setPublishDate(resultMap["published_at"].toDate());
122 
123     if (resultMap.contains("assets")) {
124         auto rawAssets = resultMap["assets"].toList();
125         // [(name, url)]
126         QVector<std::pair<QString, QString>> assets;
127         std::transform(rawAssets.begin(), rawAssets.end(), std::back_inserter(assets), [](QVariant _asset) {
128             QVariantMap asset = _asset.toMap();
129             QString name = asset["name"].toString();
130             QString url = asset["browser_download_url"].toString();
131             return std::make_pair(name, url);
132         });
133 
134         auto _releaseAsset = std::find_if(assets.begin(), assets.end(), [](std::pair<QString, QString> nameAndUrl) {
135             return downloadMatchesCurrentOS(nameAndUrl.first);
136         });
137 
138         if (_releaseAsset != assets.end()) {
139             std::pair<QString, QString> releaseAsset = *_releaseAsset;
140             auto releaseUrl = releaseAsset.second;
141             lastRelease->setDownloadUrl(releaseUrl);
142         }
143     }
144 
145     QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
146     QString myHash = QString(VERSION_COMMIT);
147     qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
148 
149     qDebug() << "Got reply from release server, name=" << lastRelease->getName()
150              << "desc=" << lastRelease->getDescriptionUrl() << "date=" << lastRelease->getPublishDate()
151              << "url=" << lastRelease->getDownloadUrl();
152 
153     const QString &tagName = resultMap["tag_name"].toString();
154     QString url = QString(STABLETAG_URL) + tagName;
155     qDebug() << "Searching for commit hash corresponding to stable channel tag: " << tagName;
156     response = netMan->get(QNetworkRequest(url));
157     connect(response, SIGNAL(finished()), this, SLOT(tagListFinished()));
158 }
159 
tagListFinished()160 void StableReleaseChannel::tagListFinished()
161 {
162     auto *reply = static_cast<QNetworkReply *>(sender());
163     QJsonParseError parseError{};
164     QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
165     reply->deleteLater();
166     if (parseError.error != QJsonParseError::NoError) {
167         qWarning() << "No reply received from the tag update server.";
168         emit error(tr("No reply received from the tag update server."));
169         return;
170     }
171 
172     QVariantMap resultMap = jsonResponse.toVariant().toMap();
173     if (!(resultMap.contains("object") && resultMap["object"].toMap().contains("sha"))) {
174         qWarning() << "Invalid received from the tag update server.";
175         emit error(tr("Invalid reply received from the tag update server."));
176         return;
177     }
178 
179     lastRelease->setCommitHash(resultMap["object"].toMap()["sha"].toString());
180     qDebug() << "Got reply from tag server, commit=" << lastRelease->getCommitHash();
181 
182     QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
183     QString myHash = QString(VERSION_COMMIT);
184     qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
185     const bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
186 
187     emit finishedCheck(needToUpdate, lastRelease->isCompatibleVersionFound(), lastRelease);
188 }
189 
fileListFinished()190 void StableReleaseChannel::fileListFinished()
191 {
192     // Only implemented to satisfy interface
193 }
194 
getManualDownloadUrl() const195 QString BetaReleaseChannel::getManualDownloadUrl() const
196 {
197     return QString(BETAMANUALDOWNLOAD_URL);
198 }
199 
getName() const200 QString BetaReleaseChannel::getName() const
201 {
202     return tr("Beta Releases");
203 }
204 
getReleaseChannelUrl() const205 QString BetaReleaseChannel::getReleaseChannelUrl() const
206 {
207     return QString(BETARELEASE_URL);
208 }
209 
releaseListFinished()210 void BetaReleaseChannel::releaseListFinished()
211 {
212     auto *reply = static_cast<QNetworkReply *>(sender());
213     QByteArray jsonData = reply->readAll();
214     reply->deleteLater();
215 
216     QJsonDocument doc = QJsonDocument::fromJson(jsonData);
217     QJsonArray array = doc.array();
218 
219     /*
220      * Get the latest release on GitHub
221      * This can be either a pre-release or a published release
222      * depending on timing. Both are acceptable.
223      */
224     QVariantMap resultMap = array.at(0).toObject().toVariantMap();
225 
226     if (array.empty() || resultMap.empty()) {
227         qWarning() << "No reply received from the release update server:" << QString(jsonData);
228         emit error(tr("No reply received from the release update server."));
229         return;
230     }
231 
232     // Make sure resultMap has all elements we'll need
233     if (!resultMap.contains("assets") || !resultMap.contains("author") || !resultMap.contains("tag_name") ||
234         !resultMap.contains("target_commitish") || !resultMap.contains("assets_url") ||
235         !resultMap.contains("published_at")) {
236         qWarning() << "Invalid received from the release update server:" << resultMap;
237         emit error(tr("Invalid reply received from the release update server."));
238         return;
239     }
240 
241     if (lastRelease == nullptr)
242         lastRelease = new Release;
243 
244     lastRelease->setCommitHash(resultMap["target_commitish"].toString());
245     lastRelease->setPublishDate(resultMap["published_at"].toDate());
246 
247     QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
248     lastRelease->setName(QString("%1 (%2)").arg(resultMap["tag_name"].toString()).arg(shortHash));
249     lastRelease->setDescriptionUrl(QString(BETARELEASE_CHANGESURL).arg(VERSION_COMMIT, shortHash));
250 
251     qDebug() << "Got reply from release server, size=" << resultMap.size() << "name=" << lastRelease->getName()
252              << "desc=" << lastRelease->getDescriptionUrl() << "commit=" << lastRelease->getCommitHash()
253              << "date=" << lastRelease->getPublishDate();
254 
255     QString betaBuildDownloadUrl = resultMap["assets_url"].toString();
256 
257     qDebug() << "Searching for a corresponding file on the beta channel: " << betaBuildDownloadUrl;
258     response = netMan->get(QNetworkRequest(betaBuildDownloadUrl));
259     connect(response, SIGNAL(finished()), this, SLOT(fileListFinished()));
260 }
261 
fileListFinished()262 void BetaReleaseChannel::fileListFinished()
263 {
264     auto *reply = static_cast<QNetworkReply *>(sender());
265     QJsonParseError parseError{};
266     QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
267     reply->deleteLater();
268     if (parseError.error != QJsonParseError::NoError) {
269         qWarning() << "No reply received from the file update server.";
270         emit error(tr("No reply received from the file update server."));
271         return;
272     }
273 
274     QVariantList resultList = jsonResponse.toVariant().toList();
275     QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
276     QString myHash = QString(VERSION_COMMIT);
277     qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
278 
279     bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
280     bool compatibleVersion = false;
281 
282     QStringList resultUrlList{};
283     for (QVariant file : resultList) {
284         QVariantMap map = file.toMap();
285         resultUrlList << map["browser_download_url"].toString();
286     }
287 
288     resultUrlList.sort();
289     // iterate in reverse so the first item is the latest os version
290     for (auto url = resultUrlList.rbegin(); url < resultUrlList.rend(); ++url) {
291         if (downloadMatchesCurrentOS(*url)) {
292             compatibleVersion = true;
293             lastRelease->setDownloadUrl(*url);
294             qDebug() << "Found compatible version url=" << *url;
295             break;
296         }
297     }
298 
299     emit finishedCheck(needToUpdate, compatibleVersion, lastRelease);
300 }
301