Compare commits
24 Commits
0.48.00
...
debian-pac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfab864745 | ||
|
|
d013928a6c | ||
|
|
9d1fc0704d | ||
|
|
81a837ebac | ||
|
|
41da9480c7 | ||
|
|
26556d8091 | ||
|
|
273644d2d7 | ||
|
|
6adf10597e | ||
|
|
4419bc0e61 | ||
|
|
f7e9846c9b | ||
|
|
5dea5e1def | ||
|
|
0fade0a473 | ||
|
|
121e9c20e0 | ||
|
|
12cec2d541 | ||
|
|
d52e6e8e11 | ||
|
|
bae1a89b75 | ||
|
|
e49711f449 | ||
|
|
a3a3ab0622 | ||
|
|
c5fe188b28 | ||
|
|
1fb0adde54 | ||
|
|
2614b275f0 | ||
|
|
1631a55830 | ||
|
|
f00b8e4efb | ||
|
|
397f79ffd5 |
53
.github/workflows/build-deb-package.yml
vendored
Normal file
53
.github/workflows/build-deb-package.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Build Debian Package
|
||||
# Check status: systemctl status changedetection.io.service
|
||||
# Get logs: journalctl -u changedetection.io.service
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build-deb:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and Package changedetection.io
|
||||
env:
|
||||
PACKAGE_VERSION: 0.48.5 # or load from somewhere else
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: changedetection.io-${{ env.PACKAGE_VERSION }}
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Build Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
devscripts \
|
||||
dh-virtualenv \
|
||||
dh-python \
|
||||
python3-all \
|
||||
python3-all-dev \
|
||||
python3.10 \
|
||||
python3.10-venv \
|
||||
python3.10-dev \
|
||||
debhelper-compat
|
||||
|
||||
- name: Build the Debian Package
|
||||
# Build it the same as the pypi way, then use the same package tar
|
||||
run: |
|
||||
mkdir /tmp/changedetection.io
|
||||
python3 -m build
|
||||
mv dist/*gz .
|
||||
debuild -us -uc
|
||||
|
||||
- name: Upload Debian Package Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: changedetection.io-deb-package
|
||||
path: ../*.deb
|
||||
|
||||
#@todo install and test that some basic content appears
|
||||
@@ -10,6 +10,8 @@ recursive-include changedetectionio/tests *
|
||||
prune changedetectionio/static/package-lock.json
|
||||
prune changedetectionio/static/styles/node_modules
|
||||
prune changedetectionio/static/styles/package-lock.json
|
||||
prune changedetectionio/tests/test-datastore
|
||||
|
||||
include changedetection.py
|
||||
include requirements.txt
|
||||
include README-pip.md
|
||||
@@ -18,5 +20,6 @@ global-exclude node_modules
|
||||
global-exclude venv
|
||||
|
||||
global-exclude test-datastore
|
||||
global-exclude changedetectionio/tests/test-datastore
|
||||
global-exclude changedetection.io*dist-info
|
||||
global-exclude changedetectionio/tests/proxy_socks5/test-datastore
|
||||
|
||||
@@ -105,6 +105,15 @@ We [recommend and use Bright Data](https://brightdata.grsm.io/n0r16zf7eivq) glob
|
||||
|
||||
Please :star: star :star: this project and help it grow! https://github.com/dgtlmoon/changedetection.io/
|
||||
|
||||
### Schedule web page watches in any timezone, limit by day of week and time.
|
||||
|
||||
Easily set a re-check schedule, for example you could limit the web page change detection to only operate during business hours.
|
||||
Or perhaps based on a foreign timezone (for example, you want to check for the latest news-headlines in a foreign country at 0900 AM),
|
||||
|
||||
<img src="./docs/scheduler.png" style="max-width:80%;" alt="How to monitor web page changes according to a schedule" title="How to monitor web page changes according to a schedule" />
|
||||
|
||||
Includes quick short-cut buttons to setup a schedule for **business hours only**, or **weekends**.
|
||||
|
||||
### We have a Chrome extension!
|
||||
|
||||
Easily add the current web page to your changedetection.io tool, simply install the extension and click "Sync" to connect it to your existing changedetection.io install.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||
|
||||
__version__ = '0.48.00'
|
||||
__version__ = '0.48.05'
|
||||
|
||||
from changedetectionio.strtobool import strtobool
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
4
changedetectionio/__main__.py
Normal file
4
changedetectionio/__main__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,6 +1,9 @@
|
||||
import difflib
|
||||
from typing import List, Iterator, Union
|
||||
|
||||
REMOVED_STYLE = "background-color: #fadad7; color: #b30000;"
|
||||
ADDED_STYLE = "background-color: #eaf2c2; color: #406619;"
|
||||
|
||||
def same_slicer(lst: List[str], start: int, end: int) -> List[str]:
|
||||
"""Return a slice of the list, or a single element if start == end."""
|
||||
return lst[start:end] if start != end else [lst[start]]
|
||||
@@ -12,11 +15,12 @@ def customSequenceMatcher(
|
||||
include_removed: bool = True,
|
||||
include_added: bool = True,
|
||||
include_replaced: bool = True,
|
||||
include_change_type_prefix: bool = True
|
||||
include_change_type_prefix: bool = True,
|
||||
html_colour: bool = False
|
||||
) -> Iterator[List[str]]:
|
||||
"""
|
||||
Compare two sequences and yield differences based on specified parameters.
|
||||
|
||||
|
||||
Args:
|
||||
before (List[str]): Original sequence
|
||||
after (List[str]): Modified sequence
|
||||
@@ -25,26 +29,35 @@ def customSequenceMatcher(
|
||||
include_added (bool): Include added parts
|
||||
include_replaced (bool): Include replaced parts
|
||||
include_change_type_prefix (bool): Add prefixes to indicate change types
|
||||
|
||||
html_colour (bool): Use HTML background colors for differences
|
||||
|
||||
Yields:
|
||||
List[str]: Differences between sequences
|
||||
"""
|
||||
cruncher = difflib.SequenceMatcher(isjunk=lambda x: x in " \t", a=before, b=after)
|
||||
|
||||
|
||||
|
||||
|
||||
for tag, alo, ahi, blo, bhi in cruncher.get_opcodes():
|
||||
if include_equal and tag == 'equal':
|
||||
yield before[alo:ahi]
|
||||
elif include_removed and tag == 'delete':
|
||||
prefix = "(removed) " if include_change_type_prefix else ''
|
||||
yield [f"{prefix}{line}" for line in same_slicer(before, alo, ahi)]
|
||||
if html_colour:
|
||||
yield [f'<span style="{REMOVED_STYLE}">{line}</span>' for line in same_slicer(before, alo, ahi)]
|
||||
else:
|
||||
yield [f"(removed) {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':
|
||||
prefix_changed = "(changed) " if include_change_type_prefix else ''
|
||||
prefix_into = "(into) " if include_change_type_prefix else ''
|
||||
yield [f"{prefix_changed}{line}" for line in same_slicer(before, alo, ahi)] + \
|
||||
[f"{prefix_into}{line}" for line in same_slicer(after, blo, bhi)]
|
||||
if html_colour:
|
||||
yield [f'<span style="{REMOVED_STYLE}">{line}</span>' for line in same_slicer(before, alo, ahi)] + \
|
||||
[f'<span style="{ADDED_STYLE}">{line}</span>' for line in same_slicer(after, blo, bhi)]
|
||||
else:
|
||||
yield [f"(changed) {line}" for line in same_slicer(before, alo, ahi)] + \
|
||||
[f"(into) {line}" for line in same_slicer(after, blo, bhi)] if include_change_type_prefix else same_slicer(before, alo, ahi) + same_slicer(after, blo, bhi)
|
||||
elif include_added and tag == 'insert':
|
||||
prefix = "(added) " if include_change_type_prefix else ''
|
||||
yield [f"{prefix}{line}" for line in same_slicer(after, blo, bhi)]
|
||||
if html_colour:
|
||||
yield [f'<span style="{ADDED_STYLE}">{line}</span>' for line in same_slicer(after, blo, bhi)]
|
||||
else:
|
||||
yield [f"(added) {line}" for line in same_slicer(after, blo, bhi)] if include_change_type_prefix else same_slicer(after, blo, bhi)
|
||||
|
||||
def render_diff(
|
||||
previous_version_file_contents: str,
|
||||
@@ -55,11 +68,12 @@ def render_diff(
|
||||
include_replaced: bool = True,
|
||||
line_feed_sep: str = "\n",
|
||||
include_change_type_prefix: bool = True,
|
||||
patch_format: bool = False
|
||||
patch_format: bool = False,
|
||||
html_colour: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
Render the difference between two file contents.
|
||||
|
||||
|
||||
Args:
|
||||
previous_version_file_contents (str): Original file contents
|
||||
newest_version_file_contents (str): Modified file contents
|
||||
@@ -70,7 +84,8 @@ def render_diff(
|
||||
line_feed_sep (str): Separator for lines in output
|
||||
include_change_type_prefix (bool): Add prefixes to indicate change types
|
||||
patch_format (bool): Use patch format for output
|
||||
|
||||
html_colour (bool): Use HTML background colors for differences
|
||||
|
||||
Returns:
|
||||
str: Rendered difference
|
||||
"""
|
||||
@@ -88,10 +103,11 @@ def render_diff(
|
||||
include_removed=include_removed,
|
||||
include_added=include_added,
|
||||
include_replaced=include_replaced,
|
||||
include_change_type_prefix=include_change_type_prefix
|
||||
include_change_type_prefix=include_change_type_prefix,
|
||||
html_colour=html_colour
|
||||
)
|
||||
|
||||
def flatten(lst: List[Union[str, List[str]]]) -> str:
|
||||
return line_feed_sep.join(flatten(x) if isinstance(x, list) else x for x in lst)
|
||||
|
||||
return flatten(rendered_diff)
|
||||
return flatten(rendered_diff)
|
||||
@@ -841,7 +841,7 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
if request.args.get("next") and request.args.get("next") == 'diff':
|
||||
return redirect(url_for('diff_history_page', uuid=uuid))
|
||||
|
||||
return redirect(url_for('index'))
|
||||
return redirect(url_for('index', tag=request.args.get("tag",'')))
|
||||
|
||||
else:
|
||||
if request.method == 'POST' and not form.validate():
|
||||
@@ -1395,13 +1395,13 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
if new_uuid:
|
||||
if add_paused:
|
||||
flash('Watch added in Paused state, saving will unpause.')
|
||||
return redirect(url_for('edit_page', uuid=new_uuid, unpause_on_save=1))
|
||||
return redirect(url_for('edit_page', uuid=new_uuid, unpause_on_save=1, tag=request.args.get('tag')))
|
||||
else:
|
||||
# Straight into the queue.
|
||||
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': new_uuid}))
|
||||
flash("Watch added.")
|
||||
|
||||
return redirect(url_for('index'))
|
||||
return redirect(url_for('index', tag=request.args.get('tag','')))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -308,8 +308,12 @@ class ValidateAppRiseServers(object):
|
||||
# so that the custom endpoints are registered
|
||||
from changedetectionio.apprise_plugin import apprise_custom_api_call_wrapper
|
||||
for server_url in field.data:
|
||||
if not apobj.add(server_url):
|
||||
message = field.gettext('\'%s\' is not a valid AppRise URL.' % (server_url))
|
||||
url = server_url.strip()
|
||||
if url.startswith("#"):
|
||||
continue
|
||||
|
||||
if not apobj.add(url):
|
||||
message = field.gettext('\'%s\' is not a valid AppRise URL.' % (url))
|
||||
raise ValidationError(message)
|
||||
|
||||
class ValidateJinja2Template(object):
|
||||
|
||||
@@ -23,7 +23,7 @@ valid_tokens = {
|
||||
}
|
||||
|
||||
default_notification_format_for_watch = 'System default'
|
||||
default_notification_format = 'Text'
|
||||
default_notification_format = 'HTML Color'
|
||||
default_notification_body = '{{watch_url}} had a change.\n---\n{{diff}}\n---\n'
|
||||
default_notification_title = 'ChangeDetection.io Notification - {{watch_url}}'
|
||||
|
||||
@@ -31,6 +31,7 @@ valid_notification_formats = {
|
||||
'Text': NotifyFormat.TEXT,
|
||||
'Markdown': NotifyFormat.MARKDOWN,
|
||||
'HTML': NotifyFormat.HTML,
|
||||
'HTML Color': 'htmlcolor',
|
||||
# Used only for editing a watch (not for global)
|
||||
default_notification_format_for_watch: default_notification_format_for_watch
|
||||
}
|
||||
@@ -76,9 +77,16 @@ def process_notification(n_object, datastore):
|
||||
|
||||
# Get the notification body from datastore
|
||||
n_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters)
|
||||
if n_object.get('notification_format', '').startswith('HTML'):
|
||||
n_body = n_body.replace("\n", '<br>')
|
||||
|
||||
n_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters)
|
||||
|
||||
url = url.strip()
|
||||
if url.startswith('#'):
|
||||
logger.trace(f"Skipping commented out notification URL - {url}")
|
||||
continue
|
||||
|
||||
if not url:
|
||||
logger.warning(f"Process Notification: skipping empty notification URL.")
|
||||
continue
|
||||
|
||||
@@ -33,8 +33,8 @@ class difference_detection_processor():
|
||||
|
||||
url = self.watch.link
|
||||
|
||||
# Protect against file://, file:/ access, check the real "link" without any meta "source:" etc prepended.
|
||||
if re.search(r'^file:/', url.strip(), re.IGNORECASE):
|
||||
# Protect against file:, file:/, file:// access, check the real "link" without any meta "source:" etc prepended.
|
||||
if re.search(r'^file:', url.strip(), re.IGNORECASE):
|
||||
if not strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
||||
raise Exception(
|
||||
"file:// type access is denied for security reasons."
|
||||
|
||||
@@ -30,11 +30,12 @@ $(document).ready(function () {
|
||||
})
|
||||
|
||||
const timezoneInput = $('#application-timezone');
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
if (!timezoneInput.val().trim()) {
|
||||
timezoneInput.val(timezone);
|
||||
timezoneInput.after('<div class="timezone-message">The timezone was set from your browser, <strong>be sure to press save!</strong></div>');
|
||||
if(timezoneInput.length) {
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (!timezoneInput.val().trim()) {
|
||||
timezoneInput.val(timezone);
|
||||
timezoneInput.after('<div class="timezone-message">The timezone was set from your browser, <strong>be sure to press save!</strong></div>');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -24,16 +24,28 @@ $(document).ready(function () {
|
||||
|
||||
toggleVisibility('#time_schedule_limit-enabled, #requests-time_schedule_limit-enabled', '#schedule-day-limits-wrapper', true)
|
||||
|
||||
setInterval(() => {
|
||||
let success = true;
|
||||
try {
|
||||
// Show the current local time according to either placeholder or entered TZ name
|
||||
if (timezone_text_widget.val().length) {
|
||||
$('#local-time-in-tz').text(getTimeInTimezone(timezone_text_widget.val()));
|
||||
} else {
|
||||
// So maybe use what is in the placeholder (which will be the default settings)
|
||||
$('#local-time-in-tz').text(getTimeInTimezone(timezone_text_widget.attr('placeholder')));
|
||||
}
|
||||
} catch (error) {
|
||||
success = false;
|
||||
$('#local-time-in-tz').text("");
|
||||
console.error(timezone_text_widget.val())
|
||||
}
|
||||
|
||||
$(timezone_text_widget).toggleClass('error', !success);
|
||||
|
||||
}, 500);
|
||||
|
||||
$('#schedule-day-limits-wrapper').on('change click blur', 'input, checkbox, select', function() {
|
||||
|
||||
if (timezone_text_widget.val().length) {
|
||||
document.getElementById('local-time-in-tz').textContent =
|
||||
getTimeInTimezone(timezone_text_widget.val());
|
||||
} else {
|
||||
// So maybe use what is in the placeholder (which will be the default settings)
|
||||
document.getElementById('local-time-in-tz').textContent =
|
||||
getTimeInTimezone(timezone_text_widget.attr('placeholder'));
|
||||
}
|
||||
let allOk = true;
|
||||
|
||||
// Controls setting the warning that the time could overlap into the next day
|
||||
@@ -63,14 +75,13 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
warning_text.toggle(!allOk)
|
||||
}, 500);
|
||||
});
|
||||
|
||||
$('table[id*="time_schedule_limit-saturday"], table[id*="time_schedule_limit-sunday"]').addClass("weekend-day")
|
||||
|
||||
// Presets [weekend] [business hours] etc
|
||||
$(document).on('click', '[data-template].set-schedule', function () {
|
||||
// Get the value of the 'data-template' attribute
|
||||
|
||||
switch ($(this).attr('data-template')) {
|
||||
case 'business-hours':
|
||||
$('.day-schedule table:not(.weekend-day) input[type="time"]').val('09:00')
|
||||
@@ -87,16 +98,11 @@ $(document).ready(function () {
|
||||
$('.day-schedule .weekend-day input[id*="-enabled"]').prop('checked', true);
|
||||
break;
|
||||
case 'reset':
|
||||
$('.day-schedule .day-schedule input[type="time"]').val('00:00')
|
||||
$('.day-schedule .day-schedule select[id*="-duration-hours"]').val('24');
|
||||
$('.day-schedule .day-schedule select[id*="-duration-minutes"]').val('0');
|
||||
$('.day-schedule .day-schedule input[id*="-enabled"]').prop('checked', true);
|
||||
break;
|
||||
case 'once-per-day':
|
||||
$('.day-schedule .day-schedule input[type="time"]').val('00:00')
|
||||
$('.day-schedule .day-schedule select[id*="-duration-hours"]').val('24');
|
||||
$('.day-schedule .day-schedule select[id*="-duration-minutes"]').val('0');
|
||||
$('.day-schedule .day-schedule input[id*="-enabled"]').prop('checked', true);
|
||||
|
||||
$('.day-schedule input[type="time"]').val('00:00')
|
||||
$('.day-schedule select[id*="-duration-hours"]').val('24');
|
||||
$('.day-schedule select[id*="-duration-minutes"]').val('0');
|
||||
$('.day-schedule input[id*="-enabled"]').prop('checked', true);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
#timespan-warning {
|
||||
#timespan-warning, input[id*='time_schedule_limit-timezone'].error {
|
||||
color: #ff0000;
|
||||
}
|
||||
.day-schedule.warning table {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
<div class="box-wrap inner">
|
||||
<form class="pure-form pure-form-stacked"
|
||||
action="{{ url_for('edit_page', uuid=uuid, next = request.args.get('next'), unpause_on_save = request.args.get('unpause_on_save')) }}" method="POST">
|
||||
action="{{ url_for('edit_page', uuid=uuid, next = request.args.get('next'), unpause_on_save = request.args.get('unpause_on_save'), tag = request.args.get('tag')) }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="tab-pane-inner" id="general">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<div class="box">
|
||||
|
||||
<form class="pure-form" action="{{ url_for('form_quick_watch_add') }}" method="POST" id="new-watch-form">
|
||||
<form class="pure-form" action="{{ url_for('form_quick_watch_add', tag=active_tag_uuid) }}" method="POST" id="new-watch-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" >
|
||||
<fieldset>
|
||||
<legend>Add a new change detection watch</legend>
|
||||
@@ -187,7 +187,7 @@
|
||||
<td>
|
||||
<a {% if watch.uuid in queued_uuids %}disabled="true"{% endif %} href="{{ url_for('form_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}"
|
||||
class="recheck pure-button pure-button-primary">{% if watch.uuid in queued_uuids %}Queued{% else %}Recheck{% endif %}</a>
|
||||
<a href="{{ url_for('edit_page', uuid=watch.uuid)}}#general" class="pure-button pure-button-primary">Edit</a>
|
||||
<a href="{{ url_for('edit_page', uuid=watch.uuid, tag=active_tag_uuid)}}#general" class="pure-button pure-button-primary">Edit</a>
|
||||
{% if watch.history_n >= 2 %}
|
||||
|
||||
{% if is_unviewed %}
|
||||
|
||||
@@ -113,7 +113,8 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa
|
||||
res = client.post(
|
||||
url_for("settings_page"),
|
||||
data={"application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
|
||||
"application-notification_body": 'triggered text was -{{triggered_text}}- 网站监测 内容更新了',
|
||||
# triggered_text will contain multiple lines
|
||||
"application-notification_body": 'triggered text was -{{triggered_text}}- ### 网站监测 内容更新了 ####',
|
||||
# https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#get-parameter-manipulation
|
||||
"application-notification_urls": test_notification_url,
|
||||
"application-minutes_between_check": 180,
|
||||
@@ -171,7 +172,7 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa
|
||||
assert os.path.isfile("test-datastore/notification.txt"), "Notification fired because I can see the output file"
|
||||
with open("test-datastore/notification.txt", 'rb') as f:
|
||||
response = f.read()
|
||||
assert b'-Oh yes please-' in response
|
||||
assert b'-Oh yes please' in response
|
||||
assert '网站监测 内容更新了'.encode('utf-8') in response
|
||||
|
||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||
|
||||
@@ -442,4 +442,67 @@ def test_global_send_test_notification(client, live_server, measure_memory_usage
|
||||
assert b"Error: You must have atleast one watch configured for 'test notification' to work" in res.data
|
||||
|
||||
|
||||
def _test_color_notifications(client, notification_body_token):
|
||||
|
||||
from changedetectionio.diff import ADDED_STYLE, REMOVED_STYLE
|
||||
|
||||
set_original_response()
|
||||
|
||||
if os.path.isfile("test-datastore/notification.txt"):
|
||||
os.unlink("test-datastore/notification.txt")
|
||||
|
||||
|
||||
test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://')+"?xxx={{ watch_url }}&+custom-header=123"
|
||||
|
||||
|
||||
# otherwise other settings would have already existed from previous tests in this file
|
||||
res = client.post(
|
||||
url_for("settings_page"),
|
||||
data={
|
||||
"application-fetch_backend": "html_requests",
|
||||
"application-minutes_between_check": 180,
|
||||
"application-notification_body": notification_body_token,
|
||||
"application-notification_format": "HTML Color",
|
||||
"application-notification_urls": test_notification_url,
|
||||
"application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
|
||||
},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert b'Settings updated' in res.data
|
||||
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
res = client.post(
|
||||
url_for("form_quick_watch_add"),
|
||||
data={"url": test_url, "tags": 'nice one'},
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
assert b"Watch added" in res.data
|
||||
|
||||
wait_for_all_checks(client)
|
||||
|
||||
set_modified_response()
|
||||
|
||||
|
||||
res = client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
assert b'1 watches queued for rechecking.' in res.data
|
||||
|
||||
wait_for_all_checks(client)
|
||||
time.sleep(3)
|
||||
|
||||
with open("test-datastore/notification.txt", 'r') as f:
|
||||
x = f.read()
|
||||
assert f'<span style="{REMOVED_STYLE}">Which is across multiple lines' in x
|
||||
|
||||
|
||||
client.get(
|
||||
url_for("form_delete", uuid="all"),
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
def test_html_color_notifications(client, live_server, measure_memory_usage):
|
||||
|
||||
#live_server_setup(live_server)
|
||||
_test_color_notifications(client, '{{diff}}')
|
||||
_test_color_notifications(client, '{{diff_full}}')
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import os
|
||||
|
||||
from flask import url_for
|
||||
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks
|
||||
import time
|
||||
|
||||
from .util import live_server_setup, wait_for_all_checks
|
||||
from .. import strtobool
|
||||
|
||||
|
||||
@@ -61,54 +59,44 @@ def test_bad_access(client, live_server, measure_memory_usage):
|
||||
assert b'Watch protocol is not permitted by SAFE_PROTOCOL_REGEX' in res.data
|
||||
|
||||
|
||||
def test_file_slashslash_access(client, live_server, measure_memory_usage):
|
||||
#live_server_setup(live_server)
|
||||
def _runner_test_various_file_slash(client, file_uri):
|
||||
|
||||
test_file_path = os.path.abspath(__file__)
|
||||
|
||||
# file:// is permitted by default, but it will be caught by ALLOW_FILE_URI
|
||||
client.post(
|
||||
url_for("form_quick_watch_add"),
|
||||
data={"url": f"file://{test_file_path}", "tags": ''},
|
||||
data={"url": file_uri, "tags": ''},
|
||||
follow_redirects=True
|
||||
)
|
||||
wait_for_all_checks(client)
|
||||
res = client.get(url_for("index"))
|
||||
|
||||
substrings = [b"URLs with hostname components are not permitted", b"No connection adapters were found for"]
|
||||
|
||||
|
||||
# If it is enabled at test time
|
||||
if strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
||||
res = client.get(
|
||||
url_for("preview_page", uuid="first"),
|
||||
follow_redirects=True
|
||||
)
|
||||
if file_uri.startswith('file:///'):
|
||||
# This one should be the full qualified path to the file and should get the contents of this file
|
||||
res = client.get(
|
||||
url_for("preview_page", uuid="first"),
|
||||
follow_redirects=True
|
||||
)
|
||||
assert b'_runner_test_various_file_slash' in res.data
|
||||
else:
|
||||
# This will give some error from requests or if it went to chrome, will give some other error :-)
|
||||
assert any(s in res.data for s in substrings)
|
||||
|
||||
assert b"test_file_slashslash_access" in res.data
|
||||
else:
|
||||
# Default should be here
|
||||
assert b'file:// type access is denied for security reasons.' in res.data
|
||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||
assert b'Deleted' in res.data
|
||||
|
||||
def test_file_slash_access(client, live_server, measure_memory_usage):
|
||||
#live_server_setup(live_server)
|
||||
|
||||
# file: is NOT permitted by default, so it will be caught by ALLOW_FILE_URI check
|
||||
|
||||
test_file_path = os.path.abspath(__file__)
|
||||
|
||||
# file:// is permitted by default, but it will be caught by ALLOW_FILE_URI
|
||||
client.post(
|
||||
url_for("form_quick_watch_add"),
|
||||
data={"url": f"file:/{test_file_path}", "tags": ''},
|
||||
follow_redirects=True
|
||||
)
|
||||
wait_for_all_checks(client)
|
||||
res = client.get(url_for("index"))
|
||||
|
||||
# If it is enabled at test time
|
||||
if strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
||||
# So it should permit it, but it should fall back to the 'requests' library giving an error
|
||||
# (but means it gets passed to playwright etc)
|
||||
assert b"URLs with hostname components are not permitted" in res.data
|
||||
else:
|
||||
# Default should be here
|
||||
assert b'file:// type access is denied for security reasons.' in res.data
|
||||
_runner_test_various_file_slash(client, file_uri=f"file://{test_file_path}")
|
||||
_runner_test_various_file_slash(client, file_uri=f"file:/{test_file_path}")
|
||||
_runner_test_various_file_slash(client, file_uri=f"file:{test_file_path}") # CVE-2024-56509
|
||||
|
||||
def test_xss(client, live_server, measure_memory_usage):
|
||||
#live_server_setup(live_server)
|
||||
|
||||
@@ -28,6 +28,8 @@ class update_worker(threading.Thread):
|
||||
|
||||
def queue_notification_for_watch(self, notification_q, n_object, watch):
|
||||
from changedetectionio import diff
|
||||
from changedetectionio.notification import default_notification_format_for_watch
|
||||
|
||||
dates = []
|
||||
trigger_text = ''
|
||||
|
||||
@@ -44,11 +46,21 @@ class update_worker(threading.Thread):
|
||||
else:
|
||||
snapshot_contents = "No snapshot/history available, the watch should fetch atleast once."
|
||||
|
||||
# If we ended up here with "System default"
|
||||
if n_object.get('notification_format') == default_notification_format_for_watch:
|
||||
n_object['notification_format'] = self.datastore.data['settings']['application'].get('notification_format')
|
||||
|
||||
html_colour_enable = False
|
||||
# HTML needs linebreak, but MarkDown and Text can use a linefeed
|
||||
if n_object.get('notification_format') == 'HTML':
|
||||
line_feed_sep = "<br>"
|
||||
# Snapshot will be plaintext on the disk, convert to some kind of HTML
|
||||
snapshot_contents = snapshot_contents.replace('\n', line_feed_sep)
|
||||
elif n_object.get('notification_format') == 'HTML Color':
|
||||
line_feed_sep = "<br>"
|
||||
# Snapshot will be plaintext on the disk, convert to some kind of HTML
|
||||
snapshot_contents = snapshot_contents.replace('\n', line_feed_sep)
|
||||
html_colour_enable = True
|
||||
else:
|
||||
line_feed_sep = "\n"
|
||||
|
||||
@@ -69,9 +81,9 @@ class update_worker(threading.Thread):
|
||||
|
||||
n_object.update({
|
||||
'current_snapshot': snapshot_contents,
|
||||
'diff': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep),
|
||||
'diff': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, html_colour=html_colour_enable),
|
||||
'diff_added': diff.render_diff(prev_snapshot, current_snapshot, include_removed=False, line_feed_sep=line_feed_sep),
|
||||
'diff_full': diff.render_diff(prev_snapshot, current_snapshot, include_equal=True, line_feed_sep=line_feed_sep),
|
||||
'diff_full': diff.render_diff(prev_snapshot, current_snapshot, include_equal=True, line_feed_sep=line_feed_sep, html_colour=html_colour_enable),
|
||||
'diff_patch': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, patch_format=True),
|
||||
'diff_removed': diff.render_diff(prev_snapshot, current_snapshot, include_added=False, line_feed_sep=line_feed_sep),
|
||||
'notification_timestamp': now,
|
||||
|
||||
13
debian/changedetection.io.service
vendored
Normal file
13
debian/changedetection.io.service
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=changedetection.io Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=changedetio
|
||||
Group=changedetio
|
||||
WorkingDirectory=/opt/changedetection.io
|
||||
ExecStart=/opt/changedetection.io/bin/python -m changedetectionio
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
13
debian/changedetection.io/lib/systemd/system/changedetection.io.service
vendored
Normal file
13
debian/changedetection.io/lib/systemd/system/changedetection.io.service
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=changedetection.io Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=changedetio
|
||||
Group=changedetio
|
||||
WorkingDirectory=/opt/changedetection.io
|
||||
ExecStart=/opt/changedetection.io/bin/python -m changedetectionio
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
5
debian/changelog
vendored
Normal file
5
debian/changelog
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
changedetection.io (0.48.5) unstable; urgency=medium
|
||||
|
||||
* Initial release.
|
||||
|
||||
-- Your Name <your.email@example.com> Wed, 01 Nov 2023 12:00:00 +0000
|
||||
14
debian/control
vendored
Normal file
14
debian/control
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
Source: changedetection.io
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: Your Name <your.email@example.com>
|
||||
Build-Depends: debhelper-compat (= 13), dh-virtualenv, dh-python, python3-all (>= 3.10), python3-all (<< 3.13), python3.10, python3.10-venv
|
||||
Standards-Version: 4.6.0
|
||||
Rules-Requires-Root: no
|
||||
Homepage: https://github.com/dgtlmoon/changedetection.io
|
||||
|
||||
Package: python-changedetection.io
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, python3 (>= 3.10), python3 (<< 3.13)
|
||||
Description: Web page change detection - Python application
|
||||
A web-based application for monitoring web pages for changes.
|
||||
1
debian/debhelper-build-stamp
vendored
Normal file
1
debian/debhelper-build-stamp
vendored
Normal file
@@ -0,0 +1 @@
|
||||
python-changedetection.io
|
||||
1
debian/install
vendored
Normal file
1
debian/install
vendored
Normal file
@@ -0,0 +1 @@
|
||||
debian/changedetection.io.service lib/systemd/system/
|
||||
23
debian/postinst
vendored
Executable file
23
debian/postinst
vendored
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Determine Python version
|
||||
PYTHON_VERSION=$(ls /usr/bin/python3.1[0-2] | head -n1 | xargs basename)
|
||||
|
||||
# Create 'changedetio' user if it doesn't exist
|
||||
if ! id "changedetio" >/dev/null 2>&1; then
|
||||
adduser --system --group --no-create-home changedetio
|
||||
fi
|
||||
|
||||
# Set ownership of the installation directory
|
||||
chown -R changedetio:changedetio /opt/changedetection.io
|
||||
|
||||
# Update the systemd service file if necessary
|
||||
sed -i "s|ExecStart=.*|ExecStart=/opt/changedetection.io/bin/$PYTHON_VERSION -m changedetectionio|" /lib/systemd/system/changedetection.io.service
|
||||
|
||||
# Enable and start the service
|
||||
systemctl daemon-reload
|
||||
systemctl enable changedetection.io.service
|
||||
systemctl start changedetection.io.service
|
||||
|
||||
exit 0
|
||||
11
debian/postrm
vendored
Executable file
11
debian/postrm
vendored
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Remove user on purge
|
||||
if [ "$1" = "purge" ]; then
|
||||
deluser --system changedetio
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
exit 0
|
||||
8
debian/prerm
vendored
Executable file
8
debian/prerm
vendored
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Stop and disable the service
|
||||
systemctl stop changedetection.io.service
|
||||
systemctl disable changedetection.io.service
|
||||
|
||||
exit 0
|
||||
10
debian/rules
vendored
Executable file
10
debian/rules
vendored
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
%:
|
||||
dh $@ --with python-virtualenv --buildsystem=pybuild
|
||||
|
||||
override_dh_virtualenv:
|
||||
dh_virtualenv --sourcedirectory=. \
|
||||
--install-suffix='' \
|
||||
--requirements=requirements.txt \
|
||||
--python=/usr/bin/python3.11
|
||||
BIN
docs/scheduler.png
Normal file
BIN
docs/scheduler.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -95,3 +95,5 @@ babel
|
||||
# Needed for > 3.10, https://github.com/microsoft/playwright-python/issues/2096
|
||||
greenlet >= 3.0.3
|
||||
|
||||
# Scheduler - Windows seemed to miss a lot of default timezone info (even "UTC" !)
|
||||
tzdata
|
||||
|
||||
Reference in New Issue
Block a user