mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-06-04 07:51:32 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 155937b220 | |||
| 3b88e14f60 | |||
| 7839b247a2 | |||
| de92b6f361 | |||
| 94a373ecee | |||
| 4f5a928413 | |||
| ecb1debf1b | |||
| 8525a4af37 | |||
| 536b626cf0 | |||
| 043eecc7ef | |||
| 73d2c0a16c |
@@ -31,15 +31,33 @@ jobs:
|
||||
echo "Checking $f"
|
||||
msgfmt --check-format -o /dev/null "$f"
|
||||
done
|
||||
- name: Lint .pot template with dennis
|
||||
- name: Lint .po/.pot files with dennis (errors only)
|
||||
run: |
|
||||
pip install "$(grep -E '^dennis ?>=' requirements.txt)"
|
||||
dennis-cmd lint --strict changedetectionio/translations/messages.pot
|
||||
- name: Lint .po files with dennis
|
||||
dennis-cmd lint --errorsonly changedetectionio/translations/
|
||||
- name: Lint .pot template with dennis (warnings)
|
||||
run: |
|
||||
dennis-cmd lint --strict --excluderules=W302 changedetectionio/translations/*/LC_MESSAGES/messages.po
|
||||
output=$(dennis-cmd lint changedetectionio/translations/messages.pot)
|
||||
echo "$output"
|
||||
warnings=$(echo "$output" | awk '/Warnings:/ {print $NF; exit}')
|
||||
if (( ${warnings:-0} > 0 )); then
|
||||
echo "ERROR: ${warnings} dennis warning(s) detected in messages.pot"
|
||||
echo "Fix the warning(s)."
|
||||
exit 1
|
||||
fi
|
||||
- name: Lint .po files with dennis (warnings)
|
||||
# W302 (unchanged) is excluded due to high false-positive rate in this codebase:
|
||||
# many msgstrs intentionally match msgid (units like "AI", "LLM", and proper nouns).
|
||||
run: |
|
||||
output=$(dennis-cmd lint --excluderules=W302 \
|
||||
changedetectionio/translations/*/LC_MESSAGES/messages.po)
|
||||
echo "$output"
|
||||
warnings=$(echo "$output" | awk '/Total number of warnings:/ {print $NF; exit}')
|
||||
if (( ${warnings:-0} > 0 )); then
|
||||
echo "ERROR: ${warnings} dennis warning(s) detected in .po files"
|
||||
echo "Fix the warning(s)."
|
||||
exit 1
|
||||
fi
|
||||
- name: Check translation catalog is up-to-date
|
||||
run: |
|
||||
pip install "$(grep -E '^babel==' requirements.txt)"
|
||||
|
||||
@@ -7,19 +7,3 @@ repos:
|
||||
args: [--fix]
|
||||
# Fomrat
|
||||
- id: ruff-format
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: dennis-lint-pot
|
||||
name: dennis lint pot
|
||||
language: system
|
||||
entry: dennis-cmd lint --strict
|
||||
files: ^changedetectionio/translations/messages\.pot$
|
||||
pass_filenames: true
|
||||
|
||||
- id: dennis-lint-po
|
||||
name: dennis lint po
|
||||
language: system
|
||||
entry: dennis-cmd lint --strict --excluderules=W302
|
||||
files: ^changedetectionio/translations/\w+/LC_MESSAGES/messages\.po$
|
||||
pass_filenames: true
|
||||
|
||||
@@ -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.7'
|
||||
__version__ = '0.55.5'
|
||||
|
||||
from changedetectionio.strtobool import strtobool
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
@@ -99,12 +99,6 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
|
||||
llm_form_input = dict(form.data.get('llm') or {})
|
||||
|
||||
# Empty IntegerField submissions come back as None from WTForms;
|
||||
# the schema declares those fields as strict `int`, so passing
|
||||
# them through would fail validation. Treat None like the
|
||||
# absent-key case: keep the stored value, don't merge.
|
||||
llm_form_input = {k: v for k, v in llm_form_input.items() if v is not None}
|
||||
|
||||
# PasswordField never re-renders, so a blank submitted value means
|
||||
# "keep stored key" — drop it from the merge.
|
||||
if not (llm_form_input.get('api_key') or '').strip():
|
||||
|
||||
@@ -9,7 +9,7 @@ import asyncio
|
||||
from changedetectionio import strtobool
|
||||
from changedetectionio.content_fetchers.exceptions import BrowserStepsInUnsupportedFetcher, EmptyReply, Non200ErrorCodeReceived
|
||||
from changedetectionio.content_fetchers.base import Fetcher
|
||||
from changedetectionio.validate_url import is_private_hostname, is_url_private_or_parser_confused
|
||||
from changedetectionio.validate_url import is_private_hostname
|
||||
|
||||
|
||||
# "html_requests" is listed as the default fetcher in store.py!
|
||||
@@ -87,12 +87,10 @@ class fetcher(Fetcher):
|
||||
|
||||
try:
|
||||
# Fresh DNS check at fetch time — catches DNS rebinding regardless of add-time cache.
|
||||
# Validates every hostname both urlparse and urllib3 see, so parser-differential
|
||||
# payloads (GHSA-rph4-96w6-q594) cannot smuggle an internal target past the gate.
|
||||
if not allow_iana_restricted:
|
||||
if is_url_private_or_parser_confused(url):
|
||||
raise Exception(f"Fetch blocked: '{url}' resolves to a private/reserved IP address "
|
||||
f"or contains a parser-differential payload. "
|
||||
parsed_initial = urlparse(url)
|
||||
if parsed_initial.hostname and is_private_hostname(parsed_initial.hostname):
|
||||
raise Exception(f"Fetch blocked: '{url}' resolves to a private/reserved IP address. "
|
||||
f"Set ALLOW_IANA_RESTRICTED_ADDRESSES=true to allow.")
|
||||
|
||||
r = session.request(method=request_method,
|
||||
@@ -113,9 +111,9 @@ class fetcher(Fetcher):
|
||||
location = r.headers.get('Location', '')
|
||||
redirect_url = urljoin(current_url, location)
|
||||
if not allow_iana_restricted:
|
||||
if is_url_private_or_parser_confused(redirect_url):
|
||||
raise Exception(f"Redirect blocked: '{redirect_url}' resolves to a private/reserved IP address "
|
||||
f"or contains a parser-differential payload.")
|
||||
parsed_redirect = urlparse(redirect_url)
|
||||
if parsed_redirect.hostname and is_private_hostname(parsed_redirect.hostname):
|
||||
raise Exception(f"Redirect blocked: '{redirect_url}' resolves to a private/reserved IP address.")
|
||||
current_url = redirect_url
|
||||
r = session.request('GET', redirect_url,
|
||||
headers=request_headers,
|
||||
|
||||
@@ -887,6 +887,7 @@ class processor_text_json_diff_form(commonSettingsForm):
|
||||
|
||||
conditions_match_logic = RadioField(_l('Match'), choices=[('ALL', _l('Match all of the following')),('ANY', _l('Match any of the following'))], default='ALL')
|
||||
conditions = FieldList(FormField(ConditionFormRow), min_entries=1) # Add rule logic here
|
||||
# dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
use_page_title_in_list = TernaryNoneBooleanField(_l('Use page <title> in list'), default=None)
|
||||
|
||||
history_snapshot_max_length = IntegerField(_l('Number of history items per watch to keep'), render_kw={"style": "width: 5em;"}, validators=[validators.Optional(), validators.NumberRange(min=2)])
|
||||
@@ -1035,6 +1036,7 @@ class globalSettingsApplicationUIForm(Form):
|
||||
open_diff_in_new_tab = BooleanField(_l("Open 'History' page in a new tab"), default=True, validators=[validators.Optional()])
|
||||
socket_io_enabled = BooleanField(_l('Realtime UI Updates Enabled'), default=True, validators=[validators.Optional()])
|
||||
favicons_enabled = BooleanField(_l('Favicons Enabled'), default=True, validators=[validators.Optional()])
|
||||
# dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
use_page_title_in_list = BooleanField(_l('Use page <title> in watch overview list')) #BooleanField=True
|
||||
|
||||
# datastore.data['settings']['application']..
|
||||
|
||||
@@ -82,23 +82,10 @@ def _check_input_size(text: str, max_chars: int) -> None:
|
||||
|
||||
def _thinking_extra_body(model: str, budget: int) -> dict | None:
|
||||
"""Return litellm extra_body to control thinking for models that support it.
|
||||
|
||||
The `thinkingConfig.thinkingBudget` payload is Gemini-specific (Anthropic and
|
||||
OpenAI reasoning models use different parameters), so we gate on the gemini/
|
||||
provider prefix first, then defer to litellm's model registry for the actual
|
||||
"does this model think?" decision. That picks up new Gemini variants and
|
||||
rolling aliases (`gemini-flash-latest`, etc.) as litellm's registry tracks
|
||||
them, without us hardcoding model names here.
|
||||
For Gemini 2.5+: passes thinkingConfig with the given budget (0 = disabled).
|
||||
For all other models: returns None (no-op).
|
||||
"""
|
||||
if not model.startswith('gemini/'):
|
||||
return None
|
||||
try:
|
||||
import litellm
|
||||
if not litellm.get_model_info(model).get('supports_reasoning'):
|
||||
return None
|
||||
except Exception:
|
||||
# Unknown model or registry lookup failed — skip the thinking config
|
||||
# rather than guess. Worst case: thinking stays at the provider default.
|
||||
if not model.startswith('gemini/gemini-2.5'):
|
||||
return None
|
||||
return {'generationConfig': {'thinkingConfig': {'thinkingBudget': budget}}}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ from apprise.utils.logic import dict_full_update
|
||||
from loguru import logger
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from changedetectionio.validate_url import is_private_hostname, is_url_private_or_parser_confused
|
||||
from changedetectionio.validate_url import is_private_hostname
|
||||
|
||||
SUPPORTED_HTTP_METHODS = {"get", "post", "put", "delete", "patch", "head"}
|
||||
|
||||
@@ -198,14 +198,12 @@ def apprise_http_custom_handler(
|
||||
|
||||
url = re.sub(rf"^{schema}", "https" if schema.endswith("s") else "http", parsed_url.get("url"))
|
||||
|
||||
# SSRF protection — block private/loopback addresses unless explicitly allowed.
|
||||
# Uses parser-agnostic check so urlparse/urllib3 differentials (GHSA-rph4-96w6-q594)
|
||||
# can't smuggle an internal target past the gate.
|
||||
# SSRF protection — block private/loopback addresses unless explicitly allowed
|
||||
if not os.getenv('ALLOW_IANA_RESTRICTED_ADDRESSES', '').lower() in ('true', '1', 'yes'):
|
||||
if is_url_private_or_parser_confused(url):
|
||||
hostname = urlparse(url).hostname or ''
|
||||
if hostname and is_private_hostname(hostname):
|
||||
raise ValueError(
|
||||
f"Notification target '{url}' is a private/reserved address "
|
||||
f"or contains a parser-differential payload. "
|
||||
f"Notification target '{hostname}' is a private/reserved address. "
|
||||
f"Set ALLOW_IANA_RESTRICTED_ADDRESSES=true to allow."
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import hashlib
|
||||
from changedetectionio.browser_steps.browser_steps import browser_steps_get_valid_steps
|
||||
from changedetectionio.content_fetchers.base import Fetcher
|
||||
from changedetectionio.strtobool import strtobool
|
||||
from changedetectionio.validate_url import is_private_hostname, is_url_private_or_parser_confused
|
||||
from changedetectionio.validate_url import is_private_hostname
|
||||
from copy import deepcopy
|
||||
from abc import abstractmethod
|
||||
import os
|
||||
@@ -104,13 +104,13 @@ class difference_detection_processor():
|
||||
"""
|
||||
if strtobool(os.getenv('ALLOW_IANA_RESTRICTED_ADDRESSES', 'false')):
|
||||
return
|
||||
parsed = urlparse(self.watch.link)
|
||||
if not parsed.hostname:
|
||||
return
|
||||
loop = asyncio.get_running_loop()
|
||||
# Use the parser-agnostic check so urlparse/urllib3 differentials (GHSA-rph4-96w6-q594)
|
||||
# can't slip a private/internal hostname past this pre-flight gate.
|
||||
if await loop.run_in_executor(None, is_url_private_or_parser_confused, self.watch.link):
|
||||
if await loop.run_in_executor(None, is_private_hostname, parsed.hostname):
|
||||
raise Exception(
|
||||
f"Fetch blocked: '{self.watch.link}' resolves to a private/reserved IP address "
|
||||
f"or contains a parser-differential payload. "
|
||||
f"Fetch blocked: '{self.watch.link}' resolves to a private/reserved IP address. "
|
||||
f"Set ALLOW_IANA_RESTRICTED_ADDRESSES=true to allow."
|
||||
)
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{{ '{{watch_title}}' }}</code></td>
|
||||
{# TRANSLATORS: dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213 #}
|
||||
<td>{{ _('The page title of the watch, uses <title> if not set, falls back to URL') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -196,81 +196,6 @@ def test_settings_form_preserves_token_counters(
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_settings_form_blank_llm_integer_fields_preserve_stored_values(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
Empty IntegerField submissions come back as None from WTForms. LLMSettings
|
||||
declares token_budget_month / max_input_chars / max_tokens_per_count_period /
|
||||
local_token_multiplier as strict `int`, so a None passed through to
|
||||
model_validate raises ValidationError and 500s the settings save.
|
||||
|
||||
Regression for settings/__init__.py — the LLM merge must drop None values
|
||||
(treat them like absent keys) so blank IntegerField submissions preserve
|
||||
the stored value instead of crashing the form.
|
||||
"""
|
||||
ds = client.application.config.get('DATASTORE')
|
||||
ds.data['settings']['application']['llm'] = {
|
||||
'model': 'gpt-4o',
|
||||
'api_key': 'sk-existing',
|
||||
'token_budget_month': 50000,
|
||||
'max_input_chars': 200000,
|
||||
'max_tokens_per_count_period': 1000,
|
||||
'local_token_multiplier': 3,
|
||||
}
|
||||
|
||||
res = client.post(
|
||||
url_for('settings.settings_page'),
|
||||
data={
|
||||
'llm-model': 'gpt-4o',
|
||||
'llm-api_key': '',
|
||||
'llm-api_base': '',
|
||||
# The bug-trigger: every LLM IntegerField submitted blank
|
||||
'llm-token_budget_month': '',
|
||||
'llm-max_input_chars': '',
|
||||
'llm-max_tokens_per_count_period': '',
|
||||
'llm-local_token_multiplier': '',
|
||||
# Minimal required fields for the rest of the form to validate.
|
||||
# 'System default' is popped from notification_format choices for the
|
||||
# global form, so it must be one of the real codes (e.g. 'html').
|
||||
'application-pager_size': '50',
|
||||
'application-notification_format': 'html',
|
||||
'application-fetch_backend': 'html_requests',
|
||||
'application-rss_diff_length': '5',
|
||||
'application-filter_failure_notification_threshold_attempts': '0',
|
||||
'requests-time_between_check-days': '0',
|
||||
'requests-time_between_check-hours': '0',
|
||||
'requests-time_between_check-minutes': '5',
|
||||
'requests-time_between_check-seconds': '0',
|
||||
'requests-time_between_check-weeks': '0',
|
||||
'requests-jitter_seconds': '0',
|
||||
'requests-workers': '10',
|
||||
'requests-timeout': '60',
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert res.status_code == 200, \
|
||||
f"Settings save crashed on blank LLM IntegerField submission (got {res.status_code})"
|
||||
# Sanity: the form must have actually validated and reached the LLM save path
|
||||
# — without this the test would trivially pass because the buggy code never ran.
|
||||
assert b'Settings updated.' in res.data, \
|
||||
"Settings form did not validate — the bug-path was never exercised. Check fixture fields."
|
||||
body = res.data.decode('utf-8', errors='replace')
|
||||
assert 'ValidationError' not in body, \
|
||||
"Pydantic ValidationError leaked into the response — blank IntegerField wasn't filtered"
|
||||
|
||||
llm = ds.data['settings']['application'].get('llm') or {}
|
||||
assert llm.get('token_budget_month') == 50000, \
|
||||
f"Blank submission must preserve stored token_budget_month (got {llm.get('token_budget_month')!r})"
|
||||
assert llm.get('max_input_chars') == 200000, \
|
||||
f"Blank submission must preserve stored max_input_chars (got {llm.get('max_input_chars')!r})"
|
||||
assert llm.get('max_tokens_per_count_period') == 1000, \
|
||||
f"Blank submission must preserve stored max_tokens_per_count_period (got {llm.get('max_tokens_per_count_period')!r})"
|
||||
assert llm.get('local_token_multiplier') == 3, \
|
||||
f"Blank submission must preserve stored local_token_multiplier (got {llm.get('local_token_multiplier')!r})"
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_settings_form_cannot_inject_fake_token_counts(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
|
||||
@@ -760,9 +760,7 @@ def test_ssrf_private_ip_blocked(client, live_server, monkeypatch, measure_memor
|
||||
|
||||
f = RequestsFetcher()
|
||||
|
||||
# Patch the underlying is_private_hostname in validate_url — the fetcher now goes through
|
||||
# is_url_private_or_parser_confused() (GHSA-rph4-96w6-q594), which calls it transitively.
|
||||
with patch('changedetectionio.validate_url.is_private_hostname', return_value=True):
|
||||
with patch('changedetectionio.content_fetchers.requests.is_private_hostname', return_value=True):
|
||||
with pytest.raises(Exception, match='private/reserved'):
|
||||
f._run_sync(
|
||||
url='http://example.com/',
|
||||
@@ -786,7 +784,7 @@ def test_ssrf_private_ip_blocked(client, live_server, monkeypatch, measure_memor
|
||||
return hostname in {'169.254.169.254', '10.0.0.1', '172.16.0.1',
|
||||
'192.168.0.1', '127.0.0.1', '::1'}
|
||||
|
||||
with patch('changedetectionio.validate_url.is_private_hostname',
|
||||
with patch('changedetectionio.content_fetchers.requests.is_private_hostname',
|
||||
side_effect=_private_only_for_redirect):
|
||||
with patch('requests.Session.request', return_value=mock_redirect):
|
||||
with pytest.raises(Exception, match='Redirect blocked'):
|
||||
@@ -831,113 +829,6 @@ def test_unresolvable_hostname_is_allowed(client, live_server, monkeypatch):
|
||||
"Unresolvable hostname watch should appear in the watch overview list"
|
||||
|
||||
|
||||
def test_ghsa_rph4_96w6_q594_urlparse_urllib3_parser_differential_ssrf(client, live_server, monkeypatch, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
GHSA-rph4-96w6-q594: SSRF via urlparse/urllib3 parser differential.
|
||||
|
||||
A URL like http://INTERNAL:8888\\@PUBLIC/ is parsed two different ways:
|
||||
- urlparse() treats \\@ as a credential separator → hostname = PUBLIC
|
||||
- urllib3 treats \\ as a path character → hostname = INTERNAL
|
||||
The pre-fetch SSRF check used urlparse(), but requests/urllib3 actually connected
|
||||
to INTERNAL. Fix: parser-agnostic gate that (a) blocks any URL containing a
|
||||
backslash and (b) validates every hostname both parsers produce.
|
||||
|
||||
Covers:
|
||||
1. extract_url_hostnames() reveals BOTH hostnames for the payload
|
||||
2. is_url_private_or_parser_confused() blocks backslash payloads outright
|
||||
3. is_safe_valid_url() rejects backslash payloads at add-time
|
||||
4. The /api/v1/watch add endpoint rejects the payload
|
||||
5. The requests fetcher refuses the payload at fetch-time
|
||||
6. The redirect-following loop refuses a backslash payload in Location
|
||||
"""
|
||||
from unittest.mock import patch, MagicMock
|
||||
from changedetectionio.validate_url import (
|
||||
extract_url_hostnames,
|
||||
is_safe_valid_url,
|
||||
is_url_private_or_parser_confused,
|
||||
)
|
||||
|
||||
monkeypatch.setenv('ALLOW_IANA_RESTRICTED_ADDRESSES', 'false')
|
||||
|
||||
# The published proof-of-concept payload — backslash splits the two parsers' views.
|
||||
payload = "http://169.254.169.254:8888" + chr(92) + "@httpbin.org/latest/meta-data/"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 1. extract_url_hostnames() returns BOTH parsers' hostnames
|
||||
# ---------------------------------------------------------------
|
||||
hosts = extract_url_hostnames(payload)
|
||||
assert '169.254.169.254' in hosts, \
|
||||
f"urllib3 sees 169.254.169.254 as the connect target; extract_url_hostnames must surface it. Got {hosts!r}"
|
||||
assert 'httpbin.org' in hosts, \
|
||||
f"urlparse sees httpbin.org; extract_url_hostnames must surface it too. Got {hosts!r}"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 2. Parser-agnostic gate blocks the payload
|
||||
# ---------------------------------------------------------------
|
||||
assert is_url_private_or_parser_confused(payload), \
|
||||
"Parser-differential payload must be blocked by the SSRF gate"
|
||||
|
||||
# And a plain backslash anywhere in the URL is enough to block, even without a private IP
|
||||
assert is_url_private_or_parser_confused("http://example.com" + chr(92) + "@evil.com/"), \
|
||||
"Any backslash in a URL must trigger the parser-differential block"
|
||||
|
||||
# Sanity: a regular public URL is not blocked
|
||||
assert not is_url_private_or_parser_confused("http://example.com/path"), \
|
||||
"Plain public URLs must continue to pass the gate"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 3. is_safe_valid_url() rejects backslash payloads at add-time
|
||||
# ---------------------------------------------------------------
|
||||
assert not is_safe_valid_url(payload), \
|
||||
"is_safe_valid_url must reject URLs containing a backslash (parser-differential vector)"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 4. The watch-add API endpoint rejects the payload
|
||||
# ---------------------------------------------------------------
|
||||
api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
|
||||
res = client.post(
|
||||
url_for('createwatch'),
|
||||
data='{"url": "%s", "fetch_backend": "html_requests"}' % payload,
|
||||
headers={'x-api-key': api_key, 'Content-Type': 'application/json'},
|
||||
)
|
||||
assert res.status_code >= 400, \
|
||||
f"API must refuse to create a watch for parser-differential URL; got status {res.status_code} body {res.data!r}"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 5. Requests fetcher refuses the payload at fetch-time
|
||||
# ---------------------------------------------------------------
|
||||
from changedetectionio.content_fetchers.requests import fetcher as RequestsFetcher
|
||||
|
||||
f = RequestsFetcher()
|
||||
with pytest.raises(Exception, match='private/reserved|parser-differential'):
|
||||
f._run_sync(
|
||||
url=payload,
|
||||
timeout=5,
|
||||
request_headers={},
|
||||
request_body=None,
|
||||
request_method='GET',
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 6. A 302 Location header pointing at a backslash payload is blocked
|
||||
# (open-redirect → SSRF via parser differential)
|
||||
# ---------------------------------------------------------------
|
||||
mock_redirect = MagicMock()
|
||||
mock_redirect.is_redirect = True
|
||||
mock_redirect.status_code = 302
|
||||
mock_redirect.headers = {'Location': payload}
|
||||
|
||||
with patch('requests.Session.request', return_value=mock_redirect):
|
||||
with pytest.raises(Exception, match='Redirect blocked'):
|
||||
f._run_sync(
|
||||
url='http://example.com/',
|
||||
timeout=5,
|
||||
request_headers={},
|
||||
request_body=None,
|
||||
request_method='GET',
|
||||
)
|
||||
|
||||
|
||||
def test_ghsa_8757_69j2_hx56_backup_restore_history_path_traversal(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
GHSA-8757-69j2-hx56: Crafted backup ZIP with absolute path in history.txt must not
|
||||
|
||||
@@ -247,31 +247,34 @@ dennis-cmd lint --excluderules=W302 changedetectionio/translations/
|
||||
|
||||
The `W303` rule ensures that HTML tags in the `msgstr` match the `msgid`. This is crucial for catching broken markup (e.g., missing closing tags).
|
||||
|
||||
##### Handling intentional deviations
|
||||
##### Handling intentional deviations and false positives
|
||||
|
||||
Some W303 warnings are intentional.
|
||||
Some W303 warnings are intentional or result from upstream false positives.
|
||||
Use the `dennis-ignore: W303` comment in the source files (templates or Python code) within a `TRANSLATORS` comment to suppress these warnings.
|
||||
This ensures the ignore instruction is extracted into the `.po` files.
|
||||
|
||||
- **CJK italic policy**: When replacing `<i>` with locale-conventional quotation marks, tags will no longer match.
|
||||
- **Upstream false positive**: Dennis misinterprets certain HTML tags (e.g., `<title>`) within `msgstr`. See https://github.com/mozilla/dennis/issues/213.
|
||||
|
||||
**Examples in Jinja2 templates:**
|
||||
|
||||
```jinja
|
||||
{# TRANSLATORS: CJK fonts lack native italics; allow substitution with conventional local styling. dennis-ignore: W303 #}
|
||||
<p>{{ _('These settings are <strong><i>added</i></strong> to any existing watch configurations.')|safe }}</p>
|
||||
|
||||
{# TRANSLATORS: dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213 #}
|
||||
<td>{{ _('The page title of the watch, uses <title> if not set, falls back to URL') }}</td>
|
||||
```
|
||||
|
||||
**Example in Python source:**
|
||||
|
||||
```python
|
||||
# dennis-ignore: W303 - CJK fonts lack native italics; allow substitution with conventional local styling.
|
||||
message = StringField(_l('This is <i>experimental</i> and may change'))
|
||||
# dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
use_page_title_in_list = BooleanField(_l('Use page <title> in watch overview list'))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## CI linter
|
||||
|
||||
A GitHub Actions job (`lint-template-i18n`) checks for adjacent `{{ _(...) }}` calls on the same line
|
||||
|
||||
Binary file not shown.
@@ -584,7 +584,7 @@ msgstr "Pozn.: Toto je aplikováno globálně dodatečně k pravidlům nastaven
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/text-options.html
|
||||
msgid "Matching text will be ignored in the text snapshot (you can still see it but it wont trigger a change)"
|
||||
msgstr "Text shody bude ignorován ve snímku textu (bude i nadále vidět, ale nebude spouštět upozornění na změnu)"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/text-options.html
|
||||
msgid "Each line processed separately, any line matching will be ignored (removed before creating the checksum)"
|
||||
@@ -772,23 +772,23 @@ msgstr "Využití"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Overview"
|
||||
msgstr "Přehled"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Provider"
|
||||
msgstr "Poskytovatel"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Prompts"
|
||||
msgstr "Prompty"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Behaviour"
|
||||
msgstr "Chování"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "AI-powered change monitoring"
|
||||
msgstr "AI podporované sledování změn"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Connect an LLM to move from \"something changed\" to \"only the thing you care about changed\"."
|
||||
@@ -925,31 +925,31 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Load available models"
|
||||
msgstr "Načíst dostupné modely"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Available models"
|
||||
msgstr "Dostupné modely"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "choose a model"
|
||||
msgstr "vybrat model"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Enter API key and click 'Load available models'"
|
||||
msgstr "Vložit API klíč a kliknout na 'Načíst dostupné modely'"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Remove AI / LLM configuration?"
|
||||
msgstr "Odstranit AI / LLM konfiguraci?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "This will remove your saved AI provider, model, and API key."
|
||||
msgstr "Toto odstraní uloženého poskytovatele AI, model a API klíč."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Remove"
|
||||
msgstr "Odstranit"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html changedetectionio/templates/base.html
|
||||
@@ -958,7 +958,7 @@ msgstr "Zrušit"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Test connection"
|
||||
msgstr "Otestovat připojení"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid ""
|
||||
@@ -1294,7 +1294,7 @@ msgstr "Sledovat skupinu / Značka"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
msgid "Groups allows you to manage filters and notifications for multiple watches under a single organisational tag."
|
||||
msgstr "Skupiny umožňují spravovat filtry a upozornění pro vícero sledování seskupené pod jedním organizačním tagem."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
msgid "# Watches"
|
||||
@@ -1329,7 +1329,7 @@ msgstr "Smazat skupinu?"
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
#, python-format
|
||||
msgid "<p>Are you sure you want to delete group <strong>%(title)s</strong>?</p><p>This action cannot be undone.</p>"
|
||||
msgstr "<p>Opravdu chcete smazat skupinu <strong>%(title)s</strong>?</p><p>Tuto akci nelze vzít zpět.</p>"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
@@ -1350,8 +1350,6 @@ msgid ""
|
||||
"<p>Are you sure you want to unlink all watches from group <strong>%(title)s</strong>?</p><p>The tag will be kept but "
|
||||
"watches will be removed from it.</p>"
|
||||
msgstr ""
|
||||
"<p>Opravud chcete odpojit všechna sledování pod skupinou <strong>%(title)s</strong>?</p><p>Tag bude zachován, ale "
|
||||
"podřazená sledování budou odstraněna.</p>"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html
|
||||
msgid "Unlink"
|
||||
@@ -1393,22 +1391,22 @@ msgstr "{} sledování ztlumeno"
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "{} watches un-muted"
|
||||
msgstr "{} sledování zesíleno"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "{} watches queued for rechecking"
|
||||
msgstr "{} sledování ve frontě ke kontrole"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "{} watches errors cleared"
|
||||
msgstr "{} chyb sledování vyčištěno"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "{} watches cleared/reset."
|
||||
msgstr "{} sledování vyčištěno/resetováno."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
@@ -1418,7 +1416,7 @@ msgstr "{} monitorů nastaveno na použití výchozího nastavení oznámení"
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "{} watches were tagged"
|
||||
msgstr "{} sledování otagováno"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Watch not found"
|
||||
@@ -1435,7 +1433,7 @@ msgstr "jasný"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "History clearing started in background"
|
||||
msgstr "Čištění historie spuštěno na pozadí"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Incorrect confirmation text."
|
||||
@@ -1444,7 +1442,7 @@ msgstr "Žádné informace"
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "The watch by UUID {} does not exist."
|
||||
msgstr "Sledování s UUID {} neexistuje."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Deleted."
|
||||
@@ -1452,15 +1450,15 @@ msgstr "Smazáno"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Cloned, you are editing the new watch."
|
||||
msgstr "Naklonováno, upravujete nové sledování."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Watch is already queued or being checked."
|
||||
msgstr "Sledování je již zařazeno do fronty ke kontrole."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Queued 1 watch for rechecking."
|
||||
msgstr "Zařazeno 1 sledování ke kontrole."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
@@ -1479,7 +1477,7 @@ msgstr "Přidává se sledování do fronty pro opětovnou kontrolu na pozadí..
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "Could not share, something went wrong while communicating with the share server - {}"
|
||||
msgstr "Sdílení selhalo, něco se pokazilo při komunikaci se sdílecím serverem = {}"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
msgid "Language set to auto-detect from browser"
|
||||
@@ -1487,52 +1485,52 @@ msgstr "Jazyk nastaven na automatickou detekci z prohlížeče"
|
||||
|
||||
#: changedetectionio/blueprint/ui/diff.py changedetectionio/blueprint/ui/preview.py
|
||||
msgid "No history found for the specified link, bad link?"
|
||||
msgstr "Historie pro vybraný odkaz nenalezena, chybný odkaz?"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/diff.py
|
||||
msgid "Not enough history (2 snapshots required) to show difference page for this watch."
|
||||
msgstr "Nedostatečná historie (vyžadovány 2 záchyty) pro zobrazení rozdílů tohoto sledování."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/diff.py
|
||||
#, python-format
|
||||
msgid "Monthly AI token budget of %(budget)s tokens reached (%(used)s used). Resets next month."
|
||||
msgstr "Dosažen měsíční počet %(budget)s AI tokenů (%(used)s použito). Resetuje se příští měsíc."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "No watches to edit"
|
||||
msgstr "Žádná sledování k úpravě"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
#, python-brace-format
|
||||
msgid "No watch with the UUID {} found."
|
||||
msgstr "Sledování s UUID {} nenalezeno."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
#, python-brace-format
|
||||
msgid "Switched to mode - {}."
|
||||
msgstr "Přepnuto na mód - {}."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
#, python-brace-format
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing. Please select a different processor."
|
||||
msgstr "Nelze načíst '{}' procesor, zásuvný modul procesoru nejspíše chybí. Vyberte prosím jiný procesor."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
#, python-brace-format
|
||||
msgid "Could not load '{}' processor, processor plugin might be missing."
|
||||
msgstr "Nelze načíst '{}' procesor, zásuvný modul procesoru nejspíše chybí."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "System settings default"
|
||||
msgstr "Výchozí systémová nastavení"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Default"
|
||||
msgstr "Výchozí"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch - unpaused!"
|
||||
msgstr "Sledování aktualizováno - znovu spuštěno!"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py
|
||||
msgid "Updated watch."
|
||||
@@ -1540,15 +1538,15 @@ msgstr "Sledování aktualizováno."
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
msgstr "Náhled nedostupný - stažení/kontrola nedokončena nebo nebyly splněny podmínky kontroly"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py
|
||||
msgid "Diff"
|
||||
msgstr "Rozdíly"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "This will remove version history (snapshots) for ALL watches, but keep your list of URLs!"
|
||||
msgstr "Toto odstraní historii verzí (snímky) pro VŠECHNA sledování, ale ponechá seznam URL adres!"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html
|
||||
msgid "You may like to use the"
|
||||
@@ -2961,6 +2959,7 @@ msgstr "Spojit všechny následující položky"
|
||||
msgid "Match any of the following"
|
||||
msgstr "Přiřaďte kteroukoli z následujících možností"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "V seznamu použijte stránku <title>"
|
||||
@@ -3060,6 +3059,7 @@ msgstr "Aktualizace UI v reálném čase"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "Povolit favikony"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "Použijte stránku <title> v přehledu sledování"
|
||||
@@ -3453,6 +3453,7 @@ msgstr ""
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "UUID monitoru."
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -3011,6 +3011,7 @@ msgstr "Passen Sie alle folgenden Punkte an"
|
||||
msgid "Match any of the following"
|
||||
msgstr "Entspricht einer der folgenden Bedingungen"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "Verwenden Sie Seite <title> in der Liste"
|
||||
@@ -3110,6 +3111,7 @@ msgstr "Echtzeit-UI-Updates aktiviert"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "Favicons Aktiviert"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "Verwenden Sie die Seite <title> in der Übersichtsliste der Beobachtungen"
|
||||
@@ -3505,6 +3507,7 @@ msgstr ""
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "Die UUID der Überwachung."
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -2953,6 +2953,7 @@ msgstr ""
|
||||
msgid "Match any of the following"
|
||||
msgstr ""
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr ""
|
||||
@@ -3052,6 +3053,7 @@ msgstr ""
|
||||
msgid "Favicons Enabled"
|
||||
msgstr ""
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr ""
|
||||
@@ -3445,6 +3447,7 @@ msgstr ""
|
||||
msgid "The UUID of the watch."
|
||||
msgstr ""
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -2953,6 +2953,7 @@ msgstr ""
|
||||
msgid "Match any of the following"
|
||||
msgstr ""
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr ""
|
||||
@@ -3052,6 +3053,7 @@ msgstr ""
|
||||
msgid "Favicons Enabled"
|
||||
msgstr ""
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr ""
|
||||
@@ -3445,6 +3447,7 @@ msgstr ""
|
||||
msgid "The UUID of the watch."
|
||||
msgstr ""
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -3026,6 +3026,7 @@ msgstr "Coincide con todo lo siguiente"
|
||||
msgid "Match any of the following"
|
||||
msgstr "Coincide con cualquiera de los siguientes"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "Usar página <title> en la lista"
|
||||
@@ -3125,6 +3126,7 @@ msgstr "Actualizaciones de UI en tiempo real habilitadas"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "Favicones habilitados"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "Usar <title> de la página en la lista general de monitores"
|
||||
@@ -3518,6 +3520,7 @@ msgstr "La URL que se está viendo."
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "El UUID del monitor."
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr "El título de la página del monitor, utiliza <title> si no se establece, vuelve a la URL"
|
||||
|
||||
@@ -2966,6 +2966,7 @@ msgstr "Faites correspondre tous les éléments suivants"
|
||||
msgid "Match any of the following"
|
||||
msgstr "Faites correspondre l'un des éléments suivants"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "Utiliser la page <title> dans la liste"
|
||||
@@ -3065,6 +3066,7 @@ msgstr "Mises à jour en temps réel hors ligne"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "Favicons Activés"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "Utiliser la page <title> dans la liste de présentation des moniteurs"
|
||||
@@ -3458,6 +3460,7 @@ msgstr ""
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "L'UUID du moniteur."
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -2955,6 +2955,7 @@ msgstr "Corrisponde a tutti i seguenti"
|
||||
msgid "Match any of the following"
|
||||
msgstr "Corrisponde a uno qualsiasi dei seguenti"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "Usa <title> pagina nell'elenco"
|
||||
@@ -3054,6 +3055,7 @@ msgstr "Aggiornamenti UI in tempo reale attivi"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "Favicon attive"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "Usa <title> pagina nell'elenco osservati"
|
||||
@@ -3447,6 +3449,7 @@ msgstr ""
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "L'UUID del monitor."
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -2972,6 +2972,7 @@ msgstr "以下のすべてに一致"
|
||||
msgid "Match any of the following"
|
||||
msgstr "以下のいずれかに一致"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "リストでページの <title> を使用"
|
||||
@@ -3071,6 +3072,7 @@ msgstr "リアルタイムUI更新を有効化"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "ファビコンを有効化"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "ウォッチ一覧リストでページの <title> を使用"
|
||||
@@ -3464,6 +3466,7 @@ msgstr "監視中のURL。"
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "ウォッチのUUID。"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr "ウォッチのページタイトル。設定されていない場合は <title> を使用し、それもなければURLにフォールバックします。"
|
||||
|
||||
@@ -2963,6 +2963,7 @@ msgstr "다음 모두와 일치"
|
||||
msgid "Match any of the following"
|
||||
msgstr "다음 중 하나와 일치"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "목록에 페이지 <title> 사용"
|
||||
@@ -3062,6 +3063,7 @@ msgstr "실시간 UI 업데이트 활성화"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "파비콘 활성화"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "모니터링 목록에 페이지 <title> 사용"
|
||||
@@ -3455,6 +3457,7 @@ msgstr "모니터링 중인 URL입니다."
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "모니터링 UUID입니다."
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr "모니터링의 페이지 제목입니다. 설정되지 않았으면 <title> 을 사용하고, 없으면 URL을 사용합니다."
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.55.7\n"
|
||||
"Project-Id-Version: changedetection.io 0.55.5\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-06-03 12:07+0200\n"
|
||||
"POT-Creation-Date: 2026-05-25 10:09+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"
|
||||
@@ -2952,6 +2952,7 @@ msgstr ""
|
||||
msgid "Match any of the following"
|
||||
msgstr ""
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr ""
|
||||
@@ -3051,6 +3052,7 @@ msgstr ""
|
||||
msgid "Favicons Enabled"
|
||||
msgstr ""
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr ""
|
||||
@@ -3444,6 +3446,7 @@ msgstr ""
|
||||
msgid "The UUID of the watch."
|
||||
msgstr ""
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -3003,6 +3003,7 @@ msgstr "Corresponder a TODOS os seguintes"
|
||||
msgid "Match any of the following"
|
||||
msgstr "Corresponder a QUALQUER um dos seguintes"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "Usar <title> da página na lista"
|
||||
@@ -3102,6 +3103,7 @@ msgstr "Atualizações de Interface em Tempo Real Ativadas"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "Favicons Ativados"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "Usar <title> da página na lista de visão geral"
|
||||
@@ -3495,6 +3497,7 @@ msgstr "A URL que está sendo monitorada."
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "O UUID do monitoramento."
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr "O título da página do monitoramento, usa <title> se não definido, ou a URL"
|
||||
|
||||
@@ -3006,6 +3006,7 @@ msgstr "Aşağıdakilerin tümünü eşleştir"
|
||||
msgid "Match any of the following"
|
||||
msgstr "Aşağıdakilerden herhangi birini eşleştir"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "Listede sayfa <title>'ını kullan"
|
||||
@@ -3105,6 +3106,7 @@ msgstr "Gerçek Zamanlı Arayüz Güncellemeleri Etkin"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "Favicon'lar Etkin"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "İzleyici genel bakış listesinde sayfa <title>'ını kullan"
|
||||
@@ -3498,6 +3500,7 @@ msgstr "İzlenen URL."
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "İzleyicinin UUID'si."
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr "İzleyicinin sayfa başlığı, ayarlanmamışsa <title> kullanır, URL'ye geri döner"
|
||||
|
||||
@@ -2985,6 +2985,7 @@ msgstr "Збіг усіх наступних умов"
|
||||
msgid "Match any of the following"
|
||||
msgstr "Збіг будь-якої з наступних умов"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "Використовувати <title> сторінки у списку"
|
||||
@@ -3084,6 +3085,7 @@ msgstr "Оновлення UI в реальному часі увімкнено"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "Фавіконки увімкнено"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "Використовувати <title> сторінки у списку огляду завдань"
|
||||
@@ -3477,6 +3479,7 @@ msgstr "URL, за яким ведеться спостереження."
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "UUID завдання."
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr "Заголовок сторінки завдання, використовує <title>, якщо не задано - URL"
|
||||
|
||||
@@ -2958,6 +2958,7 @@ msgstr "匹配以下全部"
|
||||
msgid "Match any of the following"
|
||||
msgstr "匹配以下任意"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "列表中使用页面 <title>"
|
||||
@@ -3057,6 +3058,7 @@ msgstr "启用实时界面更新"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "启用站点图标"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "在监控概览列表中使用页面 <title>"
|
||||
@@ -3450,6 +3452,7 @@ msgstr "被监控的 URL。"
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "监视器的UUID。"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr "监控项的页面标题,未设置时使用 <title>,否则回退为 URL"
|
||||
|
||||
@@ -2957,6 +2957,7 @@ msgstr "符合以下所有條件"
|
||||
msgid "Match any of the following"
|
||||
msgstr "符合以下任一條件"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in list"
|
||||
msgstr "在列表中使用頁面 <title>"
|
||||
@@ -3056,6 +3057,7 @@ msgstr "已啟用即時 UI 更新"
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "啟用網站圖示 (Favicons)"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Use page <title> in watch overview list"
|
||||
msgstr "在監測概覽列表中使用頁面 <title>"
|
||||
@@ -3449,6 +3451,7 @@ msgstr ""
|
||||
msgid "The UUID of the watch."
|
||||
msgstr "監測任務的 UUID。"
|
||||
|
||||
#. dennis-ignore: W303 - False positive caused by <title>. https://github.com/mozilla/dennis/issues/213
|
||||
#: changedetectionio/templates/_common_fields.html
|
||||
msgid "The page title of the watch, uses <title> if not set, falls back to URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -80,52 +80,6 @@ def is_private_hostname(hostname):
|
||||
return False
|
||||
|
||||
|
||||
def extract_url_hostnames(url):
|
||||
"""Return every hostname this URL could resolve to under different URL parsers.
|
||||
|
||||
Why: urllib's urlparse() and urllib3's parse_url() disagree on URLs containing
|
||||
a backslash (e.g. http://INTERNAL:8888\\@PUBLIC/ — urlparse extracts PUBLIC, but
|
||||
urllib3/requests will actually connect to INTERNAL). Any SSRF check that trusts
|
||||
only one parser can be bypassed by the other. Callers should reject the fetch
|
||||
if ANY hostname returned here is private/reserved.
|
||||
|
||||
See GHSA-rph4-96w6-q594.
|
||||
"""
|
||||
hostnames = set()
|
||||
try:
|
||||
h = urlparse(url).hostname
|
||||
if h:
|
||||
hostnames.add(h)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from urllib3.util.url import parse_url as _u3_parse_url
|
||||
u3 = _u3_parse_url(url)
|
||||
if u3.host:
|
||||
# urllib3 keeps IPv6 brackets in `.host`; strip them so socket.getaddrinfo() accepts the literal.
|
||||
hostnames.add(u3.host.strip('[]'))
|
||||
except Exception:
|
||||
pass
|
||||
return hostnames
|
||||
|
||||
|
||||
def is_url_private_or_parser_confused(url):
|
||||
"""SSRF gate that defends against urlparse/urllib3 parser-differential attacks.
|
||||
|
||||
Returns True (block the fetch) when:
|
||||
* the URL contains a backslash — no legitimate URL needs one, and it is the
|
||||
established vector for the parser-differential bypass (GHSA-rph4-96w6-q594), OR
|
||||
* any hostname produced by urlparse OR urllib3 resolves to a private/reserved IP.
|
||||
"""
|
||||
if '\\' in url:
|
||||
logger.warning(f"URL '{url}' contains a backslash — rejected to prevent urlparse/urllib3 parser-differential SSRF.")
|
||||
return True
|
||||
for hostname in extract_url_hostnames(url):
|
||||
if is_private_hostname(hostname):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_llm_api_base_safe(api_base):
|
||||
"""SSRF guard for the LLM `api_base` setting (GHSA-jrxm-qjfh-g54f).
|
||||
|
||||
@@ -224,13 +178,6 @@ def is_safe_valid_url(test_url):
|
||||
logger.warning(f'URL "{test_url}" contains suspicious characters')
|
||||
return False
|
||||
|
||||
# Reject backslashes — urllib's urlparse and urllib3's parse_url disagree on URLs containing
|
||||
# a backslash (e.g. http://INTERNAL:8888\@PUBLIC/), which is the documented SSRF bypass in
|
||||
# GHSA-rph4-96w6-q594. A backslash has no legitimate use in an HTTP URL, so block at add-time.
|
||||
if '\\' in test_url:
|
||||
logger.warning(f'URL "{test_url}" contains a backslash — rejected (parser-differential SSRF vector).')
|
||||
return False
|
||||
|
||||
# Normalize URL encoding - handle both encoded and unencoded query parameters
|
||||
test_url = normalize_url_encoding(test_url)
|
||||
|
||||
|
||||
@@ -70,9 +70,6 @@ services:
|
||||
# For complete privacy if you don't want to use the 'check version' / telemetry service
|
||||
# - DISABLE_VERSION_CHECK=true
|
||||
#
|
||||
# Disable all LLM / AI features, prompts etc
|
||||
# - LLM_FEATURES_DISABLED=true
|
||||
#
|
||||
# A valid timezone name to run as (for scheduling watch checking) see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
# - TZ=America/Los_Angeles
|
||||
#
|
||||
|
||||
+1
-1
@@ -159,7 +159,7 @@ psutil==7.2.2
|
||||
|
||||
ruff >= 0.11.2
|
||||
pre_commit >= 4.2.0
|
||||
dennis >= 1.3.0
|
||||
dennis >= 1.2.0
|
||||
|
||||
# For events between checking and socketio updates
|
||||
blinker
|
||||
|
||||
Reference in New Issue
Block a user