Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/webob/request.py : 24%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import binascii
2import io
3import os
4import re
5import sys
6import tempfile
7import mimetypes
8try:
9 import simplejson as json
10except ImportError:
11 import json
12import warnings
14from webob.acceptparse import (
15 accept_charset_property,
16 accept_encoding_property,
17 accept_language_property,
18 accept_property,
19 )
21from webob.cachecontrol import (
22 CacheControl,
23 serialize_cache_control,
24 )
26from webob.compat import (
27 PY2,
28 bytes_,
29 native_,
30 parse_qsl_text,
31 reraise,
32 text_type,
33 url_encode,
34 url_quote,
35 url_unquote,
36 quote_plus,
37 urlparse,
38 cgi_FieldStorage
39 )
41from webob.cookies import RequestCookies
43from webob.descriptors import (
44 CHARSET_RE,
45 SCHEME_RE,
46 converter,
47 converter_date,
48 environ_getter,
49 environ_decoder,
50 parse_auth,
51 parse_int,
52 parse_int_safe,
53 parse_range,
54 serialize_auth,
55 serialize_if_range,
56 serialize_int,
57 serialize_range,
58 upath_property,
59 )
61from webob.etag import (
62 IfRange,
63 AnyETag,
64 NoETag,
65 etag_property,
66 )
68from webob.headers import EnvironHeaders
70from webob.multidict import (
71 NestedMultiDict,
72 MultiDict,
73 NoVars,
74 GetDict,
75 )
77__all__ = ['BaseRequest', 'Request', 'LegacyRequest']
79class _NoDefault:
80 def __repr__(self):
81 return '(No Default)'
82NoDefault = _NoDefault()
84PATH_SAFE = "/~!$&'()*+,;=:@"
86_LATIN_ENCODINGS = (
87 'ascii', 'latin-1', 'latin', 'latin_1', 'l1', 'latin1',
88 'iso-8859-1', 'iso8859_1', 'iso_8859_1', 'iso8859', '8859',
89 )
91class BaseRequest(object):
92 # The limit after which request bodies should be stored on disk
93 # if they are read in (under this, and the request body is stored
94 # in memory):
95 request_body_tempfile_limit = 10 * 1024
97 _charset = None
99 def __init__(self, environ, charset=None, unicode_errors=None,
100 decode_param_names=None, **kw):
102 if type(environ) is not dict:
103 raise TypeError(
104 "WSGI environ must be a dict; you passed %r" % (environ,))
106 if unicode_errors is not None:
107 warnings.warn(
108 "You unicode_errors=%r to the Request constructor. Passing a "
109 "``unicode_errors`` value to the Request is no longer "
110 "supported in WebOb 1.2+. This value has been ignored " % (
111 unicode_errors,),
112 DeprecationWarning
113 )
115 if decode_param_names is not None:
116 warnings.warn(
117 "You passed decode_param_names=%r to the Request constructor. "
118 "Passing a ``decode_param_names`` value to the Request "
119 "is no longer supported in WebOb 1.2+. This value has "
120 "been ignored " % (decode_param_names,),
121 DeprecationWarning
122 )
124 if not _is_utf8(charset):
125 raise DeprecationWarning(
126 "You passed charset=%r to the Request constructor. As of "
127 "WebOb 1.2, if your application needs a non-UTF-8 request "
128 "charset, please construct the request without a charset or "
129 "with a charset of 'None', then use ``req = "
130 "req.decode(charset)``" % charset
131 )
133 d = self.__dict__
134 d['environ'] = environ
136 if kw:
137 cls = self.__class__
139 if 'method' in kw:
140 # set method first, because .body setters
141 # depend on it for checks
142 self.method = kw.pop('method')
144 for name, value in kw.items():
145 if not hasattr(cls, name):
146 raise TypeError(
147 "Unexpected keyword: %s=%r" % (name, value))
148 setattr(self, name, value)
150 def encget(self, key, default=NoDefault, encattr=None):
151 val = self.environ.get(key, default)
152 if val is NoDefault:
153 raise KeyError(key)
154 if val is default:
155 return default
156 if not encattr:
157 return val
158 encoding = getattr(self, encattr)
160 if PY2:
161 return val.decode(encoding)
163 if encoding in _LATIN_ENCODINGS: # shortcut
164 return val
165 return bytes_(val, 'latin-1').decode(encoding)
167 def encset(self, key, val, encattr=None):
168 if encattr:
169 encoding = getattr(self, encattr)
170 else:
171 encoding = 'ascii'
172 if PY2: # pragma: no cover
173 self.environ[key] = bytes_(val, encoding)
174 else:
175 self.environ[key] = bytes_(val, encoding).decode('latin-1')
177 @property
178 def charset(self):
179 if self._charset is None:
180 charset = detect_charset(self._content_type_raw)
181 if _is_utf8(charset):
182 charset = 'UTF-8'
183 self._charset = charset
184 return self._charset
186 @charset.setter
187 def charset(self, charset):
188 if _is_utf8(charset):
189 charset = 'UTF-8'
190 if charset != self.charset:
191 raise DeprecationWarning("Use req = req.decode(%r)" % charset)
193 def decode(self, charset=None, errors='strict'):
194 charset = charset or self.charset
195 if charset == 'UTF-8':
196 return self
197 # cookies and path are always utf-8
198 t = Transcoder(charset, errors)
200 new_content_type = CHARSET_RE.sub('; charset="UTF-8"',
201 self._content_type_raw)
202 content_type = self.content_type
203 r = self.__class__(
204 self.environ.copy(),
205 query_string=t.transcode_query(self.query_string),
206 content_type=new_content_type,
207 )
209 if content_type == 'application/x-www-form-urlencoded':
210 r.body = bytes_(t.transcode_query(native_(self.body)))
211 return r
212 elif content_type != 'multipart/form-data':
213 return r
215 fs_environ = self.environ.copy()
216 fs_environ.setdefault('CONTENT_LENGTH', '0')
217 fs_environ['QUERY_STRING'] = ''
218 if PY2:
219 fs = cgi_FieldStorage(fp=self.body_file,
220 environ=fs_environ,
221 keep_blank_values=True)
222 else:
223 fs = cgi_FieldStorage(fp=self.body_file,
224 environ=fs_environ,
225 keep_blank_values=True,
226 encoding=charset,
227 errors=errors)
229 fout = t.transcode_fs(fs, r._content_type_raw)
231 # this order is important, because setting body_file
232 # resets content_length
233 r.body_file = fout
234 r.content_length = fout.tell()
235 fout.seek(0)
236 return r
238 # this is necessary for correct warnings depth for both
239 # BaseRequest and Request (due to AdhocAttrMixin.__setattr__)
240 _setattr_stacklevel = 2
242 @property
243 def body_file(self):
244 """
245 Input stream of the request (wsgi.input).
246 Setting this property resets the content_length and seekable flag
247 (unlike setting req.body_file_raw).
248 """
250 if not self.is_body_readable:
251 return io.BytesIO()
253 r = self.body_file_raw
254 clen = self.content_length
256 if not self.is_body_seekable and clen is not None:
257 # we need to wrap input in LimitedLengthFile
258 # but we have to cache the instance as well
259 # otherwise this would stop working
260 # (.remaining counter would reset between calls):
261 # req.body_file.read(100)
262 # req.body_file.read(100)
263 env = self.environ
264 wrapped, raw = env.get('webob._body_file', (0, 0))
266 if raw is not r:
267 wrapped = LimitedLengthFile(r, clen)
268 wrapped = io.BufferedReader(wrapped)
269 env['webob._body_file'] = wrapped, r
270 r = wrapped
272 return r
274 @body_file.setter
275 def body_file(self, value):
276 if isinstance(value, bytes):
277 raise ValueError('Excepted fileobj but received bytes.')
279 self.content_length = None
280 self.body_file_raw = value
281 self.is_body_seekable = False
282 self.is_body_readable = True
284 @body_file.deleter
285 def body_file(self):
286 self.body = b''
288 body_file_raw = environ_getter('wsgi.input')
290 @property
291 def body_file_seekable(self):
292 """
293 Get the body of the request (wsgi.input) as a seekable file-like
294 object. Middleware and routing applications should use this
295 attribute over .body_file.
297 If you access this value, CONTENT_LENGTH will also be updated.
298 """
299 if not self.is_body_seekable:
300 self.make_body_seekable()
301 return self.body_file_raw
303 url_encoding = environ_getter('webob.url_encoding', 'UTF-8')
304 scheme = environ_getter('wsgi.url_scheme')
305 method = environ_getter('REQUEST_METHOD', 'GET')
306 http_version = environ_getter('SERVER_PROTOCOL')
307 content_length = converter(
308 environ_getter('CONTENT_LENGTH', None, '14.13'),
309 parse_int_safe, serialize_int, 'int')
310 remote_user = environ_getter('REMOTE_USER', None)
311 remote_host = environ_getter('REMOTE_HOST', None)
312 remote_addr = environ_getter('REMOTE_ADDR', None)
313 query_string = environ_getter('QUERY_STRING', '')
314 server_name = environ_getter('SERVER_NAME')
315 server_port = converter(
316 environ_getter('SERVER_PORT'),
317 parse_int, serialize_int, 'int')
319 script_name = environ_decoder('SCRIPT_NAME', '', encattr='url_encoding')
320 path_info = environ_decoder('PATH_INFO', encattr='url_encoding')
322 # bw compat
323 uscript_name = script_name
324 upath_info = path_info
326 _content_type_raw = environ_getter('CONTENT_TYPE', '')
328 def _content_type__get(self):
329 """Return the content type, but leaving off any parameters (like
330 charset, but also things like the type in ``application/atom+xml;
331 type=entry``)
333 If you set this property, you can include parameters, or if
334 you don't include any parameters in the value then existing
335 parameters will be preserved.
336 """
337 return self._content_type_raw.split(';', 1)[0]
338 def _content_type__set(self, value=None):
339 if value is not None:
340 value = str(value)
341 if ';' not in value:
342 content_type = self._content_type_raw
343 if ';' in content_type:
344 value += ';' + content_type.split(';', 1)[1]
345 self._content_type_raw = value
347 content_type = property(_content_type__get,
348 _content_type__set,
349 _content_type__set,
350 _content_type__get.__doc__)
352 _headers = None
354 def _headers__get(self):
355 """
356 All the request headers as a case-insensitive dictionary-like
357 object.
358 """
359 if self._headers is None:
360 self._headers = EnvironHeaders(self.environ)
361 return self._headers
363 def _headers__set(self, value):
364 self.headers.clear()
365 self.headers.update(value)
367 headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
369 @property
370 def client_addr(self):
371 """
372 The effective client IP address as a string. If the
373 ``HTTP_X_FORWARDED_FOR`` header exists in the WSGI environ, this
374 attribute returns the client IP address present in that header
375 (e.g. if the header value is ``192.168.1.1, 192.168.1.2``, the value
376 will be ``192.168.1.1``). If no ``HTTP_X_FORWARDED_FOR`` header is
377 present in the environ at all, this attribute will return the value
378 of the ``REMOTE_ADDR`` header. If the ``REMOTE_ADDR`` header is
379 unset, this attribute will return the value ``None``.
381 .. warning::
383 It is possible for user agents to put someone else's IP or just
384 any string in ``HTTP_X_FORWARDED_FOR`` as it is a normal HTTP
385 header. Forward proxies can also provide incorrect values (private
386 IP addresses etc). You cannot "blindly" trust the result of this
387 method to provide you with valid data unless you're certain that
388 ``HTTP_X_FORWARDED_FOR`` has the correct values. The WSGI server
389 must be behind a trusted proxy for this to be true.
390 """
391 e = self.environ
392 xff = e.get('HTTP_X_FORWARDED_FOR')
393 if xff is not None:
394 addr = xff.split(',')[0].strip()
395 else:
396 addr = e.get('REMOTE_ADDR')
397 return addr
399 @property
400 def host_port(self):
401 """
402 The effective server port number as a string. If the ``HTTP_HOST``
403 header exists in the WSGI environ, this attribute returns the port
404 number present in that header. If the ``HTTP_HOST`` header exists but
405 contains no explicit port number: if the WSGI url scheme is "https" ,
406 this attribute returns "443", if the WSGI url scheme is "http", this
407 attribute returns "80" . If no ``HTTP_HOST`` header is present in
408 the environ at all, this attribute will return the value of the
409 ``SERVER_PORT`` header (which is guaranteed to be present).
410 """
411 e = self.environ
412 host = e.get('HTTP_HOST')
413 if host is not None:
414 if ':' in host and host[-1] != ']':
415 host, port = host.rsplit(':', 1)
416 else:
417 url_scheme = e['wsgi.url_scheme']
418 if url_scheme == 'https':
419 port = '443'
420 else:
421 port = '80'
422 else:
423 port = e['SERVER_PORT']
424 return port
426 @property
427 def host_url(self):
428 """
429 The URL through the host (no path)
430 """
431 e = self.environ
432 scheme = e.get('wsgi.url_scheme')
433 url = scheme + '://'
434 host = e.get('HTTP_HOST')
435 if host is not None:
436 if ':' in host and host[-1] != ']':
437 host, port = host.rsplit(':', 1)
438 else:
439 port = None
440 else:
441 host = e.get('SERVER_NAME')
442 port = e.get('SERVER_PORT')
443 if scheme == 'https':
444 if port == '443':
445 port = None
446 elif scheme == 'http':
447 if port == '80':
448 port = None
449 url += host
450 if port:
451 url += ':%s' % port
452 return url
454 @property
455 def application_url(self):
456 """
457 The URL including SCRIPT_NAME (no PATH_INFO or query string)
458 """
459 bscript_name = bytes_(self.script_name, self.url_encoding)
460 return self.host_url + url_quote(bscript_name, PATH_SAFE)
462 @property
463 def path_url(self):
464 """
465 The URL including SCRIPT_NAME and PATH_INFO, but not QUERY_STRING
466 """
467 bpath_info = bytes_(self.path_info, self.url_encoding)
468 return self.application_url + url_quote(bpath_info, PATH_SAFE)
470 @property
471 def path(self):
472 """
473 The path of the request, without host or query string
474 """
475 bscript = bytes_(self.script_name, self.url_encoding)
476 bpath = bytes_(self.path_info, self.url_encoding)
477 return url_quote(bscript, PATH_SAFE) + url_quote(bpath, PATH_SAFE)
479 @property
480 def path_qs(self):
481 """
482 The path of the request, without host but with query string
483 """
484 path = self.path
485 qs = self.environ.get('QUERY_STRING')
486 if qs:
487 path += '?' + qs
488 return path
490 @property
491 def url(self):
492 """
493 The full request URL, including QUERY_STRING
494 """
495 url = self.path_url
496 qs = self.environ.get('QUERY_STRING')
497 if qs:
498 url += '?' + qs
499 return url
501 def relative_url(self, other_url, to_application=False):
502 """
503 Resolve other_url relative to the request URL.
505 If ``to_application`` is True, then resolve it relative to the
506 URL with only SCRIPT_NAME
507 """
508 if to_application:
509 url = self.application_url
510 if not url.endswith('/'):
511 url += '/'
512 else:
513 url = self.path_url
514 return urlparse.urljoin(url, other_url)
516 def path_info_pop(self, pattern=None):
517 """
518 'Pops' off the next segment of PATH_INFO, pushing it onto
519 SCRIPT_NAME, and returning the popped segment. Returns None if
520 there is nothing left on PATH_INFO.
522 Does not return ``''`` when there's an empty segment (like
523 ``/path//path``); these segments are just ignored.
525 Optional ``pattern`` argument is a regexp to match the return value
526 before returning. If there is no match, no changes are made to the
527 request and None is returned.
528 """
529 path = self.path_info
530 if not path:
531 return None
532 slashes = ''
533 while path.startswith('/'):
534 slashes += '/'
535 path = path[1:]
536 idx = path.find('/')
537 if idx == -1:
538 idx = len(path)
539 r = path[:idx]
540 if pattern is None or re.match(pattern, r):
541 self.script_name += slashes + r
542 self.path_info = path[idx:]
543 return r
545 def path_info_peek(self):
546 """
547 Returns the next segment on PATH_INFO, or None if there is no
548 next segment. Doesn't modify the environment.
549 """
550 path = self.path_info
551 if not path:
552 return None
553 path = path.lstrip('/')
554 return path.split('/', 1)[0]
556 def _urlvars__get(self):
557 """
558 Return any *named* variables matched in the URL.
560 Takes values from ``environ['wsgiorg.routing_args']``.
561 Systems like ``routes`` set this value.
562 """
563 if 'paste.urlvars' in self.environ:
564 return self.environ['paste.urlvars']
565 elif 'wsgiorg.routing_args' in self.environ:
566 return self.environ['wsgiorg.routing_args'][1]
567 else:
568 result = {}
569 self.environ['wsgiorg.routing_args'] = ((), result)
570 return result
572 def _urlvars__set(self, value):
573 environ = self.environ
574 if 'wsgiorg.routing_args' in environ:
575 environ['wsgiorg.routing_args'] = (
576 environ['wsgiorg.routing_args'][0], value)
577 if 'paste.urlvars' in environ:
578 del environ['paste.urlvars']
579 elif 'paste.urlvars' in environ:
580 environ['paste.urlvars'] = value
581 else:
582 environ['wsgiorg.routing_args'] = ((), value)
584 def _urlvars__del(self):
585 if 'paste.urlvars' in self.environ:
586 del self.environ['paste.urlvars']
587 if 'wsgiorg.routing_args' in self.environ:
588 if not self.environ['wsgiorg.routing_args'][0]:
589 del self.environ['wsgiorg.routing_args']
590 else:
591 self.environ['wsgiorg.routing_args'] = (
592 self.environ['wsgiorg.routing_args'][0], {})
594 urlvars = property(_urlvars__get,
595 _urlvars__set,
596 _urlvars__del,
597 doc=_urlvars__get.__doc__)
599 def _urlargs__get(self):
600 """
601 Return any *positional* variables matched in the URL.
603 Takes values from ``environ['wsgiorg.routing_args']``.
604 Systems like ``routes`` set this value.
605 """
606 if 'wsgiorg.routing_args' in self.environ:
607 return self.environ['wsgiorg.routing_args'][0]
608 else:
609 # Since you can't update this value in-place, we don't need
610 # to set the key in the environment
611 return ()
613 def _urlargs__set(self, value):
614 environ = self.environ
615 if 'paste.urlvars' in environ:
616 # Some overlap between this and wsgiorg.routing_args; we need
617 # wsgiorg.routing_args to make this work
618 routing_args = (value, environ.pop('paste.urlvars'))
619 elif 'wsgiorg.routing_args' in environ:
620 routing_args = (value, environ['wsgiorg.routing_args'][1])
621 else:
622 routing_args = (value, {})
623 environ['wsgiorg.routing_args'] = routing_args
625 def _urlargs__del(self):
626 if 'wsgiorg.routing_args' in self.environ:
627 if not self.environ['wsgiorg.routing_args'][1]:
628 del self.environ['wsgiorg.routing_args']
629 else:
630 self.environ['wsgiorg.routing_args'] = (
631 (), self.environ['wsgiorg.routing_args'][1])
633 urlargs = property(_urlargs__get,
634 _urlargs__set,
635 _urlargs__del,
636 _urlargs__get.__doc__)
638 @property
639 def is_xhr(self):
640 """Is X-Requested-With header present and equal to ``XMLHttpRequest``?
642 Note: this isn't set by every XMLHttpRequest request, it is
643 only set if you are using a Javascript library that sets it
644 (or you set the header yourself manually). Currently
645 Prototype and jQuery are known to set this header."""
646 return self.environ.get('HTTP_X_REQUESTED_WITH', '') == 'XMLHttpRequest'
648 def _host__get(self):
649 """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME"""
650 if 'HTTP_HOST' in self.environ:
651 return self.environ['HTTP_HOST']
652 else:
653 return '%(SERVER_NAME)s:%(SERVER_PORT)s' % self.environ
654 def _host__set(self, value):
655 self.environ['HTTP_HOST'] = value
656 def _host__del(self):
657 if 'HTTP_HOST' in self.environ:
658 del self.environ['HTTP_HOST']
659 host = property(_host__get, _host__set, _host__del, doc=_host__get.__doc__)
661 @property
662 def domain(self):
663 """ Returns the domain portion of the host value. Equivalent to:
665 .. code-block:: python
667 domain = request.host
668 if ':' in domain and domain[-1] != ']': # Check for ] because of IPv6
669 domain = domain.rsplit(':', 1)[0]
671 This will be equivalent to the domain portion of the ``HTTP_HOST``
672 value in the environment if it exists, or the ``SERVER_NAME`` value in
673 the environment if it doesn't. For example, if the environment
674 contains an ``HTTP_HOST`` value of ``foo.example.com:8000``,
675 ``request.domain`` will return ``foo.example.com``.
677 Note that this value cannot be *set* on the request. To set the host
678 value use :meth:`webob.request.Request.host` instead.
679 """
680 domain = self.host
681 if ':' in domain and domain[-1] != ']':
682 domain = domain.rsplit(':', 1)[0]
683 return domain
685 @property
686 def body(self):
687 """
688 Return the content of the request body.
689 """
690 if not self.is_body_readable:
691 return b''
693 self.make_body_seekable() # we need this to have content_length
694 r = self.body_file.read(self.content_length)
695 self.body_file_raw.seek(0)
696 return r
698 @body.setter
699 def body(self, value):
700 if value is None:
701 value = b''
702 if not isinstance(value, bytes):
703 raise TypeError("You can only set Request.body to bytes (not %r)"
704 % type(value))
705 self.content_length = len(value)
706 self.body_file_raw = io.BytesIO(value)
707 self.is_body_seekable = True
709 @body.deleter
710 def body(self):
711 self.body = b''
713 def _json_body__get(self):
714 """Access the body of the request as JSON"""
715 return json.loads(self.body.decode(self.charset))
717 def _json_body__set(self, value):
718 self.body = json.dumps(value, separators=(',', ':')).encode(self.charset)
720 def _json_body__del(self):
721 del self.body
723 json = json_body = property(_json_body__get, _json_body__set, _json_body__del)
725 def _text__get(self):
726 """
727 Get/set the text value of the body
728 """
729 if not self.charset:
730 raise AttributeError(
731 "You cannot access Request.text unless charset is set")
732 body = self.body
733 return body.decode(self.charset)
735 def _text__set(self, value):
736 if not self.charset:
737 raise AttributeError(
738 "You cannot access Response.text unless charset is set")
739 if not isinstance(value, text_type):
740 raise TypeError(
741 "You can only set Request.text to a unicode string "
742 "(not %s)" % type(value))
743 self.body = value.encode(self.charset)
745 def _text__del(self):
746 del self.body
748 text = property(_text__get, _text__set, _text__del, doc=_text__get.__doc__)
750 @property
751 def POST(self):
752 """
753 Return a MultiDict containing all the variables from a form
754 request. Returns an empty dict-like object for non-form requests.
756 Form requests are typically POST requests, however any other
757 requests with an appropriate Content-Type are also supported.
758 """
759 env = self.environ
760 if 'webob._parsed_post_vars' in env:
761 vars, body_file = env['webob._parsed_post_vars']
762 if body_file is self.body_file_raw:
763 return vars
764 content_type = self.content_type
765 if ((self.method != 'POST' and not content_type)
766 or content_type not in
767 ('',
768 'application/x-www-form-urlencoded',
769 'multipart/form-data')
770 ):
771 # Not an HTML form submission
772 return NoVars('Not an HTML form submission (Content-Type: %s)'
773 % content_type)
774 self._check_charset()
776 self.make_body_seekable()
777 self.body_file_raw.seek(0)
779 fs_environ = env.copy()
780 # FieldStorage assumes a missing CONTENT_LENGTH, but a
781 # default of 0 is better:
782 fs_environ.setdefault('CONTENT_LENGTH', '0')
783 fs_environ['QUERY_STRING'] = ''
784 if PY2:
785 fs = cgi_FieldStorage(
786 fp=self.body_file,
787 environ=fs_environ,
788 keep_blank_values=True)
789 else:
790 fs = cgi_FieldStorage(
791 fp=self.body_file,
792 environ=fs_environ,
793 keep_blank_values=True,
794 encoding='utf8')
796 vars = MultiDict.from_fieldstorage(fs)
797 env['webob._parsed_post_vars'] = (vars, self.body_file_raw)
798 return vars
800 @property
801 def GET(self):
802 """
803 Return a MultiDict containing all the variables from the
804 QUERY_STRING.
805 """
806 env = self.environ
807 source = env.get('QUERY_STRING', '')
808 if 'webob._parsed_query_vars' in env:
809 vars, qs = env['webob._parsed_query_vars']
810 if qs == source:
811 return vars
813 data = []
814 if source:
815 # this is disabled because we want to access req.GET
816 # for text/plain; charset=ascii uploads for example
817 #self._check_charset()
818 data = parse_qsl_text(source)
819 #d = lambda b: b.decode('utf8')
820 #data = [(d(k), d(v)) for k,v in data]
821 vars = GetDict(data, env)
822 env['webob._parsed_query_vars'] = (vars, source)
823 return vars
825 def _check_charset(self):
826 if self.charset != 'UTF-8':
827 raise DeprecationWarning(
828 "Requests are expected to be submitted in UTF-8, not %s. "
829 "You can fix this by doing req = req.decode('%s')" % (
830 self.charset, self.charset)
831 )
833 @property
834 def params(self):
835 """
836 A dictionary-like object containing both the parameters from
837 the query string and request body.
838 """
839 params = NestedMultiDict(self.GET, self.POST)
840 return params
842 @property
843 def cookies(self):
844 """
845 Return a dictionary of cookies as found in the request.
846 """
847 return RequestCookies(self.environ)
849 @cookies.setter
850 def cookies(self, val):
851 self.environ.pop('HTTP_COOKIE', None)
852 r = RequestCookies(self.environ)
853 r.update(val)
855 def copy(self):
856 """
857 Copy the request and environment object.
859 This only does a shallow copy, except of wsgi.input
860 """
861 self.make_body_seekable()
862 env = self.environ.copy()
863 new_req = self.__class__(env)
864 new_req.copy_body()
865 return new_req
867 def copy_get(self):
868 """
869 Copies the request and environment object, but turning this request
870 into a GET along the way. If this was a POST request (or any other
871 verb) then it becomes GET, and the request body is thrown away.
872 """
873 env = self.environ.copy()
874 return self.__class__(env, method='GET', content_type=None,
875 body=b'')
877 # webob.is_body_seekable marks input streams that are seekable
878 # this way we can have seekable input without testing the .seek() method
879 is_body_seekable = environ_getter('webob.is_body_seekable', False)
881 @property
882 def is_body_readable(self):
883 """
884 webob.is_body_readable is a flag that tells us that we can read the
885 input stream even though CONTENT_LENGTH is missing.
886 """
888 clen = self.content_length
890 if clen is not None and clen != 0:
891 return True
892 elif clen is None:
893 # Rely on the special flag that signifies that either Chunked
894 # Encoding is allowed (and works) or we have replaced
895 # self.body_file with something that is readable and EOF's
896 # correctly.
897 return self.environ.get(
898 'wsgi.input_terminated',
899 # For backwards compatibility, we fall back to checking if
900 # webob.is_body_readable is set in the environ
901 self.environ.get(
902 'webob.is_body_readable',
903 False
904 )
905 )
907 return False
909 @is_body_readable.setter
910 def is_body_readable(self, flag):
911 self.environ['wsgi.input_terminated'] = bool(flag)
913 def make_body_seekable(self):
914 """
915 This forces ``environ['wsgi.input']`` to be seekable.
916 That means that, the content is copied into a BytesIO or temporary
917 file and flagged as seekable, so that it will not be unnecessarily
918 copied again.
920 After calling this method the .body_file is always seeked to the
921 start of file and .content_length is not None.
923 The choice to copy to BytesIO is made from
924 ``self.request_body_tempfile_limit``
925 """
926 if self.is_body_seekable:
927 self.body_file_raw.seek(0)
928 else:
929 self.copy_body()
931 def copy_body(self):
932 """
933 Copies the body, in cases where it might be shared with another request
934 object and that is not desired.
936 This copies the body either into a BytesIO object (through setting
937 req.body) or a temporary file.
938 """
940 if self.is_body_readable:
941 # Before we copy, if we can, rewind the body file
942 if self.is_body_seekable:
943 self.body_file_raw.seek(0)
945 tempfile_limit = self.request_body_tempfile_limit
946 todo = self.content_length if self.content_length is not None else 65535
948 newbody = b''
949 fileobj = None
950 input = self.body_file
952 while todo > 0:
953 data = input.read(min(todo, 65535))
955 if not data and self.content_length is None:
956 # We attempted to read more data, but got none, break.
957 # This can happen if for instance we are reading as much as
958 # we can because we don't have a Content-Length...
959 break
960 elif not data:
961 # We have a Content-Length and we attempted to read, but
962 # there was nothing more to read. Oh the humanity! This
963 # should rarely if never happen because self.body_file
964 # should be a LimitedLengthFile which should already have
965 # raised if there was less data than expected.
966 raise DisconnectionError(
967 "Client disconnected (%s more bytes were expected)" % todo
968 )
970 if fileobj:
971 fileobj.write(data)
972 else:
973 newbody += data
975 # When we have enough data that we need a tempfile, let's
976 # create one, then clear the temporary variable we were
977 # using
978 if len(newbody) > tempfile_limit:
979 fileobj = self.make_tempfile()
980 fileobj.write(newbody)
981 newbody = b''
983 # Only decrement todo if Content-Length is set
984 if self.content_length is not None:
985 todo -= len(data)
987 if fileobj:
988 # We apparently had enough data to need a file
990 # Set the Content-Length to the amount of data that was just
991 # written.
992 self.content_length = fileobj.tell()
994 # Seek it back to the beginning
995 fileobj.seek(0)
997 self.body_file_raw = fileobj
999 # Allow it to be seeked in the future, so we don't need to copy
1000 # for things like .body
1001 self.is_body_seekable = True
1003 # Not strictly required since Content-Length is set
1004 self.is_body_readable = True
1005 else:
1006 # No file created, set the body and let it deal with creating
1007 # Content-Length and other vars.
1008 self.body = newbody
1009 else:
1010 # Always leave the request with a valid body, and this is pretty
1011 # cheap.
1012 self.body = b''
1014 def make_tempfile(self):
1015 """
1016 Create a tempfile to store big request body.
1017 This API is not stable yet. A 'size' argument might be added.
1018 """
1019 return tempfile.TemporaryFile()
1021 def remove_conditional_headers(self,
1022 remove_encoding=True,
1023 remove_range=True,
1024 remove_match=True,
1025 remove_modified=True):
1026 """
1027 Remove headers that make the request conditional.
1029 These headers can cause the response to be 304 Not Modified,
1030 which in some cases you may not want to be possible.
1032 This does not remove headers like If-Match, which are used for
1033 conflict detection.
1034 """
1035 check_keys = []
1036 if remove_range:
1037 check_keys += ['HTTP_IF_RANGE', 'HTTP_RANGE']
1038 if remove_match:
1039 check_keys.append('HTTP_IF_NONE_MATCH')
1040 if remove_modified:
1041 check_keys.append('HTTP_IF_MODIFIED_SINCE')
1042 if remove_encoding:
1043 check_keys.append('HTTP_ACCEPT_ENCODING')
1045 for key in check_keys:
1046 if key in self.environ:
1047 del self.environ[key]
1049 accept = accept_property()
1050 accept_charset = accept_charset_property()
1051 accept_encoding = accept_encoding_property()
1052 accept_language = accept_language_property()
1054 authorization = converter(
1055 environ_getter('HTTP_AUTHORIZATION', None, '14.8'),
1056 parse_auth, serialize_auth,
1057 )
1059 def _cache_control__get(self):
1060 """
1061 Get/set/modify the Cache-Control header (`HTTP spec section 14.9
1062 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
1063 """
1064 env = self.environ
1065 value = env.get('HTTP_CACHE_CONTROL', '')
1066 cache_header, cache_obj = env.get('webob._cache_control', (None, None))
1067 if cache_obj is not None and cache_header == value:
1068 return cache_obj
1069 cache_obj = CacheControl.parse(value,
1070 updates_to=self._update_cache_control,
1071 type='request')
1072 env['webob._cache_control'] = (value, cache_obj)
1073 return cache_obj
1075 def _cache_control__set(self, value):
1076 env = self.environ
1077 value = value or ''
1078 if isinstance(value, dict):
1079 value = CacheControl(value, type='request')
1080 if isinstance(value, CacheControl):
1081 str_value = str(value)
1082 env['HTTP_CACHE_CONTROL'] = str_value
1083 env['webob._cache_control'] = (str_value, value)
1084 else:
1085 env['HTTP_CACHE_CONTROL'] = str(value)
1086 env['webob._cache_control'] = (None, None)
1088 def _cache_control__del(self):
1089 env = self.environ
1090 if 'HTTP_CACHE_CONTROL' in env:
1091 del env['HTTP_CACHE_CONTROL']
1092 if 'webob._cache_control' in env:
1093 del env['webob._cache_control']
1095 def _update_cache_control(self, prop_dict):
1096 self.environ['HTTP_CACHE_CONTROL'] = serialize_cache_control(prop_dict)
1098 cache_control = property(_cache_control__get,
1099 _cache_control__set,
1100 _cache_control__del,
1101 doc=_cache_control__get.__doc__)
1104 if_match = etag_property('HTTP_IF_MATCH', AnyETag, '14.24')
1105 if_none_match = etag_property('HTTP_IF_NONE_MATCH', NoETag, '14.26',
1106 strong=False)
1108 date = converter_date(environ_getter('HTTP_DATE', None, '14.8'))
1109 if_modified_since = converter_date(
1110 environ_getter('HTTP_IF_MODIFIED_SINCE', None, '14.25'))
1111 if_unmodified_since = converter_date(
1112 environ_getter('HTTP_IF_UNMODIFIED_SINCE', None, '14.28'))
1113 if_range = converter(
1114 environ_getter('HTTP_IF_RANGE', None, '14.27'),
1115 IfRange.parse, serialize_if_range, 'IfRange object')
1118 max_forwards = converter(
1119 environ_getter('HTTP_MAX_FORWARDS', None, '14.31'),
1120 parse_int, serialize_int, 'int')
1122 pragma = environ_getter('HTTP_PRAGMA', None, '14.32')
1124 range = converter(
1125 environ_getter('HTTP_RANGE', None, '14.35'),
1126 parse_range, serialize_range, 'Range object')
1128 referer = environ_getter('HTTP_REFERER', None, '14.36')
1129 referrer = referer
1131 user_agent = environ_getter('HTTP_USER_AGENT', None, '14.43')
1133 def __repr__(self):
1134 try:
1135 name = '%s %s' % (self.method, self.url)
1136 except KeyError:
1137 name = '(invalid WSGI environ)'
1138 msg = '<%s at 0x%x %s>' % (
1139 self.__class__.__name__,
1140 abs(id(self)), name)
1141 return msg
1143 def as_bytes(self, skip_body=False):
1144 """
1145 Return HTTP bytes representing this request.
1146 If skip_body is True, exclude the body.
1147 If skip_body is an integer larger than one, skip body
1148 only if its length is bigger than that number.
1149 """
1150 url = self.url
1151 host = self.host_url
1152 assert url.startswith(host)
1153 url = url[len(host):]
1154 parts = [bytes_('%s %s %s' % (self.method, url, self.http_version))]
1156 # acquire body before we handle headers so that
1157 # content-length will be set
1158 body = None
1159 if self.is_body_readable:
1160 if skip_body > 1:
1161 if len(self.body) > skip_body:
1162 body = bytes_('<body skipped (len=%s)>' % len(self.body))
1163 else:
1164 skip_body = False
1165 if not skip_body:
1166 body = self.body
1168 for k, v in sorted(self.headers.items()):
1169 header = bytes_('%s: %s' % (k, v))
1170 parts.append(header)
1172 if body:
1173 parts.extend([b'', body])
1174 # HTTP clearly specifies CRLF
1175 return b'\r\n'.join(parts)
1177 def as_text(self):
1178 bytes = self.as_bytes()
1179 return bytes.decode(self.charset)
1181 __str__ = as_text
1183 @classmethod
1184 def from_bytes(cls, b):
1185 """
1186 Create a request from HTTP bytes data. If the bytes contain
1187 extra data after the request, raise a ValueError.
1188 """
1189 f = io.BytesIO(b)
1190 r = cls.from_file(f)
1191 if f.tell() != len(b):
1192 raise ValueError("The string contains more data than expected")
1193 return r
1195 @classmethod
1196 def from_text(cls, s):
1197 b = bytes_(s, 'utf-8')
1198 return cls.from_bytes(b)
1200 @classmethod
1201 def from_file(cls, fp):
1202 """Read a request from a file-like object (it must implement
1203 ``.read(size)`` and ``.readline()``).
1205 It will read up to the end of the request, not the end of the
1206 file (unless the request is a POST or PUT and has no
1207 Content-Length, in that case, the entire file is read).
1209 This reads the request as represented by ``str(req)``; it may
1210 not read every valid HTTP request properly.
1211 """
1212 start_line = fp.readline()
1213 is_text = isinstance(start_line, text_type)
1214 if is_text:
1215 crlf = '\r\n'
1216 colon = ':'
1217 else:
1218 crlf = b'\r\n'
1219 colon = b':'
1220 try:
1221 header = start_line.rstrip(crlf)
1222 method, resource, http_version = header.split(None, 2)
1223 method = native_(method, 'utf-8')
1224 resource = native_(resource, 'utf-8')
1225 http_version = native_(http_version, 'utf-8')
1226 except ValueError:
1227 raise ValueError('Bad HTTP request line: %r' % start_line)
1228 r = cls(environ_from_url(resource),
1229 http_version=http_version,
1230 method=method.upper()
1231 )
1232 del r.environ['HTTP_HOST']
1233 while 1:
1234 line = fp.readline()
1235 if not line.strip():
1236 # end of headers
1237 break
1238 hname, hval = line.split(colon, 1)
1239 hname = native_(hname, 'utf-8')
1240 hval = native_(hval, 'utf-8').strip()
1241 if hname in r.headers:
1242 hval = r.headers[hname] + ', ' + hval
1243 r.headers[hname] = hval
1245 clen = r.content_length
1246 if clen is None:
1247 body = fp.read()
1248 else:
1249 body = fp.read(clen)
1250 if is_text:
1251 body = bytes_(body, 'utf-8')
1252 r.body = body
1254 return r
1256 def call_application(self, application, catch_exc_info=False):
1257 """
1258 Call the given WSGI application, returning ``(status_string,
1259 headerlist, app_iter)``
1261 Be sure to call ``app_iter.close()`` if it's there.
1263 If catch_exc_info is true, then returns ``(status_string,
1264 headerlist, app_iter, exc_info)``, where the fourth item may
1265 be None, but won't be if there was an exception. If you don't
1266 do this and there was an exception, the exception will be
1267 raised directly.
1268 """
1269 if self.is_body_seekable:
1270 self.body_file_raw.seek(0)
1271 captured = []
1272 output = []
1273 def start_response(status, headers, exc_info=None):
1274 if exc_info is not None and not catch_exc_info:
1275 reraise(exc_info)
1276 captured[:] = [status, headers, exc_info]
1277 return output.append
1278 app_iter = application(self.environ, start_response)
1279 if output or not captured:
1280 try:
1281 output.extend(app_iter)
1282 finally:
1283 if hasattr(app_iter, 'close'):
1284 app_iter.close()
1285 app_iter = output
1286 if catch_exc_info:
1287 return (captured[0], captured[1], app_iter, captured[2])
1288 else:
1289 return (captured[0], captured[1], app_iter)
1291 # Will be filled in later:
1292 ResponseClass = None
1294 def send(self, application=None, catch_exc_info=False):
1295 """
1296 Like ``.call_application(application)``, except returns a
1297 response object with ``.status``, ``.headers``, and ``.body``
1298 attributes.
1300 This will use ``self.ResponseClass`` to figure out the class
1301 of the response object to return.
1303 If ``application`` is not given, this will send the request to
1304 ``self.make_default_send_app()``
1305 """
1306 if application is None:
1307 application = self.make_default_send_app()
1308 if catch_exc_info:
1309 status, headers, app_iter, exc_info = self.call_application(
1310 application, catch_exc_info=True)
1311 del exc_info
1312 else:
1313 status, headers, app_iter = self.call_application(
1314 application, catch_exc_info=False)
1315 return self.ResponseClass(
1316 status=status, headerlist=list(headers), app_iter=app_iter)
1318 get_response = send
1320 def make_default_send_app(self):
1321 global _client
1322 try:
1323 client = _client
1324 except NameError:
1325 from webob import client
1326 _client = client
1327 return client.send_request_app
1329 @classmethod
1330 def blank(cls, path, environ=None, base_url=None,
1331 headers=None, POST=None, **kw):
1332 """
1333 Create a blank request environ (and Request wrapper) with the
1334 given path (path should be urlencoded), and any keys from
1335 environ.
1337 The path will become path_info, with any query string split
1338 off and used.
1340 All necessary keys will be added to the environ, but the
1341 values you pass in will take precedence. If you pass in
1342 base_url then wsgi.url_scheme, HTTP_HOST, and SCRIPT_NAME will
1343 be filled in from that value.
1345 Any extra keyword will be passed to ``__init__``.
1346 """
1347 env = environ_from_url(path)
1348 if base_url:
1349 scheme, netloc, path, query, fragment = urlparse.urlsplit(base_url)
1350 if query or fragment:
1351 raise ValueError(
1352 "base_url (%r) cannot have a query or fragment"
1353 % base_url)
1354 if scheme:
1355 env['wsgi.url_scheme'] = scheme
1356 if netloc:
1357 if ':' not in netloc:
1358 if scheme == 'http':
1359 netloc += ':80'
1360 elif scheme == 'https':
1361 netloc += ':443'
1362 else:
1363 raise ValueError(
1364 "Unknown scheme: %r" % scheme)
1365 host, port = netloc.split(':', 1)
1366 env['SERVER_PORT'] = port
1367 env['SERVER_NAME'] = host
1368 env['HTTP_HOST'] = netloc
1369 if path:
1370 env['SCRIPT_NAME'] = url_unquote(path)
1371 if environ:
1372 env.update(environ)
1373 content_type = kw.get('content_type', env.get('CONTENT_TYPE'))
1374 if headers and 'Content-Type' in headers:
1375 content_type = headers['Content-Type']
1376 if content_type is not None:
1377 kw['content_type'] = content_type
1378 environ_add_POST(env, POST, content_type=content_type)
1379 obj = cls(env, **kw)
1380 if headers is not None:
1381 obj.headers.update(headers)
1382 return obj
1384class LegacyRequest(BaseRequest):
1385 uscript_name = upath_property('SCRIPT_NAME')
1386 upath_info = upath_property('PATH_INFO')
1388 def encget(self, key, default=NoDefault, encattr=None):
1389 val = self.environ.get(key, default)
1390 if val is NoDefault:
1391 raise KeyError(key)
1392 if val is default:
1393 return default
1394 return val
1396class AdhocAttrMixin(object):
1397 _setattr_stacklevel = 3
1399 def __setattr__(self, attr, value, DEFAULT=object()):
1400 if (getattr(self.__class__, attr, DEFAULT) is not DEFAULT or
1401 attr.startswith('_')):
1402 object.__setattr__(self, attr, value)
1403 else:
1404 self.environ.setdefault('webob.adhoc_attrs', {})[attr] = value
1406 def __getattr__(self, attr, DEFAULT=object()):
1407 try:
1408 return self.environ['webob.adhoc_attrs'][attr]
1409 except KeyError:
1410 raise AttributeError(attr)
1412 def __delattr__(self, attr, DEFAULT=object()):
1413 if getattr(self.__class__, attr, DEFAULT) is not DEFAULT:
1414 return object.__delattr__(self, attr)
1415 try:
1416 del self.environ['webob.adhoc_attrs'][attr]
1417 except KeyError:
1418 raise AttributeError(attr)
1420class Request(AdhocAttrMixin, BaseRequest):
1421 """ The default request implementation """
1423def environ_from_url(path):
1424 if SCHEME_RE.search(path):
1425 scheme, netloc, path, qs, fragment = urlparse.urlsplit(path)
1426 if fragment:
1427 raise TypeError("Path cannot contain a fragment (%r)" % fragment)
1428 if qs:
1429 path += '?' + qs
1430 if ':' not in netloc:
1431 if scheme == 'http':
1432 netloc += ':80'
1433 elif scheme == 'https':
1434 netloc += ':443'
1435 else:
1436 raise TypeError("Unknown scheme: %r" % scheme)
1437 else:
1438 scheme = 'http'
1439 netloc = 'localhost:80'
1440 if path and '?' in path:
1441 path_info, query_string = path.split('?', 1)
1442 path_info = url_unquote(path_info)
1443 else:
1444 path_info = url_unquote(path)
1445 query_string = ''
1446 env = {
1447 'REQUEST_METHOD': 'GET',
1448 'SCRIPT_NAME': '',
1449 'PATH_INFO': path_info or '',
1450 'QUERY_STRING': query_string,
1451 'SERVER_NAME': netloc.split(':')[0],
1452 'SERVER_PORT': netloc.split(':')[1],
1453 'HTTP_HOST': netloc,
1454 'SERVER_PROTOCOL': 'HTTP/1.0',
1455 'wsgi.version': (1, 0),
1456 'wsgi.url_scheme': scheme,
1457 'wsgi.input': io.BytesIO(),
1458 'wsgi.errors': sys.stderr,
1459 'wsgi.multithread': False,
1460 'wsgi.multiprocess': False,
1461 'wsgi.run_once': False,
1462 #'webob.is_body_seekable': True,
1463 }
1464 return env
1467def environ_add_POST(env, data, content_type=None):
1468 if data is None:
1469 return
1470 elif isinstance(data, text_type):
1471 data = data.encode('ascii')
1472 if env['REQUEST_METHOD'] not in ('POST', 'PUT'):
1473 env['REQUEST_METHOD'] = 'POST'
1474 has_files = False
1475 if hasattr(data, 'items'):
1476 data = list(data.items())
1477 for k, v in data:
1478 if isinstance(v, (tuple, list)):
1479 has_files = True
1480 break
1481 if content_type is None:
1482 if has_files:
1483 content_type = 'multipart/form-data'
1484 else:
1485 content_type = 'application/x-www-form-urlencoded'
1486 if content_type.startswith('multipart/form-data'):
1487 if not isinstance(data, bytes):
1488 content_type, data = _encode_multipart(data, content_type)
1489 elif content_type.startswith('application/x-www-form-urlencoded'):
1490 if has_files:
1491 raise ValueError('Submiting files is not allowed for'
1492 ' content type `%s`' % content_type)
1493 if not isinstance(data, bytes):
1494 data = url_encode(data)
1495 else:
1496 if not isinstance(data, bytes):
1497 raise ValueError('Please provide `POST` data as bytes'
1498 ' for content type `%s`' % content_type)
1499 data = bytes_(data, 'utf8')
1500 env['wsgi.input'] = io.BytesIO(data)
1501 env['webob.is_body_seekable'] = True
1502 env['CONTENT_LENGTH'] = str(len(data))
1503 env['CONTENT_TYPE'] = content_type
1506#
1507# Helper classes and monkeypatching
1508#
1510class DisconnectionError(IOError):
1511 pass
1514class LimitedLengthFile(io.RawIOBase):
1515 def __init__(self, file, maxlen):
1516 self.file = file
1517 self.maxlen = maxlen
1518 self.remaining = maxlen
1520 def __repr__(self):
1521 return '<%s(%r, maxlen=%s)>' % (
1522 self.__class__.__name__,
1523 self.file,
1524 self.maxlen
1525 )
1527 def fileno(self):
1528 return self.file.fileno()
1530 @staticmethod
1531 def readable():
1532 return True
1534 def readinto(self, buff):
1535 if not self.remaining:
1536 return 0
1537 sz0 = min(len(buff), self.remaining)
1538 data = self.file.read(sz0)
1539 sz = len(data)
1540 self.remaining -= sz
1541 if sz < sz0 and self.remaining:
1542 raise DisconnectionError(
1543 "The client disconnected while sending the body "
1544 "(%d more bytes were expected)" % (self.remaining,)
1545 )
1546 buff[:sz] = data
1547 return sz
1550def _cgi_FieldStorage__repr__patch(self):
1551 """ monkey patch for FieldStorage.__repr__
1553 Unbelievably, the default __repr__ on FieldStorage reads
1554 the entire file content instead of being sane about it.
1555 This is a simple replacement that doesn't do that
1556 """
1557 if self.file:
1558 return "FieldStorage(%r, %r)" % (self.name, self.filename)
1559 return "FieldStorage(%r, %r, %r)" % (self.name, self.filename, self.value)
1561cgi_FieldStorage.__repr__ = _cgi_FieldStorage__repr__patch
1564class FakeCGIBody(io.RawIOBase):
1565 def __init__(self, vars, content_type):
1566 warnings.warn(
1567 "FakeCGIBody is no longer used by WebOb and will be removed from a future "
1568 "version of WebOb. If you require FakeCGIBody please make a copy into "
1569 "you own project",
1570 DeprecationWarning
1571 )
1573 if content_type.startswith('multipart/form-data'):
1574 if not _get_multipart_boundary(content_type):
1575 raise ValueError('Content-type: %r does not contain boundary'
1576 % content_type)
1577 self.vars = vars
1578 self.content_type = content_type
1579 self.file = None
1581 def __repr__(self):
1582 inner = repr(self.vars)
1583 if len(inner) > 20:
1584 inner = inner[:15] + '...' + inner[-5:]
1585 return '<%s at 0x%x viewing %s>' % (
1586 self.__class__.__name__,
1587 abs(id(self)), inner)
1589 def fileno(self):
1590 return None
1592 @staticmethod
1593 def readable():
1594 return True
1596 def readinto(self, buff):
1597 if self.file is None:
1598 if self.content_type.startswith('application/x-www-form-urlencoded'):
1599 data = '&'.join(
1600 '%s=%s' % (
1601 quote_plus(bytes_(k, 'utf8')),
1602 quote_plus(bytes_(v, 'utf8'))
1603 )
1604 for k, v in self.vars.items()
1605 )
1606 self.file = io.BytesIO(bytes_(data))
1607 elif self.content_type.startswith('multipart/form-data'):
1608 self.file = _encode_multipart(
1609 self.vars.items(),
1610 self.content_type,
1611 fout=io.BytesIO()
1612 )[1]
1613 self.file.seek(0)
1614 else:
1615 assert 0, ('Bad content type: %r' % self.content_type)
1616 return self.file.readinto(buff)
1618def _get_multipart_boundary(ctype):
1619 m = re.search(r'boundary=([^ ]+)', ctype, re.I)
1620 if m:
1621 return native_(m.group(1).strip('"'))
1623def _encode_multipart(vars, content_type, fout=None):
1624 """Encode a multipart request body into a string"""
1625 f = fout or io.BytesIO()
1626 w = f.write
1627 def wt(t):
1628 w(t.encode('utf8'))
1630 CRLF = b'\r\n'
1631 boundary = _get_multipart_boundary(content_type)
1632 if not boundary:
1633 boundary = native_(binascii.hexlify(os.urandom(10)))
1634 content_type += ('; boundary=%s' % boundary)
1635 for name, value in vars:
1636 w(b'--')
1637 wt(boundary)
1638 w(CRLF)
1639 wt('Content-Disposition: form-data')
1640 if name is not None:
1641 wt('; name="%s"' % name)
1642 filename = None
1643 if getattr(value, 'filename', None):
1644 filename = value.filename
1645 elif isinstance(value, (list, tuple)):
1646 filename, value = value
1647 if hasattr(value, 'read'):
1648 value = value.read()
1650 if filename is not None:
1651 wt('; filename="%s"' % filename)
1652 mime_type = mimetypes.guess_type(filename)[0]
1653 else:
1654 mime_type = None
1656 w(CRLF)
1658 # TODO: should handle value.disposition_options
1659 if getattr(value, 'type', None):
1660 wt('Content-type: %s' % value.type)
1661 if value.type_options:
1662 for ct_name, ct_value in sorted(value.type_options.items()):
1663 wt('; %s="%s"' % (ct_name, ct_value))
1664 w(CRLF)
1665 elif mime_type:
1666 wt('Content-type: %s' % mime_type)
1667 w(CRLF)
1668 w(CRLF)
1669 if hasattr(value, 'value'):
1670 value = value.value
1671 if isinstance(value, bytes):
1672 w(value)
1673 else:
1674 wt(value)
1675 w(CRLF)
1676 wt('--%s--' % boundary)
1677 if fout:
1678 return content_type, fout
1679 else:
1680 return content_type, f.getvalue()
1682def detect_charset(ctype):
1683 m = CHARSET_RE.search(ctype)
1684 if m:
1685 return m.group(1).strip('"').strip()
1687def _is_utf8(charset):
1688 if not charset:
1689 return True
1690 else:
1691 return charset.lower().replace('-', '') == 'utf8'
1694class Transcoder(object):
1695 def __init__(self, charset, errors='strict'):
1696 self.charset = charset # source charset
1697 self.errors = errors # unicode errors
1698 self._trans = lambda b: b.decode(charset, errors).encode('utf8')
1700 def transcode_query(self, q):
1701 q_orig = q
1702 if '=' not in q:
1703 # this doesn't look like a form submission
1704 return q_orig
1706 if PY2:
1707 q = urlparse.parse_qsl(q, self.charset)
1708 t = self._trans
1709 q = [(t(k), t(v)) for k, v in q]
1710 else:
1711 q = list(parse_qsl_text(q, self.charset))
1713 return url_encode(q)
1715 def transcode_fs(self, fs, content_type):
1716 # transcode FieldStorage
1717 if PY2:
1718 def decode(b):
1719 if b is not None:
1720 return b.decode(self.charset, self.errors)
1721 else:
1722 return b
1723 else:
1724 def decode(b):
1725 return b
1727 data = []
1728 for field in fs.list or ():
1729 field.name = decode(field.name)
1730 if field.filename:
1731 field.filename = decode(field.filename)
1732 data.append((field.name, field))
1733 else:
1734 data.append((field.name, decode(field.value)))
1736 # TODO: transcode big requests to temp file
1737 content_type, fout = _encode_multipart(
1738 data,
1739 content_type,
1740 fout=io.BytesIO()
1741 )
1742 return fout