Initial commit.
[django-ssify.git] / ssify / middleware_debug.py
1 """
2 This module should only be used for debugging SSI statements.
3
4 Using DebugUnSsiMiddleware in production defeats the purpose of using SSI
5 in the first place, and is unsafe. You should use a proper webserver with SSI
6 support as a proxy (i.e. Nginx with ssi=on).
7
8 """
9 import re
10 import urlparse
11 from django.core.urlresolvers import resolve
12 from ssify import DEBUG_VERBOSE
13
14
15 SSI_SET = re.compile(r"<!--#set var='(?P<var>[^']+)' "
16                      r"value='(?P<value>|\\\\|.*?[^\\](?:\\\\)*)'-->", re.S)
17 SSI_ECHO = re.compile(r"<!--#echo var='(?P<var>[^']+)' encoding='none'-->")
18 SSI_INCLUDE = re.compile(r"<!--#include (?:virtual|file)='(?P<path>[^']+)'-->")
19 SSI_IF = re.compile(r"(?P<header><!--#if expr='(?P<expr>[^']*)'-->)"
20                     r"(?P<value>.*?)(?:<!--#else-->(?P<else>.*?))?"
21                     r"<!--#endif-->", re.S)
22         # TODO: escaped?
23 SSI_VAR = re.compile(r"\$\{(?P<var>.+)\}")  # TODO: escaped?
24
25
26 class DebugUnSsiMiddleware(object):
27     """
28     Emulates a webserver with SSI support.
29
30     This middleware should only be used for debugging purposes.
31     SsiMiddleware will enable it automatically, if SSIFY_DEBUG setting
32     is set to True, so you don't normally need to include it in
33     MIDDLEWARE_CLASSES.
34
35     If SSIFY_DEBUG_VERBOSE setting is True, it will also leave some
36     information in HTML comments.
37
38     """
39     @staticmethod
40     def _process_rendered_response(request, response):
41         """Recursively process SSI statements in the response."""
42         def ssi_include(match):
43             """Replaces SSI include with contents rendered by relevant view."""
44             path = process_value(match.group('path'))
45             func, args, kwargs = resolve(path)
46             parsed = urlparse.urlparse(path)
47             request.META['PATH_INFO'] = request.path_info = \
48                 request.path = parsed.path
49             request.META['QUERY_STRING'] = parsed.query
50             content = func(request, *args, **kwargs).content
51             content = process_content(content)
52             if DEBUG_VERBOSE:
53                 return "".join((
54                     match.group(0),
55                     content,
56                     match.group(0).replace('<!--#', '<!--#end-'),
57                 ))
58             else:
59                 return content
60
61         def ssi_set(match):
62             """Interprets SSI set statement."""
63             variables[match.group('var')] = match.group('value')
64             if DEBUG_VERBOSE:
65                 return match.group(0)
66             else:
67                 return ""
68
69         def ssi_echo(match):
70             """Interprets SSI echo, outputting the value of the variable."""
71             content = variables[match.group('var')]
72             if DEBUG_VERBOSE:
73                 return "".join((
74                     match.group(0),
75                     content,
76                     match.group(0).replace('<!--#', '<!--#end-'),
77                 ))
78             else:
79                 return content
80
81         def ssi_if(match):
82             """Interprets SSI if statement."""
83             expr = process_value(match.group('expr'))
84             if expr:
85                 content = match.group('value')
86             else:
87                 content = match.group('else')
88             if DEBUG_VERBOSE:
89                 return "".join((
90                     match.group('header'),
91                     content,
92                     match.group('header').replace('<!--#', '<!--#end-'),
93                 ))
94             else:
95                 return content
96
97         def ssi_var(match):
98             """Resolves ${var}-style variable reference."""
99             return variables[match.group('var')]
100
101         def process_value(content):
102             """Resolves any ${var}-style variable references in the content."""
103             return re.sub(SSI_VAR, ssi_var, content)
104
105         def process_content(content):
106             """Interprets SSI statements in the content."""
107             content = re.sub(SSI_SET, ssi_set, content)
108             content = re.sub(SSI_ECHO, ssi_echo, content)
109             content = re.sub(SSI_IF, ssi_if, content)
110             content = re.sub(SSI_INCLUDE, ssi_include, content)
111             return content
112
113         variables = {}
114         response.content = process_content(response.content)
115         response['Content-Length'] = len(response.content)
116
117     def process_response(self, request, response):
118         """Support for unrendered responses."""
119         if hasattr(response, 'render') and callable(response.render):
120             response.add_post_render_callback(
121                 lambda r: self._process_rendered_response(request, r)
122             )
123         else:
124             self._process_rendered_response(request, response)
125         return response