diff --git a/mercurial/hgweb/hgwebdir_mod.py b/mercurial/hgweb/hgwebdir_mod.py --- a/mercurial/hgweb/hgwebdir_mod.py +++ b/mercurial/hgweb/hgwebdir_mod.py @@ -293,8 +293,14 @@ # variable. # TODO this is kind of hacky and we should have a better # way of doing this than with REPO_NAME side-effects. + previousdata = ( + wsgireq.req.postformlist, + wsgireq.req.postformdict, + wsgireq.req.params, + ) wsgireq.req = requestmod.parserequestfromenv( - wsgireq.env, wsgireq.req.bodyfh) + wsgireq.env, wsgireq.req.bodyfh, + previousformdata=previousdata) try: # ensure caller gets private copy of ui repo = hg.repository(self.ui.copy(), real) diff --git a/mercurial/hgweb/request.py b/mercurial/hgweb/request.py --- a/mercurial/hgweb/request.py +++ b/mercurial/hgweb/request.py @@ -96,8 +96,15 @@ headers = attr.ib() # Request body input stream. bodyfh = attr.ib() + # Like ``querystringlist`` and ``querystringdict`` but for form data + # submitted on POST requests decoded from well-known content types. + postformlist = attr.ib() + postformdict = attr.ib() -def parserequestfromenv(env, bodyfh): + # All "form" parameters. A combination of query string and POST form data. + params = attr.ib() + +def parserequestfromenv(env, bodyfh, previousformdata=None): """Parse URL components from environment variables. WSGI defines request attributes via environment variables. This function @@ -220,6 +227,49 @@ #if 'Content-Length' in headers: # bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length'])) + # Form data is kinda wonky. It can come from request bodies, which we can + # only read once. Since hgwebdir may construct a new parsedrequest, we + # allow existing form data to be passed in to this function. That's kinda + # hacky. + if previousformdata is None: + if env['REQUEST_METHOD'] == 'POST': + # This is based on cgi.parse(), but without the hacky parts (like + # merging QUERY_STRING and setting QUERY_STRING as a side-effect). + ct, params = cgi.parse_header(env.get('CONTENT_TYPE', '')) + if ct == 'multipart/form-data': + # We don't have a way to preserve order. So we normalize to a + # list for consistency with x-www-form-urlencoded. + postformlist = [] + for k, l in cgi.parse_multipart(bodyfh, params).iteritems(): + for v in l: + postformlist.append((k, v)) + elif ct == 'application/x-www-form-urlencoded': + cl = int(headers['Content-Length']) + postformlist = util.urlreq.parseqsl(bodyfh.read(cl), + keep_blank_values=True) + else: + postformlist = [] + + postformdict = {} + for k, v in postformdict: + if k in postformdict: + postformdict[k].append(v) + else: + postformdict[k] = [v] + else: + postformlist = [] + postformdict = {} + + # Now that we have the raw post data. Merge in query string data to + # provide a unified interface. + formdict = {k: list(v) for k, v in postformdict.iteritems()} + for k, l in querystringdict.iteritems(): + if k not in formdict: + formdict[k] = [] + formdict[k].extend(l) + else: + postformlist, postformdict, formdict = previousformdata + return parsedrequest(method=env['REQUEST_METHOD'], url=fullurl, baseurl=baseurl, advertisedurl=advertisedfullurl, @@ -231,7 +281,9 @@ querystringlist=querystringlist, querystringdict=querystringdict, headers=headers, - bodyfh=bodyfh) + bodyfh=bodyfh, + postformlist=postformlist, postformdict=postformdict, + params=formdict) class wsgirequest(object): """Higher-level API for a WSGI request. @@ -258,15 +310,12 @@ self.multiprocess = wsgienv[r'wsgi.multiprocess'] self.run_once = wsgienv[r'wsgi.run_once'] self.env = wsgienv - self.form = normalize(cgi.parse(inp, - self.env, - keep_blank_values=1)) + self.req = parserequestfromenv(wsgienv, inp) + self.form = normalize(self.req.params) self._start_response = start_response self.server_write = None self.headers = [] - self.req = parserequestfromenv(wsgienv, inp) - def respond(self, status, type, filename=None, body=None): if not isinstance(type, str): type = pycompat.sysstr(type)