Django 1.11 compatibility fixes.
[django-ssify.git] / ssify / middleware_debug.py
1 # -*- coding: utf-8 -*-
2 # This file is part of django-ssify, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See README.md for more information.
4 #
5 """
6 This module should only be used for debugging SSI statements.
7
8 Using SsiRenderMiddleware in production defeats the purpose of using SSI
9 in the first place, and is unsafe. You should use a proper webserver with SSI
10 support as a proxy (i.e. Nginx with ssi=on).
11
12 """
13 from __future__ import unicode_literals
14 import re
15
16 try:
17     from urllib.parse import urlparse
18 except ImportError:
19     from urlparse import urlparse
20
21 try:
22     from django.urls import NoReverseMatch, reverse, resolve
23 except ImportError:
24     # Django < 2
25     from django.core.urlresolvers import NoReverseMatch, reverse, resolve
26
27 from django.core.urlresolvers import resolve
28 from .cache import get_caches
29
30 from .conf import conf
31
32
33 SSI_SET = re.compile(r"<!--#set var='(?P<var>[^']+)' "
34                      r"value='(?P<value>|\\\\|.*?[^\\](?:\\\\)*)'-->", re.S)
35 SSI_ECHO = re.compile(r"<!--#echo var='(?P<var>[^']+)' encoding='none'-->")
36 SSI_INCLUDE = re.compile(r"<!--#include (?:virtual|file)='(?P<path>[^']+)'-->")
37 SSI_IF = re.compile(r"(?P<header><!--#if expr='(?P<expr>[^']*)'-->)"
38                     r"(?P<value>.*?)(?:<!--#else-->(?P<else>.*?))?"
39                     r"<!--#endif-->", re.S)
40 SSI_VAR = re.compile(r"\$\{(?P<var>.+)\}")
41
42 UNESCAPE = re.compile(r'\\(.)')
43
44
45 class SsiRenderMiddleware(object):
46     """
47     Emulates a webserver with SSI support.
48
49     This middleware should only be used for debugging purposes.
50     SsiMiddleware will enable it automatically, if SSIFY_RENDER setting
51     is set to True, so you don't normally need to include it in
52     MIDDLEWARE_CLASSES.
53
54     If SSIFY_RENDER_VERBOSE setting is True, it will also leave some
55     information in HTML comments.
56
57     """
58     @staticmethod
59     def _process_rendered_response(request, response):
60         """Recursively process SSI statements in the response."""
61         def ssi_include(match):
62             """Replaces SSI include with contents rendered by relevant view."""
63             path = process_value(match.group('path'))
64             content = None
65             for cache in get_caches():
66                 content = cache.get(path)
67                 if content is not None:
68                     break
69             if content is None:
70                 func, args, kwargs = resolve(path)
71                 parsed = urlparse(path)
72
73                 # Reuse the original request, but reset some attributes.
74                 request.META['PATH_INFO'] = request.path_info = \
75                     request.path = parsed.path
76                 request.META['QUERY_STRING'] = parsed.query
77                 request.ssi_vars_needed = {}
78
79                 subresponse = func(request, *args, **kwargs)
80                 # FIXME: we should deal directly with bytes here.
81                 if subresponse.streaming:
82                     content = b"".join(subresponse.streaming_content)
83                 else:
84                     content = subresponse.content
85             content = process_content(content.decode('utf-8'))
86             if conf.RENDER_VERBOSE:
87                 return "".join((
88                     match.group(0),
89                     content,
90                     match.group(0).replace('<!--#', '<!--#end-'),
91                 ))
92             else:
93                 return content
94
95         def ssi_set(match):
96             """Interprets SSI set statement."""
97             content = match.group('value')
98             content = re.sub(UNESCAPE, r'\1', content)
99             variables[match.group('var')] = content
100             if conf.RENDER_VERBOSE:
101                 return content
102             else:
103                 return ""
104
105         def ssi_echo(match):
106             """Interprets SSI echo, outputting the value of the variable."""
107             content = variables[match.group('var')]
108             if conf.RENDER_VERBOSE:
109                 return "".join((
110                     match.group(0),
111                     content,
112                     match.group(0).replace('<!--#', '<!--#end-'),
113                 ))
114             else:
115                 return content
116
117         def ssi_if(match):
118             """Interprets SSI if statement."""
119             expr = process_value(match.group('expr'))
120             if expr:
121                 content = match.group('value')
122             else:
123                 content = match.group('else') or ''
124             if conf.RENDER_VERBOSE:
125                 return "".join((
126                     match.group('header'),
127                     content,
128                     match.group('header').replace('<!--#', '<!--#end-'),
129                 ))
130             else:
131                 return content
132
133         def ssi_var(match):
134             """Resolves ${var}-style variable reference."""
135             return variables[match.group('var')]
136
137         def process_value(content):
138             """Resolves any ${var}-style variable references in the content."""
139             return re.sub(SSI_VAR, ssi_var, content)
140
141         def process_content(content):
142             """Interprets SSI statements in the content."""
143             content = re.sub(SSI_SET, ssi_set, content)
144             content = re.sub(SSI_ECHO, ssi_echo, content)
145             content = re.sub(SSI_IF, ssi_if, content)
146             content = re.sub(SSI_INCLUDE, ssi_include, content)
147             return content
148
149         variables = {}
150         response.content = process_content(
151             response.content.decode('utf-8')).encode('utf-8')
152         response['Content-Length'] = len(response.content)
153
154     def process_response(self, request, response):
155         """Support for unrendered responses."""
156         if response.streaming:
157             return response
158         if hasattr(response, 'render') and callable(response.render):
159             response.add_post_render_callback(
160                 lambda r: self._process_rendered_response(request, r)
161             )
162         else:
163             self._process_rendered_response(request, response)
164         return response