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

zopefoundation / Zope / 4957744426

pending completion
4957744426

push

github

Laurent Lasudry
Fix `POST` requests with Gunicorn WSGI

4270 of 6881 branches covered (62.05%)

Branch coverage included in aggregate %.

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

27056 of 31364 relevant lines covered (86.26%)

0.86 hits per line

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

86.08
/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
                except Exception:
×
1059
                    fpos = None
×
1060
                v = self._file.read()
1✔
1061
                if fpos is not None:
1!
1062
                    self._file.seek(fpos)
1✔
1063
                self.other[key] = v
1✔
1064
                return v
1✔
1065

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

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

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

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

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

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

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

1106
        return default
1✔
1107

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

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

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

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

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

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

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

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

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

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

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

1183
        return keys
1✔
1184

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

1206
        for n in "0123456789":
1✔
1207
            key = "URL%s" % n
1✔
1208
            try:
1✔
1209
                result = result + row % (key, html.escape(self[key], False))
1✔
1210
            except KeyError:
1✔
1211
                pass
1✔
1212
        for n in "0123456789":
1✔
1213
            key = "BASE%s" % n
1✔
1214
            try:
1✔
1215
                result = result + row % (key, html.escape(self[key], False))
1✔
1216
            except KeyError:
1✔
1217
                pass
1✔
1218

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

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

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

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

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

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

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

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

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

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

1286

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

1291

1292
class TaintRequestWrapper:
1✔
1293

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

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

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

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

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

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

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

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

1322

1323
class TaintMethodWrapper:
1✔
1324

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

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

1332

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

1341

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

1359

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

1376

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

1380

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

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

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

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

1406
    name_charset = value_charset = None
1✔
1407

1408

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

1477

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

1484

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

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

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

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

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

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

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

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

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

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

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

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

1550

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

1558

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

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

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

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

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

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

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

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

1600

1601
class record:
1✔
1602

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

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

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

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

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

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

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

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

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

1643

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

1647
    result = []
1✔
1648

1649
    for k, v in items:
1✔
1650

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

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

1656
    return result
1✔
1657

1658

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

1663

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

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

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

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

1712

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