Files
dgtlmoonandClaude Opus 5.5 352b036217
Build and push containers / metadata (push) Canceled after 0s
Build and push containers / build-push-containers (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/amd64 (alpine) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm64 (alpine) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/amd64 (main) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm/v7 (main) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm/v8 (main) (push) Canceled after 0s
ChangeDetection.io Container Build Test / Build linux/arm64 (main) (push) Canceled after 0s
ChangeDetection.io App Test / lint-code (push) Canceled after 0s
ChangeDetection.io App Test / lint-translations (push) Canceled after 0s
ChangeDetection.io App Test / lint-template-i18n (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-11 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-12 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-13 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-14 (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built package works basically. (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Canceled after 0s
API - Reject broken Jinja2 templates in url, link_to_open, body and headers (shared with the edit form) (#4587)
* API - Reject broken Jinja2 in url, link_to_open, body and headers, shared validation with the edit form

The edit form validated Jinja2 templates on save but the API did not, so a broken body or
header template saved through the API only failed at fetch time, on every check.

- New jinja2_custom/validate.py: validate_jinja2_fields() renders each value that matches
  JINJA2_MARKER_PATTERN and returns {field: translated error}
- The edit form uses it instead of four copies of the same try/except
- API create and update return 400 naming the field

Translations: messages moved to the new file (msgids unchanged); also translated the header
error for fr/it and fixed the cs/es wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* rebuild

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:15:52 +02:00

53 lines
2.2 KiB
Python

"""
Validate the Jinja2 templates a user can put in a watch's URL, "Link to Open", request body and headers.
Shared by the edit form and the API, so both reject the same broken templates on save instead of
letting the watch fail on every check. Only values that look like Jinja2 (JINJA2_MARKER_PATTERN) are
rendered, the same rule the fetch uses, so literal text such as URL-encoded JSON ({%22) is left alone.
"""
from loguru import logger
from . import JINJA2_MARKER_PATTERN, render as jinja_render
def _render_exception(value):
"""Return the exception raised rendering value as a Jinja2 template, or None if it is fine or not a template."""
if not isinstance(value, str) or not JINJA2_MARKER_PATTERN.search(value):
return None
try:
jinja_render(template_str=value)
except Exception as e:
logger.error(e)
return e
return None
def validate_jinja2_fields(url=None, link_to_open=None, body=None, headers=None) -> dict:
"""Render every templated value and collect the failures.
Returns {field_name: translated error message}, keyed by the watch field name, empty when everything is valid.
"""
from flask_babel import gettext
errors = {}
for field_name, value in (('url', url), ('link_to_open', link_to_open), ('body', body)):
e = _render_exception(value)
if isinstance(e, ModuleNotFoundError):
# incase jinja2_time or others is missing
errors[field_name] = gettext('Invalid template syntax configuration: %(error)s') % {'error': e}
elif e:
errors[field_name] = gettext('Invalid template syntax: %(error)s') % {'error': e}
for header, value in (headers or {}).items():
e = _render_exception(value)
if isinstance(e, ModuleNotFoundError):
errors['headers'] = gettext('Invalid template syntax configuration: %(error)s') % {'error': e}
elif e:
errors['headers'] = gettext('Invalid template syntax in \"%(header)s\" header: %(error)s') % {'header': header, 'error': e}
if e:
# Report the first broken header only, by name, so the user knows which one to fix
break
return errors