Aegisub/src/dialog_version_check.cpp

390 lines
12 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 {
std::string url;
std::string friendly_name;
std::string description;
};
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 std::vector<AegisubUpdateDescription> &updates);
bool ShouldPreventAppExit() const override { return false; }
};
VersionCheckerResultDialog::VersionCheckerResultDialog(wxString const& main_text, const std::vector<AegisubUpdateDescription> &updates)
: 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);
for (auto const& update : updates) {
main_sizer->Add(new wxStaticLine(this), 0, wxEXPAND|wxALL, 6);
text = new wxStaticText(this, -1, to_wx(update.friendly_name));
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);
main_sizer->Add(new wxHyperlinkCtrl(this, -1, to_wx(update.url), to_wx(update.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 (updates.size() > 0)
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 (updates.size())
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());
}
void DoCheck(bool interactive) {
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?rev=%d&rel=%d&os=%s&lang=%s&aegilang=%s HTTP/1.0\r\n"
"User-Agent: Aegisub %s\r\n"
"Host: %s\r\n"
"Accept: */*\r\n"
"Connection: close\r\n\r\n"
, UPDATE_CHECKER_BASE_URL
, GetSVNRevision()
, (GetIsOfficialRelease() ? 1 : 0)
, GetOSShortName()
, GetSystemLanguage()
, GetAegisubLanguage()
, GetAegisubLongVersionString()
, 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;
std::vector<AegisubUpdateDescription> results;
for (auto const& line : agi::line_iterator<std::string>(stream)) {
if (line.empty()) continue;
std::vector<std::string> parsed;
agi::Split(parsed, line, '|');
if (parsed.size() != 6) continue;
if (atoi(parsed[1].c_str()) <= GetSVNRevision())
continue;
// 0 and 2 being things that never got used
results.push_back(AegisubUpdateDescription{
inline_string_decode(parsed[3]),
inline_string_decode(parsed[4]),
inline_string_decode(parsed[5])
});
}
if (!results.empty() || interactive) {
agi::dispatch::Main().Async([=]{
wxString text;
if (results.size() == 1)
text = _("An update to Aegisub was found.");
else if (results.size() > 1)
text = _("Several possible updates to Aegisub were found.");
else
text = _("There are no updates to Aegisub.");
new VersionCheckerResultDialog(text, results);
});
}
}
}
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