1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2020-12-31
7  * Description : Online version downloader.
8  *
9  * Copyright (C) 2020-2021 by Maik Qualmann <metzpinguin at gmail dot com>
10  * Copyright (C) 2010-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
11  *
12  * This program is free software; you can redistribute it
13  * and/or modify it under the terms of the GNU General
14  * Public License as published by the Free Software Foundation;
15  * either version 2, or (at your option)
16  * any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * ============================================================ */
24 
25 #include "onlineversiondwnl.h"
26 
27 // Qt includes
28 
29 #include <QDir>
30 #include <QSysInfo>
31 #include <QDialog>
32 #include <QFuture>
33 #include <QFutureWatcher>
34 #include <QtConcurrent>    // krazy:exclude=includes
35 #include <QByteArray>
36 #include <QPointer>
37 #include <QEventLoop>
38 #include <QApplication>
39 #include <QStandardPaths>
40 #include <QNetworkRequest>
41 #include <QCryptographicHash>
42 #include <QNetworkAccessManager>
43 
44 // KDE includes
45 
46 #include <klocalizedstring.h>
47 
48 // Local includes
49 
50 #include "digikam_debug.h"
51 #include "onlineversionchecker.h"
52 
53 namespace Digikam
54 {
55 
56 class Q_DECL_HIDDEN OnlineVersionDwnl::Private
57 {
58 public:
59 
Private()60     explicit Private()
61       : preRelease      (false),
62         updateWithDebug (false),
63         redirects       (0),
64         reply           (nullptr),
65         manager         (nullptr)
66     {
67     }
68 
69     bool                   preRelease;      ///< Flag to check pre-releases
70     bool                   updateWithDebug; ///< Flag to use version with debug symbols
71     int                    redirects;       ///< Count of redirected url
72 
73     QString                downloadUrl;     ///< Root url for current download
74     QString                checksums;       ///< Current download sha256 sums
75     QString                currentUrl;      ///< Full url of current file to download
76     QString                error;           ///< Error string about current download
77     QString                file;            ///< Info about file to download (version string, or filename)
78     QString                downloaded;      ///< Local file path to downloaded data
79 
80     QNetworkReply*         reply;           ///< Current network request reply
81     QNetworkAccessManager* manager;         ///< Network manager instance
82 };
83 
OnlineVersionDwnl(QObject * const parent,bool checkPreRelease,bool updateWithDebug)84 OnlineVersionDwnl::OnlineVersionDwnl(QObject* const parent,
85                                      bool checkPreRelease,
86                                      bool updateWithDebug)
87     : QObject(parent),
88       d      (new Private)
89 {
90     d->preRelease      = checkPreRelease;
91     d->updateWithDebug = updateWithDebug;
92 
93     if (d->preRelease)
94     {
95         d->downloadUrl = QLatin1String("https://files.kde.org/digikam/");
96     }
97     else
98     {
99         d->downloadUrl = QLatin1String("https://download.kde.org/stable/digikam/");
100     }
101 
102     d->manager         = new QNetworkAccessManager(this);
103     d->manager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
104 
105     connect(d->manager, SIGNAL(finished(QNetworkReply*)),
106             this, SLOT(slotDownloaded(QNetworkReply*)));
107 }
108 
~OnlineVersionDwnl()109 OnlineVersionDwnl::~OnlineVersionDwnl()
110 {
111     cancelDownload();
112     delete d;
113 }
114 
downloadUrl() const115 QString OnlineVersionDwnl::downloadUrl() const
116 {
117     return d->downloadUrl;
118 }
119 
cancelDownload()120 void OnlineVersionDwnl::cancelDownload()
121 {
122     if (d->reply)
123     {
124         d->reply->abort();
125         d->reply = nullptr;
126     }
127 }
128 
startDownload(const QString & version)129 void OnlineVersionDwnl::startDownload(const QString& version)
130 {
131     QUrl url;
132 
133     if (d->preRelease)
134     {
135         if (d->updateWithDebug)
136         {
137             QString base = version.section(QLatin1Char('.'), 0, -2);
138             QString suf  = version.section(QLatin1Char('.'), -1);
139             d->file      = base + QLatin1String("-debug.") + suf;
140         }
141         else
142         {
143             d->file = version;
144         }
145 
146         d->currentUrl = d->downloadUrl + d->file + QLatin1String(".sha256");
147         url           = QUrl(d->currentUrl);
148     }
149     else
150     {
151         QString arch;
152         QString bundle;
153         QString debug = d->updateWithDebug ? QLatin1String("-debug") : QString();
154 
155         if (!OnlineVersionChecker::bundleProperties(arch, bundle))
156         {
157             emit signalDownloadError(i18n("Unsupported Architecture: %1", QSysInfo::buildAbi()));
158 
159             qCDebug(DIGIKAM_GENERAL_LOG) << "Unsupported architecture";
160 
161             return;
162         }
163 
164         QString os    =
165 
166 #ifdef Q_OS_MACOS
167 
168                         QLatin1String("MacOS-");
169 
170 #else
171 
172                         QString();
173 
174 #endif
175 
176         d->file       = QString::fromLatin1("digiKam-%1-%2%3%4.%5")
177                             .arg(version)
178                             .arg(os)
179                             .arg(arch)
180                             .arg(debug)
181                             .arg(bundle);
182 
183         d->currentUrl = d->downloadUrl + QString::fromLatin1("%1/").arg(version) + d->file + QLatin1String(".sha256");
184         url           = QUrl(d->currentUrl);
185     }
186 
187     d->redirects = 0;
188     download(url);
189 }
190 
download(const QUrl & url)191 void OnlineVersionDwnl::download(const QUrl& url)
192 {
193     qCDebug(DIGIKAM_GENERAL_LOG) << "Downloading: " << url;
194 
195     d->redirects++;
196     d->reply = d->manager->get(QNetworkRequest(url));
197 
198     connect(d->reply, SIGNAL(downloadProgress(qint64,qint64)),
199             this, SIGNAL(signalDownloadProgress(qint64,qint64)));
200 
201     connect(d->reply, SIGNAL(sslErrors(QList<QSslError>)),
202             d->reply, SLOT(ignoreSslErrors()));
203 }
204 
slotDownloaded(QNetworkReply * reply)205 void OnlineVersionDwnl::slotDownloaded(QNetworkReply* reply)
206 {
207     if (reply != d->reply)
208     {
209         return;
210     }
211 
212     // mark for deletion
213 
214     reply->deleteLater();
215     d->reply = nullptr;
216 
217     if ((reply->error() != QNetworkReply::NoError)             &&
218         (reply->error() != QNetworkReply::InsecureRedirectError))
219     {
220         qCDebug(DIGIKAM_GENERAL_LOG) << "Error: " << reply->errorString();
221         emit signalDownloadError(reply->errorString());
222 
223         return;
224     }
225 
226     QUrl redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
227 
228     if (redirectUrl.isValid()         &&
229         (reply->url() != redirectUrl) &&
230         (d->redirects < 10))
231     {
232         download(redirectUrl);
233 
234         return;
235     }
236 
237     // Check if checksum arrive in first
238 
239     if (reply->url().url().endsWith(QLatin1String(".sha256")))
240     {
241         QByteArray data = reply->readAll();
242 
243         if (data.isEmpty())
244         {
245             qCDebug(DIGIKAM_GENERAL_LOG) << "Checksum file is empty";
246             emit signalDownloadError(i18n("Checksum file is empty."));
247 
248             return;
249         }
250 
251         QTextStream in(&data);
252         QString line    = in.readLine();  // first line and section 0 constains the checksum.
253         QString sums    = line.section(QLatin1Char(' '), 0, 0);
254 
255         if (sums.isEmpty())
256         {
257             qCDebug(DIGIKAM_GENERAL_LOG) << "Checksum is invalid";
258             emit signalDownloadError(i18n("Checksum is invalid."));
259 
260             return;
261         }
262 
263         d->checksums = sums;
264         qCDebug(DIGIKAM_GENERAL_LOG) << "Checksum is" << d->checksums;
265 
266         d->redirects = 0;
267         download(QUrl(d->currentUrl.remove(QLatin1String(".sha256"))));
268 
269         return;
270     }
271 
272     // Whole file to download is here
273 
274     QByteArray data = reply->readAll();
275 
276     if (data.isEmpty())
277     {
278         qCDebug(DIGIKAM_GENERAL_LOG) << "Downloaded file is empty";
279         emit signalDownloadError(i18n("Downloaded file is empty."));
280 
281         return;
282     }
283 
284     // Compute checksum in a separated thread
285 
286     emit signalComputeChecksum();
287 
288     QString hash;
289     QPointer<QEventLoop> loop = new QEventLoop(this);
290     QFutureWatcher<void> fwatcher;
291 
292     connect(&fwatcher, SIGNAL(finished()),
293             loop, SLOT(quit()));
294 
295     connect(static_cast<QDialog*>(parent()), SIGNAL(rejected()),
296             &fwatcher, SLOT(cancel()));
297 
298     fwatcher.setFuture(QtConcurrent::run([&hash, &data]()
299         {
300             QCryptographicHash sha256(QCryptographicHash::Sha256);
301             sha256.addData(data);
302             hash = QString::fromLatin1(sha256.result().toHex());
303         }
304     ));
305 
306     loop->exec();
307 
308     if (d->checksums != hash)
309     {
310         qCDebug(DIGIKAM_GENERAL_LOG) << "Checksums error";
311         emit signalDownloadError(i18n("Checksums error."));
312 
313         return;
314     }
315 
316     // Checksum is fine, now save data to disk
317 
318     QString path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
319     path         = QDir::toNativeSeparators(path + QLatin1String("/") + d->file);
320 
321     QFile file(path);
322 
323     if (file.open(QIODevice::WriteOnly))
324     {
325         file.write(data);
326         file.close();
327 
328         QFile::setPermissions(path, QFile::permissions(path) | QFileDevice::ExeUser);
329         d->downloaded = path;
330 
331         qCDebug(DIGIKAM_GENERAL_LOG) << "Download is complete: " << path;
332 
333         emit signalDownloadError(QString());  // No error: download is complete.
334     }
335     else
336     {
337         qCDebug(DIGIKAM_GENERAL_LOG) << "Cannot open " << path;
338         emit signalDownloadError(i18n("Cannot open target file."));
339     }
340 }
341 
downloadedPath() const342 QString OnlineVersionDwnl::downloadedPath() const
343 {
344     return d->downloaded;
345 }
346 
347 } // namespace Digikam
348