misc: add fields verification after tracking code (#59027)

This commit is contained in:
Thomas NOËL 2022-02-23 01:23:21 +01:00
parent b7a5464822
commit fc8e3458a9
8 changed files with 222 additions and 7 deletions

View File

@ -178,10 +178,11 @@ formulaire.
<code mime="application/json">
{
"name": "Newsletter",
"only_allow_one": "false",
"enable_tracking_codes": "true",
"confirmation": "true",
"discussion": "false",
"only_allow_one": false,
"enable_tracking_codes": true,
"tracking_code_verify_fields": ["1"],
"confirmation": true,
"discussion": false,
"fields": [
{
"label": "Nom",

View File

@ -266,6 +266,20 @@ def test_forms_edit_tracking_code(pub, formdef):
resp = resp.forms[0].submit().follow()
assert FormDef.get(1).drafts_lifespan == '5'
formdef.fields = [
fields.StringField(id='1', label='VerifyString', type='string'),
fields.DateField(id='2', label='VerifyDate', type='date'),
fields.ItemField(id='3', label='CannotVerify', type='item'),
]
formdef.store()
resp = resp.click('Tracking Code')
assert '<option value="1">VerifyString</option>' in resp
assert '<option value="2">VerifyDate</option>' in resp
assert 'CannotVerify' not in resp
resp.forms[0]['tracking_code_verify_fields$element0'].value = '1'
resp = resp.forms[0].submit().follow()
assert FormDef.get(1).tracking_code_verify_fields == ['1']
def test_forms_edit_captcha(pub, formdef):
create_superuser(pub)

View File

@ -452,6 +452,8 @@ def test_formdef_schema(pub, access):
formdef.category_id = cat.id
formdef.workflow_id = workflow.id
formdef.enable_tracking_codes = True
formdef.tracking_code_verify_fields = ['0']
formdef.store()
with mock.patch('wcs.qommon.misc.urlopen') as urlopen:
@ -488,6 +490,7 @@ def test_formdef_schema(pub, access):
# check schema
assert set(resp.json.keys()) >= {
'enable_tracking_codes',
'tracking_code_verify_fields',
'url_name',
'description',
'workflow',
@ -506,6 +509,9 @@ def test_formdef_schema(pub, access):
}
assert resp.json['name'] == 'test'
assert resp.json['enable_tracking_codes'] is True
assert resp.json['tracking_code_verify_fields'] == ['0']
# fields checks
assert resp.json['fields'][0]['label'] == 'foobar'
assert resp.json['fields'][0]['type'] == 'string'

View File

@ -1773,6 +1773,112 @@ def test_form_tracking_code(pub, nocache):
resp = resp.follow()
def test_form_tracking_code_verification(pub, nocache):
formdef = create_formdef()
formdef.fields = [
fields.StringField(id='0', label='string1', required=False),
fields.StringField(id='1', label='string2', required=False),
]
formdef.enable_tracking_codes = True
formdef.tracking_code_verify_fields = ['0', '1']
formdef.store()
resp = get_app(pub).get('/test/')
formdef.data_class().wipe()
assert '<h3>Tracking code</h3>' in resp.text
resp.forms[0]['f0'] = 'foobar1'
resp.forms[0]['f1'] = 'foobar2'
resp = resp.forms[0].submit('submit')
tracking_code = get_displayed_tracking_code(resp)
assert tracking_code is not None
assert formdef.data_class().count() == 1
assert formdef.data_class().select()[0].is_draft()
assert formdef.data_class().select()[0].tracking_code == tracking_code
assert formdef.data_class().select()[0].data['0'] == 'foobar1'
assert formdef.data_class().select()[0].data['1'] == 'foobar2'
formdata = formdef.data_class().select()[0]
formdata_id = formdata.id
resp = get_app(pub).get('/')
resp.forms[0]['code'] = tracking_code
resp = resp.forms[0].submit()
assert resp.location == 'http://example.net/code/%s/load' % tracking_code
resp = resp.follow()
assert 'Access rights verification' in resp
resp.forms[0]['f0'] = 'foobar1'
resp.forms[0]['f1'] = 'foobar2'
resp = resp.forms[0].submit('submit')
assert resp.location == 'http://example.net/test/%s/' % formdata_id
resp = resp.follow()
assert resp.location.startswith('http://example.net/test/?mt=')
resp = resp.follow()
# check anonymous user can't get to it from the URL
pub.session_manager.session_class.wipe()
resp = get_app(pub).get('http://example.net/test/%s/' % formdata_id)
assert resp.location.startswith('http://example.net/login')
# or logged users that didn't enter the code:
create_user(pub)
login(get_app(pub), username='foo', password='foo').get(
'http://example.net/test/%s/' % formdata_id, status=403
)
# verification failure
resp = get_app(pub).get('http://example.net/code/%s/load' % tracking_code)
assert 'Access rights verification' in resp
resp.forms[0]['f0'] = 'foobar1' # ok
resp.forms[0]['f1'] = 'barfoo2' # ko
resp = resp.forms[0].submit('submit')
assert 'Access denied: this content does match the form' in resp
# draft with an empty field: do not verify it
formdata.data['0'] = None
formdata.store()
resp = get_app(pub).get('http://example.net/code/%s/load' % tracking_code)
assert 'Access rights verification' in resp
assert 'f0' not in resp.forms[0].fields
assert 'f1' in resp.forms[0].fields
resp.forms[0]['f1'] = 'foobar2'
resp = resp.forms[0].submit('submit')
assert resp.location == 'http://example.net/test/%s/' % formdata_id
resp = resp.follow()
assert resp.location.startswith('http://example.net/test/?mt=')
resp = resp.follow()
# empty draft: no verification
formdata.data['1'] = None
formdata.store()
resp = get_app(pub).get('http://example.net/code/%s/load' % tracking_code)
assert resp.location == 'http://example.net/test/%s/' % formdata_id
resp = resp.follow()
assert resp.location.startswith('http://example.net/test/?mt=')
# not a draft: all validation fields are required
formdata.status = 'wf-new'
formdata.data['0'] = 'foobar1'
formdata.store()
resp = get_app(pub).get('http://example.net/code/%s/load' % tracking_code)
assert 'Access rights verification' in resp
assert 'f0' in resp.forms[0].fields
assert 'f1' in resp.forms[0].fields
resp.forms[0]['f0'] = 'foobar1'
resp.forms[0]['f1'] = ''
resp = resp.forms[0].submit('submit')
assert resp.location == 'http://example.net/test/%s/' % formdata_id
resp = resp.follow()
assert 'foobar1' in resp.text
assert 'form_comment' in resp.text # user is treated as submitter
# verification failure
resp = get_app(pub).get('http://example.net/code/%s/load' % tracking_code)
assert 'Access rights verification' in resp
resp.forms[0]['f0'] = 'foobar1' # ok
resp.forms[0]['f1'] = 'not empty' # ko
resp = resp.forms[0].submit('submit')
assert 'Access denied: this content does match the form.' in resp
def test_form_tracking_code_rate_limit(pub, freezer):
pub.load_site_options()
if not pub.site_options.has_section('options'):

View File

@ -884,3 +884,21 @@ def test_import_formdef_root_node_error():
excinfo.value.msg
== 'Provided XML file is invalid, it starts with a <wrong_root_node> tag instead of <carddef>'
)
def test_tracking_code_attributes(pub):
formdef = FormDef()
formdef.name = 'Foo'
formdef.url_name = 'foo'
formdef.confirmation = True
formdef.enable_tracking_codes = True
for verify_fields in (['1', '2'], [], None):
formdef.tracking_code_verify_fields = verify_fields
f2 = assert_xml_import_export_works(formdef)
assert f2.enable_tracking_codes == formdef.enable_tracking_codes
assert f2.tracking_code_verify_fields == formdef.tracking_code_verify_fields
assert f2.confirmation == formdef.confirmation
f2 = assert_json_import_export_works(formdef)
assert f2.enable_tracking_codes == formdef.enable_tracking_codes
assert f2.tracking_code_verify_fields == formdef.tracking_code_verify_fields
assert f2.confirmation == formdef.confirmation

View File

@ -298,6 +298,21 @@ class OptionsDirectory(Directory):
title=_('Enable support for tracking codes'),
value=self.formdef.enable_tracking_codes,
)
verify_fields = [(None, '---', None)]
for field in self.formdef.fields:
if field.type in ('string', 'date', 'email'):
verify_fields.append((field.id, field.label, field.id))
form.add(
WidgetList,
'tracking_code_verify_fields',
title=_('Fields to check after entering the tracking code'),
element_type=SingleSelectWidget,
value=self.formdef.tracking_code_verify_fields,
add_element_label=_('Add verification Field'),
element_kwargs={'render_br': False, 'options': verify_fields},
hint=_('Only text, date and email fields can be used'),
)
widget = form.add(
WcsExtraStringWidget,
'drafts_lifespan',
@ -474,6 +489,7 @@ class OptionsDirectory(Directory):
'only_allow_one',
'disabled',
'enable_tracking_codes',
'tracking_code_verify_fields',
'always_advertise',
'disabled_redirection',
'publication_date',

View File

@ -147,6 +147,7 @@ class FormDef(StorableObject):
disabled = False
only_allow_one = False
enable_tracking_codes = False
tracking_code_verify_fields = None
disabled_redirection = None
always_advertise = False
publication_date = None
@ -1012,7 +1013,7 @@ class FormDef(StorableObject):
if self.max_field_id is None and self.fields:
self.max_field_id = max(lax_int(x.id) for x in self.fields)
more_attributes = []
more_attributes = ['tracking_code_verify_fields']
if self.max_field_id:
more_attributes.append('max_field_id')
@ -1128,7 +1129,7 @@ class FormDef(StorableObject):
formdef.workflow_id = w.id
break
more_attributes = ['max_field_id']
more_attributes = ['max_field_id', 'tracking_code_verify_fields']
for attribute in cls.TEXT_ATTRIBUTES + cls.BOOLEAN_ATTRIBUTES + more_attributes:
if attribute in value:
setattr(formdef, attribute, value.get(attribute))
@ -1209,6 +1210,11 @@ class FormDef(StorableObject):
if self.max_field_id:
ET.SubElement(root, 'max_field_id').text = str(self.max_field_id)
if self.tracking_code_verify_fields is not None:
verify_fields = ET.SubElement(root, 'tracking_code_verify_fields')
for field_id in self.tracking_code_verify_fields:
ET.SubElement(verify_fields, 'field_id').text = str(field_id)
fields = ET.SubElement(root, 'fields')
for field in self.fields or []:
fields.append(field.export_to_xml(charset=charset, include_id=include_id))
@ -1400,6 +1406,12 @@ class FormDef(StorableObject):
else:
formdef.max_field_id = max(lax_int(x.id) for x in formdef.fields)
if tree.find('tracking_code_verify_fields') is not None:
formdef.tracking_code_verify_fields = [
xml_node_text(verify_field_id)
for verify_field_id in tree.findall('tracking_code_verify_fields/field_id')
]
formdef.workflow_options = {}
for option in tree.findall('options/option'):
option_value = None

View File

@ -45,7 +45,7 @@ from wcs.roles import logged_users_role
from wcs.variables import LazyFormDef
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowStatusItem
from ..qommon import _, emails, errors, get_cfg, misc, template
from ..qommon import _, emails, errors, get_cfg, misc, ngettext, template
from ..qommon.admin.emails import EmailsDirectory
from ..qommon.form import CheckboxWidget, EmailWidget, Form, HiddenErrorWidget, HtmlWidget, StringWidget
from ..qommon.template import TemplateError
@ -181,6 +181,48 @@ class TrackingCodeDirectory(Directory):
raise errors.TraversalError()
if get_request().is_from_bot():
raise errors.AccessForbiddenError()
verify_fields = []
for field in formdata.formdef.fields:
if field.id in (formdata.formdef.tracking_code_verify_fields or []):
if formdata.status == 'draft' and not formdata.data.get(field.id):
# a draft could be incomplete: do not test its empty values
continue
verify_fields.append(field)
if verify_fields:
form = Form()
for field in verify_fields:
widget = field.add_to_form(form)
widget.field = field
form.add_submit('submit', _('Verify'))
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('/')
bad_content = False
if form.is_submitted() and not form.has_errors():
for field in verify_fields:
value = formdata.data.get(field.id)
verify_value = form.get_widget('f%s' % field.id).parse()
if value != verify_value:
# global error: we do not specify which field is in error, for security
form.add_global_errors([_('Access denied: this content does match the form.')])
bad_content = True
break
if not form.is_submitted() or form.has_errors() or bad_content:
html_top()
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Access rights verification')
r += htmltext('<p>%s</p>') % ngettext(
'In order to be able to access the form, indicate the content of the following field.',
'In order to be able to access the form, indicate the content of the following fields.',
len(verify_fields),
)
r += form.render()
return r.getvalue()
get_session().mark_anonymous_formdata(formdata)
return redirect(formdata.get_url())