Skip to content

Fix CSP Strict Style Compatibility #6239

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

Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@
"schema": "node tasks/schema.js",
"stats": "node tasks/stats.js",
"find-strings": "node tasks/find_locale_strings.js",
"preprocess": "node tasks/preprocess.js",
"preprocess": "node tasks/preprocess.js %npm_config_cspNoInlineStyle% %npm_config_pathToCSS%",
"use-draftlogs": "node tasks/use_draftlogs.js",
"empty-draftlogs": "node tasks/empty_draftlogs.js",
"empty-dist": "node tasks/empty_dist.js",
20 changes: 15 additions & 5 deletions src/components/modebar/modebar.js
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@ var Lib = require('../../lib');
var Icons = require('../../fonts/ploticon');
var version = require('../../version').version;

var cspNoInlineStyle = require('./../../lib').cspNoInlineStyle;

var Parser = new DOMParser();

/**
@@ -56,11 +58,19 @@ proto.update = function(graphInfo, buttons) {
var style = fullLayout.modebar;
var bgSelector = context.displayModeBar === 'hover' ? '.js-plotly-plot .plotly:hover ' : '';

Lib.deleteRelatedStyleRule(modeBarId);
Lib.addRelatedStyleRule(modeBarId, bgSelector + '#' + modeBarId + ' .modebar-group', 'background-color: ' + style.bgcolor);
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn .icon path', 'fill: ' + style.color);
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn:hover .icon path', 'fill: ' + style.activecolor);
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn.active .icon path', 'fill: ' + style.activecolor);
if(cspNoInlineStyle) {
// set style rules on elements in csp strict style src compliant manner
Lib.setStyleOnElements(bgSelector + '#' + modeBarId + ' .modebar-group', 'background-color: ' + style.bgcolor);
Lib.setStyleOnElements('#' + modeBarId + ' .modebar-btn .icon path', 'fill: ' + style.color);
Lib.setStyleOnElementForEvent('#' + modeBarId + ' .modebar-btn', 'hover', '.icon path', 'fill: ' + style.activecolor, 'fill: ' + style.color);
Lib.setStyleOnElementForEvent('#' + modeBarId + ' .modebar-btn', 'active', '.icon path', 'fill: ' + style.activecolor, 'fill: ' + style.color);
} else {
Lib.deleteRelatedStyleRule(modeBarId);
Lib.addRelatedStyleRule(modeBarId, bgSelector + '#' + modeBarId + ' .modebar-group', 'background-color: ' + style.bgcolor);
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn .icon path', 'fill: ' + style.color);
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn:hover .icon path', 'fill: ' + style.activecolor);
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn.active .icon path', 'fill: ' + style.activecolor);
}

// if buttons or logo have changed, redraw modebar interior
var needsNewButtons = !this.hasButtons(buttons);
55 changes: 55 additions & 0 deletions src/lib/dom.js
Original file line number Diff line number Diff line change
@@ -85,6 +85,59 @@ function deleteRelatedStyleRule(uid) {
if(style) removeElement(style);
}

/**
* to set style directly on elements in CSP Strict style compatible way
*/
function setStyleOnElements(selector, style) {
var styleRule = style.split(':');
document.querySelectorAll(selector).forEach(function(el) {
// don't update style for active element, as activated state styling is separately handled
if(document.activeElement !== el) {
el.style[styleRule[0]] = styleRule[1];
}
});
}

/**
* set style on element for 'hover' and 'active' state
* this method is in CSP strict style source mode instead of addStyleRule which is not CSP compliant
* @param {string} selector selector on which to listen to event
* @param {string} state 'hover' or 'active' state
* @param {string} childSelector the child element on which the styling needs to be updated
* @param {string} style style that has to be applied on 'hover' or 'active' state
* @param {string} fallbackStyle style that has to be applied when we revert from the state
*/
function setStyleOnElementForEvent(selector, state, childSelector, style, fallbackStyle) {
var styleRule = style.split(':');
var activationEvent;
var deactivationEvent;
switch(state) {
case 'hover':
activationEvent = 'mouseenter';
deactivationEvent = 'mouseleave';
break;
case 'active':
activationEvent = 'mousedown';
deactivationEvent = '';
break;
}

document.querySelectorAll(selector).forEach(function(el) {
if(activationEvent && !el.getAttribute(activationEvent + 'eventAdded')) {
el.addEventListener(activationEvent, function() {
el.querySelector(childSelector).style[styleRule[0]] = styleRule[1];
});
el.setAttribute(activationEvent + 'eventAdded', true);
}
if(deactivationEvent && !el.getAttribute(deactivationEvent + 'eventAdded')) {
el.addEventListener(deactivationEvent, function() {
el.querySelector(childSelector).style[styleRule[0]] = fallbackStyle.split(':')[1];
});
el.setAttribute(deactivationEvent + 'eventAdded', true);
}
});
}

function getFullTransformMatrix(element) {
var allElements = getElementAndAncestors(element);
// the identity matrix
@@ -161,6 +214,8 @@ module.exports = {
addStyleRule: addStyleRule,
addRelatedStyleRule: addRelatedStyleRule,
deleteRelatedStyleRule: deleteRelatedStyleRule,
setStyleOnElements: setStyleOnElements,
setStyleOnElementForEvent: setStyleOnElementForEvent,
getFullTransformMatrix: getFullTransformMatrix,
getElementTransformMatrix: getElementTransformMatrix,
getElementAndAncestors: getElementAndAncestors,
4 changes: 4 additions & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
@@ -12,6 +12,8 @@ var BADNUM = numConstants.BADNUM;

var lib = module.exports = {};

lib.cspNoInlineStyle = false;

lib.adjustFormat = function adjustFormat(formatStr) {
if(
!formatStr ||
@@ -187,6 +189,8 @@ lib.removeElement = domModule.removeElement;
lib.addStyleRule = domModule.addStyleRule;
lib.addRelatedStyleRule = domModule.addRelatedStyleRule;
lib.deleteRelatedStyleRule = domModule.deleteRelatedStyleRule;
lib.setStyleOnElements = domModule.setStyleOnElements;
lib.setStyleOnElementForEvent = domModule.setStyleOnElementForEvent;
lib.getFullTransformMatrix = domModule.getFullTransformMatrix;
lib.getElementTransformMatrix = domModule.getElementTransformMatrix;
lib.getElementAndAncestors = domModule.getElementAndAncestors;
5 changes: 4 additions & 1 deletion src/registry.js
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@ var ExtendModule = require('./lib/extend');
var basePlotAttributes = require('./plots/attributes');
var baseLayoutAttributes = require('./plots/layout_attributes');

var cspNoInlineStyle = require('./lib').cspNoInlineStyle;

var extendFlat = ExtendModule.extendFlat;
var extendDeepAll = ExtendModule.extendDeepAll;

@@ -265,7 +267,8 @@ function registerTraceModule(_module) {
var bpmName = basePlotModule.name;

// add mapbox-gl CSS here to avoid console warning on instantiation
if(bpmName === 'mapbox') {
// in case of cspNoInlineStyle we will add the styles in a static style sheet during preprocess task
if(bpmName === 'mapbox' && !cspNoInlineStyle) {
var styleRules = basePlotModule.constants.styleRules;
for(var k in styleRules) {
addStyleRule('.js-plotly-plot .plotly .mapboxgl-' + k, styleRules[k]);
37 changes: 34 additions & 3 deletions tasks/preprocess.js
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ var path = require('path');
var sass = require('sass');

var constants = require('./util/constants');
var mapBoxGLStyleRules = require('./../src/plots/mapbox/constants').styleRules;
var common = require('./util/common');
var pullCSS = require('./util/pull_css');
var updateVersion = require('./util/update_version');
@@ -13,19 +14,49 @@ exposePartsInLib();
copyTopojsonFiles();
updateVersion(constants.pathToPlotlyVersion);

// convert scss to css to js
// if no csp: convert scss to css to js
// if csp: convert scss to css
function makeBuildCSS() {
sass.render({
file: constants.pathToSCSS,
outputStyle: 'compressed'
}, function(err, result) {
if(err) throw err;

// css to js
pullCSS(String(result.css), constants.pathToCSSBuild);
var cspNoInlineStyle = process.env.npm_config_cspNoInlineStyle;
var pathToCSS = process.env.npm_config_pathToCSS || 'plot-csp.css';
if(cspNoInlineStyle) {
var staticCSS = String(result.css);
for(var k in mapBoxGLStyleRules) {
staticCSS = addAdditionalCSSRules(staticCSS, '.js-plotly-plot .plotly .mapboxgl-' + k, mapBoxGLStyleRules[k]);
}

// if csp no inline style then build css file to include at path relative to dist folder
fs.writeFile(constants.pathToDist + pathToCSS, staticCSS, function(err) {
if(err) throw err;
});

// use plotcss.js to set global cspNoInlineStyle as true
var outStr = ['\'use strict\';',
'',
'var Lib = require(\'../src/lib\');',
'Lib.cspNoInlineStyle = true;',
''].join('\n');

fs.writeFile(constants.pathToCSSBuild, outStr, function(err) {
if(err) throw err;
});
} else {
// css to js
pullCSS(String(result.css), constants.pathToCSSBuild);
}
});
}

function addAdditionalCSSRules(staticStyleString, selector, style) {
return staticStyleString + selector + '{' + style + '}';
}

function exposePartsInLib() {
var obj = {};