1from test.test_support import run_unittest, check_warnings
2import cgi
3import os
4import sys
5import tempfile
6import unittest
7
8from collections import namedtuple
9
10class HackedSysModule:
11    # The regression test will have real values in sys.argv, which
12    # will completely confuse the test of the cgi module
13    argv = []
14    stdin = sys.stdin
15
16cgi.sys = HackedSysModule()
17
18try:
19    from cStringIO import StringIO
20except ImportError:
21    from StringIO import StringIO
22
23class ComparableException:
24    def __init__(self, err):
25        self.err = err
26
27    def __str__(self):
28        return str(self.err)
29
30    def __cmp__(self, anExc):
31        if not isinstance(anExc, Exception):
32            return -1
33        x = cmp(self.err.__class__, anExc.__class__)
34        if x != 0:
35            return x
36        return cmp(self.err.args, anExc.args)
37
38    def __getattr__(self, attr):
39        return getattr(self.err, attr)
40
41def do_test(buf, method):
42    env = {}
43    if method == "GET":
44        fp = None
45        env['REQUEST_METHOD'] = 'GET'
46        env['QUERY_STRING'] = buf
47    elif method == "POST":
48        fp = StringIO(buf)
49        env['REQUEST_METHOD'] = 'POST'
50        env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
51        env['CONTENT_LENGTH'] = str(len(buf))
52    else:
53        raise ValueError, "unknown method: %s" % method
54    try:
55        return cgi.parse(fp, env, strict_parsing=1)
56    except StandardError, err:
57        return ComparableException(err)
58
59parse_strict_test_cases = [
60    ("", ValueError("bad query field: ''")),
61    ("&", ValueError("bad query field: ''")),
62    ("&&", ValueError("bad query field: ''")),
63    (";", ValueError("bad query field: ''")),
64    (";&;", ValueError("bad query field: ''")),
65    # Should the next few really be valid?
66    ("=", {}),
67    ("=&=", {}),
68    ("=;=", {}),
69    # This rest seem to make sense
70    ("=a", {'': ['a']}),
71    ("&=a", ValueError("bad query field: ''")),
72    ("=a&", ValueError("bad query field: ''")),
73    ("=&a", ValueError("bad query field: 'a'")),
74    ("b=a", {'b': ['a']}),
75    ("b+=a", {'b ': ['a']}),
76    ("a=b=a", {'a': ['b=a']}),
77    ("a=+b=a", {'a': [' b=a']}),
78    ("&b=a", ValueError("bad query field: ''")),
79    ("b&=a", ValueError("bad query field: 'b'")),
80    ("a=a+b&b=b+c", {'a': ['a b'], 'b': ['b c']}),
81    ("a=a+b&a=b+a", {'a': ['a b', 'b a']}),
82    ("x=1&y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
83    ("x=1;y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
84    ("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
85    ("Hbc5161168c542333633315dee1182227:key_store_seqid=400006&cuyer=r&view=bustomer&order_id=0bb2e248638833d48cb7fed300000f1b&expire=964546263&lobale=en-US&kid=130003.300038&ss=env",
86     {'Hbc5161168c542333633315dee1182227:key_store_seqid': ['400006'],
87      'cuyer': ['r'],
88      'expire': ['964546263'],
89      'kid': ['130003.300038'],
90      'lobale': ['en-US'],
91      'order_id': ['0bb2e248638833d48cb7fed300000f1b'],
92      'ss': ['env'],
93      'view': ['bustomer'],
94      }),
95
96    ("group_id=5470&set=custom&_assigned_to=31392&_status=1&_category=100&SUBMIT=Browse",
97     {'SUBMIT': ['Browse'],
98      '_assigned_to': ['31392'],
99      '_category': ['100'],
100      '_status': ['1'],
101      'group_id': ['5470'],
102      'set': ['custom'],
103      })
104    ]
105
106def first_elts(list):
107    return map(lambda x:x[0], list)
108
109def first_second_elts(list):
110    return map(lambda p:(p[0], p[1][0]), list)
111
112def gen_result(data, environ):
113    fake_stdin = StringIO(data)
114    fake_stdin.seek(0)
115    form = cgi.FieldStorage(fp=fake_stdin, environ=environ)
116
117    result = {}
118    for k, v in dict(form).items():
119        result[k] = isinstance(v, list) and form.getlist(k) or v.value
120
121    return result
122
123class CgiTests(unittest.TestCase):
124
125    def test_escape(self):
126        self.assertEqual("test & string", cgi.escape("test & string"))
127        self.assertEqual("&lt;test string&gt;", cgi.escape("<test string>"))
128        self.assertEqual("&quot;test string&quot;", cgi.escape('"test string"', True))
129
130    def test_strict(self):
131        for orig, expect in parse_strict_test_cases:
132            # Test basic parsing
133            d = do_test(orig, "GET")
134            self.assertEqual(d, expect, "Error parsing %s" % repr(orig))
135            d = do_test(orig, "POST")
136            self.assertEqual(d, expect, "Error parsing %s" % repr(orig))
137
138            env = {'QUERY_STRING': orig}
139            fcd = cgi.FormContentDict(env)
140            sd = cgi.SvFormContentDict(env)
141            fs = cgi.FieldStorage(environ=env)
142            if isinstance(expect, dict):
143                # test dict interface
144                self.assertEqual(len(expect), len(fcd))
145                self.assertItemsEqual(expect.keys(), fcd.keys())
146                self.assertItemsEqual(expect.values(), fcd.values())
147                self.assertItemsEqual(expect.items(), fcd.items())
148                self.assertEqual(fcd.get("nonexistent field", "default"), "default")
149                self.assertEqual(len(sd), len(fs))
150                self.assertItemsEqual(sd.keys(), fs.keys())
151                self.assertEqual(fs.getvalue("nonexistent field", "default"), "default")
152                # test individual fields
153                for key in expect.keys():
154                    expect_val = expect[key]
155                    self.assertTrue(fcd.has_key(key))
156                    self.assertItemsEqual(fcd[key], expect[key])
157                    self.assertEqual(fcd.get(key, "default"), fcd[key])
158                    self.assertTrue(fs.has_key(key))
159                    if len(expect_val) > 1:
160                        single_value = 0
161                    else:
162                        single_value = 1
163                    try:
164                        val = sd[key]
165                    except IndexError:
166                        self.assertFalse(single_value)
167                        self.assertEqual(fs.getvalue(key), expect_val)
168                    else:
169                        self.assertTrue(single_value)
170                        self.assertEqual(val, expect_val[0])
171                        self.assertEqual(fs.getvalue(key), expect_val[0])
172                    self.assertItemsEqual(sd.getlist(key), expect_val)
173                    if single_value:
174                        self.assertItemsEqual(sd.values(),
175                                                first_elts(expect.values()))
176                        self.assertItemsEqual(sd.items(),
177                                                first_second_elts(expect.items()))
178
179    def test_weird_formcontentdict(self):
180        # Test the weird FormContentDict classes
181        env = {'QUERY_STRING': "x=1&y=2.0&z=2-3.%2b0&1=1abc"}
182        expect = {'x': 1, 'y': 2.0, 'z': '2-3.+0', '1': '1abc'}
183        d = cgi.InterpFormContentDict(env)
184        for k, v in expect.items():
185            self.assertEqual(d[k], v)
186        for k, v in d.items():
187            self.assertEqual(expect[k], v)
188        self.assertItemsEqual(expect.values(), d.values())
189
190    def test_log(self):
191        cgi.log("Testing")
192
193        cgi.logfp = StringIO()
194        cgi.initlog("%s", "Testing initlog 1")
195        cgi.log("%s", "Testing log 2")
196        self.assertEqual(cgi.logfp.getvalue(), "Testing initlog 1\nTesting log 2\n")
197        if os.path.exists("/dev/null"):
198            cgi.logfp = None
199            cgi.logfile = "/dev/null"
200            cgi.initlog("%s", "Testing log 3")
201            cgi.log("Testing log 4")
202
203    def test_fieldstorage_readline(self):
204        # FieldStorage uses readline, which has the capacity to read all
205        # contents of the input file into memory; we use readline's size argument
206        # to prevent that for files that do not contain any newlines in
207        # non-GET/HEAD requests
208        class TestReadlineFile:
209            def __init__(self, file):
210                self.file = file
211                self.numcalls = 0
212
213            def readline(self, size=None):
214                self.numcalls += 1
215                if size:
216                    return self.file.readline(size)
217                else:
218                    return self.file.readline()
219
220            def __getattr__(self, name):
221                file = self.__dict__['file']
222                a = getattr(file, name)
223                if not isinstance(a, int):
224                    setattr(self, name, a)
225                return a
226
227        f = TestReadlineFile(tempfile.TemporaryFile())
228        f.write('x' * 256 * 1024)
229        f.seek(0)
230        env = {'REQUEST_METHOD':'PUT'}
231        fs = cgi.FieldStorage(fp=f, environ=env)
232        # if we're not chunking properly, readline is only called twice
233        # (by read_binary); if we are chunking properly, it will be called 5 times
234        # as long as the chunksize is 1 << 16.
235        self.assertGreater(f.numcalls, 2)
236
237    def test_fieldstorage_invalid(self):
238        fs = cgi.FieldStorage()
239        self.assertFalse(fs)
240        self.assertRaises(TypeError, bool(fs))
241        self.assertEqual(list(fs), list(fs.keys()))
242        fs.list.append(namedtuple('MockFieldStorage', 'name')('fieldvalue'))
243        self.assertTrue(fs)
244
245    def test_fieldstorage_multipart(self):
246        #Test basic FieldStorage multipart parsing
247        env = {'REQUEST_METHOD':'POST', 'CONTENT_TYPE':'multipart/form-data; boundary=---------------------------721837373350705526688164684', 'CONTENT_LENGTH':'558'}
248        postdata = """-----------------------------721837373350705526688164684
249Content-Disposition: form-data; name="id"
250
2511234
252-----------------------------721837373350705526688164684
253Content-Disposition: form-data; name="title"
254
255
256-----------------------------721837373350705526688164684
257Content-Disposition: form-data; name="file"; filename="test.txt"
258Content-Type: text/plain
259
260Testing 123.
261
262-----------------------------721837373350705526688164684
263Content-Disposition: form-data; name="submit"
264
265 Add\x20
266-----------------------------721837373350705526688164684--
267"""
268        fs = cgi.FieldStorage(fp=StringIO(postdata), environ=env)
269        self.assertEqual(len(fs.list), 4)
270        expect = [{'name':'id', 'filename':None, 'value':'1234'},
271                  {'name':'title', 'filename':None, 'value':''},
272                  {'name':'file', 'filename':'test.txt','value':'Testing 123.\n'},
273                  {'name':'submit', 'filename':None, 'value':' Add '}]
274        for x in range(len(fs.list)):
275            for k, exp in expect[x].items():
276                got = getattr(fs.list[x], k)
277                self.assertEqual(got, exp)
278
279    def test_fieldstorage_multipart_maxline(self):
280        # Issue #18167
281        maxline = 1 << 16
282        self.maxDiff = None
283        def check(content):
284            data = """
285---123
286Content-Disposition: form-data; name="upload"; filename="fake.txt"
287Content-Type: text/plain
288
289%s
290---123--
291""".replace('\n', '\r\n') % content
292            environ = {
293                'CONTENT_LENGTH':   str(len(data)),
294                'CONTENT_TYPE':     'multipart/form-data; boundary=-123',
295                'REQUEST_METHOD':   'POST',
296            }
297            self.assertEqual(gen_result(data, environ), {'upload': content})
298        check('x' * (maxline - 1))
299        check('x' * (maxline - 1) + '\r')
300        check('x' * (maxline - 1) + '\r' + 'y' * (maxline - 1))
301
302    _qs_result = {
303        'key1': 'value1',
304        'key2': ['value2x', 'value2y'],
305        'key3': 'value3',
306        'key4': 'value4'
307    }
308    def testQSAndUrlEncode(self):
309        data = "key2=value2x&key3=value3&key4=value4"
310        environ = {
311            'CONTENT_LENGTH':   str(len(data)),
312            'CONTENT_TYPE':     'application/x-www-form-urlencoded',
313            'QUERY_STRING':     'key1=value1&key2=value2y',
314            'REQUEST_METHOD':   'POST',
315        }
316        v = gen_result(data, environ)
317        self.assertEqual(self._qs_result, v)
318
319    def testQSAndFormData(self):
320        data = """
321---123
322Content-Disposition: form-data; name="key2"
323
324value2y
325---123
326Content-Disposition: form-data; name="key3"
327
328value3
329---123
330Content-Disposition: form-data; name="key4"
331
332value4
333---123--
334"""
335        environ = {
336            'CONTENT_LENGTH':   str(len(data)),
337            'CONTENT_TYPE':     'multipart/form-data; boundary=-123',
338            'QUERY_STRING':     'key1=value1&key2=value2x',
339            'REQUEST_METHOD':   'POST',
340        }
341        v = gen_result(data, environ)
342        self.assertEqual(self._qs_result, v)
343
344    def testQSAndFormDataFile(self):
345        data = """
346---123
347Content-Disposition: form-data; name="key2"
348
349value2y
350---123
351Content-Disposition: form-data; name="key3"
352
353value3
354---123
355Content-Disposition: form-data; name="key4"
356
357value4
358---123
359Content-Disposition: form-data; name="upload"; filename="fake.txt"
360Content-Type: text/plain
361
362this is the content of the fake file
363
364---123--
365"""
366        environ = {
367            'CONTENT_LENGTH':   str(len(data)),
368            'CONTENT_TYPE':     'multipart/form-data; boundary=-123',
369            'QUERY_STRING':     'key1=value1&key2=value2x',
370            'REQUEST_METHOD':   'POST',
371        }
372        result = self._qs_result.copy()
373        result.update({
374            'upload': 'this is the content of the fake file\n'
375        })
376        v = gen_result(data, environ)
377        self.assertEqual(result, v)
378
379    def test_deprecated_parse_qs(self):
380        # this func is moved to urlparse, this is just a sanity check
381        with check_warnings(('cgi.parse_qs is deprecated, use urlparse.'
382                             'parse_qs instead', PendingDeprecationWarning)):
383            self.assertEqual({'a': ['A1'], 'B': ['B3'], 'b': ['B2']},
384                             cgi.parse_qs('a=A1&b=B2&B=B3'))
385
386    def test_deprecated_parse_qsl(self):
387        # this func is moved to urlparse, this is just a sanity check
388        with check_warnings(('cgi.parse_qsl is deprecated, use urlparse.'
389                             'parse_qsl instead', PendingDeprecationWarning)):
390            self.assertEqual([('a', 'A1'), ('b', 'B2'), ('B', 'B3')],
391                             cgi.parse_qsl('a=A1&b=B2&B=B3'))
392
393    def test_parse_header(self):
394        self.assertEqual(
395            cgi.parse_header("text/plain"),
396            ("text/plain", {}))
397        self.assertEqual(
398            cgi.parse_header("text/vnd.just.made.this.up ; "),
399            ("text/vnd.just.made.this.up", {}))
400        self.assertEqual(
401            cgi.parse_header("text/plain;charset=us-ascii"),
402            ("text/plain", {"charset": "us-ascii"}))
403        self.assertEqual(
404            cgi.parse_header('text/plain ; charset="us-ascii"'),
405            ("text/plain", {"charset": "us-ascii"}))
406        self.assertEqual(
407            cgi.parse_header('text/plain ; charset="us-ascii"; another=opt'),
408            ("text/plain", {"charset": "us-ascii", "another": "opt"}))
409        self.assertEqual(
410            cgi.parse_header('attachment; filename="silly.txt"'),
411            ("attachment", {"filename": "silly.txt"}))
412        self.assertEqual(
413            cgi.parse_header('attachment; filename="strange;name"'),
414            ("attachment", {"filename": "strange;name"}))
415        self.assertEqual(
416            cgi.parse_header('attachment; filename="strange;name";size=123;'),
417            ("attachment", {"filename": "strange;name", "size": "123"}))
418        self.assertEqual(
419            cgi.parse_header('form-data; name="files"; filename="fo\\"o;bar"'),
420            ("form-data", {"name": "files", "filename": 'fo"o;bar'}))
421
422
423def test_main():
424    run_unittest(CgiTests)
425
426if __name__ == '__main__':
427    test_main()
428