Bug 1513311 - Allow remote custom CSS to adjust section and/or card styles (#4674)
This commit is contained in:
Родитель
62c87514b3
Коммит
e34ed22aef
|
@ -19,7 +19,32 @@ const MAX_ROWS_HERO = 5;
|
|||
const MAX_ROWS_LISTS = 5;
|
||||
const MAX_ROWS_CARDGRID = 8;
|
||||
|
||||
const ALLOWED_CSS_URL_PREFIXES = ["chrome://", "resource://", "https://img-getpocket.cdn.mozilla.net/"];
|
||||
const DUMMY_CSS_SELECTOR = "DUMMY#CSS.SELECTOR";
|
||||
|
||||
/**
|
||||
* Validate a CSS declaration. The values are assumed to be normalized by CSSOM.
|
||||
*/
|
||||
export function isAllowedCSS(property, value) {
|
||||
// Bug 1454823: INTERNAL properties, e.g., -moz-context-properties, are
|
||||
// exposed but their values aren't resulting in getting nothing. Fortunately,
|
||||
// we don't care about validating the values of the current set of properties.
|
||||
if (value === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Make sure all urls are of the allowed protocols/prefixes
|
||||
const urls = value.match(/url\("[^"]+"\)/g);
|
||||
return !urls || urls.every(url => ALLOWED_CSS_URL_PREFIXES.some(prefix =>
|
||||
url.slice(5).startsWith(prefix)));
|
||||
}
|
||||
|
||||
export class _DiscoveryStreamBase extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onStyleMount = this.onStyleMount.bind(this);
|
||||
}
|
||||
|
||||
extractRows(component, limit) {
|
||||
if (component.data && component.data.recommendations) {
|
||||
const items = Math.min(limit, component.properties.items || component.data.recommendations.length);
|
||||
|
@ -29,6 +54,54 @@ export class _DiscoveryStreamBase extends React.PureComponent {
|
|||
return [];
|
||||
}
|
||||
|
||||
onStyleMount(style) {
|
||||
// Unmounting style gets rid of old styles, so nothing else to do
|
||||
if (!style) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {sheet} = style;
|
||||
const styles = JSON.parse(style.dataset.styles);
|
||||
styles.forEach((row, rowIndex) => {
|
||||
row.forEach((component, componentIndex) => {
|
||||
// Nothing to do without optional styles overrides
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(component).forEach(([selectors, declarations]) => {
|
||||
// Start with a dummy rule to validate declarations and selectors
|
||||
sheet.insertRule(`${DUMMY_CSS_SELECTOR} {}`);
|
||||
const [rule] = sheet.cssRules;
|
||||
|
||||
// Validate declarations and remove any offenders. CSSOM silently
|
||||
// discards invalid entries, so here we apply extra restrictions.
|
||||
rule.style = declarations;
|
||||
[...rule.style].forEach(property => {
|
||||
const value = rule.style[property];
|
||||
if (!isAllowedCSS(property, value)) {
|
||||
console.error(`Bad CSS declaration ${property}: ${value}`); // eslint-disable-line no-console
|
||||
rule.style.removeProperty(property);
|
||||
}
|
||||
});
|
||||
|
||||
// Set the actual desired selectors scoped to the component
|
||||
const prefix = `.ds-layout > .ds-column:nth-child(${rowIndex + 1}) > :nth-child(${componentIndex + 1})`;
|
||||
// NB: Splitting on "," doesn't work with strings with commas, but
|
||||
// we're okay with not supporting those selectors
|
||||
rule.selectorText = selectors.split(",").map(selector => prefix +
|
||||
// Assume :pseudo-classes are for component instead of descendant
|
||||
(selector[0] === ":" ? "" : " ") + selector).join(",");
|
||||
|
||||
// CSSOM silently ignores bad selectors, so we'll be noisy instead
|
||||
if (rule.selectorText === DUMMY_CSS_SELECTOR) {
|
||||
console.error(`Bad CSS selector ${selectors}`); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderComponent(component) {
|
||||
let rows;
|
||||
|
||||
|
@ -81,19 +154,29 @@ export class _DiscoveryStreamBase extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
renderStyles(styles) {
|
||||
// Use json string as both the key and styles to render so React knows when
|
||||
// to unmount and mount a new instance for new styles.
|
||||
const json = JSON.stringify(styles);
|
||||
return (<style key={json} data-styles={json} ref={this.onStyleMount} />);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {layoutRender} = this.props.DiscoveryStream;
|
||||
const styles = [];
|
||||
return (
|
||||
<div className="discovery-stream ds-layout">
|
||||
{layoutRender.map((row, rowIndex) => (
|
||||
<div key={`row-${rowIndex}`} className={`ds-column ds-column-${row.width}`}>
|
||||
{row.components.map((component, componentIndex) => (
|
||||
<div key={`component-${componentIndex}`}>
|
||||
{row.components.map((component, componentIndex) => {
|
||||
styles[rowIndex] = [...styles[rowIndex] || [], component.styles];
|
||||
return (<div key={`component-${componentIndex}`}>
|
||||
{this.renderComponent(component)}
|
||||
</div>
|
||||
))}
|
||||
</div>);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
{this.renderStyles(styles)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import {isAllowedCSS} from "content-src/components/DiscoveryStreamBase/DiscoveryStreamBase";
|
||||
|
||||
describe("<isAllowedCSS>", () => {
|
||||
it("should allow colors", () => {
|
||||
assert.isTrue(isAllowedCSS("color", "red"));
|
||||
});
|
||||
|
||||
it("should allow resource urls", () => {
|
||||
assert.isTrue(isAllowedCSS("background-image", `url("resource://activity-stream/data/content/assets/glyph-info-16.svg")`));
|
||||
});
|
||||
|
||||
it("should allow chrome urls", () => {
|
||||
assert.isTrue(isAllowedCSS("background-image", `url("chrome://browser/skin/history.svg")`));
|
||||
});
|
||||
|
||||
it("should allow allowed https urls", () => {
|
||||
assert.isTrue(isAllowedCSS("background-image", `url("https://img-getpocket.cdn.mozilla.net/media/image.png")`));
|
||||
});
|
||||
|
||||
it("should disallow other https urls", () => {
|
||||
assert.isFalse(isAllowedCSS("background-image", `url("https://mozilla.org/media/image.png")`));
|
||||
});
|
||||
|
||||
it("should disallow other protocols", () => {
|
||||
assert.isFalse(isAllowedCSS("background-image", `url("ftp://mozilla.org/media/image.png")`));
|
||||
});
|
||||
|
||||
it("should allow allowed multiple valid urls", () => {
|
||||
assert.isTrue(isAllowedCSS("background-image", `url("https://img-getpocket.cdn.mozilla.net/media/image.png"), url("chrome://browser/skin/history.svg")`));
|
||||
});
|
||||
|
||||
it("should disallow if any invaild", () => {
|
||||
assert.isFalse(isAllowedCSS("background-image", `url("chrome://browser/skin/history.svg"), url("ftp://mozilla.org/media/image.png")`));
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче