From 0408db8b4ff6f99c592ff5694b44f2c2f30314e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Wed, 12 Oct 2022 21:06:16 +0200
Subject: [PATCH] backoffice: fix access to uploaded file in file widget
 (#70194)

---
 tests/backoffice_pages/test_all.py | 53 ++++++++++++++++++++++++++++++
 wcs/backoffice/management.py       |  7 ++--
 wcs/forms/common.py                | 45 +++++++++++++++++++++++++
 wcs/forms/root.py                  | 49 +++------------------------
 4 files changed, 106 insertions(+), 48 deletions(-)

diff --git a/tests/backoffice_pages/test_all.py b/tests/backoffice_pages/test_all.py
index d9bc181d6..e07b263ea 100644
--- a/tests/backoffice_pages/test_all.py
+++ b/tests/backoffice_pages/test_all.py
@@ -8,6 +8,7 @@ import zipfile
 
 import pytest
 import responses
+from webtest import Upload
 
 import wcs.qommon.storage as st
 from wcs import fields
@@ -4877,3 +4878,55 @@ def test_backoffice_dispatch_single_user(pub, user_template):
         assert formdata.workflow_roles == {'_foobar': [user.roles[0]]}
 
     assert 'button_a_button' in resp.text
+
+
+def test_backoffice_workflow_form_file_access(pub):
+    FormDef.wipe()
+    Workflow.wipe()
+
+    role = pub.role_class(name='xxx1')
+    role.store()
+
+    user = create_superuser(pub)
+    user.roles.append(role.id)
+    user.store()
+
+    wf = Workflow(name='test')
+    status = wf.add_status('New', 'st1')
+    next_status = wf.add_status('Next', 'st2')
+
+    status.items = []
+    display_form = status.add_action('form', id='_display_form')
+    display_form.by = ['_receiver']
+    display_form.varname = 'blah'
+    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
+    display_form.formdef.fields = [
+        fields.FileField(id='1', label='test', type='file', varname='file'),
+        fields.StringField(id='2', label='test2', type='string', required=True),
+    ]
+
+    jump = status.add_action('jumponsubmit', id='_jump')
+    jump.status = next_status.id
+
+    wf.store()
+
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.workflow_roles = {'_receiver': role.id}
+    formdef.fields = []
+    formdef.store()
+
+    formdef.data_class().wipe()
+
+    formdata = formdef.data_class()()
+    formdata.just_created()
+    formdata.store()
+
+    app = login(get_app(pub))
+    resp = app.get(formdata.get_url(backoffice=True))
+    resp.form['fblah_1$file'] = Upload('test3.txt', b'foobar3', 'text/plain')
+    resp = resp.form.submit('submit')
+    # it will fail on the equired string field; this allows testing
+    # the temporary file URL.
+    assert resp.click('test3.txt').body == b'foobar3'
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 9ff70a9f5..a09c4005b 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -41,7 +41,7 @@ from wcs.conditions import Condition
 from wcs.formdata import FormData
 from wcs.formdef import FormDef
 from wcs.forms.backoffice import FormDefUI
-from wcs.forms.common import FormStatusPage
+from wcs.forms.common import FormPageMixin, FormStatusPage
 from wcs.roles import logged_users_role
 from wcs.variables import LazyFieldVar, LazyList
 from wcs.workflows import ActionsTracingEvolutionPart, WorkflowStatusItem, item_classes, template_on_formdata
@@ -752,7 +752,7 @@ class ManagementDirectory(Directory):
         return FormPage(component)
 
 
-class FormPage(Directory):
+class FormPage(Directory, FormPageMixin):
     do_not_call_in_templates = True
     _q_exports = [
         '',
@@ -2926,7 +2926,7 @@ class FormPage(Directory):
         except KeyError:
             raise errors.TraversalError()
 
-        return FormBackOfficeStatusPage(self.formdef, filled)
+        return FormBackOfficeStatusPage(self.formdef, filled, parent_view=self)
 
     def live(self):
         return FormBackofficeEditPage(self.formdef.url_name).live()
@@ -2951,6 +2951,7 @@ class FormBackOfficeStatusPage(FormStatusPage):
         'action',
         'live',
         'inspect',
+        'tempfile',
         ('inspect-tool', 'inspect_tool'),
         ('download-as-zip', 'download_as_zip'),
         ('lateral-block', 'lateral_block'),
diff --git a/wcs/forms/common.py b/wcs/forms/common.py
index ede5e8f10..1612a52e9 100644
--- a/wcs/forms/common.py
+++ b/wcs/forms/common.py
@@ -941,3 +941,48 @@ class FormStatusPage(Directory, FormTemplateMixin):
             return f._q_index()
 
         raise errors.AccessForbiddenError()
+
+
+class FormPageMixin:
+    user = None
+
+    def tempfile(self):
+        get_request().ignore_session = True
+        self.check_access()
+        if self.user and not self.user.id == get_session().user:
+            self.check_receiver()
+        try:
+            t = get_request().form['t']
+            tempfile = get_session().get_tempfile(t)
+        except KeyError:
+            raise errors.TraversalError()
+        if tempfile is None:
+            raise errors.TraversalError()
+        response = get_response()
+
+        # force potential HTML upload to be used as-is (not decorated with theme)
+        # and with minimal permissions
+        response.filter = {}
+        response.set_header(
+            'Content-Security-Policy',
+            'default-src \'none\'; img-src %s;' % get_request().build_absolute_uri(),
+        )
+
+        if tempfile['content_type']:
+            response.set_content_type(tempfile['content_type'])
+        else:
+            response.set_content_type('application/octet-stream')
+        if tempfile['charset']:
+            response.set_charset(tempfile['charset'])
+
+        if get_request().form.get('thumbnail') == '1':
+            try:
+                thumbnail = misc.get_thumbnail(
+                    get_session().get_tempfile_path(t), content_type=tempfile['content_type']
+                )
+            except misc.ThumbnailError:
+                pass
+            else:
+                response.set_content_type('image/png')
+                return thumbnail
+        return get_session().get_tempfile_content(t).get_file_pointer().read()
diff --git a/wcs/forms/root.py b/wcs/forms/root.py
index 2af5b383b..8d78095b6 100644
--- a/wcs/forms/root.py
+++ b/wcs/forms/root.py
@@ -37,7 +37,7 @@ from wcs.categories import Category
 from wcs.fields import MissingBlockFieldError, SetValueError
 from wcs.formdata import Evolution, FormData
 from wcs.formdef import FormDef
-from wcs.forms.common import FormStatusPage, FormTemplateMixin
+from wcs.forms.common import FormPageMixin, FormStatusPage, FormTemplateMixin
 from wcs.qommon.admin.texts import TextsDirectory
 from wcs.qommon.form import get_selection_error_text
 from wcs.qommon.storage import Equal, NothingToUpdate
@@ -256,7 +256,7 @@ class TrackingCodesDirectory(Directory):
         return TrackingCodeDirectory(component, self.formdef)
 
 
-class FormPage(Directory, FormTemplateMixin):
+class FormPage(Directory, FormPageMixin, FormTemplateMixin):
     _q_exports = [
         '',
         'tempfile',
@@ -312,7 +312,7 @@ class FormPage(Directory, FormTemplateMixin):
     def go_to_backoffice(self):
         return redirect(self.formdef.get_admin_url())
 
-    def check_role(self):
+    def check_access(self):
         if self.formdef.roles:
             if not self.user:
                 raise errors.AccessUnauthorizedError()
@@ -941,7 +941,7 @@ class FormPage(Directory, FormTemplateMixin):
         self._pages = None
 
     def _q_index(self):
-        self.check_role()
+        self.check_access()
         authentication_context_check_result = self.check_authentication_context()
         if authentication_context_check_result:
             return authentication_context_check_result
@@ -1676,47 +1676,6 @@ class FormPage(Directory, FormTemplateMixin):
                 break
         return redirect(url or '.')
 
-    def tempfile(self):
-        get_request().ignore_session = True
-        self.check_role()
-        if self.user and not self.user.id == get_session().user:
-            self.check_receiver()
-        try:
-            t = get_request().form['t']
-            tempfile = get_session().get_tempfile(t)
-        except KeyError:
-            raise errors.TraversalError()
-        if tempfile is None:
-            raise errors.TraversalError()
-        response = get_response()
-
-        # force potential HTML upload to be used as-is (not decorated with theme)
-        # and with minimal permissions
-        response.filter = {}
-        response.set_header(
-            'Content-Security-Policy',
-            'default-src \'none\'; img-src %s;' % get_request().build_absolute_uri(),
-        )
-
-        if tempfile['content_type']:
-            response.set_content_type(tempfile['content_type'])
-        else:
-            response.set_content_type('application/octet-stream')
-        if tempfile['charset']:
-            response.set_charset(tempfile['charset'])
-
-        if get_request().form.get('thumbnail') == '1':
-            try:
-                thumbnail = misc.get_thumbnail(
-                    get_session().get_tempfile_path(t), content_type=tempfile['content_type']
-                )
-            except misc.ThumbnailError:
-                pass
-            else:
-                response.set_content_type('image/png')
-                return thumbnail
-        return get_session().get_tempfile_content(t).get_file_pointer().read()
-
     def validating(self, data, page_error_messages=None):
         self.on_validation_page = True
         get_request().view_name = 'validation'
-- 
2.37.2

