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