1 /***************************************************************************************************
2 **
3 ** Copyright (c) 2012 Linas Valiukas and others.
4 **
5 ** Permission is hereby granted, free of charge, to any person obtaining a copy of this
6 ** software and associated documentation files (the "Software"), to deal in the Software
7 ** without restriction, including without limitation the rights to use, copy, modify,
8 ** merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
9 ** permit persons to whom the Software is furnished to do so, subject to the following conditions:
10 **
11 ** The above copyright notice and this permission notice shall be included in all copies or
12 ** substantial portions of the Software.
13 **
14 ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
15 ** NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 ** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17 ** DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 ** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 **
20 ******************************************************************************************************/
21
22 #include "fvupdater.h"
23
24 #include <qsystemdetection.h>
25 #include <qxmlstream.h>
26 #include <QApplication>
27 #include <QByteArray>
28 #include <QDate>
29 #include <QDesktopServices>
30 #include <QLatin1String>
31 #include <QMessageBox>
32 #include <QMessageLogger>
33 #include <QMutex>
34 #include <QNetworkReply>
35 #include <QNetworkRequest>
36 #include <QStaticStringData>
37 #include <QStringData>
38 #include <QStringDataPtr>
39 #include <QStringList>
40 #include <QStringRef>
41 #include <QVariant>
42 #include <QXmlStreamAttributes>
43 #include <QtDebug>
44 #include <QSslConfiguration>
45 #include <QDir>
46 #include <QGlobalStatic>
47
48 #include "../ifc/exception/vexception.h"
49 #include "../ifc/xml/vabstractconverter.h"
50 #include "../vmisc/projectversion.h"
51 #include "../vmisc/vabstractvalapplication.h"
52 #include "../vmisc/vcommonsettings.h"
53 #include "fvavailableupdate.h"
54 #include "fvupdatewindow.h"
55
56 namespace
57 {
58 Q_GLOBAL_STATIC_WITH_ARGS(const QString, defaultFeedURL,
59 (QLatin1String("https://valentinaproject.bitbucket.io/Appcast.xml")))
60 Q_GLOBAL_STATIC_WITH_ARGS(const QString, testFeedURL,
61 (QLatin1String("https://valentinaproject.bitbucket.io/Appcast_testing.xml")))
62 }
63
64 QPointer<FvUpdater> FvUpdater::m_Instance;
65
66 //---------------------------------------------------------------------------------------------------------------------
sharedUpdater()67 FvUpdater* FvUpdater::sharedUpdater()
68 {
69 static QMutex mutex;
70 if (m_Instance.isNull())
71 {
72 mutex.lock();
73 m_Instance = new FvUpdater;
74 mutex.unlock();
75 }
76
77 return m_Instance.data();
78 }
79
80 //---------------------------------------------------------------------------------------------------------------------
drop()81 void FvUpdater::drop()
82 {
83 static QMutex mutex;
84 mutex.lock();
85 delete m_Instance;
86 mutex.unlock();
87 }
88
89 //---------------------------------------------------------------------------------------------------------------------
CurrentFeedURL()90 QString FvUpdater::CurrentFeedURL()
91 {
92 return FvUpdater::IsTestBuild() ? *testFeedURL : *defaultFeedURL;
93 }
94
95 //---------------------------------------------------------------------------------------------------------------------
IsTestBuild()96 bool FvUpdater::IsTestBuild()
97 {
98 return (MAJOR_VERSION * 1000 + MINOR_VERSION) % 2 != 0;
99 }
100
101 //---------------------------------------------------------------------------------------------------------------------
FvUpdater()102 FvUpdater::FvUpdater()
103 : QObject(nullptr),
104 m_updaterWindow(nullptr),
105 m_proposedUpdate(nullptr),
106 m_silentAsMuchAsItCouldGet(true),
107 m_feedURL(),
108 m_qnam(),
109 m_reply(nullptr),
110 m_httpRequestAborted(false),
111 m_dropOnFinnish(true),
112 m_xml()
113 {
114 // noop
115 }
116
117 //---------------------------------------------------------------------------------------------------------------------
~FvUpdater()118 FvUpdater::~FvUpdater()
119 {
120 hideUpdaterWindow();
121 delete m_reply;
122 }
123
124 //---------------------------------------------------------------------------------------------------------------------
showUpdaterWindowUpdatedWithCurrentUpdateProposal()125 void FvUpdater::showUpdaterWindowUpdatedWithCurrentUpdateProposal()
126 {
127 // Destroy window if already exists
128 hideUpdaterWindow();
129
130 // Create a new window
131 m_updaterWindow = new FvUpdateWindow(VAbstractValApplication::VApp()->getMainWindow());
132 m_updaterWindow->UpdateWindowWithCurrentProposedUpdate();
133 m_updaterWindow->exec();
134 }
135
136 //---------------------------------------------------------------------------------------------------------------------
hideUpdaterWindow()137 void FvUpdater::hideUpdaterWindow()
138 {
139 if (m_updaterWindow)
140 {
141 m_updaterWindow->close();
142 }
143 }
144
145 //---------------------------------------------------------------------------------------------------------------------
SetFeedURL(const QUrl & feedURL)146 void FvUpdater::SetFeedURL(const QUrl &feedURL)
147 {
148 m_feedURL = feedURL;
149 }
150
151 //---------------------------------------------------------------------------------------------------------------------
SetFeedURL(const QString & feedURL)152 void FvUpdater::SetFeedURL(const QString &feedURL)
153 {
154 SetFeedURL(QUrl(feedURL));
155 }
156
157 //---------------------------------------------------------------------------------------------------------------------
GetFeedURL() const158 QString FvUpdater::GetFeedURL() const
159 {
160 return m_feedURL.toString();
161 }
162
163 //---------------------------------------------------------------------------------------------------------------------
IsDropOnFinnish() const164 bool FvUpdater::IsDropOnFinnish() const
165 {
166 return m_dropOnFinnish;
167 }
168
169 //---------------------------------------------------------------------------------------------------------------------
SetDropOnFinnish(bool value)170 void FvUpdater::SetDropOnFinnish(bool value)
171 {
172 m_dropOnFinnish = value;
173 }
174
175 //---------------------------------------------------------------------------------------------------------------------
GetProposedUpdate()176 QPointer<FvAvailableUpdate> FvUpdater::GetProposedUpdate()
177 {
178 return m_proposedUpdate;
179 }
180
181 //---------------------------------------------------------------------------------------------------------------------
InstallUpdate()182 void FvUpdater::InstallUpdate()
183 {
184 qDebug() << "Install update";
185
186 UpdateInstallationConfirmed();
187 }
188
189 //---------------------------------------------------------------------------------------------------------------------
SkipUpdate()190 void FvUpdater::SkipUpdate()
191 {
192 qDebug() << "Skip update";
193
194 QPointer<FvAvailableUpdate> proposedUpdate = GetProposedUpdate();
195 if (proposedUpdate.isNull())
196 {
197 qWarning() << "Proposed update is NULL (shouldn't be at this point)";
198 return;
199 }
200
201 // Start ignoring this particular version
202 IgnoreVersion(proposedUpdate->GetEnclosureVersion());
203
204 hideUpdaterWindow();
205 }
206
207 //---------------------------------------------------------------------------------------------------------------------
RemindMeLater()208 void FvUpdater::RemindMeLater()
209 {
210 qDebug() << "Remind me later";
211
212 VAbstractApplication::VApp()->Settings()->SetDateOfLastRemind(QDate::currentDate());
213
214 hideUpdaterWindow();
215 }
216
217 //---------------------------------------------------------------------------------------------------------------------
UpdateInstallationConfirmed()218 void FvUpdater::UpdateInstallationConfirmed()
219 {
220 qDebug() << "Confirm update installation";
221
222 QPointer<FvAvailableUpdate> proposedUpdate = GetProposedUpdate();
223 if (proposedUpdate.isNull())
224 {
225 qWarning() << "Proposed update is NULL (shouldn't be at this point)";
226 return;
227 }
228
229 // Open a link
230 if (not QDesktopServices::openUrl(proposedUpdate->GetEnclosureUrl()))
231 {
232 showErrorDialog(tr("Cannot open your default browser."), true);
233 return;
234 }
235
236 hideUpdaterWindow();
237 }
238
239 //---------------------------------------------------------------------------------------------------------------------
CheckForUpdates(bool silentAsMuchAsItCouldGet)240 bool FvUpdater::CheckForUpdates(bool silentAsMuchAsItCouldGet)
241 {
242 if (m_feedURL.isEmpty())
243 {
244 qCritical() << "Please set feed URL via setFeedURL() before calling CheckForUpdates().";
245 return false;
246 }
247
248 m_silentAsMuchAsItCouldGet = silentAsMuchAsItCouldGet;
249
250 // Check if application's organization name and domain are set, fail otherwise
251 // (nowhere to store QSettings to)
252 if (QCoreApplication::organizationName().isEmpty())
253 {
254 qCritical() << "QApplication::organizationName is not set. Please do that.";
255 return false;
256 }
257 if (QCoreApplication::organizationDomain().isEmpty())
258 {
259 qCritical() << "QApplication::organizationDomain is not set. Please do that.";
260 return false;
261 }
262
263 // Set application name / version is not set yet
264 if (QCoreApplication::applicationName().isEmpty())
265 {
266 qCritical() << "QApplication::applicationName is not set. Please do that.";
267 return false;
268 }
269
270 if (QCoreApplication::applicationVersion().isEmpty())
271 {
272 qCritical() << "QApplication::applicationVersion is not set. Please do that.";
273 return false;
274 }
275
276 cancelDownloadFeed();
277 m_httpRequestAborted = false;
278 startDownloadFeed(m_feedURL);
279
280 return true;
281 }
282
283 //---------------------------------------------------------------------------------------------------------------------
CheckForUpdatesSilent()284 bool FvUpdater::CheckForUpdatesSilent()
285 {
286 if (VAbstractApplication::VApp()->Settings()->GetDateOfLastRemind().daysTo(QDate::currentDate()) >= 1)
287 {
288 const bool success = CheckForUpdates(true);
289 if (m_dropOnFinnish && not success)
290 {
291 drop();
292 }
293 return success;
294 }
295 else
296 {
297 if (m_dropOnFinnish)
298 {
299 drop();
300 }
301 return true;
302 }
303 }
304
305 //---------------------------------------------------------------------------------------------------------------------
CheckForUpdatesNotSilent()306 bool FvUpdater::CheckForUpdatesNotSilent()
307 {
308 const bool success = CheckForUpdates(false);
309 if (m_dropOnFinnish && not success)
310 {
311 drop();
312 }
313 return success;
314 }
315
316 //---------------------------------------------------------------------------------------------------------------------
startDownloadFeed(const QUrl & url)317 void FvUpdater::startDownloadFeed(const QUrl &url)
318 {
319 m_xml.clear();
320
321 QNetworkRequest request;
322 request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/xml"));
323 request.setHeader(QNetworkRequest::UserAgentHeader, QCoreApplication::applicationName());
324 request.setUrl(url);
325 #ifndef QT_NO_SSL
326 request.setSslConfiguration(QSslConfiguration::defaultConfiguration());
327 #endif
328
329 m_reply = m_qnam.get(request);
330
331 connect(m_reply.data(), &QNetworkReply::readyRead, this, [this]()
332 {
333 // this slot gets called every time the QNetworkReply has new data.
334 // We read all of its new data and write it into the file.
335 // That way we use less RAM than when reading it at the finished()
336 // signal of the QNetworkReply
337 m_xml.addData(m_reply->readAll());
338 });
339 connect(m_reply.data(), &QNetworkReply::downloadProgress, this, [this](qint64 bytesRead, qint64 totalBytes)
340 {
341 Q_UNUSED(bytesRead)
342 Q_UNUSED(totalBytes)
343
344 if (m_httpRequestAborted)
345 {
346 return;
347 }
348 });
349 connect(m_reply.data(), &QNetworkReply::finished, this, &FvUpdater::httpFeedDownloadFinished);
350 }
351
352 //---------------------------------------------------------------------------------------------------------------------
cancelDownloadFeed()353 void FvUpdater::cancelDownloadFeed()
354 {
355 if (m_reply)
356 {
357 m_httpRequestAborted = true;
358 m_reply->abort();
359 }
360 }
361
362 //---------------------------------------------------------------------------------------------------------------------
httpFeedDownloadFinished()363 void FvUpdater::httpFeedDownloadFinished()
364 {
365 if (m_httpRequestAborted)
366 {
367 m_reply->deleteLater();
368 return;
369 }
370
371 const QVariant redirectionTarget = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
372 if (m_reply->error() != QNetworkReply::NoError)
373 {
374 // Error.
375 showErrorDialog(tr("Feed download failed: %1.").arg(m_reply->errorString()), false);
376 }
377 else if (not redirectionTarget.isNull())
378 {
379 const QUrl newUrl = m_feedURL.resolved(redirectionTarget.toUrl());
380
381 m_feedURL = newUrl;
382 m_reply->deleteLater();
383
384 startDownloadFeed(m_feedURL);
385 return;
386 }
387 else
388 {
389 // Done.
390 xmlParseFeed();
391 }
392
393 m_reply->deleteLater();
394
395 if (m_dropOnFinnish)
396 {
397 drop();
398 }
399 }
400
401 //---------------------------------------------------------------------------------------------------------------------
xmlParseFeed()402 bool FvUpdater::xmlParseFeed()
403 {
404 QString xmlEnclosureUrl, xmlEnclosureVersion, xmlEnclosurePlatform;
405
406 // Parse
407 while (not m_xml.atEnd())
408 {
409 m_xml.readNext();
410
411 if (m_xml.isStartElement())
412 {
413 if (m_xml.name() == QLatin1String("item"))
414 {
415 xmlEnclosureUrl.clear();
416 xmlEnclosureVersion.clear();
417 xmlEnclosurePlatform.clear();
418 }
419 else if (m_xml.name() == QLatin1String("enclosure"))
420 {
421 const QXmlStreamAttributes attribs = m_xml.attributes();
422 const QString fervorPlatform = QStringLiteral("fervor:platform");
423
424 if (attribs.hasAttribute(fervorPlatform))
425 {
426 if (CurrentlyRunningOnPlatform(attribs.value(fervorPlatform).toString().trimmed()))
427 {
428 xmlEnclosurePlatform = attribs.value(fervorPlatform).toString().trimmed();
429
430 const QString attributeUrl = QStringLiteral("url");
431 if (attribs.hasAttribute(attributeUrl))
432 {
433 xmlEnclosureUrl = attribs.value(attributeUrl).toString().trimmed();
434 }
435 else
436 {
437 xmlEnclosureUrl.clear();
438 }
439
440 const QString fervorVersion = QStringLiteral("fervor:version");
441 if (attribs.hasAttribute(fervorVersion))
442 {
443 const QString candidateVersion = attribs.value(fervorVersion).toString().trimmed();
444 if (not candidateVersion.isEmpty())
445 {
446 xmlEnclosureVersion = candidateVersion;
447 }
448 }
449 }
450 }
451 }
452 }
453 else if (m_xml.isEndElement())
454 {
455 if (m_xml.name() == QLatin1String("item"))
456 {
457 // That's it - we have analyzed a single <item> and we'll stop
458 // here (because the topmost is the most recent one, and thus
459 // the newest version.
460
461 return searchDownloadedFeedForUpdates(xmlEnclosureUrl,
462 xmlEnclosureVersion,
463 xmlEnclosurePlatform);
464 }
465 }
466
467 if (m_xml.error() && m_xml.error() != QXmlStreamReader::PrematureEndOfDocumentError)
468 {
469 showErrorDialog(tr("Feed parsing failed: %1 %2.").arg(QString::number(m_xml.lineNumber()),
470 m_xml.errorString()), false);
471 return false;
472
473 }
474 }
475
476 // No updates were found if we're at this point
477 // (not a single <item> element found)
478 showInformationDialog(tr("No updates were found."), false);
479
480 return false;
481 }
482
483 //---------------------------------------------------------------------------------------------------------------------
searchDownloadedFeedForUpdates(const QString & xmlEnclosureUrl,const QString & xmlEnclosureVersion,const QString & xmlEnclosurePlatform)484 bool FvUpdater::searchDownloadedFeedForUpdates(const QString &xmlEnclosureUrl,
485 const QString &xmlEnclosureVersion,
486 const QString &xmlEnclosurePlatform)
487 {
488 qDebug() << "Enclosure URL:" << xmlEnclosureUrl;
489 qDebug() << "Enclosure version:" << xmlEnclosureVersion;
490 qDebug() << "Enclosure platform:" << xmlEnclosurePlatform;
491
492 // Validate
493 if (xmlEnclosureUrl.isEmpty() || xmlEnclosureVersion.isEmpty() || xmlEnclosurePlatform.isEmpty())
494 {
495 showErrorDialog(tr("Feed error: invalid \"enclosure\" with the download link"), false);
496 return false;
497 }
498
499 // Relevant version?
500 if (VersionIsIgnored(xmlEnclosureVersion))
501 {
502 qDebug() << "Version '" << xmlEnclosureVersion << "' is ignored, too old or something like that.";
503
504 showInformationDialog(tr("No updates were found."), false);
505
506 return true; // Things have succeeded when you think of it.
507 }
508
509 //
510 // Success! At this point, we have found an update that can be proposed
511 // to the user.
512 //
513
514 delete m_proposedUpdate;
515 m_proposedUpdate = new FvAvailableUpdate(this);
516 m_proposedUpdate->SetEnclosureUrl(xmlEnclosureUrl);
517 m_proposedUpdate->SetEnclosureVersion(xmlEnclosureVersion);
518 m_proposedUpdate->SetEnclosurePlatform(xmlEnclosurePlatform);
519
520 // Show "look, there's an update" window
521 showUpdaterWindowUpdatedWithCurrentUpdateProposal();
522
523 return true;
524 }
525
526 //---------------------------------------------------------------------------------------------------------------------
VersionIsIgnored(const QString & version)527 bool FvUpdater::VersionIsIgnored(const QString &version)
528 {
529 // We assume that variable 'version' contains either:
530 // 1) The current version of the application (ignore)
531 // 2) The version that was skipped before and thus stored in QSettings (ignore)
532 // 3) A newer version (don't ignore)
533 // 'version' is not likely to contain an older version in any case.
534
535 int decVersion = 0x0;
536 try
537 {
538 decVersion = VAbstractConverter::GetFormatVersion(version);
539 }
540 catch (const VException &e)
541 {
542 Q_UNUSED(e)
543 return true; // Ignore invalid version
544 }
545
546 if (decVersion == APP_VERSION)
547 {
548 return true;
549 }
550
551 const int lastSkippedVersion = VAbstractApplication::VApp()->Settings()->GetLatestSkippedVersion();
552 if (lastSkippedVersion != 0x0)
553 {
554 if (decVersion == lastSkippedVersion)
555 {
556 // Implicitly skipped version - skip
557 return true;
558 }
559 }
560
561 if (decVersion > APP_VERSION)
562 {
563 // Newer version - do not skip
564 return false;
565 }
566
567 // Fallback - skip
568 return true;
569 }
570
571 //---------------------------------------------------------------------------------------------------------------------
IgnoreVersion(const QString & version)572 void FvUpdater::IgnoreVersion(const QString &version)
573 {
574 int decVersion = 0x0;
575 try
576 {
577 decVersion = VAbstractConverter::GetFormatVersion(version);
578 }
579 catch (const VException &e)
580 {
581 Q_UNUSED(e)
582 return ; // Ignore invalid version
583 }
584
585 if (decVersion == APP_VERSION)
586 {
587 // Don't ignore the current version
588 return;
589 }
590
591 VAbstractApplication::VApp()->Settings()->SetLatestSkippedVersion(decVersion);
592 }
593
594 //---------------------------------------------------------------------------------------------------------------------
CurrentlyRunningOnPlatform(const QString & platform)595 bool FvUpdater::CurrentlyRunningOnPlatform(const QString &platform)
596 {
597 const QStringList platforms = QStringList() << "Q_OS_LINUX"
598 << "Q_OS_MAC"
599 << "Q_OS_WIN32";
600
601 switch (platforms.indexOf(platform.toUpper().trimmed()))
602 {
603 case 0: // Q_OS_LINUX
604 #ifdef Q_OS_LINUX // Defined on Linux.
605 return true;
606 #else
607 return false;
608 #endif
609 case 1: // Q_OS_MAC
610 #ifdef Q_OS_MAC // Defined on MAC OS (synonym for Darwin).
611 return true;
612 #else
613 return false;
614 #endif
615 case 2: // Q_OS_WIN32
616 #ifdef Q_OS_WIN32 // Defined on all supported versions of Windows.
617 return true;
618 #else
619 return false;
620 #endif
621 default:
622 break;
623 }
624
625 // Fallback
626 return false;
627 }
628
629 //---------------------------------------------------------------------------------------------------------------------
showErrorDialog(const QString & message,bool showEvenInSilentMode)630 void FvUpdater::showErrorDialog(const QString &message, bool showEvenInSilentMode)
631 {
632 if (m_silentAsMuchAsItCouldGet)
633 {
634 if (not showEvenInSilentMode)
635 {
636 // Don't show errors in the silent mode
637 return;
638 }
639 }
640
641 QMessageBox dlFailedMsgBox;
642 dlFailedMsgBox.setIcon(QMessageBox::Critical);
643 dlFailedMsgBox.setText(message);
644 dlFailedMsgBox.exec();
645 }
646
647 //---------------------------------------------------------------------------------------------------------------------
showInformationDialog(const QString & message,bool showEvenInSilentMode)648 void FvUpdater::showInformationDialog(const QString &message, bool showEvenInSilentMode)
649 {
650 if (m_silentAsMuchAsItCouldGet)
651 {
652 if (not showEvenInSilentMode)
653 {
654 // Don't show information dialogs in the silent mode
655 return;
656 }
657 }
658
659 QMessageBox dlInformationMsgBox;
660 dlInformationMsgBox.setIcon(QMessageBox::Information);
661 dlInformationMsgBox.setText(message);
662 dlInformationMsgBox.exec();
663 }
664