Files
2025-12-10 14:38:26 -08:00

359 lines
13 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#include "version_check.h"
#include <numeric>
#include <zen/crc.h>
#include <zen/file_io.h>
#include <zen/http.h>
#include <zen/process_exec.h>
#include <zen/sys_version.h>
#include <zen/sys_info.h>
#include <wx+/image_resources.h>
#include "../ffs_paths.h"
#include "../version/version.h"
#include "small_dlgs.h"
#include <zen/symlink_target.h>
#include <zen/build_info.h>
#include <wx/uilocale.h>
#include <gtk/gtk.h>
using namespace zen;
using namespace fff;
namespace
{
const Zchar ffsUpdateCheckUserAgent[] = Zstr("FFS-Update-Check");
time_t getVersionCheckCurrentTime()
{
time_t now = std::time(nullptr);
return now;
}
void openBrowserForDownload(wxWindow* parent)
{
wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php");
}
}
bool fff::automaticUpdateCheckDue(time_t lastUpdateCheck)
{
const time_t now = std::time(nullptr);
return numeric::dist(now, lastUpdateCheck) >= 7 * 24 * 3600; //check weekly
}
namespace
{
std::wstring getIso639Language()
{
assert(runningOnMainThread()); //this function is not thread-safe: consider wxWidgets usage
std::wstring localeName(copyStringTo<std::wstring>(wxUILocale::GetLanguageCanonicalName(wxUILocale::GetSystemLanguage())));
localeName = beforeFirst(localeName, L'@', IfNotFoundReturn::all); //the locale may contain an @, e.g. "sr_RS@latin"; see wxUILocale::InitLanguagesDB()
if (!localeName.empty())
{
const std::wstring langCode = beforeFirst(localeName, L'_', IfNotFoundReturn::all);
assert(langCode.size() == 2 || langCode.size() == 3); //ISO 639: 3-letter possible!
return langCode;
}
assert(false);
return L"zz";
}
std::wstring getIso3166Country()
{
assert(runningOnMainThread()); //this function is not thread-safe, consider wxWidgets usage
std::wstring localeName(copyStringTo<std::wstring>(wxUILocale::GetLanguageCanonicalName(wxUILocale::GetSystemLanguage())));
localeName = beforeFirst(localeName, L'@', IfNotFoundReturn::all); //the locale may contain an @, e.g. "sr_RS@latin"; see wxUILocale::InitLanguagesDB()
if (contains(localeName, L'_'))
{
const std::wstring cc = afterFirst(localeName, L'_', IfNotFoundReturn::none);
assert(cc.size() == 2 || cc.size() == 3); //ISO 3166: 3-letter possible!
return cc;
}
assert(false);
return L"ZZ";
}
//coordinate with get_latest_version_number.php
std::vector<std::pair<std::string, std::string>> geHttpPostParameters() //throw SysError
{
assert(runningOnMainThread()); //this function is not thread-safe, e.g. consider wxWidgets usage in getIso639Language()
std::vector<std::pair<std::string, std::string>> params;
params.emplace_back("ffs_version", ffsVersion);
params.emplace_back("os_name", "Linux");
const OsVersion osv = getOsVersion();
params.emplace_back("os_version", numberTo<std::string>(osv.major) + "." + numberTo<std::string>(osv.minor));
const char* osArch = cpuArchName;
params.emplace_back("os_arch", osArch);
#if GTK_MAJOR_VERSION == 2
//GetContentScaleFactor() requires GTK3 or later
#elif GTK_MAJOR_VERSION == 3
params.emplace_back("dip_scale", numberTo<std::string>(wxScreenDC().GetContentScaleFactor()));
#else
#error unknown GTK version!
#endif
const std::string ffsLang = []
{
const wxLanguage lang = getLanguage();
for (const TranslationInfo& ti : getAvailableTranslations())
if (ti.languageID == lang)
return ti.locale;
return std::string("zz");
}();
params.emplace_back("ffs_lang", ffsLang);
params.emplace_back("language", utfTo<std::string>(getIso639Language()));
params.emplace_back("country", utfTo<std::string>(getIso3166Country()));
return params;
}
void showUpdateAvailableDialog(wxWindow* parent, const std::string& onlineVersion)
{
std::wstring updateDetailsMsg;
try
{
updateDetailsMsg = utfTo<std::wstring>(sendHttpGet(utfTo<Zstring>("https://api.freefilesync.org/latest_changes?" + xWwwFormUrlEncode({{"since", ffsVersion}})),
ffsUpdateCheckUserAgent, Zstring() /*caCertFilePath*/).readAll(nullptr /*notifyUnbufferedIO*/)); //throw SysError
}
catch (const SysError& e) { updateDetailsMsg = _("Failed to retrieve update information.") + + L"\n\n" + e.toString(); }
switch (showConfirmationDialog(parent, DialogInfoType::info, PopupDialogCfg().
setIcon(loadImage("FreeFileSync", dipToScreen(48))).
setTitle(_("Check for Software Updates")).
setMainInstructions(replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo<std::wstring>(onlineVersion)) + L"\n\n" + _("Download now?")).
setDetailInstructions(updateDetailsMsg), _("&Download")))
{
case ConfirmationButton::accept: //download
openBrowserForDownload(parent);
break;
case ConfirmationButton::cancel:
break;
}
}
std::string getOnlineVersion(const std::vector<std::pair<std::string, std::string>>& postParams) //throw SysError
{
const std::string response = sendHttpPost(Zstr("https://api.freefilesync.org/latest_version"), postParams, nullptr /*notifyUnbufferedIO*/,
ffsUpdateCheckUserAgent, Zstring() /*caCertFilePath*/).readAll(nullptr /*notifyUnbufferedIO*/); //throw SysError
if (response.empty() ||
!std::all_of(response.begin(), response.end(), [](const char c) { return isDigit(c) || c == FFS_VERSION_SEPARATOR; }) ||
startsWith(response, FFS_VERSION_SEPARATOR) ||
endsWith(response, FFS_VERSION_SEPARATOR) ||
contains(response, std::string() + FFS_VERSION_SEPARATOR + FFS_VERSION_SEPARATOR))
throw SysError(L"Unexpected server response: \"" + utfTo<std::wstring>(response) + L'"');
//response may be "This website has been moved...", or a Javascript challenge: https://freefilesync.org/forum/viewtopic.php?t=8400
return response;
}
std::string getUnknownVersionTag()
{
return '<' + utfTo<std::string>(_("unknown version")) + '>';
}
}
bool fff::haveNewerVersionOnline(const std::string& onlineVersion)
{
auto parseVersion = [](const std::string_view& version)
{
std::vector<size_t> output;
split(version, FFS_VERSION_SEPARATOR,
[&](const std::string_view digit) { output.push_back(stringTo<size_t>(digit)); });
assert(!output.empty());
return output;
};
const std::vector<size_t> current = parseVersion(ffsVersion);
const std::vector<size_t> online = parseVersion(onlineVersion);
if (online.empty() || online[0] == 0) //online version string may be "Unknown", see automaticUpdateCheckEval() below!
return true;
return online > current; //std::vector compares lexicographically
}
void fff::checkForUpdateNow(wxWindow& parent, std::string& lastOnlineVersion)
{
try
{
const std::string onlineVersion = getOnlineVersion(geHttpPostParameters()); //throw SysError
lastOnlineVersion = onlineVersion;
if (haveNewerVersionOnline(onlineVersion))
showUpdateAvailableDialog(&parent, onlineVersion);
else
{
std::wstring ffsVersionName = L"FreeFileSync " + utfTo<std::wstring>(ffsVersion);
showNotificationDialog(&parent, DialogInfoType::info, PopupDialogCfg().
setIcon(loadImage("update_check")).
setTitle(_("Check for Software Updates")).
setMainInstructions(replaceCpy(_("FreeFileSync is up-to-date."), L"FreeFileSync", ffsVersionName)));
}
}
catch (const SysError& e)
{
if (internetIsAlive())
{
lastOnlineVersion = getUnknownVersionTag();
switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg().
setTitle(_("Check for Software Updates")).
setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")).
setDetailInstructions(e.toString()), _("&Check"), _("&Retry")))
{
case ConfirmationButton2::accept:
openBrowserForDownload(&parent);
break;
case ConfirmationButton2::accept2: //retry
checkForUpdateNow(parent, lastOnlineVersion); //note: retry via recursion!!!
break;
case ConfirmationButton2::cancel:
break;
}
}
else
switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg().
setTitle(_("Check for Software Updates")).
setMainInstructions(replaceCpy(_("Unable to connect to %x."), L"%x", L"freefilesync.org")).
setDetailInstructions(e.toString()), _("&Retry")))
{
case ConfirmationButton::accept: //retry
checkForUpdateNow(parent, lastOnlineVersion); //note: retry via recursion!!!
break;
case ConfirmationButton::cancel:
break;
}
}
}
struct fff::UpdateCheckResultPrep
{
std::vector<std::pair<std::string, std::string>> postParameters;
std::optional<SysError> error;
};
SharedRef<const UpdateCheckResultPrep> fff::automaticUpdateCheckPrepare(wxWindow& parent)
{
assert(runningOnMainThread());
auto prep = makeSharedRef<UpdateCheckResultPrep>();
try
{
prep.ref().postParameters = geHttpPostParameters(); //throw SysError
}
catch (const SysError& e)
{
prep.ref().error = e;
}
return prep;
}
struct fff::UpdateCheckResult
{
std::string onlineVersion;
std::optional<SysError> error;
bool internetIsAlive = false;
};
SharedRef<const UpdateCheckResult> fff::automaticUpdateCheckRunAsync(const UpdateCheckResultPrep& resultPrep)
{
//assert(!runningOnMainThread()); -> allow synchronous call, too
auto result = makeSharedRef<UpdateCheckResult>();
try
{
if (resultPrep.error)
throw* resultPrep.error; //throw SysError
result.ref().onlineVersion = getOnlineVersion(resultPrep.postParameters); //throw SysError
result.ref().internetIsAlive = true;
}
catch (const SysError& e)
{
result.ref().error = e;
result.ref().internetIsAlive = internetIsAlive();
}
return result;
}
void fff::automaticUpdateCheckEval(wxWindow& parent, time_t& lastUpdateCheck, std::string& lastOnlineVersion, const UpdateCheckResult& result)
{
assert(runningOnMainThread());
if (!result.error)
{
lastUpdateCheck = getVersionCheckCurrentTime();
if (lastOnlineVersion != result.onlineVersion) //show new version popup only *once* per new release
{
lastOnlineVersion = result.onlineVersion;
if (haveNewerVersionOnline(result.onlineVersion)) //beta or development version is newer than online
showUpdateAvailableDialog(&parent, result.onlineVersion);
}
}
else
{
if (result.internetIsAlive)
{
if (lastOnlineVersion != getUnknownVersionTag())
switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg().
setTitle(_("Check for Software Updates")).
setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")).
setDetailInstructions(result.error->toString()),
_("&Check"), _("&Retry")))
{
case ConfirmationButton2::accept:
lastOnlineVersion = getUnknownVersionTag();
openBrowserForDownload(&parent);
break;
case ConfirmationButton2::accept2: //retry
automaticUpdateCheckEval(parent, lastUpdateCheck, lastOnlineVersion,
automaticUpdateCheckRunAsync(automaticUpdateCheckPrepare(parent).ref()).ref()); //retry via recursion!!!
break;
case ConfirmationButton2::cancel:
lastOnlineVersion = getUnknownVersionTag();
break;
}
}
else //no internet connection
{
if (lastOnlineVersion.empty())
lastOnlineVersion = ffsVersion;
}
}
}