mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-09-28 00:06:06 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68ed17e45f | ||
|
|
53fd36e482 | ||
|
|
6a7fae625b | ||
|
|
e765588089 |
@@ -1,7 +1,6 @@
|
||||
import re
|
||||
|
||||
import pluggy
|
||||
from price_parser import Price
|
||||
from loguru import logger
|
||||
from flask_babel import lazy_gettext as _l
|
||||
|
||||
@@ -70,6 +69,7 @@ def register_field_choices():
|
||||
@hookimpl
|
||||
def add_data(current_watch_uuid, application_datastruct, ephemeral_data):
|
||||
|
||||
from price_parser import Price
|
||||
res = {}
|
||||
if 'text' in ephemeral_data:
|
||||
res['page_filtered_text'] = ephemeral_data['text']
|
||||
|
||||
@@ -696,14 +696,14 @@ class ValidateCSSJSONXPATHInput(object):
|
||||
raise ValidationError("XPath not permitted in this field!")
|
||||
from lxml import etree, html
|
||||
import elementpath
|
||||
from changedetectionio.html_tools import SafeXPath3Parser, lxml_guard, lxml_html_parser
|
||||
from changedetectionio.html_tools import get_safe_xpath3_parser, lxml_guard, lxml_html_parser
|
||||
line = line.replace('xpath:', '')
|
||||
|
||||
try:
|
||||
# Runs on a Flask request thread - must share the worker's lxml lock.
|
||||
with lxml_guard():
|
||||
tree = html.fromstring("<html></html>", parser=lxml_html_parser())
|
||||
elementpath.select(tree, line.strip(), parser=SafeXPath3Parser)
|
||||
elementpath.select(tree, line.strip(), parser=get_safe_xpath3_parser())
|
||||
except elementpath.ElementPathError as e:
|
||||
message = field.gettext('\'%(expression)s\' is not a valid XPath expression. (%(error)s)')
|
||||
raise ValidationError(message % {'expression': line, 'error': str(e)})
|
||||
|
||||
@@ -164,7 +164,7 @@ _DEFAULT_UNSAFE_XPATH3_FUNCTIONS = [
|
||||
]
|
||||
|
||||
|
||||
def _build_safe_xpath3_parser():
|
||||
def get_safe_xpath3_parser():
|
||||
"""Return an XPath3Parser subclass with filesystem/environment access functions removed.
|
||||
|
||||
XPath 3.0 includes functions that can read arbitrary files or environment variables:
|
||||
@@ -196,9 +196,6 @@ def _build_safe_xpath3_parser():
|
||||
return SafeXPath3Parser
|
||||
|
||||
|
||||
# Module-level singleton — built once, reused everywhere.
|
||||
SafeXPath3Parser = _build_safe_xpath3_parser()
|
||||
|
||||
# Doesn't look like python supports forward slash auto enclosure in re.findall
|
||||
# So convert it to inline flag "(?i)foobar" type configuration
|
||||
@lru_cache(maxsize=100)
|
||||
@@ -386,7 +383,7 @@ def xpath_filter(xpath_filter, html_content, append_pretty_line_formatting=False
|
||||
# This allows //title to match elements in the default namespace
|
||||
namespaces[''] = tree.nsmap[None]
|
||||
|
||||
r = elementpath.select(tree, xpath_filter.strip(), namespaces=namespaces, parser=SafeXPath3Parser)
|
||||
r = elementpath.select(tree, xpath_filter.strip(), namespaces=namespaces, parser=get_safe_xpath3_parser())
|
||||
#@note: //title/text() now works with default namespaces (fixed by registering '' prefix)
|
||||
#@note: //title/text() wont work where <title>CDATA.. (use cdata_in_document_to_text first)
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ Note:
|
||||
This extension uses the Arrow library for timezone-aware datetime handling.
|
||||
All timezone names should be valid IANA timezone identifiers (e.g., 'America/New_York').
|
||||
"""
|
||||
import arrow
|
||||
|
||||
from jinja2 import nodes
|
||||
from jinja2.ext import Extension
|
||||
@@ -125,6 +124,7 @@ class TimeExtension(Extension):
|
||||
_datetime('UTC', '+', 'hours=2,minutes=30', '%Y-%m-%d %H:%M:%S')
|
||||
# Returns current time + 2.5 hours
|
||||
"""
|
||||
import arrow
|
||||
# Use default timezone if none specified
|
||||
if not timezone or timezone == '':
|
||||
timezone = self.environment.default_timezone
|
||||
@@ -162,6 +162,7 @@ class TimeExtension(Extension):
|
||||
_now('America/New_York', '%Y-%m-%d %H:%M:%S')
|
||||
# Returns current time in New York timezone
|
||||
"""
|
||||
import arrow
|
||||
# Use default timezone if none specified
|
||||
if not timezone or timezone == '':
|
||||
timezone = self.environment.default_timezone
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
|
||||
import time
|
||||
import re
|
||||
import apprise
|
||||
from apprise import NotifyFormat
|
||||
from loguru import logger
|
||||
from urllib.parse import urlparse
|
||||
from .apprise_plugin.assets import apprise_asset, APPRISE_AVATAR_URL
|
||||
from .email_helpers import as_monospaced_html_email
|
||||
from ..diff import HTML_REMOVED_STYLE, REMOVED_PLACEMARKER_OPEN, REMOVED_PLACEMARKER_CLOSED, ADDED_PLACEMARKER_OPEN, HTML_ADDED_STYLE, \
|
||||
ADDED_PLACEMARKER_CLOSED, CHANGED_INTO_PLACEMARKER_OPEN, CHANGED_INTO_PLACEMARKER_CLOSED, CHANGED_PLACEMARKER_OPEN, \
|
||||
@@ -64,6 +61,7 @@ def notification_format_align_with_apprise(n_format : str):
|
||||
:param n_format:
|
||||
:return:
|
||||
"""
|
||||
from apprise import NotifyFormat
|
||||
|
||||
if not n_format:
|
||||
return NotifyFormat.TEXT.value
|
||||
@@ -208,6 +206,7 @@ def replace_placemarkers_in_text(text, url, requested_output_format):
|
||||
|
||||
def apply_service_tweaks(url, n_body, n_title, requested_output_format):
|
||||
|
||||
from .apprise_plugin.assets import APPRISE_AVATAR_URL
|
||||
logger.debug(f"Applying markup in '{requested_output_format}' mode")
|
||||
|
||||
# Re 323 - Limit discord length to their 2000 char limit total or it wont send.
|
||||
@@ -305,6 +304,9 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format):
|
||||
|
||||
|
||||
def process_notification(n_object: NotificationContextData, datastore):
|
||||
import apprise
|
||||
from apprise import NotifyFormat
|
||||
from .apprise_plugin.assets import apprise_asset
|
||||
from changedetectionio.jinja2_custom import render as jinja_render
|
||||
from . import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, default_notification_format, valid_notification_formats
|
||||
# be sure its registered
|
||||
|
||||
@@ -132,8 +132,9 @@ def environment(mocker):
|
||||
# Fixed datetime: Wed, 09 Dec 2015 23:33:01 UTC
|
||||
# This is calculated to match the test expectations when offsets are applied
|
||||
fixed_datetime = arrow.Arrow(2015, 12, 9, 23, 33, 1, tzinfo='UTC')
|
||||
# Patch arrow.now in the TimeExtension module where it's actually used
|
||||
mocker.patch('changedetectionio.jinja2_custom.extensions.TimeExtension.arrow.now', return_value=fixed_datetime)
|
||||
# Patch on the arrow module itself - TimeExtension imports arrow lazily inside its methods
|
||||
# (import diet), so there is no module-level TimeExtension.arrow attribute to patch
|
||||
mocker.patch('arrow.now', return_value=fixed_datetime)
|
||||
return fixed_datetime
|
||||
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ def test_timezone(mocker):
|
||||
|
||||
timezone = 'America/Buenos_Aires'
|
||||
currentDate = arrow.now(timezone)
|
||||
arrowNowMock = mocker.patch("changedetectionio.jinja2_custom.extensions.TimeExtension.arrow.now")
|
||||
arrowNowMock = mocker.patch("arrow.now")
|
||||
arrowNowMock.return_value = currentDate
|
||||
finalRender = render(f"{{% now '{timezone}' %}}")
|
||||
|
||||
@@ -115,7 +115,7 @@ def test_add_weekday(mocker):
|
||||
|
||||
timezone = 'utc'
|
||||
currentDate = arrow.now(timezone)
|
||||
arrowNowMock = mocker.patch("changedetectionio.jinja2_custom.extensions.TimeExtension.arrow.now")
|
||||
arrowNowMock = mocker.patch("arrow.now")
|
||||
arrowNowMock.return_value = currentDate
|
||||
finalRender = render(f"{{% now '{timezone}' + 'weekday=1' %}}")
|
||||
|
||||
|
||||
@@ -595,11 +595,11 @@ def test_rss_xpath(client, live_server, measure_memory_usage, datastore_path):
|
||||
|
||||
|
||||
# GHSA-6fmw-82m7-jq6p — XPath arbitrary file read via unparsed-text() and friends
|
||||
# Unit-level: verify xpath_filter() and SafeXPath3Parser block all dangerous functions.
|
||||
# Unit-level: verify xpath_filter() and the safe XPath3 parser block all dangerous functions.
|
||||
def test_xpath_blocked_functions_unit():
|
||||
"""Dangerous XPath 3.0 functions must be rejected at the parser level (no live server needed)."""
|
||||
import elementpath
|
||||
from changedetectionio.html_tools import xpath_filter, SafeXPath3Parser
|
||||
from changedetectionio.html_tools import xpath_filter, get_safe_xpath3_parser
|
||||
from lxml import html
|
||||
|
||||
html_content = '<html><body><p>safe content</p></body></html>'
|
||||
@@ -627,11 +627,11 @@ def test_xpath_blocked_functions_unit():
|
||||
except elementpath.ElementPathError:
|
||||
pass # expected
|
||||
|
||||
# SafeXPath3Parser must reject the expression at parse time
|
||||
# the safe parser must reject the expression at parse time
|
||||
tree = html.fromstring(html_content)
|
||||
try:
|
||||
elementpath.select(tree, expr, parser=SafeXPath3Parser)
|
||||
assert False, f"SafeXPath3Parser should have raised for: {expr!r}"
|
||||
elementpath.select(tree, expr, parser=get_safe_xpath3_parser())
|
||||
assert False, f"safe XPath3 parser should have raised for: {expr!r}"
|
||||
except elementpath.ElementPathError:
|
||||
pass # expected
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ def test_default_timezone_override_like_safe_jinja(mocker):
|
||||
|
||||
# Mock arrow.now to return a fixed time
|
||||
fixed_time = arrow.Arrow(2025, 1, 15, 12, 0, 0, tzinfo='America/New_York')
|
||||
mock = mocker.patch("changedetectionio.jinja2_custom.extensions.TimeExtension.arrow.now", return_value=fixed_time)
|
||||
mock = mocker.patch("arrow.now", return_value=fixed_time)
|
||||
|
||||
# Use empty string timezone - should use the overridden default
|
||||
template_str = "{% now '' %}"
|
||||
@@ -46,7 +46,7 @@ def test_default_timezone_not_overridden(mocker):
|
||||
|
||||
# Mock arrow.now
|
||||
fixed_time = arrow.Arrow(2025, 1, 15, 17, 0, 0, tzinfo='UTC')
|
||||
mock = mocker.patch("changedetectionio.jinja2_custom.extensions.TimeExtension.arrow.now", return_value=fixed_time)
|
||||
mock = mocker.patch("arrow.now", return_value=fixed_time)
|
||||
|
||||
# Use empty string timezone - should use 'UTC' default
|
||||
template_str = "{% now '' %}"
|
||||
@@ -69,7 +69,7 @@ def test_datetime_format_override_like_safe_jinja(mocker):
|
||||
|
||||
# Mock arrow.now
|
||||
fixed_time = arrow.Arrow(2025, 1, 15, 14, 30, 45, tzinfo='UTC')
|
||||
mocker.patch("changedetectionio.jinja2_custom.extensions.TimeExtension.arrow.now", return_value=fixed_time)
|
||||
mocker.patch("arrow.now", return_value=fixed_time)
|
||||
|
||||
# Don't specify format - should use overridden default
|
||||
template_str = "{% now 'UTC' %}"
|
||||
@@ -89,7 +89,7 @@ def test_offset_with_overridden_timezone(mocker):
|
||||
jinja2_env.default_timezone = 'Europe/London'
|
||||
|
||||
fixed_time = arrow.Arrow(2025, 1, 15, 10, 0, 0, tzinfo='Europe/London')
|
||||
mock = mocker.patch("changedetectionio.jinja2_custom.extensions.TimeExtension.arrow.now", return_value=fixed_time)
|
||||
mock = mocker.patch("arrow.now", return_value=fixed_time)
|
||||
|
||||
# Use offset with empty timezone string
|
||||
template_str = "{% now '' + 'hours=2', '%Y-%m-%d %H:%M:%S' %}"
|
||||
@@ -110,7 +110,7 @@ def test_weekday_parameter_converted_to_int(mocker):
|
||||
|
||||
# Wednesday, Jan 15, 2025
|
||||
fixed_time = arrow.Arrow(2025, 1, 15, 12, 0, 0, tzinfo='UTC')
|
||||
mocker.patch("changedetectionio.jinja2_custom.extensions.TimeExtension.arrow.now", return_value=fixed_time)
|
||||
mocker.patch("arrow.now", return_value=fixed_time)
|
||||
|
||||
# Add offset to next Monday (weekday=0)
|
||||
template_str = "{% now 'UTC' + 'weekday=0', '%A' %}"
|
||||
@@ -127,7 +127,7 @@ def test_multiple_offset_parameters(mocker):
|
||||
jinja2_env = ImmutableSandboxedEnvironment(extensions=[TimeExtension])
|
||||
|
||||
fixed_time = arrow.Arrow(2025, 1, 15, 10, 30, 45, tzinfo='UTC')
|
||||
mocker.patch("changedetectionio.jinja2_custom.extensions.TimeExtension.arrow.now", return_value=fixed_time)
|
||||
mocker.patch("arrow.now", return_value=fixed_time)
|
||||
|
||||
# Test multiple parameters: days, hours, minutes, seconds
|
||||
template_str = "{% now 'UTC' + 'days=1,hours=2,minutes=15,seconds=10', '%Y-%m-%d %H:%M:%S' %}"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
from functools import lru_cache
|
||||
|
||||
import arrow
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
@@ -55,6 +54,7 @@ def am_i_inside_time(
|
||||
Returns:
|
||||
bool: True if the current time is within the time range, False otherwise.
|
||||
"""
|
||||
import arrow
|
||||
# Parse the target day of the week
|
||||
try:
|
||||
target_weekday = Weekday[day_of_week.capitalize()]
|
||||
@@ -114,6 +114,7 @@ def is_within_schedule(time_schedule_limit, default_tz="UTC"):
|
||||
Returns:
|
||||
bool: True if current time is within the schedule, False otherwise.
|
||||
"""
|
||||
import arrow
|
||||
if time_schedule_limit and time_schedule_limit.get('enabled'):
|
||||
# Get the timezone the time schedule is in, so we know what day it is there
|
||||
tz_name = time_schedule_limit.get('timezone')
|
||||
|
||||
Reference in New Issue
Block a user