Adding CROSSORIGIN handling
This commit is contained in:
parent
3198d9d836
commit
2864cad3d6
8 changed files with 100 additions and 26 deletions
|
@ -7,7 +7,7 @@ workflows:
|
|||
matrix:
|
||||
parameters:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
django-version: ["3.2", "4.2", "5.0", "5.1"]
|
||||
django-version: ["4.2", "5.0"]
|
||||
exclude:
|
||||
- python-version: "3.8"
|
||||
django-version: "5.0"
|
||||
|
|
|
@ -5,10 +5,18 @@ For more general information, view the [readme](README.md).
|
|||
Releases are added to the
|
||||
[github release page](https://github.com/ezhome/django-webpack-loader/releases).
|
||||
|
||||
## --- INSERT VERSION HERE ---
|
||||
|
||||
- Automatically add `crossorigin` attributes to tags with `integrity` attributes when necessary (and enabled)
|
||||
|
||||
## [3.1.1] -- 2024-08-30
|
||||
|
||||
- Add support for Django 5.1
|
||||
|
||||
## [3.2.0] -- 2024-07-28
|
||||
|
||||
- Remove support for Django 3.x (LTS is EOL)
|
||||
|
||||
## [3.1.0] -- 2024-04-04
|
||||
|
||||
Support `webpack_asset` template tag to render transformed assets URL: `{% webpack_asset 'path/to/original/file' %} == "/static/assets/resource-3c9e4020d3e3c7a09c68.txt"`
|
||||
|
|
|
@ -252,7 +252,9 @@ WEBPACK_LOADER = {
|
|||
|
||||
- `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 `<script>` and `<link>` tags. Integrity hash is get from stats file and configuration on side of `BundleTracker`, where [configuration option](https://github.com/django-webpack/webpack-bundle-tracker#options) `integrity: true` is required.
|
||||
- `INTEGRITY` is a flag enabling [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) on rendered `<script>` and `<link>` tags. Integrity hash is fetched from the stats of `BundleTrackerPlugin`. The [configuration option](https://github.com/django-webpack/webpack-bundle-tracker#options) `integrity: true` is required.
|
||||
|
||||
- `CROSSORIGIN`: If you use the `integrity` attribute in your tags and you load your webpack generated assets from another origin (that is not the same `host:port` as the one you load the webpage from), you can configure the `CROSSORIGIN` configuration option. The default value is `''` (empty string), where an empty `crossorigin` attribute will be emitted when necessary. Valid values are: `''` (empty string), `'anonymous'` (functionally same as the empty string) and `use-credentials`. For an explanation, see https://shubhamjain.co/2018/09/08/subresource-integrity-crossorigin/. A typical case for this scenario is when you develop locally and your webpack-dev-server runs with hot-reload on a local host/port other than that of django's `runserver`.
|
||||
|
||||
- `LOADER_CLASS` is the fully qualified name of a python class as a string that holds the custom Webpack loader. This is where behavior can be customized as to how the stats file is loaded. Examples include loading the stats file from a database, cache, external URL, etc. For convenience, `webpack_loader.loaders.WebpackLoader` can be extended. The `load_assets` method is likely where custom behavior will be added. This should return the stats file as an object.
|
||||
|
||||
|
|
|
@ -224,11 +224,18 @@ class LoaderTestCase(TestCase):
|
|||
|
||||
self.assertIn((
|
||||
'<script src="/static/django_webpack_loader_bundles/main.js" '
|
||||
'integrity="sha256-1wgFMxcDlOWYV727qRvWNoPHdnOGFNVMLuKd25cjR+o=" >'
|
||||
'integrity="sha256-1wgFMxcDlOWYV727qRvWNoPHdnOGFNVMLuKd25cjR+'
|
||||
'o= sha384-3RnsU3Z2OODW6qaMAPVpNC5lBb4M5I1+joXv37ACuLvCO6gQ7o'
|
||||
'OD7IC1zN1uAakD sha512-9nLlV4v2pWvgeavHop1wXxdP34CfYv/xUZHwVB'
|
||||
'N+1p+pAvHDmBw4XkvvciSGW4zQlWhaUiIi7P6nXmsLE+8Fsw==" >'
|
||||
'</script>'), result.rendered_content)
|
||||
self.assertIn((
|
||||
'<link href="/static/django_webpack_loader_bundles/main.css" rel="stylesheet" '
|
||||
'integrity="sha256-cYWwRvS04/VsttQYx4BalKYrBDuw5t8vKFhWB/LKX30=" />'),
|
||||
'<link href="/static/django_webpack_loader_bundles/main.css" '
|
||||
'rel="stylesheet" integrity="sha256-cYWwRvS04/VsttQYx4BalKYrB'
|
||||
'Duw5t8vKFhWB/LKX30= sha384-V/UxbrsEy8BK5nd+sBlN31Emmq/WdDDdI'
|
||||
'01UR8wKIFkIr6vEaT5YRaeLMfLcAQvS sha512-aigPxglXDA33t9s5i0vRa'
|
||||
'p5b7dFwyp7cSN6x8rOXrPpCTMubOR7qTFpmTIa8z9B0wtXxbSheBPNCEURBH'
|
||||
'KLQPw==" />'),
|
||||
result.rendered_content
|
||||
)
|
||||
|
||||
|
|
|
@ -16,6 +16,10 @@ DEFAULT_CONFIG = {
|
|||
'IGNORE': [r'.+\.hot-update.js', r'.+\.map'],
|
||||
'LOADER_CLASS': 'webpack_loader.loaders.WebpackLoader',
|
||||
'INTEGRITY': False,
|
||||
# See https://shubhamjain.co/2018/09/08/subresource-integrity-crossorigin/
|
||||
# See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin
|
||||
# type is Literal['anonymous', 'use-credentials', '']
|
||||
'CROSSORIGIN': '',
|
||||
# Whenever the global setting for SKIP_COMMON_CHUNKS is changed, please
|
||||
# update the fallback value in get_skip_common_chunks (utils.py).
|
||||
'SKIP_COMMON_CHUNKS': False,
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import json
|
||||
import time
|
||||
import os
|
||||
import time
|
||||
from functools import lru_cache
|
||||
from io import open
|
||||
from typing import Dict, Optional
|
||||
from urllib.parse import urlparse
|
||||
from warnings import warn
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.http import HttpRequest
|
||||
|
||||
from .exceptions import (
|
||||
WebpackError,
|
||||
|
@ -13,6 +18,21 @@ from .exceptions import (
|
|||
WebpackBundleLookupError,
|
||||
)
|
||||
|
||||
_CROSSORIGIN_NO_REQUEST = (
|
||||
'The crossorigin attribute might be necessary but you did not pass a '
|
||||
'request object. django_webpack_loader needs a request object to be able '
|
||||
'to know when to emit the crossorigin attribute on link and script tags.')
|
||||
_CROSSORIGIN_NO_HOST = (
|
||||
'You have passed the request object but it does not have a "HTTP_HOST", '
|
||||
'thus django_webpack_loader can\'t know if the crossorigin header will '
|
||||
'be necessary or not.')
|
||||
|
||||
|
||||
@lru_cache(maxsize=100)
|
||||
def _get_netloc(url: str) -> str:
|
||||
'Return a cached netloc (host:port) for the passed `url`.'
|
||||
return urlparse(url=url).netloc
|
||||
|
||||
|
||||
class WebpackLoader:
|
||||
_assets = {}
|
||||
|
@ -42,19 +62,46 @@ class WebpackLoader:
|
|||
files = self.get_assets()["assets"].values()
|
||||
return next((x for x in files if x.get("sourceFilename") == name), None)
|
||||
|
||||
def get_integrity_attr(self, chunk):
|
||||
if not self.config.get("INTEGRITY"):
|
||||
return " "
|
||||
def _add_crossorigin(
|
||||
self, request: Optional[HttpRequest], chunk_url: str,
|
||||
integrity: str, attrs: str) -> str:
|
||||
'Return an added `crossorigin` attribute if necessary.'
|
||||
def_value = f' integrity="{integrity}" '
|
||||
cfgval: str = self.config.get('CROSSORIGIN')
|
||||
if not request:
|
||||
warn(message=_CROSSORIGIN_NO_REQUEST, category=RuntimeWarning)
|
||||
return def_value
|
||||
if 'crossorigin' in attrs.lower():
|
||||
return def_value
|
||||
host: Optional[str] = request.META.get('HTTP_HOST')
|
||||
if not host:
|
||||
warn(message=_CROSSORIGIN_NO_HOST, category=RuntimeWarning)
|
||||
return def_value
|
||||
netloc = _get_netloc(url=chunk_url)
|
||||
if netloc == '' or netloc == host:
|
||||
# Crossorigin not necessary
|
||||
return def_value
|
||||
if cfgval == '':
|
||||
return f'{def_value}crossorigin '
|
||||
return f'{def_value}crossorigin="{cfgval}" '
|
||||
|
||||
integrity = chunk.get("integrity")
|
||||
def get_integrity_attr(
|
||||
self, chunk: Dict[str, str], request: Optional[HttpRequest],
|
||||
attrs: str):
|
||||
if not self.config.get('INTEGRITY'):
|
||||
# Crossorigin only necessary when integrity is used
|
||||
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])
|
||||
'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 '
|
||||
'BundleTrackerPlugin configuration?')
|
||||
return self._add_crossorigin(
|
||||
request=request, chunk_url=chunk['url'], integrity=integrity,
|
||||
attrs=attrs)
|
||||
|
||||
def filter_chunks(self, chunks):
|
||||
filtered_chunks = []
|
||||
|
|
|
@ -20,11 +20,11 @@ def render_bundle(
|
|||
if skip_common_chunks is None:
|
||||
skip_common_chunks = utils.get_skip_common_chunks(config)
|
||||
|
||||
url_to_tag_dict = utils.get_as_url_to_tag_dict(
|
||||
bundle_name, extension=extension, config=config, suffix=suffix,
|
||||
attrs=attrs, is_preload=is_preload)
|
||||
|
||||
request = context.get('request')
|
||||
url_to_tag_dict = utils.get_as_url_to_tag_dict(
|
||||
bundle_name, request=request, extension=extension, config=config,
|
||||
suffix=suffix, attrs=attrs, is_preload=is_preload)
|
||||
|
||||
if request is None:
|
||||
if skip_common_chunks:
|
||||
warn(message=_WARNING_MESSAGE, category=RuntimeWarning)
|
||||
|
@ -35,7 +35,7 @@ def render_bundle(
|
|||
used_urls = request._webpack_loader_used_urls = set()
|
||||
if skip_common_chunks:
|
||||
url_to_tag_dict = {url: tag for url, tag in url_to_tag_dict.items() if url not in used_urls}
|
||||
used_urls.update(url_to_tag_dict.keys())
|
||||
used_urls.update(url_to_tag_dict)
|
||||
return mark_safe('\n'.join(url_to_tag_dict.values()))
|
||||
|
||||
|
||||
|
@ -43,10 +43,12 @@ def render_bundle(
|
|||
def webpack_static(asset_name, config='DEFAULT'):
|
||||
return utils.get_static(asset_name, config=config)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def webpack_asset(asset_name, config='DEFAULT'):
|
||||
return utils.get_asset(asset_name, config=config)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def get_files(
|
||||
context, bundle_name, extension=None, config='DEFAULT',
|
||||
|
|
|
@ -57,7 +57,9 @@ def get_files(bundle_name, extension=None, config='DEFAULT'):
|
|||
return list(_get_bundle(loader, bundle_name, extension))
|
||||
|
||||
|
||||
def get_as_url_to_tag_dict(bundle_name, extension=None, config='DEFAULT', suffix='', attrs='', is_preload=False):
|
||||
def get_as_url_to_tag_dict(
|
||||
bundle_name, request=None, extension=None, config='DEFAULT', suffix='',
|
||||
attrs='', is_preload=False):
|
||||
'''
|
||||
Get a dict of URLs to formatted <script> & <link> tags for the assets in the
|
||||
named bundle.
|
||||
|
@ -84,7 +86,7 @@ def get_as_url_to_tag_dict(bundle_name, extension=None, config='DEFAULT', suffix
|
|||
).format(
|
||||
''.join([chunk['url'], suffix]),
|
||||
attrs,
|
||||
loader.get_integrity_attr(chunk),
|
||||
loader.get_integrity_attr(chunk, request, attrs),
|
||||
)
|
||||
elif chunk['name'].endswith(('.css', '.css.gz')):
|
||||
result[chunk['url']] = (
|
||||
|
@ -93,12 +95,14 @@ def get_as_url_to_tag_dict(bundle_name, extension=None, config='DEFAULT', suffix
|
|||
''.join([chunk['url'], suffix]),
|
||||
attrs,
|
||||
'"stylesheet"' if not is_preload else '"preload" as="style"',
|
||||
loader.get_integrity_attr(chunk),
|
||||
loader.get_integrity_attr(chunk, request, attrs),
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def get_as_tags(bundle_name, extension=None, config='DEFAULT', suffix='', attrs='', is_preload=False):
|
||||
def get_as_tags(
|
||||
bundle_name, request=None, extension=None, config='DEFAULT', suffix='',
|
||||
attrs='', is_preload=False):
|
||||
'''
|
||||
Get a list of formatted <script> & <link> tags for the assets in the
|
||||
named bundle.
|
||||
|
@ -108,7 +112,7 @@ def get_as_tags(bundle_name, extension=None, config='DEFAULT', suffix='', attrs=
|
|||
:param config: (optional) the name of the configuration
|
||||
:return: a list of formatted tags as strings
|
||||
'''
|
||||
return list(get_as_url_to_tag_dict(bundle_name, extension, config, suffix, attrs, is_preload).values())
|
||||
return list(get_as_url_to_tag_dict(bundle_name, request, extension, config, suffix, attrs, is_preload).values())
|
||||
|
||||
|
||||
def get_static(asset_name, config='DEFAULT'):
|
||||
|
|
Loading…
Reference in a new issue