diff --git a/README.md b/README.md index 45b62f9d..09115855 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ The `STATS_FILE` parameter represents the output file produced by `webpack-bundl - `TIMEOUT` is the number of seconds webpack_loader should wait for webpack to finish compiling before raising an exception. `0`, `None` or leaving the value out of settings disables timeouts +- `INTEGRITY` is flag enabling [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) on rendered `'), result.rendered_content) + def test_integrity(self): + self.compile_bundles('webpack.config.integrity.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {'INTEGRITY': True}): + view = TemplateView.as_view(template_name='single.html') + request = self.factory.get('/') + result = view(request) + + self.assertIn(( + ''), result.rendered_content) + self.assertIn(( + ''), + result.rendered_content + ) + + def test_integrity_missing_config(self): + self.compile_bundles('webpack.config.integrity.js') + + loader = get_loader(DEFAULT_CONFIG) + # remove INTEGRITY from config completely to test backward compatibility + integrity_from_config = loader.config.pop('INTEGRITY') + + view = TemplateView.as_view(template_name='single.html') + request = self.factory.get('/') + result = view(request) + + self.assertIn(( + ''), result.rendered_content + ) + self.assertIn(( + ''), + result.rendered_content + ) + + # return removed key + loader.config['INTEGRITY'] = integrity_from_config + + def test_integrity_missing_hash(self): + self.compile_bundles('webpack.config.simple.js') + + loader = get_loader(DEFAULT_CONFIG) + with patch.dict(loader.config, {'INTEGRITY': True}), self.assertRaises(WebpackLoaderBadStatsError): + view = TemplateView.as_view(template_name='single.html') + request = self.factory.get('/') + str(view(request).rendered_content) + def test_append_extensions(self): self.compile_bundles('webpack.config.gzipTest.js') view = TemplateView.as_view(template_name='append_extensions.html') diff --git a/tests/webpack.config.integrity.js b/tests/webpack.config.integrity.js new file mode 100644 index 00000000..d82de67b --- /dev/null +++ b/tests/webpack.config.integrity.js @@ -0,0 +1,41 @@ +var path = require("path"); +var webpack = require('webpack'); +var BundleTracker = require('webpack-bundle-tracker'); +var MiniCssExtractPlugin = require('mini-css-extract-plugin'); + + +module.exports = { + context: __dirname, + entry: './assets/js/index', + output: { + path: path.resolve('./assets/django_webpack_loader_bundles/'), + filename: "[name].js" + }, + + plugins: [ + new MiniCssExtractPlugin(), + new BundleTracker({path: __dirname, filename: './webpack-stats.json', integrity: true}), + ], + + module: { + rules: [ + // we pass the output from babel loader to react-hot loader + { + test: /\.jsx?$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-react'] + } + } + }, + { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], } + ], + }, + + resolve: { + modules: ['node_modules'], + extensions: ['.js', '.jsx'] + }, +} diff --git a/webpack_loader/config.py b/webpack_loader/config.py index c978d77f..8761b2f5 100644 --- a/webpack_loader/config.py +++ b/webpack_loader/config.py @@ -15,6 +15,7 @@ 'TIMEOUT': None, 'IGNORE': [r'.+\.hot-update.js', r'.+\.map'], 'LOADER_CLASS': 'webpack_loader.loader.WebpackLoader', + 'INTEGRITY': False, } } diff --git a/webpack_loader/loader.py b/webpack_loader/loader.py index c15d3122..6d46384b 100644 --- a/webpack_loader/loader.py +++ b/webpack_loader/loader.py @@ -38,6 +38,19 @@ def get_assets(self): return self._assets[self.name] return self.load_assets() + def get_integrity_attr(self, chunk): + if not self.config.get('INTEGRITY'): + return ' ' + + integrity = chunk.get('integrity') + if not integrity: + raise WebpackLoaderBadStatsError( + "The stats file does not contain valid data: INTEGRITY is set to True, " + "but chunk does not contain \"integrity\" key. Maybe you forgot to add " + "integrity: true in your BundleTracker configuration?") + + return ' integrity="{}" '.format(integrity.partition(' ')[0]) + def filter_chunks(self, chunks): filtered_chunks = [] @@ -53,9 +66,15 @@ def map_chunk_files_to_url(self, chunks): assets = self.get_assets() files = assets['assets'] + add_integrity = self.config.get('INTEGRITY') + for chunk in chunks: url = self.get_chunk_url(files[chunk]) - yield { 'name': chunk, 'url': url } + + if add_integrity: + yield {'name': chunk, 'url': url, 'integrity': files[chunk].get('integrity')} + else: + yield {'name': chunk, 'url': url} def get_chunk_url(self, chunk_file): public_path = chunk_file.get('publicPath') diff --git a/webpack_loader/utils.py b/webpack_loader/utils.py index 008fef44..b432dc49 100644 --- a/webpack_loader/utils.py +++ b/webpack_loader/utils.py @@ -35,8 +35,8 @@ def _filter_by_extension(bundle, extension): yield chunk -def _get_bundle(bundle_name, extension, config): - bundle = get_loader(config).get_bundle(bundle_name) +def _get_bundle(loader, bundle_name, extension): + bundle = loader.get_bundle(bundle_name) if extension: bundle = _filter_by_extension(bundle, extension) return bundle @@ -44,7 +44,8 @@ def _get_bundle(bundle_name, extension, config): def get_files(bundle_name, extension=None, config='DEFAULT'): '''Returns list of chunks from named bundle''' - return list(_get_bundle(bundle_name, extension, config)) + loader = get_loader(config) + return list(_get_bundle(loader, bundle_name, extension)) def get_as_tags(bundle_name, extension=None, config='DEFAULT', suffix='', attrs='', is_preload=False): @@ -58,8 +59,10 @@ def get_as_tags(bundle_name, extension=None, config='DEFAULT', suffix='', attrs= :return: a list of formatted tags as strings ''' - bundle = _get_bundle(bundle_name, extension, config) + loader = get_loader(config) + bundle = _get_bundle(loader, bundle_name, extension) tags = [] + for chunk in bundle: if chunk['name'].endswith(('.js', '.js.gz')): if is_preload: @@ -68,12 +71,21 @@ def get_as_tags(bundle_name, extension=None, config='DEFAULT', suffix='', attrs= ).format(''.join([chunk['url'], suffix]), attrs)) else: tags.append(( - '' - ).format(''.join([chunk['url'], suffix]), attrs)) + '' + ).format( + ''.join([chunk['url'], suffix]), + attrs, + loader.get_integrity_attr(chunk), + )) elif chunk['name'].endswith(('.css', '.css.gz')): tags.append(( - '' - ).format(''.join([chunk['url'], suffix]), attrs, '"stylesheet"' if not is_preload else '"preload" as="style"')) + '' + ).format( + ''.join([chunk['url'], suffix]), + attrs, + '"stylesheet"' if not is_preload else '"preload" as="style"', + loader.get_integrity_attr(chunk), + )) return tags