mirror of https://github.com/odrling/Aegisub
461 lines
14 KiB
C++
461 lines
14 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/beast/core.hpp>
|
|
#include <boost/beast/http.hpp>
|
|
#include <boost/beast/version.hpp>
|
|
#include <boost/asio/connect.hpp>
|
|
#include <boost/asio/ip/tcp.hpp>
|
|
#include <boost/asio/ssl/error.hpp>
|
|
#include <boost/asio/ssl/stream.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;
|
|
|
|
namespace ssl = boost::asio::ssl;
|
|
namespace http = boost::beast::http;
|
|
|
|
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 << "https://" << UPDATE_CHECKER_SERVER << UPDATE_CHECKER_BASE_URL << "/Aegisub-Japan7-" << AegisubVersion(update) << "-x64.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::io_context ioc;
|
|
boost::asio::ssl::context ctx(ssl::context::method::sslv23_client);
|
|
|
|
boost::asio::ip::tcp::resolver resolver(ioc);
|
|
ssl::stream<boost::asio::ip::tcp::socket> stream(ioc, ctx);
|
|
|
|
if(! SSL_set_tlsext_host_name(stream.native_handle(), UPDATE_CHECKER_SERVER)) {
|
|
boost::system::error_code ec{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
|
|
throw boost::system::system_error{ec};
|
|
}
|
|
|
|
auto const results = resolver.resolve(UPDATE_CHECKER_SERVER, "443");
|
|
|
|
boost::asio::connect(stream.next_layer(), results.begin(), results.end());
|
|
stream.handshake(boost::asio::ssl::stream_base::handshake_type::client);
|
|
|
|
std::ostringstream s;
|
|
s << UPDATE_CHECKER_BASE_URL;
|
|
s << "/latest";
|
|
std::string target = s.str();
|
|
http::request<http::string_body> req(http::verb::get, target, 11);
|
|
req.set(http::field::host, UPDATE_CHECKER_SERVER);
|
|
req.set(http::field::user_agent, "Aegisub-Japan7");
|
|
|
|
http::write(stream, req);
|
|
|
|
boost::beast::flat_buffer buffer;
|
|
|
|
http::response<http::string_body> res;
|
|
|
|
http::read(stream, buffer, res);
|
|
|
|
// Gracefully close the stream
|
|
boost::system::error_code ec;
|
|
stream.shutdown(ec);
|
|
if(ec == boost::asio::error::eof)
|
|
{
|
|
// http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error
|
|
ec.assign(0, ec.category());
|
|
}
|
|
if(ec)
|
|
throw boost::system::system_error{ec};
|
|
|
|
std::string line;
|
|
std::stringstream body(res.body().data());
|
|
|
|
std::getline(body, line, '\n');
|
|
|
|
AegisubUpdateDescription version = ParseVersionString(line);
|
|
|
|
std::ostringstream desc;
|
|
while (std::getline(body, line, '\n')) {
|
|
desc << line;
|
|
}
|
|
|
|
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
|