mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-23 17:20:34 +03:00
feat: support json content (without disabling sanitizers) (#415)
* feat: add json prop to bypass sanitizers * chore: fix lint * feat: escape keys as well test: fix json escaping * add escapeKeys into escapeOptions
This commit is contained in:
@@ -56,6 +56,11 @@ export default function updateTag (appId, options = {}, type, tags, head, body)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (attr === 'json') {
|
||||||
|
newElement.innerHTML = JSON.stringify(tag.json)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (attr === 'cssText') {
|
if (attr === 'cssText') {
|
||||||
if (newElement.styleSheet) {
|
if (newElement.styleSheet) {
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
|
|||||||
@@ -62,8 +62,13 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {}
|
|||||||
attrs += ` ${prefix}${attr}` + (isBooleanAttr ? '' : `="${tag[attr]}"`)
|
attrs += ` ${prefix}${attr}` + (isBooleanAttr ? '' : `="${tag[attr]}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let json = ''
|
||||||
|
if (tag.json) {
|
||||||
|
json = JSON.stringify(tag.json)
|
||||||
|
}
|
||||||
|
|
||||||
// grab child content from one of these attributes, if possible
|
// grab child content from one of these attributes, if possible
|
||||||
const content = tag.innerHTML || tag.cssText || ''
|
const content = tag.innerHTML || tag.cssText || json
|
||||||
|
|
||||||
// generate tag exactly without any other redundant attribute
|
// generate tag exactly without any other redundant attribute
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const tagsWithoutEndTag = ['base', 'meta', 'link']
|
|||||||
export const tagsWithInnerContent = ['noscript', 'script', 'style']
|
export const tagsWithInnerContent = ['noscript', 'script', 'style']
|
||||||
|
|
||||||
// Attributes which are inserted as childNodes instead of HTMLAttribute
|
// Attributes which are inserted as childNodes instead of HTMLAttribute
|
||||||
export const tagAttributeAsInnerContent = ['innerHTML', 'cssText']
|
export const tagAttributeAsInnerContent = ['innerHTML', 'cssText', 'json']
|
||||||
|
|
||||||
// Attributes which should be added with data- prefix
|
// Attributes which should be added with data- prefix
|
||||||
export const commonDataAttributes = ['body', 'pbody']
|
export const commonDataAttributes = ['body', 'pbody']
|
||||||
|
|||||||
+15
-5
@@ -21,7 +21,7 @@ export const clientSequences = [
|
|||||||
// sanitizes potentially dangerous characters
|
// sanitizes potentially dangerous characters
|
||||||
export function escape (info, options, escapeOptions) {
|
export function escape (info, options, escapeOptions) {
|
||||||
const { tagIDKeyName } = options
|
const { tagIDKeyName } = options
|
||||||
const { doEscape = v => v } = escapeOptions
|
const { doEscape = v => v, escapeKeys } = escapeOptions
|
||||||
const escaped = {}
|
const escaped = {}
|
||||||
|
|
||||||
for (const key in info) {
|
for (const key in info) {
|
||||||
@@ -55,15 +55,25 @@ export function escape (info, options, escapeOptions) {
|
|||||||
escaped[key] = doEscape(value)
|
escaped[key] = doEscape(value)
|
||||||
} else if (isArray(value)) {
|
} else if (isArray(value)) {
|
||||||
escaped[key] = value.map((v) => {
|
escaped[key] = value.map((v) => {
|
||||||
return isPureObject(v)
|
if (isPureObject(v)) {
|
||||||
? escape(v, options, escapeOptions)
|
return escape(v, options, { ...escapeOptions, escapeKeys: true })
|
||||||
: doEscape(v)
|
}
|
||||||
|
|
||||||
|
return doEscape(v)
|
||||||
})
|
})
|
||||||
} else if (isPureObject(value)) {
|
} else if (isPureObject(value)) {
|
||||||
escaped[key] = escape(value, options, escapeOptions)
|
escaped[key] = escape(value, options, { ...escapeOptions, escapeKeys: true })
|
||||||
} else {
|
} else {
|
||||||
escaped[key] = value
|
escaped[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (escapeKeys) {
|
||||||
|
const escapedKey = doEscape(key)
|
||||||
|
if (key !== escapedKey) {
|
||||||
|
escaped[escapedKey] = escaped[key]
|
||||||
|
delete escaped[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return escaped
|
return escaped
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import _getMetaInfo from '../../src/shared/getMetaInfo'
|
import _getMetaInfo from '../../src/shared/getMetaInfo'
|
||||||
import { loadVueMetaPlugin } from '../utils'
|
import { loadVueMetaPlugin } from '../utils'
|
||||||
import { defaultOptions } from '../../src/shared/constants'
|
import { defaultOptions } from '../../src/shared/constants'
|
||||||
|
import { serverSequences } from '../../src/shared/escaping'
|
||||||
|
|
||||||
const getMetaInfo = (component, escapeSequences) => _getMetaInfo(defaultOptions, component, escapeSequences)
|
const getMetaInfo = (component, escapeSequences) => _getMetaInfo(defaultOptions, component, escapeSequences)
|
||||||
|
|
||||||
@@ -96,4 +97,43 @@ describe('escaping', () => {
|
|||||||
__dangerouslyDisableSanitizersByTagID: { noscape: ['innerHTML'] }
|
__dangerouslyDisableSanitizersByTagID: { noscape: ['innerHTML'] }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('json is still safely escaped', () => {
|
||||||
|
const component = new Vue({
|
||||||
|
metaInfo: {
|
||||||
|
script: [
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
perfectlySave: '</script><p class="unsafe">This is safe</p><script>',
|
||||||
|
'</script>unsafeKey': 'This is also still safe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getMetaInfo(component, serverSequences)).toEqual({
|
||||||
|
title: undefined,
|
||||||
|
titleChunk: '',
|
||||||
|
titleTemplate: '%s',
|
||||||
|
htmlAttrs: {},
|
||||||
|
headAttrs: {},
|
||||||
|
bodyAttrs: {},
|
||||||
|
meta: [],
|
||||||
|
base: [],
|
||||||
|
link: [],
|
||||||
|
style: [],
|
||||||
|
script: [
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
perfectlySave: '</script><p class="unsafe">This is safe</p><script>',
|
||||||
|
'</script>unsafeKey': 'This is also still safe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
noscript: [],
|
||||||
|
__dangerouslyDisableSanitizers: [],
|
||||||
|
__dangerouslyDisableSanitizersByTagID: {}
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -116,12 +116,22 @@ const metaInfoData = {
|
|||||||
{ src: 'src1', async: false, defer: true, [defaultOptions.tagIDKeyName]: 'content', callback: () => {} },
|
{ src: 'src1', async: false, defer: true, [defaultOptions.tagIDKeyName]: 'content', callback: () => {} },
|
||||||
{ src: 'src-prepend', async: true, defer: false, pbody: true },
|
{ src: 'src-prepend', async: true, defer: false, pbody: true },
|
||||||
{ src: 'src2', async: false, defer: true, body: true },
|
{ src: 'src2', async: false, defer: true, body: true },
|
||||||
{ src: 'src3', async: false, skip: true }
|
{ src: 'src3', async: false, skip: true },
|
||||||
|
{ type: 'application/ld+json',
|
||||||
|
json: {
|
||||||
|
'@context': 'http://schema.org',
|
||||||
|
'@type': 'Organization',
|
||||||
|
'name': 'MyApp',
|
||||||
|
'url': 'https://www.myurl.com',
|
||||||
|
'logo': 'https://www.myurl.com/images/logo.png'
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
expect: [
|
expect: [
|
||||||
'<script data-vue-meta="ssr" src="src1" defer data-vmid="content" onload="this.__vm_l=1"></script>',
|
'<script data-vue-meta="ssr" src="src1" defer data-vmid="content" onload="this.__vm_l=1"></script>',
|
||||||
'<script data-vue-meta="ssr" src="src-prepend" async data-pbody="true"></script>',
|
'<script data-vue-meta="ssr" src="src-prepend" async data-pbody="true"></script>',
|
||||||
'<script data-vue-meta="ssr" src="src2" defer data-body="true"></script>'
|
'<script data-vue-meta="ssr" src="src2" defer data-body="true"></script>',
|
||||||
|
'<script data-vue-meta="ssr" type="application/ld+json">{"@context":"http://schema.org","@type":"Organization","name":"MyApp","url":"https://www.myurl.com","logo":"https://www.myurl.com/images/logo.png"}</script>'
|
||||||
],
|
],
|
||||||
test (side, defaultTest) {
|
test (side, defaultTest) {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -139,12 +149,13 @@ const metaInfoData = {
|
|||||||
// ssr doesnt generate data-body tags
|
// ssr doesnt generate data-body tags
|
||||||
const bodyPrepended = this.expect[1]
|
const bodyPrepended = this.expect[1]
|
||||||
const bodyAppended = this.expect[2]
|
const bodyAppended = this.expect[2]
|
||||||
this.expect = [this.expect[0]]
|
this.expect = [this.expect.shift(), this.expect.pop()]
|
||||||
|
|
||||||
const tags = defaultTest()
|
const tags = defaultTest()
|
||||||
|
const html = tags.text()
|
||||||
|
|
||||||
expect(tags.text()).not.toContain(bodyPrepended)
|
expect(html).not.toContain(bodyPrepended)
|
||||||
expect(tags.text()).not.toContain(bodyAppended)
|
expect(html).not.toContain(bodyAppended)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user