Ticket #9886: 9886.6.diff

File 9886.6.diff, 17.3 KB (added by Ivan Sagalaev, 14 years ago)

Patch updated to current trunk

  • django/core/handlers/modpython.py

    === modified file 'django/core/handlers/modpython.py'
     
    4242            # naughty, but also pretty harmless.
    4343            self.path_info = u'/'
    4444        self._post_parse_error = False
     45        self._stream = self._req
     46        self._read_started = False
    4547
    4648    def __repr__(self):
    4749        # Since this is called as part of error handling, we need to be very
     
    8183            # mod_python < 3.2.10 doesn't have req.is_https().
    8284            return self._req.subprocess_env.get('HTTPS', '').lower() in ('on', '1')
    8385
    84     def _load_post_and_files(self):
    85         "Populates self._post and self._files"
    86         if self.method != 'POST':
    87             self._post, self._files = http.QueryDict('', encoding=self._encoding), datastructures.MultiValueDict()
    88             return
    89 
    90         if 'content-type' in self._req.headers_in and self._req.headers_in['content-type'].startswith('multipart'):
    91             self._raw_post_data = ''
    92             try:
    93                 self._post, self._files = self.parse_file_upload(self.META, self._req)
    94             except:
    95                 # See django.core.handlers.wsgi.WSGIHandler for an explanation
    96                 # of what's going on here.
    97                 self._post = http.QueryDict('')
    98                 self._files = datastructures.MultiValueDict()
    99                 self._post_parse_error = True
    100                 raise
    101         else:
    102             self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict()
    103 
    10486    def _get_request(self):
    10587        if not hasattr(self, '_request'):
    10688            self._request = datastructures.MergeDict(self.POST, self.GET)
     
    162144                self._meta[key] = value
    163145        return self._meta
    164146
    165     def _get_raw_post_data(self):
    166         try:
    167             return self._raw_post_data
    168         except AttributeError:
    169             self._raw_post_data = self._req.read()
    170             return self._raw_post_data
    171 
    172147    def _get_method(self):
    173148        return self.META['REQUEST_METHOD'].upper()
    174149
     
    178153    FILES = property(_get_files)
    179154    META = property(_get_meta)
    180155    REQUEST = property(_get_request)
    181     raw_post_data = property(_get_raw_post_data)
    182156    method = property(_get_method)
    183157
    184158class ModPythonHandler(BaseHandler):
  • django/core/handlers/wsgi.py

    === modified file 'django/core/handlers/wsgi.py'
     
    55    from cStringIO import StringIO
    66except ImportError:
    77    from StringIO import StringIO
     8import socket
    89
    910from django import http
    1011from django.core import signals
     
    6263    505: 'HTTP VERSION NOT SUPPORTED',
    6364}
    6465
    65 def safe_copyfileobj(fsrc, fdst, length=16*1024, size=0):
    66     """
    67     A version of shutil.copyfileobj that will not read more than 'size' bytes.
    68     This makes it safe from clients sending more than CONTENT_LENGTH bytes of
    69     data in the body.
    70     """
    71     if not size:
    72         return
    73     while size > 0:
    74         buf = fsrc.read(min(length, size))
    75         if not buf:
    76             break
    77         fdst.write(buf)
    78         size -= len(buf)
     66class LimitedStream(object):
     67    '''
     68    LimitedStream wraps another stream in order to not allow reading from it
     69    past specified amount of bytes.
     70    '''
     71    def __init__(self, stream, limit, buf_size=64 * 1024 * 1024):
     72        self.stream = stream
     73        self.remaining = limit
     74        self.buffer = ''
     75        self.buf_size = buf_size
     76
     77    def _read_limited(self, size=None):
     78        if size is None or size > self.remaining:
     79            size = self.remaining
     80        if size == 0:
     81            return ''
     82        result = self.stream.read(size)
     83        self.remaining -= len(result)
     84        return result
     85
     86    def read(self, size=None):
     87        if size is None:
     88            result = self.buffer + self._read_limited()
     89            self.buffer = ''
     90        elif size < len(self.buffer):
     91            result = self.buffer[:size]
     92            self.buffer = self.buffer[size:]
     93        else: # size >= len(self.buffer)
     94            result = self.buffer + self._read_limited(size - len(self.buffer))
     95            self.buffer = ''
     96        return result
     97
     98    def readline(self, size=None):
     99        while '\n' not in self.buffer or \
     100              (size is not None and len(self.buffer) < size):
     101            chunk = self._read_limited(self.buf_size)
     102            if not chunk:
     103                break
     104            self.buffer += chunk
     105        sio = StringIO(self.buffer)
     106        line = sio.readline()
     107        self.buffer = sio.read()
     108        return line
    79109
    80110class WSGIRequest(http.HttpRequest):
    81111    def __init__(self, environ):
     
    98128        self.META['SCRIPT_NAME'] = script_name
    99129        self.method = environ['REQUEST_METHOD'].upper()
    100130        self._post_parse_error = False
     131        if isinstance(self.environ['wsgi.input'], socket._fileobject):
     132            # Under development server 'wsgi.input' is an instance of
     133            # socket._fileobject which hangs indefinitely on reading bytes past
     134            # available count. To prevent this it's wrapped in LimitedStream
     135            # that doesn't read past Content-Length bytes.
     136            #
     137            # This is not done for other kinds of inputs (like flup's FastCGI
     138            # streams) beacuse they don't suffer from this problem and we can
     139            # avoid using another wrapper with its own .read and .readline
     140            # implementation.
     141            try:
     142                content_length = int(self.environ.get('CONTENT_LENGTH', 0))
     143            except (ValueError, TypeError):
     144                content_length = 0
     145            self._stream = LimitedStream(self.environ['wsgi.input'], content_length)
     146        else:
     147            self._stream = self.environ['wsgi.input']
     148        self._read_started = False
    101149
    102150    def __repr__(self):
    103151        # Since this is called as part of error handling, we need to be very
     
    133181        return 'wsgi.url_scheme' in self.environ \
    134182            and self.environ['wsgi.url_scheme'] == 'https'
    135183
    136     def _load_post_and_files(self):
    137         # Populates self._post and self._files
    138         if self.method == 'POST':
    139             if self.environ.get('CONTENT_TYPE', '').startswith('multipart'):
    140                 self._raw_post_data = ''
    141                 try:
    142                     self._post, self._files = self.parse_file_upload(self.META, self.environ['wsgi.input'])
    143                 except:
    144                     # An error occured while parsing POST data.  Since when
    145                     # formatting the error the request handler might access
    146                     # self.POST, set self._post and self._file to prevent
    147                     # attempts to parse POST data again.
    148                     self._post = http.QueryDict('')
    149                     self._files = datastructures.MultiValueDict()
    150                     # Mark that an error occured.  This allows self.__repr__ to
    151                     # be explicit about it instead of simply representing an
    152                     # empty POST
    153                     self._post_parse_error = True
    154                     raise
    155             else:
    156                 self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict()
    157         else:
    158             self._post, self._files = http.QueryDict('', encoding=self._encoding), datastructures.MultiValueDict()
    159 
    160184    def _get_request(self):
    161185        if not hasattr(self, '_request'):
    162186            self._request = datastructures.MergeDict(self.POST, self.GET)
     
    192216            self._load_post_and_files()
    193217        return self._files
    194218
    195     def _get_raw_post_data(self):
    196         try:
    197             return self._raw_post_data
    198         except AttributeError:
    199             buf = StringIO()
    200             try:
    201                 # CONTENT_LENGTH might be absent if POST doesn't have content at all (lighttpd)
    202                 content_length = int(self.environ.get('CONTENT_LENGTH', 0))
    203             except (ValueError, TypeError):
    204                 # If CONTENT_LENGTH was empty string or not an integer, don't
    205                 # error out. We've also seen None passed in here (against all
    206                 # specs, but see ticket #8259), so we handle TypeError as well.
    207                 content_length = 0
    208             if content_length > 0:
    209                 safe_copyfileobj(self.environ['wsgi.input'], buf,
    210                         size=content_length)
    211             self._raw_post_data = buf.getvalue()
    212             buf.close()
    213             return self._raw_post_data
    214 
    215219    GET = property(_get_get, _set_get)
    216220    POST = property(_get_post, _set_post)
    217221    COOKIES = property(_get_cookies, _set_cookies)
    218222    FILES = property(_get_files)
    219223    REQUEST = property(_get_request)
    220     raw_post_data = property(_get_raw_post_data)
    221224
    222225class WSGIHandler(base.BaseHandler):
    223226    initLock = Lock()
  • django/http/__init__.py

    === modified file 'django/http/__init__.py'
     
    77from urllib import urlencode
    88from urlparse import urljoin
    99try:
     10    from cStringIO import StringIO
     11except ImportError:
     12    from StringIO import StringIO
     13try:
    1014    # The mod_python version is more efficient, so try importing it first.
    1115    from mod_python.util import parse_qsl
    1216except ImportError:
     
    132136        parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)
    133137        return parser.parse()
    134138
     139    def _get_raw_post_data(self):
     140        if not hasattr(self, '_raw_post_data'):
     141            if self._read_started:
     142                raise Exception("You cannot access raw_post_data after reading from request's data stream")
     143            self._raw_post_data = self.read()
     144            self._stream = StringIO(self._raw_post_data)
     145        return self._raw_post_data
     146    raw_post_data = property(_get_raw_post_data)
     147
     148    def _mark_post_parse_error(self):
     149        self._post = QueryDict('')
     150        self._files = MultiValueDict()
     151        self._post_parse_error = True
     152
     153    def _load_post_and_files(self):
     154        # Populates self._post and self._files
     155        if self.method != 'POST':
     156            self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict()
     157            return
     158        if self._read_started:
     159            self._mark_post_parse_error()
     160            return
     161
     162        if self.META.get('CONTENT_TYPE', '').startswith('multipart'):
     163            self._raw_post_data = ''
     164            try:
     165                self._post, self._files = self.parse_file_upload(self.META, self)
     166            except:
     167                # An error occured while parsing POST data.  Since when
     168                # formatting the error the request handler might access
     169                # self.POST, set self._post and self._file to prevent
     170                # attempts to parse POST data again.
     171                # Mark that an error occured.  This allows self.__repr__ to
     172                # be explicit about it instead of simply representing an
     173                # empty POST
     174                self._mark_post_parse_error()
     175                raise
     176        else:
     177            self._post, self._files = QueryDict(self.raw_post_data, encoding=self._encoding), MultiValueDict()
     178
     179    ## File-like and iterator interface.
     180    ##
     181    ## Expects self._stream to be set to an appropriate source of bytes by
     182    ## a corresponding request subclass (WSGIRequest or ModPythonRequest).
     183    ## Also when request data has already been read by request.POST or
     184    ## request.raw_post_data self._stream pooints to a StringIO instance
     185    ## containing that data.
     186
     187    def read(self, *args, **kwargs):
     188        self._read_started = True
     189        return self._stream.read(*args, **kwargs)
     190
     191    def readline(self, *args, **kwargs):
     192        self._read_started = True
     193        return self._stream.readline(*args, **kwargs)
     194
     195    def xreadlines(self):
     196        while True:
     197            buf = self.readline()
     198            if not buf:
     199                break
     200            yield buf
     201    __iter__ = xreadlines
     202
     203    def readlines(self):
     204        return list(iter(self))
     205
    135206class QueryDict(MultiValueDict):
    136207    """
    137208    A specialized MultiValueDict that takes a query string when initialized.
     
    198269        for key, value in dict.items(self):
    199270            dict.__setitem__(result, copy.deepcopy(key, memo), copy.deepcopy(value, memo))
    200271        return result
    201    
     272
    202273    def setlist(self, key, list_):
    203274        self._assert_mutable()
    204275        key = str_to_unicode(key, self.encoding)
  • docs/ref/request-response.txt

    === modified file 'docs/ref/request-response.txt'
     
    189189
    190190.. attribute:: HttpRequest.raw_post_data
    191191
    192     The raw HTTP POST data. This is only useful for advanced processing. Use
    193     ``POST`` instead.
     192    The raw HTTP POST data as a byte string. This is useful for processing
     193    data in different formats than of conventional HTML forms: binary images,
     194    XML payload etc. For processing form data use ``HttpRequest.POST``.
     195
     196    .. versionadded:: 1.3
     197
     198    You can also read from an HttpRequest using file-like interface, see
     199    ``HttpRequest.read()``
    194200
    195201.. attribute:: HttpRequest.urlconf
    196202
     
    249255   If you write your own XMLHttpRequest call (on the browser side), you'll
    250256   have to set this header manually if you want ``is_ajax()`` to work.
    251257
     258.. method:: HttpRequest.read(size=None)
     259.. method:: HttpRequest.readline()
     260.. method:: HttpRequest.readlines()
     261.. method:: HttpRequest.xreadlines()
     262.. method:: HttpRequest.__iter__()
     263
     264   .. versionadded:: 1.3
     265
     266   Methods implement file-like interface for reading from an HttpRequest
     267   instance. This makes it possible to process an incoming request in a
     268   streaming fashion. A common use-case is processing a big XML payload with
     269   iterative parser without constructing a whole XML tree in memory.
     270
     271   Given this standard interface an HttpRequest instance can be passed directly
     272   to an XML parser, for example ElementTree::
     273
     274       import xml.etree.ElementTree as ET
     275       for element in ET.iterparse(request):
     276           process(element)
     277
    252278
    253279QueryDict objects
    254280-----------------
  • tests/regressiontests/requests/tests.py

    === modified file 'tests/regressiontests/requests/tests.py'
     
    11from datetime import datetime, timedelta
    22import time
     3from StringIO import StringIO
    34import unittest
    45
    56from django.http import HttpRequest, HttpResponse, parse_cookie
    6 from django.core.handlers.wsgi import WSGIRequest
     7from django.core.handlers.wsgi import WSGIRequest, LimitedStream
    78from django.core.handlers.modpython import ModPythonRequest
    89from django.utils.http import cookie_date
    910
     
    1718        self.assertEqual(request.META.keys(), [])
    1819
    1920    def test_wsgirequest(self):
    20         request = WSGIRequest({'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus'})
     21        request = WSGIRequest({'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus', 'wsgi.input': StringIO('')})
    2122        self.assertEqual(request.GET.keys(), [])
    2223        self.assertEqual(request.POST.keys(), [])
    2324        self.assertEqual(request.COOKIES.keys(), [])
    24         self.assertEqual(set(request.META.keys()), set(['PATH_INFO', 'REQUEST_METHOD', 'SCRIPT_NAME']))
     25        self.assertEqual(set(request.META.keys()), set(['PATH_INFO', 'REQUEST_METHOD', 'SCRIPT_NAME', 'wsgi.input']))
    2526        self.assertEqual(request.META['PATH_INFO'], 'bogus')
    2627        self.assertEqual(request.META['REQUEST_METHOD'], 'bogus')
    2728        self.assertEqual(request.META['SCRIPT_NAME'], '')
     
    8889        max_age_cookie = response.cookies['max_age']
    8990        self.assertEqual(max_age_cookie['max-age'], 10)
    9091        self.assertEqual(max_age_cookie['expires'], cookie_date(time.time()+10))
     92
     93    def test_limited_stream(self):
     94        stream = LimitedStream(StringIO('test'), 2)
     95        self.assertEqual(stream.read(), 'te')
     96        stream = LimitedStream(StringIO('test'), 2)
     97        self.assertEqual(stream.read(5), 'te')
     98
     99    def test_stream(self):
     100        request = WSGIRequest({'REQUEST_METHOD': 'POST', 'wsgi.input': StringIO('name=value')})
     101        self.assertEqual(request.read(), 'name=value')
     102
     103    def test_read_after_value(self):
     104        """
     105        Reading from request is allowed after accessing request contents as
     106        POST or raw_post_data.
     107        """
     108        request = WSGIRequest({'REQUEST_METHOD': 'POST', 'wsgi.input': StringIO('name=value')})
     109        self.assertEqual(request.POST, {u'name': [u'value']})
     110        self.assertEqual(request.raw_post_data, 'name=value')
     111        self.assertEqual(request.read(), 'name=value')
     112
     113    def test_value_after_read(self):
     114        """
     115        Construction of POST or raw_post_data is not allowed after reading
     116        from request.
     117        """
     118        request = WSGIRequest({'REQUEST_METHOD': 'POST', 'wsgi.input': StringIO('name=value')})
     119        self.assertEqual(request.read(2), 'na')
     120        self.assertRaises(Exception, lambda: request.raw_post_data)
     121        self.assertEqual(request.POST, {})
     122
     123    def test_read_by_lines(self):
     124        request = WSGIRequest({'REQUEST_METHOD': 'POST', 'wsgi.input': StringIO('name=value')})
     125        self.assertEqual(list(request), ['name=value'])
Back to Top