Remove misleading Content-Length.
[django-ssify.git] / ssify / middleware.py
index 4351083..8e0f453 100644 (file)
+# -*- 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)