Commit b27db97b authored by Thomas Tanner's avatar Thomas Tanner Committed by Tim Graham
Browse files

Fixed #22461 -- Added if-unmodified-since support to the condition decorator.

parent fae551d7
Loading
Loading
Loading
Loading
+35 −29
Original line number Diff line number Diff line
@@ -52,6 +52,16 @@ require_safe = require_http_methods(["GET", "HEAD"])
require_safe.__doc__ = "Decorator to require that a view only accept safe methods: GET and HEAD."


def _precondition_failed(request):
    logger.warning('Precondition Failed: %s', request.path,
        extra={
            'status_code': 412,
            'request': request
        },
    )
    return HttpResponse(status=412)


def condition(etag_func=None, last_modified_func=None):
    """
    Decorator to support conditional retrieval (or change) for a view
@@ -81,8 +91,12 @@ def condition(etag_func=None, last_modified_func=None):
            if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE")
            if if_modified_since:
                if_modified_since = parse_http_date_safe(if_modified_since)
            if_unmodified_since = request.META.get("HTTP_IF_UNMODIFIED_SINCE")
            if if_unmodified_since:
                if_unmodified_since = parse_http_date_safe(if_unmodified_since)
            if_none_match = request.META.get("HTTP_IF_NONE_MATCH")
            if_match = request.META.get("HTTP_IF_MATCH")
            etags = []
            if if_none_match or if_match:
                # There can be more than one ETag in the request, so we
                # consider the list of values.
@@ -97,21 +111,19 @@ def condition(etag_func=None, last_modified_func=None):
                    if_match = None

            # Compute values (if any) for the requested resource.
            if etag_func:
                res_etag = etag_func(request, *args, **kwargs)
            else:
                res_etag = None
            def get_last_modified():
                if last_modified_func:
                    dt = last_modified_func(request, *args, **kwargs)
                    if dt:
                    res_last_modified = timegm(dt.utctimetuple())
                else:
                    res_last_modified = None
            else:
                res_last_modified = None
                        return timegm(dt.utctimetuple())

            res_etag = etag_func(request, *args, **kwargs) if etag_func else None
            res_last_modified = get_last_modified()

            response = None
            if not ((if_match and (if_modified_since or if_none_match)) or
            if not ((if_match and if_modified_since) or
                    (if_none_match and if_unmodified_since) or
                    (if_modified_since and if_unmodified_since) or
                    (if_match and if_none_match)):
                # We only get here if no undefined combinations of headers are
                # specified.
@@ -123,26 +135,20 @@ def condition(etag_func=None, last_modified_func=None):
                    if request.method in ("GET", "HEAD"):
                        response = HttpResponseNotModified()
                    else:
                        logger.warning('Precondition Failed: %s', request.path,
                            extra={
                                'status_code': 412,
                                'request': request
                            }
                        )
                        response = HttpResponse(status=412)
                elif if_match and ((not res_etag and "*" in etags) or
                        (res_etag and res_etag not in etags)):
                    logger.warning('Precondition Failed: %s', request.path,
                        extra={
                            'status_code': 412,
                            'request': request
                        }
                    )
                    response = HttpResponse(status=412)
                        response = _precondition_failed(request)
                elif (if_match and ((not res_etag and "*" in etags) or
                        (res_etag and res_etag not in etags) or
                        (res_last_modified and if_unmodified_since and
                        res_last_modified > if_unmodified_since))):
                    response = _precondition_failed(request)
                elif (not if_none_match and request.method in ("GET", "HEAD") and
                        res_last_modified and if_modified_since and
                        res_last_modified <= if_modified_since):
                    response = HttpResponseNotModified()
                elif (not if_match and
                        res_last_modified and if_unmodified_since and
                        res_last_modified > if_unmodified_since):
                    response = _precondition_failed(request)

            if response is None:
                response = func(request, *args, **kwargs)
+3 −0
Original line number Diff line number Diff line
@@ -528,6 +528,9 @@ Requests and Responses
  <django.http.HttpResponse.setdefault>` method allows setting a header unless
  it has already been set.

* The :func:`~django.views.decorators.http.condition` decorator for
  conditional view processing now supports the ``If-unmodified-since`` header.

Tests
^^^^^

+13 −3
Original line number Diff line number Diff line
@@ -15,18 +15,29 @@ or you can rely on the :class:`~django.middleware.common.CommonMiddleware`
middleware to set the ``ETag`` header.

When the client next requests the same resource, it might send along a header
such as `If-modified-since`_, containing the date of the last modification
time it was sent, or `If-none-match`_, containing the ``ETag`` it was sent.
such as either `If-modified-since`_ or `If-unmodified-since`_, containing the
date of the last modification time it was sent, or either `If-match`_ or
`If-none-match`_, containing the last ``ETag`` it was sent.
If the current version of the page matches the ``ETag`` sent by the client, or
if the resource has not been modified, a 304 status code can be sent back,
instead of a full response, telling the client that nothing has changed.
Depending on the header, if the page has been modified or does not match the
``ETag`` sent by the client, a 412 status code (Precondition Failed) may be
returned.

.. _If-match: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24
.. _If-none-match: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
.. _If-modified-since: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25
.. _If-unmodified-since: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.28

When you need more fine-grained control you may use per-view conditional
processing functions.

.. versionchanged:: 1.8

    Support for the ``If-unmodified-since`` header was added to conditional
    view processing.

.. _conditional-decorators:

The ``condition`` decorator
@@ -194,4 +205,3 @@ view takes a while to generate the content, you should consider using the
fairly quickly, stick to using the middleware and the amount of network
traffic sent back to the clients will still be reduced if the view hasn't
changed.
+60 −0
Original line number Diff line number Diff line
@@ -49,6 +49,20 @@ class ConditionalGet(TestCase):
        response = self.client.get('/condition/')
        self.assertFullResponse(response)

    def test_if_unmodified_since(self):
        self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
        response = self.client.get('/condition/')
        self.assertFullResponse(response)
        self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_NEWER_STR
        response = self.client.get('/condition/')
        self.assertFullResponse(response)
        self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_INVALID_STR
        response = self.client.get('/condition/')
        self.assertFullResponse(response)
        self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
        response = self.client.get('/condition/')
        self.assertEqual(response.status_code, 412)

    def test_if_none_match(self):
        self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG
        response = self.client.get('/condition/')
@@ -71,6 +85,7 @@ class ConditionalGet(TestCase):
        self.assertEqual(response.status_code, 412)

    def test_both_headers(self):
        # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
        self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG
        response = self.client.get('/condition/')
@@ -86,6 +101,32 @@ class ConditionalGet(TestCase):
        response = self.client.get('/condition/')
        self.assertFullResponse(response)

        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
        self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % EXPIRED_ETAG
        response = self.client.get('/condition/')
        self.assertFullResponse(response)

    def test_both_headers_2(self):
        self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
        self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % ETAG
        response = self.client.get('/condition/')
        self.assertFullResponse(response)

        self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
        self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % EXPIRED_ETAG
        response = self.client.get('/condition/')
        self.assertEqual(response.status_code, 412)

        self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
        self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % EXPIRED_ETAG
        response = self.client.get('/condition/')
        self.assertEqual(response.status_code, 412)

        self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
        self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % ETAG
        response = self.client.get('/condition/')
        self.assertEqual(response.status_code, 412)

    def test_single_condition_1(self):
        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
        response = self.client.get('/condition/last_modified/')
@@ -124,6 +165,25 @@ class ConditionalGet(TestCase):
        response = self.client.get('/condition/last_modified2/')
        self.assertFullResponse(response, check_etag=False)

    def test_single_condition_7(self):
        self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
        response = self.client.get('/condition/last_modified/')
        self.assertEqual(response.status_code, 412)
        response = self.client.get('/condition/etag/')
        self.assertFullResponse(response, check_last_modified=False)

    def test_single_condition_8(self):
        self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
        response = self.client.get('/condition/last_modified/')
        self.assertFullResponse(response, check_etag=False)

    def test_single_condition_9(self):
        self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
        response = self.client.get('/condition/last_modified2/')
        self.assertEqual(response.status_code, 412)
        response = self.client.get('/condition/etag2/')
        self.assertFullResponse(response, check_last_modified=False)

    def test_single_condition_head(self):
        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
        response = self.client.head('/condition/')