Skip to content

Commit

Permalink
[ADD] SRI hashes to static files
Browse files Browse the repository at this point in the history
  • Loading branch information
ppfeufer committed Jan 21, 2025
1 parent 4b5280a commit 17f644f
Show file tree
Hide file tree
Showing 17 changed files with 218 additions and 60 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ __pycache__
.coverage
/coverage.xml
alliance_auth.sqlite3

/aa_bulletin_board/locale/en/
71 changes: 40 additions & 31 deletions .make/conf.d/django.mk
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
pot:
@echo "Creating or updating .pot file …"
@django-admin makemessages \
-l en \
--locale en \
--keep-pot \
--ignore 'build/*'
--ignore 'build/*' \
--ignore 'node_modules/*' \
--ignore 'testauth/*' \
--ignore 'runtests.py'
@current_app_version=$$(pip show $(appname) | grep 'Version: ' | awk '{print $$NF}'); \
sed -i "/\"Project-Id-Version: /c\\\"Project-Id-Version: $(appname_verbose) $$current_app_version\\\n\"" $(translation_template); \
sed -i "/\"Report-Msgid-Bugs-To: /c\\\"Report-Msgid-Bugs-To: $(git_repository_issues)\\\n\"" $(translation_template);
Expand All @@ -18,9 +21,12 @@ add_translation:
@echo "Adding a new translation"
@read -p "Enter the language code (e.g. 'en_GB'): " language_code; \
django-admin makemessages \
-l $$language_code \
--locale $$language_code \
--keep-pot \
--ignore 'build/*'; \
--ignore 'build/*' \
--ignore 'node_modules/*' \
--ignore 'testauth/*' \
--ignore 'runtests.py'; \
current_app_version=$$(pip show $(appname) | grep 'Version: ' | awk '{print $$NF}'); \
sed -i "/\"Project-Id-Version: /c\\\"Project-Id-Version: $(appname_verbose) $$current_app_version\\\n\"" $(translation_template); \
sed -i "/\"Report-Msgid-Bugs-To: /c\\\"Report-Msgid-Bugs-To: $(git_repository_issues)\\\n\"" $(translation_template); \
Expand All @@ -34,21 +40,24 @@ add_translation:
translations:
@echo "Creating or updating translation files"
@django-admin makemessages \
-l cs_CZ \
-l de \
-l es \
-l fr_FR \
-l it_IT \
-l ja \
-l ko_KR \
-l nl_NL \
-l pl_PL \
-l ru \
-l sk \
-l uk \
-l zh_Hans \
--locale cs_CZ \
--locale de \
--locale es \
--locale fr_FR \
--locale it_IT \
--locale ja \
--locale ko_KR \
--locale nl_NL \
--locale pl_PL \
--locale ru \
--locale sk \
--locale uk \
--locale zh_Hans \
--keep-pot \
--ignore 'build/*'
--ignore 'build/*' \
--ignore 'node_modules/*' \
--ignore 'testauth/*' \
--ignore 'runtests.py'
@current_app_version=$$(pip show $(appname) | grep 'Version: ' | awk '{print $$NF}'); \
sed -i "/\"Project-Id-Version: /c\\\"Project-Id-Version: $(appname_verbose) $$current_app_version\\\n\"" $(translation_template); \
sed -i "/\"Report-Msgid-Bugs-To: /c\\\"Report-Msgid-Bugs-To: $(git_repository_issues)\\\n\"" $(translation_template); \
Expand All @@ -69,19 +78,19 @@ translations:
compile_translations:
@echo "Compiling translation files"
@django-admin compilemessages \
-l cs_CZ \
-l de \
-l es \
-l fr_FR \
-l it_IT \
-l ja \
-l ko_KR \
-l nl_NL \
-l pl_PL \
-l ru \
-l sk \
-l uk \
-l zh_Hans
--locale cs_CZ \
--locale de \
--locale es \
--locale fr_FR \
--locale it_IT \
--locale ja \
--locale ko_KR \
--locale nl_NL \
--locale pl_PL \
--locale ru \
--locale sk \
--locale uk \
--locale zh_Hans

# Migrate all database changes
.PHONY: migrate
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ Section Order:
### Security
-->

### Added

- SRI hashes to static files

### Changed

- Minimum requirements
- Alliance Auth >= 4.6.0

## [2.2.5] - 2024-12-14

### Changed
Expand Down
11 changes: 11 additions & 0 deletions aa_bulletin_board/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
Constants used in this app
"""

# Standard Library
import os

AA_BULLETIN_BOARD_BASE_DIR = os.path.join(os.path.dirname(__file__))
AA_BULLETIN_BOARD_STATIC_DIR = os.path.join(
AA_BULLETIN_BOARD_BASE_DIR, "static", "aa_bulletin_board"
)
Empty file.
39 changes: 39 additions & 0 deletions aa_bulletin_board/helper/static_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
Helper functions for static integrity calculations
"""

# Standard Library
import os
from pathlib import Path

# Third Party
from sri import Algorithm, calculate_integrity

# Alliance Auth
from allianceauth.services.hooks import get_extension_logger

# Alliance Auth (External Libs)
from app_utils.logging import LoggerAddTag

# AA Bulletin Board
from aa_bulletin_board import __title__
from aa_bulletin_board.constants import AA_BULLETIN_BOARD_STATIC_DIR

logger = LoggerAddTag(my_logger=get_extension_logger(__name__), prefix=__title__)


def calculate_integrity_hash(relative_file_path: str) -> str:
"""
Calculates the integrity hash for a given static file
:param self:
:type self:
:param relative_file_path: The file path relative to the `aa-timezones/timezones/static/timezones` folder
:type relative_file_path: str
:return: The integrity hash
:rtype: str
"""

file_path = os.path.join(AA_BULLETIN_BOARD_STATIC_DIR, relative_file_path)
integrity_hash = calculate_integrity(Path(file_path), Algorithm.SHA512)

return integrity_hash
2 changes: 1 addition & 1 deletion aa_bulletin_board/locale/django.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: AA Bulletin Board 2.2.5\n"
"Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-bulletin-board/issues\n"
"POT-Creation-Date: 2024-12-14 13:27+0100\n"
"POT-Creation-Date: 2025-01-21 05:07+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{% load aa_bulletin_board %}

<link rel="stylesheet" href="{% aa_bulletin_board_static 'aa_bulletin_board/css/aa-bulletin-board.min.css' %}">
{% aa_bulletin_board_static "css/aa-bulletin-board.min.css" %}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{% load aa_bulletin_board %}

<script src="{% aa_bulletin_board_static 'aa_bulletin_board/javascript/aa-bulletin-board-equal-height.min.js' %}"></script>
{% aa_bulletin_board_static "javascript/aa-bulletin-board-equal-height.min.js" %}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{% load aa_bulletin_board %}

<script src="{% aa_bulletin_board_static 'aa_bulletin_board/javascript/aa-bulletin-board-oembed.min.js' %}"></script>
{% aa_bulletin_board_static "javascript/aa-bulletin-board-oembed.min.js" %}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{% load static %}
{% load sri %}

<link href="{% static 'django_ckeditor_5/dist/styles.css' %}" media="all" rel="stylesheet">
{% sri_static "django_ckeditor_5/dist/styles.css" %}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{% load static %}
{% load sri %}

<script src="{% static 'django_ckeditor_5/dist/bundle.js' %}"></script>
{% sri_static "django_ckeditor_5/dist/bundle.js" %}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{% load static %}
{% load aa_bulletin_board %}

<link rel="stylesheet" href="{% static 'aa_bulletin_board/libs/sumoselect/3.4.9/sumoselect.min.css' %}">
{% aa_bulletin_board_static "libs/sumoselect/3.4.9/sumoselect.min.css" %}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{% load static %}
{% load aa_bulletin_board %}

<script src="{% static 'aa_bulletin_board/libs/sumoselect/3.4.9/jquery.sumoselect.min.js' %}"></script>
{% aa_bulletin_board_static 'libs/sumoselect/3.4.9/jquery.sumoselect.min.js' %}
66 changes: 57 additions & 9 deletions aa_bulletin_board/templatetags/aa_bulletin_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,74 @@
Versioned static URLs to break browser caches when changing the app version
"""

# Standard Library
import os

# Django
from django.conf import settings
from django.template.defaulttags import register
from django.templatetags.static import static
from django.utils.safestring import mark_safe

# Alliance Auth
from allianceauth.services.hooks import get_extension_logger

# Alliance Auth (External Libs)
from app_utils.logging import LoggerAddTag

# AA Bulletin Board
from aa_bulletin_board import __version__
from aa_bulletin_board import __title__, __version__
from aa_bulletin_board.helper.static_files import calculate_integrity_hash

logger = LoggerAddTag(my_logger=get_extension_logger(__name__), prefix=__title__)


@register.simple_tag
def aa_bulletin_board_static(path: str) -> str:
def aa_bulletin_board_static(relative_file_path: str) -> str | None:
"""
Versioned static URL
:param path:
:type path:
:return:
:rtype:
:param relative_file_path: The file path relative to the `aa-aa_bulletin_board/aa_bulletin_board/static/aa_bulletin_board` folder
:type relative_file_path: str
:return: Versioned static URL
:rtype: str
"""

static_url = static(path=path)
versioned_url = static_url + "?v=" + __version__
logger.debug(f"Getting versioned static URL for: {relative_file_path}")

file_type = os.path.splitext(relative_file_path)[1][1:]

logger.debug(f"File extension: {file_type}")

# Only support CSS and JS files
if file_type not in ["css", "js"]:
raise ValueError(f"Unsupported file type: {file_type}")

Check warning on line 46 in aa_bulletin_board/templatetags/aa_bulletin_board.py

View check run for this annotation

Codecov / codecov/patch

aa_bulletin_board/templatetags/aa_bulletin_board.py#L46

Added line #L46 was not covered by tests

static_file_path = os.path.join("aa_bulletin_board", relative_file_path)
static_url = static(static_file_path)

# Integrity hash calculation only for non-debug mode
sri_string = (
f' integrity="{calculate_integrity_hash(relative_file_path)}" crossorigin="anonymous"'
if not settings.DEBUG
else ""
)

# Versioned URL for CSS and JS files
# Add version query parameter to break browser caches when changing the app version
# Do not add version query parameter for libs as they are already versioned through their file path
versioned_url = (
static_url
if relative_file_path.startswith("libs/")
else static_url + "?v=" + __version__
)

# Return the versioned URL with integrity hash for CSS
if file_type == "css":
return mark_safe(f'<link rel="stylesheet" href="{versioned_url}"{sri_string}>')

# Return the versioned URL with integrity hash for JS files
if file_type == "js":
return mark_safe(f'<script src="{versioned_url}"{sri_string}></script>')

return versioned_url
return None

Check warning on line 75 in aa_bulletin_board/templatetags/aa_bulletin_board.py

View check run for this annotation

Codecov / codecov/patch

aa_bulletin_board/templatetags/aa_bulletin_board.py#L75

Added line #L75 was not covered by tests
54 changes: 47 additions & 7 deletions aa_bulletin_board/tests/test_templatetags.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@

# Django
from django.template import Context, Template
from django.test import TestCase
from django.test import TestCase, override_settings

# AA Bulletin Board
from aa_bulletin_board import __version__
from aa_bulletin_board.helper.static_files import calculate_integrity_hash


class TestVersionedStatic(TestCase):
"""
Test aa_bulletin_board_static template tag
"""

def test_versioned_static(self) -> None:
@override_settings(DEBUG=False)
def test_versioned_static_without_debug_enabled(self) -> None:
"""
Test versioned static template tag
Test versioned static template tag without DEBUG enabled
:return:
:rtype:
Expand All @@ -27,13 +29,51 @@ def test_versioned_static(self) -> None:
template_to_render = Template(
template_string=(
"{% load aa_bulletin_board %}"
"{% aa_bulletin_board_static 'aa_bulletin_board/css/aa-bulletin-board.min.css' %}" # pylint: disable=line-too-long
"{% aa_bulletin_board_static 'css/aa-bulletin-board.min.css' %}"
"{% aa_bulletin_board_static 'javascript/aa-bulletin-board-oembed.min.js' %}"
)
)

rendered_template = template_to_render.render(context=context)

self.assertInHTML(
needle=f'/static/aa_bulletin_board/css/aa-bulletin-board.min.css?v={context["version"]}', # pylint: disable=line-too-long
haystack=rendered_template,
expected_static_css_src = f'/static/aa_bulletin_board/css/aa-bulletin-board.min.css?v={context["version"]}'
expected_static_css_src_integrity = calculate_integrity_hash(
"css/aa-bulletin-board.min.css"
)
expected_static_js_src = f'/static/aa_bulletin_board/javascript/aa-bulletin-board-oembed.min.js?v={context["version"]}'
expected_static_js_src_integrity = calculate_integrity_hash(
"javascript/aa-bulletin-board-oembed.min.js"
)

self.assertIn(member=expected_static_css_src, container=rendered_template)
self.assertIn(
member=expected_static_css_src_integrity, container=rendered_template
)
self.assertIn(member=expected_static_js_src, container=rendered_template)
self.assertIn(
member=expected_static_js_src_integrity, container=rendered_template
)

@override_settings(DEBUG=True)
def test_versioned_static_with_debug_enabled(self) -> None:
"""
Test versioned static template tag with DEBUG enabled
:return:
:rtype:
"""

context = Context({"version": __version__})
template_to_render = Template(
template_string=(
"{% load aa_bulletin_board %}"
"{% aa_bulletin_board_static 'css/aa-bulletin-board.min.css' %}"
)
)

rendered_template = template_to_render.render(context=context)

expected_static_css_src = f'/static/aa_bulletin_board/css/aa-bulletin-board.min.css?v={context["version"]}'

self.assertIn(member=expected_static_css_src, container=rendered_template)
self.assertNotIn(member="integrity=", container=rendered_template)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ dynamic = [
"version",
]
dependencies = [
"allianceauth>=4.3.1,<5",
"allianceauth>=4.6,<5",
"allianceauth-app-utils>=1.19.1",
"django-ckeditor-5>=0.2.14",
"unidecode",
Expand Down

0 comments on commit 17f644f

Please sign in to comment.