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 8ec64fcd..f929c651 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.3] - 2025-01-13 ### Added diff --git a/aa_intel_tool/app_settings.py b/aa_intel_tool/app_settings.py index d3406df1..ee92ea65 100644 --- a/aa_intel_tool/app_settings.py +++ b/aa_intel_tool/app_settings.py @@ -57,7 +57,7 @@ class AppSettings: ) # Set the grid size for D-Scans. - # This defines the size of teh grid in which ships and + # This defines the size of the grid in which ships and # structure are considered to be "on grid" INTELTOOL_DSCAN_GRID_SIZE = clean_setting( name="INTELTOOL_DSCAN_GRID_SIZE", default_value=10000, required_type=int diff --git a/aa_intel_tool/constants.py b/aa_intel_tool/constants.py index b6b0447b..25b66146 100644 --- a/aa_intel_tool/constants.py +++ b/aa_intel_tool/constants.py @@ -3,6 +3,7 @@ """ # Standard Library +import os import re # Django @@ -81,3 +82,8 @@ APP_NAME = "aa-intel-tool" GITHUB_URL = f"https://github.com/ppfeufer/{APP_NAME}" USER_AGENT = f"{APP_NAME}/{__version__} ({GITHUB_URL}) via django-esi/{esi_version}" + +AA_INTEL_TOOL_BASE_DIR = os.path.join(os.path.dirname(__file__)) +AA_INTEL_TOOL_STATIC_DIR = os.path.join( + AA_INTEL_TOOL_BASE_DIR, "static", "aa_intel_tool" +) diff --git a/aa_intel_tool/helper/static_files.py b/aa_intel_tool/helper/static_files.py new file mode 100644 index 00000000..4622d5c6 --- /dev/null +++ b/aa_intel_tool/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 Intel Tool +from aa_intel_tool import __title__ +from aa_intel_tool.constants import AA_INTEL_TOOL_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-intel-tool/aa_intel_tool/static/aa_intel_tool` folder + :type relative_file_path: str + :return: The integrity hash + :rtype: str + """ + + file_path = os.path.join(AA_INTEL_TOOL_STATIC_DIR, relative_file_path) + integrity_hash = calculate_integrity( + path=Path(file_path), algorithm=Algorithm.SHA512 + ) + + return integrity_hash diff --git a/aa_intel_tool/locale/django.pot b/aa_intel_tool/locale/django.pot index bf5cd90f..9115fc5b 100644 --- a/aa_intel_tool/locale/django.pot +++ b/aa_intel_tool/locale/django.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: AA Intel Tool 2.5.3\n" "Report-Msgid-Bugs-To: https://github.com/ppfeufer/aa-intel-tool/issues\n" -"POT-Creation-Date: 2025-01-13 15:19+0100\n" +"POT-Creation-Date: 2025-01-31 11:24+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -46,16 +46,16 @@ msgstr "" msgid "Intel Parser v{__version__}" msgstr "" -#: aa_intel_tool/constants.py:60 aa_intel_tool/models.py:24 +#: aa_intel_tool/constants.py:61 aa_intel_tool/models.py:24 msgid "Chat list" msgstr "" -#: aa_intel_tool/constants.py:66 aa_intel_tool/models.py:22 +#: aa_intel_tool/constants.py:67 aa_intel_tool/models.py:22 #: aa_intel_tool/templates/aa_intel_tool/partials/index/form.html:28 msgid "D-Scan" msgstr "" -#: aa_intel_tool/constants.py:72 aa_intel_tool/models.py:23 +#: aa_intel_tool/constants.py:73 aa_intel_tool/models.py:23 #: aa_intel_tool/models.py:123 #: aa_intel_tool/templates/aa_intel_tool/partials/index/form.html:33 msgid "Fleet composition" diff --git a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-chatscan-highlight-js.html b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-chatscan-highlight-js.html index 7b4ae740..5156cef1 100644 --- a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-chatscan-highlight-js.html +++ b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-chatscan-highlight-js.html @@ -1,7 +1,3 @@ {% load aa_intel_tool %} - +{% aa_intel_tool_static "javascript/aa-intel-tool-chatscan-highlight.min.js" %} diff --git a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-chatscan-js.html b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-chatscan-js.html index 7452ad32..8004d734 100644 --- a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-chatscan-js.html +++ b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-chatscan-js.html @@ -5,8 +5,4 @@ {% include "aa_intel_tool/bundles/aa-intel-tool-chatscan-highlight-js.html" %} {% endif %} - +{% aa_intel_tool_static "javascript/aa-intel-tool-chatscan.min.js" %} diff --git a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-css.html b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-css.html index 3d2ac657..8443e2f9 100644 --- a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-css.html +++ b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-css.html @@ -1,8 +1,3 @@ {% load aa_intel_tool %} - +{% aa_intel_tool_static "css/aa-intel-tool.min.css" %} diff --git a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-dscan-highlight-js.html b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-dscan-highlight-js.html index 5cd7ddbd..ce9581fa 100644 --- a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-dscan-highlight-js.html +++ b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-dscan-highlight-js.html @@ -1,7 +1,3 @@ {% load aa_intel_tool %} - +{% aa_intel_tool_static "javascript/aa-intel-tool-dscan-highlight.min.js" %} diff --git a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-dscan-js.html b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-dscan-js.html index c4e726f4..b8d4eb2a 100644 --- a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-dscan-js.html +++ b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-dscan-js.html @@ -3,8 +3,4 @@ {% include "aa_intel_tool/bundles/aa-intel-tool-scan-result-common-js.html" %} {% include "aa_intel_tool/bundles/aa-intel-tool-dscan-highlight-js.html" %} - +{% aa_intel_tool_static "javascript/aa-intel-tool-dscan.min.js" %} diff --git a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-fleetcomp-js.html b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-fleetcomp-js.html index 334ecd81..dcde1a21 100644 --- a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-fleetcomp-js.html +++ b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-fleetcomp-js.html @@ -3,11 +3,7 @@ {% include "aa_intel_tool/bundles/aa-intel-tool-scan-result-common-js.html" %} {% include "aa_intel_tool/bundles/aa-intel-tool-fleetcomposition-highlight-js.html" %} - +{% aa_intel_tool_static "javascript/aa-intel-tool-fleetcomposition.min.js" %} {% if app_settings.INTELTOOL_ENABLE_MODULE_CHATSCAN %} {% include "aa_intel_tool/bundles/aa-intel-tool-chatscan-highlight-js.html" with common_already_loaded=True %} diff --git a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-fleetcomposition-highlight-js.html b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-fleetcomposition-highlight-js.html index 3bf37236..d9bd30d3 100644 --- a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-fleetcomposition-highlight-js.html +++ b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-fleetcomposition-highlight-js.html @@ -1,7 +1,3 @@ {% load aa_intel_tool %} - +{% aa_intel_tool_static "javascript/aa-intel-tool-fleetcomposition-highlight.min.js" %} diff --git a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-js.html b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-js.html index fa244e5c..d23e77c0 100644 --- a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-js.html +++ b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-js.html @@ -1,7 +1,3 @@ {% load aa_intel_tool %} - +{% aa_intel_tool_static "javascript/aa-intel-tool.min.js" %} diff --git a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-scan-result-common-js.html b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-scan-result-common-js.html index b4aef29e..41e87b68 100644 --- a/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-scan-result-common-js.html +++ b/aa_intel_tool/templates/aa_intel_tool/bundles/aa-intel-tool-scan-result-common-js.html @@ -1,7 +1,3 @@ {% load aa_intel_tool %} - +{% aa_intel_tool_static "javascript/aa-intel-tool-scan-result-common.min.js" %} diff --git a/aa_intel_tool/templatetags/aa_intel_tool.py b/aa_intel_tool/templatetags/aa_intel_tool.py index 9601fea0..55b831d0 100644 --- a/aa_intel_tool/templatetags/aa_intel_tool.py +++ b/aa_intel_tool/templatetags/aa_intel_tool.py @@ -2,25 +2,82 @@ 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 Intel Tool -from aa_intel_tool import __version__ +from aa_intel_tool import __title__, __version__ +from aa_intel_tool.helper.static_files import calculate_integrity_hash + +logger = LoggerAddTag(my_logger=get_extension_logger(__name__), prefix=__title__) @register.simple_tag -def aa_intel_tool_static(path: str) -> str: +def aa_intel_tool_static( + relative_file_path: str, script_type: str = None +) -> str | None: """ Versioned static URL - :param path: - :type path: - :return: - :rtype: + + :param relative_file_path: The file path relative to the `aa-intel-tool/aa_intel_tool/static/aa_intel_tool 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("aa_intel_tool", 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 diff --git a/aa_intel_tool/tests/test_app_settings.py b/aa_intel_tool/tests/test_app_settings.py index e00121e4..7f3c3845 100644 --- a/aa_intel_tool/tests/test_app_settings.py +++ b/aa_intel_tool/tests/test_app_settings.py @@ -6,7 +6,8 @@ from unittest import mock # Django -from django.test import TestCase +from django.conf import settings +from django.test import TestCase, override_settings # AA Intel Tool from aa_intel_tool.app_settings import AppSettings @@ -46,6 +47,7 @@ def test_scan_retention_time_custom(self): self.assertEqual(first=retention_time, second=expected_retention_time) + @override_settings() def test_chatscan_max_pilots_default(self): """ Test for the default INTELTOOL_CHATSCAN_MAX_PILOTS @@ -54,9 +56,13 @@ def test_chatscan_max_pilots_default(self): :rtype: """ + del settings.INTELTOOL_CHATSCAN_MAX_PILOTS + max_pilots = AppSettings.INTELTOOL_CHATSCAN_MAX_PILOTS expected_max_pilots = 500 + print("max_pilots:", max_pilots) + self.assertEqual(first=max_pilots, second=expected_max_pilots) @mock.patch(SETTINGS_PATH + ".AppSettings.INTELTOOL_CHATSCAN_MAX_PILOTS", 1000) diff --git a/aa_intel_tool/tests/test_templatetags.py b/aa_intel_tool/tests/test_templatetags.py index d494ef9a..4017f4a1 100644 --- a/aa_intel_tool/tests/test_templatetags.py +++ b/aa_intel_tool/tests/test_templatetags.py @@ -3,35 +3,80 @@ """ # Django -from django.test import TestCase +from django.template import Context, Template +from django.test import TestCase, override_settings # AA Intel Tool from aa_intel_tool import __version__ -from aa_intel_tool.tests.utils import render_template +from aa_intel_tool.helper.static_files import calculate_integrity_hash -class TestForumVersionedStatic(TestCase): +class TestVersionedStatic(TestCase): """ Tests for aa_intel_tool template tag """ + @override_settings(DEBUG=False) def test_versioned_static(self): """ Test should return static URL string with a version :return: """ - context = {"version": __version__} + context = Context(dict_={"version": __version__}) - rendered_template = render_template( - string=( + template_to_render = Template( + template_string=( "{% load aa_intel_tool %}" - "{% aa_intel_tool_static 'aa_intel_tool/css/aa-intel-tool.min.css' %}" - ), - context=context, + "{% aa_intel_tool_static 'css/aa-intel-tool.min.css' %}" + "{% aa_intel_tool_static 'javascript/aa-intel-tool.min.js' %}" + ) ) - self.assertEqual( - rendered_template, - f'/static/aa_intel_tool/css/aa-intel-tool.min.css?v={context["version"]}', + rendered_template = template_to_render.render(context=context) + + expected_static_css_src = ( + f'/static/aa_intel_tool/css/aa-intel-tool.min.css?v={context["version"]}' + ) + expected_static_css_src_integrity = calculate_integrity_hash( + "css/aa-intel-tool.min.css" + ) + expected_static_js_src = f'/static/aa_intel_tool/javascript/aa-intel-tool.min.js?v={context["version"]}' + expected_static_js_src_integrity = calculate_integrity_hash( + "javascript/aa-intel-tool.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_intel_tool %}" + "{% aa_intel_tool_static 'css/aa-intel-tool.min.css' %}" + ) ) + + rendered_template = template_to_render.render(context=context) + + expected_static_css_src = ( + f'/static/aa_intel_tool/css/aa-intel-tool.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 60c55332..955d385c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dynamic = [ "version", ] dependencies = [ - "allianceauth>=4.3.1,<5", + "allianceauth>=4.6,<5", "allianceauth-app-utils>=1.19.1", "django-eveuniverse>=1.3", ]