Skip to content

Commit b0ad94f

Browse files
committed
properly encode HTML in server-side rendering (fix #3078)
1 parent 67da70d commit b0ad94f

File tree

10 files changed

+76
-26
lines changed

10 files changed

+76
-26
lines changed

.flowconfig

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,3 @@ module.name_mapper='^shared/\(.*\)$' -> '<PROJECT_ROOT>/src/shared/\1'
1717
module.name_mapper='^web/\(.*\)$' -> '<PROJECT_ROOT>/src/platforms/web/\1'
1818
module.name_mapper='^server/\(.*\)$' -> '<PROJECT_ROOT>/src/server/\1'
1919
module.name_mapper='^entries/\(.*\)$' -> '<PROJECT_ROOT>/src/entries/\1'
20-
module.name_mapper='^entities$' -> '<PROJECT_ROOT>/src/compiler/parser/entity-decoder'

build/build.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ var builds = [
7272
{
7373
entry: 'src/entries/web-server-renderer.js',
7474
format: 'cjs',
75-
external: ['stream'],
75+
external: ['stream', 'entities'],
7676
out: 'packages/vue-server-renderer/index.js'
7777
}
7878
]

build/webpack.ssr.dev.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ module.exports = {
1212
resolve: {
1313
alias: alias
1414
},
15+
externals: {
16+
'entities': true
17+
},
1518
module: {
1619
loaders: [
1720
{

flow/compiler.js

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,20 +127,6 @@ declare type ASTText = {
127127

128128
// SFC-parser related declarations
129129

130-
declare module 'de-indent' {
131-
declare var exports: {
132-
(str: string): string;
133-
}
134-
}
135-
136-
declare module 'source-map' {
137-
declare class SourceMapGenerator {
138-
setSourceContent(filename: string, content: string): void;
139-
addMapping(mapping: Object): void;
140-
toString(): string;
141-
}
142-
}
143-
144130
// an object format describing a single-file component.
145131
declare type SFCDescriptor = {
146132
template: ?SFCBlock,

flow/modules.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
declare module 'entities' {
2+
declare function encodeHTML(html: string): string;
3+
declare function decodeHTML(html: string): string;
4+
}
5+
6+
declare module 'de-indent' {
7+
declare var exports: {
8+
(str: string): string;
9+
}
10+
}
11+
12+
declare module 'source-map' {
13+
declare class SourceMapGenerator {
14+
setSourceContent(filename: string, content: string): void;
15+
addMapping(mapping: Object): void;
16+
toString(): string;
17+
}
18+
}

src/core/vdom/vnode.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default class VNode {
1212
componentOptions: VNodeComponentOptions | void;
1313
child: Component | void;
1414
parent: VNode | void;
15+
raw: ?boolean;
1516

1617
constructor (
1718
tag?: string,
Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
11
/* @flow */
22

3+
import VNode from 'core/vdom/vnode'
34
import { renderAttr } from './attrs'
45
import { propsToAttrMap, isRenderableAttr } from 'web/util/attrs'
56

6-
export default function (node: VNodeWithData): ?string {
7+
export default function (node: VNodeWithData): string {
78
const props = node.data.props
9+
let res = ''
810
if (props) {
9-
let res = ''
1011
for (const key in props) {
11-
const attr = propsToAttrMap[key] || key.toLowerCase()
12-
if (isRenderableAttr(attr)) {
13-
res += renderAttr(attr, props[key])
12+
if (key === 'innerHTML') {
13+
setText(node, props[key], true)
14+
} else if (key === 'textContent') {
15+
setText(node, props[key])
16+
} else {
17+
const attr = propsToAttrMap[key] || key.toLowerCase()
18+
if (isRenderableAttr(attr)) {
19+
res += renderAttr(attr, props[key])
20+
}
1421
}
1522
}
16-
return res
1723
}
24+
return res
25+
}
26+
27+
function setText (node, text, raw) {
28+
const child = new VNode(undefined, undefined, undefined, text)
29+
child.raw = raw
30+
node.children = [child]
1831
}

src/platforms/web/util/attrs.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ const isAttr = makeMap(
3232
'target,title,type,usemap,value,width,wrap'
3333
)
3434

35-
export const isRenderableAttr = (name: string): boolean => {
35+
/* istanbul ignore next */
36+
const isRenderableAttr = (name: string): boolean => {
3637
return (
3738
isAttr(name) ||
3839
name.indexOf('data-') === 0 ||
3940
name.indexOf('aria-') === 0
4041
)
4142
}
43+
export { isRenderableAttr }
4244

4345
export const propsToAttrMap = {
4446
acceptCharset: 'accept-charset',

src/server/render.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
/* @flow */
22

3+
import { cached } from 'shared/util'
4+
import { encodeHTML } from 'entities'
35
import { createComponentInstanceForVnode } from 'core/vdom/create-component'
46

7+
const encodeHTMLCached = cached(encodeHTML)
8+
59
export function createRenderFunction (
610
modules: Array<Function>,
711
directives: Object,
@@ -20,7 +24,7 @@ export function createRenderFunction (
2024
if (node.tag) {
2125
renderElement(node, write, next, isRoot)
2226
} else {
23-
write(node.text, next)
27+
write(node.raw ? node.text : encodeHTMLCached(node.text), next)
2428
}
2529
}
2630
}

test/ssr/ssr-string.spec.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,34 @@ describe('SSR: renderToString', () => {
7878
template: '<div>{{ foo }} side {{ bar }}</div>',
7979
data: {
8080
foo: 'server',
81-
bar: 'rendering'
81+
bar: '<span>rendering</span>'
8282
}
8383
}, result => {
84-
expect(result).toContain('<div server-rendered="true">server side rendering</div>')
84+
expect(result).toContain('<div server-rendered="true">server side &lt;span&gt;rendering&lt;&sol;span&gt;</div>')
85+
done()
86+
})
87+
})
88+
89+
it('v-html', done => {
90+
renderVmWithOptions({
91+
template: '<div v-html="text"></div>',
92+
data: {
93+
text: '<span>foo</span>'
94+
}
95+
}, result => {
96+
expect(result).toContain('<div server-rendered="true"><span>foo</span></div>')
97+
done()
98+
})
99+
})
100+
101+
it('v-text', done => {
102+
renderVmWithOptions({
103+
template: '<div v-text="text"></div>',
104+
data: {
105+
text: '<span>foo</span>'
106+
}
107+
}, result => {
108+
expect(result).toContain('<div server-rendered="true">&lt;span&gt;foo&lt;&sol;span&gt;</div>')
85109
done()
86110
})
87111
})

0 commit comments

Comments
 (0)