• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / Zope / 5553818016

pending completion
5553818016

push

github

web-flow
fix (and test) limit for  `request.BODY` (#1143)

4330 of 6918 branches covered (62.59%)

Branch coverage included in aggregate %.

24 of 24 new or added lines in 2 files covered. (100.0%)

27297 of 31542 relevant lines covered (86.54%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.73
/src/ZPublisher/HTTPRequest.py
1
##############################################################################
2
#
3
# Copyright (c) 2002-2009 Zope Foundation and Contributors.
4
# All Rights Reserved.
5
#
6
# This software is subject to the provisions of the Zope Public License,
7
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE.
12
#
13
##############################################################################
14
""" HTTP request management.
1✔
15
"""
16

17
import codecs
1✔
18
import html
1✔
19
import os
1✔
20
import random
1✔
21
import re
1✔
22
import time
1✔
23
from types import SimpleNamespace
1✔
24
from urllib.parse import parse_qsl
1✔
25
from urllib.parse import unquote
1✔
26
from urllib.parse import urlparse
1✔
27

28
from AccessControl.tainted import should_be_tainted as base_should_be_tainted
1✔
29
from AccessControl.tainted import taint_string
1✔
30
from multipart import Headers
1✔
31
from multipart import MultipartParser
1✔
32
from multipart import parse_options_header
1✔
33
from zExceptions import BadRequest
1✔
34
from zope.component import queryUtility
1✔
35
from zope.i18n.interfaces import IUserPreferredLanguages
1✔
36
from zope.i18n.locales import LoadLocaleError
1✔
37
from zope.i18n.locales import locales
1✔
38
from zope.interface import directlyProvidedBy
1✔
39
from zope.interface import directlyProvides
1✔
40
from zope.interface import implementer
1✔
41
from zope.publisher.base import DebugFlags
1✔
42
from zope.publisher.http import splitport
1✔
43
from zope.publisher.interfaces.browser import IBrowserRequest
1✔
44
from ZPublisher import xmlrpc
1✔
45
from ZPublisher.BaseRequest import BaseRequest
1✔
46
from ZPublisher.BaseRequest import quote
1✔
47
from ZPublisher.Converters import get_converter
1✔
48
from ZPublisher.interfaces import IXmlrpcChecker
1✔
49
from ZPublisher.utils import basic_auth_decode
1✔
50

51
from .cookie import getCookieValuePolicy
1✔
52

53

54
# DOS attack protection -- limiting the amount of memory for forms
55
# probably should become configurable
56
FORM_MEMORY_LIMIT = 2 ** 20   # memory limit for forms
1✔
57
FORM_DISK_LIMIT = 2 ** 30     # disk limit for forms
1✔
58
FORM_MEMFILE_LIMIT = 2 ** 12  # limit for `BytesIO` -> temporary file switch
1✔
59

60

61
# This may get overwritten during configuration
62
default_encoding = 'utf-8'
1✔
63

64
isCGI_NAMEs = {
1✔
65
    'SERVER_SOFTWARE': 1,
66
    'SERVER_NAME': 1,
67
    'GATEWAY_INTERFACE': 1,
68
    'SERVER_PROTOCOL': 1,
69
    'SERVER_PORT': 1,
70
    'REQUEST_METHOD': 1,
71
    'PATH_INFO': 1,
72
    'PATH_TRANSLATED': 1,
73
    'SCRIPT_NAME': 1,
74
    'QUERY_STRING': 1,
75
    'REMOTE_HOST': 1,
76
    'REMOTE_ADDR': 1,
77
    'AUTH_TYPE': 1,
78
    'REMOTE_USER': 1,
79
    'REMOTE_IDENT': 1,
80
    'CONTENT_TYPE': 1,
81
    'CONTENT_LENGTH': 1,
82
    'SERVER_URL': 1,
83
}
84

85
isCGI_NAME = isCGI_NAMEs.__contains__
1✔
86

87
hide_key = {'HTTP_AUTHORIZATION': 1, 'HTTP_CGI_AUTHORIZATION': 1}
1✔
88

89
default_port = {'http': 80, 'https': 443}
1✔
90

91
tainting_env = str(os.environ.get('ZOPE_DTML_REQUEST_AUTOQUOTE', '')).lower()
1✔
92
TAINTING_ENABLED = tainting_env not in ('disabled', '0', 'no')
1✔
93

94
search_type = re.compile(r'(:[a-zA-Z][-a-zA-Z0-9_]+|\.[xy])$').search
1✔
95

96
_marker = []
1✔
97

98
# The trusted_proxies configuration setting contains a sequence
99
# of front-end proxies that are trusted to supply an accurate
100
# X_FORWARDED_FOR header. If REMOTE_ADDR is one of the values in this list
101
# and it has set an X_FORWARDED_FOR header, ZPublisher copies REMOTE_ADDR
102
# into X_FORWARDED_BY, and the last element of the X_FORWARDED_FOR list
103
# into REMOTE_ADDR. X_FORWARDED_FOR is left unchanged.
104
# The ZConfig machinery may sets this attribute on initialization
105
# if any trusted-proxies are defined in the configuration file.
106

107
trusted_proxies = []
1✔
108

109

110
class NestedLoopExit(Exception):
1✔
111
    pass
1✔
112

113

114
@implementer(IBrowserRequest)
1✔
115
class HTTPRequest(BaseRequest):
1✔
116
    """ Model HTTP request data.
117

118
    This object provides access to request data.  This includes, the
119
    input headers, form data, server data, and cookies.
120

121
    Request objects are created by the object publisher and will be
122
    passed to published objects through the argument name, REQUEST.
123

124
    The request object is a mapping object that represents a
125
    collection of variable to value mappings.  In addition, variables
126
    are divided into five categories:
127

128
      - Environment variables
129

130
        These variables include input headers, server data, and other
131
        request-related data.  The variable names are as specified
132
        in the <a href="https://tools.ietf.org/html/rfc3875">CGI
133
        specification</a>
134

135
      - Form data
136

137
        These are data extracted from either a URL-encoded query
138
        string or body, if present.
139

140
      - Cookies
141

142
        These are the cookie data, if present.
143

144
      - Lazy Data
145

146
        These are callables which are deferred until explicitly
147
        referenced, at which point they are resolved and stored as
148
        application data.
149

150
      - Other
151

152
        Data that may be set by an application object.
153

154
    The form attribute of a request is actually a Field Storage
155
    object.  When file uploads are used, this provides a richer and
156
    more complex interface than is provided by accessing form data as
157
    items of the request.  See the FieldStorage class documentation
158
    for more details.
159

160
    The request object may be used as a mapping object, in which case
161
    values will be looked up in the order: environment variables,
162
    other variables, form data, and then cookies.
163
    """
164

165
    _hacked_path = None
1✔
166
    args = ()
1✔
167
    _urls = ()
1✔
168
    _file = None
1✔
169

170
    charset = default_encoding
1✔
171
    retry_max_count = 0
1✔
172

173
    def supports_retry(self):
1✔
174
        return self.retry_count < self.retry_max_count
1✔
175

176
    def delay_retry(self):
1✔
177
        # Insert a delay before retrying. Moved here from supports_retry.
178
        time.sleep(random.uniform(0, 2 ** (self.retry_count)))
1✔
179

180
    def retry(self):
1✔
181
        self.retry_count = self.retry_count + 1
1✔
182
        self.stdin.seek(0)
1✔
183
        r = self.__class__(stdin=self.stdin,
1✔
184
                           environ=self._orig_env,
185
                           response=self.response.retry())
186
        r.retry_count = self.retry_count
1✔
187
        return r
1✔
188

189
    def clear(self):
1✔
190
        # Clear all references to the input stream, possibly
191
        # removing tempfiles.
192
        self.stdin = None
1✔
193
        self._file = None
1✔
194
        self._fs = None
1✔
195
        self.form.clear()
1✔
196
        # we want to clear the lazy dict here because BaseRequests don't have
197
        # one.  Without this, there's the possibility of memory leaking
198
        # after every request.
199
        self._lazies = {}
1✔
200
        BaseRequest.clear(self)
1✔
201

202
    def setServerURL(self, protocol=None, hostname=None, port=None):
1✔
203
        """ Set the parts of generated URLs. """
204
        other = self.other
1✔
205
        server_url = other.get('SERVER_URL', '')
1✔
206
        if protocol is None and hostname is None and port is None:
1!
207
            return server_url
×
208
        old_url = urlparse(server_url)
1✔
209
        if protocol is None:
1!
210
            protocol = old_url.scheme
×
211
        if hostname is None:
1!
212
            hostname = old_url.hostname
×
213
        if port is None:
1✔
214
            port = old_url.port
1✔
215

216
        if (port is None or default_port[protocol] == port):
1✔
217
            host = hostname
1✔
218
        else:
219
            host = f'{hostname}:{port}'
1✔
220
        server_url = other['SERVER_URL'] = f'{protocol}://{host}'
1✔
221
        self._resetURLS()
1✔
222
        return server_url
1✔
223

224
    def setVirtualRoot(self, path, hard=0):
1✔
225
        """ Treat the current publishing object as a VirtualRoot """
226
        other = self.other
1✔
227
        if isinstance(path, str):
1✔
228
            path = path.split('/')
1✔
229
        self._script[:] = list(map(quote, [_p for _p in path if _p]))
1✔
230
        del self._steps[:]
1✔
231
        parents = other['PARENTS']
1✔
232
        if hard:
1!
233
            del parents[:-1]
×
234
        other['VirtualRootPhysicalPath'] = parents[-1].getPhysicalPath()
1✔
235
        self._resetURLS()
1✔
236

237
    def getVirtualRoot(self):
1✔
238
        """ Return a slash-separated virtual root.
239

240
        If it is same as the physical root, return ''.
241
        """
242
        return '/'.join([''] + self._script)
1✔
243

244
    def physicalPathToVirtualPath(self, path):
1✔
245
        """ Remove the path to the VirtualRoot from a physical path """
246
        if isinstance(path, str):
1!
247
            path = path.split('/')
×
248
        rpp = self.other.get('VirtualRootPhysicalPath', ('',))
1✔
249
        i = 0
1✔
250
        for name in rpp[:len(path)]:
1✔
251
            if path[i] == name:
1!
252
                i = i + 1
1✔
253
            else:
254
                break
×
255
        return path[i:]
1✔
256

257
    def physicalPathToURL(self, path, relative=0):
1✔
258
        """ Convert a physical path into a URL in the current context """
259
        path = self._script + list(
1✔
260
            map(quote, self.physicalPathToVirtualPath(path)))
261
        if relative:
1✔
262
            path.insert(0, '')
1✔
263
        else:
264
            path.insert(0, self['SERVER_URL'])
1✔
265
        return '/'.join(path)
1✔
266

267
    def physicalPathFromURL(self, URL):
1✔
268
        """ Convert a URL into a physical path in the current context.
269
            If the URL makes no sense in light of the current virtual
270
            hosting context, a ValueError is raised."""
271
        other = self.other
1✔
272
        path = [_p for _p in URL.split('/') if _p]
1✔
273

274
        if URL.find('://') >= 0:
1!
275
            path = path[2:]
1✔
276

277
        # Check the path against BASEPATH1
278
        vhbase = self._script
1✔
279
        vhbl = len(vhbase)
1✔
280
        if path[:vhbl] == vhbase:
1!
281
            path = path[vhbl:]
1✔
282
        else:
283
            raise ValueError('Url does not match virtual hosting context')
×
284
        vrpp = other.get('VirtualRootPhysicalPath', ('',))
1✔
285
        return list(vrpp) + list(map(unquote, path))
1✔
286

287
    def _resetURLS(self):
1✔
288
        other = self.other
1✔
289
        other['URL'] = '/'.join(
1✔
290
            [other['SERVER_URL']] + self._script + self._steps)
291
        for x in self._urls:
1!
292
            del self.other[x]
×
293
        self._urls = ()
1✔
294

295
    def getClientAddr(self):
1✔
296
        """ The IP address of the client.
297
        """
298
        return self._client_addr
1✔
299

300
    def setupLocale(self):
1✔
301
        envadapter = IUserPreferredLanguages(self, None)
1✔
302
        if envadapter is None:
1!
303
            self._locale = None
×
304
            return
×
305

306
        langs = envadapter.getPreferredLanguages()
1✔
307
        for httplang in langs:
1✔
308
            parts = (httplang.split('-') + [None, None])[:3]
1✔
309
            try:
1✔
310
                self._locale = locales.getLocale(*parts)
1✔
311
                return
1✔
312
            except LoadLocaleError:
1✔
313
                # Just try the next combination
314
                pass
1✔
315
        else:
316
            # No combination gave us an existing locale, so use the default,
317
            # which is guaranteed to exist
318
            self._locale = locales.getLocale(None, None, None)
1✔
319

320
    def __init__(self, stdin, environ, response, clean=0):
1✔
321
        self.__doc__ = None  # Make HTTPRequest objects unpublishable
1✔
322
        self._orig_env = environ
1✔
323
        # Avoid the overhead of scrubbing the environment in the
324
        # case of request cloning for traversal purposes. If the
325
        # clean flag is set, we know we can use the passed in
326
        # environ dict directly.
327
        if not clean:
1✔
328
            environ = sane_environment(environ)
1✔
329

330
        if 'HTTP_AUTHORIZATION' in environ:
1✔
331
            self._auth = environ['HTTP_AUTHORIZATION']
1✔
332
            response._auth = 1
1✔
333
            del environ['HTTP_AUTHORIZATION']
1✔
334

335
        self.stdin = stdin
1✔
336
        self.environ = environ
1✔
337
        get_env = environ.get
1✔
338
        self.response = response
1✔
339
        other = self.other = {'RESPONSE': response}
1✔
340
        self.form = {}
1✔
341
        self.taintedform = {}
1✔
342
        self.steps = []
1✔
343
        self._steps = []
1✔
344
        self._lazies = {}
1✔
345
        self._debug = DebugFlags()
1✔
346
        # We don't set up the locale initially but just on first access
347
        self._locale = _marker
1✔
348

349
        if 'REMOTE_ADDR' in environ:
1✔
350
            self._client_addr = environ['REMOTE_ADDR']
1✔
351
            if 'HTTP_X_FORWARDED_FOR' in environ and \
1✔
352
               self._client_addr in trusted_proxies:
353
                # REMOTE_ADDR is one of our trusted local proxies.
354
                # Not really very remote at all.  The proxy can tell us the
355
                # IP of the real remote client in the forwarded-for header
356
                # Skip the proxy-address itself though
357
                forwarded_for = [
1✔
358
                    e.strip()
359
                    for e in environ['HTTP_X_FORWARDED_FOR'].split(',')]
360
                forwarded_for.reverse()
1✔
361
                for entry in forwarded_for:
1!
362
                    if entry not in trusted_proxies:
1✔
363
                        self._client_addr = entry
1✔
364
                        break
1✔
365
        else:
366
            self._client_addr = ''
1✔
367

368
        ################################################################
369
        # Get base info first. This isn't likely to cause
370
        # errors and might be useful to error handlers.
371
        b = script = get_env('SCRIPT_NAME', '').strip()
1✔
372

373
        # _script and the other _names are meant for URL construction
374
        self._script = list(map(quote, [_s for _s in script.split('/') if _s]))
1✔
375

376
        while b and b[-1] == '/':
1!
377
            b = b[:-1]
×
378
        p = b.rfind('/')
1✔
379
        if p >= 0:
1!
380
            b = b[:p + 1]
×
381
        else:
382
            b = ''
1✔
383
        while b and b[0] == '/':
1!
384
            b = b[1:]
×
385

386
        server_url = get_env('SERVER_URL', None)
1✔
387
        if server_url is not None:
1✔
388
            other['SERVER_URL'] = server_url = server_url.strip()
1✔
389
        else:
390
            https_environ = environ.get('HTTPS', False)
1✔
391
            if https_environ and https_environ in ('on', 'ON', '1'):
1✔
392
                protocol = 'https'
1✔
393
            elif environ.get('SERVER_PORT_SECURE', None) == 1:
1✔
394
                protocol = 'https'
1✔
395
            elif (environ.get('REQUEST_SCHEME', '') or '').lower() == 'https':
1✔
396
                protocol = 'https'
1✔
397
            elif environ.get('wsgi.url_scheme') == 'https':
1✔
398
                protocol = 'https'
1✔
399
            else:
400
                protocol = 'http'
1✔
401

402
            if 'HTTP_HOST' in environ:
1✔
403
                host = environ['HTTP_HOST'].strip()
1✔
404
                hostname, port = splitport(host)
1✔
405

406
                # NOTE: some (DAV) clients manage to forget the port. This
407
                # can be fixed with the commented code below - the problem
408
                # is that it causes problems for virtual hosting. I've left
409
                # the commented code here in case we care enough to come
410
                # back and do anything with it later.
411
                #
412
                # if port is None and 'SERVER_PORT' in environ:
413
                #     s_port = environ['SERVER_PORT']
414
                #     if s_port not in ('80', '443'):
415
                #         port = s_port
416

417
            else:
418
                hostname = environ['SERVER_NAME'].strip()
1✔
419
                port = int(environ['SERVER_PORT'])
1✔
420
            self.setServerURL(protocol=protocol, hostname=hostname, port=port)
1✔
421
            server_url = other['SERVER_URL']
1✔
422

423
        if server_url[-1:] == '/':
1!
424
            server_url = server_url[:-1]
×
425

426
        if b:
1!
427
            self.base = f"{server_url}/{b}"
×
428
        else:
429
            self.base = server_url
1✔
430
        while script[:1] == '/':
1!
431
            script = script[1:]
×
432
        if script:
1!
433
            script = f"{server_url}/{script}"
×
434
        else:
435
            script = server_url
1✔
436
        other['URL'] = self.script = script
1✔
437
        other['method'] = environ.get('REQUEST_METHOD', 'GET').upper()
1✔
438

439
        # Make WEBDAV_SOURCE_PORT reachable with a simple REQUEST.get
440
        # to stay backwards-compatible
441
        if environ.get('WEBDAV_SOURCE_PORT'):
1✔
442
            other['WEBDAV_SOURCE_PORT'] = environ.get('WEBDAV_SOURCE_PORT')
1✔
443

444
        ################################################################
445
        # Cookie values should *not* be appended to existing form
446
        # vars with the same name - they are more like default values
447
        # for names not otherwise specified in the form.
448
        cookies = {}
1✔
449
        k = get_env('HTTP_COOKIE', '')
1✔
450
        if k:
1✔
451
            parse_cookie(k, cookies)
1✔
452
        self.cookies = cookies
1✔
453
        self.taintedcookies = taint(cookies)
1✔
454

455
    def processInputs(
1✔
456
            self,
457
            # "static" variables that we want to be local for speed
458
            SEQUENCE=1,
459
            DEFAULT=2,
460
            RECORD=4,
461
            RECORDS=8,
462
            REC=12,  # RECORD | RECORDS
463
            EMPTY=16,
464
            CONVERTED=32,
465
            hasattr=hasattr,
466
            getattr=getattr,
467
            setattr=setattr):
468
        """Process request inputs
469

470
        See the `Zope Developer Guide Object Publishing chapter
471
        <https://zope.readthedocs.io/en/latest/zdgbook/ObjectPublishing.html>`_
472
        for a detailed explanation in the section `Marshalling Arguments from
473
        the Request`.
474

475
        We need to delay input parsing so that it is done under
476
        publisher control for error handling purposes.
477
        """
478
        response = self.response
1✔
479
        environ = self.environ
1✔
480
        method = environ.get('REQUEST_METHOD', 'GET')
1✔
481

482
        if method != 'GET':
1✔
483
            fp = self.stdin
1✔
484
        else:
485
            fp = None
1✔
486

487
        form = self.form
1✔
488
        other = self.other
1✔
489

490
        # If 'QUERY_STRING' is not present in environ
491
        # FieldStorage will try to get it from sys.argv[1]
492
        # which is not what we need.
493
        if 'QUERY_STRING' not in environ:
1✔
494
            environ['QUERY_STRING'] = ''
1✔
495

496
        meth = None
1✔
497

498
        self._fs = fs = ZopeFieldStorage(fp, environ)
1✔
499
        self._file = fs.file
1✔
500

501
        if 'HTTP_SOAPACTION' in environ:
1✔
502
            # Stash XML request for interpretation by a SOAP-aware view
503
            other['SOAPXML'] = fs.value
1✔
504
            if 'CONTENT_TYPE' not in environ:
1!
505
                environ['CONTENT_TYPE'] = 'application/soap+xml'
1✔
506

507
        if fs.list:
1✔
508
            fslist = fs.list
1✔
509
            tuple_items = {}
1✔
510
            defaults = {}
1✔
511
            converter = None
1✔
512

513
            for item in fslist:  # form data
1✔
514
                # Note:
515
                # we want to support 2 use cases
516
                # 1. the form data has been created by the browser
517
                # 2. the form data is free standing
518
                # A browser internally works with character data,
519
                # which it encodes for transmission to the server --
520
                # usually with `self.charset`. Therefore, we
521
                # usually expect the form data to represent data
522
                # in this charset.
523
                # We make this same assumption also for free standing
524
                # form data, i.e. we expect the form creator to know
525
                # the server's charset. However, sometimes data cannot
526
                # be represented in this charset (e.g. arbitrary binary
527
                # data). To cover this case, we decode data
528
                # with the `surrogateescape` error handler (see PEP 383).
529
                # It allows to retrieve the original byte sequence.
530
                # With an encoding modifier, the form creator
531
                # can specify the correct encoding used by a form field value.
532
                # Note: we always expect the form field name
533
                # to be representable with `self.charset`. As those
534
                # names are expected to be `ASCII`, this should be no
535
                # big restriction.
536
                # Note: the use of `surrogateescape` can lead to delayed
537
                # problems when surrogates reach the application because
538
                # they cannot be encoded with a standard error handler.
539
                # We might want to prevent this.
540
                key = item.name
1✔
541
                if key is None:
1!
542
                    continue
×
543
                character_encoding = ""
1✔
544
                key = item.name.encode("latin-1").decode(
1✔
545
                    item.name_charset or self.charset)
546

547
                if hasattr(item, 'file') and \
1✔
548
                   hasattr(item, 'filename') and \
549
                   hasattr(item, 'headers'):
550
                    item = FileUpload(item, self.charset)
1✔
551
                else:
552
                    character_encoding = item.value_charset or self.charset
1✔
553
                    item = item.value.decode(
1✔
554
                        character_encoding, "surrogateescape")
555
                # from here on, `item` contains the field value
556
                # either as `FileUpload` or `str` with
557
                # `character_encoding` as encoding,
558
                # `key` the field name (`str`)
559

560
                flags = 0
1✔
561
                # Loop through the different types and set
562
                # the appropriate flags
563

564
                # We'll search from the back to the front.
565
                # We'll do the search in two steps.  First, we'll
566
                # do a string search, and then we'll check it with
567
                # a re search.
568

569
                delim = key.rfind(':')
1✔
570
                if delim >= 0:
1✔
571
                    mo = search_type(key, delim)
1✔
572
                    if mo:
1!
573
                        delim = mo.start(0)
1✔
574
                    else:
575
                        delim = -1
×
576

577
                    while delim >= 0:
1!
578
                        type_name = key[delim + 1:]
1✔
579
                        key = key[:delim]
1✔
580
                        c = get_converter(type_name, None)
1✔
581

582
                        if c is not None:
1✔
583
                            converter = c
1✔
584
                            flags = flags | CONVERTED
1✔
585
                        elif type_name == 'list':
1✔
586
                            flags = flags | SEQUENCE
1✔
587
                        elif type_name == 'tuple':
1✔
588
                            tuple_items[key] = 1
1✔
589
                            flags = flags | SEQUENCE
1✔
590
                        elif type_name == 'method' or type_name == 'action':
1✔
591
                            if delim:
1✔
592
                                meth = key
1✔
593
                            else:
594
                                meth = item
1✔
595
                        elif (type_name == 'default_method'
1!
596
                              or type_name == 'default_action'):
597
                            if not meth:
×
598
                                if delim:
×
599
                                    meth = key
×
600
                                else:
601
                                    meth = item
×
602
                        elif type_name == 'default':
1✔
603
                            flags = flags | DEFAULT
1✔
604
                        elif type_name == 'record':
1✔
605
                            flags = flags | RECORD
1✔
606
                        elif type_name == 'records':
1✔
607
                            flags = flags | RECORDS
1✔
608
                        elif type_name == 'ignore_empty':
1!
609
                            if not item:
×
610
                                flags = flags | EMPTY
×
611
                        elif has_codec(type_name):
1!
612
                            # recode:
613
                            assert not isinstance(item, FileUpload), \
1✔
614
                                   "cannot recode files"
615
                            item = item.encode(
1✔
616
                                character_encoding, "surrogateescape")
617
                            character_encoding = type_name
1✔
618
                            # we do not use `surrogateescape` as
619
                            # we immediately want to determine
620
                            # an incompatible encoding modifier
621
                            item = item.decode(character_encoding)
1✔
622

623
                        delim = key.rfind(':')
1✔
624
                        if delim < 0:
1✔
625
                            break
1✔
626
                        mo = search_type(key, delim)
1✔
627
                        if mo:
1!
628
                            delim = mo.start(0)
1✔
629
                        else:
630
                            delim = -1
×
631

632
                # Filter out special names from form:
633
                if key in isCGI_NAMEs or key.startswith('HTTP_'):
1!
634
                    continue
×
635

636
                if flags:
1✔
637

638
                    # skip over empty fields
639
                    if flags & EMPTY:
1!
640
                        continue
×
641

642
                    # Split the key and its attribute
643
                    if flags & REC:
1✔
644
                        key = key.split(".")
1✔
645
                        key, attr = ".".join(key[:-1]), key[-1]
1✔
646

647
                    # defer conversion
648
                    if flags & CONVERTED:
1✔
649
                        try:
1✔
650
                            if character_encoding and \
1✔
651
                               getattr(converter, "binary", False):
652
                                item = item.encode(character_encoding,
1✔
653
                                                   "surrogateescape")
654
                            item = converter(item)
1✔
655

656
                        except Exception:
×
657
                            if not item and \
×
658
                               not (flags & DEFAULT) and \
659
                               key in defaults:
660
                                item = defaults[key]
×
661
                                if flags & RECORD:
×
662
                                    item = getattr(item, attr)
×
663
                                if flags & RECORDS:
×
664
                                    item = getattr(item[-1], attr)
×
665
                            else:
666
                                raise
×
667

668
                    # Determine which dictionary to use
669
                    if flags & DEFAULT:
1✔
670
                        mapping_object = defaults
1✔
671
                    else:
672
                        mapping_object = form
1✔
673

674
                    # Insert in dictionary
675
                    if key in mapping_object:
1✔
676
                        if flags & RECORDS:
1✔
677
                            # Get the list and the last record
678
                            # in the list. reclist is mutable.
679
                            reclist = mapping_object[key]
1✔
680
                            x = reclist[-1]
1✔
681
                            if not hasattr(x, attr):
1✔
682
                                # If the attribute does not
683
                                # exist, set it
684
                                if flags & SEQUENCE:
1✔
685
                                    item = [item]
1✔
686
                                setattr(x, attr, item)
1✔
687
                            else:
688
                                if flags & SEQUENCE:
1✔
689
                                    # If the attribute is a
690
                                    # sequence, append the item
691
                                    # to the existing attribute
692
                                    y = getattr(x, attr)
1✔
693
                                    y.append(item)
1✔
694
                                    setattr(x, attr, y)
1✔
695
                                else:
696
                                    # Create a new record and add
697
                                    # it to the list
698
                                    n = record()
1✔
699
                                    setattr(n, attr, item)
1✔
700
                                    mapping_object[key].append(n)
1✔
701
                        elif flags & RECORD:
1✔
702
                            b = mapping_object[key]
1✔
703
                            if flags & SEQUENCE:
1!
704
                                item = [item]
×
705
                                if not hasattr(b, attr):
×
706
                                    # if it does not have the
707
                                    # attribute, set it
708
                                    setattr(b, attr, item)
×
709
                                else:
710
                                    # it has the attribute so
711
                                    # append the item to it
712
                                    setattr(b, attr, getattr(b, attr) + item)
×
713
                            else:
714
                                # it is not a sequence so
715
                                # set the attribute
716
                                setattr(b, attr, item)
1✔
717
                        else:
718
                            # it is not a record or list of records
719
                            found = mapping_object[key]
1✔
720
                            if isinstance(found, list):
1✔
721
                                found.append(item)
1✔
722
                            else:
723
                                found = [found, item]
1✔
724
                                mapping_object[key] = found
1✔
725
                    else:
726
                        # The dictionary does not have the key
727
                        if flags & RECORDS:
1✔
728
                            # Create a new record, set its attribute
729
                            # and put it in the dictionary as a list
730
                            a = record()
1✔
731
                            if flags & SEQUENCE:
1!
732
                                item = [item]
×
733
                            setattr(a, attr, item)
1✔
734
                            mapping_object[key] = [a]
1✔
735
                        elif flags & RECORD:
1✔
736
                            # Create a new record, set its attribute
737
                            # and put it in the dictionary
738
                            if flags & SEQUENCE:
1!
739
                                item = [item]
×
740
                            r = mapping_object[key] = record()
1✔
741
                            setattr(r, attr, item)
1✔
742
                        else:
743
                            # it is not a record or list of records
744
                            if flags & SEQUENCE:
1✔
745
                                item = [item]
1✔
746
                            mapping_object[key] = item
1✔
747

748
                else:
749
                    # This branch is for case when no type was specified.
750
                    mapping_object = form
1✔
751

752
                    # Insert in dictionary
753
                    if key in mapping_object:
1✔
754
                        # it is not a record or list of records
755
                        found = mapping_object[key]
1✔
756
                        if isinstance(found, list):
1!
757
                            found.append(item)
×
758
                        else:
759
                            found = [found, item]
1✔
760
                            mapping_object[key] = found
1✔
761
                    else:
762
                        mapping_object[key] = item
1✔
763

764
            # insert defaults into form dictionary
765
            if defaults:
1✔
766
                for key, value in defaults.items():
1✔
767
                    if key not in form:
1✔
768
                        # if the form does not have the key,
769
                        # set the default
770
                        form[key] = value
1✔
771
                    else:
772
                        # The form has the key
773
                        if isinstance(value, record):
1✔
774
                            # if the key is mapped to a record, get the
775
                            # record
776
                            r = form[key]
1✔
777
                            for k, v in value.__dict__.items():
1✔
778
                                # loop through the attributes and value
779
                                # in the default dictionary
780
                                if not hasattr(r, k):
1✔
781
                                    # if the form dictionary doesn't have
782
                                    # the attribute, set it to the default
783
                                    setattr(r, k, v)
1✔
784
                            form[key] = r
1✔
785

786
                        elif isinstance(value, list):
1!
787
                            # the default value is a list
788
                            val = form[key]
1✔
789
                            if not isinstance(val, list):
1!
790
                                val = [val]
×
791
                            for x in value:
1✔
792
                                # for each x in the list
793
                                if isinstance(x, record):
1✔
794
                                    # if the x is a record
795
                                    for k, v in x.__dict__.items():
1✔
796

797
                                        # loop through each
798
                                        # attribute and value in
799
                                        # the record
800

801
                                        for y in val:
1✔
802

803
                                            # loop through each
804
                                            # record in the form
805
                                            # list if it doesn't
806
                                            # have the attributes
807
                                            # in the default
808
                                            # dictionary, set them
809

810
                                            if not hasattr(y, k):
1✔
811
                                                setattr(y, k, v)
1✔
812
                                else:
813
                                    # x is not a record
814
                                    if x not in val:
1!
815
                                        val.append(x)
1✔
816
                            form[key] = val
1✔
817
                        else:
818
                            # The form has the key, the key is not mapped
819
                            # to a record or sequence so do nothing
820
                            pass
1✔
821

822
            # Convert to tuples
823
            if tuple_items:
1✔
824
                for key in tuple_items.keys():
1✔
825
                    # Split the key and get the attr
826
                    k = key.split(".")
1✔
827
                    k, attr = '.'.join(k[:-1]), k[-1]
1✔
828
                    a = attr
1✔
829
                    new = ''
1✔
830
                    # remove any type_names in the attr
831
                    while not a == '':
1✔
832
                        a = a.split(":")
1✔
833
                        a, new = ':'.join(a[:-1]), a[-1]
1✔
834
                    attr = new
1✔
835
                    if k in form:
1✔
836
                        # If the form has the split key get its value
837
                        item = form[k]
1✔
838
                        if isinstance(item, record):
1!
839
                            # if the value is mapped to a record, check if it
840
                            # has the attribute, if it has it, convert it to
841
                            # a tuple and set it
842
                            if hasattr(item, attr):
×
843
                                value = tuple(getattr(item, attr))
×
844
                                setattr(item, attr, value)
×
845
                        else:
846
                            # It is mapped to a list of  records
847
                            for x in item:
1✔
848
                                # loop through the records
849
                                if hasattr(x, attr):
1!
850
                                    # If the record has the attribute
851
                                    # convert it to a tuple and set it
852
                                    value = tuple(getattr(x, attr))
1✔
853
                                    setattr(x, attr, value)
1✔
854
                    else:
855
                        # the form does not have the split key
856
                        if key in form:
1✔
857
                            # if it has the original key, get the item
858
                            # convert it to a tuple
859
                            item = form[key]
1✔
860
                            item = tuple(form[key])
1✔
861
                            form[key] = item
1✔
862

863
            self.taintedform = taint(self.form)
1✔
864

865
        if method == 'POST' \
1✔
866
           and 'text/xml' in fs.headers.get('content-type', '') \
867
           and use_builtin_xmlrpc(self):
868
            # Ye haaa, XML-RPC!
869
            if meth is not None:
1✔
870
                raise BadRequest('method directive not supported for '
1✔
871
                                 'xmlrpc request')
872
            meth, self.args = xmlrpc.parse_input(fs.value)
1✔
873
            response = xmlrpc.response(response)
1✔
874
            other['RESPONSE'] = self.response = response
1✔
875
            self.maybe_webdav_client = 0
1✔
876

877
        if meth:
1✔
878
            if 'PATH_INFO' in environ:
1✔
879
                path = environ['PATH_INFO']
1✔
880
                while path[-1:] == '/':
1!
881
                    path = path[:-1]
×
882
            else:
883
                path = ''
1✔
884
            other['PATH_INFO'] = f"{path}/{meth}"
1✔
885
            self._hacked_path = 1
1✔
886

887
    def resolve_url(self, url):
1✔
888
        # Attempt to resolve a url into an object in the Zope
889
        # namespace. The url must be a fully-qualified url. The
890
        # method will return the requested object if it is found
891
        # or raise the same HTTP error that would be raised in
892
        # the case of a real web request. If the passed in url
893
        # does not appear to describe an object in the system
894
        # namespace (e.g. the host, port or script name don't
895
        # match that of the current request), a ValueError will
896
        # be raised.
897
        if url.find(self.script) != 0:
1!
898
            raise ValueError('Different namespace.')
×
899
        path = url[len(self.script):]
1✔
900
        while path and path[0] == '/':
1✔
901
            path = path[1:]
1✔
902
        while path and path[-1] == '/':
1!
903
            path = path[:-1]
×
904
        req = self.clone()
1✔
905
        rsp = req.response
1✔
906
        req['PATH_INFO'] = path
1✔
907
        object = None
1✔
908

909
        # Try to traverse to get an object. Note that we call
910
        # the exception method on the response, but we don't
911
        # want to actually abort the current transaction
912
        # (which is usually the default when the exception
913
        # method is called on the response).
914
        try:
1✔
915
            object = req.traverse(path)
1✔
916
        except Exception as exc:
1✔
917
            rsp.exception()
1✔
918
            req.clear()
1✔
919
            raise exc.__class__(rsp.errmsg)
1✔
920

921
        # The traversal machinery may return a "default object"
922
        # like an index_html document. This is not appropriate
923
        # in the context of the resolve_url method so we need
924
        # to ensure we are getting the actual object named by
925
        # the given url, and not some kind of default object.
926
        if hasattr(object, 'id'):
1!
927
            if callable(object.id):
×
928
                name = object.id()
×
929
            else:
930
                name = object.id
×
931
        elif hasattr(object, '__name__'):
1!
932
            name = object.__name__
×
933
        else:
934
            name = ''
1✔
935
        if name != os.path.split(path)[-1]:
1!
936
            object = req.PARENTS[0]
×
937

938
        req.clear()
1✔
939
        return object
1✔
940

941
    def clone(self):
1✔
942
        # Return a clone of the current request object
943
        # that may be used to perform object traversal.
944
        environ = self.environ.copy()
1✔
945
        environ['REQUEST_METHOD'] = 'GET'
1✔
946
        if self._auth:
1✔
947
            environ['HTTP_AUTHORIZATION'] = self._auth
1✔
948
        if self.response is not None:
1✔
949
            response = self.response.__class__()
1✔
950
        else:
951
            response = None
1✔
952
        clone = self.__class__(None, environ, response, clean=1)
1✔
953
        clone['PARENTS'] = [self['PARENTS'][-1]]
1✔
954
        directlyProvides(clone, *directlyProvidedBy(self))
1✔
955
        return clone
1✔
956

957
    def getHeader(self, name, default=None, literal=False):
1✔
958
        """Return the named HTTP header, or an optional default
959
        argument or None if the header is not found. Note that
960
        both original and CGI-ified header names are recognized,
961
        e.g. 'Content-Type', 'CONTENT_TYPE' and 'HTTP_CONTENT_TYPE'
962
        should all return the Content-Type header, if available.
963
        """
964
        environ = self.environ
1✔
965
        if not literal:
1✔
966
            name = name.replace('-', '_').upper()
1✔
967
        val = environ.get(name, None)
1✔
968
        if val is not None:
1✔
969
            return val
1✔
970
        if name[:5] != 'HTTP_':
1!
971
            name = 'HTTP_%s' % name
1✔
972
        return environ.get(name, default)
1✔
973

974
    get_header = getHeader  # BBB
1✔
975

976
    def get(self, key, default=None, returnTaints=0,
1✔
977
            URLmatch=re.compile('URL(PATH)?([0-9]+)$').match,
978
            BASEmatch=re.compile('BASE(PATH)?([0-9]+)$').match,
979
            ):
980
        """Get a variable value
981

982
        Return a value for the variable key, or default if not found.
983

984
        If key is "REQUEST", return the request.
985
        Otherwise, the value will be looked up from one of the request data
986
        categories. The search order is:
987
        other (the target for explicitly set variables),
988
        the special URL and BASE variables,
989
        environment variables,
990
        common variables (defined by the request class),
991
        lazy variables (set with set_lazy),
992
        form data and cookies.
993

994
        If returnTaints has a true value, then the access to
995
        form and cookie variables returns values with special
996
        protection against embedded HTML fragments to counter
997
        some cross site scripting attacks.
998
        """
999

1000
        if key == 'REQUEST':
1✔
1001
            return self
1✔
1002

1003
        other = self.other
1✔
1004
        if key in other:
1✔
1005
            return other[key]
1✔
1006

1007
        if key[:1] == 'U':
1✔
1008
            match = URLmatch(key)
1✔
1009
            if match is not None:
1!
1010
                pathonly, n = match.groups()
1✔
1011
                path = self._script + self._steps
1✔
1012
                n = len(path) - int(n)
1✔
1013
                if n < 0:
1✔
1014
                    raise KeyError(key)
1✔
1015
                if pathonly:
1✔
1016
                    path = [''] + path[:n]
1✔
1017
                else:
1018
                    path = [other['SERVER_URL']] + path[:n]
1✔
1019
                URL = '/'.join(path)
1✔
1020
                if 'PUBLISHED' in other:
1✔
1021
                    # Don't cache URLs until publishing traversal is done.
1022
                    other[key] = URL
1✔
1023
                    self._urls = self._urls + (key,)
1✔
1024
                return URL
1✔
1025

1026
        if key in isCGI_NAMEs or key[:5] == 'HTTP_':
1✔
1027
            environ = self.environ
1✔
1028
            if key in environ and (key not in hide_key):
1✔
1029
                return environ[key]
1✔
1030
            return ''
1✔
1031

1032
        if key[:1] == 'B':
1✔
1033
            match = BASEmatch(key)
1✔
1034
            if match is not None:
1✔
1035
                pathonly, n = match.groups()
1✔
1036
                path = self._steps
1✔
1037
                n = int(n)
1✔
1038
                if n:
1✔
1039
                    n = n - 1
1✔
1040
                    if len(path) < n:
1✔
1041
                        raise KeyError(key)
1✔
1042

1043
                    v = self._script + path[:n]
1✔
1044
                else:
1045
                    v = self._script[:-1]
1✔
1046
                if pathonly:
1✔
1047
                    v.insert(0, '')
1✔
1048
                else:
1049
                    v.insert(0, other['SERVER_URL'])
1✔
1050
                URL = '/'.join(v)
1✔
1051
                if 'PUBLISHED' in other:
1✔
1052
                    # Don't cache URLs until publishing traversal is done.
1053
                    other[key] = URL
1✔
1054
                    self._urls = self._urls + (key,)
1✔
1055
                return URL
1✔
1056

1057
            if key == 'BODY' and self._file is not None:
1✔
1058
                v = self.other[key] = self._fs.value
1✔
1059
                return v
1✔
1060

1061
            if key == 'BODYFILE' and self._file is not None:
1✔
1062
                v = self._file
1✔
1063
                self.other[key] = v
1✔
1064
                return v
1✔
1065

1066
        v = self.common.get(key, _marker)
1✔
1067
        if v is not _marker:
1!
1068
            return v
×
1069

1070
        if self._lazies:
1!
1071
            v = self._lazies.get(key, _marker)
×
1072
            if v is not _marker:
×
1073
                if callable(v):
×
1074
                    v = v()
×
1075
                self[key] = v  # Promote lazy value
×
1076
                del self._lazies[key]
×
1077
                return v
×
1078

1079
        # Return tainted data first (marked as suspect)
1080
        if returnTaints:
1✔
1081
            v = self.taintedform.get(key, _marker)
1✔
1082
            if v is not _marker:
1!
1083
                return v
×
1084

1085
        # Untrusted data *after* trusted data
1086
        v = self.form.get(key, _marker)
1✔
1087
        if v is not _marker:
1✔
1088
            return v
1✔
1089

1090
        # Return tainted data first (marked as suspect)
1091
        if returnTaints:
1✔
1092
            v = self.taintedcookies.get(key, _marker)
1✔
1093
            if v is not _marker:
1!
1094
                return v
×
1095

1096
        # Untrusted data *after* trusted data
1097
        v = self.cookies.get(key, _marker)
1✔
1098
        if v is not _marker:
1!
1099
            return v
×
1100

1101
        return default
1✔
1102

1103
    def __getitem__(self, key, default=_marker, returnTaints=0):
1✔
1104
        v = self.get(key, default, returnTaints=returnTaints)
1✔
1105
        if v is _marker:
1✔
1106
            raise KeyError(key)
1✔
1107
        return v
1✔
1108

1109
    # Using the getattr protocol to retrieve form values and similar
1110
    # is discouraged and is likely to be deprecated in the future.
1111
    # request.get(key) or request[key] should be used instead
1112
    def __getattr__(self, key, default=_marker, returnTaints=0):
1✔
1113
        v = self.get(key, default, returnTaints=returnTaints)
1✔
1114
        if v is _marker:
1✔
1115
            if key == 'locale':
1✔
1116
                # we only create the _locale on first access, as setting it
1117
                # up might be slow and we don't want to slow down every
1118
                # request
1119
                if self._locale is _marker:
1✔
1120
                    self.setupLocale()
1✔
1121
                return self._locale
1✔
1122
            if key == 'debug':
1✔
1123
                return self._debug
1✔
1124
            raise AttributeError(key)
1✔
1125
        return v
1✔
1126

1127
    def set_lazy(self, key, callable):
1✔
1128
        self._lazies[key] = callable
×
1129

1130
    def __contains__(self, key, returnTaints=0):
1✔
1131
        return self.has_key(key, returnTaints=returnTaints)  # NOQA
1✔
1132

1133
    def has_key(self, key, returnTaints=0):
1✔
1134
        try:
1✔
1135
            self.__getitem__(key, returnTaints=returnTaints)
1✔
1136
        except Exception:
1✔
1137
            return 0
1✔
1138
        else:
1139
            return 1
1✔
1140

1141
    def keys(self, returnTaints=0):
1✔
1142
        keys = {}
1✔
1143
        keys.update(self.common)
1✔
1144
        keys.update(self._lazies)
1✔
1145

1146
        for key in self.environ.keys():
1✔
1147
            if (key in isCGI_NAMEs or key[:5] == 'HTTP_') and \
1✔
1148
               (key not in hide_key):
1149
                keys[key] = 1
1✔
1150

1151
        # Cache URLN and BASEN in self.other.
1152
        # This relies on a side effect of has_key.
1153
        n = 0
1✔
1154
        while 1:
1155
            n = n + 1
1✔
1156
            key = "URL%s" % n
1✔
1157
            if key not in self:  # NOQA
1✔
1158
                break
1✔
1159

1160
        n = 0
1✔
1161
        while 1:
1162
            n = n + 1
1✔
1163
            key = "BASE%s" % n
1✔
1164
            if key not in self:  # NOQA
1✔
1165
                break
1✔
1166

1167
        keys.update(self.other)
1✔
1168
        keys.update(self.cookies)
1✔
1169
        if returnTaints:
1!
1170
            keys.update(self.taintedcookies)
×
1171
        keys.update(self.form)
1✔
1172
        if returnTaints:
1!
1173
            keys.update(self.taintedform)
×
1174

1175
        keys = list(keys.keys())
1✔
1176
        keys.sort()
1✔
1177

1178
        return keys
1✔
1179

1180
    def __str__(self):
1✔
1181
        result = "<h3>form</h3><table>"
1✔
1182
        row = '<tr valign="top" align="left"><th>%s</th><td>%s</td></tr>'
1✔
1183
        for k, v in _filterPasswordFields(self.form.items()):
1✔
1184
            result = result + row % (
1✔
1185
                html.escape(k, False), html.escape(repr(v), False))
1186
        result = result + "</table><h3>cookies</h3><table>"
1✔
1187
        for k, v in _filterPasswordFields(self.cookies.items()):
1!
1188
            result = result + row % (
×
1189
                html.escape(k, False), html.escape(repr(v), False))
1190
        result = result + "</table><h3>lazy items</h3><table>"
1✔
1191
        for k, v in _filterPasswordFields(self._lazies.items()):
1!
1192
            result = result + row % (
×
1193
                html.escape(k, False), html.escape(repr(v), False))
1194
        result = result + "</table><h3>other</h3><table>"
1✔
1195
        for k, v in _filterPasswordFields(self.other.items()):
1✔
1196
            if k in ('PARENTS', 'RESPONSE'):
1✔
1197
                continue
1✔
1198
            result = result + row % (
1✔
1199
                html.escape(k, False), html.escape(repr(v), False))
1200

1201
        for n in "0123456789":
1✔
1202
            key = "URL%s" % n
1✔
1203
            try:
1✔
1204
                result = result + row % (key, html.escape(self[key], False))
1✔
1205
            except KeyError:
1✔
1206
                pass
1✔
1207
        for n in "0123456789":
1✔
1208
            key = "BASE%s" % n
1✔
1209
            try:
1✔
1210
                result = result + row % (key, html.escape(self[key], False))
1✔
1211
            except KeyError:
1✔
1212
                pass
1✔
1213

1214
        result = result + "</table><h3>environ</h3><table>"
1✔
1215
        for k, v in self.environ.items():
1✔
1216
            if k not in hide_key:
1!
1217
                result = result + row % (
1✔
1218
                    html.escape(k, False), html.escape(repr(v), False))
1219
        return result + "</table>"
1✔
1220

1221
    def __repr__(self):
1✔
1222
        return f"<{self.__class__.__name__}, URL={self.get('URL')}>"
1✔
1223

1224
    def text(self):
1✔
1225
        result = "FORM\n\n"
1✔
1226
        row = '%-20s %s\n'
1✔
1227
        for k, v in _filterPasswordFields(self.form.items()):
1✔
1228
            result = result + row % (k, repr(v))
1✔
1229
        result = result + "\nCOOKIES\n\n"
1✔
1230
        for k, v in _filterPasswordFields(self.cookies.items()):
1!
1231
            result = result + row % (k, repr(v))
×
1232
        result = result + "\nLAZY ITEMS\n\n"
1✔
1233
        for k, v in _filterPasswordFields(self._lazies.items()):
1!
1234
            result = result + row % (k, repr(v))
×
1235
        result = result + "\nOTHER\n\n"
1✔
1236
        for k, v in _filterPasswordFields(self.other.items()):
1✔
1237
            if k in ('PARENTS', 'RESPONSE'):
1✔
1238
                continue
1✔
1239
            result = result + row % (k, repr(v))
1✔
1240

1241
        for n in "0123456789":
1✔
1242
            key = "URL%s" % n
1✔
1243
            try:
1✔
1244
                result = result + row % (key, self[key])
1✔
1245
            except KeyError:
1✔
1246
                pass
1✔
1247
        for n in "0123456789":
1✔
1248
            key = "BASE%s" % n
1✔
1249
            try:
1✔
1250
                result = result + row % (key, self[key])
1✔
1251
            except KeyError:
1✔
1252
                pass
1✔
1253

1254
        result = result + "\nENVIRON\n\n"
1✔
1255
        for k, v in self.environ.items():
1✔
1256
            if k not in hide_key:
1!
1257
                result = result + row % (k, v)
1✔
1258
        return result
1✔
1259

1260
    def _authUserPW(self):
1✔
1261
        # Can return None
1262
        return basic_auth_decode(self._auth)
1✔
1263

1264
    def taintWrapper(self, enabled=TAINTING_ENABLED):
1✔
1265
        return enabled and TaintRequestWrapper(self) or self
1✔
1266

1267
    # Original version: zope.publisher.http.HTTPRequest.shiftNameToApplication
1268
    def shiftNameToApplication(self):
1✔
1269
        """see zope.publisher.interfaces.http.IVirtualHostRequest"""
1270
        if len(self._steps) == 1:
1!
1271
            self._script.append(self._steps.pop())
1✔
1272
            self._resetURLS()
1✔
1273
            return
1✔
1274

1275
        raise ValueError("Can only shift leading traversal "
×
1276
                         "names to application names")
1277

1278
    def getURL(self):
1✔
1279
        return self.URL
×
1280

1281

1282
class WSGIRequest(HTTPRequest):
1✔
1283
    # A request object for WSGI, no docstring to avoid being publishable.
1284
    pass
1✔
1285

1286

1287
class TaintRequestWrapper:
1✔
1288

1289
    def __init__(self, req):
1✔
1290
        self._req = req
1✔
1291

1292
    def __contains__(self, *args, **kw):
1✔
1293
        return TaintMethodWrapper(self._req.__contains__)(*args, **kw)
×
1294

1295
    def __getattr__(self, key, *args, **kw):
1✔
1296
        if key not in self._req.keys():
1✔
1297
            item = getattr(self._req, key, _marker)
1✔
1298
            if item is not _marker:
1!
1299
                return item
1✔
1300
        return TaintMethodWrapper(self._req.__getattr__)(key, *args, **kw)
1✔
1301

1302
    def __getitem__(self, *args, **kw):
1✔
1303
        return TaintMethodWrapper(self._req.__getitem__)(*args, **kw)
1✔
1304

1305
    def __len__(self):
1✔
1306
        return len(self._req)
1✔
1307

1308
    def get(self, *args, **kw):
1✔
1309
        return TaintMethodWrapper(self._req.get)(*args, **kw)
1✔
1310

1311
    def has_key(self, *args, **kw):
1✔
1312
        return TaintMethodWrapper(self._req.has_key)(*args, **kw)
×
1313

1314
    def keys(self, *args, **kw):
1✔
1315
        return TaintMethodWrapper(self._req.keys)(*args, **kw)
×
1316

1317

1318
class TaintMethodWrapper:
1✔
1319

1320
    def __init__(self, method):
1✔
1321
        self._method = method
1✔
1322

1323
    def __call__(self, *args, **kw):
1✔
1324
        kw['returnTaints'] = 1
1✔
1325
        return self._method(*args, **kw)
1✔
1326

1327

1328
def has_codec(x):
1✔
1329
    try:
1✔
1330
        codecs.lookup(x)
1✔
1331
    except (LookupError, SystemError):
×
1332
        return 0
×
1333
    else:
1334
        return 1
1✔
1335

1336

1337
def sane_environment(env):
1✔
1338
    # return an environment mapping which has been cleaned of
1339
    # funny business such as REDIRECT_ prefixes added by Apache
1340
    # or HTTP_CGI_AUTHORIZATION hacks.
1341
    dict = {}
1✔
1342
    for key, val in env.items():
1✔
1343
        while key[:9] == 'REDIRECT_':
1!
1344
            key = key[9:]
×
1345
        dict[key] = val
1✔
1346
    if 'HTTP_CGI_AUTHORIZATION' in dict:
1!
1347
        dict['HTTP_AUTHORIZATION'] = dict['HTTP_CGI_AUTHORIZATION']
×
1348
        try:
×
1349
            del dict['HTTP_CGI_AUTHORIZATION']
×
1350
        except Exception:
×
1351
            pass
×
1352
    return dict
1✔
1353

1354

1355
class ValueDescriptor:
1✔
1356
    """(non data) descriptor to compute `value` from `file`."""
1357
    def __get__(self, inst, owner=None):
1✔
1358
        if inst is None:
1!
1359
            return self
×
1360
        file = inst.file
1✔
1361
        try:
1✔
1362
            fpos = file.tell()
1✔
1363
        except Exception:
1✔
1364
            fpos = None
1✔
1365
        try:
1✔
1366
            limit = inst.VALUE_LIMIT
1✔
1367
            if limit:
1!
1368
                v = file.read(limit)
1✔
1369
                if file.read(1):
1✔
1370
                    raise BadRequest("data exceeds memory limit")
1✔
1371
            else:
1372
                v = file.read()
×
1373
            if fpos is None:
1✔
1374
                # store the value as we cannot read it again
1375
                inst.value = v
1✔
1376
            return v
1✔
1377
        finally:
1378
            if fpos is not None:
1!
1379
                file.seek(fpos)
1✔
1380

1381

1382
class Global:
1✔
1383
    """(non data) descriptor to access a (modul) global attribute."""
1384
    def __init__(self, name):
1✔
1385
        """access global *name*."""
1386
        self.name = name
1✔
1387

1388
    def __get__(self, inst, owner=None):
1✔
1389
        return globals()[self.name]
1✔
1390

1391

1392
class ValueAccessor:
1✔
1393
    VALUE_LIMIT = None
1✔
1394

1395
    value = ValueDescriptor()
1✔
1396

1397

1398
class FormField(SimpleNamespace, ValueAccessor):
1✔
1399
    """represent a single form field.
1400

1401
    Typical attributes:
1402
    name
1403
      the field name
1404
    value
1405
      the field value (`bytes`)
1406
    name_charset, value_charset
1407
      the charset for the name and value, respectively, or ``None``
1408
      if no charset has been specified.
1409

1410
    File fields additionally have the attributes:
1411
    file
1412
      a binary file containing the file content
1413
    filename
1414
      the file's name as reported by the client
1415
    headers
1416
      a case insensitive dict with header information;
1417
      usually `content-type` and `content-disposition`.
1418

1419
    Unless otherwise noted, `latin-1` decoded bytes
1420
    are used to represent textual data.
1421
    """
1422

1423
    name_charset = value_charset = None
1✔
1424

1425

1426
class ZopeFieldStorage(ValueAccessor):
1✔
1427
    VALUE_LIMIT = Global("FORM_MEMORY_LIMIT")
1✔
1428

1429
    def __init__(self, fp, environ):
1✔
1430
        self.file = fp
1✔
1431
        method = environ.get("REQUEST_METHOD", "GET").upper()
1✔
1432
        url_qs = environ.get("QUERY_STRING", "")
1✔
1433
        post_qs = ""
1✔
1434
        hl = []
1✔
1435
        content_type = environ.get("CONTENT_TYPE",
1✔
1436
                                   "application/x-www-form-urlencoded")
1437
        hl.append(("content-type", content_type))
1✔
1438
        content_type, options = parse_options_header(content_type)
1✔
1439
        content_type = content_type.lower()
1✔
1440
        content_disposition = environ.get("CONTENT_DISPOSITION")
1✔
1441
        if content_disposition is not None:
1!
1442
            hl.append(("content-disposition", content_disposition))
×
1443
        self.headers = Headers(hl)
1✔
1444
        parts = ()
1✔
1445
        if method == "POST" \
1✔
1446
           and content_type in \
1447
           ("multipart/form-data", "application/x-www-form-urlencoded"):
1448
            try:
1✔
1449
                fpos = fp.tell()
1✔
1450
            except Exception:
1✔
1451
                fpos = None
1✔
1452
            if content_type == "multipart/form-data":
1✔
1453
                parts = MultipartParser(
1✔
1454
                    fp, options["boundary"],
1455
                    mem_limit=FORM_MEMORY_LIMIT,
1456
                    disk_limit=FORM_DISK_LIMIT,
1457
                    memfile_limit=FORM_MEMFILE_LIMIT,
1458
                    charset="latin-1").parts()
1459
            elif content_type == "application/x-www-form-urlencoded":
1!
1460
                post_qs = fp.read(FORM_MEMORY_LIMIT).decode("latin-1")
1✔
1461
                if fp.read(1):
1✔
1462
                    raise BadRequest("form data processing "
1✔
1463
                                     "requires too much memory")
1464
            if fpos is not None:
1✔
1465
                fp.seek(fpos)
1✔
1466
            else:
1467
                # we cannot read the file again
1468
                self.file = None
1✔
1469
        self.list = fl = []
1✔
1470
        add_field = fl.append
1✔
1471
        post_opts = {}
1✔
1472
        if options.get("charset"):
1✔
1473
            post_opts["name_charset"] = post_opts["value_charset"] = \
1✔
1474
                options["charset"]
1475
        for qs, opts in ((url_qs, {}), (post_qs, post_opts)):
1✔
1476
            for name, val in parse_qsl(
1✔
1477
               qs,  # noqa: E121
1478
               keep_blank_values=True, encoding="latin-1"):
1479
                add_field(FormField(
1✔
1480
                    name=name, value=val.encode("latin-1"), **opts))
1481
        for part in parts:
1✔
1482
            if part.filename is not None:
1✔
1483
                # a file
1484
                field = FormField(
1✔
1485
                    name=part.name,
1486
                    file=part.file,
1487
                    filename=part.filename,
1488
                    headers=part.headers)
1489
            else:
1490
                field = FormField(
1✔
1491
                    name=part.name, value=part.raw,
1492
                    value_charset=_mp_charset(part))
1493
            add_field(field)
1✔
1494

1495

1496
def _mp_charset(part):
1✔
1497
    """the charset of *part*."""
1498
    content_type = part.headers.get("Content-Type", "")
1✔
1499
    _, options = parse_options_header(content_type)
1✔
1500
    return options.get("charset")
1✔
1501

1502

1503
# Original version: zope.publisher.browser.FileUpload
1504
class FileUpload:
1✔
1505
    '''File upload objects
1506

1507
    File upload objects are used to represent file-uploaded data.
1508

1509
    File upload objects can be used just like files.
1510

1511
    In addition, they have a 'headers' attribute that is a dictionary
1512
    containing the file-upload headers, and a 'filename' attribute
1513
    containing the name of the uploaded file.
1514

1515
    Note that file names in HTTP/1.1 use latin-1 as charset.  See
1516
    https://github.com/zopefoundation/Zope/pull/1094#issuecomment-1459654636
1517
    '''
1518

1519
    # Allow access to attributes such as headers and filename so
1520
    # that protected code can use DTML to work with FileUploads.
1521
    __allow_access_to_unprotected_subobjects__ = 1
1✔
1522

1523
    def __init__(self, aFieldStorage, charset=None):
1✔
1524
        charset = charset or default_encoding
1✔
1525
        self.file = aFieldStorage.file
1✔
1526
        self.headers = aFieldStorage.headers
1✔
1527
        # Prevent needless encode-decode when both charsets are the same.
1528
        if charset != "latin-1":
1!
1529
            self.filename = aFieldStorage.filename\
1✔
1530
                .encode("latin-1").decode(charset)
1531
            self.name = aFieldStorage.name.encode("latin-1").decode(charset)
1✔
1532
        else:
1533
            self.filename = aFieldStorage.filename
×
1534
            self.name = aFieldStorage.name
×
1535

1536
        # Add an assertion to the rfc822.Message object that implements
1537
        # self.headers so that managed code can access them.
1538
        try:
1✔
1539
            self.headers.__allow_access_to_unprotected_subobjects__ = 1
1✔
1540
        except Exception:
×
1541
            pass
×
1542

1543
    def __getattribute__(self, key):
1✔
1544
        if key in ('close', 'closed', 'detach', 'fileno', 'flush',
1✔
1545
                   'getbuffer', 'getvalue', 'isatty', 'read', 'read1',
1546
                   'readable', 'readinto', 'readline', 'readlines',
1547
                   'seek', 'seekable', 'tell', 'truncate', 'writable',
1548
                   'write', 'writelines', 'name'):
1549
            file = object.__getattribute__(self, 'file')
1✔
1550
            func = getattr(file, key, _marker)
1✔
1551
            if func is not _marker:
1✔
1552
                return func
1✔
1553
        # Always fall back to looking things up on self
1554
        return object.__getattribute__(self, key)
1✔
1555

1556
    def __iter__(self):
1✔
1557
        return self.file.__iter__()
1✔
1558

1559
    def __bool__(self):
1✔
1560
        """FileUpload objects are considered false if their
1561
           filename is empty.
1562
        """
1563
        return bool(self.filename)
1✔
1564

1565
    def __next__(self):
1✔
1566
        return self.file.__next__()
1✔
1567

1568

1569
QPARMRE = re.compile(
1✔
1570
    '([\x00- ]*([^\x00- ;,="]+)="([^"]*)"([\x00- ]*[;,])?[\x00- ]*)')
1571
PARMRE = re.compile(
1✔
1572
    '([\x00- ]*([^\x00- ;,="]+)=([^;]*)([\x00- ]*[;,])?[\x00- ]*)')
1573
PARAMLESSRE = re.compile(
1✔
1574
    '([\x00- ]*([^\x00- ;,="]+)[\x00- ]*[;,][\x00- ]*)')
1575

1576

1577
def parse_cookie(text,
1✔
1578
                 result=None,
1579
                 qparmre=QPARMRE,
1580
                 parmre=PARMRE,
1581
                 paramlessre=PARAMLESSRE,
1582
                 ):
1583

1584
    if result is None:
1!
1585
        result = {}
×
1586

1587
    mo_q = qparmre.match(text)
1✔
1588

1589
    if mo_q:
1✔
1590
        # Match quoted correct cookies
1591
        c_len = len(mo_q.group(1))
1✔
1592
        name = mo_q.group(2)
1✔
1593
        value = mo_q.group(3)
1✔
1594

1595
    else:
1596
        # Match evil MSIE cookies ;)
1597
        mo_p = parmre.match(text)
1✔
1598

1599
        if mo_p:
1✔
1600
            c_len = len(mo_p.group(1))
1✔
1601
            name = mo_p.group(2)
1✔
1602
            value = mo_p.group(3)
1✔
1603
        else:
1604
            # Broken Cookie without = nor value.
1605
            broken_p = paramlessre.match(text)
1✔
1606
            if broken_p:
1✔
1607
                c_len = len(broken_p.group(1))
1✔
1608
                name = broken_p.group(2)
1✔
1609
                value = ''
1✔
1610
            else:
1611
                return result
1✔
1612

1613
    if name not in result:
1!
1614
        result[name] = getCookieValuePolicy().load(name, value)
1✔
1615

1616
    return parse_cookie(text[c_len:], result)
1✔
1617

1618

1619
class record:
1✔
1620

1621
    # Allow access to record methods and values from DTML
1622
    __allow_access_to_unprotected_subobjects__ = 1
1✔
1623
    _guarded_writes = 1
1✔
1624

1625
    def __getattr__(self, key, default=None):
1✔
1626
        if key in ('get',
1✔
1627
                   'keys',
1628
                   'items',
1629
                   'values',
1630
                   'copy'):
1631
            return getattr(self.__dict__, key)
1✔
1632
        raise AttributeError(key)
1✔
1633

1634
    def __contains__(self, key):
1✔
1635
        return key in self.__dict__
1✔
1636

1637
    def __getitem__(self, key):
1✔
1638
        return self.__dict__[key]
1✔
1639

1640
    def __iter__(self):
1✔
1641
        return iter(self.__dict__)
1✔
1642

1643
    def __len__(self):
1✔
1644
        return len(self.__dict__)
1✔
1645

1646
    def __str__(self):
1✔
1647
        return ", ".join("%s: %s" % item for item in
1✔
1648
                         sorted(self.__dict__.items()))
1649

1650
    def __repr__(self):
1✔
1651
        # return repr( self.__dict__ )
1652
        return '{%s}' % ', '.join(
1✔
1653
            f"'{key}': {value!r}"
1654
            for key, value in sorted(self.__dict__.items()))
1655

1656
    def __eq__(self, other):
1✔
1657
        if not isinstance(other, record):
1!
1658
            return False
×
1659
        return sorted(self.__dict__.items()) == sorted(other.__dict__.items())
1✔
1660

1661

1662
def _filterPasswordFields(items):
1✔
1663
    # Collector #777:  filter out request fields which contain 'passw'
1664

1665
    result = []
1✔
1666

1667
    for k, v in items:
1✔
1668

1669
        if 'passw' in k.lower():
1✔
1670
            v = '<password obscured>'
1✔
1671

1672
        result.append((k, v))
1✔
1673

1674
    return result
1✔
1675

1676

1677
def use_builtin_xmlrpc(request):
1✔
1678
    checker = queryUtility(IXmlrpcChecker)
1✔
1679
    return checker is None or checker(request)
1✔
1680

1681

1682
def taint(d):
1✔
1683
    """return as ``dict`` the items from *d* which require tainting.
1684

1685
    *d* must be a ``dict`` with ``str`` keys and values recursively
1686
    build from elementary values, ``list``, ``tuple``, ``record`` and
1687
    ``dict``.
1688
    """
1689
    def should_taint(v):
1✔
1690
        """check whether *v* needs tainting."""
1691
        if isinstance(v, (list, tuple)):
1✔
1692
            return any(should_taint(x) for x in v)
1✔
1693
        if isinstance(v, (record, dict)):
1✔
1694
            for k in v:
1✔
1695
                if should_taint(k):
1!
1696
                    return True
×
1697
                if should_taint(v[k]):
1✔
1698
                    return True
1✔
1699
        return should_be_tainted(v)
1✔
1700

1701
    def _taint(v):
1✔
1702
        """return a copy of *v* with tainted replacements.
1703

1704
        In the copy, each ``str`` which requires tainting
1705
        is replaced by the corresponding ``taint_string``.
1706
        """
1707
        __traceback_info__ = v
1✔
1708
        if isinstance(v, (bytes, str)) and should_be_tainted(v):
1✔
1709
            return taint_string(v)
1✔
1710
        if isinstance(v, (list, tuple)):
1✔
1711
            return v.__class__(_taint(x) for x in v)
1✔
1712
        if isinstance(v, record):
1✔
1713
            rn = record()
1✔
1714
            for k in v:
1✔
1715
                __traceback_info__ = v, k
1✔
1716
                if should_be_tainted(k):
1!
1717
                    raise ValueError("Cannot taint `record` attribute names")
×
1718
                setattr(rn, k, _taint(v[k]))
1✔
1719
            return rn
1✔
1720
        if isinstance(v, dict):
1!
1721
            return v.__class__((_taint(k), _taint(v[k])) for k in v)
×
1722
        return v
1✔
1723
    td = {}
1✔
1724
    for k in d:
1✔
1725
        v = d[k]
1✔
1726
        if should_taint(k) or should_taint(v):
1✔
1727
            td[_taint(k)] = _taint(v)
1✔
1728
    return td
1✔
1729

1730

1731
def should_be_tainted(v):
1✔
1732
    """Work around ``AccessControl`` weakness."""
1733
    if isinstance(v, FileUpload):
1✔
1734
        # `base_should_be_tainted` would read `v` as a side effect
1735
        return False
1✔
1736
    try:
1✔
1737
        return base_should_be_tainted(v)
1✔
1738
    except Exception:
1✔
1739
        return False
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc