diff --git a/CHANGELOG.md b/CHANGELOG.md index cb654b3..78b0b78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ > [!NOTE] > Release notes are also available on the [wiki](https://github.com/beatonma/django-wm/wiki/Releases). -## 4.1 (2024-02-09) +## 4.1.0 (2024-02-10) > [!WARNING] > `python manage.py migrate` required for new fields. @@ -14,12 +14,15 @@ - New `objects` manager for Webmention with some common filters and actions. -- New [settings](https://github.com/beatonma/django-wm/wiki/Settings) for allowing or disabling webmentions to/from a set of domains: - - `WEBMENTIONS_DOMAINS_INCOMING_ALLOW: Iterable[str] = None` - - `WEBMENTIONS_DOMAINS_INCOMING_DENY: Iterable[str] = None` - - `WEBMENTIONS_DOMAINS_OUTGOING_ALLOW: Iterable[str] = None` - - `WEBMENTIONS_DOMAINS_OUTGOING_DENY: Iterable[str] = None` - - `WEBMENTIONS_DOMAINS_OUTGOING_OVERRIDE_TAG: str = None` +- New settings for allowing or disabling webmentions to/from a set of domains: + - `WEBMENTIONS_DOMAINS_INCOMING_ALLOW: Iterable[str] = None` + - `WEBMENTIONS_DOMAINS_INCOMING_DENY: Iterable[str] = None` + - `WEBMENTIONS_DOMAINS_OUTGOING_ALLOW: Iterable[str] = None` + - `WEBMENTIONS_DOMAINS_OUTGOING_DENY: Iterable[str] = None` + +- New settings for tags that can be added to your HTML links to allow/disable sending webmentions for just that specific link. (Overrides above allow/deny lists) + - `WEBMENTIONS_DOMAINS_OUTGOING_TAG_ALLOW: str = None` + - `WEBMENTIONS_DOMAINS_OUTGOING_TAG_DENY: str = None` ## 4.0.4 (2024-02-02) diff --git a/mentions/__init__.py b/mentions/__init__.py index 63fb28c..0926727 100644 --- a/mentions/__init__.py +++ b/mentions/__init__.py @@ -1,2 +1,2 @@ -__version__ = "4.1" +__version__ = "4.1.0" __url__ = "https://github.com/beatonma/django-wm/" diff --git a/mentions/config.py b/mentions/config.py index 9502bdc..de611b9 100644 --- a/mentions/config.py +++ b/mentions/config.py @@ -63,7 +63,6 @@ def accept_domain_incoming( def accept_domain_outgoing( url: str, - inline_override: bool, allow_self_mention: bool, domains_allow: Optional[Set[str]], domains_deny: Optional[Set[str]], @@ -72,9 +71,6 @@ def accept_domain_outgoing( Args: url: The URL to check. - inline_override: Provide True if the source of `url` is annotated with - value of `options.outgoing_domains_override_attr()`, - otherwise False. allow_self_mention: Current value of `options.allow_self_mentions()`. domains_allow: Current value of `options.outgoing_domains_allow()`. domains_deny: Current value of `options.outgoing_domains_deny()`. @@ -93,10 +89,10 @@ def accept_domain_outgoing( ) if domains_deny: - return _domain_in_set(domain, domains_deny) is inline_override + return not _domain_in_set(domain, domains_deny) if domains_allow: - return _domain_in_set(domain, domains_allow) is not inline_override + return _domain_in_set(domain, domains_allow) return True diff --git a/mentions/options.py b/mentions/options.py index 0c87eb8..26c89be 100644 --- a/mentions/options.py +++ b/mentions/options.py @@ -34,7 +34,8 @@ "incoming_domains_deny", "outgoing_domains_deny", "outgoing_domains_allow", - "outgoing_domains_override_tag", + "outgoing_domains_tag_allow", + "outgoing_domains_tag_deny", "get_config", "max_retries", "retry_interval", @@ -56,7 +57,8 @@ SETTING_DOMAINS_INCOMING_DENY = f"{NAMESPACE}_DOMAINS_INCOMING_DENY" SETTING_DOMAINS_OUTGOING_ALLOW = f"{NAMESPACE}_DOMAINS_OUTGOING_ALLOW" SETTING_DOMAINS_OUTGOING_DENY = f"{NAMESPACE}_DOMAINS_OUTGOING_DENY" -SETTING_DOMAINS_OUTGOING_OVERRIDE_TAG = f"{NAMESPACE}_DOMAINS_OUTGOING_OVERRIDE_TAG" +SETTING_DOMAINS_OUTGOING_TAG_ALLOW = f"{NAMESPACE}_DOMAINS_OUTGOING_TAG_ALLOW" +SETTING_DOMAINS_OUTGOING_TAG_DENY = f"{NAMESPACE}_DOMAINS_OUTGOING_TAG_DENY" SETTING_INCOMING_TARGET_MODEL_REQUIRED = f"{NAMESPACE}_INCOMING_TARGET_MODEL_REQUIRED" SETTING_MAX_RETRIES = f"{NAMESPACE}_MAX_RETRIES" SETTING_RETRY_INTERVAL = f"{NAMESPACE}_RETRY_INTERVAL" @@ -80,7 +82,8 @@ SETTING_DOMAINS_INCOMING_DENY: None, SETTING_DOMAINS_OUTGOING_ALLOW: None, SETTING_DOMAINS_OUTGOING_DENY: None, - SETTING_DOMAINS_OUTGOING_OVERRIDE_TAG: None, + SETTING_DOMAINS_OUTGOING_TAG_ALLOW: None, + SETTING_DOMAINS_OUTGOING_TAG_DENY: None, SETTING_INCOMING_TARGET_MODEL_REQUIRED: False, SETTING_MAX_RETRIES: 5, SETTING_RETRY_INTERVAL: 60 * 10, @@ -206,14 +209,22 @@ def outgoing_domains_deny() -> Set[str]: return _get_attr(SETTING_DOMAINS_OUTGOING_DENY, _coerce_to_set) -def outgoing_domains_override_tag() -> str: - """Return settings.WEBMENTIONS_DOMAINS_OUTGOING_OVERRIDE_TAG. +def outgoing_domains_tag_allow() -> str: + """Return settings.WEBMENTIONS_DOMAINS_OUTGOING_TAG_ALLOW. - Name of CSS class or HTML `data-` attribute which, if present, reverses - the behaviour of WEBMENTIONS_DOMAINS_OUTGOING_ALLOW or - WEBMENTIONS_DOMAINS_OUTGOING_DENY. + Tag which can be included in HTML links as a class or attribute to override + the behaviour of WEBMENTIONS_DOMAINS_OUTGOING_DENY. """ - return _get_attr(SETTING_DOMAINS_OUTGOING_OVERRIDE_TAG) + return _get_attr(SETTING_DOMAINS_OUTGOING_TAG_ALLOW) + + +def outgoing_domains_tag_deny() -> str: + """Return settings.WEBMENTIONS_DOMAINS_OUTGOING_TAG_DENY. + + Tag which can be included in HTML links as a class or attribute to override + the behaviour of WEBMENTIONS_DOMAINS_OUTGOING_ALLOW. + """ + return _get_attr(SETTING_DOMAINS_OUTGOING_TAG_DENY) def max_retries() -> int: diff --git a/mentions/tasks/outgoing/local.py b/mentions/tasks/outgoing/local.py index f7cbe58..c6a6b03 100644 --- a/mentions/tasks/outgoing/local.py +++ b/mentions/tasks/outgoing/local.py @@ -1,5 +1,5 @@ import logging -from typing import Iterable, List, Optional, Set +from typing import Iterable, Optional, Set from urllib.parse import urljoin from bs4 import Tag @@ -21,9 +21,10 @@ def get_target_links_in_html( html: str, source_path: str, allow_self_mentions: bool = options.allow_self_mentions(), - domains_allow: Optional[List[str]] = None, - domains_deny: Optional[List[str]] = None, - domain_override_attr: Optional[str] = options.outgoing_domains_override_tag(), + domains_allow: Optional[Iterable[str]] = None, + domains_deny: Optional[Iterable[str]] = None, + domains_allow_tag: Optional[str] = options.outgoing_domains_tag_allow(), + domains_deny_tag: Optional[str] = options.outgoing_domains_tag_deny(), ) -> Set[str]: """Get any links from `html` that should be treated as webmention targets. @@ -37,7 +38,8 @@ def get_target_links_in_html( allow_self_mentions: See `options.allow_self_mentions`. domains_allow: See `options.outgoing_domains_allow`. domains_deny: See `options.outgoing_domains_deny`. - domain_override_attr: See `options.outgoing_domains_override_attr` + domains_allow_tag: See `options.outgoing_domains_tag_allow` + domains_deny_tag: See `options.outgoing_domains_tag_deny` Returns: Absolute URLs for any valid links from `html`. """ @@ -55,15 +57,18 @@ def get_target_links_in_html( href = _resolve_relative_url(source_path, href) + if _has_class_or_attribute(link, domains_deny_tag): + continue + + if _has_class_or_attribute(link, domains_allow_tag): + valid_links.add(href) + continue + if is_valid_target( href, allow_self_mentions, domains_allow=domains_allow, domains_deny=domains_deny, - override_include_exclude=_has_class_or_attribute( - tag=link, - attr=domain_override_attr, - ), ): valid_links.add(href) @@ -75,7 +80,6 @@ def is_valid_target( allow_self_mention: bool, domains_allow: Optional[Iterable[str]], domains_deny: Optional[Iterable[str]], - override_include_exclude: bool = False, ) -> bool: """ Args: @@ -83,7 +87,6 @@ def is_valid_target( allow_self_mention: See `options.allow_self_mentions`. domains_allow: See `options.outgoing_domains_allow`. domains_deny: See `options.outgoing_domains_deny`. - override_include_exclude: See `options.domains_override_attr` Returns: True if `url` is a valid URL and should be treated as a possible @@ -98,7 +101,6 @@ def is_valid_target( return config.accept_domain_outgoing( url=url, - inline_override=override_include_exclude, allow_self_mention=allow_self_mention, domains_allow=domains_allow, domains_deny=domains_deny, diff --git a/tests/tests/test_options/test_config.py b/tests/tests/test_options/test_config.py index a7c7101..4b407d3 100644 --- a/tests/tests/test_options/test_config.py +++ b/tests/tests/test_options/test_config.py @@ -6,10 +6,16 @@ class AcceptDomainIncomingTests(SimpleTestCase): - """Tests for config.accept_domain_incoming neither domains_allow, domains_deny defined.""" + """Tests for config.accept_domain_incoming with neither domains_allow, domains_deny defined.""" def test_with_no_restrictions(self): - self.assertTrue(accept_domain_incoming(testfunc.random_url(), None, None)) + self.assertTrue( + accept_domain_incoming( + testfunc.random_url(), + None, + None, + ) + ) class AcceptDomainIncoming_withAllowList_Tests(SimpleTestCase): @@ -115,18 +121,30 @@ def test_mixed_domains(self): self._assertNotDenied("https://sub.deny.com", deny) +class AcceptDomainOutgoing(SimpleTestCase): + """Tests for config.accept_domain_outgoing with neither domains_allow, domains_deny defined.""" + + def test_with_no_restrictions(self): + self.assertTrue( + accept_domain_outgoing( + testfunc.random_url(), + True, + None, + None, + ) + ) + + class AcceptDomainOutgoing_withAllowList_Tests(SimpleTestCase): def _assertAllowed( self, url: str, domains_allow: Set[str], - inline_override: bool = False, allow_self_mention: bool = True, ): self.assertTrue( accept_domain_outgoing( url, - inline_override=inline_override, allow_self_mention=allow_self_mention, domains_allow=domains_allow, domains_deny=None, @@ -138,13 +156,11 @@ def _assertNotAllowed( self, url: str, domains_allow: Set[str], - inline_override: bool = False, allow_self_mention: bool = True, ): self.assertFalse( accept_domain_outgoing( url, - inline_override=inline_override, allow_self_mention=allow_self_mention, domains_allow=domains_allow, domains_deny=None, @@ -179,26 +195,17 @@ def test_wildcard_domains_are_not_accepted(self): self._assertNotAllowed("https://fallow.org", allow) self._assertNotAllowed("https://not.allow.net", allow) - def test_inline_override(self): - allow = {"*.allow.org", "*.allow.com", ""} - self._assertNotAllowed("https://allow.org", allow, inline_override=True) - self._assertAllowed( - "https://normally-disallowed.org", allow, inline_override=True - ) - class AcceptDomainOutgoing_withDenyList_Tests(SimpleTestCase): def _assertDenied( self, url: str, domains_deny: Set[str], - inline_override: bool = False, allow_self_mention: bool = True, ): self.assertFalse( accept_domain_outgoing( url, - inline_override=inline_override, allow_self_mention=allow_self_mention, domains_allow=None, domains_deny=domains_deny, @@ -210,13 +217,11 @@ def _assertNotDenied( self, url: str, domains_deny: Set[str], - inline_override: bool = False, allow_self_mention: bool = True, ): self.assertTrue( accept_domain_outgoing( url, - inline_override=inline_override, allow_self_mention=allow_self_mention, domains_allow=None, domains_deny=domains_deny, @@ -243,8 +248,3 @@ def test_wildcard_domains_are_not_denied(self): deny = {"*.deny.org", "*.also.deny.net"} self._assertNotDenied(testfunc.random_url(), deny) self._assertNotDenied("https://other.deny.net/123", deny) - - def test_inline_override(self): - deny = {"*.deny.org", "*.deny.com", ""} - self._assertNotDenied("https://deny.org", deny, inline_override=True) - self._assertDenied("https://normally-denied.org", deny, inline_override=True) diff --git a/tests/tests/test_options/test_option_formats.py b/tests/tests/test_options/test_option_formats.py index 8853e23..cc3ec0c 100644 --- a/tests/tests/test_options/test_option_formats.py +++ b/tests/tests/test_options/test_option_formats.py @@ -20,7 +20,8 @@ def test_flat_settings(self): settings.WEBMENTIONS_DEFAULT_URL_PARAMETER_MAPPING = {"foo": "bar"} settings.WEBMENTIONS_DOMAINS_OUTGOING_ALLOW = {"include"} settings.WEBMENTIONS_DOMAINS_OUTGOING_DENY = {"exclude"} - settings.WEBMENTIONS_DOMAINS_OUTGOING_OVERRIDE_TAG = "wm-override" + settings.WEBMENTIONS_DOMAINS_OUTGOING_TAG_ALLOW = "wm-send" + settings.WEBMENTIONS_DOMAINS_OUTGOING_TAG_DENY = "wm-nosend" settings.WEBMENTIONS_INCOMING_TARGET_MODEL_REQUIRED = True settings.WEBMENTIONS_MAX_RETRIES = 25 settings.WEBMENTIONS_RETRY_INTERVAL = 30 @@ -42,7 +43,8 @@ def test_flat_settings(self): self.assertDictEqual(options.default_url_parameter_mapping(), dict(foo="bar")) self.assertSetEqual(options.outgoing_domains_allow(), {"include"}) self.assertSetEqual(options.outgoing_domains_deny(), {"exclude"}) - self.assertEqual(options.outgoing_domains_override_tag(), "wm-override") + self.assertEqual(options.outgoing_domains_tag_allow(), "wm-send") + self.assertEqual(options.outgoing_domains_tag_deny(), "wm-nosend") self.assertEqual(options.user_agent(), "empty") def test_namespaced_settings(self): @@ -56,7 +58,8 @@ def test_namespaced_settings(self): "DEFAULT_URL_PARAMETER_MAPPING": {"bar": "foo"}, "DOMAINS_OUTGOING_ALLOW": ("included",), "DOMAINS_OUTGOING_DENY": ["excluded"], - "DOMAINS_OUTGOING_OVERRIDE_TAG": "override-wm", + "DOMAINS_OUTGOING_TAG_ALLOW": "send-wm", + "DOMAINS_OUTGOING_TAG_DENY": "nosend-wm", "INCOMING_TARGET_MODEL_REQUIRED": False, "MAX_RETRIES": 26, "RETRY_INTERVAL": 31, @@ -79,5 +82,6 @@ def test_namespaced_settings(self): self.assertDictEqual(options.default_url_parameter_mapping(), dict(bar="foo")) self.assertSetEqual(options.outgoing_domains_allow(), {"included"}) self.assertSetEqual(options.outgoing_domains_deny(), {"excluded"}) - self.assertEqual(options.outgoing_domains_override_tag(), "override-wm") + self.assertEqual(options.outgoing_domains_tag_allow(), "send-wm") + self.assertEqual(options.outgoing_domains_tag_deny(), "nosend-wm") self.assertEqual(options.user_agent(), "nope") diff --git a/tests/tests/test_parsing/html/test_find_target_links.py b/tests/tests/test_parsing/html/test_find_target_links.py index 5e4a3a5..0c1a707 100644 --- a/tests/tests/test_parsing/html/test_find_target_links.py +++ b/tests/tests/test_parsing/html/test_find_target_links.py @@ -121,55 +121,83 @@ def test_with_domains_deny(self): }, ) - def test_override_allow(self): - attr = "wm-deny" + def test_with_no_domains_and_tag_override(self): + send_tag = "wm-send" + nosend_tag = "wm-nosend" + html = f""" + + + + + + """ - def _get(html: str): - return get_target_links_in_html( + self.assertSetEqual( + get_target_links_in_html( html, "", - domains_allow=["allow.org"], + domains_allow=None, domains_deny=None, - domain_override_attr=attr, - ) + domains_deny_tag=nosend_tag, + ), + {"https://allow.org/by-default/", "https://attr.org/unaffected_by_tag"}, + ) + + def test_with_domains_allow_and_tag_override(self): + send_tag = "wm_send" + nosend_tag = "wm-no-send" + html = f""" + + + + + + """ self.assertSetEqual( - _get(""" unrelated-attr"""), - {"https://allow.org"}, - ) - self.assertSetEqual(_get(f""""""), set()) - self.assertSetEqual( - _get(f""""""), set() - ) - self.assertSetEqual( - _get(f""""""), - set(), - ) - - def test_override_deny(self): - attr = "wm-allow" - - def _get(html: str): - return get_target_links_in_html( + get_target_links_in_html( html, "", - domains_allow=None, - domains_deny=["deny.org"], - domain_override_attr=attr, - ) + domains_allow={ + "allow.org", + "attr.org", + "class.org", + "data.org", + }, + domains_deny=None, + domains_deny_tag=nosend_tag, + ), + {"https://allow.org", "https://allow.org/unaffected-by-tag/"}, + ) + + def test_with_domains_deny_and_tag_override(self): + send_tag = "wm-send" + nosend_tag = "wm-no_send" + html = f""" + + + + + + """ self.assertSetEqual( - _get(""""""), set() - ) - self.assertSetEqual( - _get(f""""""), - {"https://deny.org"}, - ) - self.assertSetEqual( - _get(f""""""), - {"https://deny.org"}, - ) - self.assertSetEqual( - _get(f""""""), - {"https://deny.org"}, + get_target_links_in_html( + html, + "", + domains_allow=None, + domains_deny={ + "deny.org", + "attr.org", + "class.org", + "data.org", + }, + domains_allow_tag=send_tag, + domains_deny_tag=nosend_tag, + ), + { + "https://class.org/allow-by-override", + "https://data.org/allow-by-override", + "https://attr.org/allow-by-override", + }, )