393 files

This commit is contained in:
Home Assistant Version Control
2026-09-27 17:16:54 +00:00
parent b05b7a897e
commit 39d97ac2db
404 changed files with 54006 additions and 11034 deletions
@@ -13,11 +13,15 @@ from homeassistant.core import HomeAssistant
from ..const import (
CONF_TASKS,
TRIGGER_FIELD_RANGES,
TRIGGER_RUNTIME_HOURS_MAX,
UNAVAILABLE_STATES,
TriggerType,
)
from ..helpers.aggregate import object_name
from ..helpers.trigger_fallback import threshold_limits_overlap
from ..helpers.url_safety import _SAFE_URL_SCHEMES as _SAFE_URL_SCHEMES
from ..helpers.url_safety import is_safe_url
from . import (
_get_object_entries,
)
@@ -26,36 +30,8 @@ from . import (
# Validation helpers
# ---------------------------------------------------------------------------
_SAFE_URL_SCHEMES = {"http", "https"}
def _is_safe_url(url: str | None) -> bool:
"""Reject javascript:, data:, protocol-relative and other dangerous URLs.
Only http/https and genuine path-relative URLs (no host) pass. ASCII control
characters and surrounding whitespace are stripped first, since urlparse and
browsers ignore them and they can otherwise mask a "//host" or scheme-less
host (e.g. ``" //evil.com"`` or ``"\t//evil.com"``).
"""
if not url:
return True
from urllib.parse import urlparse
cleaned = "".join(ch for ch in url if ch.isprintable()).strip()
if not cleaned:
return True
# Block protocol-relative URLs like //evil.com
if cleaned.startswith("//"):
return False
try:
parsed = urlparse(cleaned)
except Exception: # noqa: BLE001 - any malformed URL is rejected as unsafe
return False
scheme = parsed.scheme.lower()
if scheme in _SAFE_URL_SCHEMES:
return True
# An empty scheme is only safe for a true path-relative URL with no host.
return scheme == "" and not parsed.netloc
# The URL rule lives in helpers.url_safety (every write path shares it).
_is_safe_url = is_safe_url
# ---------------------------------------------------------------------------
@@ -285,12 +261,10 @@ _NUMBER_FIELDS: tuple[str, ...] = (
"trigger_target_value",
"trigger_baseline_value",
)
# Whole-number fields with an inclusive range.
_INT_FIELDS: dict[str, tuple[int, int]] = {
"trigger_for_minutes": (0, 1440),
"trigger_target_changes": (1, 10_000),
}
TRIGGER_RUNTIME_HOURS_MAX = 100_000
# Whole-number fields with an inclusive range — the bounds the options-flow
# selectors use too (const.TRIGGER_FIELD_RANGES, DRY audit 2026-09-26 B).
# TRIGGER_RUNTIME_HOURS_MAX is imported from const and re-exported here.
_INT_FIELDS: dict[str, tuple[int, int]] = TRIGGER_FIELD_RANGES
# Optional fields where an explicit null means "unset" — dropped rather than
# refused, so a client clearing a field never trips the validator.
_OPTIONAL_VALUE_FIELDS: tuple[str, ...] = (