diff --git a/filter_plugins/safe_join.py b/filter_plugins/safe_join.py new file mode 100644 index 00000000..086d9ae0 --- /dev/null +++ b/filter_plugins/safe_join.py @@ -0,0 +1,29 @@ +# file: filter_plugins/safe_join.py +""" +Ansible filter plugin that joins a base string and a tail path safely. +If the base is falsy (None, empty, etc.), returns an empty string. +""" + +def safe_join(base, tail): + """ + Safely join base and tail into a path or URL. + + - base: the base string. If falsy, returns ''. + - tail: the string to append. Leading/trailing slashes are handled. + - On any exception, returns ''. + """ + try: + if not base: + return '' + base_str = str(base).rstrip('/') + tail_str = str(tail).lstrip('/') + return f"{base_str}/{tail_str}" + except Exception: + return '' + + +class FilterModule(object): + def filters(self): + return { + 'safe_join': safe_join, + } diff --git a/group_vars/all/15_about.yml b/group_vars/all/15_about.yml index aa0b86de..b6cfd71a 100644 --- a/group_vars/all/15_about.yml +++ b/group_vars/all/15_about.yml @@ -9,18 +9,12 @@ defaults_service_provider: city: "Cybertown" postal_code: "00001" country: "Nexusland" - logo: >- - {{ (applications.assets_server.url | safe_var ~ '/logo.png') - if applications.assets_server.url | safe_var else '' }} + logo: "{{ applications.assets_server.url | safe_var | safe_join('logo.png') }}" platform: titel: "CyMaIS Demo" subtitel: "The Future of Self-Hosted Infrastructure. Secure. Automated. Sovereign." - logo: >- - {{ (applications.assets_server.url | safe_var ~ '/logo.png') - if applications.assets_server.url | safe_var else '' }} - favicon: >- - {{ (applications.assets_server.url | safe_var ~ '/favicon.ico') - if applications.assets_server.url | safe_var else '' }} + logo: "{{ applications.assets_server.url | safe_var | safe_join('logo.png') }}" + favicon: "{{ applications.assets_server.url | safe_var | safe_join('favicon.ico') }}" contact: bluesky: >- {{ ('@' ~ users.administrator.username ~ '.' ~ domains[application_id]['api']) @@ -36,4 +30,10 @@ defaults_service_provider: legal: editorial_responsible: "Johannes Gutenberg" source_code: "https://github.com/kevinveenbirkenbach/cymais" - imprint: "{{ web_protocol }}://{{domains.html_server}}/imprint.html" \ No newline at end of file + imprint: >- + {{ "{protocol}://{domain}/imprint.html" + | safe_placeholders({ + 'protocol': web_protocol, + 'domain': domains.html_server + }) + }} diff --git a/roles/docker-discourse/templates/discourse_application.yml.j2 b/roles/docker-discourse/templates/discourse_application.yml.j2 index 2642c3c9..0b3a5083 100644 --- a/roles/docker-discourse/templates/discourse_application.yml.j2 +++ b/roles/docker-discourse/templates/discourse_application.yml.j2 @@ -51,8 +51,10 @@ env: DISCOURSE_DEVELOPER_EMAILS: {{users.administrator.email}} # Set Logo + {% if service_provider.platform.logo | bool %} DISCOURSE_LOGO_URL: "{{ service_provider.platform.logo }}" DISCOURSE_LOGO_SMALL_URL: "{{ service_provider.platform.logo }}" + {% endif %} # SMTP ADDRESS, username, and password are required # WARNING the char '#' in SMTP password can cause problems! diff --git a/roles/docker-listmonk/vars/main.yml b/roles/docker-listmonk/vars/main.yml index 3aec8df7..c9595607 100644 --- a/roles/docker-listmonk/vars/main.yml +++ b/roles/docker-listmonk/vars/main.yml @@ -65,8 +65,9 @@ listmonk_settings: # - key: "messengers" # value: '[]' - - key: "app.logo_url" - value: '"{{ service_provider.platform.logo }}"' + - key: "app.logo_url" + value: '"{{ service_provider.platform.logo }}"' + when: service_provider.platform.logo | bool - key: "app.site_name" value: '"{{ service_provider.company.titel }} Mailing list"' @@ -115,8 +116,9 @@ listmonk_settings: # - key: "app.concurrency" # value: '10' - - key: "app.favicon_url" - value: '"{{ service_provider.platform.favicon }}"' + - key: "app.favicon_url" + value: '"{{ service_provider.platform.favicon }}"' + when: service_provider.platform.favicon | bool # - key: "bounce.postmark" # value: '{"enabled": false, "password": "", "username": ""}' diff --git a/tests/unit/test_safe_join.py b/tests/unit/test_safe_join.py new file mode 100644 index 00000000..1d3fd75f --- /dev/null +++ b/tests/unit/test_safe_join.py @@ -0,0 +1,51 @@ +import unittest +import sys +import os + +# Ensure filter_plugins directory is on the path +sys.path.insert( + 0, + os.path.abspath(os.path.join(os.path.dirname(__file__), '../../filter_plugins')) +) + +from safe_join import safe_join + +class TestSafeJoinFilter(unittest.TestCase): + def test_join_with_trailing_slashes(self): + self.assertEqual( + safe_join('http://example.com/', '/path/to'), + 'http://example.com/path/to' + ) + + def test_join_without_slashes(self): + self.assertEqual( + safe_join('http://example.com', 'path/to'), + 'http://example.com/path/to' + ) + + def test_base_none(self): + self.assertEqual(safe_join(None, 'path'), '') + + def test_base_empty(self): + self.assertEqual(safe_join('', 'path'), '') + + def test_tail_empty(self): + # joining with empty tail should yield base with trailing slash + self.assertEqual( + safe_join('http://example.com', ''), + 'http://example.com/' + ) + + def test_numeric_base(self): + # numeric base is cast to string + self.assertEqual(safe_join(123, 'path'), '123/path') + + def test_exception_in_str(self): + class Bad: + def __str__(self): + raise ValueError('bad') + # on exception, safe_join returns '' + self.assertEqual(safe_join(Bad(), 'x'), '') + +if __name__ == '__main__': + unittest.main()