Skip to content

Commit bd56393

Browse files
authored
Merge pull request #7109 from martian111/csp-no-inline-style
Remove inline styles that break plots in strict CSP setups
2 parents 056c8e7 + d5ad661 commit bd56393

File tree

8 files changed

+111
-65
lines changed

8 files changed

+111
-65
lines changed

draftlogs/7109_fix.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Remove inline styles that break plots in strict CSP setups [[#7109](https://github.com/plotly/plotly.js/pull/7109)],
2+
with thanks to @martian111 for the contribution!

src/components/modebar/modebar.js

+29-8
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,14 @@ proto.update = function(graphInfo, buttons) {
5454
}
5555

5656
var style = fullLayout.modebar;
57-
var bgSelector = context.displayModeBar === 'hover' ? '.js-plotly-plot .plotly:hover ' : '';
5857

59-
Lib.deleteRelatedStyleRule(modeBarId);
60-
Lib.addRelatedStyleRule(modeBarId, bgSelector + '#' + modeBarId + ' .modebar-group', 'background-color: ' + style.bgcolor);
61-
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn .icon path', 'fill: ' + style.color);
62-
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn:hover .icon path', 'fill: ' + style.activecolor);
63-
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn.active .icon path', 'fill: ' + style.activecolor);
58+
// set style for modebar-group directly instead of inline CSS that's not allowed by strict CSP's
59+
var groupSelector = '#' + modeBarId + ' .modebar-group';
60+
document.querySelectorAll(groupSelector).forEach(function(group) {
61+
group.style.backgroundColor = style.bgcolor;
62+
});
63+
// set styles on hover using event listeners instead of inline CSS that's not allowed by strict CSP's
64+
Lib.setStyleOnHover('#' + modeBarId + ' .modebar-btn', '.active', '.icon path', 'fill: ' + style.activecolor, 'fill: ' + style.color);
6465

6566
// if buttons or logo have changed, redraw modebar interior
6667
var needsNewButtons = !this.hasButtons(buttons);
@@ -129,6 +130,10 @@ proto.updateButtons = function(buttons) {
129130
proto.createGroup = function() {
130131
var group = document.createElement('div');
131132
group.className = 'modebar-group';
133+
134+
var style = this.graphInfo._fullLayout.modebar;
135+
group.style.backgroundColor = style.bgcolor;
136+
132137
return group;
133138
};
134139

@@ -246,18 +251,35 @@ proto.updateActiveButton = function(buttonClicked) {
246251
var isToggleButton = (button.getAttribute('data-toggle') === 'true');
247252
var button3 = d3.select(button);
248253

254+
// set style on button based on its state at the moment this is called
255+
// (e.g. during the handling when a modebar button is clicked)
256+
var updateButtonStyle = function(button, isActive) {
257+
var style = fullLayout.modebar;
258+
var childEl = button.querySelector('.icon path');
259+
if(childEl) {
260+
if(isActive || button.matches(':hover')) {
261+
childEl.style.fill = style.activecolor;
262+
} else {
263+
childEl.style.fill = style.color;
264+
}
265+
}
266+
};
267+
249268
// Use 'data-toggle' and 'buttonClicked' to toggle buttons
250269
// that have no one-to-one equivalent in fullLayout
251270
if(isToggleButton) {
252271
if(dataAttr === dataAttrClicked) {
253-
button3.classed('active', !button3.classed('active'));
272+
var isActive = !button3.classed('active');
273+
button3.classed('active', isActive);
274+
updateButtonStyle(button, isActive);
254275
}
255276
} else {
256277
var val = (dataAttr === null) ?
257278
dataAttr :
258279
Lib.nestedProperty(fullLayout, dataAttr).get();
259280

260281
button3.classed('active', val === thisval);
282+
updateButtonStyle(button, val === thisval);
261283
}
262284
});
263285
};
@@ -317,7 +339,6 @@ proto.removeAllButtons = function() {
317339

318340
proto.destroy = function() {
319341
Lib.removeElement(this.container.querySelector('.modebar'));
320-
Lib.deleteRelatedStyleRule(this._uid);
321342
};
322343

323344
function createModeBar(gd, buttons) {

src/fonts/ploticon.js

+11-21
Original file line numberDiff line numberDiff line change
@@ -167,29 +167,19 @@ module.exports = {
167167
name: 'newplotlylogo',
168168
svg: [
169169
'<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 132 132\'>',
170-
'<defs>',
171-
' <style>',
172-
' .cls-0{fill:#000;}',
173-
' .cls-1{fill:#FFF;}',
174-
' .cls-2{fill:#F26;}',
175-
' .cls-3{fill:#D69;}',
176-
' .cls-4{fill:#BAC;}',
177-
' .cls-5{fill:#9EF;}',
178-
' </style>',
179-
'</defs>',
180170
' <title>plotly-logomark</title>',
181171
' <g id=\'symbol\'>',
182-
' <rect class=\'cls-0\' x=\'0\' y=\'0\' width=\'132\' height=\'132\' rx=\'18\' ry=\'18\'/>',
183-
' <circle class=\'cls-5\' cx=\'102\' cy=\'30\' r=\'6\'/>',
184-
' <circle class=\'cls-4\' cx=\'78\' cy=\'30\' r=\'6\'/>',
185-
' <circle class=\'cls-4\' cx=\'78\' cy=\'54\' r=\'6\'/>',
186-
' <circle class=\'cls-3\' cx=\'54\' cy=\'30\' r=\'6\'/>',
187-
' <circle class=\'cls-2\' cx=\'30\' cy=\'30\' r=\'6\'/>',
188-
' <circle class=\'cls-2\' cx=\'30\' cy=\'54\' r=\'6\'/>',
189-
' <path class=\'cls-1\' d=\'M30,72a6,6,0,0,0-6,6v24a6,6,0,0,0,12,0V78A6,6,0,0,0,30,72Z\'/>',
190-
' <path class=\'cls-1\' d=\'M78,72a6,6,0,0,0-6,6v24a6,6,0,0,0,12,0V78A6,6,0,0,0,78,72Z\'/>',
191-
' <path class=\'cls-1\' d=\'M54,48a6,6,0,0,0-6,6v48a6,6,0,0,0,12,0V54A6,6,0,0,0,54,48Z\'/>',
192-
' <path class=\'cls-1\' d=\'M102,48a6,6,0,0,0-6,6v48a6,6,0,0,0,12,0V54A6,6,0,0,0,102,48Z\'/>',
172+
' <rect fill=\'#000\' x=\'0\' y=\'0\' width=\'132\' height=\'132\' rx=\'18\' ry=\'18\'/>',
173+
' <circle fill=\'#9EF\' cx=\'102\' cy=\'30\' r=\'6\'/>',
174+
' <circle fill=\'#BAC\' cx=\'78\' cy=\'30\' r=\'6\'/>',
175+
' <circle fill=\'#BAC\' cx=\'78\' cy=\'54\' r=\'6\'/>',
176+
' <circle fill=\'#D69\' cx=\'54\' cy=\'30\' r=\'6\'/>',
177+
' <circle fill=\'#F26\' cx=\'30\' cy=\'30\' r=\'6\'/>',
178+
' <circle fill=\'#F26\' cx=\'30\' cy=\'54\' r=\'6\'/>',
179+
' <path fill=\'#FFF\' d=\'M30,72a6,6,0,0,0-6,6v24a6,6,0,0,0,12,0V78A6,6,0,0,0,30,72Z\'/>',
180+
' <path fill=\'#FFF\' d=\'M78,72a6,6,0,0,0-6,6v24a6,6,0,0,0,12,0V78A6,6,0,0,0,78,72Z\'/>',
181+
' <path fill=\'#FFF\' d=\'M54,48a6,6,0,0,0-6,6v48a6,6,0,0,0,12,0V54A6,6,0,0,0,54,48Z\'/>',
182+
' <path fill=\'#FFF\' d=\'M102,48a6,6,0,0,0-6,6v48a6,6,0,0,0,12,0V54A6,6,0,0,0,102,48Z\'/>',
193183
' </g>',
194184
'</svg>'
195185
].join('')

src/lib/dom.js

+48-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ function addStyleRule(selector, styleString) {
6060
function addRelatedStyleRule(uid, selector, styleString) {
6161
var id = 'plotly.js-style-' + uid;
6262
var style = document.getElementById(id);
63+
if(style && style.matches('.no-inline-styles')) {
64+
// Do not proceed if user disable inline styles explicitly...
65+
return;
66+
}
6367
if(!style) {
6468
style = document.createElement('style');
6569
style.setAttribute('id', id);
@@ -69,7 +73,9 @@ function addRelatedStyleRule(uid, selector, styleString) {
6973
}
7074
var styleSheet = style.sheet;
7175

72-
if(styleSheet.insertRule) {
76+
if(!styleSheet) {
77+
loggers.warn('Cannot addRelatedStyleRule, probably due to strict CSP...');
78+
} else if(styleSheet.insertRule) {
7379
styleSheet.insertRule(selector + '{' + styleString + '}', 0);
7480
} else if(styleSheet.addRule) {
7581
styleSheet.addRule(selector, styleString, 0);
@@ -85,6 +91,46 @@ function deleteRelatedStyleRule(uid) {
8591
if(style) removeElement(style);
8692
}
8793

94+
/**
95+
* Setup event listeners on button elements to emulate the ':hover' state without using inline styles,
96+
* which is not allowed with strict CSP. This supports modebar buttons set with the 'active' class,
97+
* in which case, the active style remains even when it's no longer hovered.
98+
* @param {string} selector selector for button elements to be styled when hovered
99+
* @param {string} activeSelector selector used to determine if selected element is active
100+
* @param {string} childSelector the child element on which the styling needs to be updated
101+
* @param {string} activeStyle style that has to be applied when 'hovered' or 'active'
102+
* @param {string} inactiveStyle style that has to be applied when not 'hovered' nor 'active'
103+
*/
104+
function setStyleOnHover(selector, activeSelector, childSelector, activeStyle, inactiveStyle) {
105+
var activeStyleParts = activeStyle.split(':');
106+
var inactiveStyleParts = inactiveStyle.split(':');
107+
var eventAddedAttrName = 'data-btn-style-event-added';
108+
109+
document.querySelectorAll(selector).forEach(function(el) {
110+
if(!el.getAttribute(eventAddedAttrName)) {
111+
// Emulate ":hover" CSS style using JS event handlers to set the
112+
// style in a strict CSP-compliant manner.
113+
el.addEventListener('mouseenter', function() {
114+
var childEl = this.querySelector(childSelector);
115+
if(childEl) {
116+
childEl.style[activeStyleParts[0]] = activeStyleParts[1];
117+
}
118+
});
119+
el.addEventListener('mouseleave', function() {
120+
var childEl = this.querySelector(childSelector);
121+
if(childEl) {
122+
if(activeSelector && this.matches(activeSelector)) {
123+
childEl.style[activeStyleParts[0]] = activeStyleParts[1];
124+
} else {
125+
childEl.style[inactiveStyleParts[0]] = inactiveStyleParts[1];
126+
}
127+
}
128+
});
129+
el.setAttribute(eventAddedAttrName, true);
130+
}
131+
});
132+
}
133+
88134
function getFullTransformMatrix(element) {
89135
var allElements = getElementAndAncestors(element);
90136
// the identity matrix
@@ -162,6 +208,7 @@ module.exports = {
162208
addStyleRule: addStyleRule,
163209
addRelatedStyleRule: addRelatedStyleRule,
164210
deleteRelatedStyleRule: deleteRelatedStyleRule,
211+
setStyleOnHover: setStyleOnHover,
165212
getFullTransformMatrix: getFullTransformMatrix,
166213
getElementTransformMatrix: getElementTransformMatrix,
167214
getElementAndAncestors: getElementAndAncestors,

src/lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ lib.removeElement = domModule.removeElement;
189189
lib.addStyleRule = domModule.addStyleRule;
190190
lib.addRelatedStyleRule = domModule.addRelatedStyleRule;
191191
lib.deleteRelatedStyleRule = domModule.deleteRelatedStyleRule;
192+
lib.setStyleOnHover = domModule.setStyleOnHover;
192193
lib.getFullTransformMatrix = domModule.getFullTransformMatrix;
193194
lib.getElementTransformMatrix = domModule.getElementTransformMatrix;
194195
lib.getElementAndAncestors = domModule.getElementAndAncestors;

tasks/preprocess.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ var path = require('path');
33
var sass = require('sass');
44

55
var constants = require('./util/constants');
6+
var mapBoxGLStyleRules = require('./../src/plots/mapbox/constants').styleRules;
67
var common = require('./util/common');
78
var pullCSS = require('./util/pull_css');
89
var updateVersion = require('./util/update_version');
@@ -13,19 +14,33 @@ exposePartsInLib();
1314
copyTopojsonFiles();
1415
updateVersion(constants.pathToPlotlyVersion);
1516

16-
// convert scss to css to js
17+
// convert scss to css to js and static css file
1718
function makeBuildCSS() {
1819
sass.render({
1920
file: constants.pathToSCSS,
2021
outputStyle: 'compressed'
2122
}, function(err, result) {
2223
if(err) throw err;
2324

24-
// css to js
25+
// To support application with strict CSP where styles cannot be inlined,
26+
// build a static CSS file that can be included into such applications.
27+
var staticCSS = String(result.css);
28+
for(var k in mapBoxGLStyleRules) {
29+
staticCSS = addAdditionalCSSRules(staticCSS, '.js-plotly-plot .plotly .mapboxgl-' + k, mapBoxGLStyleRules[k]);
30+
}
31+
fs.writeFile(constants.pathToCSSDist, staticCSS, function(err) {
32+
if(err) throw err;
33+
});
34+
35+
// css to js to be inlined
2536
pullCSS(String(result.css), constants.pathToCSSBuild);
2637
});
2738
}
2839

40+
function addAdditionalCSSRules(staticStyleString, selector, style) {
41+
return staticStyleString + selector + '{' + style + '}';
42+
}
43+
2944
function exposePartsInLib() {
3045
var obj = {};
3146

tasks/util/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ module.exports = {
217217

218218
pathToSCSS: path.join(pathToSrc, 'css/style.scss'),
219219
pathToCSSBuild: path.join(pathToBuild, 'plotcss.js'),
220+
pathToCSSDist: path.join(pathToDist, 'plotly.css'),
220221

221222
pathToTestDashboardBundle: path.join(pathToBuild, 'test_dashboard-bundle.js'),
222223
pathToReglCodegenBundle: path.join(pathToBuild, 'regl_codegen-bundle.js'),

test/jasmine/tests/modebar_test.js

+2-33
Original file line numberDiff line numberDiff line change
@@ -1544,33 +1544,6 @@ describe('ModeBar', function() {
15441544
expect(style.fill).toBe(color);
15451545
}
15461546

1547-
function getStyleRule() {
1548-
var uid = gd._fullLayout._uid;
1549-
var ownerNode = document.getElementById('plotly.js-style-modebar-' + uid);
1550-
var styleSheets = document.styleSheets;
1551-
for(var i = 0; i < styleSheets.length; i++) {
1552-
var ss = styleSheets[i];
1553-
if(ss.ownerNode === ownerNode) return ss;
1554-
}
1555-
}
1556-
1557-
it('create an associated style element and destroy it on purge', function(done) {
1558-
var styleSelector, style;
1559-
Plotly.newPlot(gd, [], {})
1560-
.then(function() {
1561-
styleSelector = 'style[id*="modebar-' + gd._fullLayout._uid + '"]';
1562-
1563-
style = document.querySelector(styleSelector);
1564-
expect(style).toBeTruthy();
1565-
})
1566-
.then(function() {
1567-
Plotly.purge(gd);
1568-
style = document.querySelector(styleSelector);
1569-
expect(style).toBeNull();
1570-
})
1571-
.then(done, done.fail);
1572-
});
1573-
15741547
it('changes icon colors', function(done) {
15751548
Plotly.newPlot(gd, [], {modebar: { color: colors[0]}})
15761549
.then(function() {
@@ -1602,14 +1575,12 @@ describe('ModeBar', function() {
16021575
Plotly.newPlot(gd, [], {modebar: { bgcolor: colors[0]}})
16031576
.then(function() {
16041577
style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group'));
1605-
expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)');
1606-
expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[0]);
1578+
expect(style.backgroundColor).toBe(colors[0]);
16071579
})
16081580
.then(function() { return Plotly.relayout(gd, 'modebar.bgcolor', colors[1]); })
16091581
.then(function() {
16101582
style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group'));
1611-
expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)');
1612-
expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[1]);
1583+
expect(style.backgroundColor).toBe(colors[1]);
16131584
})
16141585
.then(done, done.fail);
16151586
});
@@ -1619,13 +1590,11 @@ describe('ModeBar', function() {
16191590
.then(function() {
16201591
style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group'));
16211592
expect(style.backgroundColor).toBe(colors[0]);
1622-
expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[0]);
16231593
})
16241594
.then(function() { return Plotly.relayout(gd, 'modebar.bgcolor', colors[1]); })
16251595
.then(function() {
16261596
style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group'));
16271597
expect(style.backgroundColor).toBe(colors[1]);
1628-
expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[1]);
16291598
})
16301599
.then(done, done.fail);
16311600
});

0 commit comments

Comments
 (0)