/*************************************************************************************************** ** ** Copyright (c) 2012 Linas Valiukas and others. ** ** Permission is hereby granted, free of charge, to any person obtaining a copy of this ** software and associated documentation files (the "Software"), to deal in the Software ** without restriction, including without limitation the rights to use, copy, modify, ** merge, publish, distribute, sublicense, and/or sell copies of the Software, and to ** permit persons to whom the Software is furnished to do so, subject to the following conditions: ** ** The above copyright notice and this permission notice shall be included in all copies or ** substantial portions of the Software. ** ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT ** NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, ** DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ** ******************************************************************************************************/ #include "fvupdater.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../ifc/exception/vexception.h" #include "../ifc/xml/vabstractconverter.h" #include "../vmisc/projectversion.h" #include "../vmisc/vabstractapplication.h" #include "../vmisc/vcommonsettings.h" #include "fvavailableupdate.h" #include "fvupdatewindow.h" namespace { const QString defaultFeedURL = QStringLiteral("https://valentinaproject.bitbucket.io/Appcast.xml"); const QString testFeedURL = QStringLiteral("https://valentinaproject.bitbucket.io/Appcast_testing.xml"); } QPointer FvUpdater::m_Instance; //--------------------------------------------------------------------------------------------------------------------- FvUpdater* FvUpdater::sharedUpdater() { static QMutex mutex; if (m_Instance.isNull()) { mutex.lock(); m_Instance = new FvUpdater; mutex.unlock(); } return m_Instance.data(); } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::drop() { static QMutex mutex; mutex.lock(); delete m_Instance; mutex.unlock(); } //--------------------------------------------------------------------------------------------------------------------- QString FvUpdater::CurrentFeedURL() { return FvUpdater::IsTestBuild() ? testFeedURL : defaultFeedURL; } //--------------------------------------------------------------------------------------------------------------------- int FvUpdater::CurrentVersion() { #ifdef Q_OS_MAC const QString path = QCoreApplication::applicationDirPath() + QLatin1String("/../Resources/VERSION"); #else const QString path = QApplication::applicationDirPath() + QDir::separator() + QLatin1String("VERSION"); #endif QFile file(path); if (file.exists()) { if (not file.open(QIODevice::ReadOnly | QIODevice::Text)) { return APP_VERSION; } QTextStream in(&file); try { return VAbstractConverter::GetVersion(in.read(15)); } catch(const VException &) { return APP_VERSION; } } else { return APP_VERSION; } } //--------------------------------------------------------------------------------------------------------------------- bool FvUpdater::IsTestBuild() { const int version = FvUpdater::CurrentVersion(); return (version != 0x0 && version != APP_VERSION); } //--------------------------------------------------------------------------------------------------------------------- FvUpdater::FvUpdater() : QObject(nullptr), m_updaterWindow(nullptr), m_proposedUpdate(nullptr), m_silentAsMuchAsItCouldGet(true), m_feedURL(), m_qnam(), m_reply(nullptr), m_httpRequestAborted(false), m_dropOnFinnish(true), m_xml() { // noop } //--------------------------------------------------------------------------------------------------------------------- FvUpdater::~FvUpdater() { hideUpdaterWindow(); delete m_reply; } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::showUpdaterWindowUpdatedWithCurrentUpdateProposal() { // Destroy window if already exists hideUpdaterWindow(); // Create a new window m_updaterWindow = new FvUpdateWindow(qApp->getMainWindow()); m_updaterWindow->UpdateWindowWithCurrentProposedUpdate(); m_updaterWindow->exec(); } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::hideUpdaterWindow() { if (m_updaterWindow) { m_updaterWindow->close(); } } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::SetFeedURL(const QUrl &feedURL) { m_feedURL = feedURL; } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::SetFeedURL(const QString &feedURL) { SetFeedURL(QUrl(feedURL)); } //--------------------------------------------------------------------------------------------------------------------- QString FvUpdater::GetFeedURL() const { return m_feedURL.toString(); } //--------------------------------------------------------------------------------------------------------------------- bool FvUpdater::IsDropOnFinnish() const { return m_dropOnFinnish; } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::SetDropOnFinnish(bool value) { m_dropOnFinnish = value; } //--------------------------------------------------------------------------------------------------------------------- QPointer FvUpdater::GetProposedUpdate() { return m_proposedUpdate; } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::InstallUpdate() { qDebug() << "Install update"; UpdateInstallationConfirmed(); } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::SkipUpdate() { qDebug() << "Skip update"; QPointer proposedUpdate = GetProposedUpdate(); if (proposedUpdate.isNull()) { qWarning() << "Proposed update is NULL (shouldn't be at this point)"; return; } // Start ignoring this particular version IgnoreVersion(proposedUpdate->GetEnclosureVersion()); hideUpdaterWindow(); } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::RemindMeLater() { qDebug() << "Remind me later"; qApp->Settings()->SetDateOfLastRemind(QDate::currentDate()); hideUpdaterWindow(); } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::UpdateInstallationConfirmed() { qDebug() << "Confirm update installation"; QPointer proposedUpdate = GetProposedUpdate(); if (proposedUpdate.isNull()) { qWarning() << "Proposed update is NULL (shouldn't be at this point)"; return; } // Open a link if (not QDesktopServices::openUrl(proposedUpdate->GetEnclosureUrl())) { showErrorDialog(tr("Cannot open your default browser."), true); return; } hideUpdaterWindow(); } //--------------------------------------------------------------------------------------------------------------------- bool FvUpdater::CheckForUpdates(bool silentAsMuchAsItCouldGet) { if (m_feedURL.isEmpty()) { qCritical() << "Please set feed URL via setFeedURL() before calling CheckForUpdates()."; return false; } m_silentAsMuchAsItCouldGet = silentAsMuchAsItCouldGet; // Check if application's organization name and domain are set, fail otherwise // (nowhere to store QSettings to) if (QCoreApplication::organizationName().isEmpty()) { qCritical() << "QApplication::organizationName is not set. Please do that."; return false; } if (QCoreApplication::organizationDomain().isEmpty()) { qCritical() << "QApplication::organizationDomain is not set. Please do that."; return false; } // Set application name / version is not set yet if (QCoreApplication::applicationName().isEmpty()) { qCritical() << "QApplication::applicationName is not set. Please do that."; return false; } if (QCoreApplication::applicationVersion().isEmpty()) { qCritical() << "QApplication::applicationVersion is not set. Please do that."; return false; } cancelDownloadFeed(); m_httpRequestAborted = false; startDownloadFeed(m_feedURL); return true; } //--------------------------------------------------------------------------------------------------------------------- bool FvUpdater::CheckForUpdatesSilent() { if (qApp->Settings()->GetDateOfLastRemind().daysTo(QDate::currentDate()) >= 1) { const bool success = CheckForUpdates(true); if (m_dropOnFinnish && not success) { drop(); } return success; } else { if (m_dropOnFinnish) { drop(); } return true; } } //--------------------------------------------------------------------------------------------------------------------- bool FvUpdater::CheckForUpdatesNotSilent() { const bool success = CheckForUpdates(false); if (m_dropOnFinnish && not success) { drop(); } return success; } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::startDownloadFeed(const QUrl &url) { m_xml.clear(); QNetworkRequest request; request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/xml")); request.setHeader(QNetworkRequest::UserAgentHeader, QCoreApplication::applicationName()); request.setUrl(url); #ifndef QT_NO_SSL request.setSslConfiguration(QSslConfiguration::defaultConfiguration()); #endif m_reply = m_qnam.get(request); connect(m_reply.data(), &QNetworkReply::readyRead, this, [this]() { // this slot gets called every time the QNetworkReply has new data. // We read all of its new data and write it into the file. // That way we use less RAM than when reading it at the finished() // signal of the QNetworkReply m_xml.addData(m_reply->readAll()); }); connect(m_reply.data(), &QNetworkReply::downloadProgress, this, [this](qint64 bytesRead, qint64 totalBytes) { Q_UNUSED(bytesRead) Q_UNUSED(totalBytes) if (m_httpRequestAborted) { return; } }); connect(m_reply.data(), &QNetworkReply::finished, this, &FvUpdater::httpFeedDownloadFinished); } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::cancelDownloadFeed() { if (m_reply) { m_httpRequestAborted = true; m_reply->abort(); } } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::httpFeedDownloadFinished() { if (m_httpRequestAborted) { m_reply->deleteLater(); return; } const QVariant redirectionTarget = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute); if (m_reply->error() != QNetworkReply::NoError) { // Error. showErrorDialog(tr("Feed download failed: %1.").arg(m_reply->errorString()), false); } else if (not redirectionTarget.isNull()) { const QUrl newUrl = m_feedURL.resolved(redirectionTarget.toUrl()); m_feedURL = newUrl; m_reply->deleteLater(); startDownloadFeed(m_feedURL); return; } else { // Done. xmlParseFeed(); } m_reply->deleteLater(); if (m_dropOnFinnish) { drop(); } } //--------------------------------------------------------------------------------------------------------------------- bool FvUpdater::xmlParseFeed() { QString xmlEnclosureUrl, xmlEnclosureVersion, xmlEnclosurePlatform; // Parse while (not m_xml.atEnd()) { m_xml.readNext(); if (m_xml.isStartElement()) { if (m_xml.name() == QLatin1String("item")) { xmlEnclosureUrl.clear(); xmlEnclosureVersion.clear(); xmlEnclosurePlatform.clear(); } else if (m_xml.name() == QLatin1String("enclosure")) { const QXmlStreamAttributes attribs = m_xml.attributes(); const QString fervorPlatform = QStringLiteral("fervor:platform"); if (attribs.hasAttribute(fervorPlatform)) { if (CurrentlyRunningOnPlatform(attribs.value(fervorPlatform).toString().trimmed())) { xmlEnclosurePlatform = attribs.value(fervorPlatform).toString().trimmed(); const QString attributeUrl = QStringLiteral("url"); if (attribs.hasAttribute(attributeUrl)) { xmlEnclosureUrl = attribs.value(attributeUrl).toString().trimmed(); } else { xmlEnclosureUrl.clear(); } const QString fervorVersion = QStringLiteral("fervor:version"); if (attribs.hasAttribute(fervorVersion)) { const QString candidateVersion = attribs.value(fervorVersion).toString().trimmed(); if (not candidateVersion.isEmpty()) { xmlEnclosureVersion = candidateVersion; } } } } } } else if (m_xml.isEndElement()) { if (m_xml.name() == QLatin1String("item")) { // That's it - we have analyzed a single and we'll stop // here (because the topmost is the most recent one, and thus // the newest version. return searchDownloadedFeedForUpdates(xmlEnclosureUrl, xmlEnclosureVersion, xmlEnclosurePlatform); } } if (m_xml.error() && m_xml.error() != QXmlStreamReader::PrematureEndOfDocumentError) { showErrorDialog(tr("Feed parsing failed: %1 %2.").arg(QString::number(m_xml.lineNumber()), m_xml.errorString()), false); return false; } } // No updates were found if we're at this point // (not a single element found) showInformationDialog(tr("No updates were found."), false); return false; } //--------------------------------------------------------------------------------------------------------------------- bool FvUpdater::searchDownloadedFeedForUpdates(const QString &xmlEnclosureUrl, const QString &xmlEnclosureVersion, const QString &xmlEnclosurePlatform) { qDebug() << "Enclosure URL:" << xmlEnclosureUrl; qDebug() << "Enclosure version:" << xmlEnclosureVersion; qDebug() << "Enclosure platform:" << xmlEnclosurePlatform; // Validate if (xmlEnclosureUrl.isEmpty() || xmlEnclosureVersion.isEmpty() || xmlEnclosurePlatform.isEmpty()) { showErrorDialog(tr("Feed error: invalid \"enclosure\" with the download link"), false); return false; } // Relevant version? if (VersionIsIgnored(xmlEnclosureVersion)) { qDebug() << "Version '" << xmlEnclosureVersion << "' is ignored, too old or something like that."; showInformationDialog(tr("No updates were found."), false); return true; // Things have succeeded when you think of it. } // // Success! At this point, we have found an update that can be proposed // to the user. // delete m_proposedUpdate; m_proposedUpdate = new FvAvailableUpdate(this); m_proposedUpdate->SetEnclosureUrl(xmlEnclosureUrl); m_proposedUpdate->SetEnclosureVersion(xmlEnclosureVersion); m_proposedUpdate->SetEnclosurePlatform(xmlEnclosurePlatform); // Show "look, there's an update" window showUpdaterWindowUpdatedWithCurrentUpdateProposal(); return true; } //--------------------------------------------------------------------------------------------------------------------- bool FvUpdater::VersionIsIgnored(const QString &version) { // We assume that variable 'version' contains either: // 1) The current version of the application (ignore) // 2) The version that was skipped before and thus stored in QSettings (ignore) // 3) A newer version (don't ignore) // 'version' is not likely to contain an older version in any case. int decVersion = 0x0; try { decVersion = VAbstractConverter::GetVersion(version); } catch (const VException &e) { Q_UNUSED(e) return true; // Ignore invalid version } if (decVersion == FvUpdater::CurrentVersion()) { return true; } const int lastSkippedVersion = qApp->Settings()->GetLatestSkippedVersion(); if (lastSkippedVersion != 0x0) { if (decVersion == lastSkippedVersion) { // Implicitly skipped version - skip return true; } } if (decVersion > FvUpdater::CurrentVersion()) { // Newer version - do not skip return false; } // Fallback - skip return true; } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::IgnoreVersion(const QString &version) { int decVersion = 0x0; try { decVersion = VAbstractConverter::GetVersion(version); } catch (const VException &e) { Q_UNUSED(e) return ; // Ignore invalid version } if (decVersion == FvUpdater::CurrentVersion()) { // Don't ignore the current version return; } qApp->Settings()->SetLatestSkippedVersion(decVersion); } //--------------------------------------------------------------------------------------------------------------------- bool FvUpdater::CurrentlyRunningOnPlatform(const QString &platform) { const QStringList platforms = QStringList() << "Q_OS_LINUX" << "Q_OS_MAC" << "Q_OS_WIN32"; switch (platforms.indexOf(platform.toUpper().trimmed())) { case 0: // Q_OS_LINUX #ifdef Q_OS_LINUX // Defined on Linux. return true; #else return false; #endif case 1: // Q_OS_MAC #ifdef Q_OS_MAC // Defined on MAC OS (synonym for Darwin). return true; #else return false; #endif case 2: // Q_OS_WIN32 #ifdef Q_OS_WIN32 // Defined on all supported versions of Windows. return true; #else return false; #endif default: break; } // Fallback return false; } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::showErrorDialog(const QString &message, bool showEvenInSilentMode) { if (m_silentAsMuchAsItCouldGet) { if (not showEvenInSilentMode) { // Don't show errors in the silent mode return; } } QMessageBox dlFailedMsgBox; dlFailedMsgBox.setIcon(QMessageBox::Critical); dlFailedMsgBox.setText(message); dlFailedMsgBox.exec(); } //--------------------------------------------------------------------------------------------------------------------- void FvUpdater::showInformationDialog(const QString &message, bool showEvenInSilentMode) { if (m_silentAsMuchAsItCouldGet) { if (not showEvenInSilentMode) { // Don't show information dialogs in the silent mode return; } } QMessageBox dlInformationMsgBox; dlInformationMsgBox.setIcon(QMessageBox::Information); dlInformationMsgBox.setText(message); dlInformationMsgBox.exec(); }