From 379f0bf500e57ba2c31fec33c6cad2ab2670ba2a Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Sun, 2 Feb 2025 14:20:10 +0100 Subject: [PATCH] [CHANGE] Use `django-sri` for sri hashes --- .make/conf.d/django.mk | 71 ++++++++++-------- CHANGELOG.md | 6 ++ aasrp/constants.py | 8 ++ aasrp/helper/static_files.py | 41 ++++++++++ aasrp/templates/aasrp/bundles/aa-srp-css.html | 7 +- .../aasrp/bundles/aa-srp-form-css.html | 7 +- .../aasrp/bundles/aa-srp-form-js.html | 6 +- aasrp/templates/aasrp/bundles/aa-srp-js.html | 6 +- .../aasrp/bundles/my-srp-requests-js.html | 6 +- .../templates/aasrp/bundles/srp-links-js.html | 6 +- .../aasrp/bundles/view-requests-js.html | 6 +- .../aasrp/bundles/x-editable-css.html | 9 +-- .../aasrp/bundles/x-editable-js.html | 8 +- aasrp/templatetags/aasrp.py | 74 +++++++++++++++---- aasrp/tests/test_templatetags.py | 60 +++++++++++++-- pyproject.toml | 2 +- 16 files changed, 221 insertions(+), 102 deletions(-) create mode 100644 aasrp/helper/static_files.py diff --git a/.make/conf.d/django.mk b/.make/conf.d/django.mk index 99c4d03d..49079be7 100644 --- a/.make/conf.d/django.mk +++ b/.make/conf.d/django.mk @@ -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); @@ -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); \ @@ -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); \ @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index cb27c1aa..b9bdc303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,12 @@ Section Order: ### Security --> +### Changed + +- Use `django-sri` for sri hashes +- Minimum requirements + - Alliance Auth >= 4.6.0 + ## [2.5.5] - 2025-01-13 ### Fixed diff --git a/aasrp/constants.py b/aasrp/constants.py index 15f5dc9e..c1cc5835 100644 --- a/aasrp/constants.py +++ b/aasrp/constants.py @@ -2,6 +2,9 @@ Constants """ +# Standard Library +import os + # Third Party from requests.__version__ import __version__ as requests_version @@ -21,6 +24,11 @@ f"{APP_NAME}/{__version__} +{GITHUB_URL} via requests/{requests_version}" ) +# aa-srp/aasrp +AA_SRP_BASE_DIR = os.path.join(os.path.dirname(__file__)) +# aa-srp/aasrp/static/aasrp +AA_SRP_STATIC_DIR = os.path.join(AA_SRP_BASE_DIR, "static", "aasrp") + SRP_REQUEST_NOTIFICATION_INQUIRY_NOTE = _( "If you have any questions regarding your SRP request, feel free to contact your " diff --git a/aasrp/helper/static_files.py b/aasrp/helper/static_files.py new file mode 100644 index 00000000..63b217ef --- /dev/null +++ b/aasrp/helper/static_files.py @@ -0,0 +1,41 @@ +""" +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 SRP +from aasrp import __title__ +from aasrp.constants import AA_SRP_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-srp/aasrp/static/aasrp` folder + :type relative_file_path: str + :return: The integrity hash + :rtype: str + """ + + file_path = os.path.join(AA_SRP_STATIC_DIR, relative_file_path) + integrity_hash = calculate_integrity( + path=Path(file_path), algorithm=Algorithm.SHA512 + ) + + return integrity_hash diff --git a/aasrp/templates/aasrp/bundles/aa-srp-css.html b/aasrp/templates/aasrp/bundles/aa-srp-css.html index d3cc40fb..21dc24bb 100644 --- a/aasrp/templates/aasrp/bundles/aa-srp-css.html +++ b/aasrp/templates/aasrp/bundles/aa-srp-css.html @@ -1,8 +1,3 @@ {% load aasrp %} - +{% aasrp_static "css/aa-srp.min.css" %} diff --git a/aasrp/templates/aasrp/bundles/aa-srp-form-css.html b/aasrp/templates/aasrp/bundles/aa-srp-form-css.html index 52e2a388..aacd702f 100644 --- a/aasrp/templates/aasrp/bundles/aa-srp-form-css.html +++ b/aasrp/templates/aasrp/bundles/aa-srp-form-css.html @@ -1,8 +1,3 @@ {% load aasrp %} - +{% aasrp_static "css/aa-srp-form.min.css" %} diff --git a/aasrp/templates/aasrp/bundles/aa-srp-form-js.html b/aasrp/templates/aasrp/bundles/aa-srp-form-js.html index 517c24f5..0642edc9 100644 --- a/aasrp/templates/aasrp/bundles/aa-srp-form-js.html +++ b/aasrp/templates/aasrp/bundles/aa-srp-form-js.html @@ -1,7 +1,3 @@ {% load aasrp %} - +{% aasrp_static "javascript/form.min.js" %} diff --git a/aasrp/templates/aasrp/bundles/aa-srp-js.html b/aasrp/templates/aasrp/bundles/aa-srp-js.html index 474a3f96..253b2831 100644 --- a/aasrp/templates/aasrp/bundles/aa-srp-js.html +++ b/aasrp/templates/aasrp/bundles/aa-srp-js.html @@ -1,7 +1,3 @@ {% load aasrp %} - +{% aasrp_static "javascript/aa-srp.min.js" %} diff --git a/aasrp/templates/aasrp/bundles/my-srp-requests-js.html b/aasrp/templates/aasrp/bundles/my-srp-requests-js.html index a91cff99..dd0dd9ed 100644 --- a/aasrp/templates/aasrp/bundles/my-srp-requests-js.html +++ b/aasrp/templates/aasrp/bundles/my-srp-requests-js.html @@ -1,7 +1,3 @@ {% load aasrp %} - +{% aasrp_static "javascript/my-srp-requests.min.js" %} diff --git a/aasrp/templates/aasrp/bundles/srp-links-js.html b/aasrp/templates/aasrp/bundles/srp-links-js.html index c143216d..c3ffd624 100644 --- a/aasrp/templates/aasrp/bundles/srp-links-js.html +++ b/aasrp/templates/aasrp/bundles/srp-links-js.html @@ -1,7 +1,3 @@ {% load aasrp %} - +{% aasrp_static "javascript/srp-links.min.js" %} diff --git a/aasrp/templates/aasrp/bundles/view-requests-js.html b/aasrp/templates/aasrp/bundles/view-requests-js.html index 90d61378..dba02e95 100644 --- a/aasrp/templates/aasrp/bundles/view-requests-js.html +++ b/aasrp/templates/aasrp/bundles/view-requests-js.html @@ -1,7 +1,3 @@ {% load aasrp %} - +{% aasrp_static "javascript/view-requests.min.js" %} diff --git a/aasrp/templates/aasrp/bundles/x-editable-css.html b/aasrp/templates/aasrp/bundles/x-editable-css.html index df789046..97c8d797 100644 --- a/aasrp/templates/aasrp/bundles/x-editable-css.html +++ b/aasrp/templates/aasrp/bundles/x-editable-css.html @@ -1,8 +1,3 @@ -{% load static %} +{% load aasrp %} - +{% aasrp_static "libs/x-editable/1.5.4/bootstrap5-editable/css/bootstrap-editable.min.css" %} diff --git a/aasrp/templates/aasrp/bundles/x-editable-js.html b/aasrp/templates/aasrp/bundles/x-editable-js.html index 7b4946b1..dd644231 100644 --- a/aasrp/templates/aasrp/bundles/x-editable-js.html +++ b/aasrp/templates/aasrp/bundles/x-editable-js.html @@ -1,7 +1,3 @@ -{% load static %} +{% load aasrp %} - +{% aasrp_static "libs/x-editable/1.5.4/bootstrap5-editable/js/bootstrap-editable.min.js" %} diff --git a/aasrp/templatetags/aasrp.py b/aasrp/templatetags/aasrp.py index 7e812900..e41beef8 100644 --- a/aasrp/templatetags/aasrp.py +++ b/aasrp/templatetags/aasrp.py @@ -2,37 +2,85 @@ Versioned static URLs to break browser caches when changing the app version """ +# Standard Library +import os + # Django +from django.conf import settings from django.contrib.auth.models import User from django.template.defaulttags import register from django.templatetags.static import static +from django.utils.safestring import mark_safe # Alliance Auth from allianceauth.framework.api.user import get_main_character_name_from_user +from allianceauth.services.hooks import get_extension_logger + +# Alliance Auth (External Libs) +from app_utils.logging import LoggerAddTag # AA SRP -from aasrp import __version__ +from aasrp import __title__, __version__ +from aasrp.helper.static_files import calculate_integrity_hash + +logger = LoggerAddTag(my_logger=get_extension_logger(__name__), prefix=__title__) @register.simple_tag -def aasrp_static(path: str) -> str: +def aasrp_static(relative_file_path: str, script_type: str = None) -> str | None: """ Versioned static URL - Adding the app version to any static file we load through this function. - This is to make sure to break the browser cache on app updates. - - Example: /static/myapp/css/myapp.css?ver=1.0.0 - :param path: - :type path: - :return: - :rtype: + :param relative_file_path: The file path relative to the `aa-srp/aasrp/static/aasrp` folder + :type relative_file_path: str + :param script_type: The script type + :type script_type: str + :return: Versioned static URL + :rtype: str """ - static_url = static(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}") + + static_file_path = os.path.join("aasrp", 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'') + + # Return the versioned URL with integrity hash for JS files + if file_type == "js": + js_type = f' type="{script_type}"' if script_type else "" + + return mark_safe( + f'' + ) - return versioned_url + return None @register.filter diff --git a/aasrp/tests/test_templatetags.py b/aasrp/tests/test_templatetags.py index 3b1acf4e..f3958d28 100644 --- a/aasrp/tests/test_templatetags.py +++ b/aasrp/tests/test_templatetags.py @@ -4,13 +4,14 @@ # Django from django.template import Context, Template -from django.test import TestCase +from django.test import TestCase, override_settings # Alliance Auth from allianceauth.tests.auth_utils import AuthUtils # AA SRP from aasrp import __version__ +from aasrp.helper.static_files import calculate_integrity_hash from aasrp.models import get_sentinel_user from aasrp.tests.utils import create_fake_user @@ -598,14 +599,15 @@ def test_should_be_dummy_id_for_none(self): self.assertEqual(first=result, second="1") -class TestForumVersionedStatic(TestCase): +class TestVersionedStatic(TestCase): """ Tests for aasrp_static template tag """ + @override_settings(DEBUG=False) def test_versioned_static(self): """ - Test should return the static URL string with the app version + Test should return the versioned static :return: :rtype: @@ -614,13 +616,57 @@ def test_versioned_static(self): context = Context(dict_={"version": __version__}) template_to_render = Template( template_string=( - "{% load aasrp %}{% aasrp_static 'aasrp/css/aa-srp.min.css' %}" + "{% load aasrp %}" + "{% aasrp_static 'css/aa-srp.min.css' %}" + "{% aasrp_static 'javascript/aa-srp.min.js' %}" ) ) rendered_template = template_to_render.render(context=context) - self.assertInHTML( - needle=f'/static/aasrp/css/aa-srp.min.css?v={context["version"]}', - haystack=rendered_template, + expected_static_css_src = ( + f'/static/aasrp/css/aa-srp.min.css?v={context["version"]}' ) + expected_static_css_src_integrity = calculate_integrity_hash( + "css/aa-srp.min.css" + ) + expected_static_js_src = ( + f'/static/aasrp/javascript/aa-srp.min.js?v={context["version"]}' + ) + expected_static_js_src_integrity = calculate_integrity_hash( + "javascript/aa-srp.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 aasrp %}" "{% aasrp_static 'css/aa-srp.min.css' %}" + ) + ) + + rendered_template = template_to_render.render(context=context) + + expected_static_css_src = ( + f'/static/aasrp/css/aa-srp.min.css?v={context["version"]}' + ) + + self.assertIn(member=expected_static_css_src, container=rendered_template) + self.assertNotIn(member="integrity=", container=rendered_template) diff --git a/pyproject.toml b/pyproject.toml index ec46bfa3..d4eff903 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dynamic = [ "version", ] dependencies = [ - "allianceauth>=4.3.1,<5", + "allianceauth>=4.6,<5", "allianceauth-app-utils>=1.25", "django-eveuniverse>=1.5.4", ]