Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core) Fix HTML encoding in webview rendered via data url #8779

Open
wants to merge 7 commits into
base: 1.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/tauri-runtime-wry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ rand = "0.8"
raw-window-handle = "0.5"
tracing = { version = "0.1", optional = true }
arboard = { version = "3", optional = true }
percent-encoding = "2.1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only needed on Linux, let's revert this back


[target."cfg(windows)".dependencies]
webview2-com = "0.19.1"
Expand All @@ -32,7 +33,6 @@ webview2-com = "0.19.1"
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
gtk = { version = "0.15", features = [ "v3_20" ] }
webkit2gtk = { version = "0.18.2", features = [ "v2_22" ] }
percent-encoding = "2.1"

[target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies]
cocoa = "0.24"
Expand Down
22 changes: 18 additions & 4 deletions core/tauri-runtime-wry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3247,13 +3247,27 @@ fn create_webview<T: UserEvent>(
if window_builder.center {
let _ = center_window(&window, window.inner_size());
}
let mut webview_builder = WebViewBuilder::new(window)
.map_err(|e| Error::CreateWebview(Box::new(e)))?

let mut webview_builder =
WebViewBuilder::new(window).map_err(|e| Error::CreateWebview(Box::new(e)))?;

// use with_html method if html content can be extracted from url.
// else defaults to with_url method
webview_builder = if let Some(html_string) = tauri_utils::html::extract_html_content(&url) {
webview_builder
.with_html(html_string)
.map_err(|e| Error::CreateWebview(Box::new(e)))?
} else {
webview_builder
.with_url(&url)
.map_err(|e| Error::CreateWebview(Box::new(e)))?
};
Comment on lines +3256 to +3264
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tauri-runtime-wry shouldn't do these checks at all, instead, PendingWindow should have a field html: Option<String> and if it is Some, we use with_html


webview_builder = webview_builder
.with_focused(focused)
.with_url(&url)
.unwrap() // safe to unwrap because we validate the URL beforehand
.with_transparent(is_window_transparent)
.with_accept_first_mouse(webview_attributes.accept_first_mouse);

if webview_attributes.file_drop_handler_enabled {
webview_builder = webview_builder
.with_file_drop_handler(create_file_drop_handler(window_event_listeners.clone()));
Expand Down
1 change: 1 addition & 0 deletions core/tauri-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ system-tray = [ ]
macos-private-api = [ ]
global-shortcut = [ ]
clipboard = [ ]
window-data-url = [ "tauri-utils/window-data-url" ]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and revert this as well

2 changes: 2 additions & 0 deletions core/tauri-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ semver = "1"
infer = "0.13"
dunce = "1"
log = "0.4.20"
data-url = { version = "0.3.1", optional = true }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto


[target."cfg(target_os = \"linux\")".dependencies]
heck = "0.4"
Expand All @@ -54,3 +55,4 @@ process-relaunch-dangerous-allow-symlink-macos = [ ]
config-json5 = [ "json5" ]
config-toml = [ "toml" ]
resources = [ "glob", "walkdir" ]
window-data-url = [ "data-url" ]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

11 changes: 11 additions & 0 deletions core/tauri-utils/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,18 @@ pub enum WindowUrl {
/// For instance, to load `tauri://localhost/users/john`,
/// you can simply provide `users/john` in this configuration.
App(PathBuf),
#[cfg(feature = "window-data-url")]
/// A data url, for example data:text/html,<h1>Hello world</h1>
/// Data url should not be encoded
DataUrl(Url),
}

impl fmt::Display for WindowUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::External(url) => write!(f, "{url}"),
#[cfg(feature = "window-data-url")]
Self::DataUrl(url) => write!(f, "{url}"),
Self::App(path) => write!(f, "{}", path.display()),
}
}
Expand Down Expand Up @@ -3281,6 +3287,11 @@ mod build {
let url = url_lit(url);
quote! { #prefix::External(#url) }
}
#[cfg(feature = "window-data-url")]
Self::DataUrl(url) => {
let url = url_lit(url);
quote! { #prefix::DataUrl(#url) }
}
})
}
}
Expand Down
14 changes: 14 additions & 0 deletions core/tauri-utils/src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,20 @@ pub fn inline_isolation(document: &mut NodeRef, dir: &Path) {
}
}

/// Temporary naive method to check if a string is a html
pub fn is_html(data_string: &str) -> bool {
data_string.contains('<') && data_string.contains('>')
}

/// Temporary naive method to extract data from html data string
pub fn extract_html_content(input: &str) -> Option<&str> {
if input.starts_with("data:text/html,") {
Some(&input[15..])
} else {
None
}
}
Comment on lines +289 to +301
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's move these back to tauri crate as they are only used there for now


#[cfg(test)]
mod tests {
use kuchiki::traits::*;
Expand Down
4 changes: 2 additions & 2 deletions core/tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ time = { version = "0.3", features = [ "parsing", "formatting" ], optional = tru
os_info = { version = "3", optional = true }
regex = { version = "1", optional = true }
glob = "0.3"
data-url = { version = "0.2", optional = true }
data-url = { version = "0.3.1", optional = true }
serialize-to-javascript = "=0.1.1"
infer = { version = "0.9", optional = true }
png = { version = "0.17", optional = true }
Expand Down Expand Up @@ -184,7 +184,7 @@ macos-private-api = [
"tauri-runtime-wry/macos-private-api"
]
windows7-compat = [ "win7-notifications" ]
window-data-url = [ "data-url" ]
window-data-url = [ "tauri-utils/window-data-url", "data-url" ]
api-all = [
"clipboard-all",
"dialog-all",
Expand Down
55 changes: 40 additions & 15 deletions core/tauri/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,8 @@ impl<R: Runtime> WindowManager<R> {
}
}
WindowUrl::External(url) => url.clone(),
#[cfg(feature = "window-data-url")]
WindowUrl::DataUrl(url) => url.clone(),
_ => unimplemented!(),
};

Expand All @@ -1005,23 +1007,46 @@ impl<R: Runtime> WindowManager<R> {
));
}

#[cfg(feature = "window-data-url")]
if let Some(csp) = self.csp() {
if url.scheme() == "data" {
if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) {
let (body, _) = data_url.decode_to_vec().unwrap();
let html = String::from_utf8_lossy(&body).into_owned();
// naive way to check if it's an html
if html.contains('<') && html.contains('>') {
match (
url.scheme(),
tauri_utils::html::extract_html_content(url.as_str()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't call extract_html_content unless we are sure it is a data url

#[cfg(feature = "window-data-url")]
if ur.scheme() == "data" {
  let html = extract_html_content();
  // ...
}

) {
#[cfg(feature = "window-data-url")]
("data", Some(html_string)) => {
// There is an issue with the external DataUrl where HTML containing special characters
// are not correctly processed. A workaround is to first percent encode the html string,
// before it processed by DataUrl.
let encoded_string = percent_encoding::utf8_percent_encode(html_string, percent_encoding::NON_ALPHANUMERIC).to_string();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't encode the URL, we should expect the user has already encoded it as per https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs
which makes this PR kinda uneeded?

Copy link

@gentean gentean Mar 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that data URLs are not encoded by default on Linux, but they are encoded by default on macOS. I haven't tried on Windows yet. I expect consistency across platforms, with no encoding.

The following content will be encoded on macOS, which is not what I expected.

const webview = new WebviewWindow('print', {
        url: `data:text/html,<html><body>你好,世界</body></html>`,
        center: true,
        visible: true,
        width: 300,
        height: 300,
});

let url = data_url::DataUrl::process(&format!("data:text/html,{}", encoded_string))
.map_err(|_| crate::Error::InvalidWindowUrl("Failed to process data url"))
.and_then(|data_url| {
data_url
.decode_to_vec()
.map_err(|_| crate::Error::InvalidWindowUrl("Failed to decode processed data url"))
})
.and_then(|(body, _)| {
let html = String::from_utf8_lossy(&body).into_owned();
let mut document = tauri_utils::html::parse(html);
tauri_utils::html::inject_csp(&mut document, &csp.to_string());
url.set_path(&format!("text/html,{}", document.to_string()));
}
}
if let Some(csp) = self.csp() {
tauri_utils::html::inject_csp(&mut document, &csp.to_string());
}
// decode back to raw html, as the content should be fully decoded
// when passing to wry / tauri-runtime-wry, which will be responsible
// for handling the encoding based on the OS.
let encoded_html = document.to_string();
Ok(
percent_encoding::percent_decode_str(encoded_html.as_str())
.decode_utf8_lossy()
.to_string(),
)
})
.unwrap_or(html_string.to_string());
pending.url = format!("data:text/html,{}", url);
}
}

pending.url = url.to_string();
_ => {
pending.url = url.to_string();
}
};

if !pending.window_builder.has_icon() {
if let Some(default_window_icon) = self.inner.default_window_icon.clone() {
Expand Down
Loading