Skip to content

Commit 8e1e361

Browse files
committed
fix: add separator option to cgi api
1 parent 93730b7 commit 8e1e361

3 files changed

Lines changed: 37 additions & 17 deletions

File tree

Lib/cgi.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ def closelog():
115115
# 0 ==> unlimited input
116116
maxlen = 0
117117

118-
def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
118+
def parse(fp=None, environ=os.environ, keep_blank_values=0,
119+
strict_parsing=0, separator='&'):
119120
"""Parse a query in the environment or from a file (default stdin)
120121
121122
Arguments, all optional:
@@ -134,6 +135,9 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
134135
strict_parsing: flag indicating what to do with parsing errors.
135136
If false (the default), errors are silently ignored.
136137
If true, errors raise a ValueError exception.
138+
139+
separator: str. The symbol to use for separating the query arguments.
140+
Defaults to &.
137141
"""
138142
if fp is None:
139143
fp = sys.stdin
@@ -154,7 +158,7 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
154158
if environ['REQUEST_METHOD'] == 'POST':
155159
ctype, pdict = parse_header(environ['CONTENT_TYPE'])
156160
if ctype == 'multipart/form-data':
157-
return parse_multipart(fp, pdict)
161+
return parse_multipart(fp, pdict, separator=separator)
158162
elif ctype == 'application/x-www-form-urlencoded':
159163
clength = int(environ['CONTENT_LENGTH'])
160164
if maxlen and clength > maxlen:
@@ -178,10 +182,10 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
178182
qs = ""
179183
environ['QUERY_STRING'] = qs # XXX Shouldn't, really
180184
return urllib.parse.parse_qs(qs, keep_blank_values, strict_parsing,
181-
encoding=encoding)
185+
encoding=encoding, separator=separator)
182186

183187

184-
def parse_multipart(fp, pdict, encoding="utf-8", errors="replace"):
188+
def parse_multipart(fp, pdict, encoding="utf-8", errors="replace", separator='&'):
185189
"""Parse multipart input.
186190
187191
Arguments:
@@ -205,7 +209,7 @@ def parse_multipart(fp, pdict, encoding="utf-8", errors="replace"):
205209
except KeyError:
206210
pass
207211
fs = FieldStorage(fp, headers=headers, encoding=encoding, errors=errors,
208-
environ={'REQUEST_METHOD': 'POST'})
212+
environ={'REQUEST_METHOD': 'POST'}, separator=separator)
209213
return {k: fs.getlist(k) for k in fs}
210214

211215
def _parseparam(s):
@@ -315,7 +319,7 @@ class FieldStorage:
315319
def __init__(self, fp=None, headers=None, outerboundary=b'',
316320
environ=os.environ, keep_blank_values=0, strict_parsing=0,
317321
limit=None, encoding='utf-8', errors='replace',
318-
max_num_fields=None):
322+
max_num_fields=None, separator='&'):
319323
"""Constructor. Read multipart/* until last part.
320324
321325
Arguments, all optional:
@@ -363,6 +367,7 @@ def __init__(self, fp=None, headers=None, outerboundary=b'',
363367
self.keep_blank_values = keep_blank_values
364368
self.strict_parsing = strict_parsing
365369
self.max_num_fields = max_num_fields
370+
self.separator = separator
366371
if 'REQUEST_METHOD' in environ:
367372
method = environ['REQUEST_METHOD'].upper()
368373
self.qs_on_post = None
@@ -589,7 +594,7 @@ def read_urlencoded(self):
589594
query = urllib.parse.parse_qsl(
590595
qs, self.keep_blank_values, self.strict_parsing,
591596
encoding=self.encoding, errors=self.errors,
592-
max_num_fields=self.max_num_fields)
597+
max_num_fields=self.max_num_fields, separator=self.separator)
593598
self.list = [MiniFieldStorage(key, value) for key, value in query]
594599
self.skip_lines()
595600

@@ -605,7 +610,7 @@ def read_multi(self, environ, keep_blank_values, strict_parsing):
605610
query = urllib.parse.parse_qsl(
606611
self.qs_on_post, self.keep_blank_values, self.strict_parsing,
607612
encoding=self.encoding, errors=self.errors,
608-
max_num_fields=self.max_num_fields)
613+
max_num_fields=self.max_num_fields, separator=self.separator)
609614
self.list.extend(MiniFieldStorage(key, value) for key, value in query)
610615

611616
klass = self.FieldStorageClass or self.__class__
@@ -649,7 +654,7 @@ def read_multi(self, environ, keep_blank_values, strict_parsing):
649654
else self.limit - self.bytes_read
650655
part = klass(self.fp, headers, ib, environ, keep_blank_values,
651656
strict_parsing, limit,
652-
self.encoding, self.errors, max_num_fields)
657+
self.encoding, self.errors, max_num_fields, self.separator)
653658

654659
if max_num_fields is not None:
655660
max_num_fields -= 1

Lib/test/test_urlparse.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
(b"&a=b", [(b'a', b'b')]),
3333
(b"a=a+b&b=b+c", [(b'a', b'a b'), (b'b', b'b c')]),
3434
(b"a=1&a=2", [(b'a', b'1'), (b'a', b'2')]),
35+
(";a=b", [(';a', 'b')]),
36+
("a=a+b;b=b+c", [('a', 'a b;b=b c')]),
37+
(b";a=b", [(b';a', b'b')]),
38+
(b"a=a+b;b=b+c", [(b'a', b'a b;b=b c')]),
3539
]
3640

3741
# Each parse_qs testcase is a two-tuple that contains
@@ -58,6 +62,10 @@
5862
(b"&a=b", {b'a': [b'b']}),
5963
(b"a=a+b&b=b+c", {b'a': [b'a b'], b'b': [b'b c']}),
6064
(b"a=1&a=2", {b'a': [b'1', b'2']}),
65+
(";a=b", {';a': ['b']}),
66+
("a=a+b;b=b+c", {'a': ['a b;b=b c']}),
67+
(b";a=b", {b';a': [b'b']}),
68+
(b"a=a+b;b=b+c", {b'a':[ b'a b;b=b c']}),
6169
]
6270

6371
class UrlParseTestCase(unittest.TestCase):
@@ -869,7 +877,7 @@ def test_parse_qsl_max_num_fields(self):
869877
urllib.parse.parse_qs('&'.join(['a=a']*10), max_num_fields=10)
870878

871879
def test_parse_qs_separator(self):
872-
semicolon_cases = [
880+
parse_qs_semicolon_cases = [
873881
(";", {}),
874882
(";;", {}),
875883
(";a=b", {'a': ['b']}),
@@ -881,13 +889,14 @@ def test_parse_qs_separator(self):
881889
(b"a=a+b;b=b+c", {b'a': [b'a b'], b'b': [b'b c']}),
882890
(b"a=1;a=2", {b'a': [b'1', b'2']}),
883891
]
884-
for orig, expect in semicolon_cases:
885-
result = urllib.parse.parse_qs(orig, separator=';')
886-
self.assertEqual(result, expect, "Error parsing %r" % orig)
892+
for orig, expect in parse_qs_semicolon_cases:
893+
with self.subTest(f"Original: {orig!r}, Expected: {expect!r}"):
894+
result = urllib.parse.parse_qs(orig, separator=';')
895+
self.assertEqual(result, expect, "Error parsing %r" % orig)
887896

888897

889898
def test_parse_qsl_separator(self):
890-
semicolon_cases = [
899+
parse_qsl_semicolon_cases = [
891900
(";", []),
892901
(";;", []),
893902
(";a=b", [('a', 'b')]),
@@ -899,9 +908,11 @@ def test_parse_qsl_separator(self):
899908
(b"a=a+b;b=b+c", [(b'a', b'a b'), (b'b', b'b c')]),
900909
(b"a=1;a=2", [(b'a', b'1'), (b'a', b'2')]),
901910
]
902-
for orig, expect in semicolon_cases:
903-
result = urllib.parse.parse_qsl(orig, separator=';')
904-
self.assertEqual(result, expect, "Error parsing %r" % orig)
911+
for orig, expect in parse_qsl_semicolon_cases:
912+
with self.subTest(f"Original: {orig!r}, Expected: {expect!r}"):
913+
result = urllib.parse.parse_qsl(orig, separator=';')
914+
self.assertEqual(result, expect, "Error parsing %r" % orig)
915+
905916

906917
def test_urlencode_sequences(self):
907918
# Other tests incidentally urlencode things; test non-covered cases:

Lib/urllib/parse.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,10 @@ def parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
734734
"""
735735
qs, _coerce_result = _coerce_args(qs)
736736

737+
if not separator or (not isinstance(separator, str)
738+
and not isinstance(separator, bytes)):
739+
raise ValueError("Separator must be of type string or bytes.")
740+
737741
# If max_num_fields is defined then check that the number of fields
738742
# is less than max_num_fields. This prevents a memory exhaustion DOS
739743
# attack via post bodies with many fields.

0 commit comments

Comments
 (0)