603 lines
15 KiB
C++
603 lines
15 KiB
C++
#include "fvupdater.h"
|
|
#include "fvupdatewindow.h"
|
|
#include "fvupdateconfirmdialog.h"
|
|
#include "fvplatform.h"
|
|
#include "fvignoredversions.h"
|
|
#include "fvavailableupdate.h"
|
|
#include <QApplication>
|
|
#include <QtNetwork>
|
|
#include <QMessageBox>
|
|
#include <QDesktopServices>
|
|
#include <QDebug>
|
|
|
|
#ifndef FV_APP_NAME
|
|
# error "FV_APP_NAME is undefined (must have been defined by Fervor.pri)"
|
|
#endif
|
|
#ifndef FV_APP_VERSION
|
|
# error "FV_APP_VERSION is undefined (must have been defined by Fervor.pri)"
|
|
#endif
|
|
|
|
|
|
#ifdef FV_DEBUG
|
|
// Unit tests
|
|
# include "fvversioncomparatortest.h"
|
|
#endif
|
|
|
|
|
|
FvUpdater* FvUpdater::m_Instance = 0;
|
|
|
|
|
|
FvUpdater* FvUpdater::sharedUpdater()
|
|
{
|
|
static QMutex mutex;
|
|
if (! m_Instance) {
|
|
mutex.lock();
|
|
|
|
if (! m_Instance) {
|
|
m_Instance = new FvUpdater;
|
|
}
|
|
|
|
mutex.unlock();
|
|
}
|
|
|
|
return m_Instance;
|
|
}
|
|
|
|
void FvUpdater::drop()
|
|
{
|
|
static QMutex mutex;
|
|
mutex.lock();
|
|
delete m_Instance;
|
|
m_Instance = 0;
|
|
mutex.unlock();
|
|
}
|
|
|
|
FvUpdater::FvUpdater() : QObject(0)
|
|
{
|
|
m_reply = 0;
|
|
m_updaterWindow = 0;
|
|
m_updateConfirmationDialog = 0;
|
|
m_proposedUpdate = 0;
|
|
|
|
// Translation mechanism
|
|
installTranslator();
|
|
|
|
#ifdef FV_DEBUG
|
|
// Unit tests
|
|
FvVersionComparatorTest* test = new FvVersionComparatorTest();
|
|
test->runAll();
|
|
delete test;
|
|
#endif
|
|
|
|
}
|
|
|
|
FvUpdater::~FvUpdater()
|
|
{
|
|
if (m_proposedUpdate) {
|
|
delete m_proposedUpdate;
|
|
m_proposedUpdate = 0;
|
|
}
|
|
|
|
hideUpdateConfirmationDialog();
|
|
hideUpdaterWindow();
|
|
}
|
|
|
|
void FvUpdater::installTranslator()
|
|
{
|
|
QTranslator translator;
|
|
QString locale = QLocale::system().name();
|
|
translator.load(QString("fervor_") + locale);
|
|
//QTextCodec::setCodecForTr(QTextCodec::codecForName("utf8"));
|
|
qApp->installTranslator(&translator);
|
|
}
|
|
|
|
void FvUpdater::showUpdaterWindowUpdatedWithCurrentUpdateProposal()
|
|
{
|
|
// Destroy window if already exists
|
|
hideUpdaterWindow();
|
|
|
|
// Create a new window
|
|
m_updaterWindow = new FvUpdateWindow();
|
|
m_updaterWindow->UpdateWindowWithCurrentProposedUpdate();
|
|
m_updaterWindow->show();
|
|
}
|
|
|
|
void FvUpdater::hideUpdaterWindow()
|
|
{
|
|
if (m_updaterWindow) {
|
|
if (! m_updaterWindow->close()) {
|
|
qWarning() << "Update window didn't close, leaking memory from now on";
|
|
}
|
|
|
|
// not deleting because of Qt::WA_DeleteOnClose
|
|
|
|
m_updaterWindow = 0;
|
|
}
|
|
}
|
|
|
|
void FvUpdater::updaterWindowWasClosed()
|
|
{
|
|
// (Re-)nullify a pointer to a destroyed QWidget or you're going to have a bad time.
|
|
m_updaterWindow = 0;
|
|
}
|
|
|
|
|
|
void FvUpdater::showUpdateConfirmationDialogUpdatedWithCurrentUpdateProposal()
|
|
{
|
|
// Destroy dialog if already exists
|
|
hideUpdateConfirmationDialog();
|
|
|
|
// Create a new window
|
|
m_updateConfirmationDialog = new FvUpdateConfirmDialog();
|
|
m_updateConfirmationDialog->UpdateWindowWithCurrentProposedUpdate();
|
|
m_updateConfirmationDialog->show();
|
|
}
|
|
|
|
void FvUpdater::hideUpdateConfirmationDialog()
|
|
{
|
|
if (m_updateConfirmationDialog) {
|
|
if (! m_updateConfirmationDialog->close()) {
|
|
qWarning() << "Update confirmation dialog didn't close, leaking memory from now on";
|
|
}
|
|
|
|
// not deleting because of Qt::WA_DeleteOnClose
|
|
|
|
m_updateConfirmationDialog = 0;
|
|
}
|
|
}
|
|
|
|
void FvUpdater::updateConfirmationDialogWasClosed()
|
|
{
|
|
// (Re-)nullify a pointer to a destroyed QWidget or you're going to have a bad time.
|
|
m_updateConfirmationDialog = 0;
|
|
}
|
|
|
|
|
|
void FvUpdater::SetFeedURL(QUrl feedURL)
|
|
{
|
|
m_feedURL = feedURL;
|
|
}
|
|
|
|
void FvUpdater::SetFeedURL(QString feedURL)
|
|
{
|
|
SetFeedURL(QUrl(feedURL));
|
|
}
|
|
|
|
QString FvUpdater::GetFeedURL()
|
|
{
|
|
return m_feedURL.toString();
|
|
}
|
|
|
|
FvAvailableUpdate* FvUpdater::GetProposedUpdate()
|
|
{
|
|
return m_proposedUpdate;
|
|
}
|
|
|
|
|
|
void FvUpdater::InstallUpdate()
|
|
{
|
|
qDebug() << "Install update";
|
|
|
|
showUpdateConfirmationDialogUpdatedWithCurrentUpdateProposal();
|
|
}
|
|
|
|
void FvUpdater::SkipUpdate()
|
|
{
|
|
qDebug() << "Skip update";
|
|
|
|
FvAvailableUpdate* proposedUpdate = GetProposedUpdate();
|
|
if (! proposedUpdate) {
|
|
qWarning() << "Proposed update is NULL (shouldn't be at this point)";
|
|
return;
|
|
}
|
|
|
|
// Start ignoring this particular version
|
|
FVIgnoredVersions::IgnoreVersion(proposedUpdate->GetEnclosureVersion());
|
|
|
|
hideUpdaterWindow();
|
|
hideUpdateConfirmationDialog(); // if any; shouldn't be shown at this point, but who knows
|
|
}
|
|
|
|
void FvUpdater::RemindMeLater()
|
|
{
|
|
qDebug() << "Remind me later";
|
|
|
|
hideUpdaterWindow();
|
|
hideUpdateConfirmationDialog(); // if any; shouldn't be shown at this point, but who knows
|
|
}
|
|
|
|
void FvUpdater::UpdateInstallationConfirmed()
|
|
{
|
|
qDebug() << "Confirm update installation";
|
|
|
|
FvAvailableUpdate* proposedUpdate = GetProposedUpdate();
|
|
if (! proposedUpdate) {
|
|
qWarning() << "Proposed update is NULL (shouldn't be at this point)";
|
|
return;
|
|
}
|
|
|
|
// Open a link
|
|
if (! QDesktopServices::openUrl(proposedUpdate->GetEnclosureUrl())) {
|
|
showErrorDialog(tr("Unable to open this link in a browser. Please do it manually."), true);
|
|
return;
|
|
}
|
|
|
|
hideUpdaterWindow();
|
|
hideUpdateConfirmationDialog();
|
|
}
|
|
|
|
void FvUpdater::UpdateInstallationNotConfirmed()
|
|
{
|
|
qDebug() << "Do not confirm update installation";
|
|
|
|
hideUpdateConfirmationDialog(); // if any; shouldn't be shown at this point, but who knows
|
|
// leave the "update proposal window" inact
|
|
}
|
|
|
|
|
|
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 (QApplication::organizationName().isEmpty()) {
|
|
qCritical() << "QApplication::organizationName is not set. Please do that.";
|
|
return false;
|
|
}
|
|
if (QApplication::organizationDomain().isEmpty()) {
|
|
qCritical() << "QApplication::organizationDomain is not set. Please do that.";
|
|
return false;
|
|
}
|
|
*/
|
|
// Set application name / version is not set yet
|
|
if (QApplication::applicationName().isEmpty()) {
|
|
QString appName = QString::fromUtf8(FV_APP_NAME);
|
|
qWarning() << "QApplication::applicationName is not set, setting it to '" << appName << "'";
|
|
QApplication::setApplicationName(appName);
|
|
}
|
|
if (QApplication::applicationVersion().isEmpty()) {
|
|
QString appVersion = QString::fromUtf8(FV_APP_VERSION);
|
|
qWarning() << "QApplication::applicationVersion is not set, setting it to '" << appVersion << "'";
|
|
QApplication::setApplicationVersion(appVersion);
|
|
}
|
|
|
|
cancelDownloadFeed();
|
|
m_httpRequestAborted = false;
|
|
startDownloadFeed(m_feedURL);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FvUpdater::CheckForUpdatesSilent()
|
|
{
|
|
return CheckForUpdates(true);
|
|
}
|
|
|
|
bool FvUpdater::CheckForUpdatesNotSilent()
|
|
{
|
|
return CheckForUpdates(false);
|
|
}
|
|
|
|
|
|
void FvUpdater::startDownloadFeed(QUrl url)
|
|
{
|
|
m_xml.clear();
|
|
|
|
QNetworkRequest request;
|
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml");
|
|
request.setHeader(QNetworkRequest::UserAgentHeader, QApplication::applicationName());
|
|
request.setUrl(url);
|
|
|
|
m_reply = m_qnam.get(request);
|
|
|
|
connect(m_reply, SIGNAL(readyRead()), this, SLOT(httpFeedReadyRead()));
|
|
connect(m_reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(httpFeedUpdateDataReadProgress(qint64, qint64)));
|
|
connect(m_reply, SIGNAL(finished()), this, SLOT(httpFeedDownloadFinished()));
|
|
}
|
|
|
|
void FvUpdater::cancelDownloadFeed()
|
|
{
|
|
if (m_reply) {
|
|
m_httpRequestAborted = true;
|
|
m_reply->abort();
|
|
}
|
|
}
|
|
|
|
void FvUpdater::httpFeedReadyRead()
|
|
{
|
|
// 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());
|
|
}
|
|
|
|
void FvUpdater::httpFeedUpdateDataReadProgress(qint64 bytesRead,
|
|
qint64 totalBytes)
|
|
{
|
|
Q_UNUSED(bytesRead);
|
|
Q_UNUSED(totalBytes);
|
|
|
|
if (m_httpRequestAborted) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
void FvUpdater::httpFeedDownloadFinished()
|
|
{
|
|
if (m_httpRequestAborted) {
|
|
m_reply->deleteLater();
|
|
return;
|
|
}
|
|
|
|
QVariant redirectionTarget = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
|
if (m_reply->error()) {
|
|
|
|
// Error.
|
|
showErrorDialog(tr("Feed download failed: %1.").arg(m_reply->errorString()), false);
|
|
|
|
} else if (! redirectionTarget.isNull()) {
|
|
QUrl newUrl = m_feedURL.resolved(redirectionTarget.toUrl());
|
|
|
|
m_feedURL = newUrl;
|
|
m_reply->deleteLater();
|
|
|
|
startDownloadFeed(m_feedURL);
|
|
return;
|
|
|
|
} else {
|
|
|
|
// Done.
|
|
xmlParseFeed();
|
|
|
|
}
|
|
|
|
m_reply->deleteLater();
|
|
m_reply = 0;
|
|
}
|
|
|
|
bool FvUpdater::xmlParseFeed()
|
|
{
|
|
QString currentTag, currentQualifiedTag;
|
|
|
|
QString xmlTitle, xmlLink, xmlReleaseNotesLink, xmlPubDate, xmlEnclosureUrl,
|
|
xmlEnclosureVersion, xmlEnclosurePlatform, xmlEnclosureType;
|
|
unsigned long xmlEnclosureLength = 0;
|
|
|
|
// Parse
|
|
while (! m_xml.atEnd()) {
|
|
|
|
m_xml.readNext();
|
|
|
|
if (m_xml.isStartElement()) {
|
|
|
|
currentTag = m_xml.name().toString();
|
|
currentQualifiedTag = m_xml.qualifiedName().toString();
|
|
|
|
if (m_xml.name() == "item") {
|
|
|
|
xmlTitle.clear();
|
|
xmlLink.clear();
|
|
xmlReleaseNotesLink.clear();
|
|
xmlPubDate.clear();
|
|
xmlEnclosureUrl.clear();
|
|
xmlEnclosureVersion.clear();
|
|
xmlEnclosurePlatform.clear();
|
|
xmlEnclosureLength = 0;
|
|
xmlEnclosureType.clear();
|
|
|
|
} else if (m_xml.name() == "enclosure") {
|
|
|
|
QXmlStreamAttributes attribs = m_xml.attributes();
|
|
|
|
if (attribs.hasAttribute("fervor:platform")) {
|
|
|
|
if (FvPlatform::CurrentlyRunningOnPlatform(attribs.value("fervor:platform").toString().trimmed())) {
|
|
|
|
xmlEnclosurePlatform = attribs.value("fervor:platform").toString().trimmed();
|
|
|
|
if (attribs.hasAttribute("url")) {
|
|
xmlEnclosureUrl = attribs.value("url").toString().trimmed();
|
|
} else {
|
|
xmlEnclosureUrl = "";
|
|
}
|
|
|
|
// First check for Sparkle's version, then overwrite with Fervor's version (if any)
|
|
if (attribs.hasAttribute("sparkle:version")) {
|
|
QString candidateVersion = attribs.value("sparkle:version").toString().trimmed();
|
|
if (! candidateVersion.isEmpty()) {
|
|
xmlEnclosureVersion = candidateVersion;
|
|
}
|
|
}
|
|
if (attribs.hasAttribute("fervor:version")) {
|
|
QString candidateVersion = attribs.value("fervor:version").toString().trimmed();
|
|
if (! candidateVersion.isEmpty()) {
|
|
xmlEnclosureVersion = candidateVersion;
|
|
}
|
|
}
|
|
|
|
if (attribs.hasAttribute("length")) {
|
|
xmlEnclosureLength = attribs.value("length").toString().toLong();
|
|
} else {
|
|
xmlEnclosureLength = 0;
|
|
}
|
|
if (attribs.hasAttribute("type")) {
|
|
xmlEnclosureType = attribs.value("type").toString().trimmed();
|
|
} else {
|
|
xmlEnclosureType = "";
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (m_xml.isEndElement()) {
|
|
|
|
if (m_xml.name() == "item") {
|
|
|
|
// That's it - we have analyzed a single <item> and we'll stop
|
|
// here (because the topmost is the most recent one, and thus
|
|
// the newest version.
|
|
|
|
return searchDownloadedFeedForUpdates(xmlTitle,
|
|
xmlLink,
|
|
xmlReleaseNotesLink,
|
|
xmlPubDate,
|
|
xmlEnclosureUrl,
|
|
xmlEnclosureVersion,
|
|
xmlEnclosurePlatform,
|
|
xmlEnclosureLength,
|
|
xmlEnclosureType);
|
|
|
|
}
|
|
|
|
} else if (m_xml.isCharacters() && ! m_xml.isWhitespace()) {
|
|
|
|
if (currentTag == "title") {
|
|
xmlTitle += m_xml.text().toString().trimmed();
|
|
|
|
} else if (currentTag == "link") {
|
|
xmlLink += m_xml.text().toString().trimmed();
|
|
|
|
} else if (currentQualifiedTag == "sparkle:releaseNotesLink") {
|
|
xmlReleaseNotesLink += m_xml.text().toString().trimmed();
|
|
|
|
} else if (currentTag == "pubDate") {
|
|
xmlPubDate += m_xml.text().toString().trimmed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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 <item> element found)
|
|
showInformationDialog(tr("No updates were found."), false);
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
bool FvUpdater::searchDownloadedFeedForUpdates(QString xmlTitle,
|
|
QString xmlLink,
|
|
QString xmlReleaseNotesLink,
|
|
QString xmlPubDate,
|
|
QString xmlEnclosureUrl,
|
|
QString xmlEnclosureVersion,
|
|
QString xmlEnclosurePlatform,
|
|
unsigned long xmlEnclosureLength,
|
|
QString xmlEnclosureType)
|
|
{
|
|
qDebug() << "Title:" << xmlTitle;
|
|
qDebug() << "Link:" << xmlLink;
|
|
qDebug() << "Release notes link:" << xmlReleaseNotesLink;
|
|
qDebug() << "Pub. date:" << xmlPubDate;
|
|
qDebug() << "Enclosure URL:" << xmlEnclosureUrl;
|
|
qDebug() << "Enclosure version:" << xmlEnclosureVersion;
|
|
qDebug() << "Enclosure platform:" << xmlEnclosurePlatform;
|
|
qDebug() << "Enclosure length:" << xmlEnclosureLength;
|
|
qDebug() << "Enclosure type:" << xmlEnclosureType;
|
|
|
|
// Validate
|
|
if (xmlReleaseNotesLink.isEmpty()) {
|
|
if (xmlLink.isEmpty()) {
|
|
showErrorDialog(tr("Feed error: \"release notes\" link is empty"), false);
|
|
return false;
|
|
} else {
|
|
xmlReleaseNotesLink = xmlLink;
|
|
}
|
|
} else {
|
|
xmlLink = xmlReleaseNotesLink;
|
|
}
|
|
if (! (xmlLink.startsWith("http://") || xmlLink.startsWith("https://"))) {
|
|
showErrorDialog(tr("Feed error: invalid \"release notes\" link"), false);
|
|
return false;
|
|
}
|
|
if (xmlEnclosureUrl.isEmpty() || xmlEnclosureVersion.isEmpty() || xmlEnclosurePlatform.isEmpty()) {
|
|
showErrorDialog(tr("Feed error: invalid \"enclosure\" with the download link"), false);
|
|
return false;
|
|
}
|
|
|
|
// Relevant version?
|
|
if (FVIgnoredVersions::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.
|
|
//
|
|
|
|
if (m_proposedUpdate) {
|
|
delete m_proposedUpdate; m_proposedUpdate = 0;
|
|
}
|
|
m_proposedUpdate = new FvAvailableUpdate();
|
|
m_proposedUpdate->SetTitle(xmlTitle);
|
|
m_proposedUpdate->SetReleaseNotesLink(xmlReleaseNotesLink);
|
|
m_proposedUpdate->SetPubDate(xmlPubDate);
|
|
m_proposedUpdate->SetEnclosureUrl(xmlEnclosureUrl);
|
|
m_proposedUpdate->SetEnclosureVersion(xmlEnclosureVersion);
|
|
m_proposedUpdate->SetEnclosurePlatform(xmlEnclosurePlatform);
|
|
m_proposedUpdate->SetEnclosureLength(xmlEnclosureLength);
|
|
m_proposedUpdate->SetEnclosureType(xmlEnclosureType);
|
|
|
|
// Show "look, there's an update" window
|
|
showUpdaterWindowUpdatedWithCurrentUpdateProposal();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void FvUpdater::showErrorDialog(QString message, bool showEvenInSilentMode)
|
|
{
|
|
if (m_silentAsMuchAsItCouldGet) {
|
|
if (! showEvenInSilentMode) {
|
|
// Don't show errors in the silent mode
|
|
return;
|
|
}
|
|
}
|
|
|
|
QMessageBox dlFailedMsgBox;
|
|
dlFailedMsgBox.setIcon(QMessageBox::Critical);
|
|
dlFailedMsgBox.setText(tr("Error"));
|
|
dlFailedMsgBox.setInformativeText(message);
|
|
dlFailedMsgBox.exec();
|
|
}
|
|
|
|
void FvUpdater::showInformationDialog(QString message, bool showEvenInSilentMode)
|
|
{
|
|
if (m_silentAsMuchAsItCouldGet) {
|
|
if (! showEvenInSilentMode) {
|
|
// Don't show information dialogs in the silent mode
|
|
return;
|
|
}
|
|
}
|
|
|
|
QMessageBox dlInformationMsgBox;
|
|
dlInformationMsgBox.setIcon(QMessageBox::Information);
|
|
dlInformationMsgBox.setText(tr("Information"));
|
|
dlInformationMsgBox.setInformativeText(message);
|
|
dlInformationMsgBox.exec();
|
|
}
|