Code

Ticket #717: 717.3.diff

File 717.3.diff, 17.6 KB (added by aaugustin, 3 years ago)
Line 
1Index: django/views/decorators/http.py
2===================================================================
3--- django/views/decorators/http.py     (revision 15662)
4+++ django/views/decorators/http.py     (working copy)
5@@ -9,10 +9,9 @@
6 
7 from calendar import timegm
8 from datetime import timedelta
9-from email.Utils import formatdate
10 
11 from django.utils.decorators import decorator_from_middleware, available_attrs
12-from django.utils.http import parse_etags, quote_etag
13+from django.utils.http import http_date, parse_http_date_safe, parse_etags, quote_etag
14 from django.utils.log import getLogger
15 from django.middleware.http import ConditionalGetMiddleware
16 from django.http import HttpResponseNotAllowed, HttpResponseNotModified, HttpResponse
17@@ -79,6 +78,8 @@
18         def inner(request, *args, **kwargs):
19             # Get HTTP request headers
20             if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE")
21+            if if_modified_since:
22+                if_modified_since = parse_http_date_safe(if_modified_since)
23             if_none_match = request.META.get("HTTP_IF_NONE_MATCH")
24             if_match = request.META.get("HTTP_IF_MATCH")
25             if if_none_match or if_match:
26@@ -102,7 +103,7 @@
27             if last_modified_func:
28                 dt = last_modified_func(request, *args, **kwargs)
29                 if dt:
30-                    res_last_modified = formatdate(timegm(dt.utctimetuple()))[:26] + 'GMT'
31+                    res_last_modified = timegm(dt.utctimetuple())
32                 else:
33                     res_last_modified = None
34             else:
35@@ -116,7 +117,8 @@
36                 if ((if_none_match and (res_etag in etags or
37                         "*" in etags and res_etag)) and
38                         (not if_modified_since or
39-                            res_last_modified == if_modified_since)):
40+                            (res_last_modified and if_modified_since and
41+                            res_last_modified <= if_modified_since))):
42                     if request.method in ("GET", "HEAD"):
43                         response = HttpResponseNotModified()
44                     else:
45@@ -136,9 +138,9 @@
46                         }
47                     )
48                     response = HttpResponse(status=412)
49-                elif (not if_none_match and if_modified_since and
50-                        request.method == "GET" and
51-                        res_last_modified == if_modified_since):
52+                elif (not if_none_match and request.method == "GET" and
53+                        res_last_modified and if_modified_since and
54+                        res_last_modified <= if_modified_since):
55                     response = HttpResponseNotModified()
56 
57             if response is None:
58@@ -146,7 +148,7 @@
59 
60             # Set relevant headers on the response if they don't already exist.
61             if res_last_modified and not response.has_header('Last-Modified'):
62-                response['Last-Modified'] = res_last_modified
63+                response['Last-Modified'] = http_date(res_last_modified)
64             if res_etag and not response.has_header('ETag'):
65                 response['ETag'] = quote_etag(res_etag)
66 
67Index: django/views/static.py
68===================================================================
69--- django/views/static.py      (revision 15662)
70+++ django/views/static.py      (working copy)
71@@ -9,12 +9,11 @@
72 import re
73 import stat
74 import urllib
75-from email.Utils import parsedate_tz, mktime_tz
76 
77 from django.template import loader
78 from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified
79 from django.template import Template, Context, TemplateDoesNotExist
80-from django.utils.http import http_date
81+from django.utils.http import http_date, parse_http_date
82 
83 def serve(request, path, document_root=None, show_indexes=False):
84     """
85@@ -128,10 +127,7 @@
86             raise ValueError
87         matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header,
88                            re.IGNORECASE)
89-        header_date = parsedate_tz(matches.group(1))
90-        if header_date is None:
91-            raise ValueError
92-        header_mtime = mktime_tz(header_date)
93+        header_mtime = parse_http_date(matches.group(1))
94         header_len = matches.group(3)
95         if header_len and int(header_len) != size:
96             raise ValueError
97Index: django/utils/http.py
98===================================================================
99--- django/utils/http.py        (revision 15662)
100+++ django/utils/http.py        (working copy)
101@@ -1,3 +1,5 @@
102+import calendar
103+import datetime
104 import re
105 import sys
106 import urllib
107@@ -8,6 +10,17 @@
108 
109 ETAG_MATCH = re.compile(r'(?:W/)?"((?:\\.|[^"])*)"')
110 
111+MONTHS = 'jan feb mar apr may jun jul aug sep oct nov dec'.split()
112+__D = r'(?P<day>\d{2})'
113+__D2 = r'(?P<day>[ \d]\d)'
114+__M = r'(?P<mon>\w{3})'
115+__Y = r'(?P<year>\d{4})'
116+__Y2 = r'(?P<year>\d{2})'
117+__T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'
118+RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T))
119+RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T))
120+ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y))
121+
122 def urlquote(url, safe='/'):
123     """
124     A version of Python's urllib.quote() function that can operate on unicode
125@@ -70,6 +83,48 @@
126     rfcdate = formatdate(epoch_seconds)
127     return '%s GMT' % rfcdate[:25]
128 
129+def parse_http_date(date):
130+    """
131+    Parses a date format as specified by HTTP RFC2616 section 3.3.1.
132+
133+    The three formats allowed by the RFC are accepted, even if only the first
134+    one is still in widespread use.
135+
136+    Returns an floating point number expressed in seconds since the epoch, in
137+    UTC.
138+    """
139+    # emails.Util.parsedate does the job for RFC1123 dates; unfortunately
140+    # RFC2616 makes it mandatory to support RFC850 dates too. So we roll
141+    # our own RFC-compliant parsing.
142+    for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE:
143+        m = regex.match(date)
144+        if m is not None:
145+            break
146+    else:
147+        raise ValueError("%r is not in a valid HTTP date format" % date)
148+    try:
149+        year = int(m.group('year'))
150+        if year < 100:
151+            year += 2000 if year < 70 else 1900
152+        month = MONTHS.index(m.group('mon').lower()) + 1
153+        day = int(m.group('day'))
154+        hour = int(m.group('hour'))
155+        min = int(m.group('min'))
156+        sec = int(m.group('sec'))
157+        result = datetime.datetime(year, month, day, hour, min, sec)
158+        return calendar.timegm(result.utctimetuple())
159+    except Exception:
160+        raise ValueError("%r is not a valid date" % date)
161+
162+def parse_http_date_safe(date):
163+    """
164+    Same as parse_http_date, but returns None if the input is invalid.
165+    """
166+    try:
167+        return parse_http_date(date)
168+    except Exception:
169+        pass
170+
171 # Base 36 functions: useful for generating compact URLs
172 
173 def base36_to_int(s):
174Index: django/middleware/http.py
175===================================================================
176--- django/middleware/http.py   (revision 15662)
177+++ django/middleware/http.py   (working copy)
178@@ -1,5 +1,5 @@
179 from django.core.exceptions import MiddlewareNotUsed
180-from django.utils.http import http_date
181+from django.utils.http import http_date, parse_http_date_safe
182 
183 class ConditionalGetMiddleware(object):
184     """
185@@ -15,7 +15,7 @@
186             response['Content-Length'] = str(len(response.content))
187 
188         if response.has_header('ETag'):
189-            if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None)
190+            if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
191             if if_none_match == response['ETag']:
192                 # Setting the status is enough here. The response handling path
193                 # automatically removes content for this status code (in
194@@ -23,10 +23,14 @@
195                 response.status_code = 304
196 
197         if response.has_header('Last-Modified'):
198-            if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None)
199-            if if_modified_since == response['Last-Modified']:
200-                # Setting the status code is enough here (same reasons as
201-                # above).
202-                response.status_code = 304
203+            if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
204+            if if_modified_since is not None:
205+                if_modified_since = parse_http_date_safe(if_modified_since)
206+            if if_modified_since is not None:
207+                last_modified = parse_http_date_safe(response['Last-Modified'])
208+                if last_modified is not None and last_modified <= if_modified_since:
209+                    # Setting the status code is enough here (same reasons as
210+                    # above).
211+                    response.status_code = 304
212 
213         return response
214Index: tests/regressiontests/views/tests/static.py
215===================================================================
216--- tests/regressiontests/views/tests/static.py (revision 15662)
217+++ tests/regressiontests/views/tests/static.py (working copy)
218@@ -51,7 +51,7 @@
219         file_name = 'file.txt'
220         response = self.client.get(
221             '/views/%s/%s' % (self.prefix, file_name),
222-            HTTP_IF_MODIFIED_SINCE='Mon, 18 Jan 2038 05:14:07 UTC'
223+            HTTP_IF_MODIFIED_SINCE='Mon, 18 Jan 2038 05:14:07 GMT'
224             # This is 24h before max Unix time. Remember to fix Django and
225             # update this test well before 2038 :)
226             )
227Index: tests/regressiontests/conditional_processing/models.py
228===================================================================
229--- tests/regressiontests/conditional_processing/models.py      (revision 15662)
230+++ tests/regressiontests/conditional_processing/models.py      (working copy)
231@@ -1,17 +1,20 @@
232 # -*- coding:utf-8 -*-
233-from datetime import datetime, timedelta
234-from calendar import timegm
235+from datetime import datetime
236 
237 from django.test import TestCase
238-from django.utils.http import parse_etags, quote_etag
239+from django.utils import unittest
240+from django.utils.http import parse_etags, quote_etag, parse_http_date
241 
242 FULL_RESPONSE = 'Test conditional get response'
243 LAST_MODIFIED = datetime(2007, 10, 21, 23, 21, 47)
244 LAST_MODIFIED_STR = 'Sun, 21 Oct 2007 23:21:47 GMT'
245+LAST_MODIFIED_NEWER_STR = 'Mon, 18 Oct 2010 16:56:23 GMT'
246+LAST_MODIFIED_INVALID_STR = 'Mon, 32 Oct 2010 16:56:23 GMT'
247 EXPIRED_LAST_MODIFIED_STR = 'Sat, 20 Oct 2007 23:21:47 GMT'
248 ETAG = 'b4246ffc4f62314ca13147c9d4f76974'
249 EXPIRED_ETAG = '7fae4cd4b0f81e7d2914700043aa8ed6'
250 
251+
252 class ConditionalGet(TestCase):
253     def assertFullResponse(self, response, check_last_modified=True, check_etag=True):
254         self.assertEquals(response.status_code, 200)
255@@ -33,6 +36,12 @@
256         self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
257         response = self.client.get('/condition/')
258         self.assertNotModified(response)
259+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_NEWER_STR
260+        response = self.client.get('/condition/')
261+        self.assertNotModified(response)
262+        self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_INVALID_STR
263+        response = self.client.get('/condition/')
264+        self.assertFullResponse(response)
265         self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
266         response = self.client.get('/condition/')
267         self.assertFullResponse(response)
268@@ -118,7 +127,7 @@
269         self.assertFullResponse(response, check_last_modified=False)
270 
271 
272-class ETagProcesing(TestCase):
273+class ETagProcessing(unittest.TestCase):
274     def testParsing(self):
275         etags = parse_etags(r'"", "etag", "e\"t\"ag", "e\\tag", W/"weak"')
276         self.assertEquals(etags, ['', 'etag', 'e"t"ag', r'e\tag', 'weak'])
277@@ -126,3 +135,20 @@
278     def testQuoting(self):
279         quoted_etag = quote_etag(r'e\t"ag')
280         self.assertEquals(quoted_etag, r'"e\\t\"ag"')
281+
282+
283+class HttpDateProcessing(unittest.TestCase):
284+    def testParsingRfc1123(self):
285+        parsed = parse_http_date('Sun, 06 Nov 1994 08:49:37 GMT')
286+        self.assertEqual(datetime.utcfromtimestamp(parsed),
287+                         datetime(1994, 11, 06, 8, 49, 37))
288+
289+    def testParsingRfc850(self):
290+        parsed = parse_http_date('Sunday, 06-Nov-94 08:49:37 GMT')
291+        self.assertEqual(datetime.utcfromtimestamp(parsed),
292+                         datetime(1994, 11, 06, 8, 49, 37))
293+
294+    def testParsingAsctime(self):
295+        parsed = parse_http_date('Sun Nov  6 08:49:37 1994')
296+        self.assertEqual(datetime.utcfromtimestamp(parsed),
297+                         datetime(1994, 11, 06, 8, 49, 37))
298Index: tests/regressiontests/middleware/tests.py
299===================================================================
300--- tests/regressiontests/middleware/tests.py   (revision 15662)
301+++ tests/regressiontests/middleware/tests.py   (working copy)
302@@ -3,6 +3,7 @@
303 from django.conf import settings
304 from django.http import HttpRequest
305 from django.middleware.common import CommonMiddleware
306+from django.middleware.http import ConditionalGetMiddleware
307 from django.test import TestCase
308 
309 
310@@ -247,3 +248,89 @@
311       self.assertEquals(r.status_code, 301)
312       self.assertEquals(r['Location'],
313                         'http://www.testserver/middleware/customurlconf/slash/')
314+
315+class ConditionalGetMiddlewareTest(TestCase):
316+    urls = 'regressiontests.middleware.cond_get_urls'
317+    def setUp(self):
318+        self.req = HttpRequest()
319+        self.req.META = {
320+            'SERVER_NAME': 'testserver',
321+            'SERVER_PORT': 80,
322+        }
323+        self.req.path = self.req.path_info = "/"
324+        self.resp = self.client.get(self.req.path)
325+
326+    # Tests for the Date header
327+
328+    def test_date_header_added(self):
329+        self.assertFalse('Date' in self.resp)
330+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
331+        self.assertTrue('Date' in self.resp)
332+
333+    # Tests for the Content-Length header
334+
335+    def test_content_length_header_added(self):
336+        content_length = len(self.resp.content)
337+        self.assertFalse('Content-Length' in self.resp)
338+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
339+        self.assertTrue('Content-Length' in self.resp)
340+        self.assertEqual(int(self.resp['Content-Length']), content_length)
341+
342+    def test_content_length_header_not_changed(self):
343+        bad_content_length = len(self.resp.content) + 10
344+        self.resp['Content-Length'] = bad_content_length
345+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
346+        self.assertEqual(int(self.resp['Content-Length']), bad_content_length)
347+
348+    # Tests for the ETag header
349+
350+    def test_if_none_match_and_no_etag(self):
351+        self.req.META['HTTP_IF_NONE_MATCH'] = 'spam'
352+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
353+        self.assertEquals(self.resp.status_code, 200)
354+
355+    def test_no_if_none_match_and_etag(self):
356+        self.resp['ETag'] = 'eggs'
357+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
358+        self.assertEquals(self.resp.status_code, 200)
359+
360+    def test_if_none_match_and_same_etag(self):
361+        self.req.META['HTTP_IF_NONE_MATCH'] = self.resp['ETag'] = 'spam'
362+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
363+        self.assertEquals(self.resp.status_code, 304)
364+
365+    def test_if_none_match_and_different_etag(self):
366+        self.req.META['HTTP_IF_NONE_MATCH'] = 'spam'
367+        self.resp['ETag'] = 'eggs'
368+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
369+        self.assertEquals(self.resp.status_code, 200)
370+
371+    # Tests for the Last-Modified header
372+
373+    def test_if_modified_since_and_no_last_modified(self):
374+        self.req.META['HTTP_IF_MODIFIED_SINCE'] = 'Sat, 12 Feb 2011 17:38:44 GMT'
375+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
376+        self.assertEquals(self.resp.status_code, 200)
377+
378+    def test_no_if_modified_since_and_last_modified(self):
379+        self.resp['Last-Modified'] = 'Sat, 12 Feb 2011 17:38:44 GMT'
380+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
381+        self.assertEquals(self.resp.status_code, 200)
382+
383+    def test_if_modified_since_and_same_last_modified(self):
384+        self.req.META['HTTP_IF_MODIFIED_SINCE'] = 'Sat, 12 Feb 2011 17:38:44 GMT'
385+        self.resp['Last-Modified'] = 'Sat, 12 Feb 2011 17:38:44 GMT'
386+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
387+        self.assertEquals(self.resp.status_code, 304)
388+
389+    def test_if_modified_since_and_last_modified_in_the_past(self):
390+        self.req.META['HTTP_IF_MODIFIED_SINCE'] = 'Sat, 12 Feb 2011 17:38:44 GMT'
391+        self.resp['Last-Modified'] = 'Sat, 12 Feb 2011 17:35:44 GMT'
392+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
393+        self.assertEquals(self.resp.status_code, 304)
394+
395+    def test_if_modified_since_and_last_modified_in_the_future(self):
396+        self.req.META['HTTP_IF_MODIFIED_SINCE'] = 'Sat, 12 Feb 2011 17:38:44 GMT'
397+        self.resp['Last-Modified'] = 'Sat, 12 Feb 2011 17:41:44 GMT'
398+        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
399+        self.assertEquals(self.resp.status_code, 200)
400Index: tests/regressiontests/middleware/cond_get_urls.py
401===================================================================
402--- tests/regressiontests/middleware/cond_get_urls.py   (revision 0)
403+++ tests/regressiontests/middleware/cond_get_urls.py   (revision 0)
404@@ -0,0 +1,6 @@
405+from django.conf.urls.defaults import patterns
406+from django.http import HttpResponse
407+
408+urlpatterns = patterns('',
409+    (r'^$', lambda request: HttpResponse('root is here')),
410+)