// 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' : 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __APPLE__ #include #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 maj_min; std::vector 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 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 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 stream(ioc, ctx); if(! SSL_set_tlsext_host_name(stream.native_handle(), UPDATE_CHECKER_SERVER)) { boost::system::error_code ec{static_cast(::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 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 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