// 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 #ifdef __APPLE__ #include #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 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 << "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 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::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::max(), '\n'); // Skip the headers since we don't care about them for (auto const& header : agi::line_iterator(stream)) if (header.empty()) break; AegisubUpdateDescription version = AegisubUpdateDescription{0, 0, 0, "", ""}; std::ostringstream desc; for (auto const& line : agi::line_iterator(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