[add] support for CSP policy requiring 'nonce' on <style>

CSP policy may prevent writing to `<style>` unless a `nonce` attribute
is set. This change makes that possible by moving the modality-related
styles into the main style sheet, and allowing additional props to be
provided to the `<style>` element when rendering on the server. For
example:

```
const { element, getStyleElement } = AppRegistry.getApplication('App');
const html = renderToString(element);
const css = renderToStaticMarkup(getStyleElement({ nonce }));
```
This commit is contained in:
Nicolas Gallagher
2018-05-14 11:13:46 -07:00
parent 3e4d8d6b2f
commit ee5e80064f
7 changed files with 37 additions and 36 deletions
@@ -39,6 +39,14 @@ describe('AppRegistry', () => {
expect(styleElement).toMatchSnapshot(); expect(styleElement).toMatchSnapshot();
}); });
test('"getStyleElement" adds props to <style>', () => {
const nonce = '2Bz9RM/UHvBbmo3jK/PbYZ==';
AppRegistry.registerComponent('App', () => RootComponent);
const { getStyleElement } = AppRegistry.getApplication('App', {});
const styleElement = getStyleElement({ nonce });
expect(styleElement.props.nonce).toBe(nonce);
});
test('"getStyleElement" produces styles that are a function of rendering "element"', () => { test('"getStyleElement" produces styles that are a function of rendering "element"', () => {
const getApplicationStyles = appName => { const getApplicationStyles = appName => {
const { element, getStyleElement } = AppRegistry.getApplication(appName, {}); const { element, getStyleElement } = AppRegistry.getApplication(appName, {});
@@ -46,9 +46,11 @@ export function getApplication(
</AppContainer> </AppContainer>
); );
// Don't escape CSS text // Don't escape CSS text
const getStyleElement = () => { const getStyleElement = props => {
const sheet = styleResolver.getStyleSheet(); const sheet = styleResolver.getStyleSheet();
return <style dangerouslySetInnerHTML={{ __html: sheet.textContent }} id={sheet.id} />; return (
<style {...props} dangerouslySetInnerHTML={{ __html: sheet.textContent }} id={sheet.id} />
);
}; };
return { element, getStyleElement }; return { element, getStyleElement };
} }
@@ -69,10 +69,6 @@ export default class StyleSheetManager {
return className; return className;
} }
injectKeyframe(): string {
// return identifier;
}
_addToCache(className, prop, value) { _addToCache(className, prop, value) {
const cache = this._cache; const cache = this._cache;
if (!cache.byProp[prop]) { if (!cache.byProp[prop]) {
@@ -8,6 +8,7 @@
*/ */
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
import modality from './modality';
export default class WebStyleSheet { export default class WebStyleSheet {
_cssRules = []; _cssRules = [];
@@ -29,6 +30,7 @@ export default class WebStyleSheet {
} }
if (domStyleElement) { if (domStyleElement) {
modality(domStyleElement);
// $FlowFixMe // $FlowFixMe
this._sheet = domStyleElement.sheet; this._sheet = domStyleElement.sheet;
this._textContent = domStyleElement.textContent; this._textContent = domStyleElement.textContent;
@@ -1,9 +1,5 @@
import modality from '../../modules/modality';
import StyleSheet from './StyleSheet'; import StyleSheet from './StyleSheet';
// initialize focus-ring fix
modality();
// allow component styles to be editable in React Dev Tools // allow component styles to be editable in React Dev Tools
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
const { canUseDOM } = require('fbjs/lib/ExecutionEnvironment'); const { canUseDOM } = require('fbjs/lib/ExecutionEnvironment');
@@ -18,12 +18,14 @@
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
const modality = () => { const rule = ':focus { outline: none; }';
let ruleExists = false;
const modality = styleElement => {
if (!canUseDOM) { if (!canUseDOM) {
return; return;
} }
let styleElement;
let hadKeyboardEvent = false; let hadKeyboardEvent = false;
let keyboardThrottleTimeoutID = 0; let keyboardThrottleTimeoutID = 0;
@@ -55,21 +57,6 @@ const modality = () => {
'[role=textbox]' '[role=textbox]'
].join(','); ].join(',');
/**
* Disable the focus ring by default
*/
const initialize = () => {
// check if the style sheet needs to be created
const id = 'react-native-modality';
styleElement = document.getElementById(id);
if (!styleElement) {
// removes focus styles by default
const style = `<style id="${id}">:focus { outline: none; }</style>`;
document.head.insertAdjacentHTML('afterbegin', style);
styleElement = document.getElementById(id);
}
};
/** /**
* Computes whether the given element should automatically trigger the * Computes whether the given element should automatically trigger the
* `focus-ring`. * `focus-ring`.
@@ -83,20 +70,22 @@ const modality = () => {
}; };
/** /**
* Add the focus ring to the focused element * Add the focus ring style
*/ */
const addFocusRing = () => { const addFocusRing = () => {
if (styleElement) { if (styleElement && ruleExists) {
styleElement.disabled = true; styleElement.sheet.deleteRule(0);
ruleExists = false;
} }
}; };
/** /**
* Remove the focus ring * Remove the focus ring style
*/ */
const removeFocusRing = () => { const removeFocusRing = () => {
if (styleElement) { if (styleElement && !ruleExists) {
styleElement.disabled = false; styleElement.sheet.insertRule(rule, 0);
ruleExists = true;
} }
}; };
@@ -136,7 +125,7 @@ const modality = () => {
}; };
if (document.body && document.body.addEventListener) { if (document.body && document.body.addEventListener) {
initialize(); removeFocusRing();
document.body.addEventListener('keydown', handleKeyDown, true); document.body.addEventListener('keydown', handleKeyDown, true);
document.body.addEventListener('focus', handleFocus, true); document.body.addEventListener('focus', handleFocus, true);
document.body.addEventListener('blur', handleBlur, true); document.body.addEventListener('blur', handleBlur, true);
@@ -31,10 +31,18 @@ const AppRegistryScreen = () => (
/> />
<DocItem <DocItem
description="Use this for server-side rendering to HTML. Returns a object of the given application's element, and a function to get styles once the element is rendered." description={
<AppText>
Use this for server-side rendering to HTML. Returns an object containing the given
application's element and a function to get styles once the element is rendered.
Additional props can be passed to the <Code>getStyleElement</Code> function, e.g., your
CSP policy may require a <Code>nonce</Code> to be set on <Code>style</Code>
elements.
</AppText>
}
label="web" label="web"
name="static getApplication" name="static getApplication"
typeInfo="(appKey: string, appParameters: ?object) => { element: ReactElement; getStyleElement: () => ReactElement }" typeInfo="(appKey: string, appParameters: ?object) => { element: ReactElement; getStyleElement: (props) => ReactElement }"
/> />
<DocItem <DocItem