From 07a2a7213a6a194ab39ebbe981596f9e48044f8a Mon Sep 17 00:00:00 2001
From: Thomas NOEL <tnoel@entrouvert.com>
Date: Thu, 6 Feb 2020 15:03:10 +0100
Subject: [PATCH] upload_storage: add frontoffice/backoffice link presence
 system (#39517)

---
 tests/test_upload_storage.py | 73 +++++++++++++++++++++++++++++++++---
 wcs/fields.py                | 11 +++++-
 wcs/forms/common.py          | 26 +++++++------
 wcs/qommon/upload_storage.py | 33 +++++++++++++++-
 wcs/workflows.py             | 11 ++++++
 5 files changed, 134 insertions(+), 20 deletions(-)

diff --git a/tests/test_upload_storage.py b/tests/test_upload_storage.py
index 24b98cfd..61d00dd5 100644
--- a/tests/test_upload_storage.py
+++ b/tests/test_upload_storage.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 
 import os
+import json
 import mock
 import pytest
 
@@ -31,9 +32,15 @@ def pub(request, emails):
     pub.write_cfg()
     open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w').write('''
 [storage-remote]
-label = test out storage
+label = remote storage
 class = wcs.qommon.upload_storage.RemoteOpaqueUploadStorage
-ws = https://crypto.example.net/ws/
+ws = https://crypto.example.net/ws1/
+
+[storage-remote-bo]
+label = remote storage backoffice only
+class = wcs.qommon.upload_storage.RemoteOpaqueUploadStorage
+ws = https://crypto.example.net/ws2/
+frontoffice_redirect = false
 
 [api-secrets]
 crypto.example.net = 1234
@@ -92,17 +99,21 @@ def test_form_file_field_upload_storage(wscall, pub):
     assert formdef.fields[0].storage == formdef.fields[1].storage == 'default'
 
     assert 'remote' in pub.get_site_storages()
+
     formdef.fields[1].storage = 'remote'
     formdef.store()
     assert formdef.fields[0].storage == 'default'
     assert formdef.fields[1].storage == 'remote'
 
-    wscall.return_value = None, 200, '{"err": 0, "data": {"redirect_url": "https://crypto.example.net/"}}'
+    wscall.return_value = None, 200, json.dumps(
+            {"err": 0, "data": {"redirect_url": "https://crypto.example.net/"}})
 
-    image_content = open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb').read()
+    image_content = open(os.path.join(os.path.dirname(__file__),
+        'image-with-gps-data.jpeg'), 'rb').read()
 
     upload_0 = Upload('file.jpg', image_content, 'image/jpeg')
     upload_1 = Upload('remote.jpg', image_content, 'image/jpeg')
+
     resp = get_app(pub).get('/test/')
     resp.forms[0]['f0$file'] = upload_0
     resp.forms[0]['f1$file'] = upload_1
@@ -113,13 +124,65 @@ def test_form_file_field_upload_storage(wscall, pub):
     resp = resp.follow()
     assert 'The form has been recorded' in resp.text
 
-    assert resp.text.count('thumbnail=1') == 1  # thumbnail only for first file
+    assert 'download?f=0&thumbnail=1' in resp.text
+    assert 'download?f=1&thumbnail=1' not in resp.text  # no thumbnail for remote storage
+    assert 'href="download?f=0"' in resp.text
+    assert 'href="download?f=1"' in resp.text
 
     resp = resp.click('remote.jpg')
     assert resp.location.startswith('https://crypto.example.net/')
     assert '&signature=' in resp.location
 
+    # no links, via webservice
+    wscall.return_value = None, 200, json.dumps(
+            {"err": 0, "data": {
+                "redirect_url": "https://crypto.example.net/",
+                "backoffice_redirect_url": None,
+                "frontoffice_redirect_url": None,
+                }})
+    resp = get_app(pub).get('/test/')
+    resp.forms[0]['f0$file'] = upload_0
+    resp.forms[0]['f1$file'] = upload_1
+    resp = resp.forms[0].submit('submit')
+    resp = resp.forms[0].submit('submit')
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+    assert 'download?f=0&thumbnail=1' in resp.text
+    assert 'download?f=1&thumbnail=1' not in resp.text
+    assert 'href="download?f=0"' in resp.text
+    assert 'href="download?f=1"' not in resp.text  # no link on frontoffice
     admin_app = login(get_app(pub), username='admin', password='admin')
+    resp = admin_app.get('/backoffice/management/test/2/')
+    assert 'download?f=0&thumbnail=1' in resp.text
+    assert 'download?f=1&thumbnail=1' not in resp.text
+    assert 'href="download?f=0"' in resp.text
+    assert 'href="download?f=1"' not in resp.text  # no link on backoffice
+    admin_app.get('/backoffice/management/test/2/download?f=1', status=404)  # cannot access
+
+    # link only on backoffice, via site-options
+    formdef.fields[1].storage = 'remote-bo'
+    formdef.store()
+    wscall.return_value = None, 200, json.dumps(
+            {"err": 0, "data": {"redirect_url": "https://crypto.example.net/"}})
+    resp = get_app(pub).get('/test/')
+    resp.forms[0]['f0$file'] = upload_0
+    resp.forms[0]['f1$file'] = upload_1
+    resp = resp.forms[0].submit('submit')
+    resp = resp.forms[0].submit('submit')
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+    assert 'download?f=0&thumbnail=1' in resp.text
+    assert 'download?f=1&thumbnail=1' not in resp.text  # no thumbnail for remote storage
+    assert 'href="download?f=0"' in resp.text
+    assert 'href="download?f=1"' not in resp.text  # no link on frontoffice
+    # go to backoffice
+    resp = admin_app.get('/backoffice/management/test/3/')
+    assert 'download?f=0&thumbnail=1' in resp.text
+    assert 'download?f=1&thumbnail=1' not in resp.text  # no thumbnail for remote storage
+    assert 'href="download?f=0"' in resp.text
+    assert 'href="download?f=1"' in resp.text  # link is present on backoffice
+
+    # api access (json export)
     resp = admin_app.get('/api/forms/test/1/', status=200)
     assert resp.json['fields']['file']['content'].startswith('/9j/4AAQSkZJRg')
     assert resp.json['fields']['remote_file']['content'] == ''
diff --git a/wcs/fields.py b/wcs/fields.py
index ebaf5ea4..aea70893 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -1038,13 +1038,20 @@ class FileField(WidgetField):
         return self.get_view_value(value, include_image_thumbnail=False)
 
     def get_view_value(self, value, include_image_thumbnail=True):
+        show_link = True
+        if value.has_redirect_url():
+            is_in_backoffice = bool(get_request() and get_request().is_in_backoffice())
+            show_link = bool(value.get_redirect_url(backoffice=is_in_backoffice))
         t = TemplateIO(html=True)
         t += htmltext('<div class="file-field">')
-        t += htmltext('<a download="%s" href="[download]?f=%s">') % (value.base_filename, self.id)
+        if show_link:
+            t += htmltext('<a download="%s" href="[download]?f=%s">') % (value.base_filename, self.id)
         if include_image_thumbnail and value.can_thumbnail():
             t += htmltext('<img alt="" src="[download]?f=%s&thumbnail=1"/>') % self.id
         t += htmltext('<span>%s</span>') % value
-        t += htmltext('</a></div>')
+        if show_link:
+            t += htmltext('</a>')
+        t += htmltext('</div>')
         return t.getvalue()
 
     def get_csv_value(self, value, **kwargs):
diff --git a/wcs/forms/common.py b/wcs/forms/common.py
index 4b9214e2..80f5ddca 100644
--- a/wcs/forms/common.py
+++ b/wcs/forms/common.py
@@ -67,6 +67,13 @@ class FileDirectory(Directory):
         if component and component != file.base_filename:
             raise errors.TraversalError()
 
+        if file.has_redirect_url():
+            redirect_url = file.get_redirect_url(backoffice=get_request().is_in_backoffice())
+            if not redirect_url:
+                raise errors.TraversalError()
+            redirect_url = sign_url_auto_orig(redirect_url)
+            return redirect(redirect_url)
+
         response = get_response()
 
         if self.thumbnails:
@@ -81,12 +88,6 @@ class FileDirectory(Directory):
             else:
                 raise errors.TraversalError()
 
-        if hasattr(file, 'storage_attrs'):  # remote storage
-            if not file.storage_attrs.get('redirect_url'):
-                raise errors.TraversalError()
-            redirect_url = sign_url_auto_orig(file.storage_attrs['redirect_url'])
-            return redirect(redirect_url)
-
         if file.content_type:
             response.set_content_type(file.content_type)
         else:
@@ -683,6 +684,13 @@ class FormStatusPage(Directory, FormTemplateMixin):
         if not hasattr(file, 'content_type'):
             raise errors.TraversalError()
 
+        if file.has_redirect_url():
+            redirect_url = file.get_redirect_url(backoffice=get_request().is_in_backoffice())
+            if not redirect_url:
+                raise errors.TraversalError()
+            redirect_url = sign_url_auto_orig(redirect_url)
+            return redirect(redirect_url)
+
         file_url = 'files/%s/' % fn
 
         if get_request().form.get('thumbnail') == '1':
@@ -691,12 +699,6 @@ class FormStatusPage(Directory, FormTemplateMixin):
             else:
                 raise errors.TraversalError()
 
-        if hasattr(file, 'storage_attrs'):  # remote storage
-            if not file.storage_attrs.get('redirect_url'):
-                raise errors.TraversalError()
-            redirect_url = sign_url_auto_orig(file.storage_attrs['redirect_url'])
-            return redirect(redirect_url)
-
         if getattr(file, 'base_filename'):
             file_url += file.base_filename
         return redirect(file_url)
diff --git a/wcs/qommon/upload_storage.py b/wcs/qommon/upload_storage.py
index b0a268a2..54236c82 100644
--- a/wcs/qommon/upload_storage.py
+++ b/wcs/qommon/upload_storage.py
@@ -80,6 +80,13 @@ class PicklableUpload(Upload):
     def can_thumbnail(self):
         return get_storage_object(getattr(self, 'storage', None)).can_thumbnail(self)
 
+    def has_redirect_url(self):
+        return get_storage_object(getattr(self, 'storage', None)).has_redirect_url(self)
+
+    def get_redirect_url(self, backoffice=False):
+        return get_storage_object(getattr(self, 'storage', None)).get_redirect_url(self,
+                backoffice=backoffice)
+
 
 class UploadStorageError(Exception):
     pass
@@ -125,10 +132,18 @@ class UploadStorage(object):
     def can_thumbnail(self, upload):
         return can_thumbnail(upload.content_type)
 
+    def has_redirect_url(self, upload):
+        return False
+
+    def get_redirect_url(self, upload, backoffice=False):
+        raise NotImplementedError('no get_redirect_url on UploadStorage object')
+
 
 class RemoteOpaqueUploadStorage(object):
-    def __init__(self, ws, **kwargs):
+    def __init__(self, ws, frontoffice_redirect='true', backoffice_redirect='true', **kwargs):
         self.ws = ws
+        self.frontoffice_redirect = bool(frontoffice_redirect == 'true')
+        self.backoffice_redirect = bool(backoffice_redirect == 'true')
 
     def save_tempfile(self, upload):
         if getattr(upload, 'storage_attrs', None):
@@ -190,6 +205,22 @@ class RemoteOpaqueUploadStorage(object):
     def can_thumbnail(self, upload):
         return False
 
+    def has_redirect_url(self, upload):
+        return True
+
+    def get_redirect_url(self, upload, backoffice=False):
+        if backoffice:
+            if not self.backoffice_redirect:
+                return None
+            if 'backoffice_redirect_url' in upload.storage_attrs:
+                return upload.storage_attrs['backoffice_redirect_url']
+        else:
+            if not self.frontoffice_redirect:
+                return None
+            if 'frontoffice_redirect_url' in upload.storage_attrs:
+                return upload.storage_attrs['frontoffice_redirect_url']
+        return upload.storage_attrs.get('redirect_url')
+
 
 def get_storage_object(storage):
     if not storage or storage == 'default':
diff --git a/wcs/workflows.py b/wcs/workflows.py
index f39a2e0c..c3c23f81 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -223,6 +223,17 @@ class AttachmentEvolutionPart: #pylint: disable=C1001
         return {'attachments': AttachmentsSubstitutionProxy(formdata),
                 'form_attachments': AttachmentsSubstitutionProxy(formdata)}
 
+    # mimic PicklableUpload methods:
+
+    def can_thumbnail(self):
+        return True
+
+    def has_redirect_url(self):
+        return False
+
+    def get_redirect_url(self, upload, backoffice=False):
+        raise NotImplementedError('no get_redirect_url on AttachmentEvolutionPart object')
+
 
 class DuplicateGlobalActionNameError(Exception):
     pass
-- 
2.24.1

