From 928d93377d37504a21dc2761e765e261891e91db Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 3 Nov 2021 19:18:51 +0100 Subject: [PATCH 5/5] forms: abort autosave after 200 ms (#58276) --- tests/form_pages/test_all.py | 24 ++++++++++++++++++++++++ wcs/forms/root.py | 12 ++++++++++++ wcs/qommon/misc.py | 26 ++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py index f0dfddb3..fe023d65 100644 --- a/tests/form_pages/test_all.py +++ b/tests/form_pages/test_all.py @@ -4276,6 +4276,30 @@ def test_form_autosave(pub): assert formdef.data_class().select()[0].data['1'] == 'foobar3' +def test_form_autosave_timeout(pub, monkeypatch): + from wcs.forms.root import FormPage + + monkeypatch.setattr(FormPage, 'AUTOSAVE_TIMEOUT', 0.0001) + + formdef = create_formdef() + formdef.fields = [ + fields.PageField(id='0', label='1st page', type='page'), + fields.StringField(id='1', label='string'), + fields.PageField(id='2', label='2nd page', type='page'), + fields.StringField(id='3', label='string 2'), + ] + formdef.enable_tracking_codes = True + formdef.store() + + formdef.data_class().wipe() + app = get_app(pub) + resp = app.get('/test/') + resp.form['f1'] = 'foobar' + + resp = app.post('/test/autosave', params=resp.form.submit_fields()) + assert resp.json == {'reason': 'autosave took more than 0.0001 seconds', 'result': 'error'} + + def test_form_autosave_with_items_field(pub): formdef = create_formdef() formdef.data_class().wipe() diff --git a/wcs/forms/root.py b/wcs/forms/root.py index 28449a96..98405e41 100644 --- a/wcs/forms/root.py +++ b/wcs/forms/root.py @@ -1297,7 +1297,19 @@ class FormPage(Directory, FormTemplateMixin): formdata.user = get_request().user formdata.store() + AUTOSAVE_TIMEOUT = 0.2 + def autosave(self): + try: + with misc.timeout(self.AUTOSAVE_TIMEOUT): + return self._autosave() + except misc.TimeoutException: + get_request().ignore_session = True + return json.dumps( + {'result': 'error', 'reason': 'autosave took more than %s seconds' % self.AUTOSAVE_TIMEOUT} + ) + + def _autosave(self): get_response().set_content_type('application/json') def result_error(reason): diff --git a/wcs/qommon/misc.py b/wcs/qommon/misc.py index 29c5351a..c3a1f728 100644 --- a/wcs/qommon/misc.py +++ b/wcs/qommon/misc.py @@ -16,6 +16,7 @@ import base64 import calendar +import contextlib import datetime import decimal import hashlib @@ -24,6 +25,7 @@ import io import json import os import re +import signal import subprocess import time import unicodedata @@ -1089,3 +1091,27 @@ def get_type_name(value): } object_type_name = object_type_names.get(value.__class__, value.__class__.__name__) return object_type_name + + +class TimeoutException(Exception): + pass + + +def _alarm_handler_for_timeout(signum, frame): + raise TimeoutException + + +# this context manager cannot be nested, only one timeout can be set at a time, +# enclosing timer will be reset +@contextlib.contextmanager +def timeout(duration): + old_value = signal.getsignal(signal.SIGALRM) + try: + signal.signal(signal.SIGALRM, _alarm_handler_for_timeout) + try: + signal.setitimer(signal.ITIMER_REAL, duration) + yield + finally: + signal.setitimer(signal.ITIMER_REAL, 0) + finally: + signal.signal(signal.SIGALRM, old_value) -- 2.33.0