Skip to content

Commit

Permalink
(#3129) fixed incorrect text escaping during SSR
Browse files Browse the repository at this point in the history
  • Loading branch information
its-the-shrimp committed Aug 20, 2023
1 parent 42e1890 commit 89bfee5
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 36 deletions.
10 changes: 1 addition & 9 deletions packages/yew-macro/src/derive_props/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down
4 changes: 3 additions & 1 deletion packages/yew/src/html/component/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,13 +301,15 @@ mod feat_ssr {
use crate::platform::pinned::oneshot;
use crate::scheduler;
use crate::virtual_dom::Collectable;
use crate::SpecialVTagKind;

impl<COMP: BaseComponent> Scope<COMP> {
pub(crate) async fn render_into_stream(
&self,
w: &mut BufWriter,
props: Rc<COMP::Properties>,
hydratable: bool,
parent_vtag_kind: SpecialVTagKind
) {
// Rust's Future implementation is stack-allocated and incurs zero runtime-cost.
//
Expand Down Expand Up @@ -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() {
Expand Down
24 changes: 23 additions & 1 deletion packages/yew/src/server_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ use crate::html::{BaseComponent, Scope};
use crate::platform::fmt::BufStream;
use crate::platform::{LocalHandle, Runtime};

/// Passed top-down as context for `render_into_stream` functions to know the current innermost
/// VTag kind to apply appropriate text escaping.
#[cfg(feature = "ssr")]
#[derive(Default, Clone, Copy)]
pub(crate) enum SpecialVTagKind {
Style, // <style> tag
Script, // <script> tag
#[default] Other
}

#[cfg(feature = "ssr")]
impl<T: AsRef<str>> From<T> for SpecialVTagKind {
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
Expand Down Expand Up @@ -99,7 +121,7 @@ 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;
})
Expand Down
10 changes: 7 additions & 3 deletions packages/yew/src/virtual_dom/vcomp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{platform::fmt::BufWriter, SpecialVTagKind};

/// A virtual component.
pub struct VComp {
Expand Down Expand Up @@ -77,6 +77,7 @@ pub(crate) trait Mountable {
w: &'a mut BufWriter,
parent_scope: &'a AnyScope,
hydratable: bool,
parent_vtag_kind: SpecialVTagKind
) -> LocalBoxFuture<'a, ()>;

#[cfg(feature = "hydration")]
Expand Down Expand Up @@ -146,12 +147,13 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
w: &'a mut BufWriter,
parent_scope: &'a AnyScope,
hydratable: bool,
parent_vtag_kind: SpecialVTagKind
) -> 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()
Expand Down Expand Up @@ -254,6 +256,7 @@ impl<COMP: BaseComponent> fmt::Debug for VChild<COMP> {
mod feat_ssr {
use super::*;
use crate::html::AnyScope;
use crate::SpecialVTagKind;

impl VComp {
#[inline]
Expand All @@ -262,10 +265,11 @@ mod feat_ssr {
w: &mut BufWriter,
parent_scope: &AnyScope,
hydratable: bool,
parent_vtag_kind: SpecialVTagKind
) {
self.mountable
.as_ref()
.render_into_stream(w, parent_scope, hydratable)
.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
.await;
}
}
Expand Down
13 changes: 10 additions & 3 deletions packages/yew/src/virtual_dom/vlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,25 +184,29 @@ mod feat_ssr {
use super::*;
use crate::html::AnyScope;
use crate::platform::fmt::{self, BufWriter};
use crate::SpecialVTagKind;

impl VList {
pub(crate) async fn render_into_stream(
&self,
w: &mut BufWriter,
parent_scope: &AnyScope,
hydratable: bool,
parent_vtag_kind: SpecialVTagKind
) {
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>(
mut children: I,
w: &mut BufWriter,
parent_scope: &AnyScope,
hydratable: bool,
parent_vtag_kind: SpecialVTagKind
) where
I: Iterator<Item = &'a VNode>,
{
Expand All @@ -215,7 +219,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);
Expand All @@ -231,6 +236,7 @@ mod feat_ssr {
&mut next_w,
parent_scope,
hydratable,
parent_vtag_kind
)
.await;
}
Expand All @@ -257,7 +263,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;
}
}
}
Expand Down
32 changes: 19 additions & 13 deletions packages/yew/src/virtual_dom/vnode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,31 +196,36 @@ mod feat_ssr {
use super::*;
use crate::html::AnyScope;
use crate::platform::fmt::BufWriter;
use crate::SpecialVTagKind;

impl VNode {
pub(crate) fn render_into_stream<'a>(
&'a self,
w: &'a mut BufWriter,
parent_scope: &'a AnyScope,
hydratable: bool,
parent_vtag_kind: SpecialVTagKind
) -> LocalBoxFuture<'a, ()> {
async fn render_into_stream_(
this: &VNode,
w: &mut BufWriter,
parent_scope: &AnyScope,
hydratable: bool,
parent_vtag_kind: SpecialVTagKind
) {
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
}
VNode::VComp(vcomp) => {
vcomp.render_into_stream(w, parent_scope, hydratable).await
}
VNode::VList(vlist) => {
vlist.render_into_stream(w, parent_scope, hydratable).await
}
VNode::VTag(vtag) =>
vtag.render_into_stream(w, parent_scope, hydratable)
.await,
VNode::VText(vtext) =>
vtext.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
.await,
VNode::VComp(vcomp) =>
vcomp.render_into_stream(w, parent_scope, hydratable, parent_vtag_kind)
.await,
VNode::VList(vlist) =>
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.
//
Expand All @@ -233,16 +238,17 @@ 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
}

VNode::VRaw(vraw) => vraw.render_into_stream(w, parent_scope, hydratable).await,
}
}

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()
}
}
}
2 changes: 1 addition & 1 deletion packages/yew/src/virtual_dom/vraw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ mod feat_ssr {
&self,
w: &mut BufWriter,
_parent_scope: &AnyScope,
hydratable: bool,
hydratable: bool
) {
let collectable = Collectable::Raw;

Expand Down
4 changes: 3 additions & 1 deletion packages/yew/src/virtual_dom/vsuspense.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ impl VSuspense {
#[cfg(feature = "ssr")]
mod feat_ssr {
use super::*;
use crate::SpecialVTagKind;
use crate::html::AnyScope;
use crate::platform::fmt::BufWriter;
use crate::virtual_dom::Collectable;
Expand All @@ -37,6 +38,7 @@ mod feat_ssr {
w: &mut BufWriter,
parent_scope: &AnyScope,
hydratable: bool,
parent_vtag_kind: SpecialVTagKind
) {
let collectable = Collectable::Suspense;

Expand All @@ -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 {
Expand Down
35 changes: 33 additions & 2 deletions packages/yew/src/virtual_dom/vtag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ mod feat_ssr {
use crate::html::AnyScope;
use crate::platform::fmt::BufWriter;
use crate::virtual_dom::VText;
use crate::SpecialVTagKind;

// Elements that cannot have any child elements.
static VOID_ELEMENTS: &[&str; 14] = &[
Expand Down Expand Up @@ -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, SpecialVTagKind::Other)
.await;
}

Expand All @@ -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("</");
Expand Down Expand Up @@ -623,4 +624,34 @@ 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>"#);
}
}
12 changes: 10 additions & 2 deletions packages/yew/src/virtual_dom/vtext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ mod feat_ssr {
use std::fmt::Write;

use super::*;
use crate::SpecialVTagKind;
use crate::html::AnyScope;
use crate::platform::fmt::BufWriter;

Expand All @@ -53,9 +54,16 @@ mod feat_ssr {
w: &mut BufWriter,
_parent_scope: &AnyScope,
_hydratable: bool,
parent_vtag_kind: SpecialVTagKind
) {
let s = html_escape::encode_text(&self.text);
let _ = w.write_str(&s);
_ = w.write_str(&match parent_vtag_kind {
SpecialVTagKind::Style =>
html_escape::encode_style(&self.text),
SpecialVTagKind::Script =>
html_escape::encode_script(&self.text),
SpecialVTagKind::Other =>
html_escape::encode_text(&self.text)
})
}
}
}
Expand Down

0 comments on commit 89bfee5

Please sign in to comment.