Skip to content
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

Add support for attrs and update output format #69

Merged
merged 4 commits into from
Dec 8, 2022

Conversation

hugorodgerbrown
Copy link
Contributor

This PR is for reference only - as I don't anticipate it being merged. We have forked the repo and will pin to our version as we want to have a drop-in replacement for static that covers all of our use cases.

The event that triggered this change was a critical failure on our CI environment where we do not have all of our static content (as it requires a frontend build step that is not part of the backend test suite). This caused the test suite to fail because we were using the sri_integrity_static tag in templates, which is still called when USE_SRI is False (which it is in testing).

Updating sri_integrity_static to not run when USE_SRI is False doesn't make sense, as if you are calling it explicitly then presumably you do want it to run. 🤔

The root cause of this is that we were building output elements that django-sri doesn't support (defer, async etc.) In addition, the W3C HTML validator didn't like the output:

Screenshot 2022-11-23 at 11 07 44

I started by making a minimal change to the output, but tbh I was bending the existing code too far, and it was becoming overly complex to support the existing use cases along with what we wanted to do.

In this version, we can support 'simple' tag attributes (defer, async etc) as well as 'complex' attributes (type="text/javascript"):

{% load sri %}
<!DOCTYPE html>
<html lang="en"></html>
    <head>
        <title>Test page</title>
        {% sri_static "index.js" 'defer' 'async'%}
        <!-- <script crossorigin="anonymous" integrity="sha256-..." src="/static/index.js" defer async></script> -->
        {% sri_static "index.css"%}
        <!-- <link crossorigin="anonymous" href="/static/index.css" integrity="sha256-..." rel="stylesheet" type="text/css"> -->
    </head>
</html>

def sri_static(path: str, algorithm: Optional[str] = None):
algorithm_type = Algorithm(algorithm or DEFAULT_ALGORITHM)
def sri_static(
path: str, *simple_attrs: str, algorithm: str | None = None, **complex_attrs: str
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: It would be nice to require the algorithm be first before the simple_attrs so the order is more deterministic. Alternatively, forcing algorithm to be a kwarg could also be nice and explicit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting one - when I was looking at it I couldn't think of a scenario where you would use different algorithms within the same project (i.e. SHA-256 for some files, SHA-384 for others). Is this a real use case?

There are three possible forms:

# least favourite as you have to be explicit on *every* use
def sri_static(path: str, algorithm: str, *simple_attrs, **complex_attrs) -> str:
    """Algorithm as required argument."""


# second least favourite - it's neat, but it does mix function kwargs with tag attrs.
def sri_static(path: str, *simple_attrs, **complex_attrs) -> str:
    """Algorithm read out from kwargs."""
    algorithm = complex_attrs.pop("algorithm", DEFAULT_ALGORITHM.value)


# personal favourite - it's explicit, it doesn't mix with tag attrs, it's optional
def sri_static(path: str, *simple_attrs, algorithm: str = DEFAULT_ALOGITHM.value, **complex_attrs) -> str:
    """Algorithm as named kwarg."""

Copy link
Owner

@RealOrangeOne RealOrangeOne Nov 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking was more:

def sri_static(path: str, algorithm: str = DEFAULT_ALOGITHM.value, *simple_attrs, **complex_attrs) -> str:

Calling becomes the same, but I think it prevents {% sri_static "index.js" "defer" algorithm="sha512" %} which looks messy

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you specify algorithm before *args it will always interpret the first unnamed argument as the algorithm, so you have to include it if you have any other unnamed arguments:

>>> def sri_static(path, algo = "sha256", *args, **kwargs):
...     print(f"path:  {path}\nalgo:  {algo}\nargs:  {args}\nkwargs: {kwargs}")
...
>>> sri_static("index.js")
path:  index.js
algo:  sha256
args:  ()
kwargs: {}
>>> sri_static("index.js", "defer")
path:  index.js
algo:  defer
args:  ()
kwargs: {}
>>> sri_static("index.js", "defer", algo="sha384")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sri_static() got multiple values for argument 'algo'
>>>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it comes down to use cases.

Selfishly, we have hundreds of static includes and none of them would require the algorithm to be explicit as we only use the same DEFAULT_ALGORITHM for everything, but we do have a whole smorgasbord of attrs (empty and not empty).

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps something like:

def sri_static(path: str, *simple_attrs, *, algorithm: str = DEFAULT_ALOGITHM.value, **complex_attrs) -> str:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RealOrangeOne this is what it looks like after the last update?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, almost. The main change is the extra *, forcing algorithm to be specified as a keyword argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes - good spot - apologies - updating now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out you can't do that:

Keyword-only argument separator not allowed after "*" parameter

Copy link
Owner

@RealOrangeOne RealOrangeOne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would definitely be happy with merging this, I think it looks great and is a nice generic solution to the problem.

The HTML validation changes are slightly separate to the core problem, as it depends what content you're validating against (HTML vs XHTML for example), but I don't feel very strongly one way or the other, so I think this is fine.

Nit comment, but otherwise looks good.

@hugorodgerbrown
Copy link
Contributor Author

@RealOrangeOne I've updated the names of the args/kwargs to more closely represent what they are (w3c refers to "empty" attributes), removed the extraneous function, and fixed a couple of linting issues.

Fix black issue

Fix up import sorting issues

Update sri.py

Update `sri_static` to ensure that "algorithm" is a kwarg.

Revert "Update sri.py"

This reverts commit 1b036e3.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants