#include "WebViewPanel.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/MainFrame.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/UserAccount.hpp" #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/WebView.hpp" #include "slic3r/GUI/WebViewPlatformUtils.hpp" #include "slic3r/GUI/MsgDialog.hpp" #include "slic3r/GUI/Field.hpp" #include "slic3r/Utils/Http.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/Config.hpp" #include // IWYU pragma: keep #include #include #include #include #include #include #include // if set to 1 the fetch() JS function gets override to include JWT in authorization header // if set to 0, the /slicer/login is invoked from WebKit (passing JWT token only to this request) // to set authorization cookie for all WebKit requests to Connect #define AUTH_VIA_FETCH_OVERRIDE 0 wxDEFINE_EVENT(EVT_PRINTABLES_CONNECT_PRINT, wxCommandEvent); namespace pt = boost::property_tree; namespace Slic3r::GUI { WebViewPanel::~WebViewPanel() { SetEvtHandlerEnabled(false); #ifdef DEBUG_URL_PANEL delete m_tools_menu; #endif } void WebViewPanel::destroy_browser() { if (!m_browser || m_do_late_webview_create) { return; } topsizer->Detach(m_browser); m_browser->Destroy(); m_browser = nullptr; } void WebViewPanel::load_url(const wxString& url) { if (!m_browser) return; this->on_page_will_load(); this->Show(); this->Raise(); #ifdef DEBUG_URL_PANEL m_url->SetLabelText(url); #endif wxString correct_url = url.empty() ? wxString("") : wxURI(url).BuildURI(); m_browser->LoadURL(correct_url); m_browser->SetFocus(); } WebViewPanel::WebViewPanel(wxWindow *parent, const wxString& default_url, const std::vector& message_handler_names, const std::string& loading_html, const std::string& error_html, bool do_create) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) , m_default_url (default_url) , m_loading_html(loading_html) , m_error_html(error_html) , m_script_message_hadler_names(message_handler_names) { topsizer = new wxBoxSizer(wxVERTICAL); m_sizer_top = new wxBoxSizer(wxHORIZONTAL); topsizer->Add(m_sizer_top, 0, wxEXPAND, 0); #ifdef DEBUG_URL_PANEL // Create the button bSizer_toolbar = new wxBoxSizer(wxHORIZONTAL); m_button_back = new wxButton(this, wxID_ANY, wxT("Back"), wxDefaultPosition, wxDefaultSize, 0); //m_button_back->Enable(false); bSizer_toolbar->Add(m_button_back, 0, wxALL, 5); m_button_forward = new wxButton(this, wxID_ANY, wxT("Forward"), wxDefaultPosition, wxDefaultSize, 0); //m_button_forward->Enable(false); bSizer_toolbar->Add(m_button_forward, 0, wxALL, 5); m_button_stop = new wxButton(this, wxID_ANY, wxT("Stop"), wxDefaultPosition, wxDefaultSize, 0); bSizer_toolbar->Add(m_button_stop, 0, wxALL, 5); m_button_reload = new wxButton(this, wxID_ANY, wxT("Reload"), wxDefaultPosition, wxDefaultSize, 0); bSizer_toolbar->Add(m_button_reload, 0, wxALL, 5); m_url = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); bSizer_toolbar->Add(m_url, 1, wxALL | wxEXPAND, 5); m_button_tools = new wxButton(this, wxID_ANY, wxT("Tools"), wxDefaultPosition, wxDefaultSize, 0); bSizer_toolbar->Add(m_button_tools, 0, wxALL, 5); // Create panel for find toolbar. wxPanel* panel = new wxPanel(this); topsizer->Add(bSizer_toolbar, 0, wxEXPAND, 0); topsizer->Add(panel, wxSizerFlags().Expand()); // Create sizer for panel. wxBoxSizer* panel_sizer = new wxBoxSizer(wxVERTICAL); panel->SetSizer(panel_sizer); // Create the info panel m_info = new wxInfoBar(this); topsizer->Add(m_info, wxSizerFlags().Expand()); #endif SetSizer(topsizer); Bind(wxEVT_SHOW, &WebViewPanel::on_show, this); Bind(wxEVT_IDLE, &WebViewPanel::on_idle, this); #ifdef DEBUG_URL_PANEL // Create the Tools menu m_tools_menu = new wxMenu(); wxMenuItem* viewSource = m_tools_menu->Append(wxID_ANY, "View Source"); wxMenuItem* viewText = m_tools_menu->Append(wxID_ANY, "View Text"); m_tools_menu->AppendSeparator(); wxMenu* script_menu = new wxMenu; m_script_custom = script_menu->Append(wxID_ANY, "Custom script"); m_tools_menu->AppendSubMenu(script_menu, "Run Script"); wxMenuItem* addUserScript = m_tools_menu->Append(wxID_ANY, "Add user script"); wxMenuItem* setCustomUserAgent = m_tools_menu->Append(wxID_ANY, "Set custom user agent"); m_context_menu = m_tools_menu->AppendCheckItem(wxID_ANY, "Enable Context Menu"); m_dev_tools = m_tools_menu->AppendCheckItem(wxID_ANY, "Enable Dev Tools"); // Connect the button events Bind(wxEVT_BUTTON, &WebViewPanel::on_back_button, this, m_button_back->GetId()); Bind(wxEVT_BUTTON, &WebViewPanel::on_forward_button, this, m_button_forward->GetId()); Bind(wxEVT_BUTTON, &WebViewPanel::on_stop_button, this, m_button_stop->GetId()); Bind(wxEVT_BUTTON, &WebViewPanel::on_reload_button, this, m_button_reload->GetId()); Bind(wxEVT_BUTTON, &WebViewPanel::on_tools_clicked, this, m_button_tools->GetId()); Bind(wxEVT_TEXT_ENTER, &WebViewPanel::on_url, this, m_url->GetId()); // Connect the menu events Bind(wxEVT_MENU, &WebViewPanel::on_view_source_request, this, viewSource->GetId()); Bind(wxEVT_MENU, &WebViewPanel::on_view_text_request, this, viewText->GetId()); Bind(wxEVT_MENU, &WebViewPanel::On_enable_context_menu, this, m_context_menu->GetId()); Bind(wxEVT_MENU, &WebViewPanel::On_enable_dev_tools, this, m_dev_tools->GetId()); Bind(wxEVT_MENU, &WebViewPanel::on_run_script_custom, this, m_script_custom->GetId()); Bind(wxEVT_MENU, &WebViewPanel::on_add_user_script, this, addUserScript->GetId()); #endif // Create the webview if (!do_create) { m_do_late_webview_create = true; return; } m_do_late_webview_create = false; late_create(); } void WebViewPanel::late_create() { m_do_late_webview_create = false; m_browser = WebView::webview_new(); if (!m_browser) { wxStaticText* text = new wxStaticText(this, wxID_ANY, _L("Failed to load a web browser.")); topsizer->Add(text, 0, wxALIGN_LEFT | wxBOTTOM, 10); return; } WebView::webview_create(m_browser,this, GUI::format_wxstr("file://%1%/web/%2%.html", boost::filesystem::path(resources_dir()).generic_string(), m_loading_html), m_script_message_hadler_names); if (Utils::ServiceConfig::instance().webdev_enabled()) { m_browser->EnableContextMenu(); m_browser->EnableAccessToDevTools(); } topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); // Connect the webview events Bind(wxEVT_WEBVIEW_ERROR, &WebViewPanel::on_error, this, m_browser->GetId()); Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewPanel::on_script_message, this, m_browser->GetId()); Bind(wxEVT_WEBVIEW_NAVIGATING, &WebViewPanel::on_navigation_request, this, m_browser->GetId()); Bind(wxEVT_WEBVIEW_LOADED, &WebViewPanel::on_loaded, this, m_browser->GetId()); Layout(); } void WebViewPanel::load_default_url_delayed() { assert(!m_default_url.empty()); m_load_default_url = true; } void WebViewPanel::load_error_page() { if (!m_browser || m_do_late_webview_create) { return; } m_browser->Stop(); m_load_error_page = true; } void WebViewPanel::on_show(wxShowEvent& evt) { m_shown = evt.IsShown(); if (!m_shown) { wxSetCursor(wxNullCursor); return; } if (m_do_late_webview_create) { m_do_late_webview_create = false; late_create(); return; } if (m_load_default_url) { m_load_default_url = false; load_default_url(); return; } if (m_after_show_func_prohibited_once) { m_after_show_func_prohibited_once = false; return; } after_on_show(evt); } void WebViewPanel::on_idle(wxIdleEvent& WXUNUSED(evt)) { if (!m_browser || m_do_late_webview_create) return; // The busy cursor on webview is switched off on Linux. // Because m_browser->IsBusy() is almost always true on Printables / Connect. #ifndef __linux__ if (m_shown) { if (m_browser->IsBusy()) { wxSetCursor(wxCURSOR_ARROWWAIT); } else { wxSetCursor(wxNullCursor); } } #endif // !__linux__ if (m_shown && m_load_error_page && !m_browser->IsBusy()) { m_load_error_page = false; if (m_load_default_url_on_next_error) { m_load_default_url_on_next_error = false; load_default_url(); } else { load_url(GUI::format_wxstr("file://%1%/web/%2%.html", boost::filesystem::path(resources_dir()).generic_string(), m_error_html)); // This is a fix of broken message handling after error. // F.e. if there is an error but we do AddUserScript & Reload, the handling will break. // So we just reset the handler here. if (!m_script_message_hadler_names.empty()) { m_browser->RemoveScriptMessageHandler(Slic3r::GUI::from_u8(m_script_message_hadler_names.front())); m_browser->AddScriptMessageHandler(Slic3r::GUI::from_u8(m_script_message_hadler_names.front())); } } } #ifdef DEBUG_URL_PANEL m_button_stop->Enable(m_browser->IsBusy()); #endif } void WebViewPanel::on_loaded(wxWebViewEvent& evt) { if (evt.GetURL().IsEmpty()) return; if (evt.GetURL().StartsWith(m_default_url)) { define_css(); } else { m_styles_defined = false; } m_load_default_url_on_next_error = false; if (evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND && m_load_default_url) { m_load_default_url = false; load_default_url(); } } /** * Callback invoked when user entered an URL and pressed enter */ void WebViewPanel::on_url(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; #ifdef DEBUG_URL_PANEL m_browser->LoadURL(m_url->GetValue()); m_browser->SetFocus(); #endif } /** * Callback invoked when user pressed the "back" button */ void WebViewPanel::on_back_button(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; if (!m_browser->CanGoBack()) return; m_browser->GoBack(); } /** * Callback invoked when user pressed the "forward" button */ void WebViewPanel::on_forward_button(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; if (!m_browser->CanGoForward()) return; m_browser->GoForward(); } /** * Callback invoked when user pressed the "stop" button */ void WebViewPanel::on_stop_button(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; m_browser->Stop(); } /** * Callback invoked when user pressed the "reload" button */ void WebViewPanel::on_reload_button(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; m_browser->Reload(); } void WebViewPanel::on_script_message(wxWebViewEvent& evt) { BOOST_LOG_TRIVIAL(error) << "unhandled script message: " << evt.GetString(); } void WebViewPanel::on_navigation_request(wxWebViewEvent &evt) { } void WebViewPanel::on_page_will_load() { } /** * Invoked when user selects the "View Source" menu item */ void WebViewPanel::on_view_source_request(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; SourceViewDialog dlg(this, m_browser->GetPageSource()); dlg.ShowModal(); } /** * Invoked when user selects the "View Text" menu item */ void WebViewPanel::on_view_text_request(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; wxDialog textViewDialog(this, wxID_ANY, "Page Text", wxDefaultPosition, wxSize(700, 500), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER); wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, m_browser->GetPageText(), wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_RICH | wxTE_READONLY); wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); sizer->Add(text, 1, wxEXPAND); SetSizer(sizer); textViewDialog.ShowModal(); } /** * Invoked when user selects the "Menu" item */ void WebViewPanel::on_tools_clicked(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; #ifdef DEBUG_URL_PANEL m_context_menu->Check(m_browser->IsContextMenuEnabled()); m_dev_tools->Check(m_browser->IsAccessToDevToolsEnabled()); wxPoint position = ScreenToClient(wxGetMousePosition()); PopupMenu(m_tools_menu, position.x, position.y); #endif } void WebViewPanel::run_script(const wxString& javascript) { if (!m_browser || !m_shown) return; // Remember the script we run in any case, so the next time the user opens // the "Run Script" dialog box, it is shown there for convenient updating. m_javascript = javascript; BOOST_LOG_TRIVIAL(trace) << "RunScript " << javascript << "\n"; m_browser->RunScriptAsync(javascript); } void WebViewPanel::on_run_script_custom(wxCommandEvent& WXUNUSED(evt)) { wxTextEntryDialog dialog ( this, "Please enter JavaScript code to execute", wxGetTextFromUserPromptStr, m_javascript, wxOK | wxCANCEL | wxCENTRE | wxTE_MULTILINE ); if (dialog.ShowModal() != wxID_OK) return; run_script(dialog.GetValue()); } void WebViewPanel::on_add_user_script(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) { return; } wxString userScript = "window.wx_test_var = 'wxWidgets webview sample';"; wxTextEntryDialog dialog ( this, "Enter the JavaScript code to run as the initialization script that runs before any script in the HTML document.", wxGetTextFromUserPromptStr, userScript, wxOK | wxCANCEL | wxCENTRE | wxTE_MULTILINE ); if (dialog.ShowModal() != wxID_OK) return; const wxString& javascript = dialog.GetValue(); BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; if (!m_browser->AddUserScript(javascript)) wxLogError("Could not add user script"); } void WebViewPanel::on_set_custom_user_agent(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; wxString customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Mobile/15E148 Safari/604.1"; wxTextEntryDialog dialog ( this, "Enter the custom user agent string you would like to use.", wxGetTextFromUserPromptStr, customUserAgent, wxOK | wxCANCEL | wxCENTRE ); if (dialog.ShowModal() != wxID_OK) return; if (!m_browser->SetUserAgent(customUserAgent)) wxLogError("Could not set custom user agent"); } void WebViewPanel::on_clear_selection(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; m_browser->ClearSelection(); } void WebViewPanel::on_delete_selection(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; m_browser->DeleteSelection(); } void WebViewPanel::on_select_all(wxCommandEvent& WXUNUSED(evt)) { if (!m_browser) return; m_browser->SelectAll(); } void WebViewPanel::On_enable_context_menu(wxCommandEvent& evt) { if (!m_browser) return; m_browser->EnableContextMenu(evt.IsChecked()); } void WebViewPanel::On_enable_dev_tools(wxCommandEvent& evt) { if (!m_browser) return; m_browser->EnableAccessToDevTools(evt.IsChecked()); } /** * Callback invoked when a loading error occurs */ void WebViewPanel::on_error(wxWebViewEvent& evt) { #define WX_ERROR_CASE(type) \ case type: \ category = #type; \ break; wxString category; switch (evt.GetInt()) { WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_CONNECTION); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_CERTIFICATE); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_AUTH); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_SECURITY); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_NOT_FOUND); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_REQUEST); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_USER_CANCELLED); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_OTHER); } BOOST_LOG_TRIVIAL(error) << this <<" WebViewPanel error: " << category << " url: " << evt.GetURL(); load_error_page(); #ifdef DEBUG_URL_PANEL m_info->ShowMessage(wxString("An error occurred loading ") + evt.GetURL() + "\n" + "'" + category + "'", wxICON_ERROR); #endif } void WebViewPanel::do_reload() { if (!m_browser) { return; } // IsBusy on Linux very often returns true due to loading about:blank after loading requested url. #ifndef __linux__ if (m_browser->IsBusy()) { return; } #endif const wxString current_url = m_browser->GetCurrentURL(); if (current_url.StartsWith(m_default_url)) { m_browser->Reload(); return; } load_default_url(); } void WebViewPanel::load_default_url() { if (!m_browser || m_do_late_webview_create) { return; } m_styles_defined = false; load_url(m_default_url); } void WebViewPanel::sys_color_changed() { #ifdef _WIN32 wxGetApp().UpdateDarkUI(this); #endif } void WebViewPanel::on_app_quit_event(const std::string& message_data) { // MacOS only suplement for cmd+Q if (wxGetApp().mainframe) { wxGetApp().mainframe->Close(); } } void WebViewPanel::on_app_minimize_event(const std::string& message_data) { // MacOS only suplement for cmd+M wxGetApp().mainframe->Iconize(true); } void WebViewPanel::define_css() { assert(false); } ConnectWebViewPanel::ConnectWebViewPanel(wxWindow* parent) : WebViewPanel(parent, GUI::from_u8(Utils::ServiceConfig::instance().connect_url()), { "_prusaSlicer" }, "connect_loading", "connect_error", false) { auto* plater = wxGetApp().plater(); plater->Bind(EVT_UA_LOGGEDOUT, &ConnectWebViewPanel::on_user_logged_out, this); plater->Bind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this); plater->Bind(EVT_UA_ID_USER_SUCCESS_AFTER_TOKEN_SUCCESS, &ConnectWebViewPanel::on_user_token, this); m_actions["appQuit"] = std::bind(&WebViewPanel::on_app_quit_event, this, std::placeholders::_1); m_actions["appMinimize"] = std::bind(&WebViewPanel::on_app_minimize_event, this, std::placeholders::_1); m_actions["reloadHomePage"] = std::bind(&ConnectWebViewPanel::on_reload_event, this, std::placeholders::_1); } void ConnectWebViewPanel::late_create() { WebViewPanel::late_create(); if (!m_browser) { return; } // This code used to be inside plater->Bind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this) auto access_token = wxGetApp().plater()->get_user_account()->get_access_token(); assert(!access_token.empty()); wxString javascript = get_login_script(true); BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; m_browser->RunScriptAsync(javascript); resend_config(); } void ConnectWebViewPanel::on_user_token(UserAccountSuccessEvent& e) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; e.Skip(); if (!m_browser) { return; } auto access_token = wxGetApp().plater()->get_user_account()->get_access_token(); assert(!access_token.empty()); wxString javascript = get_login_script(true); BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; m_browser->RunScriptAsync(javascript); resend_config(); } ConnectWebViewPanel::~ConnectWebViewPanel() { } wxString ConnectWebViewPanel::get_login_script(bool refresh) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; Plater* plater = wxGetApp().plater(); const std::string& access_token = plater->get_user_account()->get_access_token(); assert(!access_token.empty()); auto javascript = wxString::Format( #if AUTH_VIA_FETCH_OVERRIDE refresh ? "window.__access_token = '%s';window.__access_token_version = (window.__access_token_version || 0) + 1;console.log('Updated Auth token', window.__access_token);" : /* * Notes: * - The fetch() function has two distinct prototypes (i.e. input args): * 1. fetch(url: string, options: object | undefined) * 2. fetch(req: Request, options: object | undefined) * - For some reason I can't explain the headers can be extended only via Request object * i.e. the fetch prototype (2). So we need to convert (1) call into (2) before * */ R"( if (window.__fetch === undefined) { window.__fetch = fetch; window.fetch = function(req, opts = {}) { if (typeof req === 'string') { req = new Request(req, opts); opts = {}; } if (window.__access_token && (req.url[0] == '/' || req.url.indexOf('prusa3d.com') > 0)) { req.headers.set('Authorization', 'Bearer ' + window.__access_token); console.log('Header updated: ', req.headers.get('Authorization')); console.log('AT Version: ', __access_token_version); } //console.log('Injected fetch used', req, opts); return __fetch(req, opts); }; } window.__access_token = '%s'; window.__access_token_version = 0; )", #else refresh ? R"( if (location.protocol === 'https:') { if (window._prusaSlicer_initLogin !== undefined) { console.log('Init login'); if (window._prusaSlicer !== undefined) _prusaSlicer.postMessage({action: 'LOG', message: 'Refreshing login'}); _prusaSlicer_initLogin('%s'); } else { console.log('Refreshing login skipped as no _prusaSlicer_login defined (yet?)'); if (window._prusaSlicer === undefined) { console.log('Message handler _prusaSlicer not defined yet'); } else { _prusaSlicer.postMessage({action: 'LOG', message: 'Refreshing login skipped as no _prusaSlicer_initLogin defined (yet?)'}); } } } )" : R"( function _prusaSlicer_log(msg) { console.log(msg); if (window._prusaSlicer !== undefined) _prusaSlicer.postMessage({action: 'LOG', message: msg}); } function _prusaSlicer_errorHandler(err) { const msg = { action: 'ERROR', error: typeof(err) === 'string' ? err : JSON.stringify(err), critical: false }; console.error('Login error occurred', msg); window._prusaSlicer.postMessage(msg); }; function _prusaSlicer_delay(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms); }); } async function _prusaSlicer_initLogin(token) { const parts = token.split('.'); const claims = JSON.parse(atob(parts[1])); const now = new Date().getTime() / 1000; if (claims.exp <= now) { _prusaSlicer_log('Skipping initLogin as token is expired'); return; } let retry = false; let backoff = 1000; const maxBackoff = 64000 * 4; const maxRetries = 16; let numRetries = 0; do { let error = false; try { _prusaSlicer_log('Slicer Login request ' + token.substring(token.length - 8)); let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer ' + token}}); let body = await resp.text(); _prusaSlicer_log('Slicer Login resp ' + resp.status + ' (' + token.substring(token.length - 8) + ') body: ' + body); if (resp.status >= 500 || resp.status == 408) { numRetries++; retry = maxRetries <= 0 || numRetries <= maxRetries; } else { retry = false; if (resp.status >= 400) _prusaSlicer_errorHandler({status: resp.status, body}); } } catch (e) { _prusaSlicer_log('Slicer Login failed: ' + e.toString()); console.error('Slicer Login failed', e.toString()); // intentionally not taking care about max retry count, as this is not server error but likely being offline retry = true; } if (retry) { await _prusaSlicer_delay(backoff + 1000 * Math.random()); if (backoff < maxBackoff) { backoff *= 2; } } } while (retry); } if (location.protocol === 'https:' && window._prusaSlicer) { _prusaSlicer_log('Requesting login'); _prusaSlicer.postMessage({action: 'REQUEST_LOGIN'}); } )", #endif access_token ); return javascript; } wxString ConnectWebViewPanel::get_logout_script() { return "sessionStorage.removeItem('_slicer_token');"; } void ConnectWebViewPanel::on_page_will_load() { if (!m_browser) { return; } BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; auto javascript = get_login_script(false); BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; m_browser->AddUserScript(javascript); } void ConnectWebViewPanel::on_user_logged_out(UserAccountSuccessEvent& e) { e.Skip(); if (!m_browser) return; // clear token from session storage m_browser->RunScriptAsync(get_logout_script()); } void ConnectWebViewPanel::on_script_message(wxWebViewEvent& evt) { BOOST_LOG_TRIVIAL(debug) << "received message from Prusa Connect FE: " << evt.GetString(); handle_message(into_u8(evt.GetString())); } void ConnectWebViewPanel::on_navigation_request(wxWebViewEvent &evt) { #ifdef DEBUG_URL_PANEL m_url->SetValue(evt.GetURL()); #endif BOOST_LOG_TRIVIAL(debug) << "Navigation requested to: " << into_u8(evt.GetURL()); // we need to do this to redefine css when reload is hit if (evt.GetURL().StartsWith(m_default_url) && evt.GetURL() == m_browser->GetCurrentURL()) { m_styles_defined = false; } if (evt.GetURL() == m_default_url) { m_reached_default_url = true; return; } if (evt.GetURL() == (GUI::format_wxstr("file:///%1%/web/connection_failed.html", boost::filesystem::path(resources_dir()).generic_string()))) { return; } if (m_reached_default_url && !evt.GetURL().StartsWith(m_default_url)) { BOOST_LOG_TRIVIAL(info) << evt.GetURL() << " does not start with default url. Vetoing."; evt.Veto(); } else if (m_reached_default_url && evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND) { // Do not allow back button to loading screen evt.Veto(); } } void ConnectWebViewPanel::on_connect_action_error(const std::string &message_data) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; ConnectRequestHandler::on_connect_action_error(message_data); // TODO: make this more user friendly (and make sure only once opened if multiple errors happen) // MessageDialog dialog( // this, // GUI::format_wxstr(_L("WebKit Runtime Error encountered:\n\n%s"), message_data), // "WebKit Runtime Error", // wxOK // ); // dialog.ShowModal(); } void ConnectWebViewPanel::on_reload_event(const std::string& message_data) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; // Event from our error page button or keyboard shortcut m_styles_defined = false; try { std::stringstream ss(message_data); pt::ptree ptree; pt::read_json(ss, ptree); if (const auto keyboard = ptree.get_optional("fromKeyboard"); keyboard && *keyboard) { do_reload(); } else { // On error page do load of default url. load_default_url(); } } catch (const std::exception &e) { BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); return; } } void ConnectWebViewPanel::after_on_show(wxShowEvent& evt) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; run_script("window.location.reload();"); } void ConnectWebViewPanel::logout() { if (!m_browser || m_do_late_webview_create) { return; } wxString script = L"window._prusaConnect_v2.logout()"; run_script(script); Plater* plater = wxGetApp().plater(); auto javascript = wxString::Format( R"( console.log('Preparing logout'); window.fetch('/slicer/logout', {method: 'POST', headers: {Authorization: 'Bearer %s'}}) .then(function (resp){ console.log('Logout resp', resp); resp.text().then(function (json) { console.log('Logout resp body', json) }); }); )", plater->get_user_account()->get_access_token() ); BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; m_browser->RunScript(javascript); } void ConnectWebViewPanel::sys_color_changed() { resend_config(); } void ConnectWebViewPanel::on_connect_action_request_login(const std::string &message_data) { run_script_bridge(get_login_script(true)); } void ConnectWebViewPanel::on_connect_action_select_printer(const std::string& message_data) { assert(!message_data.empty()); wxGetApp().handle_connect_request_printer_select(message_data); } void ConnectWebViewPanel::on_connect_action_print(const std::string& message_data) { // PRINT request is not defined for ConnectWebViewPanel assert(false); } void ConnectWebViewPanel::define_css() { if (m_styles_defined) { return; } m_styles_defined = true; BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; #if defined(__APPLE__) // WebView on Windows does read keyboard shortcuts // Thus doing f.e. Reload twice would make the oparation to fail std::string script = R"( document.addEventListener('keydown', function (event) { if (event.key === 'F5' || (event.ctrlKey && event.key === 'r') || (event.metaKey && event.key === 'r')) { window.webkit.messageHandlers._prusaSlicer.postMessage(JSON.stringify({ action: 'reloadHomePage', fromKeyboard: 1})); } if (event.metaKey && event.key === 'q') { window.webkit.messageHandlers._prusaSlicer.postMessage(JSON.stringify({ action: 'appQuit'})); } if (event.metaKey && event.key === 'm') { window.webkit.messageHandlers._prusaSlicer.postMessage(JSON.stringify({ action: 'appMinimize'})); } }); )"; run_script(script); #endif // defined(__APPLE__) } PrinterWebViewPanel::PrinterWebViewPanel(wxWindow* parent, const wxString& default_url) : WebViewPanel(parent, default_url, {"ExternalApp"}, "other_loading", "other_error", false) { m_events["reloadHomePage"] = std::bind(&PrinterWebViewPanel::on_reload_event, this, std::placeholders::_1); m_events["appQuit"] = std::bind(&WebViewPanel::on_app_quit_event, this, std::placeholders::_1); m_events["appMinimize"] = std::bind(&WebViewPanel::on_app_minimize_event, this, std::placeholders::_1); } void PrinterWebViewPanel::on_navigation_request(wxWebViewEvent &evt) { const wxString url = evt.GetURL(); if (url.StartsWith(m_default_url)) { m_reached_default_url = true; if (url == m_browser->GetCurrentURL()) { // we need to do this to redefine css when reload is hit m_styles_defined = false; } if ( !m_api_key_sent) { if (!m_usr.empty() && !m_psk.empty()) { send_credentials(); } } } else if (m_reached_default_url && evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND) { // Do not allow back button to loading screen evt.Veto(); } } void PrinterWebViewPanel::on_loaded(wxWebViewEvent& evt) { if (evt.GetURL().IsEmpty()) return; if (evt.GetURL().StartsWith(m_default_url)) { define_css(); } else { m_styles_defined = false; } m_load_default_url_on_next_error = false; if (evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND && m_load_default_url) { m_load_default_url = false; load_default_url(); return; } if (!m_api_key.empty()) { send_api_key(); } } void PrinterWebViewPanel::on_script_message(wxWebViewEvent& evt) { BOOST_LOG_TRIVIAL(debug) << "received message from Physical printer page: " << evt.GetString(); handle_message(into_u8(evt.GetString())); } void PrinterWebViewPanel::handle_message(const std::string& message) { std::string event_string; try { std::stringstream ss(message); pt::ptree ptree; pt::read_json(ss, ptree); if (const auto action = ptree.get_optional("event"); action) { event_string = *action; } } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); return; } if (event_string.empty()) { BOOST_LOG_TRIVIAL(error) << "Received invalid message from printables (missing event). Message: " << message; return; } assert(m_events.find(event_string) != m_events.end()); // this assert means there is an event that has no handling. if (m_events.find(event_string) != m_events.end()) { m_events[event_string](message); } } void PrinterWebViewPanel::send_api_key() { if (!m_browser || m_api_key_sent) return; m_api_key_sent = true; wxString key = from_u8(m_api_key); wxString script = wxString::Format(R"( // Check if window.fetch exists before overriding if (window.originalFetch === undefined) { console.log('Patching fetch with API key'); window.originalFetch = window.fetch; window.fetch = function(input, init = {}) { init.headers = init.headers || {}; init.headers['X-Api-Key'] = sessionStorage.getItem('apiKey'); console.log('Patched fetch', input, init); return window.originalFetch(input, init); }; } sessionStorage.setItem('authType', 'ApiKey'); sessionStorage.setItem('apiKey', '%s'); )", key); m_browser->RemoveAllUserScripts(); BOOST_LOG_TRIVIAL(debug) << "RunScript " << script << "\n"; m_browser->AddUserScript(script); m_browser->Reload(); remove_webview_credentials(m_browser); } void PrinterWebViewPanel::send_credentials() { if (!m_browser || m_api_key_sent) return; m_browser->RemoveAllUserScripts(); m_browser->AddUserScript("sessionStorage.removeItem('authType'); sessionStorage.removeItem('apiKey'); console.log('Session Storage cleared');"); // reload would be done only if called from on_loaded //m_browser->Reload(); m_api_key_sent = true; setup_webview_with_credentials(m_browser, m_usr, m_psk); } void PrinterWebViewPanel::define_css() { if (m_styles_defined) { return; } m_styles_defined = true; BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; #if defined(__APPLE__) // WebView on Windows does read keyboard shortcuts // Thus doing f.e. Reload twice would make the oparation to fail std::string script = R"( document.addEventListener('keydown', function (event) { if (event.key === 'F5' || (event.ctrlKey && event.key === 'r') || (event.metaKey && event.key === 'r')) { window.webkit.messageHandlers.ExternalApp.postMessage(JSON.stringify({ event: 'reloadHomePage', fromKeyboard: 1})); } if (event.metaKey && event.key === 'q') { window.webkit.messageHandlers.ExternalApp.postMessage(JSON.stringify({ event: 'appQuit'})); } if (event.metaKey && event.key === 'm') { window.webkit.messageHandlers.ExternalApp.postMessage(JSON.stringify({ event: 'appMinimize'})); } }); )"; run_script(script); #endif // defined(__APPLE__) } void PrinterWebViewPanel::on_reload_event(const std::string& message_data) { // Event from our error page button or keyboard shortcut m_styles_defined = false; try { std::stringstream ss(message_data); pt::ptree ptree; pt::read_json(ss, ptree); if (const auto keyboard = ptree.get_optional("fromKeyboard"); keyboard && *keyboard) { do_reload(); } else { // On error page do load of default url. load_default_url(); } } catch (const std::exception &e) { BOOST_LOG_TRIVIAL(error) << "Could not parse message. " << e.what(); return; } } PrintablesWebViewPanel::PrintablesWebViewPanel(wxWindow* parent) : WebViewPanel(parent, GUI::from_u8(Utils::ServiceConfig::instance().printables_url()), { "ExternalApp" }, "other_loading", "other_error", false) { m_events["accessTokenExpired"] = std::bind(&PrintablesWebViewPanel::on_printables_event_access_token_expired, this, std::placeholders::_1); m_events["printGcode"] = std::bind(&PrintablesWebViewPanel::on_printables_event_print_gcode, this, std::placeholders::_1); m_events["downloadFile"] = std::bind(&PrintablesWebViewPanel::on_printables_event_download_file, this, std::placeholders::_1); m_events["sliceFile"] = std::bind(&PrintablesWebViewPanel::on_printables_event_slice_file, this, std::placeholders::_1); m_events["requiredLogin"] = std::bind(&PrintablesWebViewPanel::on_printables_event_required_login, this, std::placeholders::_1); m_events["openExternalUrl"] = std::bind(&PrintablesWebViewPanel::on_printables_event_open_url, this, std::placeholders::_1); m_events["reloadHomePage"] = std::bind(&PrintablesWebViewPanel::on_reload_event, this, std::placeholders::_1); m_events["appQuit"] = std::bind(&WebViewPanel::on_app_quit_event, this, std::placeholders::_1); m_events["appMinimize"] = std::bind(&WebViewPanel::on_app_minimize_event, this, std::placeholders::_1); m_events["ready"] = std::bind(&PrintablesWebViewPanel::on_printables_event_dummy, this, std::placeholders::_1); } void PrintablesWebViewPanel::handle_message(const std::string& message) { std::string event_string; try { std::stringstream ss(message); pt::ptree ptree; pt::read_json(ss, ptree); if (const auto action = ptree.get_optional("event"); action) { event_string = *action; } } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); return; } if (event_string.empty()) { BOOST_LOG_TRIVIAL(error) << "Received invalid message from printables (missing event). Message: " << message; return; } assert(m_events.find(event_string) != m_events.end()); // this assert means there is an event that has no handling. if (m_events.find(event_string) != m_events.end()) { m_events[event_string](message); } } void PrintablesWebViewPanel::on_navigation_request(wxWebViewEvent &evt) { const wxString url = evt.GetURL(); if (url.StartsWith(m_default_url)) { m_reached_default_url = true; if (url == m_browser->GetCurrentURL()) { // we need to do this to redefine css when reload is hit m_styles_defined = false; } } else if (m_reached_default_url && url.StartsWith("http")) { BOOST_LOG_TRIVIAL(info) << evt.GetURL() << " does not start with default url. Vetoing."; evt.Veto(); } else if (m_reached_default_url && evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND) { // Do not allow back button to loading screen evt.Veto(); } } wxString PrintablesWebViewPanel::get_default_url() const { return GUI::from_u8(get_url_lang_theme(GUI::from_u8(Utils::ServiceConfig::instance().printables_url() + "/homepage"))); } void PrintablesWebViewPanel::on_loaded(wxWebViewEvent& evt) { if (evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND && m_load_default_url) { m_load_default_url = false; load_default_url(); return; } if (evt.GetURL().StartsWith(m_default_url)) { define_css(); } else { m_styles_defined = false; } #ifdef _WIN32 // This is needed only once after add_request_authorization if (m_remove_request_auth) { m_remove_request_auth = false; remove_request_authorization(m_browser); } #endif m_load_default_url_on_next_error = false; } std::string PrintablesWebViewPanel::get_url_lang_theme(const wxString& url) const { // situations and reaction: // 1) url is just a path (no query no fragment) -> query with lang and theme is added // 2) url has query that contains lang and theme -> query and lang values are modified // 3) url has query with just one of lang or theme -> query is modified and missing value is added // 4) url has query of query and fragment without lang and theme -> query with lang and theme is added to the end of query std::string url_string = into_u8(url); std::string theme = wxGetApp().dark_mode() ? "dark" : "light"; wxString language = GUI::wxGetApp().current_language_code(); if (language.size() > 2) language = language.SubString(0, 1); // Replace lang and theme if already in url bool lang_found = false; std::regex lang_regex(R"((lang=)[^&#]*)"); if (std::regex_search(url_string, lang_regex)) { url_string = std::regex_replace(url_string, lang_regex, "$1" + into_u8(language)); lang_found = true; } bool theme_found = false; std::regex theme_regex(R"((theme=)[^&#]*)"); if (std::regex_search(url_string, theme_regex)) { url_string = std::regex_replace(url_string, theme_regex, "$1" + theme); theme_found = true; } if (lang_found && theme_found) return url_string; // missing params string std::string new_params = lang_found ? GUI::format("theme=%1%", theme) : theme_found ? GUI::format("lang=%1%", language) : GUI::format("lang=%1%&theme=%2%", language, theme); // Regex to capture query and optional fragment std::regex query_regex(R"((\?.*?)(#.*)?$)"); if (std::regex_search(url_string, query_regex)) { // Append params before the fragment (if it exists) return std::regex_replace(url_string, query_regex, "$1&" + new_params + "$2"); } std::regex fragment_regex(R"(#.*$)"); if (std::regex_search(url_string, fragment_regex)) { // Add params before the fragment return std::regex_replace(url_string, fragment_regex, "?" + new_params + "$&"); } return url_string + "?" + new_params; } void PrintablesWebViewPanel::after_on_show(wxShowEvent& evt) { // in case login changed, resend login / logout // DK1: it seems to me, it is safer to do login / logout (where logout means requesting the page again) // on every show of panel, // than to keep information if we have printables page in same state as slicer in terms of login // But im afraid it will be concidered not pretty... const std::string access_token = wxGetApp().plater()->get_user_account()->get_access_token(); if (access_token.empty()) { logout(m_next_show_url); } else { login(access_token, m_next_show_url); } m_next_show_url.clear(); } void PrintablesWebViewPanel::logout(const std::string& override_url/* = std::string()*/) { if (!m_shown || !m_browser) { return; } BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; hide_loading_overlay(); m_styles_defined = false; delete_cookies(m_browser, Utils::ServiceConfig::instance().printables_url()); m_browser->RunScript("localStorage.clear();"); std::string next_url = override_url.empty() ? get_url_lang_theme(m_browser->GetCurrentURL()) : get_url_lang_theme(from_u8(override_url)); #ifdef _WIN32 load_url(GUI::from_u8(next_url)); #else // We cannot do simple reload here, it would keep the access token in the header load_request(m_browser, next_url, std::string()); #endif // } void PrintablesWebViewPanel::login(const std::string& access_token, const std::string& override_url/* = std::string()*/) { if (!m_shown) { return; } BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; hide_loading_overlay(); m_styles_defined = false; // We cannot add token to header as when making the first request. // In fact, we shall not do request here, only run scripts. // postMessage accessTokenWillChange -> postMessage accessTokenChange -> window.location.reload(); wxString script = "window.postMessage(JSON.stringify({ event: 'accessTokenWillChange' }))"; run_script(script); script = GUI::format_wxstr("window.postMessage(JSON.stringify({" "event: 'accessTokenChange'," "token: '%1%'" "}));" , access_token); run_script(script); if ( override_url.empty()) { run_script("window.location.reload();"); } else { load_url(GUI::from_u8(get_url_lang_theme(from_u8(override_url)))); } } void PrintablesWebViewPanel::load_default_url() { if (!m_browser) { return; } hide_loading_overlay(); m_styles_defined = false; std::string actual_default_url = get_url_lang_theme(from_u8(Utils::ServiceConfig::instance().printables_url() + "/homepage")); const std::string access_token = wxGetApp().plater()->get_user_account()->get_access_token(); // in case of opening printables logged out - delete cookies and localstorage to get rid of last login if (access_token.empty()) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " logout"; delete_cookies(m_browser, Utils::ServiceConfig::instance().printables_url()); m_browser->AddUserScript("localStorage.clear();"); load_url(actual_default_url); return; } BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " login"; // add token to first request #ifdef _WIN32 add_request_authorization(m_browser, m_default_url, access_token); m_remove_request_auth = true; load_url(GUI::from_u8(actual_default_url)); #else load_request(m_browser, actual_default_url, access_token); #endif } void PrintablesWebViewPanel::send_refreshed_token(const std::string& access_token) { if (m_load_default_url) { return; } BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; hide_loading_overlay(); wxString script = GUI::format_wxstr("window.postMessage(JSON.stringify({" "event: 'accessTokenChange'," "token: '%1%'" "}));" , access_token); run_script(script); } void PrintablesWebViewPanel::send_will_refresh() { if (m_load_default_url) { return; } BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; wxString script = "window.postMessage(JSON.stringify({ event: 'accessTokenWillChange' }))"; run_script(script); } void PrintablesWebViewPanel::on_script_message(wxWebViewEvent& evt) { BOOST_LOG_TRIVIAL(debug) << "received message from Printables: " << evt.GetString(); handle_message(into_u8(evt.GetString())); } void PrintablesWebViewPanel::sys_color_changed() { if (m_shown && m_browser) { load_url(GUI::from_u8(get_url_lang_theme(m_browser->GetCurrentURL()))); } WebViewPanel::sys_color_changed(); } void PrintablesWebViewPanel::on_printables_event_access_token_expired(const std::string& message_data) { // { "event": "accessTokenExpired") // There seems to be a situation where we get accessTokenExpired when there is active token from Slicer POW // We need get new token and freeze webview until its not refreshed if (m_refreshing_token) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " already refreshing"; return; } BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; m_refreshing_token = true; show_loading_overlay(); wxGetApp().plater()->get_user_account()->request_refresh(); } void PrintablesWebViewPanel::on_reload_event(const std::string& message_data) { // Event from our error page button or keyboard shortcut m_styles_defined = false; try { std::stringstream ss(message_data); pt::ptree ptree; pt::read_json(ss, ptree); if (const auto keyboard = ptree.get_optional("fromKeyboard"); keyboard && *keyboard) { do_reload(); } else { // On error page do load of default url. load_default_url(); } } catch (const std::exception &e) { BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); return; } } namespace { std::string escape_url(const std::string& unescaped) { std::string ret_val; CURL* curl = curl_easy_init(); if (curl) { char* decoded = curl_easy_escape(curl, unescaped.c_str(), unescaped.size()); if (decoded) { ret_val = std::string(decoded); curl_free(decoded); } curl_easy_cleanup(curl); } return ret_val; } } void PrintablesWebViewPanel::on_printables_event_print_gcode(const std::string& message_data) { // { "event": "downloadFile", "url": "https://media.printables.com/somesecure.stl", "modelUrl": "https://www.printables.com/model/123" } std::string download_url; std::string model_url; try { std::stringstream ss(message_data); pt::ptree ptree; pt::read_json(ss, ptree); if (const auto url = ptree.get_optional("url"); url) { download_url = *url; } if (const auto url = ptree.get_optional("modelUrl"); url) { model_url = *url; } } catch (const std::exception &e) { BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); return; } assert(!download_url.empty() && !model_url.empty()); wxCommandEvent* evt = new wxCommandEvent(EVT_PRINTABLES_CONNECT_PRINT); evt->SetString(from_u8(Utils::ServiceConfig::instance().connect_printables_print_url() +"?url=" + escape_url(download_url))); wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, evt); } void PrintablesWebViewPanel::on_printables_event_download_file(const std::string& message_data) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << message_data; // { "event": "printGcode", "url": "https://media.printables.com/somesecure.gcode", "modelUrl": "https://www.printables.com/model/123" } std::string download_url; std::string model_url; try { std::stringstream ss(message_data); pt::ptree ptree; pt::read_json(ss, ptree); if (const auto url = ptree.get_optional("url"); url) { download_url = *url; } if (const auto url = ptree.get_optional("modelUrl"); url) { model_url = *url; } } catch (const std::exception &e) { BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); return; } assert(!download_url.empty() && !model_url.empty()); boost::filesystem::path url_path(download_url); show_download_notification(url_path.filename().string()); wxGetApp().printables_download_request(download_url, model_url); } void PrintablesWebViewPanel::on_printables_event_slice_file(const std::string& message_data) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << message_data; // { "event": "sliceFile", "url": "https://media.printables.com/somesecure.zip", "modelUrl": "https://www.printables.com/model/123" } std::string download_url; std::string model_url; try { std::stringstream ss(message_data); pt::ptree ptree; pt::read_json(ss, ptree); if (const auto url = ptree.get_optional("url"); url) { download_url = *url; } if (const auto url = ptree.get_optional("modelUrl"); url) { model_url = *url; } } catch (const std::exception &e) { BOOST_LOG_TRIVIAL(error) << "Could not parse printables message. " << e.what(); return; } assert(!download_url.empty() && !model_url.empty()); wxGetApp().printables_slice_request(download_url, model_url); } void PrintablesWebViewPanel::on_printables_event_required_login(const std::string& message_data) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << message_data; wxGetApp().printables_login_request(); } void PrintablesWebViewPanel::on_printables_event_open_url(const std::string& message_data) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << message_data; try { std::stringstream ss(message_data); pt::ptree ptree; pt::read_json(ss, ptree); if (const auto url = ptree.get_optional("url"); url) { wxGetApp().open_browser_with_warning_dialog(GUI::from_u8(*url)); } } catch (const std::exception &e) { BOOST_LOG_TRIVIAL(error) << "Could not parse Printables message. " << e.what(); return; } } void PrintablesWebViewPanel::define_css() { if (m_styles_defined) { return; } m_styles_defined = true; BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; std::string script = R"( // Loading overlay and Notification style var style = document.createElement('style'); style.innerHTML = ` body {} .slic3r-loading-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(127 127 127 / 50%); z-index: 50; display: flex; align-items: center; justify-content: center; } .slic3r-loading-anim { width: 60px; aspect-ratio: 4; --_g: no-repeat radial-gradient(circle closest-side,#000 90%,#0000); background: var(--_g) 0% 50%, var(--_g) 50% 50%, var(--_g) 100% 50%; background-size: calc(100%/3) 100%; animation: slic3r-loading-anim 1s infinite linear; } @keyframes slic3r-loading-anim { 33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%} 50%{background-size:calc(100%/3) 100%,calc(100%/3) 0% ,calc(100%/3) 100%} 66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0% } } .notification-popup { position: fixed; right: 10px; bottom: 10px; background-color: #333333; /* Dark background */ padding: 10px; border-radius: 6px; /* Slightly rounded corners */ color: #ffffff; /* White text */ font-family: Arial, sans-serif; font-size: 12px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.3); /* Add a subtle shadow */ min-width: 350px; max-width: 350px; min-height: 50px; } .notification-popup div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 20px; /* Add padding to make text truncate earlier */ } .notification-popup b { color: #ffa500; } .notification-popup a:hover { text-decoration: underline; /* Underline on hover */ } .notification-popup .close-button { display: inline-block; width: 20px; height: 20px; border: 2px solid #ffa500; /* Orange border for the button */ border-radius: 4px; text-align: center; font-size: 16px; line-height: 16px; cursor: pointer; padding-top: 1px; } .notification-popup .close-button:hover { background-color: #ffa500; /* Orange background on hover */ color: #333333; /* Dark color for the "X" on hover */ } .notification-popup .close-button:before { content: 'X'; color: #ffa500; /* Orange "X" */ font-weight: bold; } `; document.head.appendChild(style); // Capture click on hypertext // Rewritten from mobileApp code (function() { const listenerKey = 'custom-click-listener'; if (!document[listenerKey]) { document.addEventListener( 'click', function(event) { const target = event.target.closest('a[href]'); if (!target) return; // Ignore clicks that are not on links const url = target.href; // Allow empty iframe navigation if (url === 'about:blank') { return; // Let it proceed } // Debug log for navigation console.log(`Printables:onNavigationRequest: ${url}`); // Handle all non-printables.com domains in an external browser if (!/printables\.com/.test(url)) { window.ExternalApp.postMessage(JSON.stringify({ event: 'openExternalUrl', url })) event.preventDefault(); } // Default: Allow navigation to proceed },true); // Capture the event during the capture phase document[listenerKey] = true; } })(); )"; #if defined(__APPLE__) // WebView on Windows does read keyboard shortcuts // Thus doing f.e. Reload twice would make the oparation to fail script += R"( document.addEventListener('keydown', function (event) { if (event.key === 'F5' || (event.ctrlKey && event.key === 'r') || (event.metaKey && event.key === 'r')) { window.ExternalApp.postMessage(JSON.stringify({ event: 'reloadHomePage', fromKeyboard: 1})); } if (event.metaKey && event.key === 'q') { window.ExternalApp.postMessage(JSON.stringify({ event: 'appQuit'})); } if (event.metaKey && event.key === 'm') { window.ExternalApp.postMessage(JSON.stringify({ event: 'appMinimize'})); } }); )"; #endif // defined(__APPLE__) run_script(script); } void PrintablesWebViewPanel::show_download_notification(const std::string& filename) { // There was a trouble with passing wide characters to the script (it was displayed wrong) // Solution is to URL-encode the strings here and pass it. // Then inside javascript decodes it. const std::string message_filename = Http::url_encode(GUI::format(_u8L("Downloading %1%"),filename)); const std::string message_dest = Http::url_encode(GUI::format(_u8L("To %1%"), wxGetApp().app_config->get("url_downloader_dest"))); std::string script = GUI::format(R"( function removeNotification() { const notifDiv = document.getElementById('slicer-notification'); if (notifDiv) notifDiv.remove(); } function appendNotification() { const body = document.getElementsByTagName('body')[0]; const notifDiv = document.createElement('div'); notifDiv.innerHTML = `
PrusaSlicer: ${decodeURIComponent('%1%')}
${decodeURIComponent('%2%')}
`; notifDiv.className = 'notification-popup'; notifDiv.id = 'slicer-notification'; body.appendChild(notifDiv); window.setTimeout(removeNotification, 5000); } appendNotification(); )", message_filename, message_dest); run_script(script); } void PrintablesWebViewPanel::show_loading_overlay() { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; std::string script = R"( function slic3r_showLoadingOverlay() { const body = document.getElementsByTagName('body')[0]; const overlayDiv = document.createElement('div'); overlayDiv.className = 'slic3r-loading-overlay' overlayDiv.id = 'slic3r-loading-overlay'; overlayDiv.innerHTML = '
'; body.appendChild(overlayDiv); } slic3r_showLoadingOverlay(); )"; run_script(script); } void PrintablesWebViewPanel::hide_loading_overlay() { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__; m_refreshing_token = false; std::string script = R"( function slic3r_hideLoadingOverlay() { const overlayDiv = document.getElementById('slic3r-loading-overlay'); if (overlayDiv) overlayDiv.remove(); } slic3r_hideLoadingOverlay(); )"; run_script(script); } } // namespace slic3r::GUI