Percent-Encode Local File URLs for Embedded Web Pages

The Home tab, setup wizard, Project tab and other embedded pages were
loaded from file:// URLs built by pasting the resources path into a
string. A '#', '%' or '?' in the install path was then read as a URL
fragment, escape or query, so the pages failed to load, for example a
portable install under D:\#OneDrive showed a directory listing instead
of the setup wizard.

Add file_url_from_path(), built on wxFileSystem::FileNameToURL, and use
it wherever a local page or image URL is built from a path. Queries such
as ?lang= are appended after the path is encoded. The wizard's printer
cover images are passed to the page as file URLs too.
This commit is contained in:
Hanif Koh
2026-09-28 19:32:04 +08:00
parent 576cce2f72
commit adf92e1c83
16 changed files with 117 additions and 45 deletions
+8
View File
@@ -14,6 +14,9 @@
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <boost/any.hpp> #include <boost/any.hpp>
#include <wx/filename.h>
#include <wx/filesys.h>
#if __APPLE__ #if __APPLE__
#import <IOKit/pwr_mgt/IOPMLib.h> #import <IOKit/pwr_mgt/IOPMLib.h>
#elif _WIN32 #elif _WIN32
@@ -531,6 +534,11 @@ boost::filesystem::path into_path(const wxString &str)
return boost::filesystem::path(str.wx_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() void about()
{ {
AboutDialog dlg; AboutDialog dlg;
+3
View File
@@ -76,6 +76,9 @@ std::string into_u8(const wxString &str);
wxString from_path(const boost::filesystem::path &path); wxString from_path(const boost::filesystem::path &path);
// boost path from wxString // boost path from wxString
boost::filesystem::path into_path(const wxString &str); 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 // Display an About dialog
extern void about(); extern void about();
+1 -4
View File
@@ -98,10 +98,7 @@ void MarkdownTip::LoadStyle()
ph /= "tooltip/styled.html"; ph /= "tooltip/styled.html";
_data_dir = false; _data_dir = false;
} }
auto url = ph.string(); _tipView->LoadURL(file_url_from_path(ph));
std::replace(url.begin(), url.end(), '\\', '/');
url = "file:///" + url;
_tipView->LoadURL(from_u8(url));
_lastTip.clear(); _lastTip.clear();
} }
+1 -1
View File
@@ -3483,7 +3483,7 @@ void Sidebar::update_all_preset_comboboxes()
wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString url = from_u8(PrintHost::get_print_host_webui(&cfg));
wxString apikey; wxString apikey;
if(url.empty()) 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 { else {
const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value; const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value;
if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint))
+1 -3
View File
@@ -57,9 +57,7 @@ PrivacyUpdateDialog::PrivacyUpdateDialog(wxWindow* parent, wxWindowID id, const
fs::path ph(resources_dir()); fs::path ph(resources_dir());
ph /= "tooltip/privacyupdate.html"; ph /= "tooltip/privacyupdate.html";
m_host_url = ph.string(); m_host_url = into_u8(file_url_from_path(ph));
std::replace(m_host_url.begin(), m_host_url.end(), '\\', '/');
m_host_url = "file:///" + m_host_url;
m_vebview_release_note->LoadURL(from_u8(m_host_url)); m_vebview_release_note->LoadURL(from_u8(m_host_url));
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
+2 -2
View File
@@ -45,10 +45,10 @@ const std::vector<std::string> license_list = {
ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style) ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style)
{ {
SetBackgroundColour(*wxWHITE); 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(); wxString strlang = wxGetApp().current_language_code_safe();
if (strlang != "") 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); wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
+1 -1
View File
@@ -510,7 +510,7 @@ void UpdateVersionDialog::update_version_info(wxString release_note, wxString ve
out_buf->append(text, size); out_buf->append(text, size);
}, (void*) &html_source, MD_DIALECT_GITHUB | MD_FLAG_STRIKETHROUGH | MD_FLAG_WIKILINKS, 0); }, (void*) &html_source, MD_DIALECT_GITHUB | MD_FLAG_STRIKETHROUGH | MD_FLAG_WIKILINKS, 0);
html_source.append("</body></html>"); html_source.append("</body></html>");
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()); SetMinSize(GetSize());
SetMaxSize(GetSize()); SetMaxSize(GetSize());
+1 -3
View File
@@ -38,9 +38,7 @@ DownPluginFrame::DownPluginFrame(GUI_App *pGUI) : wxDialog((wxWindow *) (pGUI->m
// set the frame icon // set the frame icon
wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL);
wxString TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/6/index.html").make_preferred().string()); wxString TargetUrl = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/guide/6/index.html");
TargetUrl = "file://" + TargetUrl;
// Create the webview // Create the webview
m_browser = WebView::CreateWebView(this, TargetUrl); m_browser = WebView::CreateWebView(this, TargetUrl);
+12 -12
View File
@@ -218,37 +218,38 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load)
m_page = startpage; m_page = startpage;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(" enter, load=%1%, start_page=%2%")%load%int(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() ); //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()); //wxLogMessage("GUIDE: webpage_2 %s", TargetUrl.mb_str());
if (startpage == BBL_WELCOME){ if (startpage == BBL_WELCOME){
SetTitle(_L("Setup Wizard")); 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) { } else if (startpage == BBL_REGION) {
SetTitle(_L("Setup Wizard")); 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) { } else if (startpage == BBL_MODELS) {
SetTitle(_L("Setup Wizard")); 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) { } else if (startpage == BBL_FILAMENTS) {
SetTitle(_L("Setup Wizard")); SetTitle(_L("Setup Wizard"));
int nSize = m_ProfileJson["model"].size(); int nSize = m_ProfileJson["model"].size();
if (nSize>0) 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 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) { } else if (startpage == BBL_FILAMENT_ONLY) {
SetTitle(""); 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) { } else if (startpage == BBL_MODELS_ONLY) {
SetTitle(""); 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 { else {
SetTitle(_L("Setup Wizard")); 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(); wxString strlang = wxGetApp().current_language_code_safe();
@@ -256,7 +257,6 @@ wxString GuideFrame::SetStartPage(GuidePage startpage, bool load)
if (strlang != "") if (strlang != "")
TargetUrl = wxString::Format("%s&lang=%s", w2s(TargetUrl), strlang); TargetUrl = wxString::Format("%s&lang=%s", w2s(TargetUrl), strlang);
TargetUrl = "file://" + TargetUrl;
if (load) if (load)
load_url(TargetUrl); load_url(TargetUrl);
@@ -1284,7 +1284,7 @@ bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_r
entry["vendor"] = vp.id; entry["vendor"] = vp.id;
entry["nozzle_diameter"] = nozzle_str; entry["nozzle_diameter"] = nozzle_str;
entry["materials"] = materials_str; entry["materials"] = materials_str;
entry["cover"] = cover_path.string(); entry["cover"] = into_u8(file_url_from_path(cover_path));
entry["nozzle_selected"] = ""; entry["nozzle_selected"] = "";
entry["sub_path"] = ""; entry["sub_path"] = "";
m_ProfileJson["model"].push_back(entry); 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) cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file)
.make_preferred(); .make_preferred();
} }
OneModel["cover"] = cover_path.string(); OneModel["cover"] = into_u8(file_url_from_path(cover_path));
OneModel["nozzle_selected"] = ""; OneModel["nozzle_selected"] = "";
+2 -2
View File
@@ -36,10 +36,10 @@ namespace GUI {
WebViewPanel::WebViewPanel(wxWindow *parent) WebViewPanel::WebViewPanel(wxWindow *parent)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) : 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(); wxString strlang = wxGetApp().current_language_code_safe();
if (strlang != "") 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); wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
+5 -6
View File
@@ -48,20 +48,19 @@ constexpr char ORCA_BRIDGE_JS[] = R"JS(
wxString bootstrap_url() 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() wxString content_base_url()
{ {
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string(); return file_url_from_path(boost::filesystem::path(resources_dir()) / "web") + "/";
return wxString("file://") + from_u8(dir) + "/";
} }
bool is_content_url(const wxString& url) bool is_content_url(const wxString& url)
{ {
// The web view reports the URL it parsed, which escapes anything the resources path holds // The web view reports the URL it parsed, which may escape the resources path differently
// (a space, a non-ASCII character), while content_base_url() is the raw path. // from content_base_url().
return wxURI::Unescape(url.BeforeFirst('#')) == content_base_url(); return wxURI::Unescape(url.BeforeFirst('#')) == wxURI::Unescape(content_base_url());
} }
const char* orca_bridge_script() { return ORCA_BRIDGE_JS; } const char* orca_bridge_script() { return ORCA_BRIDGE_JS; }
+2 -2
View File
@@ -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 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()) { if (append_language_to_url()) {
const wxString lang = wxGetApp().current_language_code_safe(); 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) void WebViewHostDialog::load_url(const wxString& url)
+1 -3
View File
@@ -340,9 +340,7 @@ namespace Slic3r {
if (classify_printer_model(config->opt_string("printer_model")) != ElegooPrinterType::CC2) if (classify_printer_model(config->opt_string("printer_model")) != ElegooPrinterType::CC2)
return fallback_webui; return fallback_webui;
std::string web_path = resources_dir() + "/web/elegoolink/lan_service_web/index.html"; std::string web_path = GUI::into_u8(GUI::file_url_from_path(boost::filesystem::path(resources_dir()) / "web/elegoolink/lan_service_web/index.html"));
std::replace(web_path.begin(), web_path.end(), '\\', '/');
web_path = "file://" + web_path;
const std::string token = get_cc2_token(config->opt_string("printhost_apikey")); const std::string token = get_cc2_token(config->opt_string("printhost_apikey"));
const std::string host_ip = Http::get_host_header_value(host); const std::string host_ip = Http::get_host_header_value(host);
+1
View File
@@ -25,6 +25,7 @@ add_executable(${_TEST_NAME}_tests
test_plugin_cloud_metadata.cpp test_plugin_cloud_metadata.cpp
test_plugin_audit.cpp test_plugin_audit.cpp
test_shortcuts.cpp test_shortcuts.cpp
test_file_url.cpp
../fff_print/test_helpers.cpp ../fff_print/test_helpers.cpp
) )
+69
View File
@@ -0,0 +1,69 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include "slic3r/GUI/GUI.hpp"
#include <boost/filesystem/path.hpp>
#include <wx/uri.h>
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<std::string>{},
"/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<std::wstring, std::string>({
{ 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
+7 -6
View File
@@ -16,6 +16,8 @@
#include <string> #include <string>
#include <wx/uri.h>
namespace py = pybind11; namespace py = pybind11;
namespace { 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; 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"); const Slic3r::ScopedResourcesDir resources("web content check");
// The swapped-in page, then after an in-page anchor and a reload. // 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()));
CHECK(is_content_url(content_base_url() + "#tab2")); CHECK(is_content_url(content_base_url() + "#tab2"));
wxString escaped = content_base_url(); const wxString unescaped = wxURI::Unescape(content_base_url());
escaped.Replace(" ", "%20"); REQUIRE(unescaped != content_base_url());
REQUIRE(escaped != content_base_url()); CHECK(is_content_url(unescaped));
CHECK(is_content_url(escaped)); CHECK(is_content_url(unescaped + "#tab2"));
CHECK(is_content_url(escaped + "#tab2"));
// A page the plugin linked to keeps its own URL and must be left alone. // 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(content_base_url() + "guide.html"));
CHECK_FALSE(is_content_url("https://example.com/")); CHECK_FALSE(is_content_url("https://example.com/"));