mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-15 05:56:10 +00:00
Compare commits
5 Commits
0.51.00
...
proxy-url-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9144b67610 | ||
|
|
b20fe70b49 | ||
|
|
080acced85 | ||
|
|
6b895ae972 | ||
|
|
7efd4c2a99 |
@@ -1,7 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% from '_helpers.html' import render_field, render_checkbox_field, render_button, render_time_schedule_form, render_ternary_field %}
|
{% from '_helpers.html' import render_field, render_checkbox_field, render_button, render_time_schedule_form, render_ternary_field, render_fieldlist_with_inline_errors %}
|
||||||
{% from '_common_fields.html' import render_common_settings_form %}
|
{% from '_common_fields.html' import render_common_settings_form %}
|
||||||
<script>
|
<script>
|
||||||
const notification_base_url="{{url_for('ui.ui_notification.ajax_callback_send_notification_test', mode="global-settings")}}";
|
const notification_base_url="{{url_for('ui.ui_notification.ajax_callback_send_notification_test', mode="global-settings")}}";
|
||||||
@@ -312,7 +312,7 @@ nav
|
|||||||
<p><strong>Tip</strong>: "Residential" and "Mobile" proxy type can be more successfull than "Data Center" for blocked websites.
|
<p><strong>Tip</strong>: "Residential" and "Mobile" proxy type can be more successfull than "Data Center" for blocked websites.
|
||||||
|
|
||||||
<div class="pure-control-group" id="extra-proxies-setting">
|
<div class="pure-control-group" id="extra-proxies-setting">
|
||||||
{{ render_field(form.requests.form.extra_proxies) }}
|
{{ render_fieldlist_with_inline_errors(form.requests.form.extra_proxies) }}
|
||||||
<span class="pure-form-message-inline">"Name" will be used for selecting the proxy in the Watch Edit settings</span><br>
|
<span class="pure-form-message-inline">"Name" will be used for selecting the proxy in the Watch Edit settings</span><br>
|
||||||
<span class="pure-form-message-inline">SOCKS5 proxies with authentication are only supported with 'plain requests' fetcher, for other fetchers you should whitelist the IP access instead</span>
|
<span class="pure-form-message-inline">SOCKS5 proxies with authentication are only supported with 'plain requests' fetcher, for other fetchers you should whitelist the IP access instead</span>
|
||||||
{% if form.requests.proxy %}
|
{% if form.requests.proxy %}
|
||||||
@@ -330,7 +330,7 @@ nav
|
|||||||
<span class="pure-form-message-inline"><i>Extra Browsers</i> can be attached to further defeat CAPTCHA's on websites that are particularly hard to scrape.</span><br>
|
<span class="pure-form-message-inline"><i>Extra Browsers</i> can be attached to further defeat CAPTCHA's on websites that are particularly hard to scrape.</span><br>
|
||||||
<span class="pure-form-message-inline">Simply paste the connection address into the box, <a href="https://changedetection.io/tutorial/using-bright-datas-scraping-browser-pass-captchas-and-other-protection-when-monitoring">More instructions and examples here</a> </span>
|
<span class="pure-form-message-inline">Simply paste the connection address into the box, <a href="https://changedetection.io/tutorial/using-bright-datas-scraping-browser-pass-captchas-and-other-protection-when-monitoring">More instructions and examples here</a> </span>
|
||||||
</p>
|
</p>
|
||||||
{{ render_field(form.requests.form.extra_browsers) }}
|
{{ render_fieldlist_with_inline_errors(form.requests.form.extra_browsers) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -678,6 +678,51 @@ class ValidateCSSJSONXPATHInput(object):
|
|||||||
except:
|
except:
|
||||||
raise ValidationError("A system-error occurred when validating your jq expression")
|
raise ValidationError("A system-error occurred when validating your jq expression")
|
||||||
|
|
||||||
|
class ValidateSimpleURL:
|
||||||
|
"""Validate that the value can be parsed by urllib.parse.urlparse() and has a scheme/netloc."""
|
||||||
|
def __init__(self, message=None):
|
||||||
|
self.message = message or "Invalid URL."
|
||||||
|
|
||||||
|
def __call__(self, form, field):
|
||||||
|
data = (field.data or "").strip()
|
||||||
|
if not data:
|
||||||
|
return # empty is OK — pair with validators.Optional()
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
parsed = urlparse(data)
|
||||||
|
if not parsed.scheme or not parsed.netloc:
|
||||||
|
raise ValidationError(self.message)
|
||||||
|
|
||||||
|
class ValidateStartsWithRegex(object):
|
||||||
|
def __init__(self, regex, *, flags=0, message=None, allow_empty=True, split_lines=True):
|
||||||
|
# compile with given flags (we’ll pass re.IGNORECASE below)
|
||||||
|
self.pattern = re.compile(regex, flags) if isinstance(regex, str) else regex
|
||||||
|
self.message = message
|
||||||
|
self.allow_empty = allow_empty
|
||||||
|
self.split_lines = split_lines
|
||||||
|
|
||||||
|
def __call__(self, form, field):
|
||||||
|
data = field.data
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# normalize into list of lines
|
||||||
|
if isinstance(data, str) and self.split_lines:
|
||||||
|
lines = data.splitlines()
|
||||||
|
elif isinstance(data, (list, tuple)):
|
||||||
|
lines = data
|
||||||
|
else:
|
||||||
|
lines = [data]
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped:
|
||||||
|
if self.allow_empty:
|
||||||
|
continue
|
||||||
|
raise ValidationError(self.message or "Empty value not allowed.")
|
||||||
|
if not self.pattern.match(stripped):
|
||||||
|
raise ValidationError(self.message or "Invalid value.")
|
||||||
|
|
||||||
class quickWatchForm(Form):
|
class quickWatchForm(Form):
|
||||||
from . import processors
|
from . import processors
|
||||||
|
|
||||||
@@ -865,16 +910,29 @@ class processor_text_json_diff_form(commonSettingsForm):
|
|||||||
|
|
||||||
|
|
||||||
class SingleExtraProxy(Form):
|
class SingleExtraProxy(Form):
|
||||||
|
|
||||||
# maybe better to set some <script>var..
|
# maybe better to set some <script>var..
|
||||||
proxy_name = StringField('Name', [validators.Optional()], render_kw={"placeholder": "Name"})
|
proxy_name = StringField('Name', [validators.Optional()], render_kw={"placeholder": "Name"})
|
||||||
proxy_url = StringField('Proxy URL', [validators.Optional()], render_kw={"placeholder": "socks5:// or regular proxy http://user:pass@...:3128", "size":50})
|
proxy_url = StringField('Proxy URL', [
|
||||||
# @todo do the validation here instead
|
validators.Optional(),
|
||||||
|
ValidateStartsWithRegex(
|
||||||
|
regex=r'^(https?|socks5)://', # ✅ main pattern
|
||||||
|
flags=re.IGNORECASE, # ✅ makes it case-insensitive
|
||||||
|
message='Proxy URLs must start with http://, https:// or socks5://',
|
||||||
|
),
|
||||||
|
ValidateSimpleURL()
|
||||||
|
], render_kw={"placeholder": "socks5:// or regular proxy http://user:pass@...:3128", "size":50})
|
||||||
|
|
||||||
class SingleExtraBrowser(Form):
|
class SingleExtraBrowser(Form):
|
||||||
browser_name = StringField('Name', [validators.Optional()], render_kw={"placeholder": "Name"})
|
browser_name = StringField('Name', [validators.Optional()], render_kw={"placeholder": "Name"})
|
||||||
browser_connection_url = StringField('Browser connection URL', [validators.Optional()], render_kw={"placeholder": "wss://brightdata... wss://oxylabs etc", "size":50})
|
browser_connection_url = StringField('Browser connection URL', [
|
||||||
# @todo do the validation here instead
|
validators.Optional(),
|
||||||
|
ValidateStartsWithRegex(
|
||||||
|
regex=r'^(wss?|ws)://',
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
message='Browser URLs must start with wss:// or ws://'
|
||||||
|
),
|
||||||
|
ValidateSimpleURL()
|
||||||
|
], render_kw={"placeholder": "wss://brightdata... wss://oxylabs etc", "size":50})
|
||||||
|
|
||||||
class DefaultUAInputForm(Form):
|
class DefaultUAInputForm(Form):
|
||||||
html_requests = StringField('Plaintext requests', validators=[validators.Optional()], render_kw={"placeholder": "<default>"})
|
html_requests = StringField('Plaintext requests', validators=[validators.Optional()], render_kw={"placeholder": "<default>"})
|
||||||
|
|||||||
@@ -14,13 +14,31 @@
|
|||||||
{% if field.errors is mapping and 'form' in field.errors %}
|
{% if field.errors is mapping and 'form' in field.errors %}
|
||||||
{# and subfield form errors, such as used in RequiredFormField() for TimeBetweenCheckForm sub form #}
|
{# and subfield form errors, such as used in RequiredFormField() for TimeBetweenCheckForm sub form #}
|
||||||
{% set errors = field.errors['form'] %}
|
{% set errors = field.errors['form'] %}
|
||||||
|
{% for error in errors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% elif field.type == 'FieldList' %}
|
||||||
|
{# Handle FieldList of FormFields - errors is a list of dicts, one per entry #}
|
||||||
|
{% for idx, entry_errors in field.errors|enumerate %}
|
||||||
|
{% if entry_errors is mapping and entry_errors %}
|
||||||
|
{# Only show entries that have actual errors #}
|
||||||
|
<li><strong>Entry {{ idx + 1 }}:</strong>
|
||||||
|
<ul>
|
||||||
|
{% for field_name, messages in entry_errors.items() %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>{{ field_name }}: {{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{# regular list of errors with this field #}
|
{# regular list of errors with this field #}
|
||||||
{% set errors = field.errors %}
|
{% for error in field.errors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for error in errors %}
|
|
||||||
<li>{{ error }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -93,6 +111,39 @@
|
|||||||
{{ field(**kwargs)|safe }}
|
{{ field(**kwargs)|safe }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_fieldlist_with_inline_errors(fieldlist) %}
|
||||||
|
{# Specialized macro for FieldList(FormField(...)) that renders errors inline with each field #}
|
||||||
|
<div {% if fieldlist.errors %} class="error" {% endif %}>{{ fieldlist.label }}</div>
|
||||||
|
<div {% if fieldlist.errors %} class="error" {% endif %}>
|
||||||
|
<ul id="{{ fieldlist.id }}">
|
||||||
|
{% for entry in fieldlist %}
|
||||||
|
<li {% if entry.errors %} class="error" {% endif %}>
|
||||||
|
<label for="{{ entry.id }}" {% if entry.errors %} class="error" {% endif %}>{{ fieldlist.label.text }}-{{ loop.index0 }}</label>
|
||||||
|
<table id="{{ entry.id }}" {% if entry.errors %} class="error" {% endif %}>
|
||||||
|
<tbody>
|
||||||
|
{% for subfield in entry %}
|
||||||
|
<tr {% if subfield.errors %} class="error" {% endif %}>
|
||||||
|
<th {% if subfield.errors %} class="error" {% endif %}><label for="{{ subfield.id }}" {% if subfield.errors %} class="error" {% endif %}>{{ subfield.label.text }}</label></th>
|
||||||
|
<td {% if subfield.errors %} class="error" {% endif %}>
|
||||||
|
{{ subfield(**kwargs)|safe }}
|
||||||
|
{% if subfield.errors %}
|
||||||
|
<ul class="errors">
|
||||||
|
{% for error in subfield.errors %}
|
||||||
|
<li class="error">{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_conditions_fieldlist_of_formfields_as_table(fieldlist, table_id="rulesTable") %}
|
{% macro render_conditions_fieldlist_of_formfields_as_table(fieldlist, table_id="rulesTable") %}
|
||||||
<div class="fieldlist_formfields" id="{{ table_id }}">
|
<div class="fieldlist_formfields" id="{{ table_id }}">
|
||||||
<div class="fieldlist-header">
|
<div class="fieldlist-header">
|
||||||
|
|||||||
@@ -49,3 +49,39 @@ def test_select_custom(client, live_server, measure_memory_usage):
|
|||||||
#
|
#
|
||||||
# Now we should see the request in the container logs for "squid-squid-custom" because it will be the only default
|
# Now we should see the request in the container logs for "squid-squid-custom" because it will be the only default
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_proxy_validation(client, live_server, measure_memory_usage):
|
||||||
|
# live_server_setup(live_server) # Setup on conftest per function
|
||||||
|
|
||||||
|
# Goto settings, add our custom one
|
||||||
|
res = client.post(
|
||||||
|
url_for("settings.settings_page"),
|
||||||
|
data={
|
||||||
|
"requests-time_between_check-minutes": 180,
|
||||||
|
"application-ignore_whitespace": "y",
|
||||||
|
"application-fetch_backend": 'html_requests',
|
||||||
|
"requests-extra_proxies-0-proxy_name": "custom-test-proxy",
|
||||||
|
"requests-extra_proxies-0-proxy_url": "xxxxhtt/333??p://test:awesome@squid-custom:3128",
|
||||||
|
},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b"Settings updated." not in res.data
|
||||||
|
assert b'Proxy URLs must start with' in res.data
|
||||||
|
|
||||||
|
|
||||||
|
res = client.post(
|
||||||
|
url_for("settings.settings_page"),
|
||||||
|
data={
|
||||||
|
"requests-time_between_check-minutes": 180,
|
||||||
|
"application-ignore_whitespace": "y",
|
||||||
|
"application-fetch_backend": 'html_requests',
|
||||||
|
"requests-extra_proxies-0-proxy_name": "custom-test-proxy",
|
||||||
|
"requests-extra_proxies-0-proxy_url": "https://",
|
||||||
|
},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b"Settings updated." not in res.data
|
||||||
|
assert b"Invalid URL." in res.data
|
||||||
|
|
||||||
Reference in New Issue
Block a user