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`.
});
});
```
+
+## Content Security Policy
+
+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:
+
+```html
+<link rel="stylesheet" type="text/css" href="path/to/chartjs/dist/Chart.min.css">
+```
+
+And the style injection must be turned off **before creating the first chart**:
+
+```javascript
+// Disable automatic style injection
+Chart.platform.disableCSSInjection = true;
+```
function packageTask() {
return merge(
// gather "regular" files landing in the package root
- gulp.src([outDir + '*.js', 'LICENSE.md']),
+ gulp.src([outDir + '*.js', outDir + '*.css', 'LICENSE.md']),
// since we moved the dist files one folder up (package root), we need to rewrite
// samples src="../dist/ to src="../ and then copy them in the /samples directory.
"url": "https://github.com/chartjs/Chart.js/issues"
},
"devDependencies": {
+ "clean-css": "^4.2.1",
"coveralls": "^3.0.0",
"eslint": "^5.9.0",
"eslint-config-chartjs": "^0.1.0",
const resolve = require('rollup-plugin-node-resolve');
const terser = require('rollup-plugin-terser').terser;
const optional = require('./rollup.plugins').optional;
+const stylesheet = require('./rollup.plugins').stylesheet;
const pkg = require('./package.json');
const input = 'src/chart.js';
plugins: [
resolve(),
commonjs(),
+ stylesheet({
+ extract: true
+ }),
optional({
include: ['moment']
})
optional({
include: ['moment']
}),
+ stylesheet({
+ extract: true,
+ minify: true
+ }),
terser({
output: {
preamble: banner
input: input,
plugins: [
resolve(),
- commonjs()
+ commonjs(),
+ stylesheet()
],
output: {
name: 'Chart',
plugins: [
resolve(),
commonjs(),
+ stylesheet({
+ minify: true
+ }),
terser({
output: {
preamble: banner
/* eslint-env es6 */
+const cleancss = require('clean-css');
+const path = require('path');
const UMD_WRAPPER_RE = /(\(function \(global, factory\) \{)((?:\s.*?)*)(\}\(this,)/;
const CJS_FACTORY_RE = /(module.exports = )(factory\(.*?\))( :)/;
};
}
+// https://github.com/chartjs/Chart.js/issues/5208
+function stylesheet(config = {}) {
+ const minifier = new cleancss();
+ const styles = [];
+
+ return {
+ name: 'stylesheet',
+ transform(code, id) {
+ // Note that 'id' can be mapped to a CJS proxy import, in which case
+ // 'id' will start with 'commonjs-proxy', so let's first check if we
+ // are importing an existing css file (i.e. startsWith()).
+ if (!id.startsWith(path.resolve('.')) || !id.endsWith('.css')) {
+ return;
+ }
+
+ if (config.minify) {
+ code = minifier.minify(code).styles;
+ }
+
+ // keep track of all imported stylesheets (already minified)
+ styles.push(code);
+
+ return {
+ code: 'export default ' + JSON.stringify(code)
+ };
+ },
+ generateBundle(opts, bundle) {
+ if (!config.extract) {
+ return;
+ }
+
+ const entry = Object.keys(bundle).find(v => bundle[v].isEntry);
+ const name = (entry || '').replace(/\.js$/i, '.css');
+ if (!name) {
+ this.error('failed to guess the output file name');
+ }
+
+ bundle[name] = {
+ code: styles.filter(v => !!v).join('')
+ };
+ }
+ };
+}
+
module.exports = {
- optional
+ optional,
+ stylesheet
};
--- /dev/null
+.content {
+ max-width: 640px;
+ margin: auto;
+ padding: 1rem;
+}
+
+.note {
+ font-family: sans-serif;
+ color: #5050a0;
+ line-height: 1.4;
+ margin-bottom: 1rem;
+ padding: 1rem;
+}
+
+code {
+ background-color: #f5f5ff;
+ border: 1px solid #d0d0fa;
+ border-radius: 4px;
+ padding: 0.05rem 0.25rem;
+}
--- /dev/null
+<!DOCTYPE html>
+<html lang="en-US">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'">
+ <title>Scriptable > Bubble | Chart.js sample</title>
+ <link rel="stylesheet" type="text/css" href="../../dist/Chart.min.css">
+ <link rel="stylesheet" type="text/css" href="./content-security-policy.css">
+ <script src="../../dist/Chart.min.js"></script>
+ <script src="../utils.js"></script>
+ <script src="content-security-policy.js"></script>
+</head>
+<body>
+ <div class="content">
+ <div class="note">
+ In order to support a strict content security policy (<code>default-src 'self'</code>),
+ this page manually loads <code>Chart.min.css</code> and turns off the automatic style
+ injection by setting <code>Chart.platform.disableCSSInjection = true;</code>.
+ </div>
+ <div class="wrapper">
+ <canvas id="chart-0"></canvas>
+ </div>
+ </div>
+</body>
+</html>
--- /dev/null
+var utils = Samples.utils;
+
+// CSP: disable automatic style injection
+Chart.platform.disableCSSInjection = true;
+
+utils.srand(110);
+
+function generateData() {
+ var DATA_COUNT = 16;
+ var MIN_XY = -150;
+ var MAX_XY = 100;
+ var data = [];
+ var i;
+
+ for (i = 0; i < DATA_COUNT; ++i) {
+ data.push({
+ x: utils.rand(MIN_XY, MAX_XY),
+ y: utils.rand(MIN_XY, MAX_XY),
+ v: utils.rand(0, 1000)
+ });
+ }
+
+ return data;
+}
+
+window.addEventListener('load', function() {
+ new Chart('chart-0', {
+ type: 'bubble',
+ data: {
+ datasets: [{
+ backgroundColor: utils.color(0),
+ data: generateData()
+ }, {
+ backgroundColor: utils.color(1),
+ data: generateData()
+ }]
+ },
+ options: {
+ aspectRatio: 1,
+ legend: false,
+ tooltip: false,
+ elements: {
+ point: {
+ radius: function(context) {
+ var value = context.dataset.data[context.dataIndex];
+ var size = context.chart.width;
+ var base = Math.abs(value.v) / 1000;
+ return (size / 24) * base;
+ }
+ }
+ }
+ }
+ });
+});
items: [{
title: 'Progress bar',
path: 'advanced/progress-bar.html'
+ }, {
+ title: 'Content Security Policy',
+ path: 'advanced/content-security-policy.html'
}]
}];
-@import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css');
@import url('https://fonts.googleapis.com/css?family=Lato:100,300,400,700,900');
body, html {
git checkout $TARGET_BRANCH
# Copy dist files
-deploy_files '../dist/*.js' './dist'
+deploy_files '../dist/*.css ../dist/*.js' './dist'
# Copy generated documentation
deploy_files '../dist/docs/*' './docs'
git config --global user.email "$GITHUB_AUTH_EMAIL"
git config --global user.name "Chart.js"
git checkout --detach --quiet
-git add -f dist/*.js bower.json
+git add -f dist/*.css dist/*.js bower.json
git commit -m "Release $VERSION"
git tag -a "v$VERSION" -m "Version $VERSION"
git push -q auth-origin refs/tags/v$VERSION 2>/dev/null
--- /dev/null
+/*
+ * DOM element rendering detection
+ * https://davidwalsh.name/detect-node-insertion
+ */
+@keyframes chartjs-render-animation {
+ from { opacity: 0.99; }
+ to { opacity: 1; }
+}
+
+.chartjs-render-monitor {
+ animation: chartjs-render-animation 0.001s;
+}
+
+/*
+ * DOM element resizing detection
+ * https://github.com/marcj/css-element-queries
+ */
+.chartjs-size-monitor,
+.chartjs-size-monitor-expand,
+.chartjs-size-monitor-shrink {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+ pointer-events: none;
+ visibility: hidden;
+ z-index: -1;
+}
+
+.chartjs-size-monitor-expand > div {
+ position: absolute;
+ width: 1000000px;
+ height: 1000000px;
+ left: 0;
+ top: 0;
+}
+
+.chartjs-size-monitor-shrink > div {
+ position: absolute;
+ width: 200%;
+ height: 200%;
+ left: 0;
+ top: 0;
+}
'use strict';
var helpers = require('../helpers/index');
+var stylesheet = require('./platform.dom.css');
var EXPANDO_KEY = '$chartjs';
var CSS_PREFIX = 'chartjs-';
+var CSS_SIZE_MONITOR = CSS_PREFIX + 'size-monitor';
var CSS_RENDER_MONITOR = CSS_PREFIX + 'render-monitor';
var CSS_RENDER_ANIMATION = CSS_PREFIX + 'render-animation';
var ANIMATION_START_EVENTS = ['animationstart', 'webkitAnimationStart'];
};
}
-function createDiv(cls, style) {
+function createDiv(cls) {
var el = document.createElement('div');
- el.style.cssText = style || '';
el.className = cls || '';
return el;
}
// Implementation based on https://github.com/marcj/css-element-queries
function createResizer(handler) {
- var cls = CSS_PREFIX + 'size-monitor';
var maxSize = 1000000;
- var style =
- 'position:absolute;' +
- 'left:0;' +
- 'top:0;' +
- 'right:0;' +
- 'bottom:0;' +
- 'overflow:hidden;' +
- 'pointer-events:none;' +
- 'visibility:hidden;' +
- 'z-index:-1;';
// NOTE(SB) Don't use innerHTML because it could be considered unsafe.
// https://github.com/chartjs/Chart.js/issues/5902
- var resizer = createDiv(cls, style);
- var expand = createDiv(cls + '-expand', style);
- var shrink = createDiv(cls + '-shrink', style);
-
- expand.appendChild(createDiv('',
- 'position:absolute;' +
- 'height:' + maxSize + 'px;' +
- 'width:' + maxSize + 'px;' +
- 'left:0;' +
- 'top:0;'
- ));
- shrink.appendChild(createDiv('',
- 'position:absolute;' +
- 'height:200%;' +
- 'width:200%;' +
- 'left:0;' +
- 'top:0;'
- ));
+ var resizer = createDiv(CSS_SIZE_MONITOR);
+ var expand = createDiv(CSS_SIZE_MONITOR + '-expand');
+ var shrink = createDiv(CSS_SIZE_MONITOR + '-shrink');
+
+ expand.appendChild(createDiv());
+ shrink.appendChild(createDiv());
resizer.appendChild(expand);
resizer.appendChild(shrink);
}
module.exports = {
+ /**
+ * When `true`, prevents the automatic injection of the stylesheet required to
+ * correctly detect when the chart is added to the DOM and then resized. This
+ * switch has been added to allow external stylesheet (`dist/Chart(.min)?.js`)
+ * to be manually imported to make this library compatible with any CSP.
+ * See https://github.com/chartjs/Chart.js/issues/5208
+ */
+ disableCSSInjection: false,
+
/**
* This property holds whether this platform is enabled for the current environment.
* Currently used by platform.js to select the proper implementation.
*/
_enabled: typeof window !== 'undefined' && typeof document !== 'undefined',
- initialize: function() {
- var keyframes = 'from{opacity:0.99}to{opacity:1}';
-
- injectCSS(this,
- // DOM rendering detection
- // https://davidwalsh.name/detect-node-insertion
- '@-webkit-keyframes ' + CSS_RENDER_ANIMATION + '{' + keyframes + '}' +
- '@keyframes ' + CSS_RENDER_ANIMATION + '{' + keyframes + '}' +
- '.' + CSS_RENDER_MONITOR + '{' +
- '-webkit-animation:' + CSS_RENDER_ANIMATION + ' 0.001s;' +
- 'animation:' + CSS_RENDER_ANIMATION + ' 0.001s;' +
- '}'
- );
+ /**
+ * @private
+ */
+ _ensureLoaded: function() {
+ if (this._loaded) {
+ return;
+ }
+
+ this._loaded = true;
+
+ // https://github.com/chartjs/Chart.js/issues/5208
+ if (!this.disableCSSInjection) {
+ injectCSS(this, stylesheet);
+ }
},
acquireContext: function(item, config) {
// https://github.com/chartjs/Chart.js/issues/2807
var context = item && item.getContext && item.getContext('2d');
+ // Load platform resources on first chart creation, to make possible to change
+ // platform options after importing the library (e.g. `disableCSSInjection`).
+ this._ensureLoaded();
+
// `instanceof HTMLCanvasElement/CanvasRenderingContext2D` fails when the item is
// inside an iframe or when running in a protected environment. We could guess the
// types from their toString() value but let's keep things flexible and assume it's