1 /*******************************************************************************************************
2  nomacs is a fast and small image viewer with the capability of synchronizing multiple instances
3 
4  Copyright (C) 2011-2016 Markus Diem <markus@nomacs.org>
5  Copyright (C) 2011-2016 Stefan Fiel <stefan@nomacs.org>
6  Copyright (C) 2011-2016 Florian Kleber <florian@nomacs.org>
7 
8  This file is part of nomacs.
9 
10  nomacs is free software: you can redistribute it and/or modify
11  it under the terms of the GNU General Public License as published by
12  the Free Software Foundation, either version 3 of the License, or
13  (at your option) any later version.
14 
15  nomacs is distributed in the hope that it will be useful,
16  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  GNU General Public License for more details.
19 
20  You should have received a copy of the GNU General Public License
21  along with this program.  If not, see <http://www.gnu.org/licenses/>.
22 
23  related links:
24  [1] https://nomacs.org/
25  [2] https://github.com/nomacs/
26  [3] http://download.nomacs.org
27  *******************************************************************************************************/
28 
29 #include "DkUpdater.h"
30 
31 #include "DkSettings.h"
32 #include "DkTimer.h"
33 #include "DkUtils.h"
34 
35 #pragma warning(push, 0)	// no warnings from includes
36 #include <QVector>
37 #include <QDebug>
38 #include <QXmlStreamReader>
39 #include <QNetworkProxyQuery>
40 #include <QFile>
41 #include <QCoreApplication>
42 #include <QMessageBox>
43 #include <QApplication>
44 #include <QPushButton>
45 #include <QFileInfo>
46 #include <QNetworkCookieJar>
47 #include <QDir>
48 #include <QProcess>
49 #include <QStandardPaths>
50 
51 #ifdef Q_OS_WIN
52 #include <windows.h>
53 #endif
54 
55 #pragma warning(pop)
56 
57 namespace nmc {
58 
59 // DkPackage --------------------------------------------------------------------
DkPackage(const QString & name,const QString & version)60 DkPackage::DkPackage(const QString& name, const QString& version) {
61 	mName = name;
62 	mVersion = version;
63 }
64 
isEmpty() const65 bool DkPackage::isEmpty() const {
66 	return mName.isEmpty();
67 }
68 
operator ==(const DkPackage & o) const69 bool DkPackage::operator==(const DkPackage& o) const {
70 
71 	return mName == o.name();
72 }
73 
version() const74 QString DkPackage::version() const {
75 	return mVersion;
76 }
77 
name() const78 QString DkPackage::name() const {
79 	return mName;
80 }
81 
82 // DkXmlUpdateChecker --------------------------------------------------------------------
DkXmlUpdateChecker()83 DkXmlUpdateChecker::DkXmlUpdateChecker() {
84 }
85 
updatesAvailable(QXmlStreamReader & localXml,QXmlStreamReader & remoteXml) const86 QVector<DkPackage> DkXmlUpdateChecker::updatesAvailable(QXmlStreamReader& localXml, QXmlStreamReader& remoteXml) const {
87 
88 	QVector<DkPackage> localPackages = parse(localXml);
89 	QVector<DkPackage> remotePackages = parse(remoteXml);
90 	QVector<DkPackage> updatePackages;
91 
92 	for (const DkPackage& p : localPackages) {
93 
94 		int idx = remotePackages.indexOf(p);
95 
96 		if (idx != -1) {
97 			bool isEqual = remotePackages[idx].version() == p.version();
98 			qDebug() << "checking" << p.name() << "v" << p.version();
99 
100 			if (!isEqual)	// we assume that the remote is _always_ newer than the local version
101 				updatePackages.append(remotePackages[idx]);
102 			else
103 				qDebug() << "up-to-date";
104 		}
105 		else
106 			qDebug() << "I could not find" << p.name() << "in the repository";
107 	}
108 
109 	if (localPackages.empty() || remotePackages.empty())
110 		qDebug() << "WARNING: I could not find any packages. local (" << localPackages.size() << ") remote (" << remotePackages.size() << ")";
111 
112 	return updatePackages;
113 }
114 
parse(QXmlStreamReader & reader) const115 QVector<DkPackage> DkXmlUpdateChecker::parse(QXmlStreamReader& reader) const {
116 
117 	QVector<DkPackage> packages;
118 	QString pName;
119 
120 	while (!reader.atEnd()) {
121 
122 		// e.g. <Name>nomacs</Name>
123 		if (reader.tokenType() == QXmlStreamReader::StartElement && reader.qualifiedName() == "Name") {
124 			reader.readNext();
125 			pName = reader.text().toString();
126 		}
127 		// e.g. <Version>3.0.0-3</Version>
128 		else if (reader.tokenType() == QXmlStreamReader::StartElement && reader.qualifiedName() == "Version") {
129 			reader.readNext();
130 
131 			if (!pName.isEmpty()) {
132 				packages.append(DkPackage(pName, reader.text().toString()));
133 				pName = "";	// reset
134 			}
135 			else {
136 				qWarning() << "version: " << reader.text().toString() << "without a valid package name detected";
137 			}
138 		}
139 
140 		reader.readNext();
141 	}
142 
143 	return packages;
144 }
145 
146 // DkUpdater  --------------------------------------------------------------------
DkUpdater(QObject * parent)147 DkUpdater::DkUpdater(QObject* parent) : QObject(parent) {
148 
149 	silent = true;
150 	mCookie = new QNetworkCookieJar(this);
151 	mAccessManagerSetup.setCookieJar(mCookie);
152 	connect(&mAccessManagerSetup, SIGNAL(finished(QNetworkReply*)), this, SLOT(downloadFinishedSlot(QNetworkReply*)));
153 	mUpdateAborted = false;
154 }
155 
checkForUpdates()156 void DkUpdater::checkForUpdates() {
157 
158 	if (DkSettingsManager::param().sync().disableUpdateInteraction) {
159 		QMessageBox::critical(
160 			DkUtils::getMainWindow(),
161 			tr("Updates Disabled"),
162 			tr("nomacs updates are disabled.\nPlease contact your system administrator for further information."),
163 			QMessageBox::Ok);
164 		return;
165 	}
166 
167 	DkSettingsManager::param().sync().lastUpdateCheck = QDate::currentDate();
168 	DkSettingsManager::param().save();
169 
170 #ifdef Q_OS_WIN
171 	QUrl url ("http://nomacs.org/version/version_win_stable");
172 #elif defined Q_OS_LINUX
173 	QUrl url ("http://nomacs.org/version/version_linux");
174 #elif defined Q_OS_MAC
175 	QUrl url ("http://nomacs.org/version/version_mac_stable");
176 #else
177 	QUrl url ("http://nomacs.org/version/version");
178 #endif
179 
180 	// the proxy settings take > 2 sec on Win7
181 	// that is why proxy settings are only set
182 	// for manual updates
183 	if (!silent) {
184 		DkTimer dt;
185 		QNetworkProxyQuery npq(QUrl("http://www.google.com"));
186 		QList<QNetworkProxy> listOfProxies = QNetworkProxyFactory::systemProxyForQuery(npq);
187 
188 		if (!listOfProxies.empty() && listOfProxies[0].hostName() != "") {
189 			mAccessManagerSetup.setProxy(listOfProxies[0]);
190 			mAccessManagerVersion.setProxy(listOfProxies[0]);
191 		}
192 		qDebug() << "checking for proxy takes: " << dt;
193 	}
194 
195 	qDebug() << "checking for updates";
196 	connect(&mAccessManagerVersion, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*)));
197 	mReply = mAccessManagerVersion.get(QNetworkRequest(url));
198 	connect(mReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(replyError(QNetworkReply::NetworkError)));
199 }
200 
replyFinished(QNetworkReply * reply)201 void DkUpdater::replyFinished(QNetworkReply* reply) {
202 
203 	if (reply->error())
204 		return;
205 
206 	QString replyData = reply->readAll();
207 
208 	QStringList sl = replyData.split('\n', QString::SkipEmptyParts);
209 
210 	QString version, x64, x86, url, mac, XPx86;
211 	for(int i = 0; i < sl.length();i++) {
212 		QStringList values = sl[i].split(" ");
213 		if (values[0] == "version")
214 			version = values[1];
215 		else if (values[0] == "x64")
216 			x64 = values[1];
217 		else if (values[0] == "XPx86")
218 			XPx86 = values[1];
219 		else if (values[0] == "x86")
220 			x86 = values[1];
221 		else if (values[0] == "mac")
222 			mac = values[1];
223 	}
224 
225 
226 #if _MSC_VER == 1600
227 	url = XPx86;	// for WinXP packages
228 #elif defined _WIN64
229 	url = x64;
230 #elif _WIN32
231 	url = x86;
232 #elif defined Q_OS_MAC
233 	url = mac;
234 #endif
235 
236 	qDebug() << "version:" << version;
237 	qDebug() << "x64:" << x64;
238 	qDebug() << "x86:" << x86;
239 	qDebug() << "mac:" << mac;
240 
241 	if ((!version.isEmpty() && !x64.isEmpty()) || !x86.isEmpty()) {
242 		QStringList cVersion = QApplication::applicationVersion().split('.');
243 		QStringList nVersion = version.split('.');
244 
245 		if (cVersion.size() < 3 || nVersion.size() < 3) {
246 			qDebug() << "sorry, I could not parse the version number...";
247 
248 			if (!silent)
249 				emit showUpdaterMessage(tr("sorry, I could not check for newer versions"), tr("Updates"));
250 
251 			return;
252 		}
253 
254 		if (nVersion[0].toInt() > cVersion[0].toInt()  ||	// major release
255 		   (nVersion[0].toInt() == cVersion[0].toInt() &&	// major release
256 			nVersion[1].toInt() > cVersion[1].toInt()) ||	// minor release
257 		   (nVersion[0].toInt() == cVersion[0].toInt() &&	// major release
258 			nVersion[1].toInt() == cVersion[1].toInt() &&	// minor release
259 			nVersion[2].toInt() >  cVersion[2].toInt())) {	// minor-minor release
260 
261 			QString msg = tr("A new version") + " (" + sl[0] + ") " + tr("is available");
262 			msg = msg + "<br>" + tr("Do you want to download and install it now?");
263 			msg = msg + "<br>" + tr("For more information see ") + " <a href=\"https://nomacs.org\">https://nomacs.org</a>";
264 			mNomacsSetupUrl = url;
265 			mSetupVersion = version;
266 			qDebug() << "nomacs setup url:" << mNomacsSetupUrl;
267 
268 			if (!url.isEmpty())
269 				emit displayUpdateDialog(msg, tr("updates"));
270 		}
271 		else if (!silent)
272 			emit showUpdaterMessage(tr("nomacs is up-to-date"), tr("updates"));
273 	}
274 
275 }
276 
startDownload(QUrl downloadUrl)277 void DkUpdater::startDownload(QUrl downloadUrl) {
278 
279 	if (downloadUrl.isEmpty())
280 		emit showUpdaterMessage(tr("sorry, unable to download the new version"), tr("updates"));
281 
282 	qDebug() << "-----------------------------------------------------";
283 	qDebug() << "starting to download update from " << downloadUrl ;
284 
285 	//updateAborted = false;	// reset - it may have been canceled before
286 	QNetworkRequest req(downloadUrl);
287 	req.setRawHeader("User-Agent", "Auto-Updater");
288 	mReply = mAccessManagerSetup.get(req);
289 	connect(mReply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(updateDownloadProgress(qint64, qint64)));
290 }
291 
downloadFinishedSlot(QNetworkReply * data)292 void DkUpdater::downloadFinishedSlot(QNetworkReply* data) {
293 	QUrl redirect = data->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
294 	if (!redirect.isEmpty() ) {
295 		qDebug() << "redirecting: " << redirect;
296 		startDownload(redirect);
297 		return;
298 	}
299 
300 	if (!mUpdateAborted) {
301 		QString basename = "nomacs-setup";
302 		QString extension = ".msi";
303 		QString absoluteFilePath = QDir::tempPath() + "/" + basename + extension;
304 		if (QFile::exists(absoluteFilePath)) {
305 			qDebug() << "File already exists - searching for new name";
306 			// already exists, don't overwrite
307 			int i = 0;
308 			while (QFile::exists(absoluteFilePath)) {
309 				absoluteFilePath = QDir::tempPath() + "/" + basename + "-" + QString::number(i) + extension;
310 				++i;
311 			}
312 		}
313 
314 		QFile file(absoluteFilePath);
315 		if (!file.open(QIODevice::WriteOnly)) {
316 			qDebug()  << "Could not open " << QFileInfo(file).absoluteFilePath() << "for writing";
317 			return;
318 		}
319 
320 		file.write(data->readAll());
321 		qDebug() << "saved new version: " << " " << QFileInfo(file).absoluteFilePath();
322 
323 		file.close();
324 
325 		DkSettingsManager::param().global().setupVersion = mSetupVersion;
326 		DkSettingsManager::param().global().setupPath = absoluteFilePath;
327 		DkSettingsManager::param().save();
328 
329 		emit downloadFinished(absoluteFilePath);
330 	}
331 	mUpdateAborted = false;
332 	qDebug() << "downloadFinishedSlot complete";
333 }
334 
performUpdate()335 void DkUpdater::performUpdate() {
336 	if(mNomacsSetupUrl.isEmpty())
337 		qDebug() << "unable to perform update because the nomacsSetupUrl is empty";
338 	else
339 		startDownload(mNomacsSetupUrl);
340 }
341 
cancelUpdate()342 void DkUpdater::cancelUpdate()  {
343 	qDebug() << "abort update";
344 	mUpdateAborted = true;
345 	mReply->abort();
346 }
347 
replyError(QNetworkReply::NetworkError)348 void DkUpdater::replyError(QNetworkReply::NetworkError) {
349 	if (!silent)
350 		emit showUpdaterMessage(tr("Unable to connect to server ... please try again later"), tr("updates"));
351 }
352 
353 // DkTranslationUpdater --------------------------------------------------------------------
DkTranslationUpdater(bool silent,QObject * parent)354 DkTranslationUpdater::DkTranslationUpdater(bool silent, QObject* parent) : QObject(parent) {
355 
356 	this->silent = silent;
357 	connect(&mAccessManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*)));
358 
359 	updateAborted = false;
360 	updateAbortedQt = false;
361 }
362 
checkForUpdates()363 void DkTranslationUpdater::checkForUpdates() {
364 
365 	if (DkSettingsManager::param().sync().disableUpdateInteraction) {
366 		QMessageBox::critical(
367 			DkUtils::getMainWindow(),
368 			tr("Updates Disabled"),
369 			tr("nomacs updates are disabled.\nPlease contact your system administrator for further information."),
370 			QMessageBox::Ok);
371 		return;
372 	}
373 
374 	mTotal = -1;
375 	mTotalQt = -1;
376 	mReceived = 0;
377 	mReceivedQt = 0;
378 	updateAborted = false;
379 	updateAbortedQt = false;
380 
381 	// that line takes 2 secs on win7!
382 	QNetworkProxyQuery npq(QUrl("http://www.google.com"));
383 	QList<QNetworkProxy> listOfProxies = QNetworkProxyFactory::systemProxyForQuery(npq);
384 	if (!listOfProxies.empty() && listOfProxies[0].hostName() != "") {
385 		mAccessManager.setProxy(listOfProxies[0]);
386 	}
387 
388 	QUrl url ("http://nomacs.org/translations/" + DkSettingsManager::param().global().language + "/nomacs_" + DkSettingsManager::param().global().language + ".qm");
389 	qInfo() << "checking for new translations at " << url;
390 	mReply = mAccessManager.get(QNetworkRequest(url));
391 	connect(mReply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(updateDownloadProgress(qint64, qint64)));
392 
393 	url=QUrl("http://nomacs.org/translations/qt/qt_" + DkSettingsManager::param().global().language + ".qm");
394 	qDebug() << "checking for new translations at " << url;
395 	mReplyQt = mAccessManager.get(QNetworkRequest(url));
396 	connect(mReplyQt, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(updateDownloadProgressQt(qint64, qint64)));
397 }
398 
replyFinished(QNetworkReply * reply)399 void DkTranslationUpdater::replyFinished(QNetworkReply* reply) {
400 
401 	bool qtTranslation = false;
402 	if (reply->url().toString().contains("qt_"))
403 		qtTranslation = true;
404 
405 	if (updateAbortedQt && updateAborted) {
406 		emit downloadFinished();
407 		return;
408 	}
409 
410 	if (reply->error() == QNetworkReply::OperationCanceledError)
411 		return;
412 
413 	if (reply->error()) {
414 		qDebug() << "network reply error : url: " << reply->url();
415 		if (!qtTranslation && !silent)
416 			emit showUpdaterMessage(tr("Unable to download translation"), tr("update"));
417 		return;
418 	}
419 
420 	QDateTime lastModifiedRemote = reply->header(QNetworkRequest::LastModifiedHeader).toDateTime();
421 
422 	QDir storageLocation = DkUtils::getTranslationPath();
423 	QString translationName = qtTranslation ? "qt_"+ DkSettingsManager::param().global().language + ".qm" : "nomacs_"+ DkSettingsManager::param().global().language + ".qm";
424 
425 	if (isRemoteFileNewer(lastModifiedRemote, translationName)) {
426 		QString basename = qtTranslation ? "qt_" + DkSettingsManager::param().global().language : "nomacs_" + DkSettingsManager::param().global().language;
427 		QString extension = ".qm";
428 
429 		if (!storageLocation.exists()) {
430 			if (!storageLocation.mkpath(storageLocation.absolutePath())) {
431 				qDebug() << "unable to create storage location ... aborting";
432 				if (!qtTranslation && !silent)
433 					emit showUpdaterMessage(tr("Unable to update translation"), tr("update"));
434 				return;
435 			}
436 		}
437 
438 		QString absoluteFilePath = storageLocation.absolutePath() + "/" + basename + extension;
439 		if (QFile::exists(absoluteFilePath)) {
440 			qInfo() << "File already exists - overwriting";
441 		}
442 
443 		QFile file(absoluteFilePath);
444 		if (!file.open(QIODevice::WriteOnly)) {
445 			qWarning()  << "Could not open " << QFileInfo(file).absoluteFilePath() << "for writing";
446 			return;
447 		}
448 
449 		file.write(reply->readAll());
450 		qDebug() << "saved new translation: " << " " << QFileInfo(file).absoluteFilePath();
451 
452 		file.close();
453 
454 		if (!qtTranslation && !silent)
455 			emit showUpdaterMessage(tr("Translation updated"), tr("update"));
456 		qDebug() << "translation updated";
457 	} else {
458 		qDebug() << "no newer translations available";
459 		if (!silent)
460 			emit showUpdaterMessage(tr("No newer translations found"), tr("update"));
461 	}
462 	if (reply->isFinished() && mReplyQt->isFinished()) {
463 		qDebug() << "emitting downloadFinished";
464 		emit downloadFinished();
465 	}
466 
467 }
468 
updateDownloadProgress(qint64 received,qint64 total)469 void DkTranslationUpdater::updateDownloadProgress(qint64 received, qint64 total) {
470 	if (total == -1)  // if file does not exist
471 		return;
472 
473 	QDateTime lastModifiedRemote = mReply->header(QNetworkRequest::LastModifiedHeader).toDateTime();
474 
475 	QString translationName = "nomacs_"+ DkSettingsManager::param().global().language + ".qm";
476 	qDebug() << "isRemoteFileNewer:" << isRemoteFileNewer(lastModifiedRemote, translationName);
477 	if (!isRemoteFileNewer(lastModifiedRemote, translationName)) {
478 		updateAborted = true;
479 		this->mTotal = 0;
480 		this->mReceived = 0;
481 		mReply->abort();
482 		return;
483 	}
484 
485 	this->mReceived = received;
486 	this->mTotal  = total;
487 	qDebug() << "total:" << total;
488 	emit downloadProgress(this->mReceived + this->mReceivedQt, this->mTotal + this->mTotalQt);
489 }
490 
updateDownloadProgressQt(qint64 received,qint64 total)491 void DkTranslationUpdater::updateDownloadProgressQt(qint64 received, qint64 total) {
492 	if (total == -1)  // if file does not exist
493 		return;
494 
495 	QDateTime lastModifiedRemote = mReplyQt->header(QNetworkRequest::LastModifiedHeader).toDateTime();
496 	QString translationName = "qt_"+ DkSettingsManager::param().global().language + ".qm";
497 	qDebug() << "isRemoteFileNewer:" << isRemoteFileNewer(lastModifiedRemote, translationName);
498 	if (!isRemoteFileNewer(lastModifiedRemote, translationName)) {
499 		updateAbortedQt = true;
500 		this->mTotalQt = 0;
501 		this->mReceivedQt = 0;
502 		mReplyQt->abort();
503 		return;
504 	}
505 
506 	this->mReceivedQt = received;
507 	this->mTotalQt = total;
508 	qDebug() << "totalQt:" << mTotalQt;
509 	emit downloadProgress(this->mReceived + this->mReceivedQt, this->mTotal + this->mTotalQt);
510 }
511 
isRemoteFileNewer(QDateTime lastModifiedRemote,const QString & localTranslationName)512 bool DkTranslationUpdater::isRemoteFileNewer(QDateTime lastModifiedRemote, const QString& localTranslationName) {
513 
514 	if (!lastModifiedRemote.isValid())
515 		return false;
516 
517 	QString trPath = DkUtils::getTranslationPath();
518 	QFileInfo trFile(trPath, localTranslationName);
519 
520 	return !trFile.exists() || (QFileInfo(trFile).lastModified() < lastModifiedRemote);
521 }
522 
cancelUpdate()523 void DkTranslationUpdater::cancelUpdate() {
524 	mReply->abort();
525 	mReplyQt->abort();
526 	updateAborted = true;
527 	updateAbortedQt = true;
528 }
529 
530 }