mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-10-31 22:57:18 +00:00
Compare commits
32 Commits
2286-remov
...
refactor-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d579e26cd6 | ||
|
|
f8fe5c8d41 | ||
|
|
bddf9af584 | ||
|
|
643a45b5e2 | ||
|
|
e9f1cc91c7 | ||
|
|
080526eb65 | ||
|
|
c9552d2319 | ||
|
|
3c14273021 | ||
|
|
547ab70ea3 | ||
|
|
5df5d0fbe7 | ||
|
|
815cba11ca | ||
|
|
3aed4e5af9 | ||
|
|
3618c389c6 | ||
|
|
d127214d8f | ||
|
|
c0f000b1d1 | ||
|
|
ee5294740a | ||
|
|
bd6eda696c | ||
|
|
1ba29655f5 | ||
|
|
830a0a3a82 | ||
|
|
e110b3ee93 | ||
|
|
3ae9bfa6f9 | ||
|
|
6f3c3b7dfb | ||
|
|
74707909f1 | ||
|
|
d4dac23ba1 | ||
|
|
f9954f93f3 | ||
|
|
1a43b112dc | ||
|
|
db59bf73e1 | ||
|
|
8aac7bccbe | ||
|
|
9449c59fbb | ||
|
|
21f4ba2208 | ||
|
|
daef1cd036 | ||
|
|
56b365df40 |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -4,6 +4,10 @@ updates:
|
|||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
"caronc/apprise":
|
||||||
|
versioning-strategy: "increase"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
groups:
|
groups:
|
||||||
all:
|
all:
|
||||||
patterns:
|
patterns:
|
||||||
|
|||||||
1
.github/workflows/test-only.yml
vendored
1
.github/workflows/test-only.yml
vendored
@@ -59,6 +59,7 @@ jobs:
|
|||||||
echo "run test with unittest"
|
echo "run test with unittest"
|
||||||
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_notification_diff'
|
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_notification_diff'
|
||||||
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_watch_model'
|
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_watch_model'
|
||||||
|
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_jinja2_security'
|
||||||
|
|
||||||
# All tests
|
# All tests
|
||||||
echo "run test with pytest"
|
echo "run test with pytest"
|
||||||
|
|||||||
@@ -257,13 +257,7 @@ Supports managing the website watch list [via our API](https://changedetection.i
|
|||||||
Do you use changedetection.io to make money? does it save you time or money? Does it make your life easier? less stressful? Remember, we write this software when we should be doing actual paid work, we have to buy food and pay rent just like you.
|
Do you use changedetection.io to make money? does it save you time or money? Does it make your life easier? less stressful? Remember, we write this software when we should be doing actual paid work, we have to buy food and pay rent just like you.
|
||||||
|
|
||||||
|
|
||||||
Firstly, consider taking out an officially supported [website change detection subscription](https://changedetection.io?src=github) , even if you don't use it, you still get the warm fuzzy feeling of helping out the project. (And who knows, you might just use it!)
|
Consider taking out an officially supported [website change detection subscription](https://changedetection.io?src=github) , even if you don't use it, you still get the warm fuzzy feeling of helping out the project. (And who knows, you might just use it!)
|
||||||
|
|
||||||
Or directly donate an amount PayPal [](https://www.paypal.com/donate/?hosted_button_id=7CP6HR9ZCNDYJ)
|
|
||||||
|
|
||||||
Or BTC `1PLFN327GyUarpJd7nVe7Reqg9qHx5frNn`
|
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/docs/btc-support.png" style="max-width:50%;" alt="Support us!" />
|
|
||||||
|
|
||||||
## Commercial Support
|
## Commercial Support
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||||
|
|
||||||
__version__ = '0.45.17'
|
__version__ = '0.45.22'
|
||||||
|
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
import os
|
import os
|
||||||
#os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
|
os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
|
||||||
import eventlet
|
import eventlet
|
||||||
import eventlet.wsgi
|
import eventlet.wsgi
|
||||||
import getopt
|
import getopt
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
|
|
||||||
from flask_expects_json import expects_json
|
from flask_expects_json import expects_json
|
||||||
from changedetectionio import queuedWatchMetaData
|
from changedetectionio import queuedWatchMetaData
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from flask import Blueprint, request, make_response
|
from flask import Blueprint, request, make_response
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from random import randint
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from changedetectionio.content_fetchers.base import manage_user_agent
|
from changedetectionio.content_fetchers.base import manage_user_agent
|
||||||
|
from changedetectionio.safe_jinja import render as jinja_render
|
||||||
|
|
||||||
# Two flags, tell the JS which of the "Selector" or "Value" field should be enabled in the front end
|
# Two flags, tell the JS which of the "Selector" or "Value" field should be enabled in the front end
|
||||||
# 0- off, 1- on
|
# 0- off, 1- on
|
||||||
@@ -64,14 +65,12 @@ class steppable_browser_interface():
|
|||||||
action_handler = getattr(self, "action_" + call_action_name)
|
action_handler = getattr(self, "action_" + call_action_name)
|
||||||
|
|
||||||
# Support for Jinja2 variables in the value and selector
|
# Support for Jinja2 variables in the value and selector
|
||||||
from jinja2 import Environment
|
|
||||||
jinja2_env = Environment(extensions=['jinja2_time.TimeExtension'])
|
|
||||||
|
|
||||||
if selector and ('{%' in selector or '{{' in selector):
|
if selector and ('{%' in selector or '{{' in selector):
|
||||||
selector = str(jinja2_env.from_string(selector).render())
|
selector = jinja_render(template_str=selector)
|
||||||
|
|
||||||
if optional_value and ('{%' in optional_value or '{{' in optional_value):
|
if optional_value and ('{%' in optional_value or '{{' in optional_value):
|
||||||
optional_value = str(jinja2_env.from_string(optional_value).render())
|
optional_value = jinja_render(template_str=optional_value)
|
||||||
|
|
||||||
action_handler(selector, optional_value)
|
action_handler(selector, optional_value)
|
||||||
self.page.wait_for_timeout(1.5 * 1000)
|
self.page.wait_for_timeout(1.5 * 1000)
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
|||||||
import time
|
import time
|
||||||
from changedetectionio.content_fetchers import exceptions as content_fetcher_exceptions
|
from changedetectionio.content_fetchers import exceptions as content_fetcher_exceptions
|
||||||
from changedetectionio.processors import text_json_diff
|
from changedetectionio.processors import text_json_diff
|
||||||
|
from changedetectionio.safe_jinja import render as jinja_render
|
||||||
|
|
||||||
status = {'status': '', 'length': 0, 'text': ''}
|
status = {'status': '', 'length': 0, 'text': ''}
|
||||||
from jinja2 import Environment, BaseLoader
|
|
||||||
|
|
||||||
contents = ''
|
contents = ''
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -64,7 +64,9 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
|||||||
status.update({'status': 'OK', 'length': len(contents), 'text': ''})
|
status.update({'status': 'OK', 'length': len(contents), 'text': ''})
|
||||||
|
|
||||||
if status.get('text'):
|
if status.get('text'):
|
||||||
status['text'] = Environment(loader=BaseLoader()).from_string('{{text|e}}').render({'text': status['text']})
|
# parse 'text' as text for safety
|
||||||
|
v = {'text': status['text']}
|
||||||
|
status['text'] = jinja_render(template_str='{{text|e}}', **v)
|
||||||
|
|
||||||
status['time'] = "{:.2f}s".format(time.time() - now)
|
status['time'] = "{:.2f}s".format(time.time() - now)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from flask import Blueprint, flash, redirect, url_for
|
from flask import Blueprint, flash, redirect, url_for
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from changedetectionio.store import ChangeDetectionStore
|
from changedetectionio.store import ChangeDetectionStore
|
||||||
|
|||||||
@@ -12,9 +12,15 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
|||||||
from .form import SingleTag
|
from .form import SingleTag
|
||||||
add_form = SingleTag(request.form)
|
add_form = SingleTag(request.form)
|
||||||
sorted_tags = sorted(datastore.data['settings']['application'].get('tags').items(), key=lambda x: x[1]['title'])
|
sorted_tags = sorted(datastore.data['settings']['application'].get('tags').items(), key=lambda x: x[1]['title'])
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
tag_count = Counter(tag for watch in datastore.data['watching'].values() if watch.get('tags') for tag in watch['tags'])
|
||||||
|
|
||||||
output = render_template("groups-overview.html",
|
output = render_template("groups-overview.html",
|
||||||
form=add_form,
|
|
||||||
available_tags=sorted_tags,
|
available_tags=sorted_tags,
|
||||||
|
form=add_form,
|
||||||
|
tag_count=tag_count
|
||||||
)
|
)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
|
{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
|
||||||
{% from '_common_fields.jinja' import render_common_settings_form %}
|
{% from '_common_fields.html' import render_common_settings_form %}
|
||||||
<script>
|
<script>
|
||||||
const notification_base_url="{{url_for('ajax_callback_send_notification_test', watch_uuid=uuid)}}";
|
const notification_base_url="{{url_for('ajax_callback_send_notification_test', mode="group-settings")}}";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% from '_helpers.jinja' import render_simple_field, render_field %}
|
{% from '_helpers.html' import render_simple_field, render_field %}
|
||||||
<script src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
|
<script src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
<th># Watches</th>
|
||||||
<th>Tag / Label name</th>
|
<th>Tag / Label name</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -45,7 +46,8 @@
|
|||||||
<td class="watch-controls">
|
<td class="watch-controls">
|
||||||
<a class="link-mute state-{{'on' if tag.notification_muted else 'off'}}" href="{{url_for('tags.mute', uuid=tag.uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications" class="icon icon-mute" ></a>
|
<a class="link-mute state-{{'on' if tag.notification_muted else 'off'}}" href="{{url_for('tags.mute', uuid=tag.uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications" class="icon icon-mute" ></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="title-col inline">{{tag.title}}</td>
|
<td>{{ "{:,}".format(tag_count[uuid]) if uuid in tag_count else 0 }}</td>
|
||||||
|
<td class="title-col inline"> <a href="{{url_for('index', tag=uuid) }}">{{ tag.title }}</a></td>
|
||||||
<td>
|
<td>
|
||||||
<a class="pure-button pure-button-primary" href="{{ url_for('tags.form_tag_edit', uuid=uuid) }}">Edit</a>
|
<a class="pure-button pure-button-primary" href="{{ url_for('tags.form_tag_edit', uuid=uuid) }}">Edit</a>
|
||||||
<a class="pure-button pure-button-primary" href="{{ url_for('tags.delete', uuid=uuid) }}" title="Deletes and removes tag">Delete</a>
|
<a class="pure-button pure-button-primary" href="{{ url_for('tags.delete', uuid=uuid) }}" title="Deletes and removes tag">Delete</a>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import sys
|
import sys
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from changedetectionio.content_fetchers.exceptions import BrowserStepsStepException
|
from changedetectionio.content_fetchers.exceptions import BrowserStepsStepException
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -123,8 +123,7 @@ class Fetcher():
|
|||||||
def iterate_browser_steps(self):
|
def iterate_browser_steps(self):
|
||||||
from changedetectionio.blueprint.browser_steps.browser_steps import steppable_browser_interface
|
from changedetectionio.blueprint.browser_steps.browser_steps import steppable_browser_interface
|
||||||
from playwright._impl._errors import TimeoutError, Error
|
from playwright._impl._errors import TimeoutError, Error
|
||||||
from jinja2 import Environment
|
from changedetectionio.safe_jinja import render as jinja_render
|
||||||
jinja2_env = Environment(extensions=['jinja2_time.TimeExtension'])
|
|
||||||
|
|
||||||
step_n = 0
|
step_n = 0
|
||||||
|
|
||||||
@@ -143,9 +142,9 @@ class Fetcher():
|
|||||||
selector = step['selector']
|
selector = step['selector']
|
||||||
# Support for jinja2 template in step values, with date module added
|
# Support for jinja2 template in step values, with date module added
|
||||||
if '{%' in step['optional_value'] or '{{' in step['optional_value']:
|
if '{%' in step['optional_value'] or '{{' in step['optional_value']:
|
||||||
optional_value = str(jinja2_env.from_string(step['optional_value']).render())
|
optional_value = jinja_render(template_str=step['optional_value'])
|
||||||
if '{%' in step['selector'] or '{{' in step['selector']:
|
if '{%' in step['selector'] or '{{' in step['selector']:
|
||||||
selector = str(jinja2_env.from_string(step['selector']).render())
|
selector = jinja_render(template_str=step['selector'])
|
||||||
|
|
||||||
getattr(interface, "call_action")(action_name=step['operation'],
|
getattr(interface, "call_action")(action_name=step['operation'],
|
||||||
selector=selector,
|
selector=selector,
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import os
|
|||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from .safe_jinja import render as jinja_render
|
||||||
|
from changedetectionio.strtobool import strtobool
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from distutils.util import strtobool
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from threading import Event
|
from threading import Event
|
||||||
|
|
||||||
import flask_login
|
import flask_login
|
||||||
import pytz
|
import pytz
|
||||||
import timeago
|
import timeago
|
||||||
@@ -319,8 +319,6 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
@app.route("/rss", methods=['GET'])
|
@app.route("/rss", methods=['GET'])
|
||||||
def rss():
|
def rss():
|
||||||
from jinja2 import Environment, BaseLoader
|
|
||||||
jinja2_env = Environment(loader=BaseLoader)
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
# Always requires token set
|
# Always requires token set
|
||||||
app_rss_token = datastore.data['settings']['application'].get('rss_access_token')
|
app_rss_token = datastore.data['settings']['application'].get('rss_access_token')
|
||||||
@@ -388,7 +386,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
# @todo Make this configurable and also consider html-colored markup
|
# @todo Make this configurable and also consider html-colored markup
|
||||||
# @todo User could decide if <link> goes to the diff page, or to the watch link
|
# @todo User could decide if <link> goes to the diff page, or to the watch link
|
||||||
rss_template = "<html><body>\n<h4><a href=\"{{watch_url}}\">{{watch_title}}</a></h4>\n<p>{{html_diff}}</p>\n</body></html>\n"
|
rss_template = "<html><body>\n<h4><a href=\"{{watch_url}}\">{{watch_title}}</a></h4>\n<p>{{html_diff}}</p>\n</body></html>\n"
|
||||||
content = jinja2_env.from_string(rss_template).render(watch_title=watch_title, html_diff=html_diff, watch_url=watch.link)
|
content = jinja_render(template_str=rss_template, watch_title=watch_title, html_diff=html_diff, watch_url=watch.link)
|
||||||
|
|
||||||
fe.content(content=content, type='CDATA')
|
fe.content(content=content, type='CDATA')
|
||||||
|
|
||||||
@@ -452,6 +450,8 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
if search_q:
|
if search_q:
|
||||||
if (watch.get('title') and search_q in watch.get('title').lower()) or search_q in watch.get('url', '').lower():
|
if (watch.get('title') and search_q in watch.get('title').lower()) or search_q in watch.get('url', '').lower():
|
||||||
sorted_watches.append(watch)
|
sorted_watches.append(watch)
|
||||||
|
elif watch.get('last_error') and search_q in watch.get('last_error').lower():
|
||||||
|
sorted_watches.append(watch)
|
||||||
else:
|
else:
|
||||||
sorted_watches.append(watch)
|
sorted_watches.append(watch)
|
||||||
|
|
||||||
@@ -519,26 +519,29 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
notification_urls = request.form['notification_urls'].strip().splitlines()
|
notification_urls = request.form['notification_urls'].strip().splitlines()
|
||||||
|
|
||||||
if not notification_urls:
|
if not notification_urls:
|
||||||
logger.debug("Test notification - Trying by group/tag")
|
logger.debug("Test notification - Trying by group/tag in the edit form if available")
|
||||||
if request.form['tags'].strip():
|
# On an edit page, we should also fire off to the tags if they have notifications
|
||||||
|
if request.form.get('tags') and request.form['tags'].strip():
|
||||||
for k in request.form['tags'].split(','):
|
for k in request.form['tags'].split(','):
|
||||||
tag = datastore.tag_exists_by_name(k.strip())
|
tag = datastore.tag_exists_by_name(k.strip())
|
||||||
notification_urls = tag.get('notifications_urls') if tag and tag.get('notifications_urls') else None
|
notification_urls = tag.get('notifications_urls') if tag and tag.get('notifications_urls') else None
|
||||||
|
|
||||||
if not notification_urls:
|
is_global_settings_form = request.args.get('mode', '') == 'global-settings'
|
||||||
|
is_group_settings_form = request.args.get('mode', '') == 'group-settings'
|
||||||
|
if not notification_urls and not is_global_settings_form and not is_group_settings_form:
|
||||||
|
# In the global settings, use only what is typed currently in the text box
|
||||||
logger.debug("Test notification - Trying by global system settings notifications")
|
logger.debug("Test notification - Trying by global system settings notifications")
|
||||||
if datastore.data['settings']['application'].get('notification_urls'):
|
if datastore.data['settings']['application'].get('notification_urls'):
|
||||||
notification_urls = datastore.data['settings']['application']['notification_urls']
|
notification_urls = datastore.data['settings']['application']['notification_urls']
|
||||||
|
|
||||||
|
|
||||||
if not notification_urls:
|
if not notification_urls:
|
||||||
return make_response({'error': 'No Notification URLs set'}, 400)
|
return 'No Notification URLs set/found'
|
||||||
|
|
||||||
for n_url in notification_urls:
|
for n_url in notification_urls:
|
||||||
if len(n_url.strip()):
|
if len(n_url.strip()):
|
||||||
if not apobj.add(n_url):
|
if not apobj.add(n_url):
|
||||||
message = '{} is not a valid AppRise URL.'.format(n_url)
|
return f'Error - {n_url} is not a valid AppRise URL.'
|
||||||
return make_response({'error': message}, 400)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# use the same as when it is triggered, but then override it with the form test values
|
# use the same as when it is triggered, but then override it with the form test values
|
||||||
@@ -563,7 +566,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return make_response({'error': str(e)}, 400)
|
return make_response({'error': str(e)}, 400)
|
||||||
|
|
||||||
return 'OK'
|
return 'OK - Sent test notifications'
|
||||||
|
|
||||||
|
|
||||||
@app.route("/clear_history/<string:uuid>", methods=['GET'])
|
@app.route("/clear_history/<string:uuid>", methods=['GET'])
|
||||||
@@ -600,6 +603,12 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
output = render_template("clear_all_history.html")
|
output = render_template("clear_all_history.html")
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def _watch_has_tag_options_set(watch):
|
||||||
|
"""This should be fixed better so that Tag is some proper Model, a tag is just a Watch also"""
|
||||||
|
for tag_uuid, tag in datastore.data['settings']['application'].get('tags', {}).items():
|
||||||
|
if tag_uuid in watch.get('tags', []) and (tag.get('include_filters') or tag.get('subtractive_selectors')):
|
||||||
|
return True
|
||||||
|
|
||||||
@app.route("/edit/<string:uuid>", methods=['GET', 'POST'])
|
@app.route("/edit/<string:uuid>", methods=['GET', 'POST'])
|
||||||
@login_optionally_required
|
@login_optionally_required
|
||||||
# https://stackoverflow.com/questions/42984453/wtforms-populate-form-with-data-if-data-exists
|
# https://stackoverflow.com/questions/42984453/wtforms-populate-form-with-data-if-data-exists
|
||||||
@@ -610,7 +619,6 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
from .blueprint.browser_steps.browser_steps import browser_step_ui_config
|
from .blueprint.browser_steps.browser_steps import browser_step_ui_config
|
||||||
from . import processors
|
from . import processors
|
||||||
|
|
||||||
using_default_check_time = True
|
|
||||||
# More for testing, possible to return the first/only
|
# More for testing, possible to return the first/only
|
||||||
if not datastore.data['watching'].keys():
|
if not datastore.data['watching'].keys():
|
||||||
flash("No watches to edit", "error")
|
flash("No watches to edit", "error")
|
||||||
@@ -635,10 +643,6 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
# be sure we update with a copy instead of accidently editing the live object by reference
|
# be sure we update with a copy instead of accidently editing the live object by reference
|
||||||
default = deepcopy(datastore.data['watching'][uuid])
|
default = deepcopy(datastore.data['watching'][uuid])
|
||||||
|
|
||||||
# Show system wide default if nothing configured
|
|
||||||
if all(value == 0 or value == None for value in datastore.data['watching'][uuid]['time_between_check'].values()):
|
|
||||||
default['time_between_check'] = deepcopy(datastore.data['settings']['requests']['time_between_check'])
|
|
||||||
|
|
||||||
# Defaults for proxy choice
|
# Defaults for proxy choice
|
||||||
if datastore.proxy_list is not None: # When enabled
|
if datastore.proxy_list is not None: # When enabled
|
||||||
# @todo
|
# @todo
|
||||||
@@ -676,18 +680,8 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
if request.args.get('unpause_on_save'):
|
if request.args.get('unpause_on_save'):
|
||||||
extra_update_obj['paused'] = False
|
extra_update_obj['paused'] = False
|
||||||
# Re #110, if they submit the same as the default value, set it to None, so we continue to follow the default
|
|
||||||
# Assume we use the default value, unless something relevant is different, then use the form value
|
|
||||||
# values could be None, 0 etc.
|
|
||||||
# Set to None unless the next for: says that something is different
|
|
||||||
extra_update_obj['time_between_check'] = dict.fromkeys(form.time_between_check.data)
|
|
||||||
for k, v in form.time_between_check.data.items():
|
|
||||||
if v and v != datastore.data['settings']['requests']['time_between_check'][k]:
|
|
||||||
extra_update_obj['time_between_check'] = form.time_between_check.data
|
|
||||||
using_default_check_time = False
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
|
extra_update_obj['time_between_check'] = form.time_between_check.data
|
||||||
|
|
||||||
# Ignore text
|
# Ignore text
|
||||||
form_ignore_text = form.ignore_text.data
|
form_ignore_text = form.ignore_text.data
|
||||||
@@ -768,8 +762,8 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
extra_title=f" - Edit - {watch.label}",
|
extra_title=f" - Edit - {watch.label}",
|
||||||
form=form,
|
form=form,
|
||||||
has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False,
|
has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False,
|
||||||
has_empty_checktime=using_default_check_time,
|
|
||||||
has_extra_headers_file=len(datastore.get_all_headers_in_textfile_for_watch(uuid=uuid)) > 0,
|
has_extra_headers_file=len(datastore.get_all_headers_in_textfile_for_watch(uuid=uuid)) > 0,
|
||||||
|
has_special_tag_options=_watch_has_tag_options_set(watch=watch),
|
||||||
is_html_webdriver=is_html_webdriver,
|
is_html_webdriver=is_html_webdriver,
|
||||||
jq_support=jq_support,
|
jq_support=jq_support,
|
||||||
playwright_enabled=os.getenv('PLAYWRIGHT_DRIVER_URL', False),
|
playwright_enabled=os.getenv('PLAYWRIGHT_DRIVER_URL', False),
|
||||||
@@ -853,11 +847,13 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
flash("An error occurred, please see below.", "error")
|
flash("An error occurred, please see below.", "error")
|
||||||
|
|
||||||
output = render_template("settings.html",
|
output = render_template("settings.html",
|
||||||
form=form,
|
|
||||||
hide_remove_pass=os.getenv("SALTED_PASS", False),
|
|
||||||
api_key=datastore.data['settings']['application'].get('api_access_token'),
|
api_key=datastore.data['settings']['application'].get('api_access_token'),
|
||||||
emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
|
emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
|
||||||
settings_application=datastore.data['settings']['application'])
|
form=form,
|
||||||
|
hide_remove_pass=os.getenv("SALTED_PASS", False),
|
||||||
|
min_system_recheck_seconds=int(os.getenv('MINIMUM_SECONDS_RECHECK_TIME', 3)),
|
||||||
|
settings_application=datastore.data['settings']['application']
|
||||||
|
)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@@ -1293,9 +1289,8 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
url = request.form.get('url').strip()
|
url = request.form.get('url').strip()
|
||||||
if datastore.url_exists(url):
|
if datastore.url_exists(url):
|
||||||
flash('The URL {} already exists'.format(url), "error")
|
flash(f'Warning, URL {url} already exists', "notice")
|
||||||
return redirect(url_for('index'))
|
|
||||||
|
|
||||||
add_paused = request.form.get('edit_and_watch_submit_button') != None
|
add_paused = request.form.get('edit_and_watch_submit_button') != None
|
||||||
processor = request.form.get('processor', 'text_json_diff')
|
processor = request.form.get('processor', 'text_json_diff')
|
||||||
new_uuid = datastore.add_watch(url=url, tag=request.form.get('tags').strip(), extras={'paused': add_paused, 'processor': processor})
|
new_uuid = datastore.add_watch(url=url, tag=request.form.get('tags').strip(), extras={'paused': add_paused, 'processor': processor})
|
||||||
@@ -1659,14 +1654,14 @@ def notification_runner():
|
|||||||
# Trim the log length
|
# Trim the log length
|
||||||
notification_debug_log = notification_debug_log[-100:]
|
notification_debug_log = notification_debug_log[-100:]
|
||||||
|
|
||||||
# Thread runner to check every minute, look for new watches to feed into the Queue.
|
# Threaded runner, look for new watches to feed into the Queue.
|
||||||
def ticker_thread_check_time_launch_checks():
|
def ticker_thread_check_time_launch_checks():
|
||||||
import random
|
import random
|
||||||
from changedetectionio import update_worker
|
from changedetectionio import update_worker
|
||||||
|
|
||||||
proxy_last_called_time = {}
|
proxy_last_called_time = {}
|
||||||
|
|
||||||
recheck_time_minimum_seconds = int(os.getenv('MINIMUM_SECONDS_RECHECK_TIME', 20))
|
recheck_time_minimum_seconds = int(os.getenv('MINIMUM_SECONDS_RECHECK_TIME', 3))
|
||||||
logger.debug(f"System env MINIMUM_SECONDS_RECHECK_TIME {recheck_time_minimum_seconds}")
|
logger.debug(f"System env MINIMUM_SECONDS_RECHECK_TIME {recheck_time_minimum_seconds}")
|
||||||
|
|
||||||
# Spin up Workers that do the fetching
|
# Spin up Workers that do the fetching
|
||||||
@@ -1720,9 +1715,7 @@ def ticker_thread_check_time_launch_checks():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# If they supplied an individual entry minutes to threshold.
|
# If they supplied an individual entry minutes to threshold.
|
||||||
|
threshold = recheck_time_system_seconds if watch.get('time_between_check_use_default') else watch.threshold_seconds()
|
||||||
watch_threshold_seconds = watch.threshold_seconds()
|
|
||||||
threshold = watch_threshold_seconds if watch_threshold_seconds > 0 else recheck_time_system_seconds
|
|
||||||
|
|
||||||
# #580 - Jitter plus/minus amount of time to make the check seem more random to the server
|
# #580 - Jitter plus/minus amount of time to make the check seem more random to the server
|
||||||
jitter = datastore.data['settings']['requests'].get('jitter_seconds', 0)
|
jitter = datastore.data['settings']['requests'].get('jitter_seconds', 0)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
|
|
||||||
from wtforms import (
|
from wtforms import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
@@ -236,21 +236,26 @@ class ValidateJinja2Template(object):
|
|||||||
def __call__(self, form, field):
|
def __call__(self, form, field):
|
||||||
from changedetectionio import notification
|
from changedetectionio import notification
|
||||||
|
|
||||||
from jinja2 import Environment, BaseLoader, TemplateSyntaxError, UndefinedError
|
from jinja2 import BaseLoader, TemplateSyntaxError, UndefinedError
|
||||||
|
from jinja2.sandbox import ImmutableSandboxedEnvironment
|
||||||
from jinja2.meta import find_undeclared_variables
|
from jinja2.meta import find_undeclared_variables
|
||||||
|
import jinja2.exceptions
|
||||||
|
|
||||||
|
# Might be a list of text, or might be just text (like from the apprise url list)
|
||||||
|
joined_data = ' '.join(map(str, field.data)) if isinstance(field.data, list) else f"{field.data}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jinja2_env = Environment(loader=BaseLoader)
|
jinja2_env = ImmutableSandboxedEnvironment(loader=BaseLoader)
|
||||||
jinja2_env.globals.update(notification.valid_tokens)
|
jinja2_env.globals.update(notification.valid_tokens)
|
||||||
|
jinja2_env.from_string(joined_data).render()
|
||||||
rendered = jinja2_env.from_string(field.data).render()
|
|
||||||
except TemplateSyntaxError as e:
|
except TemplateSyntaxError as e:
|
||||||
raise ValidationError(f"This is not a valid Jinja2 template: {e}") from e
|
raise ValidationError(f"This is not a valid Jinja2 template: {e}") from e
|
||||||
except UndefinedError as e:
|
except UndefinedError as e:
|
||||||
raise ValidationError(f"A variable or function is not defined: {e}") from e
|
raise ValidationError(f"A variable or function is not defined: {e}") from e
|
||||||
|
except jinja2.exceptions.SecurityError as e:
|
||||||
|
raise ValidationError(f"This is not a valid Jinja2 template: {e}") from e
|
||||||
|
|
||||||
ast = jinja2_env.parse(field.data)
|
ast = jinja2_env.parse(joined_data)
|
||||||
undefined = ", ".join(find_undeclared_variables(ast))
|
undefined = ", ".join(find_undeclared_variables(ast))
|
||||||
if undefined:
|
if undefined:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
@@ -415,7 +420,7 @@ class quickWatchForm(Form):
|
|||||||
# Common to a single watch and the global settings
|
# Common to a single watch and the global settings
|
||||||
class commonSettingsForm(Form):
|
class commonSettingsForm(Form):
|
||||||
|
|
||||||
notification_urls = StringListField('Notification URL List', validators=[validators.Optional(), ValidateAppRiseServers()])
|
notification_urls = StringListField('Notification URL List', validators=[validators.Optional(), ValidateAppRiseServers(), ValidateJinja2Template()])
|
||||||
notification_title = StringField('Notification Title', default='ChangeDetection.io Notification - {{ watch_url }}', validators=[validators.Optional(), ValidateJinja2Template()])
|
notification_title = StringField('Notification Title', default='ChangeDetection.io Notification - {{ watch_url }}', validators=[validators.Optional(), ValidateJinja2Template()])
|
||||||
notification_body = TextAreaField('Notification Body', default='{{ watch_url }} had a change.', validators=[validators.Optional(), ValidateJinja2Template()])
|
notification_body = TextAreaField('Notification Body', default='{{ watch_url }} had a change.', validators=[validators.Optional(), ValidateJinja2Template()])
|
||||||
notification_format = SelectField('Notification format', choices=valid_notification_formats.keys())
|
notification_format = SelectField('Notification format', choices=valid_notification_formats.keys())
|
||||||
@@ -448,6 +453,7 @@ class watchForm(commonSettingsForm):
|
|||||||
tags = StringTagUUID('Group tag', [validators.Optional()], default='')
|
tags = StringTagUUID('Group tag', [validators.Optional()], default='')
|
||||||
|
|
||||||
time_between_check = FormField(TimeBetweenCheckForm)
|
time_between_check = FormField(TimeBetweenCheckForm)
|
||||||
|
time_between_check_use_default = BooleanField('Use global settings for time between check', default=False)
|
||||||
|
|
||||||
include_filters = StringListField('CSS/JSONPath/JQ/XPath Filters', [ValidateCSSJSONXPATHInput()], default='')
|
include_filters = StringListField('CSS/JSONPath/JQ/XPath Filters', [ValidateCSSJSONXPATHInput()], default='')
|
||||||
|
|
||||||
@@ -499,11 +505,9 @@ class watchForm(commonSettingsForm):
|
|||||||
result = False
|
result = False
|
||||||
|
|
||||||
# Attempt to validate jinja2 templates in the URL
|
# Attempt to validate jinja2 templates in the URL
|
||||||
from jinja2 import Environment
|
|
||||||
# Jinja2 available in URLs along with https://pypi.org/project/jinja2-time/
|
|
||||||
jinja2_env = Environment(extensions=['jinja2_time.TimeExtension'])
|
|
||||||
try:
|
try:
|
||||||
ready_url = str(jinja2_env.from_string(self.url.data).render())
|
from changedetectionio.safe_jinja import render as jinja_render
|
||||||
|
jinja_render(template_str=self.url.data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.url.errors.append('Invalid template syntax')
|
self.url.errors.append('Invalid template syntax')
|
||||||
result = False
|
result = False
|
||||||
|
|||||||
@@ -169,14 +169,14 @@ def xpath1_filter(xpath_filter, html_content, append_pretty_line_formatting=Fals
|
|||||||
# And where the matched result doesn't include something that will cause Inscriptis to add a newline
|
# And where the matched result doesn't include something that will cause Inscriptis to add a newline
|
||||||
# (This way each 'match' reliably has a new-line in the diff)
|
# (This way each 'match' reliably has a new-line in the diff)
|
||||||
# Divs are converted to 4 whitespaces by inscriptis
|
# Divs are converted to 4 whitespaces by inscriptis
|
||||||
if append_pretty_line_formatting and len(html_block) and (not hasattr( element, 'tag' ) or not element.tag in (['br', 'hr', 'div', 'p'])):
|
if append_pretty_line_formatting and len(html_block) and (not hasattr(element, 'tag') or not element.tag in (['br', 'hr', 'div', 'p'])):
|
||||||
html_block += TEXT_FILTER_LIST_LINE_SUFFIX
|
html_block += TEXT_FILTER_LIST_LINE_SUFFIX
|
||||||
|
|
||||||
if type(element) == etree._ElementStringResult:
|
# Some kind of text, UTF-8 or other
|
||||||
html_block += str(element)
|
if isinstance(element, (str, bytes)):
|
||||||
elif type(element) == etree._ElementUnicodeResult:
|
html_block += element
|
||||||
html_block += str(element)
|
|
||||||
else:
|
else:
|
||||||
|
# Return the HTML which will get parsed as text
|
||||||
html_block += etree.tostring(element, pretty_print=True).decode('utf-8')
|
html_block += etree.tostring(element, pretty_print=True).decode('utf-8')
|
||||||
|
|
||||||
return html_block
|
return html_block
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
|
from changedetectionio.safe_jinja import render as jinja_render
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -10,7 +12,7 @@ from loguru import logger
|
|||||||
# file:// is further checked by ALLOW_FILE_URI
|
# file:// is further checked by ALLOW_FILE_URI
|
||||||
SAFE_PROTOCOL_REGEX='^(http|https|ftp|file):'
|
SAFE_PROTOCOL_REGEX='^(http|https|ftp|file):'
|
||||||
|
|
||||||
minimum_seconds_recheck_time = int(os.getenv('MINIMUM_SECONDS_RECHECK_TIME', 60))
|
minimum_seconds_recheck_time = int(os.getenv('MINIMUM_SECONDS_RECHECK_TIME', 3))
|
||||||
mtable = {'seconds': 1, 'minutes': 60, 'hours': 3600, 'days': 86400, 'weeks': 86400 * 7}
|
mtable = {'seconds': 1, 'minutes': 60, 'hours': 3600, 'days': 86400, 'weeks': 86400 * 7}
|
||||||
|
|
||||||
from changedetectionio.notification import (
|
from changedetectionio.notification import (
|
||||||
@@ -67,6 +69,7 @@ base_config = {
|
|||||||
# Requires setting to None on submit if it's the same as the default
|
# Requires setting to None on submit if it's the same as the default
|
||||||
# Should be all None by default, so we use the system default in this case.
|
# Should be all None by default, so we use the system default in this case.
|
||||||
'time_between_check': {'weeks': None, 'days': None, 'hours': None, 'minutes': None, 'seconds': None},
|
'time_between_check': {'weeks': None, 'days': None, 'hours': None, 'minutes': None, 'seconds': None},
|
||||||
|
'time_between_check_use_default': True,
|
||||||
'title': None,
|
'title': None,
|
||||||
'trigger_text': [], # List of text or regex to wait for until a change is detected
|
'trigger_text': [], # List of text or regex to wait for until a change is detected
|
||||||
'url': '',
|
'url': '',
|
||||||
@@ -137,12 +140,11 @@ class model(dict):
|
|||||||
|
|
||||||
ready_url = url
|
ready_url = url
|
||||||
if '{%' in url or '{{' in url:
|
if '{%' in url or '{{' in url:
|
||||||
from jinja2 import Environment
|
|
||||||
# Jinja2 available in URLs along with https://pypi.org/project/jinja2-time/
|
# Jinja2 available in URLs along with https://pypi.org/project/jinja2-time/
|
||||||
jinja2_env = Environment(extensions=['jinja2_time.TimeExtension'])
|
|
||||||
try:
|
try:
|
||||||
ready_url = str(jinja2_env.from_string(url).render())
|
ready_url = jinja_render(template_str=url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.critical(f"Invalid URL template for: '{url}' - {str(e)}")
|
||||||
from flask import (
|
from flask import (
|
||||||
flash, Markup, url_for
|
flash, Markup, url_for
|
||||||
)
|
)
|
||||||
@@ -362,6 +364,7 @@ class model(dict):
|
|||||||
# @todo bump static cache of the last timestamp so we dont need to examine the file to set a proper ''viewed'' status
|
# @todo bump static cache of the last timestamp so we dont need to examine the file to set a proper ''viewed'' status
|
||||||
return snapshot_fname
|
return snapshot_fname
|
||||||
|
|
||||||
|
@property
|
||||||
@property
|
@property
|
||||||
def has_empty_checktime(self):
|
def has_empty_checktime(self):
|
||||||
# using all() + dictionary comprehension
|
# using all() + dictionary comprehension
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import apprise
|
import apprise
|
||||||
import time
|
import time
|
||||||
from jinja2 import Environment, BaseLoader
|
|
||||||
from apprise import NotifyFormat
|
from apprise import NotifyFormat
|
||||||
import json
|
import json
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -116,6 +115,7 @@ def apprise_custom_api_call_wrapper(body, title, notify_type, *args, **kwargs):
|
|||||||
|
|
||||||
def process_notification(n_object, datastore):
|
def process_notification(n_object, datastore):
|
||||||
|
|
||||||
|
from .safe_jinja import render as jinja_render
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if n_object.get('notification_timestamp'):
|
if n_object.get('notification_timestamp'):
|
||||||
logger.trace(f"Time since queued {now-n_object['notification_timestamp']:.3f}s")
|
logger.trace(f"Time since queued {now-n_object['notification_timestamp']:.3f}s")
|
||||||
@@ -123,9 +123,9 @@ def process_notification(n_object, datastore):
|
|||||||
notification_parameters = create_notification_parameters(n_object, datastore)
|
notification_parameters = create_notification_parameters(n_object, datastore)
|
||||||
|
|
||||||
# Get the notification body from datastore
|
# Get the notification body from datastore
|
||||||
jinja2_env = Environment(loader=BaseLoader)
|
n_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters)
|
||||||
n_body = jinja2_env.from_string(n_object.get('notification_body', '')).render(**notification_parameters)
|
n_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters)
|
||||||
n_title = jinja2_env.from_string(n_object.get('notification_title', '')).render(**notification_parameters)
|
|
||||||
n_format = valid_notification_formats.get(
|
n_format = valid_notification_formats.get(
|
||||||
n_object.get('notification_format', default_notification_format),
|
n_object.get('notification_format', default_notification_format),
|
||||||
valid_notification_formats[default_notification_format],
|
valid_notification_formats[default_notification_format],
|
||||||
@@ -157,7 +157,7 @@ def process_notification(n_object, datastore):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(">> Process Notification: AppRise notifying {}".format(url))
|
logger.info(">> Process Notification: AppRise notifying {}".format(url))
|
||||||
url = jinja2_env.from_string(url).render(**notification_parameters)
|
url = jinja_render(template_str=url, **notification_parameters)
|
||||||
|
|
||||||
# Re 323 - Limit discord length to their 2000 char limit total or it wont send.
|
# Re 323 - Limit discord length to their 2000 char limit total or it wont send.
|
||||||
# Because different notifications may require different pre-processing, run each sequentially :(
|
# Because different notifications may require different pre-processing, run each sequentially :(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
class difference_detection_processor():
|
class difference_detection_processor():
|
||||||
|
|||||||
18
changedetectionio/safe_jinja.py
Normal file
18
changedetectionio/safe_jinja.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
Safe Jinja2 render with max payload sizes
|
||||||
|
|
||||||
|
See https://jinja.palletsprojects.com/en/3.1.x/sandbox/#security-considerations
|
||||||
|
"""
|
||||||
|
|
||||||
|
import jinja2.sandbox
|
||||||
|
import typing as t
|
||||||
|
import os
|
||||||
|
|
||||||
|
JINJA2_MAX_RETURN_PAYLOAD_SIZE = 1024 * int(os.getenv("JINJA2_MAX_RETURN_PAYLOAD_SIZE_KB", 1024 * 10))
|
||||||
|
|
||||||
|
|
||||||
|
def render(template_str, **args: t.Any) -> str:
|
||||||
|
jinja2_env = jinja2.sandbox.ImmutableSandboxedEnvironment(extensions=['jinja2_time.TimeExtension'])
|
||||||
|
output = jinja2_env.from_string(template_str).render(args)
|
||||||
|
return output[:JINJA2_MAX_RETURN_PAYLOAD_SIZE]
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
}).done(function(data){
|
}).done(function(data){
|
||||||
console.log(data);
|
console.log(data);
|
||||||
alert('Sent');
|
alert(data);
|
||||||
}).fail(function(data){
|
}).fail(function(data){
|
||||||
console.log(data);
|
console.log(data);
|
||||||
alert('There was an error communicating with the server.');
|
alert('There was an error communicating with the server.');
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
|
function toggleOpacity(checkboxSelector, fieldSelector) {
|
||||||
|
const checkbox = document.querySelector(checkboxSelector);
|
||||||
|
const fields = document.querySelectorAll(fieldSelector);
|
||||||
|
function updateOpacity() {
|
||||||
|
const opacityValue = checkbox.checked ? 0.6 : 1;
|
||||||
|
fields.forEach(field => {
|
||||||
|
field.style.opacity = opacityValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Initial setup
|
||||||
|
updateOpacity();
|
||||||
|
checkbox.addEventListener('change', updateOpacity);
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('#notification-setting-reset-to-default').click(function (e) {
|
$('#notification-setting-reset-to-default').click(function (e) {
|
||||||
$('#notification_title').val('');
|
$('#notification_title').val('');
|
||||||
@@ -10,4 +24,7 @@ $(document).ready(function () {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$('#notification-tokens-info').toggle();
|
$('#notification-tokens-info').toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
toggleOpacity('#time_between_check_use_default', '#time_between_check');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
--color-last-checked: #bbb;
|
--color-last-checked: #bbb;
|
||||||
--color-text-footer: #444;
|
--color-text-footer: #444;
|
||||||
--color-border-watch-table-cell: #eee;
|
--color-border-watch-table-cell: #eee;
|
||||||
--color-text-watch-tag-list: #e70069;
|
--color-text-watch-tag-list: rgba(231, 0, 105, 0.4);
|
||||||
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
||||||
--color-background-new-watch-input: var(--color-white);
|
--color-background-new-watch-input: var(--color-white);
|
||||||
--color-text-new-watch-input: var(--color-text);
|
--color-text-new-watch-input: var(--color-text);
|
||||||
@@ -111,7 +111,7 @@ html[data-darkmode="true"] {
|
|||||||
--color-background-input: var(--color-grey-350);
|
--color-background-input: var(--color-grey-350);
|
||||||
--color-text-input-description: var(--color-grey-600);
|
--color-text-input-description: var(--color-grey-600);
|
||||||
--color-text-input-placeholder: var(--color-grey-600);
|
--color-text-input-placeholder: var(--color-grey-600);
|
||||||
--color-text-watch-tag-list: #fa3e92;
|
--color-text-watch-tag-list: rgba(250, 62, 146, 0.4);
|
||||||
--color-background-code: var(--color-grey-200);
|
--color-background-code: var(--color-grey-200);
|
||||||
--color-background-tab: rgba(0, 0, 0, 0.2);
|
--color-background-tab: rgba(0, 0, 0, 0.2);
|
||||||
--color-background-tab-hover: rgba(0, 0, 0, 0.5);
|
--color-background-tab-hover: rgba(0, 0, 0, 0.5);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
--color-text-footer: #444;
|
--color-text-footer: #444;
|
||||||
--color-border-watch-table-cell: #eee;
|
--color-border-watch-table-cell: #eee;
|
||||||
|
|
||||||
--color-text-watch-tag-list: #e70069;
|
--color-text-watch-tag-list: rgba(231, 0, 105, 0.4);
|
||||||
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
||||||
--color-background-new-watch-input: var(--color-white);
|
--color-background-new-watch-input: var(--color-white);
|
||||||
--color-text-new-watch-input: var(--color-text);
|
--color-text-new-watch-input: var(--color-text);
|
||||||
@@ -127,7 +127,7 @@ html[data-darkmode="true"] {
|
|||||||
--color-background-input: var(--color-grey-350);
|
--color-background-input: var(--color-grey-350);
|
||||||
--color-text-input-description: var(--color-grey-600);
|
--color-text-input-description: var(--color-grey-600);
|
||||||
--color-text-input-placeholder: var(--color-grey-600);
|
--color-text-input-placeholder: var(--color-grey-600);
|
||||||
--color-text-watch-tag-list: #fa3e92;
|
--color-text-watch-tag-list: rgba(250, 62, 146, 0.4);
|
||||||
--color-background-code: var(--color-grey-200);
|
--color-background-code: var(--color-grey-200);
|
||||||
|
|
||||||
--color-background-tab: rgba(0, 0, 0, 0.2);
|
--color-background-tab: rgba(0, 0, 0, 0.2);
|
||||||
|
|||||||
@@ -187,8 +187,11 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.watch-tag-list {
|
.watch-tag-list {
|
||||||
color: var(--color-text-watch-tag-list);
|
color: var(--color-white);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
background: var(--color-text-watch-tag-list);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
@@ -925,23 +928,26 @@ body.full-width {
|
|||||||
font-size: .875em;
|
font-size: .875em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.text-filtering {
|
}
|
||||||
h3 {
|
|
||||||
margin-top: 0;
|
.border-fieldset {
|
||||||
}
|
h3 {
|
||||||
border: 1px solid #ccc;
|
margin-top: 0;
|
||||||
padding: 1rem;
|
}
|
||||||
border-radius: 5px;
|
border: 1px solid #ccc;
|
||||||
margin-bottom: 1rem;
|
padding: 1rem;
|
||||||
fieldset:last-of-type {
|
border-radius: 5px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
fieldset:last-of-type {
|
||||||
|
padding-bottom: 0;
|
||||||
|
.pure-control-group {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
.pure-control-group {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ ul#requests-extra_browsers {
|
|||||||
--color-last-checked: #bbb;
|
--color-last-checked: #bbb;
|
||||||
--color-text-footer: #444;
|
--color-text-footer: #444;
|
||||||
--color-border-watch-table-cell: #eee;
|
--color-border-watch-table-cell: #eee;
|
||||||
--color-text-watch-tag-list: #e70069;
|
--color-text-watch-tag-list: rgba(231, 0, 105, 0.4);
|
||||||
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
|
||||||
--color-background-new-watch-input: var(--color-white);
|
--color-background-new-watch-input: var(--color-white);
|
||||||
--color-text-new-watch-input: var(--color-text);
|
--color-text-new-watch-input: var(--color-text);
|
||||||
@@ -327,7 +327,7 @@ html[data-darkmode="true"] {
|
|||||||
--color-background-input: var(--color-grey-350);
|
--color-background-input: var(--color-grey-350);
|
||||||
--color-text-input-description: var(--color-grey-600);
|
--color-text-input-description: var(--color-grey-600);
|
||||||
--color-text-input-placeholder: var(--color-grey-600);
|
--color-text-input-placeholder: var(--color-grey-600);
|
||||||
--color-text-watch-tag-list: #fa3e92;
|
--color-text-watch-tag-list: rgba(250, 62, 146, 0.4);
|
||||||
--color-background-code: var(--color-grey-200);
|
--color-background-code: var(--color-grey-200);
|
||||||
--color-background-tab: rgba(0, 0, 0, 0.2);
|
--color-background-tab: rgba(0, 0, 0, 0.2);
|
||||||
--color-background-tab-hover: rgba(0, 0, 0, 0.5);
|
--color-background-tab-hover: rgba(0, 0, 0, 0.5);
|
||||||
@@ -532,8 +532,11 @@ code {
|
|||||||
margin: 0 3px 0 5px; }
|
margin: 0 3px 0 5px; }
|
||||||
|
|
||||||
.watch-tag-list {
|
.watch-tag-list {
|
||||||
color: var(--color-text-watch-tag-list);
|
color: var(--color-white);
|
||||||
white-space: nowrap; }
|
white-space: nowrap;
|
||||||
|
background: var(--color-text-watch-tag-list);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 5px; }
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
@@ -1038,17 +1041,18 @@ body.full-width .edit-form {
|
|||||||
color: var(--color-text-input-description); }
|
color: var(--color-text-input-description); }
|
||||||
.edit-form .pure-form-message-inline code {
|
.edit-form .pure-form-message-inline code {
|
||||||
font-size: .875em; }
|
font-size: .875em; }
|
||||||
.edit-form .text-filtering {
|
|
||||||
border: 1px solid #ccc;
|
.border-fieldset {
|
||||||
padding: 1rem;
|
border: 1px solid #ccc;
|
||||||
border-radius: 5px;
|
padding: 1rem;
|
||||||
margin-bottom: 1rem; }
|
border-radius: 5px;
|
||||||
.edit-form .text-filtering h3 {
|
margin-bottom: 1rem; }
|
||||||
margin-top: 0; }
|
.border-fieldset h3 {
|
||||||
.edit-form .text-filtering fieldset:last-of-type {
|
margin-top: 0; }
|
||||||
|
.border-fieldset fieldset:last-of-type {
|
||||||
|
padding-bottom: 0; }
|
||||||
|
.border-fieldset fieldset:last-of-type .pure-control-group {
|
||||||
padding-bottom: 0; }
|
padding-bottom: 0; }
|
||||||
.edit-form .text-filtering fieldset:last-of-type .pure-control-group {
|
|
||||||
padding-bottom: 0; }
|
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from distutils.util import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
flash
|
flash
|
||||||
@@ -872,3 +872,16 @@ class ChangeDetectionStore:
|
|||||||
self.__data["watching"][awatch]['include_filters'][num] = 'xpath1:' + selector
|
self.__data["watching"][awatch]['include_filters'][num] = 'xpath1:' + selector
|
||||||
if selector.startswith('xpath:'):
|
if selector.startswith('xpath:'):
|
||||||
self.__data["watching"][awatch]['include_filters'][num] = selector.replace('xpath:', 'xpath1:', 1)
|
self.__data["watching"][awatch]['include_filters'][num] = selector.replace('xpath:', 'xpath1:', 1)
|
||||||
|
|
||||||
|
# Use more obvious default time setting
|
||||||
|
def update_15(self):
|
||||||
|
for uuid in self.__data["watching"]:
|
||||||
|
if self.__data["watching"][uuid]['time_between_check'] == self.__data['settings']['requests']['time_between_check']:
|
||||||
|
# What the old logic was, which was pretty confusing
|
||||||
|
self.__data["watching"][uuid]['time_between_check_use_default'] = True
|
||||||
|
elif all(value is None or value == 0 for value in self.__data["watching"][uuid]['time_between_check'].values()):
|
||||||
|
self.__data["watching"][uuid]['time_between_check_use_default'] = True
|
||||||
|
else:
|
||||||
|
# Something custom here
|
||||||
|
self.__data["watching"][uuid]['time_between_check_use_default'] = False
|
||||||
|
|
||||||
|
|||||||
23
changedetectionio/strtobool.py
Normal file
23
changedetectionio/strtobool.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Because strtobool was removed in python 3.12 distutils
|
||||||
|
|
||||||
|
_MAP = {
|
||||||
|
'y': True,
|
||||||
|
'yes': True,
|
||||||
|
't': True,
|
||||||
|
'true': True,
|
||||||
|
'on': True,
|
||||||
|
'1': True,
|
||||||
|
'n': False,
|
||||||
|
'no': False,
|
||||||
|
'f': False,
|
||||||
|
'false': False,
|
||||||
|
'off': False,
|
||||||
|
'0': False
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def strtobool(value):
|
||||||
|
try:
|
||||||
|
return _MAP[str(value).lower()]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError('"{}" is not a valid bool value'.format(value))
|
||||||
6
changedetectionio/templates/IMPORTANT.md
Normal file
6
changedetectionio/templates/IMPORTANT.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Important notes about templates
|
||||||
|
|
||||||
|
Template names should always end in ".html", ".htm", ".xml", ".xhtml", ".svg", even the `import`'ed templates.
|
||||||
|
|
||||||
|
Jinja2's `def select_jinja_autoescape(self, filename: str) -> bool:` will check the filename extension and enable autoescaping
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
{% from '_helpers.jinja' import render_field %}
|
{% from '_helpers.html' import render_field %}
|
||||||
|
|
||||||
{% macro render_common_settings_form(form, emailprefix, settings_application) %}
|
{% macro render_common_settings_form(form, emailprefix, settings_application) %}
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
|
{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<script>
|
<script>
|
||||||
const screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid)}}";
|
const screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid)}}";
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
|
{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
|
||||||
{% from '_common_fields.jinja' import render_common_settings_form %}
|
{% from '_common_fields.html' import render_common_settings_form %}
|
||||||
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
||||||
<script src="{{url_for('static_content', group='js', filename='vis.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='vis.js')}}" defer></script>
|
||||||
<script>
|
<script>
|
||||||
const browser_steps_available_screenshots=JSON.parse('{{ watch.get_browsersteps_available_screenshots|tojson }}');
|
const browser_steps_available_screenshots=JSON.parse('{{ watch.get_browsersteps_available_screenshots|tojson }}');
|
||||||
const browser_steps_config=JSON.parse('{{ browser_steps_config|tojson }}');
|
const browser_steps_config=JSON.parse('{{ browser_steps_config|tojson }}');
|
||||||
const browser_steps_fetch_screenshot_image_url="{{url_for('browser_steps.browser_steps_fetch_screenshot_image', uuid=uuid)}}";
|
<!-- Should be _external so that firefox and others load it more reliably -->
|
||||||
|
const browser_steps_fetch_screenshot_image_url="{{url_for('browser_steps.browser_steps_fetch_screenshot_image', uuid=uuid, _external=True)}}";
|
||||||
const browser_steps_last_error_step={{ watch.browser_steps_last_error_step|tojson }};
|
const browser_steps_last_error_step={{ watch.browser_steps_last_error_step|tojson }};
|
||||||
const browser_steps_start_url="{{url_for('browser_steps.browsersteps_start_session', uuid=uuid)}}";
|
const browser_steps_start_url="{{url_for('browser_steps.browsersteps_start_session', uuid=uuid)}}";
|
||||||
const browser_steps_sync_url="{{url_for('browser_steps.browsersteps_ui_update', uuid=uuid)}}";
|
const browser_steps_sync_url="{{url_for('browser_steps.browsersteps_ui_update', uuid=uuid)}}";
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
<script src="{{url_for('static_content', group='js', filename='browser-steps.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='browser-steps.js')}}" defer></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% set has_tag_filters_extra="WARNING: Watch has tag/groups set with special filters\n" if has_special_tag_options else '' %}
|
||||||
<script src="{{url_for('static_content', group='js', filename='recheck-proxy.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='recheck-proxy.js')}}" defer></script>
|
||||||
|
|
||||||
<div class="edit-form monospaced-textarea">
|
<div class="edit-form monospaced-textarea">
|
||||||
@@ -85,15 +87,9 @@
|
|||||||
{{ render_field(form.tags) }}
|
{{ render_field(form.tags) }}
|
||||||
<span class="pure-form-message-inline">Organisational tag/group name used in the main listing page</span>
|
<span class="pure-form-message-inline">Organisational tag/group name used in the main listing page</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group time-between-check border-fieldset">
|
||||||
{{ render_field(form.time_between_check, class="time-check-widget") }}
|
{{ render_field(form.time_between_check, class="time-check-widget") }}
|
||||||
{% if has_empty_checktime %}
|
{{ render_checkbox_field(form.time_between_check_use_default, class="use-default-timecheck") }}
|
||||||
<span class="pure-form-message-inline">Currently using the <a
|
|
||||||
href="{{ url_for('settings_page', uuid=uuid) }}">default global settings</a>, change to another value if you want to be specific.</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="pure-form-message-inline">Set to blank to use the <a
|
|
||||||
href="{{ url_for('settings_page', uuid=uuid) }}">default global settings</a>.</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_checkbox_field(form.extract_title_as_title) }}
|
{{ render_checkbox_field(form.extract_title_as_title) }}
|
||||||
@@ -280,7 +276,7 @@ User-Agent: wonderbra 1.0") }}
|
|||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{% set field = render_field(form.include_filters,
|
{% set field = render_field(form.include_filters,
|
||||||
rows=5,
|
rows=5,
|
||||||
placeholder="#example
|
placeholder=has_tag_filters_extra+"#example
|
||||||
xpath://body/div/span[contains(@class, 'example-class')]",
|
xpath://body/div/span[contains(@class, 'example-class')]",
|
||||||
class="m-d")
|
class="m-d")
|
||||||
%}
|
%}
|
||||||
@@ -316,7 +312,7 @@ xpath://body/div/span[contains(@class, 'example-class')]",
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<fieldset class="pure-control-group">
|
<fieldset class="pure-control-group">
|
||||||
{{ render_field(form.subtractive_selectors, rows=5, placeholder="header
|
{{ render_field(form.subtractive_selectors, rows=5, placeholder=has_tag_filters_extra+"header
|
||||||
footer
|
footer
|
||||||
nav
|
nav
|
||||||
.stockticker") }}
|
.stockticker") }}
|
||||||
@@ -328,7 +324,7 @@ nav
|
|||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="text-filtering">
|
<div class="text-filtering border-fieldset">
|
||||||
<fieldset class="pure-group" id="text-filtering-type-options">
|
<fieldset class="pure-group" id="text-filtering-type-options">
|
||||||
<h3>Text filtering</h3>
|
<h3>Text filtering</h3>
|
||||||
Limit trigger/ignore/block/extract to;<br>
|
Limit trigger/ignore/block/extract to;<br>
|
||||||
@@ -485,13 +481,17 @@ Unavailable") }}
|
|||||||
<td>{{ "{:,}".format(watch.history|length) }}</td>
|
<td>{{ "{:,}".format(watch.history|length) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Last fetch time</td>
|
<td>Last fetch duration</td>
|
||||||
<td>{{ watch.fetch_time }}s</td>
|
<td>{{ watch.fetch_time }}s</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Notification alert count</td>
|
<td>Notification alert count</td>
|
||||||
<td>{{ watch.notification_alert_count }}</td>
|
<td>{{ watch.notification_alert_count }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Server type reply</td>
|
||||||
|
<td>{{ watch.get('remote_server_reply') }}</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% from '_helpers.jinja' import render_field %}
|
{% from '_helpers.html' import render_field %}
|
||||||
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
||||||
<div class="edit-form monospaced-textarea">
|
<div class="edit-form monospaced-textarea">
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
|
{% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
|
||||||
{% from '_common_fields.jinja' import render_common_settings_form %}
|
{% from '_common_fields.html' import render_common_settings_form %}
|
||||||
<script>
|
<script>
|
||||||
const notification_base_url="{{url_for('ajax_callback_send_notification_test', watch_uuid=uuid)}}";
|
const notification_base_url="{{url_for('ajax_callback_send_notification_test', mode="global-settings")}}";
|
||||||
{% if emailprefix %}
|
{% if emailprefix %}
|
||||||
const email_notification_prefix=JSON.parse('{{emailprefix|tojson}}');
|
const email_notification_prefix=JSON.parse('{{emailprefix|tojson}}');
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_field(form.requests.form.time_between_check, class="time-check-widget") }}
|
{{ render_field(form.requests.form.time_between_check, class="time-check-widget") }}
|
||||||
<span class="pure-form-message-inline">Default time for all watches, when the watch does not have a specific time setting.</span>
|
<span class="pure-form-message-inline">Default recheck time for all watches, current system minimum is <i>{{min_system_recheck_seconds}}</i> seconds (<a href="https://github.com/dgtlmoon/changedetection.io/wiki/Misc-system-settings#enviroment-variables">more info</a>).</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_field(form.requests.form.jitter_seconds, class="jitter_seconds") }}
|
{{ render_field(form.requests.form.jitter_seconds, class="jitter_seconds") }}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% from '_helpers.jinja' import render_simple_field, render_field, render_nolabel_field, sort_by_title %}
|
{% from '_helpers.html' import render_simple_field, render_field, render_nolabel_field, sort_by_title %}
|
||||||
<script src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
|
<script src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
|
||||||
<script src="{{url_for('static_content', group='js', filename='watch-overview.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='watch-overview.js')}}" defer></script>
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
<td>
|
<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')) }}"
|
<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>
|
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)}}" class="pure-button pure-button-primary">Edit</a>
|
<a href="{{ url_for('edit_page', uuid=watch.uuid)}}#general" class="pure-button pure-button-primary">Edit</a>
|
||||||
{% if watch.history_n >= 2 %}
|
{% if watch.history_n >= 2 %}
|
||||||
|
|
||||||
{% if is_unviewed %}
|
{% if is_unviewed %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
import os.path
|
||||||
import time
|
import time
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from .util import live_server_setup, wait_for_all_checks
|
from .util import live_server_setup, wait_for_all_checks
|
||||||
@@ -107,7 +107,6 @@ def test_check_add_line_contains_trigger(client, live_server):
|
|||||||
#live_server_setup(live_server)
|
#live_server_setup(live_server)
|
||||||
|
|
||||||
# Give the endpoint time to spin up
|
# Give the endpoint time to spin up
|
||||||
time.sleep(1)
|
|
||||||
test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://') + "?xxx={{ watch_url }}"
|
test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://') + "?xxx={{ watch_url }}"
|
||||||
|
|
||||||
res = client.post(
|
res = client.post(
|
||||||
@@ -166,6 +165,7 @@ def test_check_add_line_contains_trigger(client, live_server):
|
|||||||
|
|
||||||
# Takes a moment for apprise to fire
|
# Takes a moment for apprise to fire
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
assert os.path.isfile("test-datastore/notification.txt"), "Notification fired because I can see the output file"
|
||||||
with open("test-datastore/notification.txt", 'r') as f:
|
with open("test-datastore/notification.txt", 'r') as f:
|
||||||
response= f.read()
|
response= f.read()
|
||||||
assert '-Oh yes please-' in response
|
assert '-Oh yes please-' in response
|
||||||
|
|||||||
@@ -100,6 +100,12 @@ def test_setup_group_tag(client, live_server):
|
|||||||
assert b'Should be only this' in res.data
|
assert b'Should be only this' in res.data
|
||||||
assert b'And never this' not in res.data
|
assert b'And never this' not in res.data
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
url_for("edit_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
# 2307 the UI notice should appear in the placeholder
|
||||||
|
assert b'WARNING: Watch has tag/groups set with special filters' in res.data
|
||||||
|
|
||||||
# RSS Group tag filter
|
# RSS Group tag filter
|
||||||
# An extra one that should be excluded
|
# An extra one that should be excluded
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from .util import live_server_setup
|
from .util import live_server_setup, wait_for_all_checks
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup(client, live_server):
|
||||||
|
live_server_setup(live_server)
|
||||||
|
|
||||||
# If there was only a change in the whitespacing, then we shouldnt have a change detected
|
# If there was only a change in the whitespacing, then we shouldnt have a change detected
|
||||||
def test_jinja2_in_url_query(client, live_server):
|
def test_jinja2_in_url_query(client, live_server):
|
||||||
live_server_setup(live_server)
|
#live_server_setup(live_server)
|
||||||
|
|
||||||
# Give the endpoint time to spin up
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
test_url = url_for('test_return_query', _external=True)
|
test_url = url_for('test_return_query', _external=True)
|
||||||
@@ -24,10 +24,35 @@ def test_jinja2_in_url_query(client, live_server):
|
|||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
assert b"Watch added" in res.data
|
assert b"Watch added" in res.data
|
||||||
time.sleep(3)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
# It should report nothing found (no new 'unviewed' class)
|
# It should report nothing found (no new 'unviewed' class)
|
||||||
res = client.get(
|
res = client.get(
|
||||||
url_for("preview_page", uuid="first"),
|
url_for("preview_page", uuid="first"),
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
assert b'date=2' in res.data
|
assert b'date=2' in res.data
|
||||||
|
|
||||||
|
# https://techtonics.medium.com/secure-templating-with-jinja2-understanding-ssti-and-jinja2-sandbox-environment-b956edd60456
|
||||||
|
def test_jinja2_security_url_query(client, live_server):
|
||||||
|
#live_server_setup(live_server)
|
||||||
|
|
||||||
|
# Add our URL to the import page
|
||||||
|
test_url = url_for('test_return_query', _external=True)
|
||||||
|
|
||||||
|
# because url_for() will URL-encode the var, but we dont here
|
||||||
|
full_url = "{}?{}".format(test_url,
|
||||||
|
"date={{ ''.__class__.__mro__[1].__subclasses__()}}", )
|
||||||
|
res = client.post(
|
||||||
|
url_for("form_quick_watch_add"),
|
||||||
|
data={"url": full_url, "tags": "test"},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"Watch added" in res.data
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
|
# It should report nothing found (no new 'unviewed' class)
|
||||||
|
res = client.get(url_for("index"))
|
||||||
|
assert b'is invalid and cannot be used' in res.data
|
||||||
|
# Some of the spewed output from the subclasses
|
||||||
|
assert b'dict_values' not in res.data
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ from flask import url_for
|
|||||||
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks
|
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
def test_setup(client, live_server):
|
||||||
|
live_server_setup(live_server)
|
||||||
|
|
||||||
def test_bad_access(client, live_server):
|
def test_bad_access(client, live_server):
|
||||||
live_server_setup(live_server)
|
#live_server_setup(live_server)
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("import_page"),
|
url_for("import_page"),
|
||||||
data={"urls": 'https://localhost'},
|
data={"urls": 'https://localhost'},
|
||||||
@@ -63,4 +65,25 @@ def test_bad_access(client, live_server):
|
|||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
res = client.get(url_for("index"))
|
res = client.get(url_for("index"))
|
||||||
|
|
||||||
assert b'file:// type access is denied for security reasons.' in res.data
|
assert b'file:// type access is denied for security reasons.' in res.data
|
||||||
|
|
||||||
|
def test_xss(client, live_server):
|
||||||
|
#live_server_setup(live_server)
|
||||||
|
from changedetectionio.notification import (
|
||||||
|
default_notification_format
|
||||||
|
)
|
||||||
|
# the template helpers were named .jinja which meant they were not having jinja2 autoescape enabled.
|
||||||
|
res = client.post(
|
||||||
|
url_for("settings_page"),
|
||||||
|
data={"application-notification_urls": '"><img src=x onerror=alert(document.domain)>',
|
||||||
|
"application-notification_title": '"><img src=x onerror=alert(document.domain)>',
|
||||||
|
"application-notification_body": '"><img src=x onerror=alert(document.domain)>',
|
||||||
|
"application-notification_format": default_notification_format,
|
||||||
|
"requests-time_between_check-minutes": 180,
|
||||||
|
'application-fetch_backend': "html_requests"},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b"<img src=x onerror=alert(" not in res.data
|
||||||
|
assert b"<img" in res.data
|
||||||
|
|
||||||
|
|||||||
@@ -54,102 +54,3 @@ def test_check_watch_field_storage(client, live_server):
|
|||||||
assert b"woohoo" in res.data
|
assert b"woohoo" in res.data
|
||||||
assert b"curl: foo" in res.data
|
assert b"curl: foo" in res.data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Re https://github.com/dgtlmoon/changedetection.io/issues/110
|
|
||||||
def test_check_recheck_global_setting(client, live_server):
|
|
||||||
|
|
||||||
res = client.post(
|
|
||||||
url_for("settings_page"),
|
|
||||||
data={
|
|
||||||
"requests-time_between_check-minutes": 1566,
|
|
||||||
'application-fetch_backend': "html_requests"
|
|
||||||
},
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b"Settings updated." in res.data
|
|
||||||
|
|
||||||
# Now add a record
|
|
||||||
|
|
||||||
test_url = "http://somerandomsitewewatch.com"
|
|
||||||
|
|
||||||
res = client.post(
|
|
||||||
url_for("import_page"),
|
|
||||||
data={"urls": test_url},
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b"1 Imported" in res.data
|
|
||||||
|
|
||||||
# Now visit the edit page, it should have the default minutes
|
|
||||||
|
|
||||||
res = client.get(
|
|
||||||
url_for("edit_page", uuid="first"),
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should show the default minutes
|
|
||||||
assert b"change to another value if you want to be specific" in res.data
|
|
||||||
assert b"1566" in res.data
|
|
||||||
|
|
||||||
res = client.post(
|
|
||||||
url_for("settings_page"),
|
|
||||||
data={
|
|
||||||
"requests-time_between_check-minutes": 222,
|
|
||||||
'application-fetch_backend': "html_requests"
|
|
||||||
},
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b"Settings updated." in res.data
|
|
||||||
|
|
||||||
res = client.get(
|
|
||||||
url_for("edit_page", uuid="first"),
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should show the default minutes
|
|
||||||
assert b"change to another value if you want to be specific" in res.data
|
|
||||||
assert b"222" in res.data
|
|
||||||
|
|
||||||
# Now change it specifically, it should show the new minutes
|
|
||||||
res = client.post(
|
|
||||||
url_for("edit_page", uuid="first"),
|
|
||||||
data={"url": test_url,
|
|
||||||
"time_between_check-minutes": 55,
|
|
||||||
'fetch_backend': "html_requests"
|
|
||||||
},
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
|
|
||||||
res = client.get(
|
|
||||||
url_for("edit_page", uuid="first"),
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b"55" in res.data
|
|
||||||
|
|
||||||
# Now submit an empty field, it should give back the default global minutes
|
|
||||||
res = client.post(
|
|
||||||
url_for("settings_page"),
|
|
||||||
data={
|
|
||||||
"requests-time_between_check-minutes": 666,
|
|
||||||
"application-fetch_backend": "html_requests"
|
|
||||||
},
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b"Settings updated." in res.data
|
|
||||||
|
|
||||||
res = client.post(
|
|
||||||
url_for("edit_page", uuid="first"),
|
|
||||||
data={"url": test_url,
|
|
||||||
"time_between_check-minutes": "",
|
|
||||||
'fetch_backend': "html_requests"
|
|
||||||
},
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert b"Updated watch." in res.data
|
|
||||||
|
|
||||||
res = client.get(
|
|
||||||
url_for("edit_page", uuid="first"),
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b"666" in res.data
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python3
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
@@ -255,6 +255,69 @@ def test_xpath23_prefix_validation(client, live_server):
|
|||||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
assert b'Deleted' in res.data
|
assert b'Deleted' in res.data
|
||||||
|
|
||||||
|
def test_xpath1_lxml(client, live_server):
|
||||||
|
#live_server_setup(live_server)
|
||||||
|
|
||||||
|
d = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>rpilocator.com</title>
|
||||||
|
<link>https://rpilocator.com</link>
|
||||||
|
<description>Find Raspberry Pi Computers in Stock</description>
|
||||||
|
<lastBuildDate>Thu, 19 May 2022 23:27:30 GMT</lastBuildDate>
|
||||||
|
<image>
|
||||||
|
<url>https://rpilocator.com/favicon.png</url>
|
||||||
|
<title>rpilocator.com</title>
|
||||||
|
<link>https://rpilocator.com/</link>
|
||||||
|
<width>32</width>
|
||||||
|
<height>32</height>
|
||||||
|
</image>
|
||||||
|
<item>
|
||||||
|
<title>Stock Alert (UK): RPi CM4</title>
|
||||||
|
<foo>something else unrelated</foo>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Stock Alert (UK): Big monitorěěěě</title>
|
||||||
|
<foo>something else unrelated</foo>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>'''.encode('utf-8')
|
||||||
|
|
||||||
|
with open("test-datastore/endpoint-content.txt", "wb") as f:
|
||||||
|
f.write(d)
|
||||||
|
|
||||||
|
|
||||||
|
test_url = url_for('test_endpoint', _external=True)
|
||||||
|
res = client.post(
|
||||||
|
url_for("import_page"),
|
||||||
|
data={"urls": test_url},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"1 Imported" in res.data
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
|
res = client.post(
|
||||||
|
url_for("edit_page", uuid="first"),
|
||||||
|
data={"include_filters": "xpath1://title/text()", "url": test_url, "tags": "", "headers": "",
|
||||||
|
'fetch_backend': "html_requests"},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
##### #2312
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
res = client.get(url_for("index"))
|
||||||
|
assert b'_ElementStringResult' not in res.data # tested with 5.1.1 when it was removed and 5.1.0
|
||||||
|
assert b'Exception' not in res.data
|
||||||
|
res = client.get(
|
||||||
|
url_for("preview_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b"rpilocator.com" in res.data # in selector
|
||||||
|
assert "Stock Alert (UK): Big monitorěěěě".encode('utf-8') in res.data # not in selector
|
||||||
|
|
||||||
|
#####
|
||||||
|
|
||||||
|
|
||||||
def test_xpath1_validation(client, live_server):
|
def test_xpath1_validation(client, live_server):
|
||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
|
|||||||
57
changedetectionio/tests/unit/test_jinja2_security.py
Normal file
57
changedetectionio/tests/unit/test_jinja2_security.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
# run from dir above changedetectionio/ dir
|
||||||
|
# python3 -m unittest changedetectionio.tests.unit.test_jinja2_security
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from changedetectionio import safe_jinja
|
||||||
|
|
||||||
|
|
||||||
|
# mostly
|
||||||
|
class TestJinja2SSTI(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_exception(self):
|
||||||
|
import jinja2
|
||||||
|
|
||||||
|
# Where sandbox should kick in
|
||||||
|
attempt_list = [
|
||||||
|
"My name is {{ self.__init__.__globals__.__builtins__.__import__('os').system('id') }}",
|
||||||
|
"{{ self._TemplateReference__context.cycler.__init__.__globals__.os }}",
|
||||||
|
"{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}",
|
||||||
|
"{{cycler.__init__.__globals__.os.popen('id').read()}}",
|
||||||
|
"{{joiner.__init__.__globals__.os.popen('id').read()}}",
|
||||||
|
"{{namespace.__init__.__globals__.os.popen('id').read()}}",
|
||||||
|
"{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/hello.txt', 'w').write('Hello here !') }}",
|
||||||
|
"My name is {{ self.__init__.__globals__ }}",
|
||||||
|
"{{ dict.__base__.__subclasses__() }}"
|
||||||
|
]
|
||||||
|
for attempt in attempt_list:
|
||||||
|
with self.assertRaises(jinja2.exceptions.SecurityError):
|
||||||
|
safe_jinja.render(attempt)
|
||||||
|
|
||||||
|
def test_exception_debug_calls(self):
|
||||||
|
import jinja2
|
||||||
|
# Where sandbox should kick in - configs and debug calls
|
||||||
|
attempt_list = [
|
||||||
|
"{% debug %}",
|
||||||
|
]
|
||||||
|
for attempt in attempt_list:
|
||||||
|
# Usually should be something like 'Encountered unknown tag 'debug'.'
|
||||||
|
with self.assertRaises(jinja2.exceptions.TemplateSyntaxError):
|
||||||
|
safe_jinja.render(attempt)
|
||||||
|
|
||||||
|
# https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection/jinja2-ssti#accessing-global-objects
|
||||||
|
def test_exception_empty_calls(self):
|
||||||
|
import jinja2
|
||||||
|
attempt_list = [
|
||||||
|
"{{config}}",
|
||||||
|
"{{ debug }}"
|
||||||
|
"{{[].__class__}}",
|
||||||
|
]
|
||||||
|
for attempt in attempt_list:
|
||||||
|
self.assertEqual(len(safe_jinja.render(attempt)), 0, f"string test '{attempt}' is correctly empty")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -116,7 +116,7 @@ def extract_UUID_from_client(client):
|
|||||||
)
|
)
|
||||||
# <span id="api-key">{{api_key}}</span>
|
# <span id="api-key">{{api_key}}</span>
|
||||||
|
|
||||||
m = re.search('edit/(.+?)"', str(res.data))
|
m = re.search('edit/(.+?)[#"]', str(res.data))
|
||||||
uuid = m.group(1)
|
uuid = m.group(1)
|
||||||
return uuid.strip()
|
return uuid.strip()
|
||||||
|
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ class update_worker(threading.Thread):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Exception reached processing watch UUID: {uuid}")
|
logger.error(f"Exception reached processing watch UUID: {uuid}")
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
self.datastore.update_watch(uuid=uuid, update_obj={'last_error': str(e)})
|
self.datastore.update_watch(uuid=uuid, update_obj={'last_error': "Exception: " + str(e)})
|
||||||
# Other serious error
|
# Other serious error
|
||||||
process_changedetection_results = False
|
process_changedetection_results = False
|
||||||
# import traceback
|
# import traceback
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ services:
|
|||||||
#
|
#
|
||||||
# Default number of parallel/concurrent fetchers
|
# Default number of parallel/concurrent fetchers
|
||||||
# - FETCH_WORKERS=10
|
# - FETCH_WORKERS=10
|
||||||
|
#
|
||||||
|
# Absolute minimum seconds to recheck, overrides any watch minimum, change to 0 to disable
|
||||||
|
# - MINIMUM_SECONDS_RECHECK_TIME=3
|
||||||
# Comment out ports: when using behind a reverse proxy , enable networks: etc.
|
# Comment out ports: when using behind a reverse proxy , enable networks: etc.
|
||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ apprise~=1.7.4
|
|||||||
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
|
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
|
||||||
# and 2.0.0 https://github.com/dgtlmoon/changedetection.io/issues/2241 not yet compatible
|
# and 2.0.0 https://github.com/dgtlmoon/changedetection.io/issues/2241 not yet compatible
|
||||||
# use v1.x due to https://github.com/eclipse/paho.mqtt.python/issues/814
|
# use v1.x due to https://github.com/eclipse/paho.mqtt.python/issues/814
|
||||||
paho-mqtt < 2.0.0
|
paho-mqtt>=1.6.1,<2.0.0
|
||||||
|
|
||||||
# This mainly affects some ARM builds, which unlike the other builds ignores "ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1"
|
# This mainly affects some ARM builds, which unlike the other builds ignores "ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1"
|
||||||
# so without this pinning, the newer versions on ARM will forcefully try to build rust, which results in "rust compiler not found"
|
# so without this pinning, the newer versions on ARM will forcefully try to build rust, which results in "rust compiler not found"
|
||||||
@@ -52,7 +52,7 @@ cryptography~=3.4
|
|||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
|
|
||||||
# XPath filtering, lxml is required by bs4 anyway, but put it here to be safe.
|
# XPath filtering, lxml is required by bs4 anyway, but put it here to be safe.
|
||||||
lxml
|
lxml >=4.8.0,<6
|
||||||
|
|
||||||
# XPath 2.0-3.1 support - 4.2.0 broke something?
|
# XPath 2.0-3.1 support - 4.2.0 broke something?
|
||||||
elementpath==4.1.5
|
elementpath==4.1.5
|
||||||
|
|||||||
Reference in New Issue
Block a user