[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

View File

@@ -39,6 +39,14 @@ describe('AppRegistry', () => {
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"', () => {
const getApplicationStyles = appName => {
const { element, getStyleElement } = AppRegistry.getApplication(appName, {});

View File

@@ -46,9 +46,11 @@ export function getApplication(
</AppContainer>
);
// Don't escape CSS text
const getStyleElement = () => {
const getStyleElement = props => {
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 };
}

View File

@@ -69,10 +69,6 @@ export default class StyleSheetManager {
return className;
}
injectKeyframe(): string {
// return identifier;
}
_addToCache(className, prop, value) {
const cache = this._cache;
if (!cache.byProp[prop]) {

View File

@@ -8,6 +8,7 @@
*/
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
import modality from './modality';
export default class WebStyleSheet {
_cssRules = [];
@@ -29,6 +30,7 @@ export default class WebStyleSheet {
}
if (domStyleElement) {
modality(domStyleElement);
// $FlowFixMe
this._sheet = domStyleElement.sheet;
this._textContent = domStyleElement.textContent;

View File

@@ -1,9 +1,5 @@
import modality from '../../modules/modality';
import StyleSheet from './StyleSheet';
// initialize focus-ring fix
modality();
// allow component styles to be editable in React Dev Tools
if (process.env.NODE_ENV !== 'production') {
const { canUseDOM } = require('fbjs/lib/ExecutionEnvironment');

View File

@@ -18,12 +18,14 @@
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
const modality = () => {
const rule = ':focus { outline: none; }';
let ruleExists = false;
const modality = styleElement => {
if (!canUseDOM) {
return;
}
let styleElement;
let hadKeyboardEvent = false;
let keyboardThrottleTimeoutID = 0;
@@ -55,21 +57,6 @@ const modality = () => {
'[role=textbox]'
].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
* `focus-ring`.
@@ -83,20 +70,22 @@ const modality = () => {
};
/**
* Add the focus ring to the focused element
* Add the focus ring style
*/
const addFocusRing = () => {
if (styleElement) {
styleElement.disabled = true;
if (styleElement && ruleExists) {
styleElement.sheet.deleteRule(0);
ruleExists = false;
}
};
/**
* Remove the focus ring
* Remove the focus ring style
*/
const removeFocusRing = () => {
if (styleElement) {
styleElement.disabled = false;
if (styleElement && !ruleExists) {
styleElement.sheet.insertRule(rule, 0);
ruleExists = true;
}
};
@@ -136,7 +125,7 @@ const modality = () => {
};
if (document.body && document.body.addEventListener) {
initialize();
removeFocusRing();
document.body.addEventListener('keydown', handleKeyDown, true);
document.body.addEventListener('focus', handleFocus, true);
document.body.addEventListener('blur', handleBlur, true);

View File

@@ -31,10 +31,18 @@ const AppRegistryScreen = () => (
/>
<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"
name="static getApplication"
typeInfo="(appKey: string, appParameters: ?object) => { element: ReactElement; getStyleElement: () => ReactElement }"
typeInfo="(appKey: string, appParameters: ?object) => { element: ReactElement; getStyleElement: (props) => ReactElement }"
/>
<DocItem