diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp index 78a511c90c..5301b91d86 100644 --- a/src/slic3r/GUI/GUI.cpp +++ b/src/slic3r/GUI/GUI.cpp @@ -14,6 +14,9 @@ #include #include +#include +#include + #if __APPLE__ #import #elif _WIN32 @@ -531,6 +534,11 @@ boost::filesystem::path into_path(const wxString &str) return boost::filesystem::path(str.wx_str()); } +wxString file_url_from_path(const boost::filesystem::path &path) +{ + return wxFileSystem::FileNameToURL(wxFileName(from_path(path))); +} + void about() { AboutDialog dlg; diff --git a/src/slic3r/GUI/GUI.hpp b/src/slic3r/GUI/GUI.hpp index db882b79cf..0b3ac6d7b5 100644 --- a/src/slic3r/GUI/GUI.hpp +++ b/src/slic3r/GUI/GUI.hpp @@ -76,6 +76,9 @@ std::string into_u8(const wxString &str); wxString from_path(const boost::filesystem::path &path); // boost path from wxString boost::filesystem::path into_path(const wxString &str); +// file:// URL of a local path, percent-encoded so characters such as '#', '%' and '?' stay part of the path. +// Append any query or fragment to the result. +wxString file_url_from_path(const boost::filesystem::path &path); // Display an About dialog extern void about(); diff --git a/src/slic3r/GUI/MarkdownTip.cpp b/src/slic3r/GUI/MarkdownTip.cpp index 7ffeae9e36..a22f4da87f 100644 --- a/src/slic3r/GUI/MarkdownTip.cpp +++ b/src/slic3r/GUI/MarkdownTip.cpp @@ -98,10 +98,7 @@ void MarkdownTip::LoadStyle() ph /= "tooltip/styled.html"; _data_dir = false; } - auto url = ph.string(); - std::replace(url.begin(), url.end(), '\\', '/'); - url = "file:///" + url; - _tipView->LoadURL(from_u8(url)); + _tipView->LoadURL(file_url_from_path(ph)); _lastTip.clear(); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 951739e213..908034b412 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3483,7 +3483,7 @@ void Sidebar::update_all_preset_comboboxes() wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString apikey; if(url.empty()) - url = wxString::Format("file://%s/web/orca/missing_connection.html", from_u8(resources_dir())); + url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/orca/missing_connection.html"); else { const auto host_type = cfg.option>("host_type")->value; if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) diff --git a/src/slic3r/GUI/PrivacyUpdateDialog.cpp b/src/slic3r/GUI/PrivacyUpdateDialog.cpp index c417767a42..c396e85bfe 100644 --- a/src/slic3r/GUI/PrivacyUpdateDialog.cpp +++ b/src/slic3r/GUI/PrivacyUpdateDialog.cpp @@ -57,9 +57,7 @@ PrivacyUpdateDialog::PrivacyUpdateDialog(wxWindow* parent, wxWindowID id, const fs::path ph(resources_dir()); ph /= "tooltip/privacyupdate.html"; - m_host_url = ph.string(); - std::replace(m_host_url.begin(), m_host_url.end(), '\\', '/'); - m_host_url = "file:///" + m_host_url; + m_host_url = into_u8(file_url_from_path(ph)); m_vebview_release_note->LoadURL(from_u8(m_host_url)); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); diff --git a/src/slic3r/GUI/Project.cpp b/src/slic3r/GUI/Project.cpp index 8d87939afc..7833cc7499 100644 --- a/src/slic3r/GUI/Project.cpp +++ b/src/slic3r/GUI/Project.cpp @@ -45,10 +45,10 @@ const std::vector license_list = { ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style) { SetBackgroundColour(*wxWHITE); - m_project_home_url = wxString::Format("file://%s/web/model/index.html", from_u8(resources_dir())); + m_project_home_url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/model/index.html"); wxString strlang = wxGetApp().current_language_code_safe(); if (strlang != "") - m_project_home_url = wxString::Format("file://%s/web/model/index.html?lang=%s", from_u8(resources_dir()), strlang); + m_project_home_url += "?lang=" + strlang; wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index c8338dc79a..58b3a40ca9 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -510,7 +510,7 @@ void UpdateVersionDialog::update_version_info(wxString release_note, wxString ve out_buf->append(text, size); }, (void*) &html_source, MD_DIALECT_GITHUB | MD_FLAG_STRIKETHROUGH | MD_FLAG_WIKILINKS, 0); html_source.append(""); - m_vebview_release_note->LoadURL("file://" + (boost::filesystem::path (resources_dir()) / "web/guide/0/index.html").string()); + m_vebview_release_note->LoadURL(file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/0/index.html")); SetMinSize(GetSize()); SetMaxSize(GetSize()); diff --git a/src/slic3r/GUI/WebDownPluginDlg.cpp b/src/slic3r/GUI/WebDownPluginDlg.cpp index dca17110f9..6358582831 100644 --- a/src/slic3r/GUI/WebDownPluginDlg.cpp +++ b/src/slic3r/GUI/WebDownPluginDlg.cpp @@ -38,9 +38,7 @@ DownPluginFrame::DownPluginFrame(GUI_App *pGUI) : wxDialog((wxWindow *) (pGUI->m // set the frame icon wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL); - wxString TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/6/index.html").make_preferred().string()); - - TargetUrl = "file://" + TargetUrl; + wxString TargetUrl = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/6/index.html"); // Create the webview m_browser = WebView::CreateWebView(this, TargetUrl); diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp index 9cea8bec28..e9e74b798e 100644 --- a/src/slic3r/GUI/WebGuideDialog.cpp +++ b/src/slic3r/GUI/WebGuideDialog.cpp @@ -218,37 +218,38 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load) m_page = startpage; BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(" enter, load=%1%, start_page=%2%")%load%int(startpage); //wxLogMessage("GUIDE: webpage_1 %s", (boost::filesystem::path(resources_dir()) / "web\\guide\\1\\index.html").make_preferred().string().c_str() ); - wxString TargetUrl = from_u8( (boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=1").make_preferred().string() ); + const wxString guide_url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/0/index.html"); + wxString TargetUrl = guide_url + "?target=1"; //wxLogMessage("GUIDE: webpage_2 %s", TargetUrl.mb_str()); if (startpage == BBL_WELCOME){ SetTitle(_L("Setup Wizard")); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=1").make_preferred().string()); + TargetUrl = guide_url + "?target=1"; } else if (startpage == BBL_REGION) { SetTitle(_L("Setup Wizard")); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=11").make_preferred().string()); + TargetUrl = guide_url + "?target=11"; } else if (startpage == BBL_MODELS) { SetTitle(_L("Setup Wizard")); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string()); + TargetUrl = guide_url + "?target=21"; } else if (startpage == BBL_FILAMENTS) { SetTitle(_L("Setup Wizard")); int nSize = m_ProfileJson["model"].size(); if (nSize>0) - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=22").make_preferred().string()); + TargetUrl = guide_url + "?target=22"; else - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string()); + TargetUrl = guide_url + "?target=21"; } else if (startpage == BBL_FILAMENT_ONLY) { SetTitle(""); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=23").make_preferred().string()); + TargetUrl = guide_url + "?target=23"; } else if (startpage == BBL_MODELS_ONLY) { SetTitle(""); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=24").make_preferred().string()); + TargetUrl = guide_url + "?target=24"; } else { SetTitle(_L("Setup Wizard")); - TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string()); + TargetUrl = guide_url + "?target=21"; } wxString strlang = wxGetApp().current_language_code_safe(); @@ -256,7 +257,6 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load) if (strlang != "") TargetUrl = wxString::Format("%s&lang=%s", w2s(TargetUrl), strlang); - TargetUrl = "file://" + TargetUrl; if (load) load_url(TargetUrl); @@ -1284,7 +1284,7 @@ bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_r entry["vendor"] = vp.id; entry["nozzle_diameter"] = nozzle_str; entry["materials"] = materials_str; - entry["cover"] = cover_path.string(); + entry["cover"] = into_u8(file_url_from_path(cover_path)); entry["nozzle_selected"] = ""; entry["sub_path"] = ""; m_ProfileJson["model"].push_back(entry); @@ -1732,7 +1732,7 @@ int GuideFrame::LoadProfileFamily(std::string strVendor, std::string strFilePath cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file) .make_preferred(); } - OneModel["cover"] = cover_path.string(); + OneModel["cover"] = into_u8(file_url_from_path(cover_path)); OneModel["nozzle_selected"] = ""; diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index f20d24163c..a98b196eb1 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -36,10 +36,10 @@ namespace GUI { WebViewPanel::WebViewPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) { - wxString url = wxString::Format("file://%s/web/homepage/index.html", from_u8(resources_dir())); + wxString url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/homepage/index.html"); wxString strlang = wxGetApp().current_language_code_safe(); if (strlang != "") - url = wxString::Format("file://%s/web/homepage/index.html?lang=%s", from_u8(resources_dir()), strlang); + url += "?lang=" + strlang; wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/Widgets/WebHosting.cpp b/src/slic3r/GUI/Widgets/WebHosting.cpp index a1b0df496a..b6af6d3a39 100644 --- a/src/slic3r/GUI/Widgets/WebHosting.cpp +++ b/src/slic3r/GUI/Widgets/WebHosting.cpp @@ -48,20 +48,19 @@ constexpr char ORCA_BRIDGE_JS[] = R"JS( wxString bootstrap_url() { - return wxString("file://") + from_u8((boost::filesystem::path(resources_dir()) / BOOTSTRAP_PAGE).make_preferred().string()); + return file_url_from_path(boost::filesystem::path(resources_dir()) / BOOTSTRAP_PAGE); } wxString content_base_url() { - const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string(); - return wxString("file://") + from_u8(dir) + "/"; + return file_url_from_path(boost::filesystem::path(resources_dir()) / "web") + "/"; } bool is_content_url(const wxString& url) { - // The web view reports the URL it parsed, which escapes anything the resources path holds - // (a space, a non-ASCII character), while content_base_url() is the raw path. - return wxURI::Unescape(url.BeforeFirst('#')) == content_base_url(); + // The web view reports the URL it parsed, which may escape the resources path differently + // from content_base_url(). + return wxURI::Unescape(url.BeforeFirst('#')) == wxURI::Unescape(content_base_url()); } const char* orca_bridge_script() { return ORCA_BRIDGE_JS; } diff --git a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp index e4e3d2878a..3c14ab37e1 100644 --- a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp +++ b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp @@ -192,7 +192,7 @@ bool WebViewHostDialog::create_webview(const std::string& resource_path, wxString WebViewHostDialog::build_resource_url(const std::string& resource_path) const { - wxString target_url = from_u8((boost::filesystem::path(resources_dir()) / resource_path).make_preferred().string()); + wxString target_url = file_url_from_path(boost::filesystem::path(resources_dir()) / resource_path); if (append_language_to_url()) { const wxString lang = wxGetApp().current_language_code_safe(); @@ -202,7 +202,7 @@ wxString WebViewHostDialog::build_resource_url(const std::string& resource_path) } } - return wxString("file://") + target_url; + return target_url; } void WebViewHostDialog::load_url(const wxString& url) diff --git a/src/slic3r/Utils/ElegooLink.cpp b/src/slic3r/Utils/ElegooLink.cpp index 03b7bdc6c7..64ff4a9455 100644 --- a/src/slic3r/Utils/ElegooLink.cpp +++ b/src/slic3r/Utils/ElegooLink.cpp @@ -340,9 +340,7 @@ namespace Slic3r { if (classify_printer_model(config->opt_string("printer_model")) != ElegooPrinterType::CC2) return fallback_webui; - std::string web_path = resources_dir() + "/web/elegoolink/lan_service_web/index.html"; - std::replace(web_path.begin(), web_path.end(), '\\', '/'); - web_path = "file://" + web_path; + std::string web_path = GUI::into_u8(GUI::file_url_from_path(boost::filesystem::path(resources_dir()) / "web/elegoolink/lan_service_web/index.html")); const std::string token = get_cc2_token(config->opt_string("printhost_apikey")); const std::string host_ip = Http::get_host_header_value(host); diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 7f541f3701..032445415a 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -25,6 +25,7 @@ add_executable(${_TEST_NAME}_tests test_plugin_cloud_metadata.cpp test_plugin_audit.cpp test_shortcuts.cpp + test_file_url.cpp ../fff_print/test_helpers.cpp ) diff --git a/tests/slic3rutils/test_file_url.cpp b/tests/slic3rutils/test_file_url.cpp new file mode 100644 index 0000000000..60881cd92b --- /dev/null +++ b/tests/slic3rutils/test_file_url.cpp @@ -0,0 +1,69 @@ +#include +#include + +#include "slic3r/GUI/GUI.hpp" + +#include + +#include + +using namespace Slic3r::GUI; + +#ifndef _WIN32 +TEST_CASE("A file URL keeps characters special to URLs in its path", "[FileUrl]") +{ + const std::string path = GENERATE(as{}, + "/opt/test#dir/resources/web/homepage/index.html", + "/opt/test%20x/resources/web/homepage/index.html", + "/opt/Orca Slicer/resources/web/homepage/index.html", + "/opt/what?/resources/web/homepage/index.html", + "/home/Jos\xC3\xA9/\xE8\xB5\x84\xE6\xBA\x90/resources/web/homepage/index.html"); + + const wxString url = file_url_from_path(boost::filesystem::path(path)); + CAPTURE(path, url.utf8_string()); + + // WebView::CreateWebView() and WebView::LoadUrl() re-parse the URL before loading it. + const wxURI uri(wxURI(url).BuildURI()); + CHECK(uri.GetScheme() == "file"); + CHECK(!uri.HasQuery()); + CHECK(!uri.HasFragment()); + CHECK(wxURI::Unescape(uri.GetPath()).utf8_string() == path); +} + +TEST_CASE("A file URL of a plain path is the path behind file://", "[FileUrl]") +{ + const std::string path = "/opt/OrcaSlicer/resources/web/homepage/index.html"; + CHECK(file_url_from_path(boost::filesystem::path(path)) == "file://" + path); +} + +TEST_CASE("A query appended to a file URL stays separate from its path", "[FileUrl]") +{ + const std::string path = "/opt/test#dir%20x/resources/web/guide/0/index.html"; + const wxURI uri(file_url_from_path(boost::filesystem::path(path)) + "?target=21&lang=de"); + CHECK(wxURI::Unescape(uri.GetPath()).utf8_string() == path); + CHECK(uri.GetQuery() == "target=21&lang=de"); + CHECK(!uri.HasFragment()); +} +#else +TEST_CASE("A file URL of a Windows path has a drive letter and forward slashes", "[FileUrl]") +{ + const auto [path, url] = GENERATE(table({ + { L"C:\\Program Files\\OrcaSlicer\\resources\\web\\homepage\\index.html", + "file:///C:/Program%20Files/OrcaSlicer/resources/web/homepage/index.html" }, + { L"D:\\#OneDrive\\OrcaSlicer\\resources\\web\\guide\\0\\index.html", + "file:///D:/%23OneDrive/OrcaSlicer/resources/web/guide/0/index.html" }, + { L"D:\\100%\\OrcaSlicer\\resources\\web\\homepage\\index.html", + "file:///D:/100%25/OrcaSlicer/resources/web/homepage/index.html" }, + })); + CHECK(file_url_from_path(boost::filesystem::path(path)).utf8_string() == url); +} + +TEST_CASE("A query appended to a Windows file URL stays separate from its path", "[FileUrl]") +{ + const wxURI uri(file_url_from_path(boost::filesystem::path(L"D:\\#OneDrive\\OrcaSlicer\\resources\\web\\guide\\0\\index.html")) + + "?target=21&lang=de"); + CHECK(wxURI::Unescape(uri.GetPath()) == "/D:/#OneDrive/OrcaSlicer/resources/web/guide/0/index.html"); + CHECK(uri.GetQuery() == "target=21&lang=de"); + CHECK(!uri.HasFragment()); +} +#endif diff --git a/tests/slic3rutils/test_plugin_host_api.cpp b/tests/slic3rutils/test_plugin_host_api.cpp index e02864f703..6e1b8fd92a 100644 --- a/tests/slic3rutils/test_plugin_host_api.cpp +++ b/tests/slic3rutils/test_plugin_host_api.cpp @@ -16,6 +16,8 @@ #include +#include + namespace py = pybind11; namespace { @@ -210,17 +212,16 @@ TEST_CASE("A reloaded plugin page is recognised by its base URL, fragment aside" { using namespace Slic3r::GUI::web_hosting; - // A resources path holding a space, which the web view reports escaped. + // A resources path holding a space, which the web view may report escaped differently. const Slic3r::ScopedResourcesDir resources("web content check"); // The swapped-in page, then after an in-page anchor and a reload. CHECK(is_content_url(content_base_url())); CHECK(is_content_url(content_base_url() + "#tab2")); - wxString escaped = content_base_url(); - escaped.Replace(" ", "%20"); - REQUIRE(escaped != content_base_url()); - CHECK(is_content_url(escaped)); - CHECK(is_content_url(escaped + "#tab2")); + const wxString unescaped = wxURI::Unescape(content_base_url()); + REQUIRE(unescaped != content_base_url()); + CHECK(is_content_url(unescaped)); + CHECK(is_content_url(unescaped + "#tab2")); // A page the plugin linked to keeps its own URL and must be left alone. CHECK_FALSE(is_content_url(content_base_url() + "guide.html")); CHECK_FALSE(is_content_url("https://example.com/"));