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