2022-11-24 19:53:01 +00:00
import os
2022-03-12 12:29:30 +00:00
import re
2024-04-03 14:17:15 +00:00
from changedetectionio . strtobool import strtobool
2022-02-23 09:49:25 +00:00
2022-03-12 12:29:30 +00:00
from wtforms import (
BooleanField ,
Form ,
IntegerField ,
RadioField ,
SelectField ,
StringField ,
SubmitField ,
TextAreaField ,
fields ,
validators ,
2022-11-24 19:53:01 +00:00
widgets
2022-03-12 12:29:30 +00:00
)
2023-11-01 14:36:49 +00:00
from flask_wtf . file import FileField , FileAllowed
2022-11-24 19:53:01 +00:00
from wtforms . fields import FieldList
2023-11-01 14:36:49 +00:00
2022-03-12 12:29:30 +00:00
from wtforms . validators import ValidationError
2021-06-21 06:21:05 +00:00
2023-11-01 14:36:49 +00:00
from validators . url import url as url_validator
2022-11-24 19:53:01 +00:00
# default
# each select <option data-enabled="enabled-0-0"
from changedetectionio . blueprint . browser_steps . browser_steps import browser_step_ui_config
2024-02-10 23:09:12 +00:00
from changedetectionio import html_tools , content_fetchers
2023-10-26 18:19:59 +00:00
2022-03-12 12:29:30 +00:00
from changedetectionio . notification import (
valid_notification_formats ,
)
2021-12-04 13:41:48 +00:00
2022-04-19 19:43:07 +00:00
from wtforms . fields import FormField
2023-06-19 21:29:13 +00:00
dictfilt = lambda x , y : dict ( [ ( i , x [ i ] ) for i in x if i in set ( y ) ] )
2021-12-29 22:18:29 +00:00
valid_method = {
' GET ' ,
' POST ' ,
' PUT ' ,
' PATCH ' ,
' DELETE ' ,
2024-01-08 22:32:44 +00:00
' OPTIONS ' ,
2021-12-29 22:18:29 +00:00
}
default_method = ' GET '
2023-11-01 14:36:49 +00:00
allow_simplehost = not strtobool ( os . getenv ( ' BLOCK_SIMPLEHOSTS ' , ' False ' ) )
2022-04-24 11:12:50 +00:00
2021-06-21 06:21:05 +00:00
class StringListField ( StringField ) :
widget = widgets . TextArea ( )
def _value ( self ) :
if self . data :
2022-04-24 11:12:50 +00:00
# ignore empty lines in the storage
data = list ( filter ( lambda x : len ( x . strip ( ) ) , self . data ) )
# Apply strip to each line
data = list ( map ( lambda x : x . strip ( ) , data ) )
return " \r \n " . join ( data )
2021-06-21 06:21:05 +00:00
else :
return u ' '
# incoming
def process_formdata ( self , valuelist ) :
2022-04-24 11:12:50 +00:00
if valuelist and len ( valuelist [ 0 ] . strip ( ) ) :
# Remove empty strings, stripping and splitting \r\n, only \n etc.
self . data = valuelist [ 0 ] . splitlines ( )
# Remove empty lines from the final data
self . data = list ( filter ( lambda x : len ( x . strip ( ) ) , self . data ) )
2021-06-21 06:21:05 +00:00
else :
self . data = [ ]
class SaltyPasswordField ( StringField ) :
widget = widgets . PasswordInput ( )
encrypted_password = " "
def build_password ( self , password ) :
import base64
2022-03-12 12:29:30 +00:00
import hashlib
2021-06-21 06:21:05 +00:00
import secrets
# Make a new salt on every new password and store it with the password
salt = secrets . token_bytes ( 32 )
key = hashlib . pbkdf2_hmac ( ' sha256 ' , password . encode ( ' utf-8 ' ) , salt , 100000 )
store = base64 . b64encode ( salt + key ) . decode ( ' ascii ' )
return store
# incoming
def process_formdata ( self , valuelist ) :
if valuelist :
2021-06-21 12:12:47 +00:00
# Be really sure it's non-zero in length
if len ( valuelist [ 0 ] . strip ( ) ) > 0 :
self . encrypted_password = self . build_password ( valuelist [ 0 ] )
self . data = " "
2021-06-21 06:21:05 +00:00
else :
2021-06-21 12:12:47 +00:00
self . data = False
2021-06-21 06:21:05 +00:00
2023-06-19 21:29:13 +00:00
class StringTagUUID ( StringField ) :
# process_formdata(self, valuelist) handled manually in POST handler
# Is what is shown when field <input> is rendered
def _value ( self ) :
# Tag UUID to name, on submit it will convert it back (in the submit handler of init.py)
if self . data and type ( self . data ) is list :
tag_titles = [ ]
for i in self . data :
tag = self . datastore . data [ ' settings ' ] [ ' application ' ] [ ' tags ' ] . get ( i )
if tag :
tag_title = tag . get ( ' title ' )
if tag_title :
tag_titles . append ( tag_title )
return ' , ' . join ( tag_titles )
if not self . data :
return ' '
return ' error '
2022-04-24 14:56:32 +00:00
class TimeBetweenCheckForm ( Form ) :
weeks = IntegerField ( ' Weeks ' , validators = [ validators . Optional ( ) , validators . NumberRange ( min = 0 , message = " Should contain zero or more seconds " ) ] )
days = IntegerField ( ' Days ' , validators = [ validators . Optional ( ) , validators . NumberRange ( min = 0 , message = " Should contain zero or more seconds " ) ] )
hours = IntegerField ( ' Hours ' , validators = [ validators . Optional ( ) , validators . NumberRange ( min = 0 , message = " Should contain zero or more seconds " ) ] )
minutes = IntegerField ( ' Minutes ' , validators = [ validators . Optional ( ) , validators . NumberRange ( min = 0 , message = " Should contain zero or more seconds " ) ] )
seconds = IntegerField ( ' Seconds ' , validators = [ validators . Optional ( ) , validators . NumberRange ( min = 0 , message = " Should contain zero or more seconds " ) ] )
# @todo add total seconds minimum validatior = minimum_seconds_recheck_time
2021-06-21 06:21:05 +00:00
# Separated by key:value
class StringDictKeyValue ( StringField ) :
widget = widgets . TextArea ( )
def _value ( self ) :
if self . data :
output = u ' '
for k in self . data . keys ( ) :
output + = " {} : {} \r \n " . format ( k , self . data [ k ] )
return output
else :
return u ' '
# incoming
def process_formdata ( self , valuelist ) :
if valuelist :
self . data = { }
# Remove empty strings
cleaned = list ( filter ( None , valuelist [ 0 ] . split ( " \n " ) ) )
for s in cleaned :
2021-08-07 12:15:41 +00:00
parts = s . strip ( ) . split ( ' : ' , 1 )
2021-06-21 06:21:05 +00:00
if len ( parts ) == 2 :
self . data . update ( { parts [ 0 ] . strip ( ) : parts [ 1 ] . strip ( ) } )
else :
self . data = { }
2021-08-12 10:05:59 +00:00
class ValidateContentFetcherIsReady ( object ) :
"""
Validates that anything that looks like a regex passes as a regex
"""
def __init__ ( self , message = None ) :
self . message = message
def __call__ ( self , form , field ) :
2023-11-13 15:39:11 +00:00
return
2022-03-12 12:29:30 +00:00
2023-11-13 15:39:11 +00:00
# AttributeError: module 'changedetectionio.content_fetcher' has no attribute 'extra_browser_unlocked<>ASDF213r123r'
2021-08-12 10:05:59 +00:00
# Better would be a radiohandler that keeps a reference to each class
2024-02-10 23:09:12 +00:00
# if field.data is not None and field.data != 'system':
# klass = getattr(content_fetcher, field.data)
# some_object = klass()
# try:
# ready = some_object.is_ready()
#
# except urllib3.exceptions.MaxRetryError as e:
# driver_url = some_object.command_executor
# message = field.gettext('Content fetcher \'%s\' did not respond.' % (field.data))
# message += '<br>' + field.gettext(
# 'Be sure that the selenium/webdriver runner is running and accessible via network from this container/host.')
# message += '<br>' + field.gettext('Did you follow the instructions in the wiki?')
# message += '<br><br>' + field.gettext('WebDriver Host: %s' % (driver_url))
# message += '<br><a href="https://github.com/dgtlmoon/changedetection.io/wiki/Fetching-pages-with-WebDriver">Go here for more information</a>'
# message += '<br>'+field.gettext('Content fetcher did not respond properly, unable to use it.\n %s' % (str(e)))
#
# raise ValidationError(message)
#
# except Exception as e:
# message = field.gettext('Content fetcher \'%s\' did not respond properly, unable to use it.\n %s')
# raise ValidationError(message % (field.data, e))
2021-08-12 10:05:59 +00:00
2022-01-10 16:38:04 +00:00
class ValidateNotificationBodyAndTitleWhenURLisSet ( object ) :
"""
Validates that they entered something in both notification title + body when the URL is set
Due to https : / / github . com / dgtlmoon / changedetection . io / issues / 360
"""
def __init__ ( self , message = None ) :
self . message = message
def __call__ ( self , form , field ) :
if len ( field . data ) :
if not len ( form . notification_title . data ) or not len ( form . notification_body . data ) :
message = field . gettext ( ' Notification Body and Title is required when a Notification URL is used ' )
raise ValidationError ( message )
2021-09-17 16:37:26 +00:00
class ValidateAppRiseServers ( object ) :
"""
Validates that each URL given is compatible with AppRise
"""
def __init__ ( self , message = None ) :
self . message = message
def __call__ ( self , form , field ) :
import apprise
apobj = apprise . Apprise ( )
for server_url in field . data :
if not apobj . add ( server_url ) :
message = field . gettext ( ' \' %s \' is not a valid AppRise URL. ' % ( server_url ) )
raise ValidationError ( message )
2022-12-05 18:58:43 +00:00
class ValidateJinja2Template ( object ) :
2021-08-22 16:45:32 +00:00
"""
Validates that a { token } is from a valid set
"""
def __init__ ( self , message = None ) :
self . message = message
def __call__ ( self , form , field ) :
from changedetectionio import notification
2022-12-05 18:58:43 +00:00
2023-09-23 12:50:21 +00:00
from jinja2 import Environment , BaseLoader , TemplateSyntaxError , UndefinedError
2022-12-05 18:58:43 +00:00
from jinja2 . meta import find_undeclared_variables
try :
jinja2_env = Environment ( loader = BaseLoader )
jinja2_env . globals . update ( notification . valid_tokens )
2023-09-23 12:50:21 +00:00
2022-12-05 18:58:43 +00:00
rendered = jinja2_env . from_string ( field . data ) . render ( )
except TemplateSyntaxError as e :
raise ValidationError ( f " This is not a valid Jinja2 template: { e } " ) from e
2023-09-23 12:50:21 +00:00
except UndefinedError as e :
raise ValidationError ( f " A variable or function is not defined: { e } " ) from e
2022-12-05 18:58:43 +00:00
ast = jinja2_env . parse ( field . data )
undefined = " , " . join ( find_undeclared_variables ( ast ) )
if undefined :
raise ValidationError (
f " The following tokens used in the notification are not valid: { undefined } "
)
2022-12-04 15:09:09 +00:00
2022-01-15 21:52:39 +00:00
class validateURL ( object ) :
2022-12-04 15:09:09 +00:00
2022-01-15 21:52:39 +00:00
"""
Flask wtform validators wont work with basic auth
"""
def __init__ ( self , message = None ) :
self . message = message
2021-08-12 10:05:59 +00:00
2022-01-15 21:52:39 +00:00
def __call__ ( self , form , field ) :
2023-11-01 14:36:49 +00:00
# This should raise a ValidationError() or not
validate_url ( field . data )
def validate_url ( test_url ) :
# If hosts that only contain alphanumerics are allowed ("localhost" for example)
try :
url_validator ( test_url , simple_host = allow_simplehost )
except validators . ValidationError :
#@todo check for xss
message = f " ' { test_url } ' is not a valid URL. "
# This should be wtforms.validators.
raise ValidationError ( message )
from . model . Watch import is_safe_url
if not is_safe_url ( test_url ) :
# This should be wtforms.validators.
raise ValidationError ( ' Watch protocol is not permitted by SAFE_PROTOCOL_REGEX or incorrect URL format ' )
2022-12-05 18:58:43 +00:00
2021-07-11 12:07:39 +00:00
class ValidateListRegex ( object ) :
2021-06-21 07:17:22 +00:00
"""
Validates that anything that looks like a regex passes as a regex
"""
def __init__ ( self , message = None ) :
self . message = message
def __call__ ( self , form , field ) :
for line in field . data :
2023-10-26 18:19:59 +00:00
if re . search ( html_tools . PERL_STYLE_REGEX , line , re . IGNORECASE ) :
2021-06-21 07:17:22 +00:00
try :
2023-10-26 18:19:59 +00:00
regex = html_tools . perl_style_slash_enclosed_regex_to_options ( line )
re . compile ( regex )
2021-06-21 07:17:22 +00:00
except re . error :
message = field . gettext ( ' RegEx \' %s \' is not a valid regular expression. ' )
raise ValidationError ( message % ( line ) )
2022-04-09 08:35:14 +00:00
2022-03-12 12:29:30 +00:00
class ValidateCSSJSONXPATHInput ( object ) :
2021-07-11 12:07:39 +00:00
"""
Filter validation
@todo CSS validator ; )
"""
2022-03-12 12:29:30 +00:00
def __init__ ( self , message = None , allow_xpath = True , allow_json = True ) :
2021-07-11 12:07:39 +00:00
self . message = message
2022-03-12 12:29:30 +00:00
self . allow_xpath = allow_xpath
self . allow_json = allow_json
2021-07-11 12:07:39 +00:00
def __call__ ( self , form , field ) :
2022-01-05 16:58:07 +00:00
2022-03-12 12:29:30 +00:00
if isinstance ( field . data , str ) :
data = [ field . data ]
else :
data = field . data
for line in data :
2022-01-05 16:58:07 +00:00
# Nothing to see here
2022-03-12 12:29:30 +00:00
if not len ( line . strip ( ) ) :
return
2022-01-05 16:58:07 +00:00
2022-03-12 12:29:30 +00:00
# Does it look like XPath?
2023-11-13 15:42:21 +00:00
if line . strip ( ) [ 0 ] == ' / ' or line . strip ( ) . startswith ( ' xpath: ' ) :
2022-03-12 12:29:30 +00:00
if not self . allow_xpath :
raise ValidationError ( " XPath not permitted in this field! " )
from lxml import etree , html
2023-11-13 15:42:21 +00:00
import elementpath
# xpath 2.0-3.1
from elementpath . xpath3 import XPath3Parser
2022-03-12 12:29:30 +00:00
tree = html . fromstring ( " <html></html> " )
2023-11-13 15:42:21 +00:00
line = line . replace ( ' xpath: ' , ' ' )
try :
elementpath . select ( tree , line . strip ( ) , parser = XPath3Parser )
except elementpath . ElementPathError as e :
message = field . gettext ( ' \' %s \' is not a valid XPath expression. ( %s ) ' )
raise ValidationError ( message % ( line , str ( e ) ) )
except :
raise ValidationError ( " A system-error occurred when validating your XPath expression " )
if line . strip ( ) . startswith ( ' xpath1: ' ) :
if not self . allow_xpath :
raise ValidationError ( " XPath not permitted in this field! " )
from lxml import etree , html
tree = html . fromstring ( " <html></html> " )
2023-11-13 20:23:43 +00:00
line = re . sub ( r ' ^xpath1: ' , ' ' , line )
2022-01-05 16:58:07 +00:00
2022-03-12 12:29:30 +00:00
try :
tree . xpath ( line . strip ( ) )
except etree . XPathEvalError as e :
message = field . gettext ( ' \' %s \' is not a valid XPath expression. ( %s ) ' )
raise ValidationError ( message % ( line , str ( e ) ) )
except :
raise ValidationError ( " A system-error occurred when validating your XPath expression " )
2022-01-05 16:58:07 +00:00
2022-03-12 12:29:30 +00:00
if ' json: ' in line :
if not self . allow_json :
raise ValidationError ( " JSONPath not permitted in this field! " )
2021-07-11 12:07:39 +00:00
2022-03-12 12:29:30 +00:00
from jsonpath_ng . exceptions import (
JsonPathLexerError ,
JsonPathParserError ,
)
from jsonpath_ng . ext import parse
2021-07-11 12:07:39 +00:00
2022-03-12 12:29:30 +00:00
input = line . replace ( ' json: ' , ' ' )
2021-07-11 12:07:39 +00:00
2022-03-12 12:29:30 +00:00
try :
parse ( input )
except ( JsonPathParserError , JsonPathLexerError ) as e :
message = field . gettext ( ' \' %s \' is not a valid JSONPath expression. ( %s ) ' )
raise ValidationError ( message % ( input , str ( e ) ) )
except :
raise ValidationError ( " A system-error occurred when validating your JSONPath expression " )
2021-09-17 16:37:26 +00:00
2022-03-12 12:29:30 +00:00
# Re #265 - maybe in the future fetch the page and offer a
# warning/notice that its possible the rule doesnt yet match anything?
2022-10-09 14:12:45 +00:00
if not self . allow_json :
raise ValidationError ( " jq not permitted in this field! " )
2022-10-12 07:53:16 +00:00
if ' jq: ' in line :
try :
import jq
except ModuleNotFoundError :
# `jq` requires full compilation in windows and so isn't generally available
raise ValidationError ( " jq not support not found " )
2022-10-09 14:12:45 +00:00
input = line . replace ( ' jq: ' , ' ' )
try :
jq . compile ( input )
except ( ValueError ) as e :
message = field . gettext ( ' \' %s \' is not a valid jq expression. ( %s ) ' )
raise ValidationError ( message % ( input , str ( e ) ) )
except :
raise ValidationError ( " A system-error occurred when validating your jq expression " )
2021-07-18 03:26:23 +00:00
class quickWatchForm ( Form ) :
2023-03-18 19:36:26 +00:00
from . import processors
2022-04-09 17:50:56 +00:00
url = fields . URLField ( ' URL ' , validators = [ validateURL ( ) ] )
2023-06-19 21:29:13 +00:00
tags = StringTagUUID ( ' Group tag ' , [ validators . Optional ( ) ] )
2022-07-28 10:13:26 +00:00
watch_submit_button = SubmitField ( ' Watch ' , render_kw = { " class " : " pure-button pure-button-primary " } )
2023-03-18 19:36:26 +00:00
processor = RadioField ( u ' Processor ' , choices = processors . available_processors ( ) , default = " text_json_diff " )
2022-07-28 10:13:26 +00:00
edit_and_watch_submit_button = SubmitField ( ' Edit > Watch ' , render_kw = { " class " : " pure-button pure-button-primary " } )
2021-07-18 03:26:23 +00:00
2022-04-19 19:43:07 +00:00
# Common to a single watch and the global settings
2021-09-17 16:37:26 +00:00
class commonSettingsForm ( Form ) :
2023-06-19 21:29:13 +00:00
2022-12-05 18:58:43 +00:00
notification_urls = StringListField ( ' Notification URL List ' , validators = [ validators . Optional ( ) , ValidateAppRiseServers ( ) ] )
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 ( ) ] )
2022-09-11 07:08:13 +00:00
notification_format = SelectField ( ' Notification format ' , choices = valid_notification_formats . keys ( ) )
2024-02-10 23:09:12 +00:00
fetch_backend = RadioField ( u ' Fetch Method ' , choices = content_fetchers . available_fetchers ( ) , validators = [ ValidateContentFetcherIsReady ( ) ] )
2021-09-19 20:57:15 +00:00
extract_title_as_title = BooleanField ( ' Extract <title> from document and use as watch title ' , default = False )
2022-09-11 07:08:13 +00:00
webdriver_delay = IntegerField ( ' Wait seconds before extracting text ' , validators = [ validators . Optional ( ) , validators . NumberRange ( min = 1 ,
message = " Should contain one or more seconds " ) ] )
2023-03-18 19:36:26 +00:00
class importForm ( Form ) :
from . import processors
processor = RadioField ( u ' Processor ' , choices = processors . available_processors ( ) , default = " text_json_diff " )
urls = TextAreaField ( ' URLs ' )
2023-11-01 14:36:49 +00:00
xlsx_file = FileField ( ' Upload .xlsx file ' , validators = [ FileAllowed ( [ ' xlsx ' ] , ' Must be .xlsx file! ' ) ] )
file_mapping = SelectField ( ' File mapping ' , [ validators . DataRequired ( ) ] , choices = { ( ' wachete ' , ' Wachete mapping ' ) , ( ' custom ' , ' Custom mapping ' ) } )
2021-09-17 16:37:26 +00:00
2022-11-24 19:53:01 +00:00
class SingleBrowserStep ( Form ) :
operation = SelectField ( ' Operation ' , [ validators . Optional ( ) ] , choices = browser_step_ui_config . keys ( ) )
# maybe better to set some <script>var..
selector = StringField ( ' Selector ' , [ validators . Optional ( ) ] , render_kw = { " placeholder " : " CSS or xPath selector " } )
optional_value = StringField ( ' value ' , [ validators . Optional ( ) ] , render_kw = { " placeholder " : " Value " } )
# @todo move to JS? ajax fetch new field?
# remove_button = SubmitField('-', render_kw={"type": "button", "class": "pure-button pure-button-primary", 'title': 'Remove'})
# add_button = SubmitField('+', render_kw={"type": "button", "class": "pure-button pure-button-primary", 'title': 'Add new step after'})
2021-09-17 16:37:26 +00:00
class watchForm ( commonSettingsForm ) :
2022-04-09 17:50:56 +00:00
url = fields . URLField ( ' URL ' , validators = [ validateURL ( ) ] )
2023-06-19 21:29:13 +00:00
tags = StringTagUUID ( ' Group tag ' , [ validators . Optional ( ) ] , default = ' ' )
2021-07-18 03:26:23 +00:00
2022-04-24 14:56:32 +00:00
time_between_check = FormField ( TimeBetweenCheckForm )
2022-04-09 17:50:56 +00:00
2022-11-03 11:13:54 +00:00
include_filters = StringListField ( ' CSS/JSONPath/JQ/XPath Filters ' , [ ValidateCSSJSONXPATHInput ( ) ] , default = ' ' )
2022-04-09 17:50:56 +00:00
2022-03-12 12:29:30 +00:00
subtractive_selectors = StringListField ( ' Remove elements ' , [ ValidateCSSJSONXPATHInput ( allow_xpath = False , allow_json = False ) ] )
2022-06-06 14:57:50 +00:00
extract_text = StringListField ( ' Extract text ' , [ ValidateListRegex ( ) ] )
2022-04-09 17:50:56 +00:00
title = StringField ( ' Title ' , default = ' ' )
2021-06-21 06:21:05 +00:00
2022-04-09 10:15:34 +00:00
ignore_text = StringListField ( ' Ignore text ' , [ ValidateListRegex ( ) ] )
headers = StringDictKeyValue ( ' Request headers ' )
body = TextAreaField ( ' Request body ' , [ validators . Optional ( ) ] )
method = SelectField ( ' Request method ' , choices = valid_method , default = default_method )
ignore_status_codes = BooleanField ( ' Ignore status codes (process non-2xx status codes as normal) ' , default = False )
2023-03-20 19:16:57 +00:00
check_unique_lines = BooleanField ( ' Only trigger when unique lines appear ' , default = False )
2024-02-02 10:36:58 +00:00
sort_text_alphabetically = BooleanField ( ' Sort text alphabetically ' , default = False )
2023-03-20 19:16:57 +00:00
filter_text_added = BooleanField ( ' Added lines ' , default = True )
filter_text_replaced = BooleanField ( ' Replaced/changed lines ' , default = True )
filter_text_removed = BooleanField ( ' Removed lines ' , default = True )
# @todo this class could be moved to its own text_json_diff_watchForm and this goes to restock_diff_Watchform perhaps
2023-03-18 19:36:26 +00:00
in_stock_only = BooleanField ( ' Only trigger when product goes BACK to in-stock ' , default = True )
2021-08-16 11:13:17 +00:00
trigger_text = StringListField ( ' Trigger/wait for text ' , [ validators . Optional ( ) , ValidateListRegex ( ) ] )
2022-11-24 19:53:01 +00:00
if os . getenv ( " PLAYWRIGHT_DRIVER_URL " ) :
browser_steps = FieldList ( FormField ( SingleBrowserStep ) , min_entries = 10 )
2023-03-20 19:16:57 +00:00
text_should_not_be_present = StringListField ( ' Block change-detection while text matches ' , [ validators . Optional ( ) , ValidateListRegex ( ) ] )
2022-07-10 11:56:01 +00:00
webdriver_js_execute_code = TextAreaField ( ' Execute JavaScript before change detection ' , render_kw = { " rows " : " 5 " } , validators = [ validators . Optional ( ) ] )
2022-02-23 09:49:25 +00:00
save_button = SubmitField ( ' Save ' , render_kw = { " class " : " pure-button pure-button-primary " } )
2022-08-01 12:47:00 +00:00
2022-05-08 18:35:36 +00:00
proxy = RadioField ( ' Proxy ' )
2022-07-23 15:15:27 +00:00
filter_failure_notification_send = BooleanField (
' Send a notification when the filter can no longer be found on the page ' , default = False )
2022-02-23 09:49:25 +00:00
2022-09-08 07:10:04 +00:00
notification_muted = BooleanField ( ' Notifications Muted / Off ' , default = False )
2022-11-20 08:37:48 +00:00
notification_screenshot = BooleanField ( ' Attach screenshot to notification (where possible) ' , default = False )
2022-08-31 13:49:13 +00:00
2021-12-29 22:18:29 +00:00
def validate ( self , * * kwargs ) :
if not super ( ) . validate ( ) :
return False
result = True
# Fail form validation when a body is set for a GET
if self . method . data == ' GET ' and self . body . data :
self . body . errors . append ( ' Body must be empty when Request Method is set to GET ' )
result = False
2022-11-27 15:18:11 +00:00
# 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 :
ready_url = str ( jinja2_env . from_string ( self . url . data ) . render ( ) )
except Exception as e :
self . url . errors . append ( ' Invalid template syntax ' )
result = False
2021-12-29 22:18:29 +00:00
return result
2021-06-21 06:21:05 +00:00
2022-04-19 19:43:07 +00:00
2022-12-19 20:48:01 +00:00
class SingleExtraProxy ( Form ) :
# maybe better to set some <script>var..
proxy_name = StringField ( ' Name ' , [ validators . Optional ( ) ] , render_kw = { " placeholder " : " Name " } )
2023-10-09 14:57:30 +00:00
proxy_url = StringField ( ' Proxy URL ' , [ validators . Optional ( ) ] , render_kw = { " placeholder " : " socks5:// or regular proxy http://user:pass@...:3128 " , " size " : 50 } )
2022-12-19 20:48:01 +00:00
# @todo do the validation here instead
2023-11-13 15:39:11 +00:00
class SingleExtraBrowser ( Form ) :
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 } )
# @todo do the validation here instead
2022-04-19 19:43:07 +00:00
# datastore.data['settings']['requests']..
class globalSettingsRequestForm ( Form ) :
2022-04-24 14:56:32 +00:00
time_between_check = FormField ( TimeBetweenCheckForm )
2022-05-08 18:35:36 +00:00
proxy = RadioField ( ' Proxy ' )
2022-06-13 10:41:53 +00:00
jitter_seconds = IntegerField ( ' Random jitter seconds ± check ' ,
render_kw = { " style " : " width: 5em; " } ,
validators = [ validators . NumberRange ( min = 0 , message = " Should contain zero or more seconds " ) ] )
2022-12-19 20:48:01 +00:00
extra_proxies = FieldList ( FormField ( SingleExtraProxy ) , min_entries = 5 )
2023-11-13 15:39:11 +00:00
extra_browsers = FieldList ( FormField ( SingleExtraBrowser ) , min_entries = 5 )
2022-12-19 20:48:01 +00:00
def validate_extra_proxies ( self , extra_validators = None ) :
for e in self . data [ ' extra_proxies ' ] :
if e . get ( ' proxy_name ' ) or e . get ( ' proxy_url ' ) :
if not e . get ( ' proxy_name ' , ' ' ) . strip ( ) or not e . get ( ' proxy_url ' , ' ' ) . strip ( ) :
self . extra_proxies . errors . append ( ' Both a name, and a Proxy URL is required. ' )
return False
2022-04-24 14:56:32 +00:00
2022-04-19 19:43:07 +00:00
# datastore.data['settings']['application']..
class globalSettingsApplicationForm ( commonSettingsForm ) :
2023-01-29 21:36:55 +00:00
api_access_token_enabled = BooleanField ( ' API access token security check enabled ' , default = True , validators = [ validators . Optional ( ) ] )
2023-09-14 11:07:01 +00:00
base_url = StringField ( ' Notification base URL override ' ,
validators = [ validators . Optional ( ) ] ,
render_kw = { " placeholder " : os . getenv ( ' BASE_URL ' , ' Not set ' ) }
)
2023-01-29 21:36:55 +00:00
empty_pages_are_a_change = BooleanField ( ' Treat empty pages as a change? ' , default = False )
2024-02-10 23:09:12 +00:00
fetch_backend = RadioField ( ' Fetch Method ' , default = " html_requests " , choices = content_fetchers . available_fetchers ( ) , validators = [ ValidateContentFetcherIsReady ( ) ] )
2022-04-19 19:43:07 +00:00
global_ignore_text = StringListField ( ' Ignore Text ' , [ ValidateListRegex ( ) ] )
2023-01-29 21:36:55 +00:00
global_subtractive_selectors = StringListField ( ' Remove elements ' , [ ValidateCSSJSONXPATHInput ( allow_xpath = False , allow_json = False ) ] )
2022-03-12 12:29:30 +00:00
ignore_whitespace = BooleanField ( ' Ignore whitespace ' )
2023-01-29 21:36:55 +00:00
password = SaltyPasswordField ( )
2023-05-25 14:38:54 +00:00
pager_size = IntegerField ( ' Pager size ' ,
render_kw = { " style " : " width: 5em; " } ,
validators = [ validators . NumberRange ( min = 0 ,
message = " Should be atleast zero (disabled) " ) ] )
2022-04-19 19:43:07 +00:00
removepassword_button = SubmitField ( ' Remove password ' , render_kw = { " class " : " pure-button pure-button-primary " } )
render_anchor_tag_content = BooleanField ( ' Render anchor tag content ' , default = False )
2023-01-29 21:36:55 +00:00
shared_diff_access = BooleanField ( ' Allow access to view diff page when password is enabled ' , default = False , validators = [ validators . Optional ( ) ] )
2022-07-23 15:15:27 +00:00
filter_failure_notification_threshold_attempts = IntegerField ( ' Number of times the filter can be missing before sending a notification ' ,
render_kw = { " style " : " width: 5em; " } ,
validators = [ validators . NumberRange ( min = 0 ,
message = " Should contain zero or more attempts " ) ] )
2022-04-09 08:35:14 +00:00
2022-04-19 19:43:07 +00:00
class globalSettingsForm ( Form ) :
# Define these as FormFields/"sub forms", this way it matches the JSON storage
# datastore.data['settings']['application']..
# datastore.data['settings']['requests']..
2022-04-09 08:35:14 +00:00
2022-04-19 19:43:07 +00:00
requests = FormField ( globalSettingsRequestForm )
application = FormField ( globalSettingsApplicationForm )
2022-03-21 21:54:27 +00:00
save_button = SubmitField ( ' Save ' , render_kw = { " class " : " pure-button pure-button-primary " } )
2022-12-05 13:48:03 +00:00
class extractDataForm ( Form ) :
2022-12-05 15:36:00 +00:00
extract_regex = StringField ( ' RegEx to extract ' , validators = [ validators . Length ( min = 1 , message = " Needs a RegEx " ) ] )
2022-12-05 13:48:03 +00:00
extract_submit_button = SubmitField ( ' Extract as CSV ' , render_kw = { " class " : " pure-button pure-button-primary " } )