419a074891ee1381fab10c167799246a8918134a
[django-ssify.git] / ssify / middleware.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 Middleware classes provide by django-ssify.
7
8 The main middleware you should use is SsiMiddleware. It's responsible
9 for providing the SSI variables needed for the SSI part of rendering.
10
11 If you're using django's UpdateCacheMiddleware, add
12 PrepareForCacheMiddleware *after it* also. It will add all the data
13 needed by SsiMiddleware to the response.
14
15 If you're using SessionMiddleware with LocaleMiddleware and your
16 USE_I18N or USE_L10N is True, you should also use the provided
17 LocaleMiddleware instead of the stock one.
18
19 And, last but not least, if using CsrfViewMiddleware, move it to the
20 top of MIDDLEWARE_CLASSES, even before SsiMiddleware, and use
21 `csrf_token` from `ssify` tags library in your templates, this way
22 your CSRF tokens will be set correctly.
23
24 So, you should end up with something like this:
25
26     MIDDLEWARE_CLASSES = [
27        'django.middleware.csrf.CsrfViewMiddleware',
28        'ssify.middleware.SsiMiddleware',
29        'django.middleware.cache.UpdateCacheMiddleware',
30        'ssify.middleware.PrepareForCacheMiddleware',
31        ...
32        'ssify.middleware.LocaleMiddleware',
33        ...
34     ]
35
36
37 """
38 from __future__ import unicode_literals
39 from django.conf import settings
40 from django.middleware import locale
41 from django.utils.cache import patch_vary_headers
42 from .conf import conf
43 from .serializers import json_decode, json_encode
44 from .utils import ssi_vary_on_cookie
45 from .variables import SsiVariable, provide_vars
46
47
48 CACHE_HEADERS = ('Pragma', 'Cache-Control', 'Vary')
49
50
51 class PrepareForCacheMiddleware(object):
52     """
53     Patches the response object with all the data SsiMiddleware needs.
54
55     This should go after UpdateCacheMiddleware in MIDDLEWARE_CLASSES.
56     """
57     @staticmethod
58     def process_response(request, response):
59         """Adds a 'X-Ssi-Vars-Needed' header to the response."""
60         if ('X-Ssi-Vars-Needed' not in response and
61                 getattr(request, 'ssi_vars_needed', None)):
62             vars_needed = {}
63             for (k, v) in request.ssi_vars_needed.items():
64                 vars_needed[k] = v.definition
65             response['X-Ssi-Vars-Needed'] = json_encode(
66                 vars_needed, sort_keys=True)
67
68         if ('X-ssi-restore' not in response and
69                 getattr(request, 'ssi_patch_response', None)):
70             # We have some response modifiers set by ssi_includes and
71             # ssi_variables. Those are used, because unrendered SSI
72             # templates Django cache receives should have different
73             # caching headers, than pages rendered with request-specific
74             # information.
75             # What we do here is apply the modifiers, but restore
76             # previous values of any cache-relevant headers and set
77             # a custom header with modified values to set them
78             # after-cache.
79             original_fields = {}
80             for field in CACHE_HEADERS:
81                 original_fields[field] = response.get(field, None)
82             for modifier in request.ssi_patch_response:
83                 modifier(response)
84             restore_fields = {}
85             for field in CACHE_HEADERS:
86                 new_value = response.get(field, None)
87                 if new_value != original_fields[field]:
88                     restore_fields[field] = new_value
89                     if original_fields[field] is None:
90                         del response[field]
91                     else:
92                         response[field] = original_fields[field]
93             response['X-ssi-restore'] = json_encode(restore_fields)
94
95         return response
96
97
98 class SsiMiddleware(object):
99     """
100     The main django-ssify middleware.
101
102     It prepends the response content with SSI set statements,
103     providing values for any SSI variables used in the templates.
104
105     It also patches the Vary header with the values given by
106     the SSI variables.
107
108     If SSIFY_RENDER is set, it also passes the response through
109     SsiRenderMiddleware, which interprets and renders the SSI
110     statements, so you can see the output without an actual
111     SSI-enabled webserver.
112
113     """
114     def process_request(self, request):
115         request.ssi_patch_response = []
116
117     def process_view(self, request, view_func, view_args, view_kwargs):
118         request.ssi_vars_needed = {}
119
120     def _process_rendered_response(self, request, response):
121         # Prepend the SSI variables.
122         if hasattr(request, 'ssi_vars_needed'):
123             vars_needed = request.ssi_vars_needed
124         elif 'X-Ssi-Vars-Needed' in response:
125             vars_needed = json_decode(response['X-Ssi-Vars-Needed'])
126             for k, v in vars_needed.items():
127                 vars_needed[k] = SsiVariable(*v)
128             if not settings.DEBUG:
129                 del response['X-Ssi-Vars-Needed']
130         else:
131             vars_needed = None
132
133         if vars_needed:
134             response.content = provide_vars(request, vars_needed) + \
135                 response.content
136
137         if 'X-ssi-restore' in response:
138             # The modifiers have already been applied to the response
139             # by the PrepareForCacheMiddleware.
140             # All we need to do is restore cache-relevant headers.
141             for header, content in json_decode(response['X-ssi-restore']).items():
142                 if content is None:
143                     del response[header]
144                 else:
145                     response[header] = content
146             if not settings.DEBUG:
147                 del response['X-ssi-restore']
148         else:
149             for response_modifier in getattr(request, 'ssi_patch_response', []):
150                 response_modifier(response)
151
152     def process_response(self, request, response):
153         if hasattr(response, 'render') and callable(response.render):
154             response.add_post_render_callback(
155                 lambda r: self._process_rendered_response(request, r)
156             )
157         else:
158             self._process_rendered_response(request, response)
159
160         if conf.RENDER:
161             from .middleware_debug import SsiRenderMiddleware
162             response = SsiRenderMiddleware().process_response(
163                 request, response)
164
165         return response
166
167
168 class LocaleMiddleware(locale.LocaleMiddleware):
169     """
170     Version of the LocaleMiddleware for use together with the
171     SsiMiddleware if USE_I18N or USE_L10N is set.
172
173     Stock LocaleMiddleware looks for user language selection in
174     the session data and cookies, before it falls back to parsing
175     Accept-Language. The effect of accessing the session is adding
176     the `Vary: Cookie` header to the response.  While this is correct
177     behaviour, it renders the cache system useless (see
178     https://code.djangoproject.com/ticket/13217).
179
180     This version of LocaleMiddleware doesn't mark the session
181     as accessed on every request, so SessionMiddleware doesn't add the
182     Vary: Cookie header (unless something else actually uses the session
183     in a meaningful way, of course). Instead, it tells SsiMiddleware
184     to add the Vary: Cookie header to the final response.
185
186     """
187     def process_request(self, request):
188         if hasattr(request, 'session'):
189             session_accessed_before = request.session.accessed
190         else:
191             session_accessed_before = None
192         super(LocaleMiddleware, self).process_request(request)
193         if session_accessed_before is False:
194             if (request.session.accessed and
195                     (settings.USE_I18N or settings.USE_L10N)):
196                 request.session.accessed = False
197                 if not hasattr(request, 'ssi_patch_response'):
198                     request.ssi_patch_response = []
199                 request.ssi_patch_response.append(ssi_vary_on_cookie)