noo/client/fervor/fvupdater.cpp

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();
}