X-Git-Url: https://git.mdrn.pl/django-ssify.git/blobdiff_plain/3d2c8ba4dfb4774daff367dae1656099eb2b562c..bc43e9922ca91c95797311937e7b5c0134211db5:/ssify/middleware.py diff --git a/ssify/middleware.py b/ssify/middleware.py index 4351083..8e0f453 100644 --- a/ssify/middleware.py +++ b/ssify/middleware.py @@ -1,62 +1,155 @@ +# -*- coding: utf-8 -*- +# This file is part of django-ssify, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See README.md for more information. +# +""" +Middleware classes provide by django-ssify. + +The main middleware you should use is SsiMiddleware. It's responsible +for providing the SSI variables needed for the SSI part of rendering. + +If you're using django's UpdateCacheMiddleware, add +PrepareForCacheMiddleware *after it* also. It will add all the data +needed by SsiMiddleware to the response. + +If you're using SessionMiddleware with LocaleMiddleware and your +USE_I18N or USE_L10N is True, you should also use the provided +LocaleMiddleware instead of the stock one. + +And, last but not least, if using CsrfViewMiddleware, move it to the +top of MIDDLEWARE_CLASSES, even before SsiMiddleware, and use +`csrf_token` from `ssify` tags library in your templates, this way +your CSRF tokens will be set correctly. + +So, you should end up with something like this: + + MIDDLEWARE_CLASSES = [ + 'django.middleware.csrf.CsrfViewMiddleware', + 'ssify.middleware.SsiMiddleware', + 'django.middleware.cache.UpdateCacheMiddleware', + 'ssify.middleware.PrepareForCacheMiddleware', + ... + 'ssify.middleware.LocaleMiddleware', + ... + ] + + +""" +from __future__ import unicode_literals from django.conf import settings -from django.utils.cache import patch_vary_headers from django.middleware import locale +from django.utils.cache import patch_vary_headers +from .conf import conf from .serializers import json_decode, json_encode +from .utils import ssi_vary_on_cookie from .variables import SsiVariable, provide_vars -from . import DEBUG + + +CACHE_HEADERS = ('Pragma', 'Cache-Control', 'Vary') class PrepareForCacheMiddleware(object): + """ + Patches the response object with all the data SsiMiddleware needs. + + This should go after UpdateCacheMiddleware in MIDDLEWARE_CLASSES. + """ @staticmethod def process_response(request, response): - if getattr(request, 'ssi_vars_needed', None): - vars_needed = {k: v.definition - for (k, v) in request.ssi_vars_needed.items()} + """Adds a 'X-Ssi-Vars-Needed' header to the response.""" + if ('X-Ssi-Vars-Needed' not in response and + getattr(request, 'ssi_vars_needed', None)): + vars_needed = {} + for (k, v) in request.ssi_vars_needed.items(): + vars_needed[k] = v.definition response['X-Ssi-Vars-Needed'] = json_encode( vars_needed, sort_keys=True) + + if ('X-ssi-restore' not in response and + getattr(request, 'ssi_patch_response', None)): + # We have some response modifiers set by ssi_includes and + # ssi_variables. Those are used, because unrendered SSI + # templates Django cache receives should have different + # caching headers, than pages rendered with request-specific + # information. + # What we do here is apply the modifiers, but restore + # previous values of any cache-relevant headers and set + # a custom header with modified values to set them + # after-cache. + original_fields = {} + for field in CACHE_HEADERS: + original_fields[field] = response.get(field, None) + for modifier in request.ssi_patch_response: + modifier(response) + restore_fields = {} + for field in CACHE_HEADERS: + new_value = response.get(field, None) + if new_value != original_fields[field]: + restore_fields[field] = new_value + if original_fields[field] is None: + del response[field] + else: + response[field] = original_fields[field] + response['X-ssi-restore'] = json_encode(restore_fields) + return response class SsiMiddleware(object): + """ + The main django-ssify middleware. + + It prepends the response content with SSI set statements, + providing values for any SSI variables used in the templates. + + It also patches the Vary header with the values given by + the SSI variables. + + If SSIFY_RENDER is set, it also passes the response through + SsiRenderMiddleware, which interprets and renders the SSI + statements, so you can see the output without an actual + SSI-enabled webserver. + + """ def process_request(self, request): - request.ssi_vary = set() - #request.ssi_cache_control_after = set() + request.ssi_patch_response = [] def process_view(self, request, view_func, view_args, view_kwargs): request.ssi_vars_needed = {} def _process_rendered_response(self, request, response): + if 'Content-Length' in response: + del response['Content-Length'] # Prepend the SSI variables. if hasattr(request, 'ssi_vars_needed'): vars_needed = request.ssi_vars_needed + elif 'X-Ssi-Vars-Needed' in response: + vars_needed = json_decode(response['X-Ssi-Vars-Needed']) + for k, v in vars_needed.items(): + vars_needed[k] = SsiVariable(*v) + if not settings.DEBUG: + del response['X-Ssi-Vars-Needed'] else: - vars_needed = json_decode(response.get('X-Ssi-Vars-Needed', '{}')) - vars_needed = {k: SsiVariable(*v) - for (k, v) in vars_needed.items()} + vars_needed = None if vars_needed: response.content = provide_vars(request, vars_needed) + \ response.content - # Add the Vary headers declared by all the SSI vars. - patch_vary_headers(response, sorted(request.ssi_vary)) - # TODO: cache control? - - # With a cached response, CsrfViewMiddleware.process_response - # was never called, so if we used the csrf token, we must do - # its job of setting the csrf token cookie on our own. - if (not getattr(request, 'csrf_processing_done', False) - and request.META.get("CSRF_COOKIE_USED", False)): - response.set_cookie(settings.CSRF_COOKIE_NAME, - request.META["CSRF_COOKIE"], - max_age=getattr(settings, 'CSRF_COOKIE_AGE', - 60 * 60 * 24 * 7 * 52), - domain=settings.CSRF_COOKIE_DOMAIN, - path=settings.CSRF_COOKIE_PATH, - secure=settings.CSRF_COOKIE_SECURE, - httponly=settings.CSRF_COOKIE_HTTPONLY - ) - request.csrf_processing_done = True + if 'X-ssi-restore' in response: + # The modifiers have already been applied to the response + # by the PrepareForCacheMiddleware. + # All we need to do is restore cache-relevant headers. + for header, content in json_decode(response['X-ssi-restore']).items(): + if content is None: + del response[header] + else: + response[header] = content + if not settings.DEBUG: + del response['X-ssi-restore'] + else: + for response_modifier in getattr(request, 'ssi_patch_response', []): + response_modifier(response) def process_response(self, request, response): if hasattr(response, 'render') and callable(response.render): @@ -66,9 +159,9 @@ class SsiMiddleware(object): else: self._process_rendered_response(request, response) - if DEBUG: - from .middleware_debug import DebugUnSsiMiddleware - response = DebugUnSsiMiddleware().process_response( + if conf.RENDER: + from .middleware_debug import SsiRenderMiddleware + response = SsiRenderMiddleware().process_response( request, response) return response @@ -103,4 +196,6 @@ class LocaleMiddleware(locale.LocaleMiddleware): if (request.session.accessed and (settings.USE_I18N or settings.USE_L10N)): request.session.accessed = False - request.ssi_vary.add('Cookie') + if not hasattr(request, 'ssi_patch_response'): + request.ssi_patch_response = [] + request.ssi_patch_response.append(ssi_vary_on_cookie)