mirror of https://github.com/artf/grapesjs.git
Browse Source
The editor builds most of its chrome by assigning HTML strings, so every
`style="..."` literal in a template is parsed as an inline style attribute and
blocked by a strict `style-src-attr` policy. `setAttribute('style', ...)` has
the same problem. CSSOM writes (`el.style.prop = value`) are not covered by
CSP, so they are the way to apply values only known at runtime.
Static styles move to utility classes, adding `gjs-pointer-events-all` and
`gjs-clear-float` next to the existing `gjs-hidden` and `gjs-no-pointer-events`:
canvas and frame tools, frame remove icon, modal collector, device add button
and the float clearers in the asset manager, file uploader, modal and style
manager.
Runtime values move to the CSSOM through a new `setStyleText` helper, which
applies a declaration string property by property. It splits only on top-level
`;`, so data URLs and quoted values survive, and it keeps `!important` and
custom properties: asset preview background, navigator indentation, style
manager layer preview, select option styles in the style and trait managers,
RTE action `style` attributes, the canvas iframe component and the color
picker swatches.
Elements whose visibility is toggled at runtime by resetting the inline
display keep using the CSSOM for their initial state, so the toggles still
work. The style manager clear button instead switches to toggling `gjs-hidden`,
since its update is debounced and a class avoids a flash on render.
Out of scope: the `style` attribute of user components (`ComponentView`), which
is the content the editor exists to author, and the SVG image placeholder,
which is serialized to a base64 data URL and governed by `img-src`. Both are
recorded in the allowlist of the new guard spec.
pull/6811/head
25 changed files with 293 additions and 57 deletions
@ -0,0 +1,62 @@ |
|||
import fs from 'fs'; |
|||
import path from 'path'; |
|||
|
|||
/** |
|||
* The editor renders most of its chrome by assigning HTML strings, so a |
|||
* `style="..."` literal in a template ends up parsed as an inline style |
|||
* attribute, which a strict `style-src-attr` CSP blocks. The same goes for |
|||
* `setAttribute('style', ...)`. |
|||
* |
|||
* CSSOM writes (`el.style.prop = value`, `setStyleText`) are not covered by CSP |
|||
* and are the supported way to apply runtime values. |
|||
*/ |
|||
const SRC_DIR = path.join(__dirname, '../../../src'); |
|||
|
|||
// Files allowed to keep an inline style, with the reason why
|
|||
const ALLOWED: Record<string, string> = { |
|||
'dom_components/model/ComponentImage.ts': |
|||
'SVG placeholder serialized to a base64 data URL and used as `img` src, so it is a separate document governed by `img-src`', |
|||
'dom_components/view/ComponentView.ts': |
|||
'writes the style attribute of a user component, which is the content the editor exists to author (and is off by default via `avoidInlineStyle`)', |
|||
}; |
|||
|
|||
const PATTERNS = [ |
|||
{ name: 'style attribute in markup', re: /(^|[^-\w])style\s*=\s*["'`]/ }, |
|||
{ name: "setAttribute('style')", re: /setAttribute\(\s*['"`]style['"`]/ }, |
|||
]; |
|||
|
|||
const stripComments = (code: string) => code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1'); |
|||
|
|||
const walk = (dir: string): string[] => |
|||
fs.readdirSync(dir, { withFileTypes: true }).reduce<string[]>((res, entry) => { |
|||
const full = path.join(dir, entry.name); |
|||
if (entry.isDirectory()) return res.concat(walk(full)); |
|||
return entry.name.endsWith('.ts') ? res.concat(full) : res; |
|||
}, []); |
|||
|
|||
describe('No inline style attributes in editor markup', () => { |
|||
test('src is free of `style=` and `setAttribute("style")`, except the documented cases', () => { |
|||
const found: string[] = []; |
|||
|
|||
walk(SRC_DIR).forEach((file) => { |
|||
const relative = path.relative(SRC_DIR, file).split(path.sep).join('/'); |
|||
if (ALLOWED[relative]) return; |
|||
|
|||
stripComments(fs.readFileSync(file, 'utf8')) |
|||
.split('\n') |
|||
.forEach((line, i) => { |
|||
PATTERNS.forEach(({ name, re }) => { |
|||
re.test(line) && found.push(`${relative}:${i + 1} (${name}) ${line.trim()}`); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
expect(found).toEqual([]); |
|||
}); |
|||
|
|||
test('the allowlist only names files that exist', () => { |
|||
Object.keys(ALLOWED).forEach((relative) => { |
|||
expect(fs.existsSync(path.join(SRC_DIR, relative))).toBe(true); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,63 @@ |
|||
import { setStyleText } from '../../../src/utils/dom'; |
|||
|
|||
describe('setStyleText', () => { |
|||
let el: HTMLElement; |
|||
|
|||
beforeEach(() => { |
|||
el = document.createElement('div'); |
|||
}); |
|||
|
|||
test('applies a single declaration', () => { |
|||
setStyleText(el, 'color: red'); |
|||
expect(el.style.color).toBe('red'); |
|||
}); |
|||
|
|||
test('applies multiple declarations', () => { |
|||
setStyleText(el, 'color: red; padding-left: 10px'); |
|||
expect(el.style.color).toBe('red'); |
|||
expect(el.style.paddingLeft).toBe('10px'); |
|||
}); |
|||
|
|||
test('replaces any style previously set', () => { |
|||
setStyleText(el, 'color: red; width: 10px'); |
|||
setStyleText(el, 'color: blue'); |
|||
expect(el.style.color).toBe('blue'); |
|||
expect(el.style.width).toBe(''); |
|||
}); |
|||
|
|||
test('keeps `;` nested in functions', () => { |
|||
const url = 'data:image/gif;base64,R0lGODlh'; |
|||
setStyleText(el, `background-image: url(${url}); color: red`); |
|||
// jsdom re-serializes the url with quotes, what matters is that the
|
|||
// `;` inside it did not split the declaration
|
|||
expect(el.style.backgroundImage).toContain(url); |
|||
expect(el.style.color).toBe('red'); |
|||
}); |
|||
|
|||
test('keeps `;` nested in strings', () => { |
|||
setStyleText(el, `content: "a;b"; color: red`); |
|||
expect(el.style.color).toBe('red'); |
|||
}); |
|||
|
|||
test('supports !important', () => { |
|||
setStyleText(el, 'color: red !important'); |
|||
expect(el.style.getPropertyPriority('color')).toBe('important'); |
|||
expect(el.style.color).toBe('red'); |
|||
}); |
|||
|
|||
test('supports custom properties', () => { |
|||
setStyleText(el, '--my-var: 10px'); |
|||
expect(el.style.getPropertyValue('--my-var')).toBe('10px'); |
|||
}); |
|||
|
|||
test('tolerates empty, partial and trailing declarations', () => { |
|||
setStyleText(el, ';; color: red ;; padding ;'); |
|||
expect(el.style.color).toBe('red'); |
|||
}); |
|||
|
|||
test('clears the style with an empty input', () => { |
|||
setStyleText(el, 'color: red'); |
|||
setStyleText(el); |
|||
expect(el.getAttribute('style')).toBe(null); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue