Fix incorrect text escaping during SSR (#3381)

Fixes: #3129

---

* (#3129) fixed incorrect text escaping during SSR

* fixed formatting issues

* moved SpecialVTagKind to feat_ssr, improved its description

* added a test case for multiple text nodes in a style tag

* fixed formatting

* SpecialVTagKind -> VTagKind
This commit is contained in:
Tim Kurdov 2023-08-21 17:29:46 +01:00 committed by GitHub
parent b28e69a574
commit afde963230
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 142 additions and 28 deletions

View File

@ -345,15 +345,7 @@ impl TryFrom<Field> for PropField {
impl PartialOrd for PropField {
fn partial_cmp(&self, other: &PropField) -> Option<Ordering> {
if self.name == other.name {
Some(Ordering::Equal)
} else if self.name == "children" {
Some(Ordering::Greater)
} else if other.name == "children" {
Some(Ordering::Less)
} else {
self.name.partial_cmp(&other.name)
}
Some(self.cmp(other))
}
}

View File

@ -294,6 +294,7 @@ mod feat_ssr {
use std::fmt::Write;
use super::*;
use crate::feat_ssr::VTagKind;
use crate::html::component::lifecycle::{
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
};
@ -308,6 +309,7 @@ mod feat_ssr {
w: &mut BufWriter,
props: Rc<COMP::Properties>,
hydratable: bool,
parent_vtag_kind: VTagKind,
) {
// Rust's Future implementation is stack-allocated and incurs zero runtime-cost.
//
@ -340,7 +342,7 @@ mod feat_ssr {
let html = rx.await.unwrap();
let self_any_scope = AnyScope::from(self.clone());
html.render_into_stream(w, &self_any_scope, hydratable)
html.render_into_stream(w, &self_any_scope, hydratable, parent_vtag_kind)
.await;
if let Some(prepared_state) = self.get_component().unwrap().prepare_state() {

View File

@ -9,6 +9,37 @@ use crate::html::{BaseComponent, Scope};
use crate::platform::fmt::BufStream;
use crate::platform::{LocalHandle, Runtime};
#[cfg(feature = "ssr")]
pub(crate) mod feat_ssr {
/// Passed top-down as context for `render_into_stream` functions to know the current innermost
/// `VTag` kind to apply appropriate text escaping.
/// Right now this is used to make `VText` nodes aware of their environment and correctly
/// escape their contents when rendering them during SSR.
#[derive(Default, Clone, Copy)]
pub(crate) enum VTagKind {
/// <style> tag
Style,
/// <script> tag
Script,
#[default]
/// any other tag
Other,
}
impl<T: AsRef<str>> From<T> for VTagKind {
fn from(value: T) -> Self {
let value = value.as_ref();
if value.eq_ignore_ascii_case("style") {
Self::Style
} else if value.eq_ignore_ascii_case("script") {
Self::Script
} else {
Self::Other
}
}
}
}
/// A Yew Server-side Renderer that renders on the current thread.
///
/// # Note
@ -99,7 +130,12 @@ where
let render_span = tracing::debug_span!("render_stream_item");
render_span.follows_from(outer_span);
scope
.render_into_stream(&mut w, self.props.into(), self.hydratable)
.render_into_stream(
&mut w,
self.props.into(),
self.hydratable,
Default::default(),
)
.instrument(render_span)
.await;
})

View File

@ -20,7 +20,7 @@ use crate::html::Scoped;
#[cfg(any(feature = "ssr", feature = "csr"))]
use crate::html::{AnyScope, Scope};
#[cfg(feature = "ssr")]
use crate::platform::fmt::BufWriter;
use crate::{feat_ssr::VTagKind, platform::fmt::BufWriter};
/// A virtual component.
pub struct VComp {
@ -77,6 +77,7 @@ pub(crate) trait Mountable {
w: &'a mut BufWriter,
parent_scope: &'a AnyScope,
hydratable: bool,
parent_vtag_kind: VTagKind,
) -> LocalBoxFuture<'a, ()>;
#[cfg(feature = "hydration")]
@ -146,12 +147,13 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
w: &'a mut BufWriter,
parent_scope: &'a AnyScope,
hydratable: bool,
parent_vtag_kind: VTagKind,
) -> LocalBoxFuture<'a, ()> {
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
async move {
scope
.render_into_stream(w, self.props.clone(), hydratable)
.render_into_stream(w, self.props.clone(), hydratable, parent_vtag_kind)
.await;
}
.boxed_local()
@ -262,10 +264,11 @@ mod feat_ssr {
w: &mut BufWriter,
parent_scope: &AnyScope,
hydratable: bool,
parent_vtag_kind: VTagKind,
) {
self.mountable
.as_ref()
.render_into_stream(w, parent_scope, hydratable)
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
.await;
}
}

View File

@ -182,6 +182,7 @@ mod feat_ssr {
use futures::{join, pin_mut, poll, FutureExt};
use super::*;
use crate::feat_ssr::VTagKind;
use crate::html::AnyScope;
use crate::platform::fmt::{self, BufWriter};
@ -191,11 +192,14 @@ mod feat_ssr {
w: &mut BufWriter,
parent_scope: &AnyScope,
hydratable: bool,
parent_vtag_kind: VTagKind,
) {
match &self[..] {
[] => {}
[child] => {
child.render_into_stream(w, parent_scope, hydratable).await;
child
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
.await;
}
_ => {
async fn render_child_iter<'a, I>(
@ -203,6 +207,7 @@ mod feat_ssr {
w: &mut BufWriter,
parent_scope: &AnyScope,
hydratable: bool,
parent_vtag_kind: VTagKind,
) where
I: Iterator<Item = &'a VNode>,
{
@ -215,7 +220,8 @@ mod feat_ssr {
//
// We capture and return the mutable reference to avoid this.
m.render_into_stream(w, parent_scope, hydratable).await;
m.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
.await;
w
};
pin_mut!(child_fur);
@ -231,6 +237,7 @@ mod feat_ssr {
&mut next_w,
parent_scope,
hydratable,
parent_vtag_kind,
)
.await;
}
@ -257,7 +264,8 @@ mod feat_ssr {
}
let children = self.iter();
render_child_iter(children, w, parent_scope, hydratable).await;
render_child_iter(children, w, parent_scope, hydratable, parent_vtag_kind)
.await;
}
}
}

View File

@ -194,6 +194,7 @@ mod feat_ssr {
use futures::future::{FutureExt, LocalBoxFuture};
use super::*;
use crate::feat_ssr::VTagKind;
use crate::html::AnyScope;
use crate::platform::fmt::BufWriter;
@ -203,23 +204,31 @@ mod feat_ssr {
w: &'a mut BufWriter,
parent_scope: &'a AnyScope,
hydratable: bool,
parent_vtag_kind: VTagKind,
) -> LocalBoxFuture<'a, ()> {
async fn render_into_stream_(
this: &VNode,
w: &mut BufWriter,
parent_scope: &AnyScope,
hydratable: bool,
parent_vtag_kind: VTagKind,
) {
match this {
VNode::VTag(vtag) => vtag.render_into_stream(w, parent_scope, hydratable).await,
VNode::VText(vtext) => {
vtext.render_into_stream(w, parent_scope, hydratable).await
vtext
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
.await
}
VNode::VComp(vcomp) => {
vcomp.render_into_stream(w, parent_scope, hydratable).await
vcomp
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
.await
}
VNode::VList(vlist) => {
vlist.render_into_stream(w, parent_scope, hydratable).await
vlist
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
.await
}
// We are pretty safe here as it's not possible to get a web_sys::Node without
// DOM support in the first place.
@ -233,7 +242,7 @@ mod feat_ssr {
VNode::VPortal(_) => {}
VNode::VSuspense(vsuspense) => {
vsuspense
.render_into_stream(w, parent_scope, hydratable)
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
.await
}
@ -241,8 +250,9 @@ mod feat_ssr {
}
}
async move { render_into_stream_(self, w, parent_scope, hydratable).await }
.boxed_local()
async move {
render_into_stream_(self, w, parent_scope, hydratable, parent_vtag_kind).await
}.boxed_local()
}
}
}

View File

@ -27,6 +27,7 @@ impl VSuspense {
#[cfg(feature = "ssr")]
mod feat_ssr {
use super::*;
use crate::feat_ssr::VTagKind;
use crate::html::AnyScope;
use crate::platform::fmt::BufWriter;
use crate::virtual_dom::Collectable;
@ -37,6 +38,7 @@ mod feat_ssr {
w: &mut BufWriter,
parent_scope: &AnyScope,
hydratable: bool,
parent_vtag_kind: VTagKind,
) {
let collectable = Collectable::Suspense;
@ -46,7 +48,7 @@ mod feat_ssr {
// always render children on the server side.
self.children
.render_into_stream(w, parent_scope, hydratable)
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
.await;
if hydratable {

View File

@ -451,6 +451,7 @@ mod feat_ssr {
use std::fmt::Write;
use super::*;
use crate::feat_ssr::VTagKind;
use crate::html::AnyScope;
use crate::platform::fmt::BufWriter;
use crate::virtual_dom::VText;
@ -505,7 +506,7 @@ mod feat_ssr {
VTagInner::Textarea { .. } => {
if let Some(m) = self.value() {
VText::new(m.to_owned())
.render_into_stream(w, parent_scope, hydratable)
.render_into_stream(w, parent_scope, hydratable, VTagKind::Other)
.await;
}
@ -518,7 +519,7 @@ mod feat_ssr {
} => {
if !VOID_ELEMENTS.contains(&tag.as_ref()) {
children
.render_into_stream(w, parent_scope, hydratable)
.render_into_stream(w, parent_scope, hydratable, tag.into())
.await;
let _ = w.write_str("</");
@ -623,4 +624,59 @@ mod ssr_tests {
assert_eq!(s, r#"<textarea>teststring</textarea>"#);
}
#[test]
async fn test_escaping_in_style_tag() {
#[function_component]
fn Comp() -> Html {
html! { <style>{"body > a {color: #cc0;}"}</style> }
}
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(s, r#"<style>body > a {color: #cc0;}</style>"#);
}
#[test]
async fn test_escaping_in_script_tag() {
#[function_component]
fn Comp() -> Html {
html! { <script>{"foo.bar = x < y;"}</script> }
}
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(s, r#"<script>foo.bar = x < y;</script>"#);
}
#[test]
async fn test_multiple_vtext_in_style_tag() {
#[function_component]
fn Comp() -> Html {
let one = "html { background: black } ";
let two = "body > a { color: white } ";
html! {
<style>
{one}
{two}
</style>
}
}
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(
s,
r#"<style>html { background: black } body > a { color: white } </style>"#
);
}
}

View File

@ -44,6 +44,7 @@ mod feat_ssr {
use std::fmt::Write;
use super::*;
use crate::feat_ssr::VTagKind;
use crate::html::AnyScope;
use crate::platform::fmt::BufWriter;
@ -53,9 +54,13 @@ mod feat_ssr {
w: &mut BufWriter,
_parent_scope: &AnyScope,
_hydratable: bool,
parent_vtag_kind: VTagKind,
) {
let s = html_escape::encode_text(&self.text);
let _ = w.write_str(&s);
_ = w.write_str(&match parent_vtag_kind {
VTagKind::Style => html_escape::encode_style(&self.text),
VTagKind::Script => html_escape::encode_script(&self.text),
VTagKind::Other => html_escape::encode_text(&self.text),
})
}
}
}