diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 608043efdf..9604c77d66 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -29,6 +29,10 @@ set(SLIC3R_GUI_SOURCES GUI/UserAccount.hpp GUI/WebViewDialog.cpp GUI/WebViewDialog.hpp + GUI/WebViewPanel.cpp + GUI/WebViewPanel.hpp + GUI/ConnectRequestHandler.cpp + GUI/ConnectRequestHandler.hpp GUI/WebView.cpp GUI/WebView.hpp GUI/WebViewPlatformUtils.hpp diff --git a/src/slic3r/GUI/ConnectRequestHandler.cpp b/src/slic3r/GUI/ConnectRequestHandler.cpp new file mode 100644 index 0000000000..7a156bc78b --- /dev/null +++ b/src/slic3r/GUI/ConnectRequestHandler.cpp @@ -0,0 +1,133 @@ +#include "ConnectRequestHandler.hpp" + +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/UserAccount.hpp" + +#include +#include +#include + +namespace pt = boost::property_tree; + +namespace Slic3r::GUI { + +ConnectRequestHandler::ConnectRequestHandler() +{ + m_actions["REQUEST_LOGIN"] = std::bind(&ConnectRequestHandler::on_connect_action_request_login, this, std::placeholders::_1); + m_actions["REQUEST_CONFIG"] = std::bind(&ConnectRequestHandler::on_connect_action_request_config, this, std::placeholders::_1); + m_actions["WEBAPP_READY"] = std::bind(&ConnectRequestHandler::on_connect_action_webapp_ready,this, std::placeholders::_1); + m_actions["SELECT_PRINTER"] = std::bind(&ConnectRequestHandler::on_connect_action_select_printer, this, std::placeholders::_1); + m_actions["PRINT"] = std::bind(&ConnectRequestHandler::on_connect_action_print, this, std::placeholders::_1); + m_actions["REQUEST_OPEN_IN_BROWSER"] = std::bind(&ConnectRequestHandler::on_connect_action_request_open_in_browser, this, std::placeholders::_1); + m_actions["ERROR"] = std::bind(&ConnectRequestHandler::on_connect_action_error, this, std::placeholders::_1); + m_actions["LOG"] = std::bind(&ConnectRequestHandler::on_connect_action_log, this, std::placeholders::_1); +} +ConnectRequestHandler::~ConnectRequestHandler() +{ +} +void ConnectRequestHandler::handle_message(const std::string& message) +{ + // read msg and choose action + /* + v0: + {"type":"request","detail":{"action":"requestAccessToken"}} + v1: + {"action":"REQUEST_ACCESS_TOKEN"} + */ + std::string action_string; + try { + std::stringstream ss(message); + pt::ptree ptree; + pt::read_json(ss, ptree); + // v1: + if (const auto action = ptree.get_optional("action"); action) { + action_string = *action; + } + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Could not parse _prusaConnect message. " << e.what(); + return; + } + + if (action_string.empty()) { + BOOST_LOG_TRIVIAL(error) << "Recieved invalid message from _prusaConnect (missing action). Message: " << message; + return; + } + assert(m_actions.find(action_string) != m_actions.end()); // this assert means there is a action that has no handling. + if (m_actions.find(action_string) != m_actions.end()) { + m_actions[action_string](message); + } +} + +void ConnectRequestHandler::on_connect_action_error(const std::string &message_data) +{ + BOOST_LOG_TRIVIAL(error) << "WebView runtime error: " << message_data; +} + +void ConnectRequestHandler::resend_config() +{ + on_connect_action_request_config({}); +} + +void ConnectRequestHandler::on_connect_action_log(const std::string& message_data) +{ + BOOST_LOG_TRIVIAL(info) << "WebView log: " << message_data; +} + +void ConnectRequestHandler::on_connect_action_request_login(const std::string &message_data) +{} + +void ConnectRequestHandler::on_connect_action_request_config(const std::string& message_data) +{ + /* + accessToken?: string; + clientVersion?: string; + colorMode?: "LIGHT" | "DARK"; + language?: ConnectLanguage; + sessionId?: string; + */ + const std::string token = wxGetApp().plater()->get_user_account()->get_access_token(); + //const std::string sesh = wxGetApp().plater()->get_user_account()->get_shared_session_key(); + const std::string dark_mode = wxGetApp().dark_mode() ? "DARK" : "LIGHT"; + wxString language = GUI::wxGetApp().current_language_code(); + language = language.SubString(0, 1); + const std::string init_options = GUI::format("{\"accessToken\": \"%4%\",\"clientVersion\": \"%1%\", \"colorMode\": \"%2%\", \"language\": \"%3%\"}", SLIC3R_VERSION, dark_mode, language, token ); + wxString script = GUI::format_wxstr("window._prusaConnect_v1.init(%1%)", init_options); + run_script_bridge(script); + +} +void ConnectRequestHandler::on_connect_action_request_open_in_browser(const std::string& 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 _prusaConnect message. " << e.what(); + return; + } +} + +SourceViewDialog::SourceViewDialog(wxWindow* parent, wxString source) : + wxDialog(parent, wxID_ANY, "Source Code", + wxDefaultPosition, wxSize(700,500), + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +{ + wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, source, + wxDefaultPosition, wxDefaultSize, + wxTE_MULTILINE | + wxTE_RICH | + wxTE_READONLY); + + wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(text, 1, wxEXPAND); + SetSizer(sizer); +} +} // namespace Slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/ConnectRequestHandler.hpp b/src/slic3r/GUI/ConnectRequestHandler.hpp new file mode 100644 index 0000000000..ea64913977 --- /dev/null +++ b/src/slic3r/GUI/ConnectRequestHandler.hpp @@ -0,0 +1,44 @@ +#ifndef slic3r_ConnectRequestHandler_hpp_ +#define slic3r_ConnectRequestHandler_hpp_ + +#include +#include +#include +#include +#include +#include + +//#define DEBUG_URL_PANEL + +namespace Slic3r::GUI { +class ConnectRequestHandler +{ +public: + ConnectRequestHandler(); + ~ConnectRequestHandler(); + + void handle_message(const std::string& message); + void resend_config(); +protected: + // action callbacs stored in m_actions + virtual void on_connect_action_log(const std::string& message_data); + virtual void on_connect_action_error(const std::string& message_data); + virtual void on_connect_action_request_login(const std::string& message_data); + virtual void on_connect_action_request_config(const std::string& message_data); + virtual void on_connect_action_request_open_in_browser(const std::string& message_data); + virtual void on_connect_action_select_printer(const std::string& message_data) = 0; + virtual void on_connect_action_print(const std::string& message_data) = 0; + virtual void on_connect_action_webapp_ready(const std::string& message_data) = 0; + virtual void run_script_bridge(const wxString &script) = 0; + + std::map> m_actions; +}; + +class SourceViewDialog : public wxDialog +{ +public: + SourceViewDialog(wxWindow* parent, wxString source); +}; + +} // namespace Slic3r::GUI +#endif /* slic3r_ConnectRequestHandler_hpp_ */ \ No newline at end of file diff --git a/src/slic3r/GUI/Downloader.cpp b/src/slic3r/GUI/Downloader.cpp index c5ee0ff373..61e69227bb 100644 --- a/src/slic3r/GUI/Downloader.cpp +++ b/src/slic3r/GUI/Downloader.cpp @@ -9,6 +9,7 @@ #include #include +#include namespace Slic3r { namespace GUI { @@ -72,6 +73,21 @@ std::string filename_from_url(const std::string& url) return std::string(); return std::string(url_plain.begin() + slash + 1, url_plain.end()); } +std::string unescape_url(const std::string& unescaped) +{ + std::string ret_val; + CURL* curl = curl_easy_init(); + if (curl) { + int decodelen; + char* decoded = curl_easy_unescape(curl, unescaped.c_str(), unescaped.size(), &decodelen); + if (decoded) { + ret_val = std::string(decoded); + curl_free(decoded); + } + curl_easy_cleanup(curl); + } + return ret_val; +} } Download::Download(int ID, std::string url, wxEvtHandler* evt_handler, const boost::filesystem::path& dest_folder) @@ -133,23 +149,18 @@ void Downloader::start_download(const std::string& full_url) { assert(m_initialized); - // TODO: There is a misterious slash appearing in recieved msg on windows -#ifdef _WIN32 - if (!boost::starts_with(full_url, "prusaslicer://open/?file=")) { -#else - if (!boost::starts_with(full_url, "prusaslicer://open?file=")) { -#endif - BOOST_LOG_TRIVIAL(error) << "Could not start download due to wrong URL: " << full_url; - // TODO: show error? + std::string escaped_url = unescape_url(full_url); + if (boost::starts_with(escaped_url, "prusaslicer://open?file=")) { + escaped_url = escaped_url.substr(24); + }else if (boost::starts_with(escaped_url, "prusaslicer://open/?file=")) { + escaped_url = escaped_url.substr(25); + } else { + BOOST_LOG_TRIVIAL(error) << "Could not start download due to wrong URL: " << full_url; return; - } + } + size_t id = get_next_id(); - // TODO: still same mistery -#ifdef _WIN32 - std::string escaped_url = FileGet::escape_url(full_url.substr(25)); -#else - std::string escaped_url = FileGet::escape_url(full_url.substr(24)); -#endif + if (!boost::starts_with(escaped_url, "https://") || !FileGet::is_subdomain(escaped_url, "printables.com")) { std::string msg = format(_L("Download won't start. Download URL doesn't point to https://printables.com : %1%"), escaped_url); BOOST_LOG_TRIVIAL(error) << msg; diff --git a/src/slic3r/GUI/DownloaderFileGet.cpp b/src/slic3r/GUI/DownloaderFileGet.cpp index 7a401bf8d0..8cbb89abcd 100644 --- a/src/slic3r/GUI/DownloaderFileGet.cpp +++ b/src/slic3r/GUI/DownloaderFileGet.cpp @@ -23,21 +23,6 @@ namespace GUI { const size_t DOWNLOAD_MAX_CHUNK_SIZE = 10 * 1024 * 1024; const size_t DOWNLOAD_SIZE_LIMIT = 1024 * 1024 * 1024; -std::string FileGet::escape_url(const std::string& unescaped) -{ - std::string ret_val; - CURL* curl = curl_easy_init(); - if (curl) { - int decodelen; - char* decoded = curl_easy_unescape(curl, unescaped.c_str(), unescaped.size(), &decodelen); - if (decoded) { - ret_val = std::string(decoded); - curl_free(decoded); - } - curl_easy_cleanup(curl); - } - return ret_val; -} bool FileGet::is_subdomain(const std::string& url, const std::string& domain) { // domain should be f.e. printables.com (.com including) diff --git a/src/slic3r/GUI/DownloaderFileGet.hpp b/src/slic3r/GUI/DownloaderFileGet.hpp index 37a59ec30e..7859d52f62 100644 --- a/src/slic3r/GUI/DownloaderFileGet.hpp +++ b/src/slic3r/GUI/DownloaderFileGet.hpp @@ -27,7 +27,6 @@ public: void cancel(); void pause(); void resume(); - static std::string escape_url(const std::string& url); static bool is_subdomain(const std::string& url, const std::string& domain); private: std::unique_ptr p; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 879fc090ba..a9836a36f7 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -103,7 +103,6 @@ #include "WifiConfigDialog.hpp" #include "UserAccount.hpp" #include "UserAccountUtils.hpp" -#include "WebViewDialog.hpp" #include "LoginDialog.hpp" // IWYU pragma: keep #include "PresetArchiveDatabase.hpp" @@ -4113,6 +4112,12 @@ void GUI_App::show_printer_webview_tab() mainframe->show_printer_webview_tab(preset_bundle->physical_printers.get_selected_printer_config()); } +void GUI_App::printables_request(const std::string& url) +{ + this->mainframe->select_tab(size_t(0)); + start_download(url); +} + bool LogGui::ignorred_message(const wxString& msg) diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 08ef7667ca..7b18a0d324 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -436,7 +436,7 @@ public: void request_project_download(std::string project_id) {} void request_open_project(std::string project_id) {} void request_remove_project(std::string project_id) {} - + void printables_request(const std::string& url); private: bool on_init_inner(); void init_app_config(); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index cbfba459a5..94a18ba293 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -64,7 +64,7 @@ #include "GalleryDialog.hpp" #include "NotificationManager.hpp" #include "Preferences.hpp" -#include "WebViewDialog.hpp" +#include "WebViewPanel.hpp" #include "UserAccount.hpp" #ifdef _WIN32 @@ -801,15 +801,39 @@ void MainFrame::create_preset_tabs() add_created_tab(new TabSLAMaterial(m_tabpanel), "resin"); add_created_tab(new TabPrinter(m_tabpanel), wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF ? "printer" : "sla_printer"); + m_printables_webview = new PrintablesWebViewPanel(m_tabpanel); + add_printables_webview_tab(); + m_connect_webview = new ConnectWebViewPanel(m_tabpanel); m_printer_webview = new PrinterWebViewPanel(m_tabpanel, L""); - m_printables_webview = new WebViewPanel(m_tabpanel, GUI::from_u8("https://www.printables.com"), { "_prusaSlicer" }); + // new created tabs have to be hidden by default m_connect_webview->Hide(); m_printer_webview->Hide(); } +void MainFrame::on_account_login(const std::string& token) +{ + add_connect_webview_tab(); + assert (m_printables_webview); + m_printables_webview->login(token); +} +void MainFrame::on_account_will_refresh() +{ + m_printables_webview->send_will_refresh(); +} +void MainFrame::on_account_did_refresh(const std::string& token) +{ + m_printables_webview->send_refreshed_token(token); +} +void MainFrame::on_account_logout() +{ + remove_connect_webview_tab(); + assert (m_printables_webview); + m_printables_webview->logout(); +} + void MainFrame::add_connect_webview_tab() { if (m_connect_webview_added) { @@ -820,7 +844,7 @@ void MainFrame::add_connect_webview_tab() // insert "Connect" tab to position next to "Printer" tab // order of tabs: Plater - Print Settings - Filaments - Printers - Prusa Connect - Prusa Link - int n = m_tabpanel->FindPage(wxGetApp().get_tab(Preset::TYPE_PRINTER)) + 1; + int n = m_tabpanel->FindPage(m_printables_webview) + 1; wxWindow* page = m_connect_webview; const wxString text(L"Prusa Connect"); const std::string bmp_name = ""; @@ -856,7 +880,7 @@ void MainFrame::add_printables_webview_tab() return; } - int n = m_tabpanel->FindPage(m_connect_webview) + 1; + int n = m_tabpanel->FindPage(wxGetApp().get_tab(Preset::TYPE_PRINTER)) + 1; wxWindow* page = m_printables_webview; const wxString text(L"Printables"); const std::string bmp_name = ""; @@ -865,6 +889,8 @@ void MainFrame::add_printables_webview_tab() m_printables_webview->load_default_url_delayed(); m_printables_webview_added = true; } + +// no longer needed? void MainFrame::remove_printables_webview_tab() { if (!m_printables_webview_added) { @@ -1221,6 +1247,8 @@ void MainFrame::on_sys_color_changed() for (Tab* tab : wxGetApp().tabs_list) tab->sys_color_changed(); + if (m_printables_webview) + m_printables_webview->sys_color_changed(); if (m_connect_webview) m_connect_webview->sys_color_changed(); if (m_printer_webview) diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 4cb184605d..359961fe90 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -47,7 +47,7 @@ class PreferencesDialog; class GalleryDialog; class ConnectWebViewPanel; class PrinterWebViewPanel; -class WebViewPanel; +class PrintablesWebViewPanel; enum QuickSlice { @@ -101,7 +101,7 @@ class MainFrame : public DPIFrame ConnectWebViewPanel* m_connect_webview{ nullptr }; bool m_connect_webview_added{ false }; - WebViewPanel* m_printables_webview{ nullptr }; + PrintablesWebViewPanel* m_printables_webview{ nullptr }; bool m_printables_webview_added{ false }; PrinterWebViewPanel* m_printer_webview{ nullptr }; bool m_printer_webview_added{ false }; @@ -127,6 +127,9 @@ class MainFrame : public DPIFrame bool can_delete_all() const; bool can_reslice() const; + void add_connect_webview_tab(); + void remove_connect_webview_tab(); + // MenuBar items changeable in respect to printer technology enum MenuItems { // FFF SLA @@ -217,8 +220,10 @@ public: void add_to_recent_projects(const wxString& filename); void technology_changed(); - void add_connect_webview_tab(); - void remove_connect_webview_tab(); + void on_account_login(const std::string& token); + void on_account_will_refresh(); + void on_account_did_refresh(const std::string& token); + void on_account_logout(); void show_connect_tab(const wxString &url); void add_printables_webview_tab(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 3d84240587..395f29dc8d 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -915,13 +915,16 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_OPEN_EXTERNAL_LOGIN_WIZARD, open_external_login); this->q->Bind(EVT_OPEN_EXTERNAL_LOGIN, open_external_login); + // void on_account_login(const std::string& token); + // void on_account_will_refresh(); + // void on_account_did_refresh(const std::string& token); + // void on_account_logout(); this->q->Bind(EVT_UA_LOGGEDOUT, [this](UserAccountSuccessEvent& evt) { user_account->clear(); std::string text = _u8L("Logged out from Prusa Account."); this->notification_manager->close_notification_of_type(NotificationType::UserAccountID); this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); - this->main_frame->remove_connect_webview_tab(); - this->main_frame->remove_printables_webview_tab(); + this->main_frame->on_account_logout(); this->main_frame->refresh_account_menu(true); // Update sidebar printer status sidebar->update_printer_presets_combobox(); @@ -938,16 +941,19 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) std::string who = user_account->get_username(); std::string username; if (user_account->on_user_id_success(evt.data, username)) { - // Do not show notification on refresh. if (who != username) { + // show notification only on login (not refresh). std::string text = format(_u8L("Logged to Prusa Account as %1%."), username); // login notification this->notification_manager->close_notification_of_type(NotificationType::UserAccountID); // show connect tab this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::ImportantNotificationLevel, text); + + this->main_frame->on_account_login(user_account->get_access_token()); + } else { + // refresh do different operations than on_account_login + this->main_frame->on_account_did_refresh(user_account->get_access_token()); } - this->main_frame->add_connect_webview_tab(); - this->main_frame->add_printables_webview_tab(); // Update User name in TopBar this->main_frame->refresh_account_menu(); wxGetApp().update_wizard_login_page(); @@ -960,8 +966,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) user_account->clear(); this->notification_manager->close_notification_of_type(NotificationType::UserAccountID); this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("Failed to connect to Prusa Account.")); - this->main_frame->remove_connect_webview_tab(); - this->main_frame->remove_printables_webview_tab(); + this->main_frame->on_account_logout(); // Update User name in TopBar this->main_frame->refresh_account_menu(true); // Update sidebar printer status @@ -974,8 +979,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) user_account->clear(); this->notification_manager->close_notification_of_type(NotificationType::UserAccountID); this->notification_manager->push_notification(NotificationType::UserAccountID, NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("Failed to connect to Prusa Account.")); - this->main_frame->remove_connect_webview_tab(); - this->main_frame->remove_printables_webview_tab(); + this->main_frame->on_account_logout(); // Update User name in TopBar this->main_frame->refresh_account_menu(true); // Update sidebar printer status @@ -1036,7 +1040,10 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_UA_REFRESH_TIME, [this](UserAccountTimeEvent& evt) { this->user_account->set_refresh_time(evt.data); - }); + }); + this->q->Bind(EVT_UA_ENQUEUED_REFRESH, [this](SimpleEvent& evt) { + this->main_frame->on_account_will_refresh(); + }); } wxGetApp().other_instance_message_handler()->init(this->q); diff --git a/src/slic3r/GUI/UserAccountSession.cpp b/src/slic3r/GUI/UserAccountSession.cpp index 9c18cad514..1ff4f22f18 100644 --- a/src/slic3r/GUI/UserAccountSession.cpp +++ b/src/slic3r/GUI/UserAccountSession.cpp @@ -31,6 +31,7 @@ wxDEFINE_EVENT(EVT_UA_FAIL, UserAccountFailEvent); wxDEFINE_EVENT(EVT_UA_RESET, UserAccountFailEvent); wxDEFINE_EVENT(EVT_UA_PRUSACONNECT_PRINTER_DATA_FAIL, UserAccountFailEvent); wxDEFINE_EVENT(EVT_UA_REFRESH_TIME, UserAccountTimeEvent); +wxDEFINE_EVENT(EVT_UA_ENQUEUED_REFRESH, SimpleEvent); void UserActionPost::perform(/*UNUSED*/ wxEvtHandler* evt_handler, /*UNUSED*/ const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) const { @@ -227,6 +228,7 @@ void UserAccountSession::enqueue_test_with_refresh() void UserAccountSession::enqueue_refresh(const std::string& body) { + wxQueueEvent(p_evt_handler, new SimpleEvent(EVT_UA_ENQUEUED_REFRESH)); std::string post_fields; { std::lock_guard lock(m_credentials_mutex); diff --git a/src/slic3r/GUI/UserAccountSession.hpp b/src/slic3r/GUI/UserAccountSession.hpp index 223af17d30..6b3d793d3c 100644 --- a/src/slic3r/GUI/UserAccountSession.hpp +++ b/src/slic3r/GUI/UserAccountSession.hpp @@ -30,6 +30,7 @@ wxDECLARE_EVENT(EVT_UA_FAIL, UserAccountFailEvent); // Soft fail - clears only a wxDECLARE_EVENT(EVT_UA_RESET, UserAccountFailEvent); // Hard fail - clears all wxDECLARE_EVENT(EVT_UA_PRUSACONNECT_PRINTER_DATA_FAIL, UserAccountFailEvent); // Failed to get data for printer to select, soft fail, action does not repeat wxDECLARE_EVENT(EVT_UA_REFRESH_TIME, UserAccountTimeEvent); +wxDECLARE_EVENT(EVT_UA_ENQUEUED_REFRESH, SimpleEvent); typedef std::function UserActionSuccessFn; typedef std::function UserActionFailFn; diff --git a/src/slic3r/GUI/WebView.cpp b/src/slic3r/GUI/WebView.cpp index 85a537ae6c..b236b29155 100644 --- a/src/slic3r/GUI/WebView.cpp +++ b/src/slic3r/GUI/WebView.cpp @@ -1,6 +1,9 @@ #include "WebView.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/format.hpp" + +#include "libslic3r/Platform.hpp" #include #include @@ -21,9 +24,9 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, const wxString& url, const if (webView) { wxString correct_url = url.empty() ? wxString("") : wxURI(url).BuildURI(); - + wxString user_agent = Slic3r::GUI::format_wxstr("%1%/%2% (%3%)",SLIC3R_APP_FULL_NAME, SLIC3R_VERSION, Slic3r::platform_to_string(Slic3r::platform())); #ifdef __WIN32__ - webView->SetUserAgent(SLIC3R_APP_FULL_NAME); + webView->SetUserAgent(user_agent); webView->Create(parent, wxID_ANY, correct_url, wxDefaultPosition, wxDefaultSize); //We register the wxfs:// protocol for testing purposes //webView->RegisterHandler(wxSharedPtr(new wxWebViewArchiveHandler("wxfs"))); @@ -35,7 +38,7 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, const wxString& url, const // And the memory: file system //webView->RegisterHandler(wxSharedPtr(new wxWebViewFSHandler("memory"))); webView->Create(parent, wxID_ANY, correct_url, wxDefaultPosition, wxDefaultSize); - webView->SetUserAgent(wxString::FromUTF8(SLIC3R_APP_FULL_NAME)); + webView->SetUserAgent(user_agent); #endif #ifndef __WIN32__ Slic3r::GUI::wxGetApp().CallAfter([message_handlers, webView] { @@ -57,5 +60,3 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, const wxString& url, const } return webView; } - - diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index 9ce243c6ca..52bacaa391 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -22,11 +22,6 @@ #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_OPEN_EXTERNAL_LOGIN, wxCommandEvent); namespace pt = boost::property_tree; @@ -34,904 +29,6 @@ namespace pt = boost::property_tree; namespace Slic3r { namespace GUI { - -WebViewPanel::~WebViewPanel() -{ - SetEvtHandlerEnabled(false); -#ifdef DEBUG_URL_PANEL - delete m_tools_menu; -#endif -} - -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 - m_browser->LoadURL(url); - m_browser->SetFocus(); -} - - -WebViewPanel::WebViewPanel(wxWindow *parent, const wxString& default_url, const std::vector& message_handler_names, const std::string& loading_html/* = "loading"*/) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) - , m_default_url (default_url) - , m_loading_html(loading_html) - , m_script_message_hadler_names(message_handler_names) -{ - wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); -#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); - - // Create the webview - m_browser = WebView::CreateWebView(this, /*m_default_url*/ 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(); - } - 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; - } - topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); -#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"); - -#endif - - Bind(wxEVT_SHOW, &WebViewPanel::on_show, this); - - // 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()); - -#ifdef DEBUG_URL_PANEL - // 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 - //Connect the idle events - Bind(wxEVT_IDLE, &WebViewPanel::on_idle, this); -} - -void WebViewPanel::load_default_url_delayed() -{ - assert(!m_default_url.empty()); - m_load_default_url = true; -} - -void WebViewPanel::load_error_page() -{ - if (!m_browser) - return; - - m_browser->Stop(); - m_load_error_page = true; -} - -void WebViewPanel::on_show(wxShowEvent& evt) -{ - m_shown = evt.IsShown(); - if (evt.IsShown() && m_load_default_url) { - m_load_default_url = false; - load_url(m_default_url); - } -} - -void WebViewPanel::on_idle(wxIdleEvent& WXUNUSED(evt)) -{ - if (!m_browser) - return; - if (m_browser->IsBusy()) { - wxSetCursor(wxCURSOR_ARROWWAIT); - } else { - wxSetCursor(wxNullCursor); - - if (m_shown && m_load_error_page) { - m_load_error_page = false; - if (m_load_default_url_on_next_error) { - m_load_default_url_on_next_error = false; - load_url(m_default_url); - } else { - load_url(GUI::format_wxstr("file://%1%/web/connection_failed.html", boost::filesystem::path(resources_dir()).generic_string())); - } - } - } -#ifdef DEBUG_URL_PANEL - m_button_stop->Enable(m_browser->IsBusy()); -#endif -} - -/** - * 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; - m_browser->GoBack(); -} - -/** - * Callback invoked when user pressed the "forward" button - */ -void WebViewPanel::on_forward_button(wxCommandEvent& WXUNUSED(evt)) -{ - if (!m_browser) - 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) -{ -} - -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(debug) << "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)) -{ - 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) << "WebViewPanel error: " << category; - load_error_page(); -#ifdef DEBUG_URL_PANEL - m_info->ShowMessage(wxString("An error occurred loading ") + evt.GetURL() + "\n" + - "'" + category + "'", wxICON_ERROR); -#endif -} - -void WebViewPanel::sys_color_changed() -{ -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(this); -#endif -} - -SourceViewDialog::SourceViewDialog(wxWindow* parent, wxString source) : - wxDialog(parent, wxID_ANY, "Source Code", - wxDefaultPosition, wxSize(700,500), - wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) -{ - wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, source, - wxDefaultPosition, wxDefaultSize, - wxTE_MULTILINE | - wxTE_RICH | - wxTE_READONLY); - - wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(text, 1, wxEXPAND); - SetSizer(sizer); -} - -ConnectRequestHandler::ConnectRequestHandler() -{ - m_actions["REQUEST_LOGIN"] = std::bind(&ConnectRequestHandler::on_connect_action_request_login, this, std::placeholders::_1); - m_actions["REQUEST_CONFIG"] = std::bind(&ConnectRequestHandler::on_connect_action_request_config, this, std::placeholders::_1); - m_actions["WEBAPP_READY"] = std::bind(&ConnectRequestHandler::on_connect_action_webapp_ready,this, std::placeholders::_1); - m_actions["SELECT_PRINTER"] = std::bind(&ConnectRequestHandler::on_connect_action_select_printer, this, std::placeholders::_1); - m_actions["PRINT"] = std::bind(&ConnectRequestHandler::on_connect_action_print, this, std::placeholders::_1); - m_actions["REQUEST_OPEN_IN_BROWSER"] = std::bind(&ConnectRequestHandler::on_connect_action_request_open_in_browser, this, std::placeholders::_1); - m_actions["ERROR"] = std::bind(&ConnectRequestHandler::on_connect_action_error, this, std::placeholders::_1); - m_actions["LOG"] = std::bind(&ConnectRequestHandler::on_connect_action_log, this, std::placeholders::_1); -} -ConnectRequestHandler::~ConnectRequestHandler() -{ -} -void ConnectRequestHandler::handle_message(const std::string& message) -{ - // read msg and choose action - /* - v0: - {"type":"request","detail":{"action":"requestAccessToken"}} - v1: - {"action":"REQUEST_ACCESS_TOKEN"} - */ - std::string action_string; - try { - std::stringstream ss(message); - pt::ptree ptree; - pt::read_json(ss, ptree); - // v1: - if (const auto action = ptree.get_optional("action"); action) { - action_string = *action; - } - } - catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "Could not parse _prusaConnect message. " << e.what(); - return; - } - - if (action_string.empty()) { - BOOST_LOG_TRIVIAL(error) << "Recieved invalid message from _prusaConnect (missing action). Message: " << message; - return; - } - assert(m_actions.find(action_string) != m_actions.end()); // this assert means there is a action that has no handling. - if (m_actions.find(action_string) != m_actions.end()) { - m_actions[action_string](message); - } -} - -void ConnectRequestHandler::on_connect_action_error(const std::string &message_data) -{ - BOOST_LOG_TRIVIAL(error) << "WebView runtime error: " << message_data; -} - -void ConnectRequestHandler::resend_config() -{ - on_connect_action_request_config({}); -} - -void ConnectRequestHandler::on_connect_action_log(const std::string& message_data) -{ - BOOST_LOG_TRIVIAL(info) << "WebView log: " << message_data; -} - -void ConnectRequestHandler::on_connect_action_request_login(const std::string &message_data) -{} - - -void ConnectRequestHandler::on_connect_action_request_config(const std::string& message_data) -{ - /* - accessToken?: string; - clientVersion?: string; - colorMode?: "LIGHT" | "DARK"; - language?: ConnectLanguage; - sessionId?: string; - */ - const std::string token = wxGetApp().plater()->get_user_account()->get_access_token(); - //const std::string sesh = wxGetApp().plater()->get_user_account()->get_shared_session_key(); - const std::string dark_mode = wxGetApp().dark_mode() ? "DARK" : "LIGHT"; - wxString language = GUI::wxGetApp().current_language_code(); - language = language.SubString(0, 1); - const std::string init_options = GUI::format("{\"accessToken\": \"%4%\",\"clientVersion\": \"%1%\", \"colorMode\": \"%2%\", \"language\": \"%3%\"}", SLIC3R_VERSION, dark_mode, language, token ); - wxString script = GUI::format_wxstr("window._prusaConnect_v1.init(%1%)", init_options); - run_script_bridge(script); - -} -void ConnectRequestHandler::on_connect_action_request_open_in_browser(const std::string& 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 _prusaConnect message. " << e.what(); - return; - } -} - -ConnectWebViewPanel::ConnectWebViewPanel(wxWindow* parent) - : WebViewPanel(parent, GUI::from_u8(Utils::ServiceConfig::instance().connect_url()), { "_prusaSlicer" }, "connect_loading") -{ - // m_browser->RegisterHandler(wxSharedPtr(new WebViewHandler("https"))); - - auto* plater = wxGetApp().plater(); - plater->Bind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this); - plater->Bind(EVT_UA_LOGGEDOUT, &ConnectWebViewPanel::on_user_logged_out, this); -} - -ConnectWebViewPanel::~ConnectWebViewPanel() -{ - m_browser->Unbind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this); -} - -wxString ConnectWebViewPanel::get_login_script(bool refresh) -{ - 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; - 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) { - retry = true; - } 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()); - 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() -{ - auto javascript = get_login_script(false); - BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; - m_browser->AddUserScript(javascript); -} - -void ConnectWebViewPanel::on_user_token(UserAccountSuccessEvent& e) -{ - e.Skip(); - 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_logged_out(UserAccountSuccessEvent& e) -{ - e.Skip(); - // 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()); - 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(); - } -} - -void ConnectWebViewPanel::on_connect_action_error(const std::string &message_data) -{ - 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::logout() -{ - wxString script = L"window._prusaConnect_v1.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(true); -} - -PrinterWebViewPanel::PrinterWebViewPanel(wxWindow* parent, const wxString& default_url) - : WebViewPanel(parent, default_url, {}) -{ - if (!m_browser) - return; - - m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PrinterWebViewPanel::on_loaded, this); -#ifndef NDEBUG - m_browser->EnableAccessToDevTools(); - m_browser->EnableContextMenu(); -#endif -} - -void PrinterWebViewPanel::on_loaded(wxWebViewEvent& evt) -{ - if (evt.GetURL().IsEmpty()) - return; - if (!m_api_key.empty()) { - send_api_key(); - } else if (!m_usr.empty() && !m_psk.empty()) { - send_credentials(); - } -} - -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');"); - m_browser->Reload(); - m_api_key_sent = true; - setup_webview_with_credentials(m_browser, m_usr, m_psk); -} - -void PrinterWebViewPanel::sys_color_changed() -{ -} - WebViewDialog::WebViewDialog(wxWindow* parent, const wxString& url, const wxString& dialog_name, const wxSize& size, const std::vector& message_handler_names, const std::string& loading_html/* = "loading"*/) : DPIDialog(parent, wxID_ANY, dialog_name, wxDefaultPosition, size, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) , m_loading_html(loading_html) diff --git a/src/slic3r/GUI/WebViewDialog.hpp b/src/slic3r/GUI/WebViewDialog.hpp index 05efed3453..82ff2f9739 100644 --- a/src/slic3r/GUI/WebViewDialog.hpp +++ b/src/slic3r/GUI/WebViewDialog.hpp @@ -1,14 +1,13 @@ #ifndef slic3r_WebViewDialog_hpp_ #define slic3r_WebViewDialog_hpp_ -//#define DEBUG_URL_PANEL - #include #include #include #include "GUI_Utils.hpp" #include "UserAccountSession.hpp" +#include "ConnectRequestHandler.hpp" #ifdef DEBUG_URL_PANEL #include @@ -22,91 +21,6 @@ wxDECLARE_EVENT(EVT_OPEN_EXTERNAL_LOGIN, wxCommandEvent); namespace Slic3r { namespace GUI { -class WebViewPanel : public wxPanel -{ -public: - WebViewPanel(wxWindow *parent, const wxString& default_url, const std::vector& message_handler_names, const std::string& loading_html = "loading"); - virtual ~WebViewPanel(); - - void load_url(const wxString& url); - void load_default_url_delayed(); - void load_error_page(); - - void on_show(wxShowEvent& evt); - virtual void on_script_message(wxWebViewEvent& evt); - - void on_idle(wxIdleEvent& evt); - void on_url(wxCommandEvent& evt); - void on_back_button(wxCommandEvent& evt); - void on_forward_button(wxCommandEvent& evt); - void on_stop_button(wxCommandEvent& evt); - void on_reload_button(wxCommandEvent& evt); - - void on_view_source_request(wxCommandEvent& evt); - void on_view_text_request(wxCommandEvent& evt); - void on_tools_clicked(wxCommandEvent& evt); - void on_error(wxWebViewEvent& evt); - - void run_script(const wxString& javascript); - void on_run_script_custom(wxCommandEvent& evt); - void on_add_user_script(wxCommandEvent& evt); - void on_set_custom_user_agent(wxCommandEvent& evt); - void on_clear_selection(wxCommandEvent& evt); - void on_delete_selection(wxCommandEvent& evt); - void on_select_all(wxCommandEvent& evt); - void On_enable_context_menu(wxCommandEvent& evt); - void On_enable_dev_tools(wxCommandEvent& evt); - virtual void on_navigation_request(wxWebViewEvent &evt); - - wxString get_default_url() const { return m_default_url; } - void set_default_url(const wxString& url) { m_default_url = url; } - - virtual void sys_color_changed(); - - void set_load_default_url_on_next_error(bool val) { m_load_default_url_on_next_error = val; } - -protected: - - virtual void on_page_will_load(); - - wxWebView* m_browser { nullptr }; - bool m_load_default_url { false }; -#ifdef DEBUG_URL_PANEL - - wxBoxSizer *bSizer_toolbar; - wxButton * m_button_back; - wxButton * m_button_forward; - wxButton * m_button_stop; - wxButton * m_button_reload; - wxTextCtrl *m_url; - wxButton * m_button_tools; - - wxMenu* m_tools_menu; - wxMenuItem* m_script_custom; - - wxInfoBar *m_info; - wxStaticText* m_info_text; - - wxMenuItem* m_context_menu; - wxMenuItem* m_dev_tools; -#endif - - // Last executed JavaScript snippet, for convenience. - wxString m_javascript; - wxString m_response_js; - wxString m_default_url; - - std::string m_loading_html; - //DECLARE_EVENT_TABLE() - - bool m_load_error_page { false }; - bool m_shown { false }; - bool m_load_default_url_on_next_error { false }; - - std::vector m_script_message_hadler_names; -}; - - class WebViewDialog : public DPIDialog { public: @@ -176,87 +90,6 @@ protected: std::vector m_script_message_hadler_names; }; -class ConnectRequestHandler -{ -public: - ConnectRequestHandler(); - ~ConnectRequestHandler(); - - void handle_message(const std::string& message); - void resend_config(); -protected: - // action callbacs stored in m_actions - virtual void on_connect_action_log(const std::string& message_data); - virtual void on_connect_action_error(const std::string& message_data); - virtual void on_connect_action_request_login(const std::string& message_data); - virtual void on_connect_action_request_config(const std::string& message_data); - virtual void on_connect_action_request_open_in_browser(const std::string& message_data); - virtual void on_connect_action_select_printer(const std::string& message_data) = 0; - virtual void on_connect_action_print(const std::string& message_data) = 0; - virtual void on_connect_action_webapp_ready(const std::string& message_data) = 0; - virtual void run_script_bridge(const wxString &script) = 0; - - std::map> m_actions; -}; - -class ConnectWebViewPanel : public WebViewPanel, public ConnectRequestHandler -{ -public: - ConnectWebViewPanel(wxWindow* parent); - ~ConnectWebViewPanel() override; - void on_script_message(wxWebViewEvent& evt) override; - void logout(); - void sys_color_changed() override; - void on_navigation_request(wxWebViewEvent &evt) override; -protected: - void on_connect_action_request_login(const std::string &message_data) override; - void on_connect_action_select_printer(const std::string& message_data) override; - void on_connect_action_print(const std::string& message_data) override; - void on_connect_action_webapp_ready(const std::string& message_data) override {} - void run_script_bridge(const wxString& script) override {run_script(script); } - void on_page_will_load() override; - void on_connect_action_error(const std::string &message_data) override; -private: - static wxString get_login_script(bool refresh); - static wxString get_logout_script(); - void on_user_token(UserAccountSuccessEvent& e); - void on_user_logged_out(UserAccountSuccessEvent& e); - bool m_reached_default_url {false}; -}; - -class PrinterWebViewPanel : public WebViewPanel -{ -public: - PrinterWebViewPanel(wxWindow* parent, const wxString& default_url); - - void on_loaded(wxWebViewEvent& evt); - - void send_api_key(); - void send_credentials(); - void set_api_key(const std::string &key) - { - if (m_api_key != key) { - clear(); - m_api_key = key; - } - } - void set_credentials(const std::string &usr, const std::string &psk) - { - if (m_usr != usr || m_psk != psk) { - clear(); - m_usr = usr; - m_psk = psk; - } - } - void clear() { m_api_key.clear(); m_usr.clear(); m_psk.clear(); m_api_key_sent = false; } - void sys_color_changed() override; -private: - std::string m_api_key; - std::string m_usr; - std::string m_psk; - bool m_api_key_sent {false}; -}; - class PrinterPickWebViewDialog : public WebViewDialog, public ConnectRequestHandler { public: @@ -276,12 +109,6 @@ private: std::string& m_ret_val; }; -class SourceViewDialog : public wxDialog -{ -public: - SourceViewDialog(wxWindow* parent, wxString source); -}; - class LoginWebViewDialog : public WebViewDialog { public: @@ -297,4 +124,4 @@ private: } // GUI } // Slic3r -#endif /* slic3r_Tab_hpp_ */ +#endif /* slic3r_WebViewDialog_hpp_ */ diff --git a/src/slic3r/GUI/WebViewPanel.cpp b/src/slic3r/GUI/WebViewPanel.cpp new file mode 100644 index 0000000000..12c8c1ff5b --- /dev/null +++ b/src/slic3r/GUI/WebViewPanel.cpp @@ -0,0 +1,1077 @@ +#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/Utils/ServiceConfig.hpp" + +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/Field.hpp" + +#include // IWYU pragma: keep + +#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 + +namespace pt = boost::property_tree; + +namespace Slic3r::GUI { + +WebViewPanel::~WebViewPanel() +{ + SetEvtHandlerEnabled(false); +#ifdef DEBUG_URL_PANEL + delete m_tools_menu; +#endif +} + +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 + m_browser->LoadURL(url); + m_browser->SetFocus(); +} + + +WebViewPanel::WebViewPanel(wxWindow *parent, const wxString& default_url, const std::vector& message_handler_names, const std::string& loading_html/* = "loading"*/) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) + , m_default_url (default_url) + , m_loading_html(loading_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); + + // Create the webview + m_browser = WebView::CreateWebView(this, /*m_default_url*/ 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(); + } + 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; + } + topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); +#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"); + +#endif + + Bind(wxEVT_SHOW, &WebViewPanel::on_show, this); + + // 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()); + +#ifdef DEBUG_URL_PANEL + // 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 + //Connect the idle events + Bind(wxEVT_IDLE, &WebViewPanel::on_idle, this); +} + +void WebViewPanel::load_default_url_delayed() +{ + assert(!m_default_url.empty()); + m_load_default_url = true; +} + +void WebViewPanel::load_error_page() +{ + if (!m_browser) + return; + + m_browser->Stop(); + m_load_error_page = true; +} + +void WebViewPanel::on_show(wxShowEvent& evt) +{ + m_shown = evt.IsShown(); + if (evt.IsShown() && m_load_default_url) { + load_url(m_default_url); + } +} + +void WebViewPanel::on_idle(wxIdleEvent& WXUNUSED(evt)) +{ + if (!m_browser) + return; + if (m_browser->IsBusy()) { + wxSetCursor(wxCURSOR_ARROWWAIT); + } else { + wxSetCursor(wxNullCursor); + + if (m_shown && m_load_error_page) { + m_load_error_page = false; + if (m_load_default_url_on_next_error) { + m_load_default_url_on_next_error = false; + load_url(m_default_url); + } else { + load_url(GUI::format_wxstr("file://%1%/web/connection_failed.html", boost::filesystem::path(resources_dir()).generic_string())); + } + } + } +#ifdef DEBUG_URL_PANEL + m_button_stop->Enable(m_browser->IsBusy()); +#endif +} + +/** + * 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; + m_browser->GoBack(); +} + +/** + * Callback invoked when user pressed the "forward" button + */ +void WebViewPanel::on_forward_button(wxCommandEvent& WXUNUSED(evt)) +{ + if (!m_browser) + 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) +{ +} + +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(debug) << "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)) +{ + 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) << "WebViewPanel error: " << category; + load_error_page(); +#ifdef DEBUG_URL_PANEL + m_info->ShowMessage(wxString("An error occurred loading ") + evt.GetURL() + "\n" + + "'" + category + "'", wxICON_ERROR); +#endif +} + +void WebViewPanel::sys_color_changed() +{ +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(this); +#endif +} + +ConnectWebViewPanel::ConnectWebViewPanel(wxWindow* parent) + : WebViewPanel(parent, GUI::from_u8(Utils::ServiceConfig::instance().connect_url()), { "_prusaSlicer" }, "connect_loading") +{ + // m_browser->RegisterHandler(wxSharedPtr(new WebViewHandler("https"))); + + auto* plater = wxGetApp().plater(); + plater->Bind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this); + plater->Bind(EVT_UA_LOGGEDOUT, &ConnectWebViewPanel::on_user_logged_out, this); +} + +ConnectWebViewPanel::~ConnectWebViewPanel() +{ + m_browser->Unbind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this); +} + +wxString ConnectWebViewPanel::get_login_script(bool refresh) +{ + 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; + 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) { + retry = true; + } 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()); + 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() +{ + auto javascript = get_login_script(false); + BOOST_LOG_TRIVIAL(debug) << "RunScript " << javascript << "\n"; + m_browser->AddUserScript(javascript); +} + +void ConnectWebViewPanel::on_user_token(UserAccountSuccessEvent& e) +{ + e.Skip(); + 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_logged_out(UserAccountSuccessEvent& e) +{ + e.Skip(); + // 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()); + 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(); + } +} + +void ConnectWebViewPanel::on_connect_action_error(const std::string &message_data) +{ + 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::logout() +{ + wxString script = L"window._prusaConnect_v1.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(true); +} + +PrinterWebViewPanel::PrinterWebViewPanel(wxWindow* parent, const wxString& default_url) + : WebViewPanel(parent, default_url, {}) +{ + if (!m_browser) + return; + + m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PrinterWebViewPanel::on_loaded, this); +#ifndef NDEBUG + m_browser->EnableAccessToDevTools(); + m_browser->EnableContextMenu(); +#endif +} + +void PrinterWebViewPanel::on_loaded(wxWebViewEvent& evt) +{ + if (evt.GetURL().IsEmpty()) + return; + if (!m_api_key.empty()) { + send_api_key(); + } else if (!m_usr.empty() && !m_psk.empty()) { + send_credentials(); + } +} + +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');"); + m_browser->Reload(); + m_api_key_sent = true; + setup_webview_with_credentials(m_browser, m_usr, m_psk); +} + +void PrinterWebViewPanel::sys_color_changed() +{ +} + + +PrintablesWebViewPanel::PrintablesWebViewPanel(wxWindow* parent) + : WebViewPanel(parent, GUI::from_u8(Utils::ServiceConfig::instance().printables_url()), { "ExternalApp" }, "loading") +{ + m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PrintablesWebViewPanel::on_loaded, this); + 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["reloadHomePage"] = std::bind(&PrintablesWebViewPanel::on_reload_event, 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_printables_event_access_token_expired(const std::string& message_data) +{ + // accessTokenExpired + // Printables pozaduje refresh access tokenu, muze byt volano nekolikrat.Nechme na Mobilni aplikaci at zaridi to ze zareaguje jen jednou +} +void PrintablesWebViewPanel::on_printables_event_print_gcode(const std::string& message_data) +{ + // printGcode + // Uzivatel kliknul na tlacitko tisk u gcode. Dalsi posilane atributy jsou url, material, printer, nozzlediam +} +void PrintablesWebViewPanel::on_reload_event(const std::string& message_data) +{ + load_default_url(); +} +/* +Eventy Slicer -> Printables +accessTokenWillChange +WebUI zavola event predtim nez udela refresh access tokenu proti Prusa Accountu na Printables to bude znamenat pozastaveni requestu Mobile app muze chtit udelat refresh i bez explicitni predchozi printables zadosti skrz accessTokenExpired event +accessTokenChange +window postMessage JSON stringify { event 'accessTokenChange' token 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVC' } +volani po uspesne rotaci tokenu +historyBack +navigace zpet triggerovana z mobilni aplikace +historyForward +navigace vpred triggerovana z mobilni aplikace +*/ + +void PrintablesWebViewPanel::on_navigation_request(wxWebViewEvent &evt) +{ + const wxString url = evt.GetURL(); + // download with url + if (url.StartsWith(L"prusaslicer")) { + evt.Veto(); + wxGetApp().printables_request(into_u8(url)); + return; + } + if (url.StartsWith(m_default_url)) { + m_reached_default_url = true; + } else if (m_reached_default_url) { + BOOST_LOG_TRIVIAL(info) << evt.GetURL() << " does not start with default url. Vetoing."; + evt.Veto(); + } + + +} + +void PrintablesWebViewPanel::load_default_url() +{ + std::string actual_default_url = get_url_lang_theme(Utils::ServiceConfig::instance().printables_url()); + 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()) { + delete_cookies(m_browser, Utils::ServiceConfig::instance().printables_url()); + m_browser->AddUserScript("localStorage.clear();"); + load_url(actual_default_url); + return; + } + // add token to first request +#ifdef _WIN32 + add_request_authorization(m_browser, m_default_url, access_token); + load_url(GUI::from_u8(actual_default_url)); +#else + load_request(m_browser, actual_default_url, access_token); +#endif +} + +void PrintablesWebViewPanel::on_loaded(wxWebViewEvent& evt) +{ +#ifdef _WIN32 + // This is needed only once after add_request_authorization + remove_request_authorization(m_browser); +#endif +} + +std::string PrintablesWebViewPanel::get_url_lang_theme(const wxString& url) +{ + // 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::on_show(wxShowEvent& evt) +{ + m_shown = evt.IsShown(); + if (!m_shown) { + return; + } + if (m_load_default_url) { + m_load_default_url = false; + load_default_url(); + return; + } + // in case login changed, resend login / logout + // DK: 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(); + } else { + login(access_token); + } + +} + +void PrintablesWebViewPanel::logout() +{ + if (!m_shown) { + return; + } + delete_cookies(m_browser, Utils::ServiceConfig::instance().printables_url()); + m_browser->RunScript("localStorage.clear();"); + +#ifdef _WIN32 + load_url(GUI::from_u8(get_url_lang_theme(m_browser->GetCurrentURL()))); +#else + // We cannot do simple reload here, it would keep the access token in the header + load_request(m_browser, get_url_lang_theme(m_browser->GetCurrentURL()), std::string()); +#endif // + +} +void PrintablesWebViewPanel::login(const std::string access_token) +{ + if (!m_shown) { + return; + } + // 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); + + run_script("window.location.reload();"); +} +void PrintablesWebViewPanel::send_refreshed_token(const std::string access_token) +{ + if (m_load_default_url) { + return; + } + 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; + } + wxString script = "window.postMessage(JSON.stringify({ event: 'accessTokenWillChange' }))"; + run_script(script); +} + +void PrintablesWebViewPanel::on_script_message(wxWebViewEvent& evt) +{ + BOOST_LOG_TRIVIAL(error) << "received message from Printables: " << evt.GetString(); + handle_message(into_u8(evt.GetString())); +} + +void PrintablesWebViewPanel::sys_color_changed() +{ + if (m_shown) { + load_url(GUI::from_u8(get_url_lang_theme(m_browser->GetCurrentURL()))); + } + WebViewPanel::sys_color_changed(); +} + +} // namespace slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/WebViewPanel.hpp b/src/slic3r/GUI/WebViewPanel.hpp new file mode 100644 index 0000000000..eaeeb433de --- /dev/null +++ b/src/slic3r/GUI/WebViewPanel.hpp @@ -0,0 +1,195 @@ +#ifndef slic3r_WebViewPanel_hpp_ +#define slic3r_WebViewPanel_hpp_ + +#include +#include +#include + +#include "GUI_Utils.hpp" +#include "UserAccountSession.hpp" +#include "ConnectRequestHandler.hpp" + +#ifdef DEBUG_URL_PANEL +#include +#endif + +class wxWebView; +class wxWebViewEvent; + +wxDECLARE_EVENT(EVT_OPEN_EXTERNAL_LOGIN, wxCommandEvent); + +namespace Slic3r::GUI { + +class WebViewPanel : public wxPanel +{ +public: + WebViewPanel(wxWindow *parent, const wxString& default_url, const std::vector& message_handler_names, const std::string& loading_html = "loading"); + virtual ~WebViewPanel(); + + void load_url(const wxString& url); + void load_default_url_delayed(); + void load_error_page(); + + virtual void on_show(wxShowEvent& evt); + virtual void on_script_message(wxWebViewEvent& evt); + + void on_idle(wxIdleEvent& evt); + void on_url(wxCommandEvent& evt); + virtual void on_back_button(wxCommandEvent& evt); + virtual void on_forward_button(wxCommandEvent& evt); + void on_stop_button(wxCommandEvent& evt); + virtual void on_reload_button(wxCommandEvent& evt); + + void on_view_source_request(wxCommandEvent& evt); + void on_view_text_request(wxCommandEvent& evt); + void on_tools_clicked(wxCommandEvent& evt); + void on_error(wxWebViewEvent& evt); + + void run_script(const wxString& javascript); + void on_run_script_custom(wxCommandEvent& evt); + void on_add_user_script(wxCommandEvent& evt); + void on_set_custom_user_agent(wxCommandEvent& evt); + void on_clear_selection(wxCommandEvent& evt); + void on_delete_selection(wxCommandEvent& evt); + void on_select_all(wxCommandEvent& evt); + void On_enable_context_menu(wxCommandEvent& evt); + void On_enable_dev_tools(wxCommandEvent& evt); + virtual void on_navigation_request(wxWebViewEvent &evt); + + wxString get_default_url() const { return m_default_url; } + void set_default_url(const wxString& url) { m_default_url = url; } + + virtual void sys_color_changed(); + + void set_load_default_url_on_next_error(bool val) { m_load_default_url_on_next_error = val; } + +protected: + virtual void on_page_will_load(); + + wxWebView* m_browser { nullptr }; + bool m_load_default_url { false }; + + wxBoxSizer* topsizer; + wxBoxSizer* m_sizer_top; +#ifdef DEBUG_URL_PANEL + + wxBoxSizer *bSizer_toolbar; + wxButton * m_button_back; + wxButton * m_button_forward; + wxButton * m_button_stop; + wxButton * m_button_reload; + wxTextCtrl *m_url; + wxButton * m_button_tools; + + wxMenu* m_tools_menu; + wxMenuItem* m_script_custom; + + wxInfoBar *m_info; + wxStaticText* m_info_text; + + wxMenuItem* m_context_menu; + wxMenuItem* m_dev_tools; +#endif + + // Last executed JavaScript snippet, for convenience. + wxString m_javascript; + wxString m_response_js; + wxString m_default_url; + bool m_reached_default_url {false}; + + std::string m_loading_html; + //DECLARE_EVENT_TABLE() + + bool m_load_error_page { false }; + bool m_shown { false }; + bool m_load_default_url_on_next_error { false }; + + std::vector m_script_message_hadler_names; +}; + +class ConnectWebViewPanel : public WebViewPanel, public ConnectRequestHandler +{ +public: + ConnectWebViewPanel(wxWindow* parent); + ~ConnectWebViewPanel() override; + void on_script_message(wxWebViewEvent& evt) override; + void logout(); + void sys_color_changed() override; + void on_navigation_request(wxWebViewEvent &evt) override; +protected: + void on_connect_action_request_login(const std::string &message_data) override; + void on_connect_action_select_printer(const std::string& message_data) override; + void on_connect_action_print(const std::string& message_data) override; + void on_connect_action_webapp_ready(const std::string& message_data) override {} + void run_script_bridge(const wxString& script) override {run_script(script); } + void on_page_will_load() override; + void on_connect_action_error(const std::string &message_data) override; +private: + static wxString get_login_script(bool refresh); + static wxString get_logout_script(); + void on_user_token(UserAccountSuccessEvent& e); + void on_user_logged_out(UserAccountSuccessEvent& e); +}; + +class PrinterWebViewPanel : public WebViewPanel +{ +public: + PrinterWebViewPanel(wxWindow* parent, const wxString& default_url); + + void on_loaded(wxWebViewEvent& evt); + + void send_api_key(); + void send_credentials(); + void set_api_key(const std::string &key) + { + if (m_api_key != key) { + clear(); + m_api_key = key; + } + } + void set_credentials(const std::string &usr, const std::string &psk) + { + if (m_usr != usr || m_psk != psk) { + clear(); + m_usr = usr; + m_psk = psk; + } + } + void clear() { m_api_key.clear(); m_usr.clear(); m_psk.clear(); m_api_key_sent = false; } + void sys_color_changed() override; +private: + std::string m_api_key; + std::string m_usr; + std::string m_psk; + bool m_api_key_sent {false}; +}; + +class PrintablesWebViewPanel : public WebViewPanel +{ +public: + PrintablesWebViewPanel(wxWindow* parent); + void on_navigation_request(wxWebViewEvent &evt) override; + void on_loaded(wxWebViewEvent& evt); + void on_show(wxShowEvent& evt) override; + void on_script_message(wxWebViewEvent& evt) override; + void sys_color_changed() override; + + void logout(); + void login(const std::string access_token); + void send_refreshed_token(const std::string access_token); + void send_will_refresh(); +private: + void handle_message(const std::string& message); + void on_printables_event_access_token_expired(const std::string& message_data); + void on_printables_event_print_gcode(const std::string& message_data); + void on_reload_event(const std::string& message_data); + + void load_default_url(); + std::string get_url_lang_theme(const wxString& url); + + std::map> m_events; + +}; +} // namespace Slic3r::GUI + +#endif /* slic3r_WebViewPanel_hpp_ */ \ No newline at end of file diff --git a/src/slic3r/GUI/WebViewPlatformUtils.hpp b/src/slic3r/GUI/WebViewPlatformUtils.hpp index 2c1429d21a..f6d5a8b2ed 100644 --- a/src/slic3r/GUI/WebViewPlatformUtils.hpp +++ b/src/slic3r/GUI/WebViewPlatformUtils.hpp @@ -7,5 +7,8 @@ namespace Slic3r::GUI { void setup_webview_with_credentials(wxWebView* web_view, const std::string& username, const std::string& password); void remove_webview_credentials(wxWebView* web_view); void delete_cookies(wxWebView* web_view, const std::string& url); + void add_request_authorization(wxWebView* web_view, const wxString& address, const std::string& token); + void remove_request_authorization(wxWebView* web_view); + void load_request(wxWebView* web_view, const std::string& address, const std::string& token); } diff --git a/src/slic3r/GUI/WebViewPlatformUtilsLinux.cpp b/src/slic3r/GUI/WebViewPlatformUtilsLinux.cpp index 35161a1c45..80290293c6 100644 --- a/src/slic3r/GUI/WebViewPlatformUtilsLinux.cpp +++ b/src/slic3r/GUI/WebViewPlatformUtilsLinux.cpp @@ -6,6 +6,7 @@ #include "WebViewPlatformUtils.hpp" #include +#include namespace Slic3r::GUI { @@ -60,7 +61,7 @@ void delete_cookie_callback (GObject* source_object, GAsyncResult* result, void* { WebKitCookieManager *cookie_manager = WEBKIT_COOKIE_MANAGER(source_object); GError* err = nullptr; - gboolean b = webkit_cookie_manager_delete_cookie_finish(cookie_manager, result, &err); + webkit_cookie_manager_delete_cookie_finish(cookie_manager, result, &err); if (err) { BOOST_LOG_TRIVIAL(error) << "Error deleting cookies: " << err->message; g_error_free(err); @@ -107,4 +108,40 @@ void delete_cookies(wxWebView* web_view, const std::string& url) WebKitCookieManager* cookieManager = webkit_web_context_get_cookie_manager(context); webkit_cookie_manager_get_cookies(cookieManager, uri, nullptr, (GAsyncReadyCallback)Slic3r::GUI::get_cookie_callback, nullptr); } + +void add_request_authorization(wxWebView* web_view, const wxString& address, const std::string& token) +{ + // unused on Linux + assert(true); +} +void remove_request_authorization(wxWebView* web_view) +{ + // unused on Linux + assert(true); +} + +void load_request(wxWebView* web_view, const std::string& address, const std::string& token) +{ + WebKitWebView* native_backend = static_cast(web_view->GetNativeBackend()); + WebKitURIRequest* request = webkit_uri_request_new(address.c_str()); + if(!request) + { + BOOST_LOG_TRIVIAL(error) << "load_request failed: request is nullptr. address: " << address; + return; + } + SoupMessageHeaders* soup_headers = webkit_uri_request_get_http_headers(request); + if (!soup_headers) + { + BOOST_LOG_TRIVIAL(error) << "load_request failed: soup_headers is nullptr."; + return; + } + if (!token.empty()) + { + soup_message_headers_append(soup_headers, "Authorization", ("External " + token).c_str()); + } + + // Load the request in the WebView + webkit_web_view_load_request(native_backend, request); +} + } diff --git a/src/slic3r/GUI/WebViewPlatformUtilsMac.mm b/src/slic3r/GUI/WebViewPlatformUtilsMac.mm index b09c69f31f..ad04db60d2 100644 --- a/src/slic3r/GUI/WebViewPlatformUtilsMac.mm +++ b/src/slic3r/GUI/WebViewPlatformUtilsMac.mm @@ -155,7 +155,27 @@ void delete_cookies(wxWebView* web_view, const std::string& url) } } }]; - +} +void add_request_authorization(wxWebView* web_view, const wxString& address, const std::string& token) +{ + // unused on MacOS + assert(true); +} +void remove_request_authorization(wxWebView* web_view) +{ + // unused on MacOS + assert(true); +} +void load_request(wxWebView* web_view, const std::string& address, const std::string& token) +{ + WKWebView* backend = static_cast(web_view->GetNativeBackend()); + NSString *url_string = [NSString stringWithCString:address.c_str() encoding:[NSString defaultCStringEncoding]]; + NSString *token_string = [NSString stringWithCString:token.c_str() encoding:[NSString defaultCStringEncoding]]; + NSURL *url = [NSURL URLWithString:url_string]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + NSString *auth_value = [NSString stringWithFormat:@"External %@", token_string]; + [request setValue:auth_value forHTTPHeaderField:@"Authorization"]; + [backend loadRequest:request]; } } diff --git a/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp b/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp index 4e98a0d7dc..7445031258 100644 --- a/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp +++ b/src/slic3r/GUI/WebViewPlatformUtilsWin32.cpp @@ -135,6 +135,7 @@ void delete_cookies(wxWebView* webview, const std::string& url) std::string domain = cookie.second.get("domain"); // Delete cookie by name and domain wxString name_and_domain = GUI::format_wxstr(L"{\"name\": \"%1%\", \"domain\": \"%2%\"}", name, domain); + BOOST_LOG_TRIVIAL(debug) << "Deleting cookie: " << name_and_domain; webView2->CallDevToolsProtocolMethod(L"Network.deleteCookies", name_and_domain.c_str(), Microsoft::WRL::Callback( [](HRESULT errorCode, LPCWSTR resultJson) -> HRESULT { return S_OK; }).Get()); @@ -145,6 +146,150 @@ void delete_cookies(wxWebView* webview, const std::string& url) } +static EventRegistrationToken m_webResourceRequestedTokenForImageBlocking = {}; +static wxString filter_patern; +namespace { +void RequestHeadersToLog(ICoreWebView2HttpRequestHeaders* requestHeaders) +{ + wxCOMPtr iterator; + requestHeaders->GetIterator(&iterator); + BOOL hasCurrent = FALSE; + BOOST_LOG_TRIVIAL(info) <<"Logging request headers:"; + + while (SUCCEEDED(iterator->get_HasCurrentHeader(&hasCurrent)) && hasCurrent) + { + wchar_t* name = nullptr; + wchar_t* value = nullptr; + + iterator->GetCurrentHeader(&name, &value); + BOOST_LOG_TRIVIAL(debug) <<"name: " << name << L", value: " << value; + if (name) { + CoTaskMemFree(name); + } + if (value) { + CoTaskMemFree(value); + } + + BOOL hasNext = FALSE; + iterator->MoveNext(&hasNext); + } +} +} + +void add_request_authorization(wxWebView* webview, const wxString& address, const std::string& token) +{ + // This function adds a filter so when pattern document is being requested, callback is triggered + // Inside add_WebResourceRequested callback, there is a Authorization header added. + // The filter needs to be removed to stop adding the auth header + ICoreWebView2 *webView2 = static_cast(webview->GetNativeBackend()); + if (!webView2) { + return; + } + wxCOMPtr wv2_2; + HRESULT hr = webView2->QueryInterface(IID_PPV_ARGS(&wv2_2)); + if (FAILED(hr)) { + return; + } + filter_patern = address + "/*"; + webView2->AddWebResourceRequestedFilter( filter_patern.c_str(), COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT); + + if (FAILED(webView2->add_WebResourceRequested( + Microsoft::WRL::Callback( + [token](ICoreWebView2 *sender, ICoreWebView2WebResourceRequestedEventArgs *args) { + // Get the web resource request + wxCOMPtr request; + HRESULT hr = args->get_Request(&request); + if (FAILED(hr)) + { + BOOST_LOG_TRIVIAL(error) << "Adding request Authorization: Failed to get_Request."; + return S_OK; + } + // Get the request headers + wxCOMPtr headers; + hr = request->get_Headers(&headers); + if (FAILED(hr)) + { + BOOST_LOG_TRIVIAL(error) << "Adding request Authorization: Failed to get_Headers."; + return S_OK; + } + LPWSTR wideUri = nullptr; + request->get_Uri(&wideUri); + std::wstring ws(wideUri); + + std::string val = "External " + token; + // Add or modify the Authorization header + hr = headers->SetHeader(L"Authorization", GUI::from_u8(val).c_str()); + BOOST_LOG_TRIVIAL(debug) << "add_WebResourceRequested " << ws; + + // This function is only needed for debug purpose + RequestHeadersToLog(headers.Get()); + return S_OK; + } + ).Get(), &m_webResourceRequestedTokenForImageBlocking + ))) { + + BOOST_LOG_TRIVIAL(error) << "Adding request Authorization: Failed to add callback."; + } + + +} + +void remove_request_authorization(wxWebView* webview) +{ + ICoreWebView2 *webView2 = static_cast(webview->GetNativeBackend()); + if (!webView2) { + return; + } + webView2->RemoveWebResourceRequestedFilter(filter_patern.c_str(), COREWEBVIEW2_WEB_RESOURCE_CONTEXT_DOCUMENT); + if(FAILED(webView2->remove_WebResourceRequested( m_webResourceRequestedTokenForImageBlocking))) { + BOOST_LOG_TRIVIAL(error) << "WebView: Failed to remove resources"; + } +} + +void load_request(wxWebView* web_view, const std::string& address, const std::string& token) +{ + // This function should create its own GET request and send it (works on linux) + // For that we would use NavigateWithWebResourceRequest. + // For that we need ICoreWebView2Environment smart pointer. + // Such pointer does exists inside wxWebView edge backend. (wxWebViewEdgeImpl::m_webViewEnvironment) + // But its currently private and not getable. (It wouldn't be such problem to create the getter) + + ICoreWebView2 *webView2 = static_cast(web_view->GetNativeBackend()); + if (!webView2) { + return; + } + + // GetEnviroment does not exists + wxCOMPtr webViewEnvironment; + //webViewEnvironment = static_cast(web_view->GetEnviroment()); + if (!webViewEnvironment.Get()) { + return; + } + + wxCOMPtr webViewEnvironment2; + if (FAILED(webViewEnvironment->QueryInterface(IID_PPV_ARGS(&webViewEnvironment2)))) + { + return; + } + wxCOMPtr webResourceRequest; + + if (FAILED(webViewEnvironment2->CreateWebResourceRequest( + L"https://www.printables.com/", L"GET", NULL, + L"Content-Type: application/x-www-form-urlencoded", &webResourceRequest))) + { + return; + } + wxCOMPtr wv2_2; + if (FAILED(webView2->QueryInterface(IID_PPV_ARGS(&wv2_2)))) { + return; + } + if (FAILED(wv2_2->NavigateWithWebResourceRequest(webResourceRequest.get()))) + { + return; + } + +} + } // namespace Slic3r::GUI #endif // WIN32 diff --git a/src/slic3r/Utils/ServiceConfig.cpp b/src/slic3r/Utils/ServiceConfig.cpp index e5bf44141c..84b66bb2e9 100644 --- a/src/slic3r/Utils/ServiceConfig.cpp +++ b/src/slic3r/Utils/ServiceConfig.cpp @@ -24,7 +24,9 @@ ServiceConfig::ServiceConfig() , m_account_url("https://account.prusa3d.com") , m_account_client_id("oamhmhZez7opFosnwzElIgE2oGgI2iJORSkw587O") , m_media_url("https://media.printables.com") - , m_preset_repo_url("https://preset-repo-api.prusa3d.com") { + , m_preset_repo_url("https://preset-repo-api.prusa3d.com") + , m_printables_url("https://www.printables.com") +{ #ifdef SLIC3R_REPO_URL m_preset_repo_url = SLIC3R_REPO_URL; #endif @@ -34,6 +36,7 @@ ServiceConfig::ServiceConfig() update_from_env(m_account_client_id, "PRUSA_ACCOUNT_CLIENT_ID"); update_from_env(m_media_url, "PRUSA_MEDIA_URL", true); update_from_env(m_preset_repo_url, "PRUSA_PRESET_REPO_URL", true); + update_from_env(m_printables_url, "PRUSA_PRINTABLES_URL", true); } ServiceConfig& ServiceConfig::instance() diff --git a/src/slic3r/Utils/ServiceConfig.hpp b/src/slic3r/Utils/ServiceConfig.hpp index fdcf40d034..6bfe5d9a94 100644 --- a/src/slic3r/Utils/ServiceConfig.hpp +++ b/src/slic3r/Utils/ServiceConfig.hpp @@ -30,6 +30,8 @@ public: bool webdev_enabled() const { return m_webdev_enabled; } void set_webdev_enabled(bool enabled) { m_webdev_enabled = enabled; } + const std::string& printables_url() const { return m_printables_url; } + static ServiceConfig& instance(); private: std::string m_connect_url; @@ -37,6 +39,7 @@ private: std::string m_account_client_id; std::string m_media_url; std::string m_preset_repo_url; + std::string m_printables_url; bool m_webdev_enabled{false}; };