From c06444abd4d8b92b04ea1ac675d2f5708bfba895 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Tue, 26 Oct 2021 22:20:45 +0200 Subject: [PATCH] forms: prevent autosave from overwriting session's data (#58208) autosave() needs to write into session's data only when establishing the draft formdata, after that it's not needed anymore. Preventing it to further modify magictoken's data prevent race condition between AJAX call to autosave and normal form's submission by users. --- tests/form_pages/test_all.py | 47 ++++++++++++++++++++++++++++++++++++ wcs/forms/root.py | 11 ++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py index db6604c7..20825796 100644 --- a/tests/form_pages/test_all.py +++ b/tests/form_pages/test_all.py @@ -8058,3 +8058,50 @@ def test_autosave_and_datasource_failure(mocker, pub, settings): data = formdef.data_class().select()[0].data assert '1' not in data or (data['1'] is not None and data['1_display'] is not None) + + +def test_autosave_never_overwrite(mocker, pub, settings): + create_user(pub) + + formdef = create_formdef() + formdef.data_class().wipe() + + formdef.fields = [ + fields.PageField(id='0', label='1st page', type='page'), + fields.StringField(id='1', label='string1'), + fields.PageField(id='2', label='2nd page', type='page'), + fields.StringField(id='3', label='string2'), + ] + formdef.store() + + app = get_app(pub) + login(app, username='foo', password='foo') + + resp = app.get('/test/') + resp.form.set('f1', '1') + # go to the second page + resp = resp.form.submit('submit') + resp.form.set('f3', '1') + # autosave wrong data + autosave_data = dict(resp.form.submit_fields()) + autosave_data['f3'] = 'wtf!' + resp_autosave = app.post('/test/autosave', params=autosave_data) + assert resp_autosave.json == {'result': 'success'} + # check the draft is fucked + data = formdef.data_class().select()[0].data + assert data['3'] == 'wtf!' + # now finish submitting + resp = resp.form.submit('submit') # -> validation page + # autosave wrong data + # _ajax_form_token is just a form_token, so take the current one to + # simulate a rogue autosave from the second page + autosave_data['_ajax_form_token'] = resp.form['_form_id'].value + resp_autosave = app.post('/test/autosave', params=autosave_data) + assert resp_autosave.json == {'result': 'success'} + data = formdef.data_class().select()[0].data + assert data['3'] == 'wtf!' + # validate + resp = resp.form.submit('submit') # -> submit + + # great everything is still fine in the end + assert formdef.data_class().select()[0].data == {'1': '1', '3': '1'} diff --git a/wcs/forms/root.py b/wcs/forms/root.py index 28449a96..5dec93fc 100644 --- a/wcs/forms/root.py +++ b/wcs/forms/root.py @@ -1327,7 +1327,10 @@ class FormPage(Directory, FormTemplateMixin): self.feed_current_data(magictoken) - form_data = session.get_by_magictoken(magictoken, {}) + current_form_data = session.get_by_magictoken(magictoken, {}) + # prevent autosave to write into session concurrently with user's + # submits, only do it when initializing the draft formdata. + form_data = current_form_data.copy() if not form_data: return result_error('missing data') @@ -1356,6 +1359,12 @@ class FormPage(Directory, FormTemplateMixin): except SubmittedDraftException: return result_error('form has already been submitted') + # save draft_formdata_id if it changed, otherwise prevent the session + # and current filling datas to be overwritten + if current_form_data.get('draft_formdata_id') == form_data.get('draft_formdata_id'): + get_request().ignore_session = True + else: + current_form_data['draft_formdata_id'] = form_data.get('draft_formdata_id') return json.dumps({'result': 'success'}) def save_draft(self, data, page_no=None): -- 2.33.0