Aegisub/src/dialog_version_check.cpp

438 lines
13 KiB
C++

// Copyright (c) 2007, Rodrigo Braz Monteiro
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of the Aegisub Group nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Aegisub Project http://www.aegisub.org/
#ifdef WITH_UPDATE_CHECKER
#ifdef _MSC_VER
#pragma warning(disable : 4250) // 'boost::asio::basic_socket_iostream<Protocol>' : inherits 'std::basic_ostream<_Elem,_Traits>::std::basic_ostream<_Elem,_Traits>::_Add_vtordisp2' via dominance
#endif
#include "compat.h"
#include "format.h"
#include "options.h"
#include "string_codec.h"
#include "version.h"
#include <libaegisub/dispatch.h>
#include <libaegisub/exception.h>
#include <libaegisub/line_iterator.h>
#include <libaegisub/scoped_ptr.h>
#include <libaegisub/split.h>
#include <ctime>
#include <boost/asio/ip/tcp.hpp>
#include <functional>
#include <mutex>
#include <vector>
#include <wx/button.h>
#include <wx/checkbox.h>
#include <wx/dialog.h>
#include <wx/event.h>
#include <wx/hyperlink.h>
#include <wx/intl.h>
#include <wx/platinfo.h>
#include <wx/sizer.h>
#include <wx/statline.h>
#include <wx/stattext.h>
#include <wx/string.h>
#include <wx/textctrl.h>
#ifdef __APPLE__
#include <CoreFoundation/CoreFoundation.h>
#endif
namespace {
std::mutex VersionCheckLock;
struct AegisubUpdateDescription {
int major;
int minor;
int patch;
std::string extra;
std::string description;
};
AegisubUpdateDescription ParseVersionString(std::string version_string) {
std::vector<std::string> maj_min;
std::vector<std::string> patch;
agi::Split(maj_min, version_string, '.');
agi::Split(patch, maj_min[2], '-');
std::string extra = "";
if (patch.size() > 1) {
extra = patch[1];
}
return AegisubUpdateDescription{
atoi(maj_min[0].c_str()),
atoi(maj_min[1].c_str()),
atoi(patch[0].c_str()),
extra,
""
};
}
bool IsNewer(AegisubUpdateDescription update) {
AegisubUpdateDescription current = ParseVersionString(GetReleaseVersion());
if (update.major != current.major)
return update.major > current.major;
if (update.minor != current.minor)
return update.minor > current.minor;
if (update.patch != current.patch)
return update.patch > current.patch;
return update.extra.compare(current.extra) > 0;
}
std::string AegisubVersion(AegisubUpdateDescription update) {
std::ostringstream s;
s << update.major << "." << update.minor << "." << update.patch;
if (!update.extra.empty())
s << "-" << update.extra;
return s.str();
}
class VersionCheckerResultDialog final : public wxDialog {
void OnCloseButton(wxCommandEvent &evt);
void OnRemindMeLater(wxCommandEvent &evt);
void OnClose(wxCloseEvent &evt);
wxCheckBox *automatic_check_checkbox;
public:
VersionCheckerResultDialog(wxString const& main_text, const AegisubUpdateDescription update);
bool ShouldPreventAppExit() const override { return false; }
};
VersionCheckerResultDialog::VersionCheckerResultDialog(wxString const& main_text, const AegisubUpdateDescription update)
: wxDialog(nullptr, -1, _("Version Checker"))
{
const int controls_width = 500;
wxSizer *main_sizer = new wxBoxSizer(wxVERTICAL);
wxStaticText *text = new wxStaticText(this, -1, main_text);
text->Wrap(controls_width);
main_sizer->Add(text, 0, wxBOTTOM|wxEXPAND, 6);
main_sizer->Add(new wxStaticLine(this), 0, wxEXPAND|wxALL, 6);
if (IsNewer(update)) {
text = new wxStaticText(this, -1, to_wx("Aegisub-Japan7"));
wxFont boldfont = text->GetFont();
boldfont.SetWeight(wxFONTWEIGHT_BOLD);
text->SetFont(boldfont);
main_sizer->Add(text, 0, wxEXPAND|wxBOTTOM, 6);
wxTextCtrl *descbox = new wxTextCtrl(this, -1, to_wx(update.description), wxDefaultPosition, wxSize(controls_width,60), wxTE_MULTILINE|wxTE_READONLY);
main_sizer->Add(descbox, 0, wxEXPAND|wxBOTTOM, 6);
std::ostringstream surl;
surl << "http://" << UPDATE_CHECKER_SERVER << UPDATE_CHECKER_BASE_URL << "/Aegisub-Japan7-x64-" << AegisubVersion(update) << ".exe";
std::string url = surl.str();
main_sizer->Add(new wxHyperlinkCtrl(this, -1, to_wx(url), to_wx(url)), 0, wxALIGN_LEFT|wxBOTTOM, 6);
}
automatic_check_checkbox = new wxCheckBox(this, -1, _("&Auto Check for Updates"));
automatic_check_checkbox->SetValue(OPT_GET("App/Auto/Check For Updates")->GetBool());
wxButton *remind_later_button = nullptr;
if (IsNewer(update))
remind_later_button = new wxButton(this, wxID_NO, _("Remind me again in a &week"));
wxButton *close_button = new wxButton(this, wxID_OK, _("&Close"));
SetAffirmativeId(wxID_OK);
SetEscapeId(wxID_OK);
if (IsNewer(update))
main_sizer->Add(new wxStaticLine(this), 0, wxEXPAND|wxALL, 6);
main_sizer->Add(automatic_check_checkbox, 0, wxEXPAND|wxBOTTOM, 6);
auto button_sizer = new wxStdDialogButtonSizer();
button_sizer->AddButton(close_button);
if (remind_later_button)
button_sizer->AddButton(remind_later_button);
button_sizer->Realize();
main_sizer->Add(button_sizer, 0, wxEXPAND, 0);
wxSizer *outer_sizer = new wxBoxSizer(wxVERTICAL);
outer_sizer->Add(main_sizer, 0, wxALL|wxEXPAND, 12);
SetSizerAndFit(outer_sizer);
Centre();
Show();
Bind(wxEVT_BUTTON, std::bind(&VersionCheckerResultDialog::Close, this, false), wxID_OK);
Bind(wxEVT_BUTTON, &VersionCheckerResultDialog::OnRemindMeLater, this, wxID_NO);
Bind(wxEVT_CLOSE_WINDOW, &VersionCheckerResultDialog::OnClose, this);
}
void VersionCheckerResultDialog::OnRemindMeLater(wxCommandEvent &) {
// In one week
time_t new_next_check_time = time(nullptr) + 7*24*60*60;
OPT_SET("Version/Next Check")->SetInt(new_next_check_time);
Close();
}
void VersionCheckerResultDialog::OnClose(wxCloseEvent &) {
OPT_SET("App/Auto/Check For Updates")->SetBool(automatic_check_checkbox->GetValue());
Destroy();
}
DEFINE_EXCEPTION(VersionCheckError, agi::Exception);
void PostErrorEvent(bool interactive, wxString const& error_text) {
if (interactive) {
agi::dispatch::Main().Async([=]{
new VersionCheckerResultDialog(error_text, {});
});
}
}
static const char * GetOSShortName() {
int osver_maj, osver_min;
wxOperatingSystemId osid = wxGetOsVersion(&osver_maj, &osver_min);
if (osid & wxOS_WINDOWS_NT) {
if (osver_maj == 5 && osver_min == 0)
return "win2k";
else if (osver_maj == 5 && osver_min == 1)
return "winxp";
else if (osver_maj == 5 && osver_min == 2)
return "win2k3"; // this is also xp64
else if (osver_maj == 6 && osver_min == 0)
return "win60"; // vista and server 2008
else if (osver_maj == 6 && osver_min == 1)
return "win61"; // 7 and server 2008r2
else if (osver_maj == 6 && osver_min == 2)
return "win62"; // 8
else
return "windows"; // future proofing? I doubt we run on nt4
}
// CF returns 0x10 for some reason, which wx has recently started
// turning into 10
else if (osid & wxOS_MAC_OSX_DARWIN && (osver_maj == 0x10 || osver_maj == 10)) {
// ugliest hack in the world? nah.
static char osxstring[] = "osx00";
char minor = osver_min >> 4;
char patch = osver_min & 0x0F;
osxstring[3] = minor + ((minor<=9) ? '0' : ('a'-1));
osxstring[4] = patch + ((patch<=9) ? '0' : ('a'-1));
return osxstring;
}
else if (osid & wxOS_UNIX_LINUX)
return "linux";
else if (osid & wxOS_UNIX_FREEBSD)
return "freebsd";
else if (osid & wxOS_UNIX_OPENBSD)
return "openbsd";
else if (osid & wxOS_UNIX_NETBSD)
return "netbsd";
else if (osid & wxOS_UNIX_SOLARIS)
return "solaris";
else if (osid & wxOS_UNIX_AIX)
return "aix";
else if (osid & wxOS_UNIX_HPUX)
return "hpux";
else if (osid & wxOS_UNIX)
return "unix";
else if (osid & wxOS_OS2)
return "os2";
else if (osid & wxOS_DOS)
return "dos";
else
return "unknown";
}
#ifdef WIN32
typedef BOOL (WINAPI * PGetUserPreferredUILanguages)(DWORD dwFlags, PULONG pulNumLanguages, wchar_t *pwszLanguagesBuffer, PULONG pcchLanguagesBuffer);
// Try using Win 6+ functions if available
static wxString GetUILanguage() {
agi::scoped_holder<HMODULE, BOOL (__stdcall *)(HMODULE)> kernel32(LoadLibraryW(L"kernel32.dll"), FreeLibrary);
if (!kernel32) return "";
PGetUserPreferredUILanguages gupuil = (PGetUserPreferredUILanguages)GetProcAddress(kernel32, "GetUserPreferredUILanguages");
if (!gupuil) return "";
ULONG numlang = 0, output_len = 0;
if (gupuil(MUI_LANGUAGE_NAME, &numlang, 0, &output_len) != TRUE || !output_len)
return "";
std::vector<wchar_t> output(output_len);
if (!gupuil(MUI_LANGUAGE_NAME, &numlang, &output[0], &output_len) || numlang < 1)
return "";
// We got at least one language, just treat it as the only, and a null-terminated string
return &output[0];
}
static wxString GetSystemLanguage() {
wxString res = GetUILanguage();
if (!res)
// On an old version of Windows, let's just return the LANGID as a string
res = fmt_wx("x-win%04x", GetUserDefaultUILanguage());
return res;
}
#elif __APPLE__
static wxString GetSystemLanguage() {
CFLocaleRef locale = CFLocaleCopyCurrent();
CFStringRef localeName = (CFStringRef)CFLocaleGetValue(locale, kCFLocaleIdentifier);
char buf[128] = { 0 };
CFStringGetCString(localeName, buf, sizeof buf, kCFStringEncodingUTF8);
CFRelease(locale);
return wxString::FromUTF8(buf);
}
#else
static wxString GetSystemLanguage() {
return wxLocale::GetLanguageInfo(wxLocale::GetSystemLanguage())->CanonicalName;
}
#endif
static wxString GetAegisubLanguage() {
return to_wx(OPT_GET("App/Language")->GetString());
}
AegisubUpdateDescription GetLatestVersion() {
boost::asio::ip::tcp::iostream stream;
stream.connect(UPDATE_CHECKER_SERVER, "http");
if (!stream)
throw VersionCheckError(from_wx(_("Could not connect to updates server.")));
agi::format(stream,
"GET %s/latest HTTP/1.1\r\n"
"User-Agent: Aegisub-Japan7\r\n"
"Host: %s\r\n"
"Accept: */*\r\n"
"Connection: close\r\n\r\n"
, UPDATE_CHECKER_BASE_URL
, UPDATE_CHECKER_SERVER);
std::string http_version;
stream >> http_version;
int status_code;
stream >> status_code;
if (!stream || http_version.substr(0, 5) != "HTTP/")
throw VersionCheckError(from_wx(_("Could not download from updates server.")));
if (status_code != 200)
throw VersionCheckError(agi::format(_("HTTP request failed, got HTTP response %d."), status_code));
stream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
// Skip the headers since we don't care about them
for (auto const& header : agi::line_iterator<std::string>(stream))
if (header.empty()) break;
AegisubUpdateDescription version = AegisubUpdateDescription{0, 0, 0, "", ""};
std::ostringstream desc;
for (auto const& line : agi::line_iterator<std::string>(stream)) {
if (version.major == 0 && version.minor == 0 && version.minor == 0) {
version = ParseVersionString(line);
} else {
desc << line << "\n";
}
}
if (version.major != 0 && version.minor != 0 && version.patch != 0) {
version.description = desc.str();
return version;
}
throw VersionCheckError(from_wx(_("Could not get update from updates server.")));
}
void DoCheck(bool interactive) {
AegisubUpdateDescription update = GetLatestVersion();
if (IsNewer(update) || interactive) {
agi::dispatch::Main().Async([=]{
wxString text;
if (IsNewer(update))
text = _("An update to Aegisub was found.");
else
text = _("There are no updates to Aegisub.");
new VersionCheckerResultDialog(text, update);
});
}
}
}
void PerformVersionCheck(bool interactive) {
agi::dispatch::Background().Async([=]{
if (!interactive) {
// Automatic checking enabled?
if (!OPT_GET("App/Auto/Check For Updates")->GetBool())
return;
// Is it actually time for a check?
time_t next_check = OPT_GET("Version/Next Check")->GetInt();
if (next_check > time(nullptr))
return;
}
if (!VersionCheckLock.try_lock()) return;
try {
DoCheck(interactive);
}
catch (const agi::Exception &e) {
PostErrorEvent(interactive, fmt_tl(
"There was an error checking for updates to Aegisub:\n%s\n\nIf other applications can access the Internet fine, this is probably a temporary server problem on our end.",
e.GetMessage()));
}
catch (...) {
PostErrorEvent(interactive, _("An unknown error occurred while checking for updates to Aegisub."));
}
VersionCheckLock.unlock();
agi::dispatch::Main().Async([]{
time_t new_next_check_time = time(nullptr) + 60*60; // in one hour
OPT_SET("Version/Next Check")->SetInt(new_next_check_time);
});
});
}
#endif