Skip to content

Commit 55128f7

Browse files
committed
Move CSS in a separate file to be CSP-compliant (#6048)
In order to be compatible with any CSP, we need to prevent the automatic creation of the DOM 'style' element and offer our CSS as a separate file that can be manually loaded (`Chart.js` or `Chart.min.js`). Users can now opt-out the style injection using `Chart.platform.disableCSSInjection = true` (note that the style sheet is now injected on the first chart creation). To prevent duplicating and maintaining the same CSS code at different places, move all these rules in `platform.dom.css` and write a minimal rollup plugin to inject that style as string in `platform.dom.js`. Additionally, this plugin extract the imported style in `./dist/Chart.js` and `./dist/Chart.min.js`.
1 parent c6c4db7 commit 55128f7

File tree

14 files changed

+266
-50
lines changed

14 files changed

+266
-50
lines changed

docs/getting-started/integration.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,18 @@ require(['moment'], function() {
8484
});
8585
});
8686
```
87+
88+
## Content Security Policy
89+
90+
By default, Chart.js injects CSS directly into the DOM. For webpages secured using [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP), this requires to allow `style-src 'unsafe-inline'`. For stricter CSP environments, where only `style-src 'self'` is allowed, the following CSS file needs to be manually added to your webpage:
91+
92+
```html
93+
<link rel="stylesheet" type="text/css" href="path/to/chartjs/dist/Chart.min.css">
94+
```
95+
96+
And the style injection must be turned off **before creating the first chart**:
97+
98+
```javascript
99+
// Disable automatic style injection
100+
Chart.platform.disableCSSInjection = true;
101+
```

gulpfile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ function buildTask() {
8686
function packageTask() {
8787
return merge(
8888
// gather "regular" files landing in the package root
89-
gulp.src([outDir + '*.js', 'LICENSE.md']),
89+
gulp.src([outDir + '*.js', outDir + '*.css', 'LICENSE.md']),
9090

9191
// since we moved the dist files one folder up (package root), we need to rewrite
9292
// samples src="../dist/ to src="../ and then copy them in the /samples directory.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"url": "https://github.com/chartjs/Chart.js/issues"
2424
},
2525
"devDependencies": {
26+
"clean-css": "^4.2.1",
2627
"coveralls": "^3.0.0",
2728
"eslint": "^5.9.0",
2829
"eslint-config-chartjs": "^0.1.0",

rollup.config.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const commonjs = require('rollup-plugin-commonjs');
44
const resolve = require('rollup-plugin-node-resolve');
55
const terser = require('rollup-plugin-terser').terser;
66
const optional = require('./rollup.plugins').optional;
7+
const stylesheet = require('./rollup.plugins').stylesheet;
78
const pkg = require('./package.json');
89

910
const input = 'src/chart.js';
@@ -23,6 +24,9 @@ module.exports = [
2324
plugins: [
2425
resolve(),
2526
commonjs(),
27+
stylesheet({
28+
extract: true
29+
}),
2630
optional({
2731
include: ['moment']
2832
})
@@ -49,6 +53,10 @@ module.exports = [
4953
optional({
5054
include: ['moment']
5155
}),
56+
stylesheet({
57+
extract: true,
58+
minify: true
59+
}),
5260
terser({
5361
output: {
5462
preamble: banner
@@ -76,7 +84,8 @@ module.exports = [
7684
input: input,
7785
plugins: [
7886
resolve(),
79-
commonjs()
87+
commonjs(),
88+
stylesheet()
8089
],
8190
output: {
8291
name: 'Chart',
@@ -91,6 +100,9 @@ module.exports = [
91100
plugins: [
92101
resolve(),
93102
commonjs(),
103+
stylesheet({
104+
minify: true
105+
}),
94106
terser({
95107
output: {
96108
preamble: banner

rollup.plugins.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/* eslint-env es6 */
2+
const cleancss = require('clean-css');
3+
const path = require('path');
24

35
const UMD_WRAPPER_RE = /(\(function \(global, factory\) \{)((?:\s.*?)*)(\}\(this,)/;
46
const CJS_FACTORY_RE = /(module.exports = )(factory\(.*?\))( :)/;
@@ -56,6 +58,51 @@ function optional(config = {}) {
5658
};
5759
}
5860

61+
// https://github.com/chartjs/Chart.js/issues/5208
62+
function stylesheet(config = {}) {
63+
const minifier = new cleancss();
64+
const styles = [];
65+
66+
return {
67+
name: 'stylesheet',
68+
transform(code, id) {
69+
// Note that 'id' can be mapped to a CJS proxy import, in which case
70+
// 'id' will start with 'commonjs-proxy', so let's first check if we
71+
// are importing an existing css file (i.e. startsWith()).
72+
if (!id.startsWith(path.resolve('.')) || !id.endsWith('.css')) {
73+
return;
74+
}
75+
76+
if (config.minify) {
77+
code = minifier.minify(code).styles;
78+
}
79+
80+
// keep track of all imported stylesheets (already minified)
81+
styles.push(code);
82+
83+
return {
84+
code: 'export default ' + JSON.stringify(code)
85+
};
86+
},
87+
generateBundle(opts, bundle) {
88+
if (!config.extract) {
89+
return;
90+
}
91+
92+
const entry = Object.keys(bundle).find(v => bundle[v].isEntry);
93+
const name = (entry || '').replace(/\.js$/i, '.css');
94+
if (!name) {
95+
this.error('failed to guess the output file name');
96+
}
97+
98+
bundle[name] = {
99+
code: styles.filter(v => !!v).join('')
100+
};
101+
}
102+
};
103+
}
104+
59105
module.exports = {
60-
optional
106+
optional,
107+
stylesheet
61108
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.content {
2+
max-width: 640px;
3+
margin: auto;
4+
padding: 1rem;
5+
}
6+
7+
.note {
8+
font-family: sans-serif;
9+
color: #5050a0;
10+
line-height: 1.4;
11+
margin-bottom: 1rem;
12+
padding: 1rem;
13+
}
14+
15+
code {
16+
background-color: #f5f5ff;
17+
border: 1px solid #d0d0fa;
18+
border-radius: 4px;
19+
padding: 0.05rem 0.25rem;
20+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE html>
2+
<html lang="en-US">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
8+
<title>Scriptable > Bubble | Chart.js sample</title>
9+
<link rel="stylesheet" type="text/css" href="../../dist/Chart.min.css">
10+
<link rel="stylesheet" type="text/css" href="./content-security-policy.css">
11+
<script src="../../dist/Chart.min.js"></script>
12+
<script src="../utils.js"></script>
13+
<script src="content-security-policy.js"></script>
14+
</head>
15+
<body>
16+
<div class="content">
17+
<div class="note">
18+
In order to support a strict content security policy (<code>default-src 'self'</code>),
19+
this page manually loads <code>Chart.min.css</code> and turns off the automatic style
20+
injection by setting <code>Chart.platform.disableCSSInjection = true;</code>.
21+
</div>
22+
<div class="wrapper">
23+
<canvas id="chart-0"></canvas>
24+
</div>
25+
</div>
26+
</body>
27+
</html>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
var utils = Samples.utils;
2+
3+
// CSP: disable automatic style injection
4+
Chart.platform.disableCSSInjection = true;
5+
6+
utils.srand(110);
7+
8+
function generateData() {
9+
var DATA_COUNT = 16;
10+
var MIN_XY = -150;
11+
var MAX_XY = 100;
12+
var data = [];
13+
var i;
14+
15+
for (i = 0; i < DATA_COUNT; ++i) {
16+
data.push({
17+
x: utils.rand(MIN_XY, MAX_XY),
18+
y: utils.rand(MIN_XY, MAX_XY),
19+
v: utils.rand(0, 1000)
20+
});
21+
}
22+
23+
return data;
24+
}
25+
26+
window.addEventListener('load', function() {
27+
new Chart('chart-0', {
28+
type: 'bubble',
29+
data: {
30+
datasets: [{
31+
backgroundColor: utils.color(0),
32+
data: generateData()
33+
}, {
34+
backgroundColor: utils.color(1),
35+
data: generateData()
36+
}]
37+
},
38+
options: {
39+
aspectRatio: 1,
40+
legend: false,
41+
tooltip: false,
42+
elements: {
43+
point: {
44+
radius: function(context) {
45+
var value = context.dataset.data[context.dataIndex];
46+
var size = context.chart.width;
47+
var base = Math.abs(value.v) / 1000;
48+
return (size / 24) * base;
49+
}
50+
}
51+
}
52+
}
53+
});
54+
});

samples/samples.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@
187187
items: [{
188188
title: 'Progress bar',
189189
path: 'advanced/progress-bar.html'
190+
}, {
191+
title: 'Content Security Policy',
192+
path: 'advanced/content-security-policy.html'
190193
}]
191194
}];
192195

samples/style.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
@import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css');
21
@import url('https://fonts.googleapis.com/css?family=Lato:100,300,400,700,900');
32

43
body, html {

scripts/deploy.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ cd $TARGET_DIR
4141
git checkout $TARGET_BRANCH
4242

4343
# Copy dist files
44-
deploy_files '../dist/*.js' './dist'
44+
deploy_files '../dist/*.css ../dist/*.js' './dist'
4545

4646
# Copy generated documentation
4747
deploy_files '../dist/docs/*' './docs'

scripts/release.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ git remote add auth-origin https://[email protected]/$TRAVIS_REPO_SL
2121
git config --global user.email "$GITHUB_AUTH_EMAIL"
2222
git config --global user.name "Chart.js"
2323
git checkout --detach --quiet
24-
git add -f dist/*.js bower.json
24+
git add -f dist/*.css dist/*.js bower.json
2525
git commit -m "Release $VERSION"
2626
git tag -a "v$VERSION" -m "Version $VERSION"
2727
git push -q auth-origin refs/tags/v$VERSION 2>/dev/null

src/platforms/platform.dom.css

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* DOM element rendering detection
3+
* https://davidwalsh.name/detect-node-insertion
4+
*/
5+
@keyframes chartjs-render-animation {
6+
from { opacity: 0.99; }
7+
to { opacity: 1; }
8+
}
9+
10+
.chartjs-render-monitor {
11+
animation: chartjs-render-animation 0.001s;
12+
}
13+
14+
/*
15+
* DOM element resizing detection
16+
* https://github.com/marcj/css-element-queries
17+
*/
18+
.chartjs-size-monitor,
19+
.chartjs-size-monitor-expand,
20+
.chartjs-size-monitor-shrink {
21+
position: absolute;
22+
left: 0;
23+
top: 0;
24+
right: 0;
25+
bottom: 0;
26+
overflow: hidden;
27+
pointer-events: none;
28+
visibility: hidden;
29+
z-index: -1;
30+
}
31+
32+
.chartjs-size-monitor-expand > div {
33+
position: absolute;
34+
width: 1000000px;
35+
height: 1000000px;
36+
left: 0;
37+
top: 0;
38+
}
39+
40+
.chartjs-size-monitor-shrink > div {
41+
position: absolute;
42+
width: 200%;
43+
height: 200%;
44+
left: 0;
45+
top: 0;
46+
}

0 commit comments

Comments
 (0)