mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-09-22 05:16:07 +00:00
WIP
This commit is contained in:
@@ -5,7 +5,7 @@ import re
|
||||
from loguru import logger
|
||||
from markupsafe import Markup
|
||||
|
||||
from changedetectionio.diff import REMOVED_STYLE, ADDED_STYLE
|
||||
from changedetectionio.diff import REMOVED_STYLE, ADDED_STYLE, DIFF_HTML_LABEL_REMOVED, DIFF_HTML_LABEL_ADDED
|
||||
from changedetectionio.store import ChangeDetectionStore
|
||||
from changedetectionio.auth_decorator import login_optionally_required
|
||||
from changedetectionio import html_tools, diff
|
||||
@@ -27,6 +27,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
|
||||
|
||||
# Step 2: Simple regex to unescape only our exact diff spans
|
||||
# Unescape opening tags with exact styles
|
||||
# This matches the styles used in DIFF_HTML_LABEL_REMOVED, DIFF_HTML_LABEL_ADDED, etc.
|
||||
result = re.sub(
|
||||
rf'<span style="({REMOVED_STYLE}|{ADDED_STYLE})" title="([A-Za-z0-9]+)">',
|
||||
r'<span style="\1" title="\2">',
|
||||
|
||||
@@ -14,6 +14,12 @@ DIFF_LABEL_TEXT_REMOVED = '(removed) {content}'
|
||||
DIFF_LABEL_TEXT_CHANGED = '(changed) {content}'
|
||||
DIFF_LABEL_TEXT_INTO = '(into) {content}'
|
||||
|
||||
# Diff HTML label formats (use {content} as placeholder)
|
||||
DIFF_HTML_LABEL_REMOVED = f'<span style="{REMOVED_STYLE}" title="Removed">{{content}}</span>'
|
||||
DIFF_HTML_LABEL_ADDED = f'<span style="{ADDED_STYLE}" title="Added">{{content}}</span>'
|
||||
DIFF_HTML_LABEL_REPLACED = f'<span style="{ADDED_STYLE}" title="Replaced">{{content}}</span>'
|
||||
DIFF_HTML_LABEL_INSERTED = f'<span style="{ADDED_STYLE}" title="Inserted">{{content}}</span>'
|
||||
|
||||
# Compiled regex patterns for performance
|
||||
WHITESPACE_NORMALIZE_RE = re.compile(r'\s+')
|
||||
|
||||
@@ -78,13 +84,13 @@ def render_inline_word_diff(before_line: str, after_line: str, html_colour: bool
|
||||
content = m.group(1).rstrip()
|
||||
trailing = m.group(1)[len(content):] if len(m.group(1)) > len(content) else ''
|
||||
line_break = '\n' if whole_line_replaced else ''
|
||||
return f'<span style="{REMOVED_STYLE}" title="Removed">{content}</span>{trailing}{line_break}'
|
||||
return f'{DIFF_HTML_LABEL_REMOVED.format(content=content)}{trailing}{line_break}'
|
||||
|
||||
def replace_added(m):
|
||||
content = m.group(1).rstrip()
|
||||
trailing = m.group(1)[len(content):] if len(m.group(1)) > len(content) else ''
|
||||
line_break = '\n' if whole_line_replaced else ''
|
||||
return f'<span style="{ADDED_STYLE}" title="Added">{content}</span>{trailing}{line_break}'
|
||||
return f'{DIFF_HTML_LABEL_ADDED.format(content=content)}{trailing}{line_break}'
|
||||
|
||||
diff_output = REDLINES_REMOVED_RE.sub(replace_removed, diff_output)
|
||||
diff_output = REDLINES_ADDED_RE.sub(replace_added, diff_output)
|
||||
@@ -209,7 +215,7 @@ def customSequenceMatcher(
|
||||
yield context_lines_to_include
|
||||
elif include_removed and tag == 'delete':
|
||||
if html_colour:
|
||||
yield [f'<span style="{REMOVED_STYLE}" title="Removed">{line}</span>' for line in same_slicer(before, alo, ahi)]
|
||||
yield [DIFF_HTML_LABEL_REMOVED.format(content=line) for line in same_slicer(before, alo, ahi)]
|
||||
else:
|
||||
yield [DIFF_LABEL_TEXT_REMOVED.format(content=line) for line in same_slicer(before, alo, ahi)] if include_change_type_prefix else same_slicer(before, alo, ahi)
|
||||
elif include_replaced and tag == 'replace':
|
||||
@@ -227,14 +233,14 @@ def customSequenceMatcher(
|
||||
else:
|
||||
# Fall back to line-level diff for multi-line changes or when word_diff disabled
|
||||
if html_colour:
|
||||
yield [f'<span style="{REMOVED_STYLE}" title="Removed">{line}</span>' for line in before_lines] + \
|
||||
[f'<span style="{ADDED_STYLE}" title="Replaced">{line}</span>' for line in after_lines]
|
||||
yield [DIFF_HTML_LABEL_REMOVED.format(content=line) for line in before_lines] + \
|
||||
[DIFF_HTML_LABEL_REPLACED.format(content=line) for line in after_lines]
|
||||
else:
|
||||
yield [DIFF_LABEL_TEXT_CHANGED.format(content=line) for line in before_lines] + \
|
||||
[DIFF_LABEL_TEXT_INTO.format(content=line) for line in after_lines] if include_change_type_prefix else before_lines + after_lines
|
||||
elif include_added and tag == 'insert':
|
||||
if html_colour:
|
||||
yield [f'<span style="{ADDED_STYLE}" title="Inserted">{line}</span>' for line in same_slicer(after, blo, bhi)]
|
||||
yield [DIFF_HTML_LABEL_INSERTED.format(content=line) for line in same_slicer(after, blo, bhi)]
|
||||
else:
|
||||
yield [DIFF_LABEL_TEXT_ADDED.format(content=line) for line in same_slicer(after, blo, bhi)] if include_change_type_prefix else same_slicer(after, blo, bhi)
|
||||
|
||||
|
||||
@@ -459,7 +459,7 @@ def test_global_send_test_notification(client, live_server, measure_memory_usage
|
||||
|
||||
def _test_color_notifications(client, notification_body_token):
|
||||
|
||||
from changedetectionio.diff import ADDED_STYLE, REMOVED_STYLE
|
||||
from changedetectionio.diff import ADDED_STYLE, REMOVED_STYLE, DIFF_HTML_LABEL_REMOVED
|
||||
|
||||
set_original_response()
|
||||
|
||||
@@ -507,7 +507,7 @@ def _test_color_notifications(client, notification_body_token):
|
||||
|
||||
with open("test-datastore/notification.txt", 'r') as f:
|
||||
x = f.read()
|
||||
assert f'<span style="{REMOVED_STYLE}" title="Removed">Which is across multiple lines' in x
|
||||
assert DIFF_HTML_LABEL_REMOVED.format(content='Which is across multiple lines') in x
|
||||
|
||||
|
||||
client.get(
|
||||
|
||||
@@ -4,7 +4,7 @@ import time
|
||||
from flask import url_for
|
||||
from urllib.request import urlopen
|
||||
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks
|
||||
from ..diff import ADDED_STYLE
|
||||
from ..diff import ADDED_STYLE, DIFF_HTML_LABEL_ADDED
|
||||
|
||||
sleep_time_for_fetch_thread = 3
|
||||
|
||||
@@ -53,7 +53,7 @@ def test_check_basic_change_detection_functionality_source(client, live_server,
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
assert f'<span style="{ADDED_STYLE}" title="Added">modified </span>head title</title></head>'.encode('utf-8') in res.data
|
||||
assert f'{DIFF_HTML_LABEL_ADDED.format(content="modified ")}head title</title></head>'.encode('utf-8') in res.data
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class TestJinja2SSTI(unittest.TestCase):
|
||||
from markupsafe import Markup, escape
|
||||
|
||||
# Import the constants from diff module
|
||||
from changedetectionio.diff import REMOVED_STYLE, ADDED_STYLE
|
||||
from changedetectionio.diff import REMOVED_STYLE, ADDED_STYLE, DIFF_HTML_LABEL_REMOVED, DIFF_HTML_LABEL_ADDED, DIFF_HTML_LABEL_INSERTED
|
||||
|
||||
# Recreate the filter logic for testing
|
||||
def diff_unescape_difference_spans(content):
|
||||
@@ -89,7 +89,7 @@ class TestJinja2SSTI(unittest.TestCase):
|
||||
return Markup(result)
|
||||
|
||||
# Test 1: Valid diff spans should be unescaped
|
||||
valid_diff_content = f'<span style="{REMOVED_STYLE}" title="Removed">old text</span>\n<span style="{ADDED_STYLE}" title="Inserted">new text</span>'
|
||||
valid_diff_content = f'{DIFF_HTML_LABEL_REMOVED.format(content="old text")}\n{DIFF_HTML_LABEL_INSERTED.format(content="new text")}'
|
||||
result = diff_unescape_difference_spans(valid_diff_content)
|
||||
self.assertIn('<span style=', str(result)) # Should contain unescaped spans
|
||||
self.assertIn('old text', str(result))
|
||||
@@ -122,7 +122,7 @@ class TestJinja2SSTI(unittest.TestCase):
|
||||
# Test 3: Invalid diff spans should remain escaped
|
||||
invalid_diff_spans = [
|
||||
'<span style="color: red;" title="Evil">bad</span>', # Wrong style
|
||||
f'<span style="{REMOVED_STYLE}" title="Evil Script">bad</span>', # Invalid title chars
|
||||
f'<span style="{REMOVED_STYLE}" title="Evil Script">bad</span>', # Invalid title chars (space not allowed)
|
||||
f'<span style="{REMOVED_STYLE}">no title</span>', # Missing title
|
||||
]
|
||||
|
||||
@@ -132,7 +132,7 @@ class TestJinja2SSTI(unittest.TestCase):
|
||||
self.assertIn('<span', str(result), f"Invalid span remained escaped: {invalid_span}")
|
||||
|
||||
# Test 4: Mixed content - valid diffs + XSS should only unescape valid parts
|
||||
mixed_content = f'<span style="{REMOVED_STYLE}" title="Removed">safe</span><script>alert(1)</script>'
|
||||
mixed_content = f'{DIFF_HTML_LABEL_REMOVED.format(content="safe")}<script>alert(1)</script>'
|
||||
result = diff_unescape_difference_spans(mixed_content)
|
||||
self.assertIn('<span style=', str(result)) # Valid diff unescaped
|
||||
self.assertIn('<script>', str(result)) # XSS remained escaped
|
||||
|
||||
@@ -11,7 +11,11 @@ from changedetectionio.diff import (
|
||||
DIFF_LABEL_TEXT_ADDED,
|
||||
DIFF_LABEL_TEXT_REMOVED,
|
||||
DIFF_LABEL_TEXT_CHANGED,
|
||||
DIFF_LABEL_TEXT_INTO
|
||||
DIFF_LABEL_TEXT_INTO,
|
||||
DIFF_HTML_LABEL_REMOVED,
|
||||
DIFF_HTML_LABEL_ADDED,
|
||||
DIFF_HTML_LABEL_REPLACED,
|
||||
DIFF_HTML_LABEL_INSERTED
|
||||
)
|
||||
|
||||
# mostly
|
||||
@@ -113,8 +117,8 @@ class TestDiffBuilder(unittest.TestCase):
|
||||
output = diff.render_diff(before, after, include_equal=False, word_diff=True, html_colour=True)
|
||||
|
||||
# Should highlight only the changed word (110 -> 111)
|
||||
self.assertIn('<span style="background-color: #fadad7; color: #b30000;" title="Removed">110</span>', output)
|
||||
self.assertIn('<span style="background-color: #eaf2c2; color: #406619;" title="Added">111</span>', output)
|
||||
self.assertIn(DIFF_HTML_LABEL_REMOVED.format(content='110'), output)
|
||||
self.assertIn(DIFF_HTML_LABEL_ADDED.format(content='111'), output)
|
||||
# Unchanged text should not be wrapped in spans
|
||||
self.assertIn('points by user', output)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user