a1f8444491c7e7e76196705aed46e0bcb048b1b5
[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.utils.cache import patch_vary_headers
41 from django.middleware import locale
42 from .serializers import json_decode, json_encode
43 from .variables import SsiVariable, provide_vars
44 from . import DEBUG
45
46
47 class PrepareForCacheMiddleware(object):
48     """
49     Patches the response object with all the data SsiMiddleware needs.
50
51     This should go after UpdateCacheMiddleware in MIDDLEWARE_CLASSES.
52     """
53     @staticmethod
54     def process_response(request, response):
55         """Adds a 'X-Ssi-Vars-Needed' header to the response."""
56         if getattr(request, 'ssi_vars_needed', None):
57             vars_needed = {}
58             for (k, v) in request.ssi_vars_needed.items():
59                 vars_needed[k] = v.definition
60             response['X-Ssi-Vars-Needed'] = json_encode(
61                 vars_needed, sort_keys=True)
62         return response
63
64
65 class SsiMiddleware(object):
66     """
67     The main django-ssify middleware.
68
69     It prepends the response content with SSI set statements,
70     providing values for any SSI variables used in the templates.
71
72     It also patches the Vary header with the values given by
73     the SSI variables.
74
75     If SSIFY_DEBUG is set, it also passes the response through
76     DebugUnSsiMiddleware, which interprets and renders the SSI
77     statements, so you can see the output without an actual
78     SSI-enabled webserver.
79
80     """
81     def process_request(self, request):
82         request.ssi_vary = set()
83         #request.ssi_cache_control_after = set()
84
85     def process_view(self, request, view_func, view_args, view_kwargs):
86         request.ssi_vars_needed = {}
87
88     def _process_rendered_response(self, request, response):
89         # Prepend the SSI variables.
90         if hasattr(request, 'ssi_vars_needed'):
91             vars_needed = request.ssi_vars_needed
92         else:
93             vars_needed = json_decode(response.get('X-Ssi-Vars-Needed', '{}'))
94             for k, v in vars_needed.items():
95                 vars_needed[k] = SsiVariable(*v)
96
97         if vars_needed:
98             response.content = provide_vars(request, vars_needed) + \
99                 response.content
100
101         # Add the Vary headers declared by all the SSI vars.
102         patch_vary_headers(response, sorted(request.ssi_vary))
103         # TODO: cache control?
104
105     def process_response(self, request, response):
106         if hasattr(response, 'render') and callable(response.render):
107             response.add_post_render_callback(
108                 lambda r: self._process_rendered_response(request, r)
109             )
110         else:
111             self._process_rendered_response(request, response)
112
113         if DEBUG:
114             from .middleware_debug import DebugUnSsiMiddleware
115             response = DebugUnSsiMiddleware().process_response(
116                 request, response)
117
118         return response
119
120
121 class LocaleMiddleware(locale.LocaleMiddleware):
122     """
123     Version of the LocaleMiddleware for use together with the
124     SsiMiddleware if USE_I18N or USE_L10N is set.
125
126     Stock LocaleMiddleware looks for user language selection in
127     the session data and cookies, before it falls back to parsing
128     Accept-Language. The effect of accessing the session is adding
129     the `Vary: Cookie` header to the response.  While this is correct
130     behaviour, it renders the cache system useless (see
131     https://code.djangoproject.com/ticket/13217).
132
133     This version of LocaleMiddleware doesn't mark the session
134     as accessed on every request, so SessionMiddleware doesn't add the
135     Vary: Cookie header (unless something else actually uses the session
136     in a meaningful way, of course). Instead, it tells SsiMiddleware
137     to add the Vary: Cookie header to the final response.
138
139     """
140     def process_request(self, request):
141         if hasattr(request, 'session'):
142             session_accessed_before = request.session.accessed
143         else:
144             session_accessed_before = None
145         super(LocaleMiddleware, self).process_request(request)
146         if session_accessed_before is False:
147             if (request.session.accessed and
148                     (settings.USE_I18N or settings.USE_L10N)):
149                 request.session.accessed = False
150                 request.ssi_vary.add('Cookie')