Skip to content

Commit 46ea43c

Browse files
committed
Remove inline styles that break plots in strict CSP setups
Support strict Content Security Policies that don't allow unsafe inline styles by: * Removing add/deleteRelatedStyleRule from modebar and use event listeners to emulate the on hover behavior by setting style properties directly on the element, which is allowed in strict CSP environments. * Output the main/library-wide CSS rules that are inlined when the library loads into a static CSS file so that users can include it within their applications in an acceptable manner. This allows `addRelatedStyleRule` calls to fail without affecting functionality. * Provide a way to prevent `addRelatedStyleRule` from running when the static CSS file is already included in the app to prevent superfluous errors in console output. * Replace inline styles from "newplotlylogo" with attribute to set the fill color directly on the elements. Note: The `dist/plotly.css` file will need to be added to the release files. Fixes plotly#2355 Plotly uses inline CSS
1 parent 77e58b5 commit 46ea43c

File tree

6 files changed

+102
-31
lines changed

6 files changed

+102
-31
lines changed

src/components/modebar/modebar.js

+24-7
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,8 @@ proto.update = function(graphInfo, buttons) {
5656
var style = fullLayout.modebar;
5757
var bgSelector = context.displayModeBar === 'hover' ? '.js-plotly-plot .plotly:hover ' : '';
5858

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);
59+
// set styles on hover using event listeners instead of inline CSS that's not allowed by strict CSP's
60+
Lib.setStyleOnHover('#' + modeBarId + ' .modebar-btn', '.active', '.icon path', 'fill: ' + style.activecolor, 'fill: ' + style.color);
6461

6562
// if buttons or logo have changed, redraw modebar interior
6663
var needsNewButtons = !this.hasButtons(buttons);
@@ -129,6 +126,10 @@ proto.updateButtons = function(buttons) {
129126
proto.createGroup = function() {
130127
var group = document.createElement('div');
131128
group.className = 'modebar-group';
129+
130+
var style = this.graphInfo._fullLayout.modebar;
131+
group.style.backgroundColor = style.bgcolor;
132+
132133
return group;
133134
};
134135

@@ -246,18 +247,35 @@ proto.updateActiveButton = function(buttonClicked) {
246247
var isToggleButton = (button.getAttribute('data-toggle') === 'true');
247248
var button3 = d3.select(button);
248249

250+
// set style on button based on its state at the moment this is called
251+
// (e.g. during the handling when a modebar button is clicked)
252+
var updateButtonStyle = function(button, isActive) {
253+
var style = fullLayout.modebar;
254+
var childEl = button.querySelector('.icon path');
255+
if(childEl) {
256+
if(isActive || button.matches(':hover')) {
257+
childEl.style.fill = style.activecolor;
258+
} else {
259+
childEl.style.fill = style.color;
260+
}
261+
}
262+
};
263+
249264
// Use 'data-toggle' and 'buttonClicked' to toggle buttons
250265
// that have no one-to-one equivalent in fullLayout
251266
if(isToggleButton) {
252267
if(dataAttr === dataAttrClicked) {
253-
button3.classed('active', !button3.classed('active'));
268+
var isActive = !button3.classed('active');
269+
button3.classed('active', isActive);
270+
updateButtonStyle(button, isActive);
254271
}
255272
} else {
256273
var val = (dataAttr === null) ?
257274
dataAttr :
258275
Lib.nestedProperty(fullLayout, dataAttr).get();
259276

260277
button3.classed('active', val === thisval);
278+
updateButtonStyle(button, val === thisval);
261279
}
262280
});
263281
};
@@ -317,7 +335,6 @@ proto.removeAllButtons = function() {
317335

318336
proto.destroy = function() {
319337
Lib.removeElement(this.container.querySelector('.modebar'));
320-
Lib.deleteRelatedStyleRule(this._uid);
321338
};
322339

323340
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
@@ -221,6 +221,7 @@ module.exports = {
221221

222222
pathToSCSS: path.join(pathToSrc, 'css/style.scss'),
223223
pathToCSSBuild: path.join(pathToBuild, 'plotcss.js'),
224+
pathToCSSDist: path.join(pathToDist, 'plotly.css'),
224225

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

0 commit comments

Comments
 (0)