From 6df7d395e190e0138439b5c60ab8c3fee5a2a176 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 20 Mar 2020 11:08:35 +0100
Subject: [PATCH] misc: remove the use of to_json decorator for modern
 endpoints (#25690)

---
 passerelle/views.py  | 105 ++++++++++++++++++++++++++++++++++++++++---
 tests/test_mdph13.py |   5 ---
 2 files changed, 100 insertions(+), 10 deletions(-)

diff --git a/passerelle/views.py b/passerelle/views.py
index 0fbdf379..8ed1105b 100644
--- a/passerelle/views.py
+++ b/passerelle/views.py
@@ -18,17 +18,19 @@ import datetime
 import hashlib
 import inspect
 import json
+import re
 
 from django.apps import apps
 from django.conf.urls import url
 from django.core.cache import cache
-from django.core.exceptions import PermissionDenied
+from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
 from django.contrib.auth import logout as auth_logout
 from django.contrib.auth import views as auth_views
 from django.db import transaction
 from django.db.models import TextField
 from django.db.models.functions import Cast
-from django.http import HttpResponse, HttpResponseRedirect, Http404
+from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponseBadRequest
+from django.http.response import HttpResponseBase
 from django.views.decorators.csrf import csrf_exempt
 from django.views.generic import (
     RedirectView, View, TemplateView, CreateView, DeleteView, UpdateView,
@@ -46,13 +48,18 @@ from django.utils.six.moves.urllib.parse import quote
 
 from dateutil import parser as date_parser
 from jsonschema import validate, ValidationError
+from requests import RequestException
 
 from passerelle.base.models import BaseResource, ResourceLog
 from passerelle.compat import json_loads
-from passerelle.utils.jsonresponse import APIError
+from passerelle.utils.conversion import exception_to_text
 from passerelle.utils.json import unflatten
+from passerelle.utils.jsonresponse import APIError
+from passerelle.utils.jsonresponse import CALLBACK_NAME
+from passerelle.utils.jsonresponse import DEFAULT_DEBUG
+from passerelle.utils.jsonresponse import JSONEncoder
 
-from .utils import to_json, is_authorized
+from .utils import is_authorized
 from .forms import GenericConnectorForm
 
 if 'mellon' in settings.INSTALLED_APPS:
@@ -75,6 +82,7 @@ class LoginView(auth_views.LoginView):
                                         + quote(request.GET.get('next')))
         return super(LoginView, self).dispatch(request, *args, **kwargs)
 
+
 login = LoginView.as_view()
 
 
@@ -305,6 +313,7 @@ class InvalidParameterValue(Exception):
     def __str__(self):
         return 'invalid value for parameter "%s"' % self.parameter_name
 
+
 IGNORED_PARAMS = ('apikey', 'signature', 'nonce', 'algo', 'timestamp', 'orig', 'jsonpCallback',
                   'callback', '_', 'raise', 'debug', 'decode', 'format')
 
@@ -469,6 +478,57 @@ class GenericEndpointView(GenericConnectorMixin, SingleObjectMixin, View):
             cache.set(cache_key, result, self.endpoint.endpoint_info.cache_duration)
         return result
 
+    def _obj_to_response(self, req, obj):
+        if isinstance(obj, dict) and 'err' not in obj:
+            obj['err'] = 0
+        return obj
+
+    def _err_to_response(self, err):
+        if hasattr(err, "__module__"):
+            err_module = err.__module__ + "."
+        else:
+            err_module = ""
+
+        if hasattr(err, "owner"):
+            err_module += err.owner.__name__ + "."
+
+        err_class = err_module + err.__class__.__name__
+
+        err_desc = force_text(err)
+
+        return {
+            "err": 1,
+            "err_class": err_class,
+            "err_desc": err_desc,
+            "data": getattr(err, 'data', None),
+        }
+
+    def _render_data(self, req, data, status=200):
+        debug = DEFAULT_DEBUG
+        debug = debug or req.GET.get('debug', 'false').lower() in ('true', 't', '1', 'on')
+        debug = debug or req.GET.get('decode', '0').lower() in ('true', 't', '1', 'on')
+        if 'callback' in req.GET or 'jsonpCallback' in req.GET:
+            output_format = req.GET.get('format', 'jsonp')
+        else:
+            output_format = req.GET.get('format', 'json')
+        jsonp_cb = req.GET.get('callback') or req.GET.get('jsonpCallback') or CALLBACK_NAME
+        if not re.match(r'^[$a-zA-Z_][a-zA-Z0-9_]*$', jsonp_cb):
+            return HttpResponseBadRequest('invalid JSONP callback name')
+        content_type = "application/json"
+
+        kwargs = {'cls': JSONEncoder}
+        if debug:
+            kwargs["indent"] = 4
+            kwargs["ensure_ascii"] = False
+            kwargs["encoding"] = "utf8"
+
+        plain = json.dumps(data, **kwargs)
+        if output_format == 'jsonp':
+            plain = "%s(%s);" % (jsonp_cb, plain)
+            content_type = "application/javascript"
+
+        return HttpResponse(plain, content_type="%s; charset=UTF-8" % content_type, status=status)
+
     def get(self, request, *args, **kwargs):
         if self.endpoint.endpoint_info.pattern:
             pattern = url(self.endpoint.endpoint_info.pattern, self.endpoint)
@@ -478,8 +538,43 @@ class GenericEndpointView(GenericConnectorMixin, SingleObjectMixin, View):
             kwargs['other_params'] = match.kwargs
         elif kwargs.get('rest'):
             raise Http404()
+
         connector = self.get_object()
-        return to_json(logger=connector.logger)(self.perform)(request, *args, **kwargs)
+
+        try:
+            resp = self.perform(request, *args, **kwargs)
+            if isinstance(resp, HttpResponseBase):
+                return resp
+
+            data = self._obj_to_response(request, resp)
+            status = 200
+        except Exception as e:
+            extras = {'method': request.method, 'exception': exception_to_text(e), 'request': request}
+            if request.method == 'POST':
+                max_size = connector.logging_parameters.requests_max_size
+                extras.update({'body': repr(request.body[:max_size])})
+            if (not isinstance(e, (Http404, PermissionDenied, ObjectDoesNotExist, RequestException))
+                    and getattr(e, 'log_error', True)):
+                connector.logger.exception("Error occurred while processing request", extra=extras)
+            elif isinstance(e, APIError):
+                connector.logger.warning("Error occurred while processing request", extra=extras)
+
+            if int(request.GET.get('raise', 0)):
+                raise
+
+            data = self._err_to_response(e)
+            if getattr(e, 'err_code', None):
+                data['err'] = e.err_code
+            if getattr(e, 'http_status', None):
+                status = e.http_status
+            elif isinstance(e, (ObjectDoesNotExist, Http404)):
+                status = 404
+            elif isinstance(e, PermissionDenied):
+                status = 403
+            else:
+                status = 500
+
+        return self._render_data(request, data, status)
 
     def post(self, request, *args, **kwargs):
         return self.get(request, *args, **kwargs)
diff --git a/tests/test_mdph13.py b/tests/test_mdph13.py
index 1fa89a2c..c916d53e 100644
--- a/tests/test_mdph13.py
+++ b/tests/test_mdph13.py
@@ -18,7 +18,6 @@
 import json
 import base64
 import datetime
-import logging
 
 import requests
 import requests.exceptions
@@ -498,15 +497,11 @@ def test_dossier_http_error(app, mdph13, mock_http, caplog):
     mock_http.add_response({'status_code': 401, 'content': 'wtf', 'reason': 'Authentication required'})
     response = app.post(url, status=500)
     assert response.json['err_class'] == 'requests.exceptions.HTTPError'
-    assert caplog.records[-1].levelno == logging.ERROR
     assert caplog.records[-1].getMessage() == 'GET http://cd13.fr/situation/dossier/1234 (=> 401)'
-    assert hasattr(caplog.records[-1].request, 'META')
 
     def raise_ssl_error(url, request):
         raise requests.exceptions.SSLError(request=request)
     mock_http.add_response(raise_ssl_error)
     response = app.post(url, status=500)
     assert response.json['err_class'] == 'requests.exceptions.SSLError'
-    assert caplog.records[-1].levelno == logging.ERROR
     assert caplog.records[-1].getMessage() == 'GET http://cd13.fr/situation/dossier/1234 (=> SSLError())'
-    assert hasattr(caplog.records[-1].request, 'META')
-- 
2.20.1

