Skip to content

Commit 6f1d6a8

Browse files
authored
Protect void elements from translations which try to set their children (#155)
A broken translation may accidentally set a value which <Localized> will normally insert as a child of its wrapped component. For void elements we need to actively forbid this because React throws when trying to set children of void elements, breaking the entire app.
1 parent 261e45b commit 6f1d6a8

File tree

6 files changed

+167
-1
lines changed

6 files changed

+167
-1
lines changed

fluent-react/src/localized.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
33

44
import { isReactLocalization } from './localization';
55
import { parseMarkup } from './markup';
6+
import VOID_ELEMENTS from '../vendor/voidElementTags';
67

78
/*
89
* Prepare props passed to `Localized` for formatting.
@@ -111,7 +112,15 @@ export default class Localized extends Component {
111112
}
112113
}
113114

114-
// If the message has a null value, we're onl interested in its attributes.
115+
// If the wrapped component is a known void element, explicitly dismiss the
116+
// message value and do not pass it to cloneElement in order to avoi the
117+
// "void element tags must neither have `children` nor use
118+
// `dangerouslySetInnerHTML`" error.
119+
if (elem.type in VOID_ELEMENTS) {
120+
return cloneElement(elem, localizedProps);
121+
}
122+
123+
// If the message has a null value, we're only interested in its attributes.
115124
// Do not pass the null value to cloneElement as it would nuke all children
116125
// of the wrapped component.
117126
if (messageValue === null) {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from 'react';
2+
import assert from 'assert';
3+
import { shallow } from 'enzyme';
4+
import { MessageContext } from '../../fluent/src';
5+
import ReactLocalization from '../src/localization';
6+
import { Localized } from '../src/index';
7+
8+
suite('Localized - void elements', function() {
9+
test('do not render the value in void elements', function() {
10+
const mcx = new MessageContext();
11+
const l10n = new ReactLocalization([mcx]);
12+
13+
mcx.addMessages(`
14+
foo = FOO
15+
`)
16+
17+
const wrapper = shallow(
18+
<Localized id="foo">
19+
<input />
20+
</Localized>,
21+
{ context: { l10n } }
22+
);
23+
24+
assert.ok(wrapper.contains(
25+
<input />
26+
));
27+
});
28+
29+
test('render attributes in void elements', function() {
30+
const mcx = new MessageContext();
31+
const l10n = new ReactLocalization([mcx]);
32+
33+
mcx.addMessages(`
34+
foo =
35+
.title = TITLE
36+
`)
37+
38+
const wrapper = shallow(
39+
<Localized id="foo" attrs={{title: true}}>
40+
<input />
41+
</Localized>,
42+
{ context: { l10n } }
43+
);
44+
45+
assert.ok(wrapper.contains(
46+
<input title="TITLE" />
47+
));
48+
});
49+
50+
test('render attributes but not value in void elements', function() {
51+
const mcx = new MessageContext();
52+
const l10n = new ReactLocalization([mcx]);
53+
54+
mcx.addMessages(`
55+
foo = FOO
56+
.title = TITLE
57+
`)
58+
59+
const wrapper = shallow(
60+
<Localized id="foo" attrs={{title: true}}>
61+
<input />
62+
</Localized>,
63+
{ context: { l10n } }
64+
);
65+
66+
assert.ok(wrapper.contains(
67+
<input title="TITLE" />
68+
));
69+
});
70+
});

fluent-react/vendor/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2013-present, Facebook, Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

fluent-react/vendor/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Void HTML Elements
2+
3+
This directory contains MIT-licensed source files from React DOM's codebase
4+
which define a list of void HTML elements. React DOM doesn't allow these
5+
elements to contain any children.
6+
7+
In `fluent-react`, a broken translation may have a value where none is
8+
expected. `<Localized>` components must protect wrapped void elements from
9+
having this unexpected value inserted as children. React throws when trying
10+
to set children of void elements, breaking the entire app.
11+
12+
See [PR #155](https://github.com/projectfluent/fluent.js/pull/155) for more
13+
information.
14+
15+
The files were sourced from the `v16.2.0` tag of React DOM:
16+
17+
- [omittedCloseTags.js](https://github.com/facebook/react/blob/v16.2.0/packages/react-dom/src/shared/omittedCloseTags.js),
18+
- [voidElementTags.js](https://github.com/facebook/react/blob/v16.2.0/packages/react-dom/src/shared/voidElementTags.js).
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in this directory.
6+
*/
7+
8+
// For HTML, certain tags should omit their close tag. We keep a whitelist for
9+
// those special-case tags.
10+
11+
var omittedCloseTags = {
12+
area: true,
13+
base: true,
14+
br: true,
15+
col: true,
16+
embed: true,
17+
hr: true,
18+
img: true,
19+
input: true,
20+
keygen: true,
21+
link: true,
22+
meta: true,
23+
param: true,
24+
source: true,
25+
track: true,
26+
wbr: true,
27+
// NOTE: menuitem's close tag should be omitted, but that causes problems.
28+
};
29+
30+
export default omittedCloseTags;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in this directory.
6+
*/
7+
8+
import omittedCloseTags from './omittedCloseTags';
9+
10+
// For HTML, certain tags cannot have children. This has the same purpose as
11+
// `omittedCloseTags` except that `menuitem` should still have its closing tag.
12+
13+
var voidElementTags = {
14+
menuitem: true,
15+
...omittedCloseTags,
16+
};
17+
18+
export default voidElementTags;

0 commit comments

Comments
 (0)