diff options
Diffstat (limited to 'src/webob/request.py')
| -rw-r--r-- | src/webob/request.py | 414 |
1 files changed, 206 insertions, 208 deletions
diff --git a/src/webob/request.py b/src/webob/request.py index 2684ba1..bd6a293 100644 --- a/src/webob/request.py +++ b/src/webob/request.py @@ -1,15 +1,12 @@ import binascii import io +import mimetypes import os import re import sys import tempfile -import mimetypes - -try: - import simplejson as json -except ImportError: - import json +from urllib import parse as urlparse +from urllib.parse import quote as url_quote, quote_plus, urlencode as url_encode import warnings from webob.acceptparse import ( @@ -18,33 +15,16 @@ from webob.acceptparse import ( accept_language_property, accept_property, ) - from webob.cachecontrol import CacheControl, serialize_cache_control - -from webob.compat import ( - PY2, - bytes_, - native_, - parse_qsl_text, - reraise, - text_type, - url_encode, - url_quote, - url_unquote, - quote_plus, - urlparse, - cgi_FieldStorage, -) - +from webob.compat import cgi_FieldStorage from webob.cookies import RequestCookies - from webob.descriptors import ( CHARSET_RE, SCHEME_RE, converter, converter_date, - environ_getter, environ_decoder, + environ_getter, parse_auth, parse_int, parse_int_safe, @@ -55,14 +35,18 @@ from webob.descriptors import ( serialize_range, upath_property, ) - -from webob.etag import IfRange, AnyETag, NoETag, etag_property - +from webob.etag import AnyETag, IfRange, NoETag, etag_property from webob.headers import EnvironHeaders +from webob.multidict import GetDict, MultiDict, NestedMultiDict, NoVars +from webob.util import bytes_, parse_qsl_text, text_, url_unquote + +try: + import simplejson as json +except ImportError: + import json -from webob.multidict import NestedMultiDict, MultiDict, NoVars, GetDict -__all__ = ["BaseRequest", "Request", "LegacyRequest"] +__all__ = ["BaseRequest", "Request"] class _NoDefault: @@ -90,7 +74,7 @@ _LATIN_ENCODINGS = ( ) -class BaseRequest(object): +class BaseRequest: # The limit after which request bodies should be stored on disk # if they are read in (under this, and the request body is stored # in memory): @@ -98,42 +82,12 @@ class BaseRequest(object): _charset = None - def __init__( - self, environ, charset=None, unicode_errors=None, decode_param_names=None, **kw - ): + def __init__(self, environ, **kw): if type(environ) is not dict: raise TypeError("WSGI environ must be a dict; you passed %r" % (environ,)) - if unicode_errors is not None: - warnings.warn( - "You unicode_errors=%r to the Request constructor. Passing a " - "``unicode_errors`` value to the Request is no longer " - "supported in WebOb 1.2+. This value has been ignored " - % (unicode_errors,), - DeprecationWarning, - ) - - if decode_param_names is not None: - warnings.warn( - "You passed decode_param_names=%r to the Request constructor. " - "Passing a ``decode_param_names`` value to the Request " - "is no longer supported in WebOb 1.2+. This value has " - "been ignored " % (decode_param_names,), - DeprecationWarning, - ) - - if not _is_utf8(charset): - raise DeprecationWarning( - "You passed charset=%r to the Request constructor. As of " - "WebOb 1.2, if your application needs a non-UTF-8 request " - "charset, please construct the request without a charset or " - "with a charset of 'None', then use ``req = " - "req.decode(charset)``" % charset - ) - - d = self.__dict__ - d["environ"] = environ + self.__dict__["environ"] = environ if kw: cls = self.__class__ @@ -150,19 +104,20 @@ class BaseRequest(object): def encget(self, key, default=NoDefault, encattr=None): val = self.environ.get(key, default) + if val is NoDefault: raise KeyError(key) + if val is default: return default + if not encattr: return val encoding = getattr(self, encattr) - if PY2: - return val.decode(encoding) - if encoding in _LATIN_ENCODINGS: # shortcut return val + return bytes_(val, "latin-1").decode(encoding) def encset(self, key, val, encattr=None): @@ -170,29 +125,30 @@ class BaseRequest(object): encoding = getattr(self, encattr) else: encoding = "ascii" - if PY2: # pragma: no cover - self.environ[key] = bytes_(val, encoding) - else: - self.environ[key] = bytes_(val, encoding).decode("latin-1") + self.environ[key] = bytes_(val, encoding).decode("latin-1") @property def charset(self): if self._charset is None: charset = detect_charset(self._content_type_raw) + if _is_utf8(charset): charset = "UTF-8" self._charset = charset + return self._charset @charset.setter def charset(self, charset): if _is_utf8(charset): charset = "UTF-8" + if charset != self.charset: raise DeprecationWarning("Use req = req.decode(%r)" % charset) def decode(self, charset=None, errors="strict"): charset = charset or self.charset + if charset == "UTF-8": return self # cookies and path are always utf-8 @@ -207,7 +163,8 @@ class BaseRequest(object): ) if content_type == "application/x-www-form-urlencoded": - r.body = bytes_(t.transcode_query(native_(self.body))) + r.body = bytes_(t.transcode_query(text_(self.body))) + return r elif content_type != "multipart/form-data": return r @@ -215,18 +172,13 @@ class BaseRequest(object): fs_environ = self.environ.copy() fs_environ.setdefault("CONTENT_LENGTH", "0") fs_environ["QUERY_STRING"] = "" - if PY2: - fs = cgi_FieldStorage( - fp=self.body_file, environ=fs_environ, keep_blank_values=True - ) - else: - fs = cgi_FieldStorage( - fp=self.body_file, - environ=fs_environ, - keep_blank_values=True, - encoding=charset, - errors=errors, - ) + fs = cgi_FieldStorage( + fp=self.body_file, + environ=fs_environ, + keep_blank_values=True, + encoding=charset, + errors=errors, + ) fout = t.transcode_fs(fs, r._content_type_raw) @@ -235,6 +187,7 @@ class BaseRequest(object): r.body_file = fout r.content_length = fout.tell() fout.seek(0) + return r # this is necessary for correct warnings depth for both @@ -298,8 +251,10 @@ class BaseRequest(object): If you access this value, CONTENT_LENGTH will also be updated. """ + if not self.is_body_seekable: self.make_body_seekable() + return self.body_file_raw url_encoding = environ_getter("webob.url_encoding", "UTF-8") @@ -339,13 +294,16 @@ class BaseRequest(object): you don't include any parameters in the value then existing parameters will be preserved. """ + return self._content_type_raw.split(";", 1)[0] def _content_type__set(self, value=None): if value is not None: value = str(value) + if ";" not in value: content_type = self._content_type_raw + if ";" in content_type: value += ";" + content_type.split(";", 1)[1] self._content_type_raw = value @@ -364,8 +322,10 @@ class BaseRequest(object): All the request headers as a case-insensitive dictionary-like object. """ + if self._headers is None: self._headers = EnvironHeaders(self.environ) + return self._headers def _headers__set(self, value): @@ -398,10 +358,12 @@ class BaseRequest(object): """ e = self.environ xff = e.get("HTTP_X_FORWARDED_FOR") + if xff is not None: addr = xff.split(",")[0].strip() else: addr = e.get("REMOTE_ADDR") + return addr @property @@ -418,17 +380,20 @@ class BaseRequest(object): """ e = self.environ host = e.get("HTTP_HOST") + if host is not None: if ":" in host and host[-1] != "]": host, port = host.rsplit(":", 1) else: url_scheme = e["wsgi.url_scheme"] + if url_scheme == "https": port = "443" else: port = "80" else: port = e["SERVER_PORT"] + return port @property @@ -440,6 +405,7 @@ class BaseRequest(object): scheme = e.get("wsgi.url_scheme") url = scheme + "://" host = e.get("HTTP_HOST") + if host is not None: if ":" in host and host[-1] != "]": host, port = host.rsplit(":", 1) @@ -448,6 +414,7 @@ class BaseRequest(object): else: host = e.get("SERVER_NAME") port = e.get("SERVER_PORT") + if scheme == "https": if port == "443": port = None @@ -455,8 +422,10 @@ class BaseRequest(object): if port == "80": port = None url += host + if port: url += ":%s" % port + return url @property @@ -465,6 +434,7 @@ class BaseRequest(object): The URL including SCRIPT_NAME (no PATH_INFO or query string) """ bscript_name = bytes_(self.script_name, self.url_encoding) + return self.host_url + url_quote(bscript_name, PATH_SAFE) @property @@ -473,6 +443,7 @@ class BaseRequest(object): The URL including SCRIPT_NAME and PATH_INFO, but not QUERY_STRING """ bpath_info = bytes_(self.path_info, self.url_encoding) + return self.application_url + url_quote(bpath_info, PATH_SAFE) @property @@ -482,6 +453,7 @@ class BaseRequest(object): """ bscript = bytes_(self.script_name, self.url_encoding) bpath = bytes_(self.path_info, self.url_encoding) + return url_quote(bscript, PATH_SAFE) + url_quote(bpath, PATH_SAFE) @property @@ -491,8 +463,10 @@ class BaseRequest(object): """ path = self.path qs = self.environ.get("QUERY_STRING") + if qs: path += "?" + qs + return path @property @@ -502,8 +476,10 @@ class BaseRequest(object): """ url = self.path_url qs = self.environ.get("QUERY_STRING") + if qs: url += "?" + qs + return url def relative_url(self, other_url, to_application=False): @@ -513,12 +489,15 @@ class BaseRequest(object): If ``to_application`` is True, then resolve it relative to the URL with only SCRIPT_NAME """ + if to_application: url = self.application_url + if not url.endswith("/"): url += "/" else: url = self.path_url + return urlparse.urljoin(url, other_url) def path_info_pop(self, pattern=None): @@ -535,19 +514,24 @@ class BaseRequest(object): request and None is returned. """ path = self.path_info + if not path: return None slashes = "" + while path.startswith("/"): slashes += "/" path = path[1:] idx = path.find("/") + if idx == -1: idx = len(path) r = path[:idx] + if pattern is None or re.match(pattern, r): self.script_name += slashes + r self.path_info = path[idx:] + return r def path_info_peek(self): @@ -556,9 +540,11 @@ class BaseRequest(object): next segment. Doesn't modify the environment. """ path = self.path_info + if not path: return None path = path.lstrip("/") + return path.split("/", 1)[0] def _urlvars__get(self): @@ -568,6 +554,7 @@ class BaseRequest(object): Takes values from ``environ['wsgiorg.routing_args']``. Systems like ``routes`` set this value. """ + if "paste.urlvars" in self.environ: return self.environ["paste.urlvars"] elif "wsgiorg.routing_args" in self.environ: @@ -575,15 +562,18 @@ class BaseRequest(object): else: result = {} self.environ["wsgiorg.routing_args"] = ((), result) + return result def _urlvars__set(self, value): environ = self.environ + if "wsgiorg.routing_args" in environ: environ["wsgiorg.routing_args"] = ( environ["wsgiorg.routing_args"][0], value, ) + if "paste.urlvars" in environ: del environ["paste.urlvars"] elif "paste.urlvars" in environ: @@ -594,6 +584,7 @@ class BaseRequest(object): def _urlvars__del(self): if "paste.urlvars" in self.environ: del self.environ["paste.urlvars"] + if "wsgiorg.routing_args" in self.environ: if not self.environ["wsgiorg.routing_args"][0]: del self.environ["wsgiorg.routing_args"] @@ -614,15 +605,18 @@ class BaseRequest(object): Takes values from ``environ['wsgiorg.routing_args']``. Systems like ``routes`` set this value. """ + if "wsgiorg.routing_args" in self.environ: return self.environ["wsgiorg.routing_args"][0] else: # Since you can't update this value in-place, we don't need # to set the key in the environment + return () def _urlargs__set(self, value): environ = self.environ + if "paste.urlvars" in environ: # Some overlap between this and wsgiorg.routing_args; we need # wsgiorg.routing_args to make this work @@ -655,10 +649,12 @@ class BaseRequest(object): only set if you are using a Javascript library that sets it (or you set the header yourself manually). Currently Prototype and jQuery are known to set this header.""" + return self.environ.get("HTTP_X_REQUESTED_WITH", "") == "XMLHttpRequest" def _host__get(self): """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME""" + if "HTTP_HOST" in self.environ: return self.environ["HTTP_HOST"] else: @@ -693,8 +689,10 @@ class BaseRequest(object): value use :meth:`webob.request.Request.host` instead. """ domain = self.host + if ":" in domain and domain[-1] != "]": domain = domain.rsplit(":", 1)[0] + return domain @property @@ -702,18 +700,21 @@ class BaseRequest(object): """ Return the content of the request body. """ + if not self.is_body_readable: return b"" self.make_body_seekable() # we need this to have content_length r = self.body_file.read(self.content_length) self.body_file_raw.seek(0) + return r @body.setter def body(self, value): if value is None: value = b"" + if not isinstance(value, bytes): raise TypeError( "You can only set Request.body to bytes (not %r)" % type(value) @@ -728,6 +729,7 @@ class BaseRequest(object): def _json_body__get(self): """Access the body of the request as JSON""" + return json.loads(self.body.decode(self.charset)) def _json_body__set(self, value): @@ -742,9 +744,11 @@ class BaseRequest(object): """ Get/set the text value of the body """ + if not self.charset: raise AttributeError("You cannot access Request.text unless charset is set") body = self.body + return body.decode(self.charset) def _text__set(self, value): @@ -752,7 +756,8 @@ class BaseRequest(object): raise AttributeError( "You cannot access Response.text unless charset is set" ) - if not isinstance(value, text_type): + + if not isinstance(value, str): raise TypeError( "You can only set Request.text to a unicode string " "(not %s)" % type(value) @@ -774,17 +779,21 @@ class BaseRequest(object): requests with an appropriate Content-Type are also supported. """ env = self.environ + if "webob._parsed_post_vars" in env: vars, body_file = env["webob._parsed_post_vars"] + if body_file is self.body_file_raw: return vars content_type = self.content_type + if (self.method != "POST" and not content_type) or content_type not in ( "", "application/x-www-form-urlencoded", "multipart/form-data", ): # Not an HTML form submission + return NoVars( "Not an HTML form submission (Content-Type: %s)" % content_type ) @@ -798,21 +807,17 @@ class BaseRequest(object): # default of 0 is better: fs_environ.setdefault("CONTENT_LENGTH", "0") fs_environ["QUERY_STRING"] = "" - if PY2: - fs = cgi_FieldStorage( - fp=self.body_file, environ=fs_environ, keep_blank_values=True - ) - else: - fs = cgi_FieldStorage( - fp=self.body_file, - environ=fs_environ, - keep_blank_values=True, - encoding="utf8", - ) + fs = cgi_FieldStorage( + fp=self.body_file, + environ=fs_environ, + keep_blank_values=True, + encoding="utf8", + ) self.body_file_raw.seek(0) vars = MultiDict.from_fieldstorage(fs) env["webob._parsed_post_vars"] = (vars, self.body_file_raw) + return vars @property @@ -823,12 +828,15 @@ class BaseRequest(object): """ env = self.environ source = env.get("QUERY_STRING", "") + if "webob._parsed_query_vars" in env: vars, qs = env["webob._parsed_query_vars"] + if qs == source: return vars data = [] + if source: # this is disabled because we want to access req.GET # for text/plain; charset=ascii uploads for example @@ -838,6 +846,7 @@ class BaseRequest(object): # data = [(d(k), d(v)) for k,v in data] vars = GetDict(data, env) env["webob._parsed_query_vars"] = (vars, source) + return vars def _check_charset(self): @@ -855,6 +864,7 @@ class BaseRequest(object): the query string and request body. """ params = NestedMultiDict(self.GET, self.POST) + return params @property @@ -862,6 +872,7 @@ class BaseRequest(object): """ Return a dictionary of cookies as found in the request. """ + return RequestCookies(self.environ) @cookies.setter @@ -880,6 +891,7 @@ class BaseRequest(object): env = self.environ.copy() new_req = self.__class__(env) new_req.copy_body() + return new_req def copy_get(self): @@ -889,6 +901,7 @@ class BaseRequest(object): verb) then it becomes GET, and the request body is thrown away. """ env = self.environ.copy() + return self.__class__(env, method="GET", content_type=None, body=b"") # webob.is_body_seekable marks input streams that are seekable @@ -911,6 +924,7 @@ class BaseRequest(object): # Encoding is allowed (and works) or we have replaced # self.body_file with something that is readable and EOF's # correctly. + return self.environ.get( "wsgi.input_terminated", # For backwards compatibility, we fall back to checking if @@ -937,6 +951,7 @@ class BaseRequest(object): The choice to copy to BytesIO is made from ``self.request_body_tempfile_limit`` """ + if self.is_body_seekable: self.body_file_raw.seek(0) else: @@ -953,6 +968,7 @@ class BaseRequest(object): if self.is_body_readable: # Before we copy, if we can, rewind the body file + if self.is_body_seekable: self.body_file_raw.seek(0) @@ -970,6 +986,7 @@ class BaseRequest(object): # We attempted to read more data, but got none, break. # This can happen if for instance we are reading as much as # we can because we don't have a Content-Length... + break elif not data: # We have a Content-Length and we attempted to read, but @@ -989,12 +1006,14 @@ class BaseRequest(object): # When we have enough data that we need a tempfile, let's # create one, then clear the temporary variable we were # using + if len(newbody) > tempfile_limit: fileobj = self.make_tempfile() fileobj.write(newbody) newbody = b"" # Only decrement todo if Content-Length is set + if self.content_length is not None: todo -= len(data) @@ -1030,6 +1049,7 @@ class BaseRequest(object): Create a tempfile to store big request body. This API is not stable yet. A 'size' argument might be added. """ + return tempfile.TemporaryFile() def remove_conditional_headers( @@ -1049,12 +1069,16 @@ class BaseRequest(object): conflict detection. """ check_keys = [] + if remove_range: check_keys += ["HTTP_IF_RANGE", "HTTP_RANGE"] + if remove_match: check_keys.append("HTTP_IF_NONE_MATCH") + if remove_modified: check_keys.append("HTTP_IF_MODIFIED_SINCE") + if remove_encoding: check_keys.append("HTTP_ACCEPT_ENCODING") @@ -1079,19 +1103,23 @@ class BaseRequest(object): env = self.environ value = env.get("HTTP_CACHE_CONTROL", "") cache_header, cache_obj = env.get("webob._cache_control", (None, None)) + if cache_obj is not None and cache_header == value: return cache_obj cache_obj = CacheControl.parse( value, updates_to=self._update_cache_control, type="request" ) env["webob._cache_control"] = (value, cache_obj) + return cache_obj def _cache_control__set(self, value): env = self.environ value = value or "" + if isinstance(value, dict): value = CacheControl(value, type="request") + if isinstance(value, CacheControl): str_value = str(value) env["HTTP_CACHE_CONTROL"] = str_value @@ -1102,8 +1130,10 @@ class BaseRequest(object): def _cache_control__del(self): env = self.environ + if "HTTP_CACHE_CONTROL" in env: del env["HTTP_CACHE_CONTROL"] + if "webob._cache_control" in env: del env["webob._cache_control"] @@ -1161,6 +1191,7 @@ class BaseRequest(object): except KeyError: name = "(invalid WSGI environ)" msg = "<%s at 0x%x %s>" % (self.__class__.__name__, abs(id(self)), name) + return msg def as_bytes(self, skip_body=False): @@ -1179,12 +1210,14 @@ class BaseRequest(object): # acquire body before we handle headers so that # content-length will be set body = None + if self.is_body_readable: if skip_body > 1: if len(self.body) > skip_body: body = bytes_("<body skipped (len=%s)>" % len(self.body)) else: skip_body = False + if not skip_body: body = self.body @@ -1195,10 +1228,12 @@ class BaseRequest(object): if body: parts.extend([b"", body]) # HTTP clearly specifies CRLF + return b"\r\n".join(parts) def as_text(self, skip_body=False): bytes = self.as_bytes(skip_body) + return bytes.decode(self.charset) __str__ = as_text @@ -1211,13 +1246,16 @@ class BaseRequest(object): """ f = io.BytesIO(b) r = cls.from_file(f) + if f.tell() != len(b): raise ValueError("The string contains more data than expected") + return r @classmethod def from_text(cls, s): b = bytes_(s, "utf-8") + return cls.from_bytes(b) @classmethod @@ -1233,7 +1271,8 @@ class BaseRequest(object): not read every valid HTTP request properly. """ start_line = fp.readline() - is_text = isinstance(start_line, text_type) + is_text = isinstance(start_line, str) + if is_text: crlf = "\r\n" colon = ":" @@ -1243,32 +1282,38 @@ class BaseRequest(object): try: header = start_line.rstrip(crlf) method, resource, http_version = header.split(None, 2) - method = native_(method, "utf-8") - resource = native_(resource, "utf-8") - http_version = native_(http_version, "utf-8") + method = text_(method, "utf-8") + resource = text_(resource, "utf-8") + http_version = text_(http_version, "utf-8") except ValueError: raise ValueError("Bad HTTP request line: %r" % start_line) r = cls( environ_from_url(resource), http_version=http_version, method=method.upper() ) del r.environ["HTTP_HOST"] + while 1: line = fp.readline() + if not line.strip(): # end of headers + break hname, hval = line.split(colon, 1) - hname = native_(hname, "utf-8") - hval = native_(hval, "utf-8").strip() + hname = text_(hname, "utf-8") + hval = text_(hval, "utf-8").strip() + if hname in r.headers: hval = r.headers[hname] + ", " + hval r.headers[hname] = hval clen = r.content_length + if clen is None: body = fp.read() else: body = fp.read(clen) + if is_text: body = bytes_(body, "utf-8") r.body = body @@ -1288,6 +1333,7 @@ class BaseRequest(object): do this and there was an exception, the exception will be raised directly. """ + if self.is_body_seekable: self.body_file_raw.seek(0) captured = [] @@ -1295,11 +1341,14 @@ class BaseRequest(object): def start_response(status, headers, exc_info=None): if exc_info is not None and not catch_exc_info: - reraise(exc_info) + etype, exc, tb = exc_info + raise etype(exc).with_traceback(tb) captured[:] = [status, headers, exc_info] + return output.append app_iter = application(self.environ, start_response) + if output or not captured: try: output.extend(app_iter) @@ -1307,6 +1356,7 @@ class BaseRequest(object): if hasattr(app_iter, "close"): app_iter.close() app_iter = output + if catch_exc_info: return (captured[0], captured[1], app_iter, captured[2]) else: @@ -1327,8 +1377,10 @@ class BaseRequest(object): If ``application`` is not given, this will send the request to ``self.make_default_send_app()`` """ + if application is None: application = self.make_default_send_app() + if catch_exc_info: status, headers, app_iter, exc_info = self.call_application( application, catch_exc_info=True @@ -1338,6 +1390,7 @@ class BaseRequest(object): status, headers, app_iter = self.call_application( application, catch_exc_info=False ) + return self.ResponseClass( status=status, headerlist=list(headers), app_iter=app_iter ) @@ -1352,6 +1405,7 @@ class BaseRequest(object): from webob import client _client = client + return client.send_request_app @classmethod @@ -1372,14 +1426,18 @@ class BaseRequest(object): Any extra keyword will be passed to ``__init__``. """ env = environ_from_url(path) + if base_url: scheme, netloc, path, query, fragment = urlparse.urlsplit(base_url) + if query or fragment: raise ValueError( "base_url (%r) cannot have a query or fragment" % base_url ) + if scheme: env["wsgi.url_scheme"] = scheme + if netloc: if ":" not in netloc: if scheme == "http": @@ -1392,36 +1450,29 @@ class BaseRequest(object): env["SERVER_PORT"] = port env["SERVER_NAME"] = host env["HTTP_HOST"] = netloc + if path: env["SCRIPT_NAME"] = url_unquote(path) + if environ: env.update(environ) content_type = kw.get("content_type", env.get("CONTENT_TYPE")) + if headers and "Content-Type" in headers: content_type = headers["Content-Type"] + if content_type is not None: kw["content_type"] = content_type environ_add_POST(env, POST, content_type=content_type) obj = cls(env, **kw) + if headers is not None: obj.headers.update(headers) - return obj - - -class LegacyRequest(BaseRequest): - uscript_name = upath_property("SCRIPT_NAME") - upath_info = upath_property("PATH_INFO") - def encget(self, key, default=NoDefault, encattr=None): - val = self.environ.get(key, default) - if val is NoDefault: - raise KeyError(key) - if val is default: - return default - return val + return obj -class AdhocAttrMixin(object): +class AdhocAttrMixin: _setattr_stacklevel = 3 def __setattr__(self, attr, value, DEFAULT=DEFAULT): @@ -1454,10 +1505,13 @@ class Request(AdhocAttrMixin, BaseRequest): def environ_from_url(path): if SCHEME_RE.search(path): scheme, netloc, path, qs, fragment = urlparse.urlsplit(path) + if fragment: raise TypeError("Path cannot contain a fragment (%r)" % fragment) + if qs: path += "?" + qs + if ":" not in netloc: if scheme == "http": netloc += ":80" @@ -1468,6 +1522,7 @@ def environ_from_url(path): else: scheme = "http" netloc = "localhost:80" + if path and "?" in path: path_info, query_string = path.split("?", 1) path_info = url_unquote(path_info) @@ -1492,28 +1547,35 @@ def environ_from_url(path): "wsgi.run_once": False, "webob.is_body_seekable": True, } + return env def environ_add_POST(env, data, content_type=None): if data is None: return - elif isinstance(data, text_type): + elif isinstance(data, str): data = data.encode("ascii") + if env["REQUEST_METHOD"] not in ("POST", "PUT"): env["REQUEST_METHOD"] = "POST" has_files = False + if hasattr(data, "items"): data = list(data.items()) + for _, v in data: - if isinstance(v, (tuple, list)): + if isinstance(v, (tuple, list)) or hasattr(v, "filename"): has_files = True + break + if content_type is None: if has_files: content_type = "multipart/form-data" else: content_type = "application/x-www-form-urlencoded" + if content_type.startswith("multipart/form-data"): if not isinstance(data, bytes): content_type, data = _encode_multipart(data, content_type) @@ -1522,6 +1584,7 @@ def environ_add_POST(env, data, content_type=None): raise ValueError( "Submiting files is not allowed for" " content type `%s`" % content_type ) + if not isinstance(data, bytes): data = url_encode(data) else: @@ -1569,88 +1632,22 @@ class LimitedLengthFile(io.RawIOBase): data = self.file.read(sz0) sz = len(data) self.remaining -= sz + if sz < sz0 and self.remaining: raise DisconnectionError( "The client disconnected while sending the body " "(%d more bytes were expected)" % (self.remaining,) ) buff[:sz] = data - return sz - - -def _cgi_FieldStorage__repr__patch(self): - """monkey patch for FieldStorage.__repr__ - - Unbelievably, the default __repr__ on FieldStorage reads - the entire file content instead of being sane about it. - This is a simple replacement that doesn't do that - """ - if self.file: - return "FieldStorage(%r, %r)" % (self.name, self.filename) - return "FieldStorage(%r, %r, %r)" % (self.name, self.filename, self.value) - - -cgi_FieldStorage.__repr__ = _cgi_FieldStorage__repr__patch - - -class FakeCGIBody(io.RawIOBase): - def __init__(self, vars, content_type): - warnings.warn( - "FakeCGIBody is no longer used by WebOb and will be removed from a future " - "version of WebOb. If you require FakeCGIBody please make a copy into " - "you own project", - DeprecationWarning, - ) - - if content_type.startswith("multipart/form-data"): - if not _get_multipart_boundary(content_type): - raise ValueError( - "Content-type: %r does not contain boundary" % content_type - ) - self.vars = vars - self.content_type = content_type - self.file = None - - def __repr__(self): - inner = repr(self.vars) - if len(inner) > 20: - inner = inner[:15] + "..." + inner[-5:] - return "<%s at 0x%x viewing %s>" % ( - self.__class__.__name__, - abs(id(self)), - inner, - ) - - def fileno(self): - return None - - @staticmethod - def readable(): - return True - def readinto(self, buff): - if self.file is None: - if self.content_type.startswith("application/x-www-form-urlencoded"): - data = "&".join( - "%s=%s" - % (quote_plus(bytes_(k, "utf8")), quote_plus(bytes_(v, "utf8"))) - for k, v in self.vars.items() - ) - self.file = io.BytesIO(bytes_(data)) - elif self.content_type.startswith("multipart/form-data"): - self.file = _encode_multipart( - self.vars.items(), self.content_type, fout=io.BytesIO() - )[1] - self.file.seek(0) - else: - assert 0, "Bad content type: %r" % self.content_type - return self.file.readinto(buff) + return sz def _get_multipart_boundary(ctype): m = re.search(r"boundary=([^ ]+)", ctype, re.I) + if m: - return native_(m.group(1).strip('"')) + return text_(m.group(1).strip('"')) def _encode_multipart(vars, content_type, fout=None): @@ -1663,21 +1660,26 @@ def _encode_multipart(vars, content_type, fout=None): CRLF = b"\r\n" boundary = _get_multipart_boundary(content_type) + if not boundary: - boundary = native_(binascii.hexlify(os.urandom(10))) + boundary = text_(binascii.hexlify(os.urandom(10))) content_type += "; boundary=%s" % boundary + for name, value in vars: w(b"--") wt(boundary) w(CRLF) wt("Content-Disposition: form-data") + if name is not None: wt('; name="%s"' % name) filename = None + if getattr(value, "filename", None): filename = value.filename elif isinstance(value, (list, tuple)): filename, value = value + if hasattr(value, "read"): value = value.read() @@ -1690,8 +1692,10 @@ def _encode_multipart(vars, content_type, fout=None): w(CRLF) # TODO: should handle value.disposition_options + if getattr(value, "type", None): wt("Content-type: %s" % value.type) + if value.type_options: for ct_name, ct_value in sorted(value.type_options.items()): wt('; %s="%s"' % (ct_name, ct_value)) @@ -1700,14 +1704,17 @@ def _encode_multipart(vars, content_type, fout=None): wt("Content-type: %s" % mime_type) w(CRLF) w(CRLF) + if hasattr(value, "value"): value = value.value + if isinstance(value, bytes): w(value) else: wt(value) w(CRLF) wt("--%s--" % boundary) + if fout: return content_type, fout else: @@ -1716,6 +1723,7 @@ def _encode_multipart(vars, content_type, fout=None): def detect_charset(ctype): m = CHARSET_RE.search(ctype) + if m: return m.group(1).strip('"').strip() @@ -1727,7 +1735,7 @@ def _is_utf8(charset): return charset.lower().replace("-", "") == "utf8" -class Transcoder(object): +class Transcoder: def __init__(self, charset, errors="strict"): self.charset = charset # source charset self.errors = errors # unicode errors @@ -1735,37 +1743,26 @@ class Transcoder(object): def transcode_query(self, q): q_orig = q + if "=" not in q: # this doesn't look like a form submission + return q_orig - if PY2: - q = urlparse.parse_qsl(q, self.charset) - t = self._trans - q = [(t(k), t(v)) for k, v in q] - else: - q = list(parse_qsl_text(q, self.charset)) + q = list(parse_qsl_text(q, self.charset)) return url_encode(q) def transcode_fs(self, fs, content_type): # transcode FieldStorage - if PY2: - - def decode(b): - if b is not None: - return b.decode(self.charset, self.errors) - else: - return b - - else: - - def decode(b): - return b + def decode(b): + return b data = [] + for field in fs.list or (): field.name = decode(field.name) + if field.filename: field.filename = decode(field.filename) data.append((field.name, field)) @@ -1774,4 +1771,5 @@ class Transcoder(object): # TODO: transcode big requests to temp file content_type, fout = _encode_multipart(data, content_type, fout=io.BytesIO()) + return fout |
