1 /*!
2  * \copyright Copyright (c) 2016-2021 Governikus GmbH & Co. KG, Germany
3  */
4 
5 #include "AppUpdater.h"
6 
7 #include "AppSettings.h"
8 #include "Downloader.h"
9 #include "SecureStorage.h"
10 #include "VersionNumber.h"
11 
12 #include <QCryptographicHash>
13 #include <QDir>
14 #include <QFile>
15 #include <QLoggingCategory>
16 #include <QMetaEnum>
17 #include <QStandardPaths>
18 
19 using namespace governikus;
20 
Q_DECLARE_LOGGING_CATEGORY(appupdate)21 Q_DECLARE_LOGGING_CATEGORY(appupdate)
22 
23 AppUpdater::AppUpdater()
24 	: mForceUpdate(false)
25 	, mAppUpdateJsonUrl()
26 	, mAppUpdateData()
27 	, mDownloadPath(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/'))
28 	, mDownloadInProgress(false)
29 {
30 	const auto* secureStorage = Env::getSingleton<SecureStorage>();
31 
32 	mAppUpdateJsonUrl = VersionNumber::getApplicationVersion().isDeveloperVersion() ? secureStorage->getAppcastBetaUpdateUrl() : secureStorage->getAppcastUpdateUrl();
33 }
34 
35 
checkAppUpdate(bool pForceUpdate)36 bool AppUpdater::checkAppUpdate(bool pForceUpdate)
37 {
38 	mForceUpdate = pForceUpdate;
39 	mAppUpdateData = AppUpdateData();
40 	return download(mAppUpdateJsonUrl);
41 }
42 
43 
download(const QUrl & pUrl)44 bool AppUpdater::download(const QUrl& pUrl)
45 {
46 	if (mDownloadInProgress)
47 	{
48 		return false;
49 	}
50 
51 	auto* downloader = Env::getSingleton<Downloader>();
52 	connect(downloader, &Downloader::fireDownloadProgress, this, &AppUpdater::onDownloadProgress);
53 	connect(downloader, &Downloader::fireDownloadSuccess, this, &AppUpdater::onDownloadFinished);
54 	connect(downloader, &Downloader::fireDownloadFailed, this, &AppUpdater::onDownloadFailed);
55 	connect(downloader, &Downloader::fireDownloadUnnecessary, this, &AppUpdater::onDownloadUnnecessary);
56 
57 	mDownloadInProgress = true;
58 	downloader->download(pUrl);
59 	return true;
60 }
61 
62 
save(const QByteArray & pData,const QString & pFilename)63 QString AppUpdater::save(const QByteArray& pData, const QString& pFilename)
64 {
65 	const QDir dir(mDownloadPath);
66 	if (!dir.exists())
67 	{
68 		qCDebug(appupdate) << "Create cache directory:" << dir.mkpath(mDownloadPath);
69 	}
70 
71 	QFile file(mDownloadPath + pFilename);
72 
73 	if (!file.open(QIODevice::WriteOnly))
74 	{
75 		qCCritical(appupdate) << "File could not be opened for writing:" << file.fileName();
76 		return QString();
77 	}
78 
79 	if (file.write(pData) != pData.size())
80 	{
81 		qCCritical(appupdate) << "Not all data could be written to file:" << file.fileName();
82 		file.close();
83 		file.remove();
84 		return QString();
85 	}
86 
87 	qCDebug(appupdate) << "Data written to file:" << file.fileName();
88 	file.close();
89 	return file.fileName();
90 }
91 
92 
abortDownload()93 bool AppUpdater::abortDownload()
94 {
95 	if (mDownloadInProgress)
96 	{
97 		return Env::getSingleton<Downloader>()->abort(mAppUpdateData.getUrl());
98 	}
99 
100 	return false;
101 }
102 
103 
downloadUpdate()104 bool AppUpdater::downloadUpdate()
105 {
106 	Q_ASSERT(mAppUpdateData.isValid());
107 
108 	if (mAppUpdateData.isValid())
109 	{
110 		return download(mAppUpdateData.getChecksumUrl());
111 	}
112 
113 	return false;
114 }
115 
116 
getUpdateData() const117 const AppUpdateData& AppUpdater::getUpdateData() const
118 {
119 	return mAppUpdateData;
120 }
121 
122 
skipVersion(const QString & pVersion)123 void AppUpdater::skipVersion(const QString& pVersion)
124 {
125 	qCInfo(appupdate) << "Skip application update:" << pVersion;
126 	Env::getSingleton<AppSettings>()->getGeneralSettings().skipVersion(pVersion);
127 }
128 
129 
130 #ifndef QT_NO_DEBUG
getDownloadPath() const131 QString AppUpdater::getDownloadPath() const
132 {
133 	return mDownloadPath;
134 }
135 
136 
setDownloadPath(const QString & pPath)137 void AppUpdater::setDownloadPath(const QString& pPath)
138 {
139 	mDownloadPath = pPath;
140 
141 	if (!mDownloadPath.endsWith(QLatin1Char('/')))
142 	{
143 		mDownloadPath += QLatin1Char('/');
144 	}
145 }
146 
147 
148 #endif
149 
150 
getHashAlgo(const QByteArray & pAlgo)151 QCryptographicHash::Algorithm AppUpdater::getHashAlgo(const QByteArray& pAlgo)
152 {
153 	const QByteArray algo = pAlgo.isEmpty() ? pAlgo : pAlgo.left(1).toUpper() + pAlgo.mid(1).toLower();
154 	const auto metatype = QMetaEnum::fromType<QCryptographicHash::Algorithm>();
155 	bool ok = false;
156 	const int key = metatype.keyToValue(algo.constData(), &ok);
157 	return ok ? static_cast<QCryptographicHash::Algorithm>(key) : QCryptographicHash::Algorithm::Sha256;
158 }
159 
160 
onDownloadFinished(const QUrl & pUpdateUrl,const QDateTime & pNewTimestamp,const QByteArray & pData)161 void AppUpdater::onDownloadFinished(const QUrl& pUpdateUrl, const QDateTime& pNewTimestamp, const QByteArray& pData)
162 {
163 	Q_UNUSED(pNewTimestamp)
164 
165 	if (pUpdateUrl == mAppUpdateJsonUrl)
166 	{
167 		AppUpdateData newData(pData);
168 		if (newData.isValid())
169 		{
170 			mAppUpdateData = newData;
171 			const auto& version = mAppUpdateData.getVersion();
172 
173 			if (VersionNumber(version) > VersionNumber::getApplicationVersion())
174 			{
175 				if (!mForceUpdate && version == Env::getSingleton<AppSettings>()->getGeneralSettings().getSkipVersion())
176 				{
177 					qCInfo(appupdate) << "Version will be skipped:" << version;
178 					Q_EMIT fireAppcastCheckFinished(false, GlobalStatus::Code::No_Error);
179 				}
180 				else
181 				{
182 					mForceUpdate = false;
183 					qCInfo(appupdate) << "Found new version:" << version << ", greater than old version" << QCoreApplication::applicationVersion();
184 					Env::getSingleton<Downloader>()->download(mAppUpdateData.getNotesUrl());
185 					return;
186 				}
187 			}
188 			else
189 			{
190 				qCDebug(appupdate) << "No new version:" << version;
191 				Q_EMIT fireAppcastCheckFinished(false, GlobalStatus::Code::No_Error);
192 			}
193 		}
194 		else
195 		{
196 			Q_EMIT fireAppcastCheckFinished(false, newData.getParsingResult().getStatusCode());
197 		}
198 		clearDownloaderConnection();
199 	}
200 	else if (pUpdateUrl == mAppUpdateData.getNotesUrl())
201 	{
202 		qCDebug(appupdate) << "Release notes downloaded successfully";
203 		mAppUpdateData.setNotes(QString::fromUtf8(pData));
204 		Q_EMIT fireAppcastCheckFinished(true, GlobalStatus::Code::No_Error);
205 		clearDownloaderConnection();
206 	}
207 	else if (pUpdateUrl == mAppUpdateData.getChecksumUrl())
208 	{
209 		qCDebug(appupdate) << "Checksum file downloaded successfully:" << pUpdateUrl.fileName();
210 		save(pData, pUpdateUrl.fileName());
211 
212 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
213 		const auto split = Qt::SkipEmptyParts;
214 #else
215 		const auto split = QString::SkipEmptyParts;
216 #endif
217 
218 		const auto extensionSplit = pUpdateUrl.fileName().split(QLatin1Char('.'), split);
219 		const auto extension = extensionSplit.isEmpty() ? QByteArray() : extensionSplit.last().toLatin1();
220 		mAppUpdateData.setChecksum(pData, getHashAlgo(extension));
221 
222 		const auto package = mDownloadPath + mAppUpdateData.getUrl().fileName();
223 		if (QFile::exists(package))
224 		{
225 			qCDebug(appupdate) << "Package already exists:" << package;
226 			mAppUpdateData.setUpdatePackagePath(package);
227 			if (mAppUpdateData.isChecksumValid())
228 			{
229 				clearDownloaderConnection();
230 				qCDebug(appupdate) << "Re-use valid package...";
231 				Q_EMIT fireAppDownloadFinished(GlobalStatus::Code::No_Error);
232 				return;
233 			}
234 			qCDebug(appupdate) << "Checksum of package invalid...";
235 		}
236 
237 		qCDebug(appupdate) << "Download package...";
238 		Env::getSingleton<Downloader>()->download(mAppUpdateData.getUrl());
239 	}
240 	else if (pUpdateUrl == mAppUpdateData.getUrl())
241 	{
242 		clearDownloaderConnection();
243 		const auto filename = mAppUpdateData.getUrl().fileName();
244 		qCDebug(appupdate) << "Package downloaded successfully:" << filename;
245 		const auto file = save(pData, filename);
246 
247 		auto status = GlobalStatus::Code::No_Error;
248 		if (file.isNull())
249 		{
250 			status = GlobalStatus::Code::Downloader_Cannot_Save_File;
251 		}
252 		else
253 		{
254 			mAppUpdateData.setUpdatePackagePath(file);
255 			if (!mAppUpdateData.isChecksumValid())
256 			{
257 				status = GlobalStatus::Code::Downloader_Data_Corrupted;
258 			}
259 		}
260 
261 		Q_EMIT fireAppDownloadFinished(status);
262 	}
263 }
264 
265 
onDownloadFailed(const QUrl & pUpdateUrl,GlobalStatus::Code pErrorCode)266 void AppUpdater::onDownloadFailed(const QUrl& pUpdateUrl, GlobalStatus::Code pErrorCode)
267 {
268 	if (pUpdateUrl == mAppUpdateJsonUrl)
269 	{
270 		qCDebug(appupdate) << "App Update JSON failed:" << GlobalStatus(pErrorCode).toErrorDescription();
271 		Q_EMIT fireAppcastCheckFinished(false, pErrorCode);
272 	}
273 	else if (pUpdateUrl == mAppUpdateData.getNotesUrl())
274 	{
275 		qCDebug(appupdate) << "Release notes download failed:" << GlobalStatus(pErrorCode).toErrorDescription();
276 		Q_EMIT fireAppcastCheckFinished(true, pErrorCode);
277 	}
278 	else if (pUpdateUrl == mAppUpdateData.getChecksumUrl() || pUpdateUrl == mAppUpdateData.getUrl())
279 	{
280 		qCDebug(appupdate) << "Download failed:" << GlobalStatus(pErrorCode).toErrorDescription() << ',' << pUpdateUrl;
281 		Q_EMIT fireAppDownloadFinished(pErrorCode);
282 	}
283 	else
284 	{
285 		// do not clear connection to Downloader
286 		return;
287 	}
288 
289 	clearDownloaderConnection();
290 }
291 
292 
onDownloadUnnecessary(const QUrl & pUpdateUrl)293 void AppUpdater::onDownloadUnnecessary(const QUrl& pUpdateUrl)
294 {
295 	if (pUpdateUrl == mAppUpdateJsonUrl || pUpdateUrl == mAppUpdateData.getNotesUrl() || pUpdateUrl == mAppUpdateData.getUrl() || pUpdateUrl == mAppUpdateData.getChecksumUrl())
296 	{
297 		Q_ASSERT(false);
298 		qCCritical(appupdate) << "Got a DownloadUnnecessary from Downloader, but App Updates always have to be fresh, this should not be happening";
299 		Q_EMIT fireAppcastCheckFinished(false, GlobalStatus::Code::Network_Other_Error);
300 		clearDownloaderConnection();
301 	}
302 }
303 
304 
onDownloadProgress(const QUrl & pUpdateUrl,qint64 pBytesReceived,qint64 pBytesTotal)305 void AppUpdater::onDownloadProgress(const QUrl& pUpdateUrl, qint64 pBytesReceived, qint64 pBytesTotal)
306 {
307 	if (pUpdateUrl == mAppUpdateData.getUrl())
308 	{
309 		const qint64 total = pBytesTotal == -1 ? mAppUpdateData.getSize() : pBytesTotal;
310 		Q_EMIT fireAppDownloadProgress(pBytesReceived, total);
311 	}
312 }
313 
314 
clearDownloaderConnection()315 void AppUpdater::clearDownloaderConnection()
316 {
317 	Env::getSingleton<Downloader>()->disconnect(this);
318 	mDownloadInProgress = false;
319 }
320