Ticket #9886: 9886.5.diff

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

Updated to current trunk, tests converted to unittests

  • django/core/handlers/modpython.py

    === modified file 'django/core/handlers/modpython.py'
     
    3737            # naughty, but also pretty harmless.
    3838            self.path_info = u'/'
    3939        self._post_parse_error = False
     40        self._stream = self._req
     41        self._read_started = False
    4042
    4143    def __repr__(self):
    4244        # Since this is called as part of error handling, we need to be very
     
    7678            # mod_python < 3.2.10 doesn't have req.is_https().
    7779            return self._req.subprocess_env.get('HTTPS', '').lower() in ('on', '1')
    7880
    79     def _load_post_and_files(self):
    80         "Populates self._post and self._files"
    81         if self.method != 'POST':
    82             self._post, self._files = http.QueryDict('', encoding=self._encoding), datastructures.MultiValueDict()
    83             return
    84 
    85         if 'content-type' in self._req.headers_in and self._req.headers_in['content-type'].startswith('multipart'):
    86             self._raw_post_data = ''
    87             try:
    88                 self._post, self._files = self.parse_file_upload(self.META, self._req)
    89             except:
    90                 # See django.core.handlers.wsgi.WSGIHandler for an explanation
    91                 # of what's going on here.
    92                 self._post = http.QueryDict('')
    93                 self._files = datastructures.MultiValueDict()
    94                 self._post_parse_error = True
    95                 raise
    96         else:
    97             self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict()
    98 
    9981    def _get_request(self):
    10082        if not hasattr(self, '_request'):
    10183            self._request = datastructures.MergeDict(self.POST, self.GET)
     
    157139                self._meta[key] = value
    158140        return self._meta
    159141
    160     def _get_raw_post_data(self):
    161         try:
    162             return self._raw_post_data
    163         except AttributeError:
    164             self._raw_post_data = self._req.read()
    165             return self._raw_post_data
    166 
    167142    def _get_method(self):
    168143        return self.META['REQUEST_METHOD'].upper()
    169144
     
    173148    FILES = property(_get_files)
    174149    META = property(_get_meta)
    175150    REQUEST = property(_get_request)
    176     raw_post_data = property(_get_raw_post_data)
    177151    method = property(_get_method)
    178152
    179153class ModPythonHandler(BaseHandler):
  • django/core/handlers/wsgi.py

    === modified file 'django/core/handlers/wsgi.py'
     
    44    from cStringIO import StringIO
    55except ImportError:
    66    from StringIO import StringIO
     7import socket
    78
    89from django import http
    910from django.core import signals
     
    5758    505: 'HTTP VERSION NOT SUPPORTED',
    5859}
    5960
    60 def safe_copyfileobj(fsrc, fdst, length=16*1024, size=0):
    61     """
    62     A version of shutil.copyfileobj that will not read more than 'size' bytes.
    63     This makes it safe from clients sending more than CONTENT_LENGTH bytes of
    64     data in the body.
    65     """
    66     if not size:
    67         return
    68     while size > 0:
    69         buf = fsrc.read(min(length, size))
    70         if not buf:
    71             break
    72         fdst.write(buf)
    73         size -= len(buf)
     61class LimitedStream(object):
     62    '''
     63    LimitedStream wraps another stream in order to not allow reading from it
     64    past specified amount of bytes.
     65    '''
     66    def __init__(self, stream, limit, buf_size=64 * 1024 * 1024):
     67        self.stream = stream
     68        self.remaining = limit
     69        self.buffer = ''
     70        self.buf_size = buf_size
     71
     72    def _read_limited(self, size=None):
     73        if size is None or size > self.remaining:
     74            size = self.remaining
     75        if size == 0:
     76            return ''
     77        result = self.stream.read(size)
     78        self.remaining -= len(result)
     79        return result
     80
     81    def read(self, size=None):
     82        if size is None:
     83            result = self.buffer + self._read_limited()
     84            self.buffer = ''
     85        elif size < len(self.buffer):
     86            result = self.buffer[:size]
     87            self.buffer = self.buffer[size:]
     88        else: # size >= len(self.buffer)
     89            result = self.buffer + self._read_limited(size - len(self.buffer))
     90            self.buffer = ''
     91        return result
     92
     93    def readline(self, size=None):
     94        while '\n' not in self.buffer or \
     95              (size is not None and len(self.buffer) < size):
     96            chunk = self._read_limited(self.buf_size)
     97            if not chunk:
     98                break
     99            self.buffer += chunk
     100        sio = StringIO(self.buffer)
     101        line = sio.readline()
     102        self.buffer = sio.read()
     103        return line
    74104
    75105class WSGIRequest(http.HttpRequest):
    76106    def __init__(self, environ):
     
    93123        self.META['SCRIPT_NAME'] = script_name
    94124        self.method = environ['REQUEST_METHOD'].upper()
    95125        self._post_parse_error = False
     126        if isinstance(self.environ['wsgi.input'], socket._fileobject):
     127            # Under development server 'wsgi.input' is an instance of
     128            # socket._fileobject which hangs indefinitely on reading bytes past
     129            # available count. To prevent this it's wrapped in LimitedStream
     130            # that doesn't read past Content-Length bytes.
     131            #
     132            # This is not done for other kinds of inputs (like flup's FastCGI
     133            # streams) beacuse they don't suffer from this problem and we can
     134            # avoid using another wrapper with its own .read and .readline
     135            # implementation.
     136            try:
     137                content_length = int(self.environ.get('CONTENT_LENGTH', 0))
     138            except (ValueError, TypeError):
     139                content_length = 0
     140            self._stream = LimitedStream(self.environ['wsgi.input'], content_length)
     141        else:
     142            self._stream = self.environ['wsgi.input']
     143        self._read_started = False
    96144
    97145    def __repr__(self):
    98146        # Since this is called as part of error handling, we need to be very
     
    128176        return 'wsgi.url_scheme' in self.environ \
    129177            and self.environ['wsgi.url_scheme'] == 'https'
    130178
    131     def _load_post_and_files(self):
    132         # Populates self._post and self._files
    133         if self.method == 'POST':
    134             if self.environ.get('CONTENT_TYPE', '').startswith('multipart'):
    135                 self._raw_post_data = ''
    136                 try:
    137                     self._post, self._files = self.parse_file_upload(self.META, self.environ['wsgi.input'])
    138                 except:
    139                     # An error occured while parsing POST data.  Since when
    140                     # formatting the error the request handler might access
    141                     # self.POST, set self._post and self._file to prevent
    142                     # attempts to parse POST data again.
    143                     self._post = http.QueryDict('')
    144                     self._files = datastructures.MultiValueDict()
    145                     # Mark that an error occured.  This allows self.__repr__ to
    146                     # be explicit about it instead of simply representing an
    147                     # empty POST
    148                     self._post_parse_error = True
    149                     raise
    150             else:
    151                 self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict()
    152         else:
    153             self._post, self._files = http.QueryDict('', encoding=self._encoding), datastructures.MultiValueDict()
    154 
    155179    def _get_request(self):
    156180        if not hasattr(self, '_request'):
    157181            self._request = datastructures.MergeDict(self.POST, self.GET)
     
    187211            self._load_post_and_files()
    188212        return self._files
    189213
    190     def _get_raw_post_data(self):
    191         try:
    192             return self._raw_post_data
    193         except AttributeError:
    194             buf = StringIO()
    195             try:
    196                 # CONTENT_LENGTH might be absent if POST doesn't have content at all (lighttpd)
    197                 content_length = int(self.environ.get('CONTENT_LENGTH', 0))
    198             except (ValueError, TypeError):
    199                 # If CONTENT_LENGTH was empty string or not an integer, don't
    200                 # error out. We've also seen None passed in here (against all
    201                 # specs, but see ticket #8259), so we handle TypeError as well.
    202                 content_length = 0
    203             if content_length > 0:
    204                 safe_copyfileobj(self.environ['wsgi.input'], buf,
    205                         size=content_length)
    206             self._raw_post_data = buf.getvalue()
    207             buf.close()
    208             return self._raw_post_data
    209 
    210214    GET = property(_get_get, _set_get)
    211215    POST = property(_get_post, _set_post)
    212216    COOKIES = property(_get_cookies, _set_cookies)
    213217    FILES = property(_get_files)
    214218    REQUEST = property(_get_request)
    215     raw_post_data = property(_get_raw_post_data)
    216219
    217220class WSGIHandler(base.BaseHandler):
    218221    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:
     
    126130        parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)
    127131        return parser.parse()
    128132
     133    def _get_raw_post_data(self):
     134        if not hasattr(self, '_raw_post_data'):
     135            if self._read_started:
     136                raise Exception("You cannot access raw_post_data after reading from request's data stream")
     137            self._raw_post_data = self.read()
     138            self._stream = StringIO(self._raw_post_data)
     139        return self._raw_post_data
     140    raw_post_data = property(_get_raw_post_data)
     141
     142    def _mark_post_parse_error(self):
     143        self._post = QueryDict('')
     144        self._files = MultiValueDict()
     145        self._post_parse_error = True
     146
     147    def _load_post_and_files(self):
     148        # Populates self._post and self._files
     149        if self.method != 'POST':
     150            self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict()
     151            return
     152        if self._read_started:
     153            self._mark_post_parse_error()
     154            return
     155
     156        if self.META.get('CONTENT_TYPE', '').startswith('multipart'):
     157            self._raw_post_data = ''
     158            try:
     159                self._post, self._files = self.parse_file_upload(self.META, self)
     160            except:
     161                # An error occured while parsing POST data.  Since when
     162                # formatting the error the request handler might access
     163                # self.POST, set self._post and self._file to prevent
     164                # attempts to parse POST data again.
     165                # Mark that an error occured.  This allows self.__repr__ to
     166                # be explicit about it instead of simply representing an
     167                # empty POST
     168                self._mark_post_parse_error()
     169                raise
     170        else:
     171            self._post, self._files = QueryDict(self.raw_post_data, encoding=self._encoding), MultiValueDict()
     172
     173    ## File-like and iterator interface.
     174    ##
     175    ## Expects self._stream to be set to an appropriate source of bytes by
     176    ## a corresponding request subclass (WSGIRequest or ModPythonRequest).
     177    ## Also when request data has already been read by request.POST or
     178    ## request.raw_post_data self._stream pooints to a StringIO instance
     179    ## containing that data.
     180
     181    def read(self, *args, **kwargs):
     182        self._read_started = True
     183        return self._stream.read(*args, **kwargs)
     184
     185    def readline(self, *args, **kwargs):
     186        self._read_started = True
     187        return self._stream.readline(*args, **kwargs)
     188
     189    def xreadlines(self):
     190        while True:
     191            buf = self.readline()
     192            if not buf:
     193                break
     194            yield buf
     195    __iter__ = xreadlines
     196
     197    def readlines(self):
     198        return list(iter(self))
     199
    129200class QueryDict(MultiValueDict):
    130201    """
    131202    A specialized MultiValueDict that takes a query string when initialized.
     
    192263        for key, value in dict.items(self):
    193264            dict.__setitem__(result, copy.deepcopy(key, memo), copy.deepcopy(value, memo))
    194265        return result
    195    
     266
    196267    def setlist(self, key, list_):
    197268        self._assert_mutable()
    198269        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