Compare commits

...
Author SHA1 Message Date
Hanif Koh cf3f8019c3 Encode the Login Error Page URL and Cover More Windows Path Forms
The login dialog's error page was still loaded from a raw resources path;
it now uses file_url_from_path like the other local pages.

The Windows file URL tests now also cover a resources path joined with a
forward-slash relative path, as the callers build them, and a UNC path.
2026-09-28 20:02:53 +08:00
Hanif Koh adf92e1c83 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.
2026-09-28 19:32:04 +08:00
17 changed files with 123 additions and 46 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"] = "";
+1 -1
View File
@@ -599,7 +599,7 @@ void ZUserLogin::OnScriptResponseMessage(wxCommandEvent &WXUNUSED(evt))
bool ZUserLogin::ShowErrorPage() bool ZUserLogin::ShowErrorPage()
{ {
wxString ErrortUrl = from_u8((boost::filesystem::path(resources_dir()) / "web\\login\\error.html").make_preferred().string()); wxString ErrortUrl = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/login/error.html");
load_url(ErrortUrl); load_url(ErrortUrl);
return true; return true;
+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
) )
+74
View File
@@ -0,0 +1,74 @@
#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" },
// Callers join the resources directory with a forward-slash relative path.
{ L"D:\\#OneDrive\\OrcaSlicer\\resources/web/homepage/index.html",
"file:///D:/%23OneDrive/OrcaSlicer/resources/web/homepage/index.html" },
{ L"\\\\server\\share\\OrcaSlicer\\resources\\web\\homepage\\index.html",
"file://server/share/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/"));