Răsfoiți Sursa

properly encode HTML in server-side rendering (fix #3078)

Evan You 10 ani în urmă
părinte
comite
b0ad94fc87

+ 0 - 1
.flowconfig

@@ -17,4 +17,3 @@ module.name_mapper='^shared/\(.*\)$' -> '<PROJECT_ROOT>/src/shared/\1'
 module.name_mapper='^web/\(.*\)$' -> '<PROJECT_ROOT>/src/platforms/web/\1'
 module.name_mapper='^server/\(.*\)$' -> '<PROJECT_ROOT>/src/server/\1'
 module.name_mapper='^entries/\(.*\)$' -> '<PROJECT_ROOT>/src/entries/\1'
-module.name_mapper='^entities$' -> '<PROJECT_ROOT>/src/compiler/parser/entity-decoder'

+ 1 - 1
build/build.js

@@ -72,7 +72,7 @@ var builds = [
   {
     entry: 'src/entries/web-server-renderer.js',
     format: 'cjs',
-    external: ['stream'],
+    external: ['stream', 'entities'],
     out: 'packages/vue-server-renderer/index.js'
   }
 ]

+ 3 - 0
build/webpack.ssr.dev.config.js

@@ -12,6 +12,9 @@ module.exports = {
   resolve: {
     alias: alias
   },
+  externals: {
+    'entities': true
+  },
   module: {
     loaders: [
       {

+ 0 - 14
flow/compiler.js

@@ -127,20 +127,6 @@ declare type ASTText = {
 
 // SFC-parser related declarations
 
-declare module 'de-indent' {
-  declare var exports: {
-    (str: string): string;
-  }
-}
-
-declare module 'source-map' {
-  declare class SourceMapGenerator {
-    setSourceContent(filename: string, content: string): void;
-    addMapping(mapping: Object): void;
-    toString(): string;
-  }
-}
-
 // an object format describing a single-file component.
 declare type SFCDescriptor = {
   template: ?SFCBlock,

+ 18 - 0
flow/modules.js

@@ -0,0 +1,18 @@
+declare module 'entities' {
+  declare function encodeHTML(html: string): string;
+  declare function decodeHTML(html: string): string;
+}
+
+declare module 'de-indent' {
+  declare var exports: {
+    (str: string): string;
+  }
+}
+
+declare module 'source-map' {
+  declare class SourceMapGenerator {
+    setSourceContent(filename: string, content: string): void;
+    addMapping(mapping: Object): void;
+    toString(): string;
+  }
+}

+ 1 - 0
src/core/vdom/vnode.js

@@ -12,6 +12,7 @@ export default class VNode {
   componentOptions: VNodeComponentOptions | void;
   child: Component | void;
   parent: VNode | void;
+  raw: ?boolean;
 
   constructor (
     tag?: string,

+ 19 - 6
src/platforms/web/server/modules/props.js

@@ -1,18 +1,31 @@
 /* @flow */
 
+import VNode from 'core/vdom/vnode'
 import { renderAttr } from './attrs'
 import { propsToAttrMap, isRenderableAttr } from 'web/util/attrs'
 
-export default function (node: VNodeWithData): ?string {
+export default function (node: VNodeWithData): string {
   const props = node.data.props
+  let res = ''
   if (props) {
-    let res = ''
     for (const key in props) {
-      const attr = propsToAttrMap[key] || key.toLowerCase()
-      if (isRenderableAttr(attr)) {
-        res += renderAttr(attr, props[key])
+      if (key === 'innerHTML') {
+        setText(node, props[key], true)
+      } else if (key === 'textContent') {
+        setText(node, props[key])
+      } else {
+        const attr = propsToAttrMap[key] || key.toLowerCase()
+        if (isRenderableAttr(attr)) {
+          res += renderAttr(attr, props[key])
+        }
       }
     }
-    return res
   }
+  return res
+}
+
+function setText (node, text, raw) {
+  const child = new VNode(undefined, undefined, undefined, text)
+  child.raw = raw
+  node.children = [child]
 }

+ 3 - 1
src/platforms/web/util/attrs.js

@@ -32,13 +32,15 @@ const isAttr = makeMap(
   'target,title,type,usemap,value,width,wrap'
 )
 
-export const isRenderableAttr = (name: string): boolean => {
+/* istanbul ignore next */
+const isRenderableAttr = (name: string): boolean => {
   return (
     isAttr(name) ||
     name.indexOf('data-') === 0 ||
     name.indexOf('aria-') === 0
   )
 }
+export { isRenderableAttr }
 
 export const propsToAttrMap = {
   acceptCharset: 'accept-charset',

+ 5 - 1
src/server/render.js

@@ -1,7 +1,11 @@
 /* @flow */
 
+import { cached } from 'shared/util'
+import { encodeHTML } from 'entities'
 import { createComponentInstanceForVnode } from 'core/vdom/create-component'
 
+const encodeHTMLCached = cached(encodeHTML)
+
 export function createRenderFunction (
   modules: Array<Function>,
   directives: Object,
@@ -20,7 +24,7 @@ export function createRenderFunction (
       if (node.tag) {
         renderElement(node, write, next, isRoot)
       } else {
-        write(node.text, next)
+        write(node.raw ? node.text : encodeHTMLCached(node.text), next)
       }
     }
   }

+ 26 - 2
test/ssr/ssr-string.spec.js

@@ -78,10 +78,34 @@ describe('SSR: renderToString', () => {
       template: '<div>{{ foo }} side {{ bar }}</div>',
       data: {
         foo: 'server',
-        bar: 'rendering'
+        bar: '<span>rendering</span>'
       }
     }, result => {
-      expect(result).toContain('<div server-rendered="true">server side rendering</div>')
+      expect(result).toContain('<div server-rendered="true">server side &lt;span&gt;rendering&lt;&sol;span&gt;</div>')
+      done()
+    })
+  })
+
+  it('v-html', done => {
+    renderVmWithOptions({
+      template: '<div v-html="text"></div>',
+      data: {
+        text: '<span>foo</span>'
+      }
+    }, result => {
+      expect(result).toContain('<div server-rendered="true"><span>foo</span></div>')
+      done()
+    })
+  })
+
+  it('v-text', done => {
+    renderVmWithOptions({
+      template: '<div v-text="text"></div>',
+      data: {
+        text: '<span>foo</span>'
+      }
+    }, result => {
+      expect(result).toContain('<div server-rendered="true">&lt;span&gt;foo&lt;&sol;span&gt;</div>')
       done()
     })
   })