Skip to content

Commit ee9c516

Browse files
committed
feat(directive): add whitelist for allowed attributes on element
fix #11
1 parent 31301d7 commit ee9c516

File tree

3 files changed

+123
-2
lines changed

3 files changed

+123
-2
lines changed

src/vue/directive.ts

+97-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,98 @@ import { VNode } from 'vue/types/vnode'
33
import { warn } from '../util/warn'
44
import { FluentVueObject } from '../types'
55

6+
// This part is from fluent-dom library
7+
const LOCALIZABLE_ATTRIBUTES = {
8+
'http://www.w3.org/1999/xhtml': {
9+
global: ['title', 'aria-label', 'aria-valuetext', 'aria-moz-hint'],
10+
a: ['download'],
11+
area: ['download', 'alt'],
12+
// value is special-cased in isAttrNameLocalizable
13+
input: ['alt', 'placeholder'],
14+
menuitem: ['label'],
15+
menu: ['label'],
16+
optgroup: ['label'],
17+
option: ['label'],
18+
track: ['label'],
19+
img: ['alt'],
20+
textarea: ['placeholder'],
21+
th: ['abbr']
22+
},
23+
'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul': {
24+
global: ['accesskey', 'aria-label', 'aria-valuetext', 'aria-moz-hint', 'label'],
25+
key: ['key', 'keycode'],
26+
textbox: ['placeholder'],
27+
toolbarbutton: ['tooltiptext']
28+
}
29+
} as any
30+
31+
/**
32+
* Check if attribute is allowed for the given element.
33+
*
34+
* This method is used by the sanitizer when the translation markup contains
35+
* DOM attributes, or when the translation has traits which map to DOM
36+
* attributes.
37+
*
38+
* `explicitlyAllowed` can be passed as a list of attributes explicitly
39+
* allowed on this element.
40+
*
41+
* @param {string} name
42+
* @param {Element} element
43+
* @param {Array} explicitlyAllowed
44+
* @returns {boolean}
45+
* @private
46+
*/
47+
function isAttrNameLocalizable(
48+
name: string,
49+
element: HTMLElement,
50+
explicitlyAllowed: Array<string> | null = null
51+
) {
52+
if (explicitlyAllowed && explicitlyAllowed.includes(name)) {
53+
return true
54+
}
55+
56+
if (element.namespaceURI === null) {
57+
return false
58+
}
59+
60+
const allowed = LOCALIZABLE_ATTRIBUTES[element.namespaceURI]
61+
if (!allowed) {
62+
return false
63+
}
64+
65+
const attrName = name.toLowerCase()
66+
const elemName = element.localName
67+
68+
// Is it a globally safe attribute?
69+
if (allowed.global.includes(attrName)) {
70+
return true
71+
}
72+
73+
// Are there no allowed attributes for this element?
74+
if (!allowed[elemName]) {
75+
return false
76+
}
77+
78+
// Is it allowed on this element?
79+
if (allowed[elemName].includes(attrName)) {
80+
return true
81+
}
82+
83+
// Special case for value on HTML inputs with type button, reset, submit
84+
if (
85+
element.namespaceURI === 'http://www.w3.org/1999/xhtml' &&
86+
elemName === 'input' &&
87+
attrName === 'value'
88+
) {
89+
const type = (element as HTMLInputElement).type.toLowerCase()
90+
if (type === 'submit' || type === 'button' || type === 'reset') {
91+
return true
92+
}
93+
}
94+
95+
return false
96+
}
97+
698
function translate(el: HTMLElement, fluent: FluentVueObject, binding: DirectiveBinding) {
799
const key = binding.arg
8100

@@ -22,8 +114,11 @@ function translate(el: HTMLElement, fluent: FluentVueObject, binding: DirectiveB
22114
el.textContent = fluent.formatPattern(bundle, msg.value, binding.value)
23115
}
24116

25-
for (const [attr] of Object.entries(binding.modifiers)) {
26-
el.setAttribute(attr, fluent.formatPattern(bundle, msg.attributes[attr], binding.value))
117+
const allowedAttrs = Object.keys(binding.modifiers)
118+
for (const [attr, attrValue] of Object.entries(msg.attributes)) {
119+
if (isAttrNameLocalizable(attr, el, allowedAttrs)) {
120+
el.setAttribute(attr, fluent.formatPattern(bundle, attrValue, binding.value))
121+
}
27122
}
28123
}
29124

test/vue/__snapshots__/directive.test.ts.snap

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`directive automaticaly binds whitelisted attrs 1`] = `<a aria-label="Hello ⁨John⁩">Text</a>`;
4+
35
exports[`directive can translate DOM attributes 1`] = `<a href="/foo" aria-label="Localized aria">Hello ⁨John⁩</a>`;
46

57
exports[`directive can use parameters 1`] = `<a href="/foo">Hello ⁨John⁩</a>`;

test/vue/directive.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,30 @@ describe('directive', () => {
109109
expect(mounted).toMatchSnapshot()
110110
})
111111

112+
it('automaticaly binds whitelisted attrs', () => {
113+
// Arrange
114+
bundle.addResource(
115+
new FluentResource(ftl`
116+
link = Text
117+
.aria-label = Hello {$name}
118+
.not-allowed = Not allowed attrs value
119+
`)
120+
)
121+
122+
const component = {
123+
data: () => ({
124+
name: 'John'
125+
}),
126+
template: `<a v-t:link="{ name }">Fallback</a>`
127+
}
128+
129+
// Act
130+
const mounted = mount(component, options)
131+
132+
// Assert
133+
expect(mounted).toMatchSnapshot()
134+
})
135+
112136
it('works without fallbacks', () => {
113137
// Arrange
114138
bundle.addResource(

0 commit comments

Comments
 (0)