Compare commits

..

3 Commits

Author SHA1 Message Date
dgtlmoon ab7391c799 rebuild translations 2026-05-19 18:05:25 +02:00
dgtlmoon 2c6ee825fc Also gate in worker 2026-05-19 18:02:20 +02:00
dgtlmoon 5976f671e4 LLM - Master on/off switch (enable/disable) 2026-05-19 17:58:32 +02:00
7 changed files with 12 additions and 51 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
# Semver means never use .01, or 00. Should be .1.
__version__ = '0.55.5'
__version__ = '0.55.4'
from changedetectionio.strtobool import strtobool
from json.decoder import JSONDecodeError
+2 -15
View File
@@ -364,10 +364,6 @@ def process_notification(n_object: NotificationContextData, datastore):
# Should always be false for 'text' mode or its too hard to read
# But otherwise, this could be some setting
word_diff=False if requested_output_format_original == 'text' else True,
# HTML-format notifications must escape diff content (GHSA-q8xq-qg4x-wphg).
# FormattableDiff/Extract escape internally so {{ diff(...) }} stays callable —
# the post-Jinja escape loop below would otherwise convert them to plain str.
escape_output='html' in requested_output_format,
)
)
@@ -398,19 +394,10 @@ def process_notification(n_object: NotificationContextData, datastore):
# so they survive escape and are still replaced with <span> tags later.
if 'html' in requested_output_format:
from markupsafe import escape as html_escape
from changedetectionio.notification_service import FormattableDiff, FormattableExtract
_page_content_keys = {'raw_diff', 'current_snapshot', 'prev_snapshot', 'triggered_text'}
for key in [k for k in notification_parameters if k.startswith('diff') or k in _page_content_keys]:
value = notification_parameters.get(key)
if not value:
continue
# FormattableDiff / FormattableExtract are callable str subclasses — {{ diff(lines=5) }}
# etc. relies on __call__. Wrapping them with str(html_escape(...)) here would lose
# __call__ and break those tokens. They escape internally via escape_output=True
# (set by add_rendered_diff_to_notification_vars above) for both __str__ and __call__.
if isinstance(value, (FormattableDiff, FormattableExtract)):
continue
notification_parameters[key] = str(html_escape(str(value)))
if notification_parameters.get(key):
notification_parameters[key] = str(html_escape(str(notification_parameters[key])))
with (apprise.LogCapture(level=apprise.logging.DEBUG) as logs):
for url in n_object['notification_urls']:
+5 -25
View File
@@ -99,7 +99,7 @@ class FormattableExtract(str):
Multiple changed fragments are joined with newlines.
Being a str subclass means it is natively JSON serializable.
"""
def __new__(cls, prev_snapshot, current_snapshot, extract_fn, escape_output=False):
def __new__(cls, prev_snapshot, current_snapshot, extract_fn):
if prev_snapshot or current_snapshot:
from changedetectionio import diff as diff_module
# word_diff=True is required — placemarker extraction regexes only exist in word-diff output
@@ -107,12 +107,6 @@ class FormattableExtract(str):
extracted = extract_fn(raw)
else:
extracted = ''
if escape_output and extracted:
# Placemarkers (@removed_PLACEMARKER_OPEN etc) contain no HTML chars,
# so html_escape leaves them intact — they still get swapped to <span>
# tags later by apply_service_tweaks. See GHSA-q8xq-qg4x-wphg.
from markupsafe import escape as html_escape
extracted = str(html_escape(extracted))
instance = super().__new__(cls, extracted)
return instance
@@ -134,23 +128,16 @@ class FormattableDiff(str):
Being a str subclass means it is natively JSON serializable.
"""
def __new__(cls, prev_snapshot, current_snapshot, escape_output=False, **base_kwargs):
def __new__(cls, prev_snapshot, current_snapshot, **base_kwargs):
if prev_snapshot or current_snapshot:
from changedetectionio import diff as diff_module
rendered = diff_module.render_diff(prev_snapshot, current_snapshot, **base_kwargs)
else:
rendered = ''
if escape_output and rendered:
# Placemarkers (@removed_PLACEMARKER_OPEN etc) contain no HTML chars,
# so html_escape leaves them intact — they still get swapped to <span>
# tags later by apply_service_tweaks. See GHSA-q8xq-qg4x-wphg.
from markupsafe import escape as html_escape
rendered = str(html_escape(rendered))
instance = super().__new__(cls, rendered)
instance._prev = prev_snapshot
instance._current = current_snapshot
instance._base_kwargs = base_kwargs
instance._escape_output = escape_output
return instance
def __call__(self, lines=None, added_only=False, removed_only=False, context=0,
@@ -176,10 +163,6 @@ class FormattableDiff(str):
if lines is not None:
result = '\n'.join(result.splitlines()[:int(lines)])
if self._escape_output and result:
from markupsafe import escape as html_escape
result = str(html_escape(result))
return result
@@ -253,7 +236,7 @@ class NotificationContextData(dict):
super().__setitem__(key, value)
def add_rendered_diff_to_notification_vars(notification_scan_text:str, prev_snapshot:str, current_snapshot:str, word_diff:bool, escape_output:bool=False):
def add_rendered_diff_to_notification_vars(notification_scan_text:str, prev_snapshot:str, current_snapshot:str, word_diff:bool):
"""
Efficiently renders only the diff placeholders that are actually used in the notification text.
@@ -266,9 +249,6 @@ def add_rendered_diff_to_notification_vars(notification_scan_text:str, prev_snap
prev_snapshot: Previous version of content for diff comparison
current_snapshot: Current version of content for diff comparison
word_diff: Whether to use word-level (True) or line-level (False) diffing
escape_output: If True, the rendered diff output is HTML-escaped. Used for HTML-format
notifications so attacker-controlled page content can't inject live markup.
Both the cached str representation and the result of {{ diff(...) }} calls are escaped.
Returns:
dict: Only the diff placeholders that were found in notification_scan_text, with rendered content
@@ -307,10 +287,10 @@ def add_rendered_diff_to_notification_vars(notification_scan_text:str, prev_snap
if not re.search(pattern, notification_scan_text, re.IGNORECASE):
continue
if key in diff_specs:
ret[key] = FormattableDiff(prev_snapshot, current_snapshot, escape_output=escape_output, **diff_specs[key])
ret[key] = FormattableDiff(prev_snapshot, current_snapshot, **diff_specs[key])
rendered_count += 1
elif key in extract_specs:
ret[key] = FormattableExtract(prev_snapshot, current_snapshot, extract_fn=extract_specs[key], escape_output=escape_output)
ret[key] = FormattableExtract(prev_snapshot, current_snapshot, extract_fn=extract_specs[key])
rendered_count += 1
if rendered_count:
@@ -634,12 +634,6 @@ def _test_color_notifications(client, notification_body_token, datastore_path):
def test_html_color_notifications(client, live_server, measure_memory_usage, datastore_path):
_test_color_notifications(client, '{{diff}}',datastore_path=datastore_path)
_test_color_notifications(client, '{{diff_full}}',datastore_path=datastore_path)
# Regression: the html-output escape pass in handler.py used to convert
# FormattableDiff into a plain str, stripping its __call__ and breaking any
# {{ diff(...) }} / {{ diff_added(...) }} token on htmlcolor/html notifications
# with 'str' object is not callable (see commit 08d30c6 + #3923).
# word_diff=false reproduces the exact form the user-reported failure used.
_test_color_notifications(client, '{{diff(word_diff=false)}}', datastore_path=datastore_path)
def _test_custom_html_in_notification_body_not_escaped(client, datastore_path, content_type=None):
@@ -2318,11 +2318,11 @@ msgstr "Último Comprobado"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Changed"
msgstr "Cambiado"
msgstr "Cambiadp"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Last Changed"
msgstr "Último Cambiado"
msgstr "Último Cambiadp"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "No web page change detection watches configured, please add a URL in the box above, or"
+2 -2
View File
@@ -6,9 +6,9 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: changedetection.io 0.55.5\n"
"Project-Id-Version: changedetection.io 0.55.4\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-05-19 19:05+0200\n"
"POT-Creation-Date: 2026-05-19 18:05+0200\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"