~j5-dev/+junk/cherrypy3-3.2.0rc1

« back to all changes in this revision

Viewing changes to cherrypy/_cpreqbody.py

  • Committer: steveh at sjsoft
  • Date: 2010-07-01 13:07:15 UTC
  • Revision ID: steveh@sjsoft.com-20100701130715-w56oim8346qzqlka
New upstream release

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""Request body processing for CherryPy.
 
2
 
 
3
When an HTTP request includes an entity body, it is often desirable to
 
4
provide that information to applications in a form other than the raw bytes.
 
5
Different content types demand different approaches. Examples:
 
6
 
 
7
 * For a GIF file, we want the raw bytes in a stream.
 
8
 * An HTML form is better parsed into its component fields, and each text field
 
9
    decoded from bytes to unicode.
 
10
 * A JSON body should be deserialized into a Python dict or list.
 
11
 
 
12
When the request contains a Content-Type header, the media type is used as a
 
13
key to look up a value in the 'request.body.processors' dict. If the full media
 
14
type is not found, then the major type is tried; for example, if no processor
 
15
is found for the 'image/jpeg' type, then we look for a processor for the 'image'
 
16
types altogether. If neither the full type nor the major type has a matching
 
17
processor, then a default processor is used (self.default_proc). For most
 
18
types, this means no processing is done, and the body is left unread as a
 
19
raw byte stream. Processors are configurable in an 'on_start_resource' hook.
 
20
 
 
21
Some processors, especially those for the 'text' types, attempt to decode bytes
 
22
to unicode. If the Content-Type request header includes a 'charset' parameter,
 
23
this is used to decode the entity. Otherwise, one or more default charsets may
 
24
be attempted, although this decision is up to each processor. If a processor
 
25
successfully decodes an Entity or Part, it should set the 'charset' attribute
 
26
on the Entity or Part to the name of the successful charset, so that
 
27
applications can easily re-encode or transcode the value if they wish.
 
28
 
 
29
If the Content-Type of the request entity is of major type 'multipart', then
 
30
the above parsing process, and possibly a decoding process, is performed for
 
31
each part.
 
32
 
 
33
For both the full entity and multipart parts, a Content-Disposition header may
 
34
be used to fill .name and .filename attributes on the request.body or the Part.
 
35
"""
 
36
 
 
37
import re
 
38
import tempfile
 
39
from urllib import unquote_plus
 
40
 
 
41
import cherrypy
 
42
from cherrypy.lib import httputil
 
43
 
 
44
 
 
45
# -------------------------------- Processors -------------------------------- #
 
46
 
 
47
def process_urlencoded(entity):
 
48
    """Read application/x-www-form-urlencoded data into entity.params."""
 
49
    qs = entity.fp.read()
 
50
    for charset in entity.attempt_charsets:
 
51
        try:
 
52
            params = {}
 
53
            for aparam in qs.split('&'):
 
54
                for pair in aparam.split(';'):
 
55
                    if not pair:
 
56
                        continue
 
57
                    
 
58
                    atoms = pair.split('=', 1)
 
59
                    if len(atoms) == 1:
 
60
                        atoms.append('')
 
61
                    
 
62
                    key = unquote_plus(atoms[0]).decode(charset)
 
63
                    value = unquote_plus(atoms[1]).decode(charset)
 
64
                    
 
65
                    if key in params:
 
66
                        if not isinstance(params[key], list):
 
67
                            params[key] = [params[key]]
 
68
                        params[key].append(value)
 
69
                    else:
 
70
                        params[key] = value
 
71
        except UnicodeDecodeError:
 
72
            pass
 
73
        else:
 
74
            entity.charset = charset
 
75
            break
 
76
    else:
 
77
        raise cherrypy.HTTPError(
 
78
            400, "The request entity could not be decoded. The following "
 
79
            "charsets were attempted: %s" % repr(entity.attempt_charsets))
 
80
        
 
81
    # Now that all values have been successfully parsed and decoded,
 
82
    # apply them to the entity.params dict.
 
83
    for key, value in params.items():
 
84
        if key in entity.params:
 
85
            if not isinstance(entity.params[key], list):
 
86
                entity.params[key] = [entity.params[key]]
 
87
            entity.params[key].append(value)
 
88
        else:
 
89
            entity.params[key] = value
 
90
 
 
91
 
 
92
def process_multipart(entity):
 
93
    """Read all multipart parts into entity.parts."""
 
94
    ib = u""
 
95
    if u'boundary' in entity.content_type.params:
 
96
        # http://tools.ietf.org/html/rfc2046#section-5.1.1
 
97
        # "The grammar for parameters on the Content-type field is such that it
 
98
        # is often necessary to enclose the boundary parameter values in quotes
 
99
        # on the Content-type line"
 
100
        ib = entity.content_type.params['boundary'].strip(u'"')
 
101
    
 
102
    if not re.match(u"^[ -~]{0,200}[!-~]$", ib):
 
103
        raise ValueError(u'Invalid boundary in multipart form: %r' % (ib,))
 
104
    
 
105
    ib = (u'--' + ib).encode('ascii')
 
106
    
 
107
    # Find the first marker
 
108
    while True:
 
109
        b = entity.readline()
 
110
        if not b:
 
111
            return
 
112
        
 
113
        b = b.strip()
 
114
        if b == ib:
 
115
            break
 
116
    
 
117
    # Read all parts
 
118
    while True:
 
119
        part = entity.part_class.from_fp(entity.fp, ib)
 
120
        entity.parts.append(part)
 
121
        part.process()
 
122
        if part.fp.done:
 
123
            break
 
124
 
 
125
def process_multipart_form_data(entity):
 
126
    """Read all multipart/form-data parts into entity.parts or entity.params."""
 
127
    process_multipart(entity)
 
128
    
 
129
    kept_parts = []
 
130
    for part in entity.parts:
 
131
        if part.name is None:
 
132
            kept_parts.append(part)
 
133
        else:
 
134
            if part.filename is None:
 
135
                # It's a regular field
 
136
                entity.params[part.name] = part.fullvalue()
 
137
            else:
 
138
                # It's a file upload. Retain the whole part so consumer code
 
139
                # has access to its .file and .filename attributes.
 
140
                entity.params[part.name] = part
 
141
    
 
142
    entity.parts = kept_parts
 
143
 
 
144
def _old_process_multipart(entity):
 
145
    """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3."""
 
146
    process_multipart(entity)
 
147
    
 
148
    params = entity.params
 
149
    
 
150
    for part in entity.parts:
 
151
        if part.name is None:
 
152
            key = u'parts'
 
153
        else:
 
154
            key = part.name
 
155
        
 
156
        if part.filename is None:
 
157
            # It's a regular field
 
158
            value = part.fullvalue()
 
159
        else:
 
160
            # It's a file upload. Retain the whole part so consumer code
 
161
            # has access to its .file and .filename attributes.
 
162
            value = part
 
163
        
 
164
        if key in params:
 
165
            if not isinstance(params[key], list):
 
166
                params[key] = [params[key]]
 
167
            params[key].append(value)
 
168
        else:
 
169
            params[key] = value
 
170
 
 
171
 
 
172
 
 
173
# --------------------------------- Entities --------------------------------- #
 
174
 
 
175
 
 
176
class Entity(object):
 
177
    """An HTTP request body, or MIME multipart body."""
 
178
    
 
179
    __metaclass__ = cherrypy._AttributeDocstrings
 
180
    
 
181
    params = None
 
182
    params__doc = u"""
 
183
    If the request Content-Type is 'application/x-www-form-urlencoded' or
 
184
    multipart, this will be a dict of the params pulled from the entity
 
185
    body; that is, it will be the portion of request.params that come
 
186
    from the message body (sometimes called "POST params", although they
 
187
    can be sent with various HTTP method verbs). This value is set between
 
188
    the 'before_request_body' and 'before_handler' hooks (assuming that
 
189
    process_request_body is True)."""
 
190
    
 
191
    default_content_type = u'application/x-www-form-urlencoded'
 
192
    # http://tools.ietf.org/html/rfc2046#section-4.1.2:
 
193
    # "The default character set, which must be assumed in the
 
194
    # absence of a charset parameter, is US-ASCII."
 
195
    # However, many browsers send data in utf-8 with no charset.
 
196
    attempt_charsets = [u'utf-8']
 
197
    processors = {u'application/x-www-form-urlencoded': process_urlencoded,
 
198
                  u'multipart/form-data': process_multipart_form_data,
 
199
                  u'multipart': process_multipart,
 
200
                  }
 
201
    
 
202
    def __init__(self, fp, headers, params=None, parts=None):
 
203
        # Make an instance-specific copy of the class processors
 
204
        # so Tools, etc. can replace them per-request.
 
205
        self.processors = self.processors.copy()
 
206
        
 
207
        self.fp = fp
 
208
        self.headers = headers
 
209
        
 
210
        if params is None:
 
211
            params = {}
 
212
        self.params = params
 
213
        
 
214
        if parts is None:
 
215
            parts = []
 
216
        self.parts = parts
 
217
        
 
218
        # Content-Type
 
219
        self.content_type = headers.elements(u'Content-Type')
 
220
        if self.content_type:
 
221
            self.content_type = self.content_type[0]
 
222
        else:
 
223
            self.content_type = httputil.HeaderElement.from_str(
 
224
                self.default_content_type)
 
225
        
 
226
        # Copy the class 'attempt_charsets', prepending any Content-Type charset
 
227
        dec = self.content_type.params.get(u"charset", None)
 
228
        if dec:
 
229
            dec = dec.decode('ISO-8859-1')
 
230
            self.attempt_charsets = [dec] + [c for c in self.attempt_charsets
 
231
                                             if c != dec]
 
232
        else:
 
233
            self.attempt_charsets = self.attempt_charsets[:]
 
234
        
 
235
        # Length
 
236
        self.length = None
 
237
        clen = headers.get(u'Content-Length', None)
 
238
        # If Transfer-Encoding is 'chunked', ignore any Content-Length.
 
239
        if clen is not None and 'chunked' not in headers.get(u'Transfer-Encoding', ''):
 
240
            try:
 
241
                self.length = int(clen)
 
242
            except ValueError:
 
243
                pass
 
244
        
 
245
        # Content-Disposition
 
246
        self.name = None
 
247
        self.filename = None
 
248
        disp = headers.elements(u'Content-Disposition')
 
249
        if disp:
 
250
            disp = disp[0]
 
251
            if 'name' in disp.params:
 
252
                self.name = disp.params['name']
 
253
                if self.name.startswith(u'"') and self.name.endswith(u'"'):
 
254
                    self.name = self.name[1:-1]
 
255
            if 'filename' in disp.params:
 
256
                self.filename = disp.params['filename']
 
257
                if self.filename.startswith(u'"') and self.filename.endswith(u'"'):
 
258
                    self.filename = self.filename[1:-1]
 
259
    
 
260
    # The 'type' attribute is deprecated in 3.2; remove it in 3.3.
 
261
    type = property(lambda self: self.content_type)
 
262
    
 
263
    def read(self, size=None, fp_out=None):
 
264
        return self.fp.read(size, fp_out)
 
265
    
 
266
    def readline(self, size=None):
 
267
        return self.fp.readline(size)
 
268
    
 
269
    def readlines(self, sizehint=None):
 
270
        return self.fp.readlines(sizehint)
 
271
    
 
272
    def __iter__(self):
 
273
        return self
 
274
    
 
275
    def next(self):
 
276
        line = self.readline()
 
277
        if not line:
 
278
            raise StopIteration
 
279
        return line
 
280
    
 
281
    def read_into_file(self, fp_out=None):
 
282
        """Read the request body into fp_out (or make_file() if None). Return fp_out."""
 
283
        if fp_out is None:
 
284
            fp_out = self.make_file()
 
285
        self.read(fp_out=fp_out)
 
286
        return fp_out
 
287
    
 
288
    def make_file(self):
 
289
        """Return a file into which the request body will be read.
 
290
        
 
291
        By default, this will return a TemporaryFile. Override as needed."""
 
292
        return tempfile.TemporaryFile()
 
293
    
 
294
    def fullvalue(self):
 
295
        """Return this entity as a string, whether stored in a file or not."""
 
296
        if self.file:
 
297
            # It was stored in a tempfile. Read it.
 
298
            self.file.seek(0)
 
299
            value = self.file.read()
 
300
            self.file.seek(0)
 
301
        else:
 
302
            value = self.value
 
303
        return value
 
304
    
 
305
    def process(self):
 
306
        """Execute the best-match processor for the given media type."""
 
307
        proc = None
 
308
        ct = self.content_type.value
 
309
        try:
 
310
            proc = self.processors[ct]
 
311
        except KeyError:
 
312
            toptype = ct.split(u'/', 1)[0]
 
313
            try:
 
314
                proc = self.processors[toptype]
 
315
            except KeyError:
 
316
                pass
 
317
        if proc is None:
 
318
            self.default_proc()
 
319
        else:
 
320
            proc(self)
 
321
    
 
322
    def default_proc(self):
 
323
        # Leave the fp alone for someone else to read. This works fine
 
324
        # for request.body, but the Part subclasses need to override this
 
325
        # so they can move on to the next part.
 
326
        pass
 
327
 
 
328
 
 
329
class Part(Entity):
 
330
    """A MIME part entity, part of a multipart entity."""
 
331
    
 
332
    default_content_type = u'text/plain'
 
333
    # "The default character set, which must be assumed in the absence of a
 
334
    # charset parameter, is US-ASCII."
 
335
    attempt_charsets = [u'us-ascii', u'utf-8']
 
336
    # This is the default in stdlib cgi. We may want to increase it.
 
337
    maxrambytes = 1000
 
338
    
 
339
    def __init__(self, fp, headers, boundary):
 
340
        Entity.__init__(self, fp, headers)
 
341
        self.boundary = boundary
 
342
        self.file = None
 
343
        self.value = None
 
344
    
 
345
    def from_fp(cls, fp, boundary):
 
346
        headers = cls.read_headers(fp)
 
347
        return cls(fp, headers, boundary)
 
348
    from_fp = classmethod(from_fp)
 
349
    
 
350
    def read_headers(cls, fp):
 
351
        headers = httputil.HeaderMap()
 
352
        while True:
 
353
            line = fp.readline()
 
354
            if not line:
 
355
                # No more data--illegal end of headers
 
356
                raise EOFError(u"Illegal end of headers.")
 
357
            
 
358
            if line == '\r\n':
 
359
                # Normal end of headers
 
360
                break
 
361
            if not line.endswith('\r\n'):
 
362
                raise ValueError(u"MIME requires CRLF terminators: %r" % line)
 
363
            
 
364
            if line[0] in ' \t':
 
365
                # It's a continuation line.
 
366
                v = line.strip().decode(u'ISO-8859-1')
 
367
            else:
 
368
                k, v = line.split(":", 1)
 
369
                k = k.strip().decode(u'ISO-8859-1')
 
370
                v = v.strip().decode(u'ISO-8859-1')
 
371
            
 
372
            existing = headers.get(k)
 
373
            if existing:
 
374
                v = u", ".join((existing, v))
 
375
            headers[k] = v
 
376
        
 
377
        return headers
 
378
    read_headers = classmethod(read_headers)
 
379
    
 
380
    def read_lines_to_boundary(self, fp_out=None):
 
381
        """Read bytes from self.fp and return or write them to a file.
 
382
        
 
383
        If the 'fp_out' argument is None (the default), all bytes read are
 
384
        returned in a single byte string.
 
385
        
 
386
        If the 'fp_out' argument is not None, it must be a file-like object that
 
387
        supports the 'write' method; all bytes read will be written to the fp,
 
388
        and that fp is returned.
 
389
        """
 
390
        endmarker = self.boundary + "--"
 
391
        delim = ""
 
392
        prev_lf = True
 
393
        lines = []
 
394
        seen = 0
 
395
        while True:
 
396
            line = self.fp.readline(1<<16)
 
397
            if not line:
 
398
                raise EOFError(u"Illegal end of multipart body.")
 
399
            if line.startswith("--") and prev_lf:
 
400
                strippedline = line.strip()
 
401
                if strippedline == self.boundary:
 
402
                    break
 
403
                if strippedline == endmarker:
 
404
                    self.fp.finish()
 
405
                    break
 
406
            
 
407
            line = delim + line
 
408
            
 
409
            if line.endswith("\r\n"):
 
410
                delim = "\r\n"
 
411
                line = line[:-2]
 
412
                prev_lf = True
 
413
            elif line.endswith("\n"):
 
414
                delim = "\n"
 
415
                line = line[:-1]
 
416
                prev_lf = True
 
417
            else:
 
418
                delim = ""
 
419
                prev_lf = False
 
420
            
 
421
            if fp_out is None:
 
422
                lines.append(line)
 
423
                seen += len(line)
 
424
                if seen > self.maxrambytes:
 
425
                    fp_out = self.make_file()
 
426
                    for line in lines:
 
427
                        fp_out.write(line)
 
428
            else:
 
429
                fp_out.write(line)
 
430
        
 
431
        if fp_out is None:
 
432
            result = ''.join(lines)
 
433
            for charset in self.attempt_charsets:
 
434
                try:
 
435
                    result = result.decode(charset)
 
436
                except UnicodeDecodeError:
 
437
                    pass
 
438
                else:
 
439
                    self.charset = charset
 
440
                    return result
 
441
            else:
 
442
                raise cherrypy.HTTPError(
 
443
                    400, "The request entity could not be decoded. The following "
 
444
                    "charsets were attempted: %s" % repr(self.attempt_charsets))
 
445
        else:
 
446
            fp_out.seek(0)
 
447
            return fp_out
 
448
    
 
449
    def default_proc(self):
 
450
        if self.filename:
 
451
            # Always read into a file if a .filename was given.
 
452
            self.file = self.read_into_file()
 
453
        else:
 
454
            result = self.read_lines_to_boundary()
 
455
            if isinstance(result, basestring):
 
456
                self.value = result
 
457
            else:
 
458
                self.file = result
 
459
    
 
460
    def read_into_file(self, fp_out=None):
 
461
        """Read the request body into fp_out (or make_file() if None). Return fp_out."""
 
462
        if fp_out is None:
 
463
            fp_out = self.make_file()
 
464
        self.read_lines_to_boundary(fp_out=fp_out)
 
465
        return fp_out
 
466
 
 
467
Entity.part_class = Part
 
468
 
 
469
 
 
470
class Infinity(object):
 
471
    def __cmp__(self, other):
 
472
        return 1
 
473
    def __sub__(self, other):
 
474
        return self
 
475
inf = Infinity()
 
476
 
 
477
 
 
478
comma_separated_headers = ['Accept', 'Accept-Charset', 'Accept-Encoding',
 
479
    'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control', 'Connection',
 
480
    'Content-Encoding', 'Content-Language', 'Expect', 'If-Match',
 
481
    'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'Te', 'Trailer',
 
482
    'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning', 'Www-Authenticate']
 
483
 
 
484
 
 
485
class SizedReader:
 
486
    
 
487
    def __init__(self, fp, length, maxbytes, bufsize=8192, has_trailers=False):
 
488
        # Wrap our fp in a buffer so peek() works
 
489
        self.fp = fp
 
490
        self.length = length
 
491
        self.maxbytes = maxbytes
 
492
        self.buffer = ''
 
493
        self.bufsize = bufsize
 
494
        self.bytes_read = 0
 
495
        self.done = False
 
496
        self.has_trailers = has_trailers
 
497
    
 
498
    def read(self, size=None, fp_out=None):
 
499
        """Read bytes from the request body and return or write them to a file.
 
500
        
 
501
        A number of bytes less than or equal to the 'size' argument are read
 
502
        off the socket. The actual number of bytes read are tracked in
 
503
        self.bytes_read. The number may be smaller than 'size' when 1) the
 
504
        client sends fewer bytes, 2) the 'Content-Length' request header
 
505
        specifies fewer bytes than requested, or 3) the number of bytes read
 
506
        exceeds self.maxbytes (in which case, 413 is raised).
 
507
        
 
508
        If the 'fp_out' argument is None (the default), all bytes read are
 
509
        returned in a single byte string.
 
510
        
 
511
        If the 'fp_out' argument is not None, it must be a file-like object that
 
512
        supports the 'write' method; all bytes read will be written to the fp,
 
513
        and None is returned.
 
514
        """
 
515
        
 
516
        if self.length is None:
 
517
            if size is None:
 
518
                remaining = inf
 
519
            else:
 
520
                remaining = size
 
521
        else:
 
522
            remaining = self.length - self.bytes_read
 
523
            if size and size < remaining:
 
524
                remaining = size
 
525
        if remaining == 0:
 
526
            self.finish()
 
527
            if fp_out is None:
 
528
                return ''
 
529
            else:
 
530
                return None
 
531
        
 
532
        chunks = []
 
533
        
 
534
        # Read bytes from the buffer.
 
535
        if self.buffer:
 
536
            if remaining is inf:
 
537
                data = self.buffer
 
538
                self.buffer = ''
 
539
            else:
 
540
                data = self.buffer[:remaining]
 
541
                self.buffer = self.buffer[remaining:]
 
542
            datalen = len(data)
 
543
            remaining -= datalen
 
544
            
 
545
            # Check lengths.
 
546
            self.bytes_read += datalen
 
547
            if self.maxbytes and self.bytes_read > self.maxbytes:
 
548
                raise cherrypy.HTTPError(413)
 
549
            
 
550
            # Store the data.
 
551
            if fp_out is None:
 
552
                chunks.append(data)
 
553
            else:
 
554
                fp_out.write(data)
 
555
        
 
556
        # Read bytes from the socket.
 
557
        while remaining > 0:
 
558
            chunksize = min(remaining, self.bufsize)
 
559
            try:
 
560
                data = self.fp.read(chunksize)
 
561
            except Exception, e:
 
562
                if e.__class__.__name__ == 'MaxSizeExceeded':
 
563
                    # Post data is too big
 
564
                    raise cherrypy.HTTPError(
 
565
                        413, "Maximum request length: %r" % e.args[1])
 
566
                else:
 
567
                    raise
 
568
            if not data:
 
569
                self.finish()
 
570
                break
 
571
            datalen = len(data)
 
572
            remaining -= datalen
 
573
            
 
574
            # Check lengths.
 
575
            self.bytes_read += datalen
 
576
            if self.maxbytes and self.bytes_read > self.maxbytes:
 
577
                raise cherrypy.HTTPError(413)
 
578
            
 
579
            # Store the data.
 
580
            if fp_out is None:
 
581
                chunks.append(data)
 
582
            else:
 
583
                fp_out.write(data)
 
584
        
 
585
        if fp_out is None:
 
586
            return ''.join(chunks)
 
587
    
 
588
    def readline(self, size=None):
 
589
        """Read a line from the request body and return it."""
 
590
        chunks = []
 
591
        while size is None or size > 0:
 
592
            chunksize = self.bufsize
 
593
            if size is not None and size < self.bufsize:
 
594
                chunksize = size
 
595
            data = self.read(chunksize)
 
596
            if not data:
 
597
                break
 
598
            pos = data.find('\n') + 1
 
599
            if pos:
 
600
                chunks.append(data[:pos])
 
601
                remainder = data[pos:]
 
602
                self.buffer += remainder
 
603
                self.bytes_read -= len(remainder)
 
604
                break
 
605
            else:
 
606
                chunks.append(data)
 
607
        return ''.join(chunks)
 
608
    
 
609
    def readlines(self, sizehint=None):
 
610
        """Read lines from the request body and return them."""
 
611
        if self.length is not None:
 
612
            if sizehint is None:
 
613
                sizehint = self.length - self.bytes_read
 
614
            else:
 
615
                sizehint = min(sizehint, self.length - self.bytes_read)
 
616
        
 
617
        lines = []
 
618
        seen = 0
 
619
        while True:
 
620
            line = self.readline()
 
621
            if not line:
 
622
                break
 
623
            lines.append(line)
 
624
            seen += len(line)
 
625
            if seen >= sizehint:
 
626
                break
 
627
        return lines
 
628
    
 
629
    def finish(self):
 
630
        self.done = True
 
631
        if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
 
632
            self.trailers = {}
 
633
            
 
634
            try:
 
635
                for line in self.fp.read_trailer_lines():
 
636
                    if line[0] in ' \t':
 
637
                        # It's a continuation line.
 
638
                        v = line.strip()
 
639
                    else:
 
640
                        try:
 
641
                            k, v = line.split(":", 1)
 
642
                        except ValueError:
 
643
                            raise ValueError("Illegal header line.")
 
644
                        k = k.strip().title()
 
645
                        v = v.strip()
 
646
                    
 
647
                    if k in comma_separated_headers:
 
648
                        existing = self.trailers.get(envname)
 
649
                        if existing:
 
650
                            v = ", ".join((existing, v))
 
651
                    self.trailers[k] = v
 
652
            except Exception, e:
 
653
                if e.__class__.__name__ == 'MaxSizeExceeded':
 
654
                    # Post data is too big
 
655
                    raise cherrypy.HTTPError(
 
656
                        413, "Maximum request length: %r" % e.args[1])
 
657
                else:
 
658
                    raise
 
659
 
 
660
 
 
661
class RequestBody(Entity):
 
662
    
 
663
    # Don't parse the request body at all if the client didn't provide
 
664
    # a Content-Type header. See http://www.cherrypy.org/ticket/790
 
665
    default_content_type = u''
 
666
    
 
667
    bufsize = 8 * 1024
 
668
    maxbytes = None
 
669
    
 
670
    def __init__(self, fp, headers, params=None, request_params=None):
 
671
        Entity.__init__(self, fp, headers, params)
 
672
        
 
673
        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
 
674
        # When no explicit charset parameter is provided by the
 
675
        # sender, media subtypes of the "text" type are defined
 
676
        # to have a default charset value of "ISO-8859-1" when
 
677
        # received via HTTP.
 
678
        if self.content_type.value.startswith('text/'):
 
679
            for c in (u'ISO-8859-1', u'iso-8859-1', u'Latin-1', u'latin-1'):
 
680
                if c in self.attempt_charsets:
 
681
                    break
 
682
            else:
 
683
                self.attempt_charsets.append(u'ISO-8859-1')
 
684
        
 
685
        # Temporary fix while deprecating passing .parts as .params.
 
686
        self.processors[u'multipart'] = _old_process_multipart
 
687
        
 
688
        if request_params is None:
 
689
            request_params = {}
 
690
        self.request_params = request_params
 
691
    
 
692
    def process(self):
 
693
        """Include body params in request params."""
 
694
        # "The presence of a message-body in a request is signaled by the
 
695
        # inclusion of a Content-Length or Transfer-Encoding header field in
 
696
        # the request's message-headers."
 
697
        # It is possible to send a POST request with no body, for example;
 
698
        # however, app developers are responsible in that case to set
 
699
        # cherrypy.request.process_body to False so this method isn't called.
 
700
        h = cherrypy.serving.request.headers
 
701
        if u'Content-Length' not in h and u'Transfer-Encoding' not in h:
 
702
            raise cherrypy.HTTPError(411)
 
703
        
 
704
        self.fp = SizedReader(self.fp, self.length,
 
705
                              self.maxbytes, bufsize=self.bufsize,
 
706
                              has_trailers='Trailer' in h)
 
707
        super(RequestBody, self).process()
 
708
        
 
709
        # Body params should also be a part of the request_params
 
710
        # add them in here.
 
711
        request_params = self.request_params
 
712
        for key, value in self.params.items():
 
713
            # Python 2 only: keyword arguments must be byte strings (type 'str').
 
714
            if isinstance(key, unicode):
 
715
                key = key.encode('ISO-8859-1')
 
716
            
 
717
            if key in request_params:
 
718
                if not isinstance(request_params[key], list):
 
719
                    request_params[key] = [request_params[key]]
 
720
                request_params[key].append(value)
 
721
            else:
 
722
                request_params[key] = value
 
723