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.
6 This module should only be used for debugging SSI statements.
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).
13 from __future__ import unicode_literals
17 from urllib.parse import urlparse
19 from urlparse import urlparse
22 from django.urls import NoReverseMatch, reverse, resolve
25 from django.core.urlresolvers import NoReverseMatch, reverse, resolve
27 from django.core.urlresolvers import resolve
28 from .cache import get_caches
30 from .conf import conf
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>.+)\}")
42 UNESCAPE = re.compile(r'\\(.)')
45 class SsiRenderMiddleware(object):
47 Emulates a webserver with SSI support.
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
54 If SSIFY_RENDER_VERBOSE setting is True, it will also leave some
55 information in HTML comments.
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'))
65 for cache in get_caches():
66 content = cache.get(path)
67 if content is not None:
70 func, args, kwargs = resolve(path)
71 parsed = urlparse(path)
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 = {}
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)
84 content = subresponse.content
85 content = process_content(content.decode('utf-8'))
86 if conf.RENDER_VERBOSE:
90 match.group(0).replace('<!--#', '<!--#end-'),
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:
106 """Interprets SSI echo, outputting the value of the variable."""
107 content = variables[match.group('var')]
108 if conf.RENDER_VERBOSE:
112 match.group(0).replace('<!--#', '<!--#end-'),
118 """Interprets SSI if statement."""
119 expr = process_value(match.group('expr'))
121 content = match.group('value')
123 content = match.group('else') or ''
124 if conf.RENDER_VERBOSE:
126 match.group('header'),
128 match.group('header').replace('<!--#', '<!--#end-'),
134 """Resolves ${var}-style variable reference."""
135 return variables[match.group('var')]
137 def process_value(content):
138 """Resolves any ${var}-style variable references in the content."""
139 return re.sub(SSI_VAR, ssi_var, content)
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)
150 response.content = process_content(
151 response.content.decode('utf-8')).encode('utf-8')
152 response['Content-Length'] = len(response.content)
154 def process_response(self, request, response):
155 """Support for unrendered responses."""
156 if response.streaming:
158 if hasattr(response, 'render') and callable(response.render):
159 response.add_post_render_callback(
160 lambda r: self._process_rendered_response(request, r)
163 self._process_rendered_response(request, response)