Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #19468 -- Decoded request.path correctly on Python 3.

Thanks aliva for the report and claudep for the feedback.
  • Loading branch information...
commit 1e4a27d08790c96f657d2e960c8142d1ca69aede 1 parent d9a0b6a
Aymeric Augustin authored December 17, 2012
3  django/contrib/staticfiles/handlers.py
@@ -6,6 +6,7 @@
6 6
     from urlparse import urlparse
7 7
 
8 8
 from django.conf import settings
  9
+from django.core.handlers.base import get_path_info
9 10
 from django.core.handlers.wsgi import WSGIHandler
10 11
 
11 12
 from django.contrib.staticfiles import utils
@@ -67,6 +68,6 @@ def get_response(self, request):
67 68
         return super(StaticFilesHandler, self).get_response(request)
68 69
 
69 70
     def __call__(self, environ, start_response):
70  
-        if not self._should_handle(environ['PATH_INFO']):
  71
+        if not self._should_handle(get_path_info(environ)):
71 72
             return self.application(environ, start_response)
72 73
         return super(StaticFilesHandler, self).__call__(environ, start_response)
43  django/core/handlers/base.py
@@ -5,10 +5,14 @@
5 5
 import types
6 6
 
7 7
 from django import http
  8
+from django.conf import settings
  9
+from django.core import exceptions
  10
+from django.core import urlresolvers
8 11
 from django.core import signals
9 12
 from django.utils.encoding import force_text
10 13
 from django.utils.importlib import import_module
11 14
 from django.utils import six
  15
+from django.views import debug
12 16
 
13 17
 logger = logging.getLogger('django.request')
14 18
 
@@ -32,8 +36,6 @@ def load_middleware(self):
32 36
 
33 37
         Must be called after the environment is fixed (see __call__ in subclasses).
34 38
         """
35  
-        from django.conf import settings
36  
-        from django.core import exceptions
37 39
         self._view_middleware = []
38 40
         self._template_response_middleware = []
39 41
         self._response_middleware = []
@@ -75,9 +77,6 @@ def load_middleware(self):
75 77
 
76 78
     def get_response(self, request):
77 79
         "Returns an HttpResponse object for the given HttpRequest"
78  
-        from django.core import exceptions, urlresolvers
79  
-        from django.conf import settings
80  
-
81 80
         try:
82 81
             # Setup default url resolver for this thread, this code is outside
83 82
             # the try/except so we don't get a spurious "unbound local
@@ -147,7 +146,6 @@ def get_response(self, request):
147 146
                                 'request': request
148 147
                             })
149 148
                 if settings.DEBUG:
150  
-                    from django.views import debug
151 149
                     response = debug.technical_404_response(request, e)
152 150
                 else:
153 151
                     try:
@@ -204,8 +202,6 @@ def handle_uncaught_exception(self, request, resolver, exc_info):
204 202
         caused by anything, so assuming something like the database is always
205 203
         available would be an error.
206 204
         """
207  
-        from django.conf import settings
208  
-
209 205
         if settings.DEBUG_PROPAGATE_EXCEPTIONS:
210 206
             raise
211 207
 
@@ -218,7 +214,6 @@ def handle_uncaught_exception(self, request, resolver, exc_info):
218 214
         )
219 215
 
220 216
         if settings.DEBUG:
221  
-            from django.views import debug
222 217
             return debug.technical_500_response(request, *exc_info)
223 218
 
224 219
         # If Http500 handler is not installed, re-raise last exception
@@ -238,6 +233,20 @@ def apply_response_fixes(self, request, response):
238 233
             response = func(request, response)
239 234
         return response
240 235
 
  236
+
  237
+def get_path_info(environ):
  238
+    """
  239
+    Returns the HTTP request's PATH_INFO as a unicode string.
  240
+    """
  241
+    path_info = environ.get('PATH_INFO', str('/'))
  242
+    # Under Python 3, strings in environ are decoded with ISO-8859-1;
  243
+    # re-encode to recover the original bytestring provided by the webserver.
  244
+    if six.PY3:
  245
+        path_info = path_info.encode('iso-8859-1')
  246
+    # It'd be better to implement URI-to-IRI decoding, see #19508.
  247
+    return path_info.decode('utf-8')
  248
+
  249
+
241 250
 def get_script_name(environ):
242 251
     """
243 252
     Returns the equivalent of the HTTP request's SCRIPT_NAME environment
@@ -246,7 +255,6 @@ def get_script_name(environ):
246 255
     from the client's perspective), unless the FORCE_SCRIPT_NAME setting is
247 256
     set (to anything).
248 257
     """
249  
-    from django.conf import settings
250 258
     if settings.FORCE_SCRIPT_NAME is not None:
251 259
         return force_text(settings.FORCE_SCRIPT_NAME)
252 260
 
@@ -255,9 +263,14 @@ def get_script_name(environ):
255 263
     # rewrites. Unfortunately not every Web server (lighttpd!) passes this
256 264
     # information through all the time, so FORCE_SCRIPT_NAME, above, is still
257 265
     # needed.
258  
-    script_url = environ.get('SCRIPT_URL', '')
259  
-    if not script_url:
260  
-        script_url = environ.get('REDIRECT_URL', '')
  266
+    script_url = environ.get('SCRIPT_URL', environ.get('REDIRECT_URL', str('')))
261 267
     if script_url:
262  
-        return force_text(script_url[:-len(environ.get('PATH_INFO', ''))])
263  
-    return force_text(environ.get('SCRIPT_NAME', ''))
  268
+        script_name = script_url[:-len(environ.get('PATH_INFO', str('')))]
  269
+    else:
  270
+        script_name = environ.get('SCRIPT_NAME', str(''))
  271
+    # Under Python 3, strings in environ are decoded with ISO-8859-1;
  272
+    # re-encode to recover the original bytestring provided by the webserver.
  273
+    if six.PY3:
  274
+        script_name = script_name.encode('iso-8859-1')
  275
+    # It'd be better to implement URI-to-IRI decoding, see #19508.
  276
+    return script_name.decode('utf-8')
2  django/core/handlers/wsgi.py
@@ -128,7 +128,7 @@ def readline(self, size=None):
128 128
 class WSGIRequest(http.HttpRequest):
129 129
     def __init__(self, environ):
130 130
         script_name = base.get_script_name(environ)
131  
-        path_info = force_text(environ.get('PATH_INFO', '/'))
  131
+        path_info = base.get_path_info(environ)
132 132
         if not path_info or path_info == script_name:
133 133
             # Sometimes PATH_INFO exists, but is empty (e.g. accessing
134 134
             # the SCRIPT_NAME URL without a trailing slash). We really need to
6  django/test/client.py
@@ -245,7 +245,11 @@ def _get_path(self, parsed):
245 245
         # If there are parameters, add them
246 246
         if parsed[3]:
247 247
             path += str(";") + force_str(parsed[3])
248  
-        return unquote(path)
  248
+        path = unquote(path)
  249
+        # WSGI requires latin-1 encoded strings. See get_path_info().
  250
+        if six.PY3:
  251
+            path = path.encode('utf-8').decode('iso-8859-1')
  252
+        return path
249 253
 
250 254
     def get(self, path, data={}, **extra):
251 255
         "Construct a GET request."
3  tests/regressiontests/handlers/tests.py
... ...
@@ -1,6 +1,7 @@
1 1
 from django.core.handlers.wsgi import WSGIHandler
2 2
 from django.test import RequestFactory
3 3
 from django.test.utils import override_settings
  4
+from django.utils import six
4 5
 from django.utils import unittest
5 6
 
6 7
 class HandlerTests(unittest.TestCase):
@@ -22,7 +23,7 @@ def test_lock_safety(self):
22 23
     def test_bad_path_info(self):
23 24
         """Tests for bug #15672 ('request' referenced before assignment)"""
24 25
         environ = RequestFactory().get('/').environ
25  
-        environ['PATH_INFO'] = b'\xed'
  26
+        environ['PATH_INFO'] = '\xed'
26 27
         handler = WSGIHandler()
27 28
         response = handler(environ, lambda *a, **k: None)
28 29
         self.assertEqual(response.status_code, 400)
11  tests/regressiontests/requests/tests.py
@@ -11,6 +11,7 @@
11 11
 from django.http import HttpRequest, HttpResponse, parse_cookie, build_request_repr, UnreadablePostError
12 12
 from django.test.client import FakePayload
13 13
 from django.test.utils import override_settings, str_prefix
  14
+from django.utils import six
14 15
 from django.utils import unittest
15 16
 from django.utils.http import cookie_date, urlencode
16 17
 from django.utils.timezone import utc
@@ -57,6 +58,16 @@ def test_wsgirequest_repr(self):
57 58
         self.assertEqual(build_request_repr(request, path_override='/otherpath/', GET_override={'a': 'b'}, POST_override={'c': 'd'}, COOKIES_override={'e': 'f'}, META_override={'g': 'h'}),
58 59
                          str_prefix("<WSGIRequest\npath:/otherpath/,\nGET:{%(_)s'a': %(_)s'b'},\nPOST:{%(_)s'c': %(_)s'd'},\nCOOKIES:{%(_)s'e': %(_)s'f'},\nMETA:{%(_)s'g': %(_)s'h'}>"))
59 60
 
  61
+    def test_wsgirequest_path_info(self):
  62
+        def wsgi_str(path_info):
  63
+            path_info = path_info.encode('utf-8')           # Actual URL sent by the browser (bytestring)
  64
+            if six.PY3:
  65
+                path_info = path_info.decode('iso-8859-1')  # Value in the WSGI environ dict (native string)
  66
+            return path_info
  67
+        # Regression for #19468
  68
+        request = WSGIRequest({'PATH_INFO': wsgi_str("/سلام/"), 'REQUEST_METHOD': 'get', 'wsgi.input': BytesIO(b'')})
  69
+        self.assertEqual(request.path, "/سلام/")
  70
+
60 71
     def test_parse_cookie(self):
61 72
         self.assertEqual(parse_cookie('invalid@key=true'), {})
62 73
 

0 notes on commit 1e4a27d

Please sign in to comment.
Something went wrong with that request. Please try again.