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