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

zopefoundation / Zope / 4957951960

pending completion
4957951960

push

github

Laurent Lasudry
Fix `POST` requests with Gunicorn WSGI

4270 of 6881 branches covered (62.05%)

Branch coverage included in aggregate %.

7 of 7 new or added lines in 1 file covered. (100.0%)

27057 of 31365 relevant lines covered (86.26%)

0.86 hits per line

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

86.09
/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 = 4000    # 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
    _file = None
1✔
168
    _urls = ()
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.form.clear()
1✔
195
        # we want to clear the lazy dict here because BaseRequests don't have
196
        # one.  Without this, there's the possibility of memory leaking
197
        # after every request.
198
        self._lazies = {}
1✔
199
        BaseRequest.clear(self)
1✔
200

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

495
        meth = None
1✔
496

497
        fs = ZopeFieldStorage(fp, environ)
1✔
498

499
        # Keep a reference to the FieldStorage. Otherwise it's
500
        # __del__ method is called too early and closing FieldStorage.file.
501
        self._hold(fs)
1✔
502

503
        if not hasattr(fs, 'list'):
1✔
504
            if 'HTTP_SOAPACTION' in environ:
1!
505
                # Stash XML request for interpretation by a SOAP-aware view
506
                other['SOAPXML'] = fs.value
×
507
            elif (method == 'POST'
1✔
508
                  and 'text/xml' in fs.headers.get('content-type', '')
509
                  and use_builtin_xmlrpc(self)):
510
                # Ye haaa, XML-RPC!
511
                meth, self.args = xmlrpc.parse_input(fs.value)
1✔
512
                response = xmlrpc.response(response)
1✔
513
                other['RESPONSE'] = self.response = response
1✔
514
                self.maybe_webdav_client = 0
1✔
515
            else:
516
                self._file = fs.file
1✔
517
        else:
518
            fslist = fs.list
1✔
519
            tuple_items = {}
1✔
520
            defaults = {}
1✔
521
            converter = None
1✔
522

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

557
                if hasattr(item, 'file') and \
1✔
558
                   hasattr(item, 'filename') and \
559
                   hasattr(item, 'headers'):
560
                    item = FileUpload(item, self.charset)
1✔
561
                else:
562
                    character_encoding = item.value_charset or self.charset
1✔
563
                    item = item.value.decode(
1✔
564
                        character_encoding, "surrogateescape")
565
                # from here on, `item` contains the field value
566
                # either as `FileUpload` or `str` with
567
                # `character_encoding` as encoding,
568
                # `key` the field name (`str`)
569

570
                flags = 0
1✔
571
                # Loop through the different types and set
572
                # the appropriate flags
573

574
                # We'll search from the back to the front.
575
                # We'll do the search in two steps.  First, we'll
576
                # do a string search, and then we'll check it with
577
                # a re search.
578

579
                delim = key.rfind(':')
1✔
580
                if delim >= 0:
1✔
581
                    mo = search_type(key, delim)
1✔
582
                    if mo:
1!
583
                        delim = mo.start(0)
1✔
584
                    else:
585
                        delim = -1
×
586

587
                    while delim >= 0:
1!
588
                        type_name = key[delim + 1:]
1✔
589
                        key = key[:delim]
1✔
590
                        c = get_converter(type_name, None)
1✔
591

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

633
                        delim = key.rfind(':')
1✔
634
                        if delim < 0:
1✔
635
                            break
1✔
636
                        mo = search_type(key, delim)
1✔
637
                        if mo:
1!
638
                            delim = mo.start(0)
1✔
639
                        else:
640
                            delim = -1
×
641

642
                # Filter out special names from form:
643
                if key in isCGI_NAMEs or key.startswith('HTTP_'):
1!
644
                    continue
×
645

646
                if flags:
1✔
647

648
                    # skip over empty fields
649
                    if flags & EMPTY:
1!
650
                        continue
×
651

652
                    # Split the key and its attribute
653
                    if flags & REC:
1✔
654
                        key = key.split(".")
1✔
655
                        key, attr = ".".join(key[:-1]), key[-1]
1✔
656

657
                    # defer conversion
658
                    if flags & CONVERTED:
1✔
659
                        try:
1✔
660
                            if character_encoding and \
1✔
661
                               getattr(converter, "binary", False):
662
                                item = item.encode(character_encoding,
1✔
663
                                                   "surrogateescape")
664
                            item = converter(item)
1✔
665

666
                        except Exception:
×
667
                            if not item and \
×
668
                               not (flags & DEFAULT) and \
669
                               key in defaults:
670
                                item = defaults[key]
×
671
                                if flags & RECORD:
×
672
                                    item = getattr(item, attr)
×
673
                                if flags & RECORDS:
×
674
                                    item = getattr(item[-1], attr)
×
675
                            else:
676
                                raise
×
677

678
                    # Determine which dictionary to use
679
                    if flags & DEFAULT:
1✔
680
                        mapping_object = defaults
1✔
681
                    else:
682
                        mapping_object = form
1✔
683

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

758
                else:
759
                    # This branch is for case when no type was specified.
760
                    mapping_object = form
1✔
761

762
                    # Insert in dictionary
763
                    if key in mapping_object:
1✔
764
                        # it is not a record or list of records
765
                        found = mapping_object[key]
1✔
766
                        if isinstance(found, list):
1!
767
                            found.append(item)
×
768
                        else:
769
                            found = [found, item]
1✔
770
                            mapping_object[key] = found
1✔
771
                    else:
772
                        mapping_object[key] = item
1✔
773

774
            # insert defaults into form dictionary
775
            if defaults:
1✔
776
                for key, value in defaults.items():
1✔
777
                    if key not in form:
1✔
778
                        # if the form does not have the key,
779
                        # set the default
780
                        form[key] = value
1✔
781
                    else:
782
                        # The form has the key
783
                        if isinstance(value, record):
1✔
784
                            # if the key is mapped to a record, get the
785
                            # record
786
                            r = form[key]
1✔
787
                            for k, v in value.__dict__.items():
1✔
788
                                # loop through the attributes and value
789
                                # in the default dictionary
790
                                if not hasattr(r, k):
1✔
791
                                    # if the form dictionary doesn't have
792
                                    # the attribute, set it to the default
793
                                    setattr(r, k, v)
1✔
794
                            form[key] = r
1✔
795

796
                        elif isinstance(value, list):
1!
797
                            # the default value is a list
798
                            val = form[key]
1✔
799
                            if not isinstance(val, list):
1!
800
                                val = [val]
×
801
                            for x in value:
1✔
802
                                # for each x in the list
803
                                if isinstance(x, record):
1✔
804
                                    # if the x is a record
805
                                    for k, v in x.__dict__.items():
1✔
806

807
                                        # loop through each
808
                                        # attribute and value in
809
                                        # the record
810

811
                                        for y in val:
1✔
812

813
                                            # loop through each
814
                                            # record in the form
815
                                            # list if it doesn't
816
                                            # have the attributes
817
                                            # in the default
818
                                            # dictionary, set them
819

820
                                            if not hasattr(y, k):
1✔
821
                                                setattr(y, k, v)
1✔
822
                                else:
823
                                    # x is not a record
824
                                    if x not in val:
1!
825
                                        val.append(x)
1✔
826
                            form[key] = val
1✔
827
                        else:
828
                            # The form has the key, the key is not mapped
829
                            # to a record or sequence so do nothing
830
                            pass
1✔
831

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

873
            self.taintedform = taint(self.form)
1✔
874

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

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

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

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

936
        req.clear()
1✔
937
        return object
1✔
938

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

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

972
    get_header = getHeader  # BBB
1✔
973

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

980
        Return a value for the variable key, or default if not found.
981

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

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

998
        if key == 'REQUEST':
1✔
999
            return self
1✔
1000

1001
        other = self.other
1✔
1002
        if key in other:
1✔
1003
            return other[key]
1✔
1004

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

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

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

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

1055
            if key == 'BODY' and self._file is not None:
1✔
1056
                try:
1✔
1057
                    fpos = self._file.tell()
1✔
1058
                    self._file.seek(0)
1✔
1059
                except Exception:
×
1060
                    fpos = None
×
1061
                v = self._file.read()
1✔
1062
                if fpos is not None:
1!
1063
                    self._file.seek(fpos)
1✔
1064
                self.other[key] = v
1✔
1065
                return v
1✔
1066

1067
            if key == 'BODYFILE' and self._file is not None:
1!
1068
                v = self._file
1✔
1069
                self.other[key] = v
1✔
1070
                return v
1✔
1071

1072
        v = self.common.get(key, _marker)
1✔
1073
        if v is not _marker:
1!
1074
            return v
×
1075

1076
        if self._lazies:
1!
1077
            v = self._lazies.get(key, _marker)
×
1078
            if v is not _marker:
×
1079
                if callable(v):
×
1080
                    v = v()
×
1081
                self[key] = v  # Promote lazy value
×
1082
                del self._lazies[key]
×
1083
                return v
×
1084

1085
        # Return tainted data first (marked as suspect)
1086
        if returnTaints:
1✔
1087
            v = self.taintedform.get(key, _marker)
1✔
1088
            if v is not _marker:
1!
1089
                return v
×
1090

1091
        # Untrusted data *after* trusted data
1092
        v = self.form.get(key, _marker)
1✔
1093
        if v is not _marker:
1✔
1094
            return v
1✔
1095

1096
        # Return tainted data first (marked as suspect)
1097
        if returnTaints:
1✔
1098
            v = self.taintedcookies.get(key, _marker)
1✔
1099
            if v is not _marker:
1!
1100
                return v
×
1101

1102
        # Untrusted data *after* trusted data
1103
        v = self.cookies.get(key, _marker)
1✔
1104
        if v is not _marker:
1!
1105
            return v
×
1106

1107
        return default
1✔
1108

1109
    def __getitem__(self, key, default=_marker, returnTaints=0):
1✔
1110
        v = self.get(key, default, returnTaints=returnTaints)
1✔
1111
        if v is _marker:
1✔
1112
            raise KeyError(key)
1✔
1113
        return v
1✔
1114

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

1133
    def set_lazy(self, key, callable):
1✔
1134
        self._lazies[key] = callable
×
1135

1136
    def __contains__(self, key, returnTaints=0):
1✔
1137
        return self.has_key(key, returnTaints=returnTaints)  # NOQA
1✔
1138

1139
    def has_key(self, key, returnTaints=0):
1✔
1140
        try:
1✔
1141
            self.__getitem__(key, returnTaints=returnTaints)
1✔
1142
        except Exception:
1✔
1143
            return 0
1✔
1144
        else:
1145
            return 1
1✔
1146

1147
    def keys(self, returnTaints=0):
1✔
1148
        keys = {}
1✔
1149
        keys.update(self.common)
1✔
1150
        keys.update(self._lazies)
1✔
1151

1152
        for key in self.environ.keys():
1✔
1153
            if (key in isCGI_NAMEs or key[:5] == 'HTTP_') and \
1✔
1154
               (key not in hide_key):
1155
                keys[key] = 1
1✔
1156

1157
        # Cache URLN and BASEN in self.other.
1158
        # This relies on a side effect of has_key.
1159
        n = 0
1✔
1160
        while 1:
1161
            n = n + 1
1✔
1162
            key = "URL%s" % n
1✔
1163
            if key not in self:  # NOQA
1✔
1164
                break
1✔
1165

1166
        n = 0
1✔
1167
        while 1:
1168
            n = n + 1
1✔
1169
            key = "BASE%s" % n
1✔
1170
            if key not in self:  # NOQA
1✔
1171
                break
1✔
1172

1173
        keys.update(self.other)
1✔
1174
        keys.update(self.cookies)
1✔
1175
        if returnTaints:
1!
1176
            keys.update(self.taintedcookies)
×
1177
        keys.update(self.form)
1✔
1178
        if returnTaints:
1!
1179
            keys.update(self.taintedform)
×
1180

1181
        keys = list(keys.keys())
1✔
1182
        keys.sort()
1✔
1183

1184
        return keys
1✔
1185

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

1207
        for n in "0123456789":
1✔
1208
            key = "URL%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
        for n in "0123456789":
1✔
1214
            key = "BASE%s" % n
1✔
1215
            try:
1✔
1216
                result = result + row % (key, html.escape(self[key], False))
1✔
1217
            except KeyError:
1✔
1218
                pass
1✔
1219

1220
        result = result + "</table><h3>environ</h3><table>"
1✔
1221
        for k, v in self.environ.items():
1✔
1222
            if k not in hide_key:
1!
1223
                result = result + row % (
1✔
1224
                    html.escape(k, False), html.escape(repr(v), False))
1225
        return result + "</table>"
1✔
1226

1227
    def __repr__(self):
1✔
1228
        return f"<{self.__class__.__name__}, URL={self.get('URL')}>"
1✔
1229

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

1247
        for n in "0123456789":
1✔
1248
            key = "URL%s" % n
1✔
1249
            try:
1✔
1250
                result = result + row % (key, self[key])
1✔
1251
            except KeyError:
1✔
1252
                pass
1✔
1253
        for n in "0123456789":
1✔
1254
            key = "BASE%s" % n
1✔
1255
            try:
1✔
1256
                result = result + row % (key, self[key])
1✔
1257
            except KeyError:
1✔
1258
                pass
1✔
1259

1260
        result = result + "\nENVIRON\n\n"
1✔
1261
        for k, v in self.environ.items():
1✔
1262
            if k not in hide_key:
1!
1263
                result = result + row % (k, v)
1✔
1264
        return result
1✔
1265

1266
    def _authUserPW(self):
1✔
1267
        # Can return None
1268
        return basic_auth_decode(self._auth)
1✔
1269

1270
    def taintWrapper(self, enabled=TAINTING_ENABLED):
1✔
1271
        return enabled and TaintRequestWrapper(self) or self
1✔
1272

1273
    # Original version: zope.publisher.http.HTTPRequest.shiftNameToApplication
1274
    def shiftNameToApplication(self):
1✔
1275
        """see zope.publisher.interfaces.http.IVirtualHostRequest"""
1276
        if len(self._steps) == 1:
1!
1277
            self._script.append(self._steps.pop())
1✔
1278
            self._resetURLS()
1✔
1279
            return
1✔
1280

1281
        raise ValueError("Can only shift leading traversal "
×
1282
                         "names to application names")
1283

1284
    def getURL(self):
1✔
1285
        return self.URL
×
1286

1287

1288
class WSGIRequest(HTTPRequest):
1✔
1289
    # A request object for WSGI, no docstring to avoid being publishable.
1290
    pass
1✔
1291

1292

1293
class TaintRequestWrapper:
1✔
1294

1295
    def __init__(self, req):
1✔
1296
        self._req = req
1✔
1297

1298
    def __contains__(self, *args, **kw):
1✔
1299
        return TaintMethodWrapper(self._req.__contains__)(*args, **kw)
×
1300

1301
    def __getattr__(self, key, *args, **kw):
1✔
1302
        if key not in self._req.keys():
1✔
1303
            item = getattr(self._req, key, _marker)
1✔
1304
            if item is not _marker:
1!
1305
                return item
1✔
1306
        return TaintMethodWrapper(self._req.__getattr__)(key, *args, **kw)
1✔
1307

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

1311
    def __len__(self):
1✔
1312
        return len(self._req)
1✔
1313

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

1317
    def has_key(self, *args, **kw):
1✔
1318
        return TaintMethodWrapper(self._req.has_key)(*args, **kw)
×
1319

1320
    def keys(self, *args, **kw):
1✔
1321
        return TaintMethodWrapper(self._req.keys)(*args, **kw)
×
1322

1323

1324
class TaintMethodWrapper:
1✔
1325

1326
    def __init__(self, method):
1✔
1327
        self._method = method
1✔
1328

1329
    def __call__(self, *args, **kw):
1✔
1330
        kw['returnTaints'] = 1
1✔
1331
        return self._method(*args, **kw)
1✔
1332

1333

1334
def has_codec(x):
1✔
1335
    try:
1✔
1336
        codecs.lookup(x)
1✔
1337
    except (LookupError, SystemError):
×
1338
        return 0
×
1339
    else:
1340
        return 1
1✔
1341

1342

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

1360

1361
class ValueDescriptor:
1✔
1362
    """(non data) descriptor to compute `value` from `file`."""
1363
    def __get__(self, inst, owner=None):
1✔
1364
        if inst is None:
1!
1365
            return self
×
1366
        file = inst.file
1✔
1367
        try:
1✔
1368
            fpos = file.tell()
1✔
1369
        except Exception:
×
1370
            fpos = None
×
1371
        try:
1✔
1372
            return file.read()
1✔
1373
        finally:
1374
            if fpos is not None:
1!
1375
                file.seek(fpos)
1✔
1376

1377

1378
class ValueAccessor:
1✔
1379
    value = ValueDescriptor()
1✔
1380

1381

1382
class FormField(SimpleNamespace, ValueAccessor):
1✔
1383
    """represent a single form field.
1384

1385
    Typical attributes:
1386
    name
1387
      the field name
1388
    value
1389
      the field value (`bytes`)
1390
    name_charset, value_charset
1391
      the charset for the name and value, respectively, or ``None``
1392
      if no charset has been specified.
1393

1394
    File fields additionally have the attributes:
1395
    file
1396
      a binary file containing the file content
1397
    filename
1398
      the file's name as reported by the client
1399
    headers
1400
      a case insensitive dict with header information;
1401
      usually `content-type` and `content-disposition`.
1402

1403
    Unless otherwise noted, `latin-1` decoded bytes
1404
    are used to represent textual data.
1405
    """
1406

1407
    name_charset = value_charset = None
1✔
1408

1409

1410
class ZopeFieldStorage(ValueAccessor):
1✔
1411
    def __init__(self, fp, environ):
1✔
1412
        self.file = fp
1✔
1413
        method = environ.get("REQUEST_METHOD", "GET").upper()
1✔
1414
        url_qs = environ.get("QUERY_STRING", "")
1✔
1415
        post_qs = ""
1✔
1416
        hl = []
1✔
1417
        content_type = environ.get("CONTENT_TYPE",
1✔
1418
                                   "application/x-www-form-urlencoded")
1419
        hl.append(("content-type", content_type))
1✔
1420
        content_type, options = parse_options_header(content_type)
1✔
1421
        content_type = content_type.lower()
1✔
1422
        content_disposition = environ.get("CONTENT_DISPOSITION")
1✔
1423
        if content_disposition is not None:
1!
1424
            hl.append(("content-disposition", content_disposition))
×
1425
        self.headers = Headers(hl)
1✔
1426
        parts = ()
1✔
1427
        if method == "POST":
1✔
1428
            try:
1✔
1429
                fpos = fp.tell()
1✔
1430
            except Exception:
1✔
1431
                fpos = None
1✔
1432
            if content_type == "multipart/form-data":
1✔
1433
                parts = MultipartParser(
1✔
1434
                    fp, options["boundary"],
1435
                    mem_limit=FORM_MEMORY_LIMIT,
1436
                    disk_limit=FORM_DISK_LIMIT,
1437
                    memfile_limit=FORM_MEMFILE_LIMIT,
1438
                    charset="latin-1").parts()
1439
            elif content_type == "application/x-www-form-urlencoded":
1✔
1440
                post_qs = fp.read(FORM_MEMORY_LIMIT).decode("latin-1")
1✔
1441
                if fp.read(1):
1✔
1442
                    raise BadRequest("form data processing "
1✔
1443
                                     "requires too much memory")
1444
            elif url_qs:
1✔
1445
                raise NotImplementedError("request parameters and body")
1446
            if fpos is not None:
1✔
1447
                fp.seek(fpos)
1✔
1448
        elif url_qs and fp is not None:
1✔
1449
            raise NotImplementedError("request parameters and body")
1450
        fl = []
1✔
1451
        add_field = fl.append
1✔
1452
        post_opts = {}
1✔
1453
        if options.get("charset"):
1✔
1454
            post_opts["name_charset"] = post_opts["value_charset"] = \
1✔
1455
                options["charset"]
1456
        for qs, opts in ((url_qs, {}), (post_qs, post_opts)):
1✔
1457
            for name, val in parse_qsl(
1✔
1458
               qs,  # noqa: E121
1459
               keep_blank_values=True, encoding="latin-1"):
1460
                add_field(FormField(
1✔
1461
                    name=name, value=val.encode("latin-1"), **opts))
1462
        for part in parts:
1✔
1463
            if part.filename:
1✔
1464
                # a file
1465
                field = FormField(
1✔
1466
                    name=part.name,
1467
                    file=part.file,
1468
                    filename=part.filename,
1469
                    headers=part.headers)
1470
            else:
1471
                field = FormField(
1✔
1472
                    name=part.name, value=part.raw,
1473
                    value_charset=_mp_charset(part))
1474
            add_field(field)
1✔
1475
        if fl:
1✔
1476
            self.list = fl
1✔
1477

1478

1479
def _mp_charset(part):
1✔
1480
    """the charset of *part*."""
1481
    content_type = part.headers.get("Content-Type", "")
1✔
1482
    _, options = parse_options_header(content_type)
1✔
1483
    return options.get("charset")
1✔
1484

1485

1486
# Original version: zope.publisher.browser.FileUpload
1487
class FileUpload:
1✔
1488
    '''File upload objects
1489

1490
    File upload objects are used to represent file-uploaded data.
1491

1492
    File upload objects can be used just like files.
1493

1494
    In addition, they have a 'headers' attribute that is a dictionary
1495
    containing the file-upload headers, and a 'filename' attribute
1496
    containing the name of the uploaded file.
1497

1498
    Note that file names in HTTP/1.1 use latin-1 as charset.  See
1499
    https://github.com/zopefoundation/Zope/pull/1094#issuecomment-1459654636
1500
    '''
1501

1502
    # Allow access to attributes such as headers and filename so
1503
    # that protected code can use DTML to work with FileUploads.
1504
    __allow_access_to_unprotected_subobjects__ = 1
1✔
1505

1506
    def __init__(self, aFieldStorage, charset=None):
1✔
1507
        charset = charset or default_encoding
1✔
1508
        self.file = aFieldStorage.file
1✔
1509
        self.headers = aFieldStorage.headers
1✔
1510
        # Prevent needless encode-decode when both charsets are the same.
1511
        if charset != "latin-1":
1!
1512
            self.filename = aFieldStorage.filename\
1✔
1513
                .encode("latin-1").decode(charset)
1514
            self.name = aFieldStorage.name.encode("latin-1").decode(charset)
1✔
1515
        else:
1516
            self.filename = aFieldStorage.filename
×
1517
            self.name = aFieldStorage.name
×
1518

1519
        # Add an assertion to the rfc822.Message object that implements
1520
        # self.headers so that managed code can access them.
1521
        try:
1✔
1522
            self.headers.__allow_access_to_unprotected_subobjects__ = 1
1✔
1523
        except Exception:
×
1524
            pass
×
1525

1526
    def __getattribute__(self, key):
1✔
1527
        if key in ('close', 'closed', 'detach', 'fileno', 'flush',
1✔
1528
                   'getbuffer', 'getvalue', 'isatty', 'read', 'read1',
1529
                   'readable', 'readinto', 'readline', 'readlines',
1530
                   'seek', 'seekable', 'tell', 'truncate', 'writable',
1531
                   'write', 'writelines', 'name'):
1532
            file = object.__getattribute__(self, 'file')
1✔
1533
            func = getattr(file, key, _marker)
1✔
1534
            if func is not _marker:
1!
1535
                return func
1✔
1536
        # Always fall back to looking things up on self
1537
        return object.__getattribute__(self, key)
1✔
1538

1539
    def __iter__(self):
1✔
1540
        return self.file.__iter__()
1✔
1541

1542
    def __bool__(self):
1✔
1543
        """FileUpload objects are considered false if their
1544
           filename is empty.
1545
        """
1546
        return bool(self.filename)
1✔
1547

1548
    def __next__(self):
1✔
1549
        return self.file.__next__()
1✔
1550

1551

1552
QPARMRE = re.compile(
1✔
1553
    '([\x00- ]*([^\x00- ;,="]+)="([^"]*)"([\x00- ]*[;,])?[\x00- ]*)')
1554
PARMRE = re.compile(
1✔
1555
    '([\x00- ]*([^\x00- ;,="]+)=([^;]*)([\x00- ]*[;,])?[\x00- ]*)')
1556
PARAMLESSRE = re.compile(
1✔
1557
    '([\x00- ]*([^\x00- ;,="]+)[\x00- ]*[;,][\x00- ]*)')
1558

1559

1560
def parse_cookie(text,
1✔
1561
                 result=None,
1562
                 qparmre=QPARMRE,
1563
                 parmre=PARMRE,
1564
                 paramlessre=PARAMLESSRE,
1565
                 ):
1566

1567
    if result is None:
1!
1568
        result = {}
×
1569

1570
    mo_q = qparmre.match(text)
1✔
1571

1572
    if mo_q:
1✔
1573
        # Match quoted correct cookies
1574
        c_len = len(mo_q.group(1))
1✔
1575
        name = mo_q.group(2)
1✔
1576
        value = mo_q.group(3)
1✔
1577

1578
    else:
1579
        # Match evil MSIE cookies ;)
1580
        mo_p = parmre.match(text)
1✔
1581

1582
        if mo_p:
1✔
1583
            c_len = len(mo_p.group(1))
1✔
1584
            name = mo_p.group(2)
1✔
1585
            value = mo_p.group(3)
1✔
1586
        else:
1587
            # Broken Cookie without = nor value.
1588
            broken_p = paramlessre.match(text)
1✔
1589
            if broken_p:
1✔
1590
                c_len = len(broken_p.group(1))
1✔
1591
                name = broken_p.group(2)
1✔
1592
                value = ''
1✔
1593
            else:
1594
                return result
1✔
1595

1596
    if name not in result:
1!
1597
        result[name] = getCookieValuePolicy().load(name, value)
1✔
1598

1599
    return parse_cookie(text[c_len:], result)
1✔
1600

1601

1602
class record:
1✔
1603

1604
    # Allow access to record methods and values from DTML
1605
    __allow_access_to_unprotected_subobjects__ = 1
1✔
1606
    _guarded_writes = 1
1✔
1607

1608
    def __getattr__(self, key, default=None):
1✔
1609
        if key in ('get',
1✔
1610
                   'keys',
1611
                   'items',
1612
                   'values',
1613
                   'copy'):
1614
            return getattr(self.__dict__, key)
1✔
1615
        raise AttributeError(key)
1✔
1616

1617
    def __contains__(self, key):
1✔
1618
        return key in self.__dict__
1✔
1619

1620
    def __getitem__(self, key):
1✔
1621
        return self.__dict__[key]
1✔
1622

1623
    def __iter__(self):
1✔
1624
        return iter(self.__dict__)
1✔
1625

1626
    def __len__(self):
1✔
1627
        return len(self.__dict__)
1✔
1628

1629
    def __str__(self):
1✔
1630
        return ", ".join("%s: %s" % item for item in
1✔
1631
                         sorted(self.__dict__.items()))
1632

1633
    def __repr__(self):
1✔
1634
        # return repr( self.__dict__ )
1635
        return '{%s}' % ', '.join(
1✔
1636
            f"'{key}': {value!r}"
1637
            for key, value in sorted(self.__dict__.items()))
1638

1639
    def __eq__(self, other):
1✔
1640
        if not isinstance(other, record):
1!
1641
            return False
×
1642
        return sorted(self.__dict__.items()) == sorted(other.__dict__.items())
1✔
1643

1644

1645
def _filterPasswordFields(items):
1✔
1646
    # Collector #777:  filter out request fields which contain 'passw'
1647

1648
    result = []
1✔
1649

1650
    for k, v in items:
1✔
1651

1652
        if 'passw' in k.lower():
1✔
1653
            v = '<password obscured>'
1✔
1654

1655
        result.append((k, v))
1✔
1656

1657
    return result
1✔
1658

1659

1660
def use_builtin_xmlrpc(request):
1✔
1661
    checker = queryUtility(IXmlrpcChecker)
1✔
1662
    return checker is None or checker(request)
1✔
1663

1664

1665
def taint(d):
1✔
1666
    """return as ``dict`` the items from *d* which require tainting.
1667

1668
    *d* must be a ``dict`` with ``str`` keys and values recursively
1669
    build from elementary values, ``list``, ``tuple``, ``record`` and
1670
    ``dict``.
1671
    """
1672
    def should_taint(v):
1✔
1673
        """check whether *v* needs tainting."""
1674
        if isinstance(v, (list, tuple)):
1✔
1675
            return any(should_taint(x) for x in v)
1✔
1676
        if isinstance(v, (record, dict)):
1✔
1677
            for k in v:
1✔
1678
                if should_taint(k):
1!
1679
                    return True
×
1680
                if should_taint(v[k]):
1✔
1681
                    return True
1✔
1682
        return should_be_tainted(v)
1✔
1683

1684
    def _taint(v):
1✔
1685
        """return a copy of *v* with tainted replacements.
1686

1687
        In the copy, each ``str`` which requires tainting
1688
        is replaced by the corresponding ``taint_string``.
1689
        """
1690
        __traceback_info__ = v
1✔
1691
        if isinstance(v, (bytes, str)) and should_be_tainted(v):
1✔
1692
            return taint_string(v)
1✔
1693
        if isinstance(v, (list, tuple)):
1✔
1694
            return v.__class__(_taint(x) for x in v)
1✔
1695
        if isinstance(v, record):
1✔
1696
            rn = record()
1✔
1697
            for k in v:
1✔
1698
                __traceback_info__ = v, k
1✔
1699
                if should_be_tainted(k):
1!
1700
                    raise ValueError("Cannot taint `record` attribute names")
×
1701
                setattr(rn, k, _taint(v[k]))
1✔
1702
            return rn
1✔
1703
        if isinstance(v, dict):
1!
1704
            return v.__class__((_taint(k), _taint(v[k])) for k in v)
×
1705
        return v
1✔
1706
    td = {}
1✔
1707
    for k in d:
1✔
1708
        v = d[k]
1✔
1709
        if should_taint(k) or should_taint(v):
1✔
1710
            td[_taint(k)] = _taint(v)
1✔
1711
    return td
1✔
1712

1713

1714
def should_be_tainted(v):
1✔
1715
    """Work around ``AccessControl`` weakness."""
1716
    if isinstance(v, FileUpload):
1✔
1717
        # `base_should_be_tainted` would read `v` as a side effect
1718
        return False
1✔
1719
    try:
1✔
1720
        return base_should_be_tainted(v)
1✔
1721
    except Exception:
1✔
1722
        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