Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #418 Use of inline styles doesn't play nice with Content Security Policy #1976

Conversation

shaurya-sisodia
Copy link

@shaurya-sisodia shaurya-sisodia commented Jun 13, 2022

fix #418

Description

  • updating styles directly on HTMLELement style description rather than updating dynamic stylesheets
  • removing logic related to dynamic stylesheets as they aren't required and violate CSP strict style-src rule
  • removed UTs related to Dynamic stylesheets

Checklist

  • Created tests which fail without the change (if possible)
  • All tests passing (yarn test)
  • Extended the README / documentation, if necessary

…an updating dynamic stylesheets

* removing logic related to dynamic stylesheets as they aren't required and violate CSP sctrict style-src rule
* removed UTs related to Dynamic stylesheets
src/gridstack.ts Outdated
@@ -776,7 +775,7 @@ export class GridStack {
} else {
this.el.parentNode.removeChild(this.el);
}
this._removeStylesheet();
this._max = undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need, already undefined.

Copy link
Member

@adumesny adumesny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for looking into this. Have a few comments and I will need to pull it locally later to test as well.

src/gridstack.ts Outdated
/** @internal */
protected _styles: GridCSSStyleSheet;
/** @internal max height of the gridstack*/
public _max: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it need to be public ? if so '_' isn't the right prefix, and rename to curRow or something (need to see how it's used)

src/gridstack.ts Outdated
@@ -240,6 +235,8 @@ export class GridStack {
protected _extraDragRow = 0;
/** @internal true if nested grid should get column count from our width */
protected _autoColumn?: boolean;
/** @internal calculates height for a given row */
private _getHeight: (row: number) => string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this a proper TS method, not var you assign to function later.

src/gridstack.ts Outdated
@@ -338,6 +335,8 @@ export class GridStack {

this._setStaticClass();

this._getHeight = (rows: number) => (rows * (this.opts.cellHeight as number)) + this.opts.cellHeightUnit;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

src/gridstack.ts Outdated
this._styles._id = id;
this._styles._max = 0;
if (!this._max) {
this._max = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't prevent if (!this._max) {...} clause, so likely incorrect placement.

src/gridstack.ts Outdated
}

// apply layout styles on all widgets
const widgets: HTMLElement[] = Utils.getElements(prefix);
for (const widget of widgets) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Utils.getElements(prefix).forEach(w => Utils.updatePositionStyleOnWidget(w, this._getHeight));

src/utils.ts Outdated

// list of fields we will skip during cloneDeep (nested objects, other internal)
const skipFields = ['_isNested', 'el', 'grid', 'subGrid', 'engine'];
import { GridStackElement, GridStackNode, GridStackOptions, numberOrString, GridStackPosition, GridStackWidget } from './types';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indent all left so we can see actual changes...

* fixed indent in utils.ts
* incorporated review comments
@shaurya-sisodia
Copy link
Author

Hi @adumesny

Thank you so much for such quick feedback.

I have incorporated the review comments. I have a couple of questions. I have fixed the wrong indent that I committed.
Please have a look.

Regards,
Shaurya

src/gridstack.ts Outdated
@@ -240,6 +235,8 @@ export class GridStack {
protected _extraDragRow = 0;
/** @internal true if nested grid should get column count from our width */
protected _autoColumn?: boolean;
/** @internal calculates height for a given row */
private readonly _getHeight = (rows: number) => (rows * (this.opts.cellHeight as number)) + this.opts.cellHeightUnit;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/** @internal returns the CSS height for a given row */
private _getCSSHeight(rows: number): string {
return (rows * (this.opts.cellHeight as number)) + this.opts.cellHeightUnit;
}

and move it to the function area.

src/gridstack.ts Outdated
this._styles._id = id;
this._styles._max = 0;
if (!this._maxRowIndex) {
this._maxRowIndex = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again this doesn't seem right... set to actual value instead ?

if (!this._styles) {
let id = 'gridstack-style-' + (Math.random() * 100000).toFixed();
// insert style to parent (instead of 'head' by default) to support WebComponent
let styleLocation = this.opts.styleInHead ? undefined : this.el.parentNode as HTMLElement;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

styleInHead needs to be removed everywhere (doc and code) as it no longer applies

src/gridstack.ts Outdated

// these are done once only
Utils.addCSSRule(this._styles, prefix, `min-height: ${cellHeight}${cellHeightUnit}`);
Utils.updateStyleOnElements(prefix, {"min-height": cellHeight+cellHeightUnit});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use ' (single) instead of " in TS code.

// content margins
let top: string = this.opts.marginTop + this.opts.marginUnit;
let bottom: string = this.opts.marginBottom + this.opts.marginUnit;
let right: string = this.opts.marginRight + this.opts.marginUnit;
let left: string = this.opts.marginLeft + this.opts.marginUnit;
let content = `${prefix} > .grid-stack-item-content`;
let placeholder = `.${this.opts._styleSheetClass} > .grid-stack-placeholder > .placeholder-content`;
Utils.addCSSRule(this._styles, content, `top: ${top}; right: ${right}; bottom: ${bottom}; left: ${left};`);
Utils.addCSSRule(this._styles, placeholder, `top: ${top}; right: ${right}; bottom: ${bottom}; left: ${left};`);
Utils.updateStyleOnElements(content, {top, right, bottom, left});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

every single new element will also need those attributes, so how do you re-use this code since it isn't a css style anymore ?

src/utils.ts Outdated
@@ -44,7 +44,7 @@ export function obsoleteAttr(el: HTMLElement, oldName: string, newName: string,
if (oldAttr !== null) {
el.setAttribute(newName, oldAttr);
console.warn('gridstack.js: attribute `' + oldName + '`=' + oldAttr + ' is deprecated on this object in ' + rev + ' and has been replaced with `' +
newName + '`. It will be **completely** removed in v1.0');
newName + '`. It will be **completely** removed in v1.0');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

src/gridstack.ts Outdated
@@ -1305,6 +1280,7 @@ export class GridStack {
if (n.y !== undefined && n.y !== null) { el.setAttribute('gs-y', String(n.y)); }
if (n.w) { el.setAttribute('gs-w', String(n.w)); }
if (n.h) { el.setAttribute('gs-h', String(n.h)); }
Utils.updatePositionStyleOnWidget(el, this._getHeight);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would pass n as that is the source of truth, not the gs-*** attributes

src/utils.ts Outdated
const minH= Utils.toNumber(el.getAttribute('gs-min-h'));
const maxH = Utils.toNumber(el.getAttribute('gs-max-h'));
const y = Utils.toNumber(el.getAttribute('gs-y'));
el.style.height = getHeight(h);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would pass GridStackPosition instead.
and you are setting the height(s) and top only - what about left and widths ? seem one sided...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also I would not set properties (like min/max) that are not set

src/utils.ts Outdated
parent.insertBefore(style, parent.firstChild);
/** update CSS style on elements with given selector */
static updateStyleOnElements(selector: string, styles: {[props: string]: string | string[] }): void {
const elements = Utils.getElements(selector);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Utils.getElements(selector).forEach(el => Utils.addElementStyle(el, styles));

let el = document.querySelector('STYLE[gs-style-id=' + id + ']');
if (el && el.parentNode) el.remove();
/** update CSS style on element */
static addElementStyle(el: HTMLElement, styles: {[props: string]: string | string[] }): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!(styles instanceof Object)) return; // not sure this even needed if (!styles) is enough
for (const key in styles) {
if (!styles.hasOwnProperty(key)) return;
const style = styles[key];
if (Array.isArray(style)) { // why do we need this ?
style.forEach(val => el.style[key] = val);
} else {
el.style[key] = style;
}
}

Copy link
Member

@adumesny adumesny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also your code will update all the elements styles when one of them changes, which will be very inefficient (prev code only did incremental stylesheet update as maxRow was reached).

without deleting gridstack.css (or at least any widget location/size info) and gridstack-extra.css you can't tell if you have handled all cases (I doubt it). Are you ONLY removing JS code that create inline styles, but still allowing static styles sheets (with dynamic class setting). I don't know enough about CSP to tell how far we would need to go...

this will need very different architecture.

@adumesny
Copy link
Member

adumesny commented Jun 16, 2022

I tried demo/two.html and your branch is pretty broken as you resize items around...

@shaurya-sisodia
Copy link
Author

Hi @adumesny

CSP strict style rule prohibits the following:

  • Inserting rule using CSSStyleSheet.insertRule() or CSSStyleSheet.addRule()
  • Appending the an element with inline style in the dom: Eg- El.appendChild(<div style="width:100px"></div>);
  • Appending dynamically generated CSSStyleSheets into the DOM.

That's why I had to change the implementation such that we set styles on each element.
In my architecture, we have to update Styles on each element because we have to take into account that the position on multiple elements might have changed too besides the CSS style calculation itself.

We can use the old architecture and still make it CSP compliant for all browsers except SAFARI.
As written here in the following issue:
I have come across replaceSync accepts inline CSS which is not CSP compliant · Issue #98 · WICG/construct-stylesheets (github.com)

We can use CSSStyleSheet.replaceSync() instead of insertRule() to update styles on dynamic style sheet. We can also instead of appending this dynamic stylesheet can use
document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);

We can use old architecture with adoptedStyleSheets and replaceSync is all the browsers and new architecture in case of SAFARI. Or ask applications to use precalculated stylesheets in case of safari similar to IE 8 Support.

@adumesny
Copy link
Member

so we have a mix of css rules + classes for thing that don't change (eg column based layout) but attributes for height stuff...nthat would satisfy CSP but looks odd. Also we do NOT need to update all elements as we know when a node is dirty and needs to be updated (we already track that) so the call to generate styles need to taken out as it was done only of we added rows (or cellHeight/margin changed). Your logic isn't correct.

also keep in m id you changed break some of the demos so there are issues there - just playing with two.html shows issues dragging/resizing around. https://gridstackjs.com/demo/two.html is fine while your version using demo/two.html is not

@shaurya-sisodia
Copy link
Author

Hi @adumesny

I am aware of the demos breaking and have fixed the changes locally and will commit shortly.

Also so we can use the architecture but update styles on elements only when they are dirty?

@shaurya-sisodia
Copy link
Author

shaurya-sisodia commented Jun 19, 2022

Hi @adumesny

I have fixed the code and incorporated the review you sent. The demo is also working perfectly. We are updating styles on dirty nodes only.

Can you please review the architecture? If you feel it's risky I will look to accommodate the risky part.

The proposed architecture is:

  • To update any dynamically calculated styles directly on elements. These are height, min-height, max-height, and top attributes on widgets. And position attributes top, left, right, and bottom on widget content and resizable icon.
  • We update these on styling on all dirty nodes in _updateStyles method. And when widget is added or dropped into the gridstack.
  • We are still using the gs-** attributes as we still need all static stylesheets like before.

I have fixed the demo it was breaking before because I wasn't updating the position styling on widget content and resizable icon when they are dropped into the grid. But now it all should be fixed.

Please let me know your concerns. Thanks for putting your time into this.

Regards

shaurya-sisodia and others added 4 commits June 20, 2022 08:55
…nc to insert css rules

* safari doesn't support contructable stylesheets and replaceSync yet so we are updating styles directly on elements to make gridstack csp compliant
@adumesny
Copy link
Member

adumesny commented May 1, 2023

this is likely not needed (and quite a risky change) given nonce support we now have.
#2229

I would like to close it - we can always get back to it from 'won't fix' issue listed.

@adumesny adumesny closed this May 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use of inline styles doesn't play nice with Content Security Policy
2 participants