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

zopefoundation / Zope / 13792825307

11 Mar 2025 04:10PM UTC coverage: 82.337% (+0.002%) from 82.335%
13792825307

push

github

dataflake
- Add configuration switch to turn off the built-in XML-RPC support

3532 of 6003 branches covered (58.84%)

Branch coverage included in aggregate %.

11 of 12 new or added lines in 3 files covered. (91.67%)

28110 of 32427 relevant lines covered (86.69%)

0.87 hits per line

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

86.92
/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.
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
from xmlrpc.client import ResponseError
1✔
28

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

54
from .cookie import getCookieValuePolicy
1✔
55

56

57
# DOS attack protection -- limiting the amount of memory for forms
58
# probably should become configurable
59
FORM_PART_LIMIT = 2 ** 10     # limit for individual form parts
1✔
60
FORM_MEMORY_LIMIT = 2 ** 20   # memory limit for forms
1✔
61
FORM_DISK_LIMIT = 2 ** 30     # disk limit for forms
1✔
62
FORM_MEMFILE_LIMIT = 2 ** 12  # limit for `BytesIO` -> temporary file switch
1✔
63

64

65
# This may get overwritten during configuration
66
default_encoding = 'utf-8'
1✔
67

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

89
isCGI_NAME = isCGI_NAMEs.__contains__
1✔
90

91
hide_key = {'HTTP_AUTHORIZATION': 1, 'HTTP_CGI_AUTHORIZATION': 1}
1✔
92

93
default_port = {'http': 80, 'https': 443}
1✔
94

95
tainting_env = str(os.environ.get('ZOPE_DTML_REQUEST_AUTOQUOTE', '')).lower()
1✔
96
TAINTING_ENABLED = tainting_env not in ('disabled', '0', 'no')
1✔
97

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

100
_marker = []
1✔
101

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

111
trusted_proxies = []
1✔
112

113
CONFIG = getConfiguration()
1✔
114

115

116
class NestedLoopExit(Exception):
1✔
117
    pass
1✔
118

119

120
@implementer(IBrowserRequest)
1✔
121
class HTTPRequest(BaseRequest):
1✔
122
    """ Model HTTP request data.
123

124
    This object provides access to request data.  This includes, the
125
    input headers, form data, server data, and cookies.
126

127
    Request objects are created by the object publisher and will be
128
    passed to published objects through the argument name, REQUEST.
129

130
    The request object is a mapping object that represents a
131
    collection of variable to value mappings.  In addition, variables
132
    are divided into five categories:
133

134
      - Environment variables
135

136
        These variables include input headers, server data, and other
137
        request-related data.  The variable names are as specified
138
        in the <a href="https://tools.ietf.org/html/rfc3875">CGI
139
        specification</a>
140

141
      - Form data
142

143
        These are data extracted from either a URL-encoded query
144
        string or body, if present.
145

146
      - Cookies
147

148
        These are the cookie data, if present.
149

150
      - Lazy Data
151

152
        These are callables which are deferred until explicitly
153
        referenced, at which point they are resolved and stored as
154
        application data.
155

156
      - Other
157

158
        Data that may be set by an application object.
159

160
    The form attribute of a request is actually a Field Storage
161
    object.  When file uploads are used, this provides a richer and
162
    more complex interface than is provided by accessing form data as
163
    items of the request.  See the FieldStorage class documentation
164
    for more details.
165

166
    The request object may be used as a mapping object, in which case
167
    values will be looked up in the order: environment variables,
168
    other variables, form data, and then cookies.
169
    """
170

171
    _hacked_path = None
1✔
172
    args = ()
1✔
173
    _urls = ()
1✔
174
    _file = None
1✔
175

176
    charset = default_encoding
1✔
177
    retry_max_count = 0
1✔
178

179
    def supports_retry(self):
1✔
180
        return self.retry_count < self.retry_max_count
1✔
181

182
    def delay_retry(self):
1✔
183
        # Insert a delay before retrying. Moved here from supports_retry.
184
        time.sleep(random.uniform(0, 2 ** (self.retry_count)))
1✔
185

186
    def retry(self):
1✔
187
        self.retry_count = self.retry_count + 1
1✔
188
        self.stdin.seek(0)
1✔
189
        r = self.__class__(stdin=self.stdin,
1✔
190
                           environ=self._orig_env,
191
                           response=self.response.retry())
192
        r.retry_count = self.retry_count
1✔
193
        return r
1✔
194

195
    def clear(self):
1✔
196
        # Clear all references to the input stream, possibly
197
        # removing tempfiles.
198
        self.stdin = None
1✔
199
        self._file = None
1✔
200
        self._fs = None
1✔
201
        for f in self.form.values():
1✔
202
            if isinstance(f, FileUpload):
1✔
203
                f.close()
1✔
204
        self.form.clear()
1✔
205
        # we want to clear the lazy dict here because BaseRequests don't have
206
        # one.  Without this, there's the possibility of memory leaking
207
        # after every request.
208
        self._lazies = {}
1✔
209
        BaseRequest.clear(self)
1✔
210

211
    def setServerURL(self, protocol=None, hostname=None, port=None):
1✔
212
        """ Set the parts of generated URLs. """
213
        other = self.other
1✔
214
        server_url = other.get('SERVER_URL', '')
1✔
215
        if protocol is None and hostname is None and port is None:
1!
216
            return server_url
×
217
        old_url = urlparse(server_url)
1✔
218
        if protocol is None:
1!
219
            protocol = old_url.scheme
×
220
        if hostname is None:
1!
221
            hostname = old_url.hostname
×
222
        if port is None:
1✔
223
            port = old_url.port
1✔
224

225
        if (port is None or default_port[protocol] == port):
1✔
226
            host = hostname
1✔
227
        else:
228
            host = f'{hostname}:{port}'
1✔
229
        server_url = other['SERVER_URL'] = f'{protocol}://{host}'
1✔
230
        self._resetURLS()
1✔
231
        return server_url
1✔
232

233
    def setVirtualRoot(self, path, hard=0):
1✔
234
        """ Treat the current publishing object as a VirtualRoot """
235
        other = self.other
1✔
236
        if isinstance(path, str):
1✔
237
            path = path.split('/')
1✔
238
        self._script[:] = list(map(quote, [_p for _p in path if _p]))
1✔
239
        del self._steps[:]
1✔
240
        parents = other['PARENTS']
1✔
241
        if hard:
1!
242
            del parents[:-1]
×
243
        other['VirtualRootPhysicalPath'] = parents[-1].getPhysicalPath()
1✔
244
        self._resetURLS()
1✔
245

246
    def getVirtualRoot(self):
1✔
247
        """ Return a slash-separated virtual root.
248

249
        If it is same as the physical root, return ''.
250
        """
251
        return '/'.join([''] + self._script)
1✔
252

253
    def physicalPathToVirtualPath(self, path):
1✔
254
        """ Remove the path to the VirtualRoot from a physical path """
255
        if isinstance(path, str):
1!
256
            path = path.split('/')
×
257
        rpp = self.other.get('VirtualRootPhysicalPath', ('',))
1✔
258
        i = 0
1✔
259
        for name in rpp[:len(path)]:
1✔
260
            if path[i] == name:
1✔
261
                i = i + 1
1✔
262
            else:
263
                break
1✔
264
        return path[i:]
1✔
265

266
    def physicalPathToURL(self, path, relative=0):
1✔
267
        """ Convert a physical path into a URL in the current context """
268
        path = self._script + list(
1✔
269
            map(quote, self.physicalPathToVirtualPath(path)))
270
        if relative:
1✔
271
            path.insert(0, '')
1✔
272
        else:
273
            path.insert(0, self['SERVER_URL'])
1✔
274
        return '/'.join(path)
1✔
275

276
    def physicalPathFromURL(self, URL):
1✔
277
        """ Convert a URL into a physical path in the current context.
278
            If the URL makes no sense in light of the current virtual
279
            hosting context, a ValueError is raised."""
280
        other = self.other
1✔
281
        path = [_p for _p in URL.split('/') if _p]
1✔
282

283
        if URL.find('://') >= 0:
1!
284
            path = path[2:]
1✔
285

286
        # Check the path against BASEPATH1
287
        vhbase = self._script
1✔
288
        vhbl = len(vhbase)
1✔
289
        if path[:vhbl] == vhbase:
1!
290
            path = path[vhbl:]
1✔
291
        else:
292
            raise ValueError('Url does not match virtual hosting context')
×
293
        vrpp = other.get('VirtualRootPhysicalPath', ('',))
1✔
294
        return list(vrpp) + list(map(unquote, path))
1✔
295

296
    def _resetURLS(self):
1✔
297
        other = self.other
1✔
298
        other['URL'] = '/'.join(
1✔
299
            [other['SERVER_URL']] + self._script + self._steps)
300
        for x in self._urls:
1!
301
            del self.other[x]
×
302
        self._urls = ()
1✔
303

304
    def getClientAddr(self):
1✔
305
        """ The IP address of the client.
306
        """
307
        return self._client_addr
1✔
308

309
    def setupLocale(self):
1✔
310
        envadapter = IUserPreferredLanguages(self, None)
1✔
311
        if envadapter is None:
1!
312
            self._locale = None
×
313
            return
×
314

315
        langs = envadapter.getPreferredLanguages()
1✔
316
        for httplang in langs:
1✔
317
            parts = (httplang.split('-') + [None, None])[:3]
1✔
318
            try:
1✔
319
                self._locale = locales.getLocale(*parts)
1✔
320
                return
1✔
321
            except LoadLocaleError:
1✔
322
                # Just try the next combination
323
                pass
1✔
324
        else:
325
            # No combination gave us an existing locale, so use the default,
326
            # which is guaranteed to exist
327
            self._locale = locales.getLocale(None, None, None)
1✔
328

329
    def __init__(self, stdin, environ, response, clean=0):
1✔
330
        self.__doc__ = None  # Make HTTPRequest objects unpublishable
1✔
331
        self._orig_env = environ
1✔
332
        # Avoid the overhead of scrubbing the environment in the
333
        # case of request cloning for traversal purposes. If the
334
        # clean flag is set, we know we can use the passed in
335
        # environ dict directly.
336
        if not clean:
1✔
337
            environ = sane_environment(environ)
1✔
338

339
        if 'HTTP_AUTHORIZATION' in environ:
1✔
340
            self._auth = environ['HTTP_AUTHORIZATION']
1✔
341
            response._auth = 1
1✔
342
            del environ['HTTP_AUTHORIZATION']
1✔
343

344
        self.stdin = stdin
1✔
345
        self.environ = environ
1✔
346
        get_env = environ.get
1✔
347
        self.response = response
1✔
348
        other = self.other = {'RESPONSE': response}
1✔
349
        self.form = {}
1✔
350
        self.taintedform = {}
1✔
351
        self.steps = []
1✔
352
        self._steps = []
1✔
353
        self._lazies = {}
1✔
354
        self._debug = DebugFlags()
1✔
355
        # We don't set up the locale initially but just on first access
356
        self._locale = _marker
1✔
357

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

377
        ################################################################
378
        # Get base info first. This isn't likely to cause
379
        # errors and might be useful to error handlers.
380
        b = script = get_env('SCRIPT_NAME', '').strip()
1✔
381

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

385
        while b and b[-1] == '/':
1!
386
            b = b[:-1]
×
387
        p = b.rfind('/')
1✔
388
        if p >= 0:
1!
389
            b = b[:p + 1]
×
390
        else:
391
            b = ''
1✔
392
        while b and b[0] == '/':
1!
393
            b = b[1:]
×
394

395
        server_url = get_env('SERVER_URL', None)
1✔
396
        if server_url is not None:
1✔
397
            other['SERVER_URL'] = server_url = server_url.strip()
1✔
398
        else:
399
            https_environ = environ.get('HTTPS', False)
1✔
400
            if https_environ and https_environ in ('on', 'ON', '1'):
1✔
401
                protocol = 'https'
1✔
402
            elif environ.get('SERVER_PORT_SECURE', None) == 1:
1✔
403
                protocol = 'https'
1✔
404
            elif (environ.get('REQUEST_SCHEME', '') or '').lower() == 'https':
1✔
405
                protocol = 'https'
1✔
406
            elif environ.get('wsgi.url_scheme') == 'https':
1✔
407
                protocol = 'https'
1✔
408
            else:
409
                protocol = 'http'
1✔
410

411
            if 'HTTP_HOST' in environ:
1✔
412
                host = environ['HTTP_HOST'].strip()
1✔
413
                hostname, port = splitport(host)
1✔
414

415
                # NOTE: some (DAV) clients manage to forget the port. This
416
                # can be fixed with the commented code below - the problem
417
                # is that it causes problems for virtual hosting. I've left
418
                # the commented code here in case we care enough to come
419
                # back and do anything with it later.
420
                #
421
                # if port is None and 'SERVER_PORT' in environ:
422
                #     s_port = environ['SERVER_PORT']
423
                #     if s_port not in ('80', '443'):
424
                #         port = s_port
425

426
            else:
427
                hostname = environ['SERVER_NAME'].strip()
1✔
428
                port = int(environ['SERVER_PORT'])
1✔
429
            self.setServerURL(protocol=protocol, hostname=hostname, port=port)
1✔
430
            server_url = other['SERVER_URL']
1✔
431

432
        if server_url[-1:] == '/':
1!
433
            server_url = server_url[:-1]
×
434

435
        if b:
1!
436
            self.base = f"{server_url}/{b}"
×
437
        else:
438
            self.base = server_url
1✔
439
        while script[:1] == '/':
1!
440
            script = script[1:]
×
441
        if script:
1!
442
            script = f"{server_url}/{script}"
×
443
        else:
444
            script = server_url
1✔
445
        other['URL'] = self.script = script
1✔
446
        other['method'] = environ.get('REQUEST_METHOD', 'GET').upper()
1✔
447

448
        # Make WEBDAV_SOURCE_PORT reachable with a simple REQUEST.get
449
        # to stay backwards-compatible
450
        if environ.get('WEBDAV_SOURCE_PORT'):
1✔
451
            other['WEBDAV_SOURCE_PORT'] = environ.get('WEBDAV_SOURCE_PORT')
1✔
452

453
        ################################################################
454
        # Cookie values should *not* be appended to existing form
455
        # vars with the same name - they are more like default values
456
        # for names not otherwise specified in the form.
457
        cookies = {}
1✔
458
        k = get_env('HTTP_COOKIE', '')
1✔
459
        if k:
1✔
460
            parse_cookie(k, cookies)
1✔
461
        self.cookies = cookies
1✔
462
        self.taintedcookies = taint(cookies)
1✔
463

464
    def processInputs(
1✔
465
            self,
466
            # "static" variables that we want to be local for speed
467
            SEQUENCE=1,
468
            DEFAULT=2,
469
            RECORD=4,
470
            RECORDS=8,
471
            REC=12,  # RECORD | RECORDS
472
            EMPTY=16,
473
            CONVERTED=32,
474
            hasattr=hasattr,
475
            getattr=getattr,
476
            setattr=setattr):
477
        """Process request inputs
478

479
        See the `Zope Developer Guide Object Publishing chapter
480
        <https://zope.readthedocs.io/en/latest/zdgbook/ObjectPublishing.html>`_
481
        for a detailed explanation in the section `Marshalling Arguments from
482
        the Request`.
483

484
        We need to delay input parsing so that it is done under
485
        publisher control for error handling purposes.
486
        """
487
        response = self.response
1✔
488
        environ = self.environ
1✔
489
        method = environ.get('REQUEST_METHOD', 'GET')
1✔
490

491
        if method != 'GET':
1✔
492
            fp = self.stdin
1✔
493
        else:
494
            fp = None
1✔
495

496
        form = self.form
1✔
497
        other = self.other
1✔
498

499
        # If 'QUERY_STRING' is not present in environ
500
        # FieldStorage will try to get it from sys.argv[1]
501
        # which is not what we need.
502
        if 'QUERY_STRING' not in environ:
1✔
503
            environ['QUERY_STRING'] = ''
1✔
504

505
        meth = None
1✔
506

507
        try:
1✔
508
            self._fs = fs = ZopeFieldStorage(fp, environ)
1✔
509
        except MultipartError as exc:
1✔
510
            raise BadRequest(exc)
1✔
511
        self._file = fs.file
1✔
512

513
        if 'HTTP_SOAPACTION' in environ:
1✔
514
            # Stash XML request for interpretation by a SOAP-aware view
515
            other['SOAPXML'] = fs.value
1✔
516
            if 'CONTENT_TYPE' not in environ:
1!
517
                environ['CONTENT_TYPE'] = 'application/soap+xml'
1✔
518

519
        if fs.list:
1✔
520
            fslist = fs.list
1✔
521
            tuple_items = {}
1✔
522
            defaults = {}
1✔
523
            converter = None
1✔
524

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

557
                try:
1✔
558
                    key = item.name.encode("latin-1").decode(
1✔
559
                        item.name_charset or self.charset)
560
                except UnicodeDecodeError as exc:
1✔
561
                    # Incoming request contains fields with names that cannot
562
                    # be represented in the server character set. Potentially
563
                    # malicious, so raise BadRequest.
564
                    raise BadRequest(exc)
1✔
565

566
                if hasattr(item, 'file') and \
1✔
567
                   hasattr(item, 'filename') and \
568
                   hasattr(item, 'headers'):
569
                    item = FileUpload(item, self.charset)
1✔
570
                else:
571
                    character_encoding = item.value_charset or self.charset
1✔
572
                    item = item.value.decode(
1✔
573
                        character_encoding, "surrogateescape")
574
                # from here on, `item` contains the field value
575
                # either as `FileUpload` or `str` with
576
                # `character_encoding` as encoding,
577
                # `key` the field name (`str`)
578

579
                flags = 0
1✔
580
                # Loop through the different types and set
581
                # the appropriate flags
582

583
                # We'll search from the back to the front.
584
                # We'll do the search in two steps.  First, we'll
585
                # do a string search, and then we'll check it with
586
                # a re search.
587

588
                delim = key.rfind(':')
1✔
589
                if delim >= 0:
1✔
590
                    mo = search_type(key, delim)
1✔
591
                    if mo:
1!
592
                        delim = mo.start(0)
1✔
593
                    else:
594
                        delim = -1
×
595

596
                    while delim >= 0:
1!
597
                        type_name = key[delim + 1:]
1✔
598
                        key = key[:delim]
1✔
599
                        c = get_converter(type_name, None)
1✔
600

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

642
                        delim = key.rfind(':')
1✔
643
                        if delim < 0:
1✔
644
                            break
1✔
645
                        mo = search_type(key, delim)
1✔
646
                        if mo:
1!
647
                            delim = mo.start(0)
1✔
648
                        else:
649
                            delim = -1
×
650

651
                # Filter out special names from form:
652
                if key in isCGI_NAMEs or key.startswith('HTTP_'):
1!
653
                    continue
×
654

655
                if flags:
1✔
656

657
                    # skip over empty fields
658
                    if flags & EMPTY:
1!
659
                        continue
×
660

661
                    # Split the key and its attribute
662
                    if flags & REC:
1✔
663
                        key = key.split(".")
1✔
664
                        key, attr = ".".join(key[:-1]), key[-1]
1✔
665

666
                    # defer conversion
667
                    if flags & CONVERTED:
1✔
668
                        try:
1✔
669
                            if character_encoding and \
1✔
670
                               getattr(converter, "binary", False):
671
                                item = item.encode(character_encoding,
1✔
672
                                                   "surrogateescape")
673
                            item = converter(item)
1✔
674

675
                        except Exception:
×
676
                            if not item and \
×
677
                               not (flags & DEFAULT) and \
678
                               key in defaults:
679
                                item = defaults[key]
×
680
                                if flags & RECORD:
×
681
                                    item = getattr(item, attr)
×
682
                                if flags & RECORDS:
×
683
                                    item = getattr(item[-1], attr)
×
684
                            else:
685
                                raise
×
686

687
                    # Determine which dictionary to use
688
                    if flags & DEFAULT:
1✔
689
                        mapping_object = defaults
1✔
690
                    else:
691
                        mapping_object = form
1✔
692

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

767
                else:
768
                    # This branch is for case when no type was specified.
769
                    mapping_object = form
1✔
770

771
                    # Insert in dictionary
772
                    if key in mapping_object:
1✔
773
                        # it is not a record or list of records
774
                        found = mapping_object[key]
1✔
775
                        if isinstance(found, list):
1!
776
                            found.append(item)
×
777
                        else:
778
                            found = [found, item]
1✔
779
                            mapping_object[key] = found
1✔
780
                    else:
781
                        mapping_object[key] = item
1✔
782

783
            # insert defaults into form dictionary
784
            if defaults:
1✔
785
                for key, value in defaults.items():
1✔
786
                    if key not in form:
1✔
787
                        # if the form does not have the key,
788
                        # set the default
789
                        form[key] = value
1✔
790
                    else:
791
                        # The form has the key
792
                        if isinstance(value, record):
1✔
793
                            # if the key is mapped to a record, get the
794
                            # record
795
                            r = form[key]
1✔
796
                            for k, v in value.__dict__.items():
1✔
797
                                # loop through the attributes and value
798
                                # in the default dictionary
799
                                if not hasattr(r, k):
1✔
800
                                    # if the form dictionary doesn't have
801
                                    # the attribute, set it to the default
802
                                    setattr(r, k, v)
1✔
803
                            form[key] = r
1✔
804

805
                        elif isinstance(value, list):
1✔
806
                            # the default value is a list
807
                            val = form[key]
1✔
808
                            if not isinstance(val, list):
1!
809
                                val = [val]
×
810
                            for x in value:
1✔
811
                                # for each x in the list
812
                                if isinstance(x, record):
1✔
813
                                    # if the x is a record
814
                                    for k, v in x.__dict__.items():
1✔
815

816
                                        # loop through each
817
                                        # attribute and value in
818
                                        # the record
819

820
                                        for y in val:
1✔
821

822
                                            # loop through each
823
                                            # record in the form
824
                                            # list if it doesn't
825
                                            # have the attributes
826
                                            # in the default
827
                                            # dictionary, set them
828

829
                                            if not hasattr(y, k):
1✔
830
                                                setattr(y, k, v)
1✔
831
                                else:
832
                                    # x is not a record
833
                                    if x not in val:
1!
834
                                        val.append(x)
1✔
835
                            form[key] = val
1✔
836
                        else:
837
                            # The form has the key, the key is not mapped
838
                            # to a record or sequence so do nothing
839
                            pass
1✔
840

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

882
            self.taintedform = taint(self.form)
1✔
883

884
        if method == 'POST' \
1✔
885
           and 'text/xml' in fs.headers.get('content-type', '') \
886
           and use_builtin_xmlrpc(self):
887
            # Ye haaa, XML-RPC!
888
            if meth is not None:
1✔
889
                raise BadRequest('method directive not supported for '
1✔
890
                                 'xmlrpc request')
891

892
            if not CONFIG.enable_xmlrpc:
1!
NEW
893
                raise BadRequest('Unsupported request type')
×
894

895
            try:
1✔
896
                meth, self.args = xmlrpc.parse_input(fs.value)
1✔
897
            except ResponseError as e:
1✔
898
                raise BadRequest(e)
1✔
899
            response = xmlrpc.response(response)
1✔
900
            other['RESPONSE'] = self.response = response
1✔
901
            self.maybe_webdav_client = 0
1✔
902

903
        if meth:
1✔
904
            if 'PATH_INFO' in environ:
1✔
905
                path = environ['PATH_INFO']
1✔
906
                while path[-1:] == '/':
1!
907
                    path = path[:-1]
×
908
            else:
909
                path = ''
1✔
910
            other['PATH_INFO'] = f"{path}/{meth}"
1✔
911
            self._hacked_path = 1
1✔
912

913
    def resolve_url(self, url):
1✔
914
        # Attempt to resolve a url into an object in the Zope
915
        # namespace. The url must be a fully-qualified url. The
916
        # method will return the requested object if it is found
917
        # or raise the same HTTP error that would be raised in
918
        # the case of a real web request. If the passed in url
919
        # does not appear to describe an object in the system
920
        # namespace (e.g. the host, port or script name don't
921
        # match that of the current request), a ValueError will
922
        # be raised.
923
        if url.find(self.script) != 0:
1!
924
            raise ValueError('Different namespace.')
×
925
        path = url[len(self.script):]
1✔
926
        while path and path[0] == '/':
1✔
927
            path = path[1:]
1✔
928
        while path and path[-1] == '/':
1!
929
            path = path[:-1]
×
930
        req = self.clone()
1✔
931
        rsp = req.response
1✔
932
        req['PATH_INFO'] = path
1✔
933
        object = None
1✔
934

935
        # Try to traverse to get an object. Note that we call
936
        # the exception method on the response, but we don't
937
        # want to actually abort the current transaction
938
        # (which is usually the default when the exception
939
        # method is called on the response).
940
        try:
1✔
941
            object = req.traverse(path)
1✔
942
        except Exception as exc:
1✔
943
            rsp.exception()
1✔
944
            req.clear()
1✔
945
            raise exc.__class__(rsp.errmsg)
1✔
946

947
        # The traversal machinery may return a "default object"
948
        # like an index_html document. This is not appropriate
949
        # in the context of the resolve_url method so we need
950
        # to ensure we are getting the actual object named by
951
        # the given url, and not some kind of default object.
952
        if hasattr(object, 'id'):
1!
953
            if callable(object.id):
×
954
                name = object.id()
×
955
            else:
956
                name = object.id
×
957
        elif hasattr(object, '__name__'):
1!
958
            name = object.__name__
×
959
        else:
960
            name = ''
1✔
961
        if name != os.path.split(path)[-1]:
1!
962
            object = req.PARENTS[0]
×
963

964
        req.clear()
1✔
965
        return object
1✔
966

967
    def clone(self):
1✔
968
        # Return a clone of the current request object
969
        # that may be used to perform object traversal.
970
        environ = self.environ.copy()
1✔
971
        environ['REQUEST_METHOD'] = 'GET'
1✔
972
        if self._auth:
1✔
973
            environ['HTTP_AUTHORIZATION'] = self._auth
1✔
974
        if self.response is not None:
1✔
975
            response = self.response.__class__()
1✔
976
        else:
977
            response = None
1✔
978
        clone = self.__class__(None, environ, response, clean=1)
1✔
979
        clone['PARENTS'] = [self['PARENTS'][-1]]
1✔
980
        directlyProvides(clone, *directlyProvidedBy(self))
1✔
981
        return clone
1✔
982

983
    def getHeader(self, name, default=None, literal=False):
1✔
984
        """Return the named HTTP header, or an optional default
985
        argument or None if the header is not found. Note that
986
        both original and CGI-ified header names are recognized,
987
        e.g. 'Content-Type', 'CONTENT_TYPE' and 'HTTP_CONTENT_TYPE'
988
        should all return the Content-Type header, if available.
989
        """
990
        environ = self.environ
1✔
991
        if not literal:
1✔
992
            name = name.replace('-', '_').upper()
1✔
993
        val = environ.get(name, None)
1✔
994
        if val is not None:
1✔
995
            return val
1✔
996
        if name[:5] != 'HTTP_':
1!
997
            name = 'HTTP_%s' % name
1✔
998
        return environ.get(name, default)
1✔
999

1000
    get_header = getHeader  # BBB
1✔
1001

1002
    def get(self, key, default=None, returnTaints=0,
1✔
1003
            URLmatch=re.compile('URL(PATH)?([0-9]+)$').match,
1004
            BASEmatch=re.compile('BASE(PATH)?([0-9]+)$').match,
1005
            ):
1006
        """Get a variable value
1007

1008
        Return a value for the variable key, or default if not found.
1009

1010
        If key is "REQUEST", return the request.
1011
        Otherwise, the value will be looked up from one of the request data
1012
        categories. The search order is:
1013
        other (the target for explicitly set variables),
1014
        the special URL and BASE variables,
1015
        environment variables,
1016
        common variables (defined by the request class),
1017
        lazy variables (set with set_lazy),
1018
        form data and cookies.
1019

1020
        If returnTaints has a true value, then the access to
1021
        form and cookie variables returns values with special
1022
        protection against embedded HTML fragments to counter
1023
        some cross site scripting attacks.
1024
        """
1025

1026
        if key == 'REQUEST':
1✔
1027
            return self
1✔
1028

1029
        other = self.other
1✔
1030
        if key in other:
1✔
1031
            return other[key]
1✔
1032

1033
        if key[:1] == 'U':
1✔
1034
            match = URLmatch(key)
1✔
1035
            if match is not None:
1!
1036
                pathonly, n = match.groups()
1✔
1037
                path = self._script + self._steps
1✔
1038
                n = len(path) - int(n)
1✔
1039
                if n < 0:
1✔
1040
                    raise KeyError(key)
1✔
1041
                if pathonly:
1✔
1042
                    path = [''] + path[:n]
1✔
1043
                else:
1044
                    path = [other['SERVER_URL']] + path[:n]
1✔
1045
                URL = '/'.join(path)
1✔
1046
                if 'PUBLISHED' in other:
1✔
1047
                    # Don't cache URLs until publishing traversal is done.
1048
                    other[key] = URL
1✔
1049
                    self._urls = self._urls + (key,)
1✔
1050
                return URL
1✔
1051

1052
        if key in isCGI_NAMEs or key[:5] == 'HTTP_':
1✔
1053
            environ = self.environ
1✔
1054
            if key in environ and (key not in hide_key):
1✔
1055
                return environ[key]
1✔
1056
            return ''
1✔
1057

1058
        if key[:1] == 'B':
1✔
1059
            match = BASEmatch(key)
1✔
1060
            if match is not None:
1✔
1061
                pathonly, n = match.groups()
1✔
1062
                path = self._steps
1✔
1063
                n = int(n)
1✔
1064
                if n:
1✔
1065
                    n = n - 1
1✔
1066
                    if len(path) < n:
1✔
1067
                        raise KeyError(key)
1✔
1068

1069
                    v = self._script + path[:n]
1✔
1070
                else:
1071
                    v = self._script[:-1]
1✔
1072
                if pathonly:
1✔
1073
                    v.insert(0, '')
1✔
1074
                else:
1075
                    v.insert(0, other['SERVER_URL'])
1✔
1076
                URL = '/'.join(v)
1✔
1077
                if 'PUBLISHED' in other:
1✔
1078
                    # Don't cache URLs until publishing traversal is done.
1079
                    other[key] = URL
1✔
1080
                    self._urls = self._urls + (key,)
1✔
1081
                return URL
1✔
1082

1083
            if key == 'BODY' and self._file is not None:
1✔
1084
                v = self.other[key] = self._fs.value
1✔
1085
                return v
1✔
1086

1087
            if key == 'BODYFILE' and self._file is not None:
1✔
1088
                v = self._file
1✔
1089
                self.other[key] = v
1✔
1090
                return v
1✔
1091

1092
        v = self.common.get(key, _marker)
1✔
1093
        if v is not _marker:
1!
1094
            return v
×
1095

1096
        if self._lazies:
1!
1097
            v = self._lazies.get(key, _marker)
×
1098
            if v is not _marker:
×
1099
                if callable(v):
×
1100
                    v = v()
×
1101
                self[key] = v  # Promote lazy value
×
1102
                del self._lazies[key]
×
1103
                return v
×
1104

1105
        # Return tainted data first (marked as suspect)
1106
        if returnTaints:
1✔
1107
            v = self.taintedform.get(key, _marker)
1✔
1108
            if v is not _marker:
1!
1109
                return v
×
1110

1111
        # Untrusted data *after* trusted data
1112
        v = self.form.get(key, _marker)
1✔
1113
        if v is not _marker:
1✔
1114
            return v
1✔
1115

1116
        # Return tainted data first (marked as suspect)
1117
        if returnTaints:
1✔
1118
            v = self.taintedcookies.get(key, _marker)
1✔
1119
            if v is not _marker:
1!
1120
                return v
×
1121

1122
        # Untrusted data *after* trusted data
1123
        v = self.cookies.get(key, _marker)
1✔
1124
        if v is not _marker:
1!
1125
            return v
×
1126

1127
        return default
1✔
1128

1129
    def __getitem__(self, key, default=_marker, returnTaints=0):
1✔
1130
        v = self.get(key, default, returnTaints=returnTaints)
1✔
1131
        if v is _marker:
1✔
1132
            raise KeyError(key)
1✔
1133
        return v
1✔
1134

1135
    # Using the getattr protocol to retrieve form values and similar
1136
    # is discouraged and is likely to be deprecated in the future.
1137
    # request.get(key) or request[key] should be used instead
1138
    def __getattr__(self, key, default=_marker, returnTaints=0):
1✔
1139
        v = self.get(key, default, returnTaints=returnTaints)
1✔
1140
        if v is _marker:
1✔
1141
            if key == 'locale':
1✔
1142
                # we only create the _locale on first access, as setting it
1143
                # up might be slow and we don't want to slow down every
1144
                # request
1145
                if self._locale is _marker:
1✔
1146
                    self.setupLocale()
1✔
1147
                return self._locale
1✔
1148
            if key == 'debug':
1✔
1149
                return self._debug
1✔
1150
            raise AttributeError(key)
1✔
1151
        return v
1✔
1152

1153
    def set_lazy(self, key, callable):
1✔
1154
        self._lazies[key] = callable
×
1155

1156
    def __contains__(self, key, returnTaints=0):
1✔
1157
        return self.has_key(key, returnTaints=returnTaints)  # NOQA
1✔
1158

1159
    def has_key(self, key, returnTaints=0):
1✔
1160
        try:
1✔
1161
            self.__getitem__(key, returnTaints=returnTaints)
1✔
1162
        except Exception:
1✔
1163
            return 0
1✔
1164
        else:
1165
            return 1
1✔
1166

1167
    def keys(self, returnTaints=0):
1✔
1168
        keys = {}
1✔
1169
        keys.update(self.common)
1✔
1170
        keys.update(self._lazies)
1✔
1171

1172
        for key in self.environ.keys():
1✔
1173
            if (key in isCGI_NAMEs or key[:5] == 'HTTP_') and \
1✔
1174
               (key not in hide_key):
1175
                keys[key] = 1
1✔
1176

1177
        # Cache URLN and BASEN in self.other.
1178
        # This relies on a side effect of has_key.
1179
        n = 0
1✔
1180
        while 1:
1✔
1181
            n = n + 1
1✔
1182
            key = "URL%s" % n
1✔
1183
            if key not in self:  # NOQA
1✔
1184
                break
1✔
1185

1186
        n = 0
1✔
1187
        while 1:
1✔
1188
            n = n + 1
1✔
1189
            key = "BASE%s" % n
1✔
1190
            if key not in self:  # NOQA
1✔
1191
                break
1✔
1192

1193
        keys.update(self.other)
1✔
1194
        keys.update(self.cookies)
1✔
1195
        if returnTaints:
1!
1196
            keys.update(self.taintedcookies)
×
1197
        keys.update(self.form)
1✔
1198
        if returnTaints:
1!
1199
            keys.update(self.taintedform)
×
1200

1201
        keys = list(keys.keys())
1✔
1202
        keys.sort()
1✔
1203

1204
        return keys
1✔
1205

1206
    def __str__(self):
1✔
1207
        result = "<h3>form</h3><table>"
1✔
1208
        row = '<tr valign="top" align="left"><th>%s</th><td>%s</td></tr>'
1✔
1209
        for k, v in _filterPasswordFields(self.form.items()):
1✔
1210
            result = result + row % (
1✔
1211
                html.escape(k, False), html.escape(repr(v), False))
1212
        result = result + "</table><h3>cookies</h3><table>"
1✔
1213
        for k, v in _filterPasswordFields(self.cookies.items()):
1!
1214
            result = result + row % (
×
1215
                html.escape(k, False), html.escape(repr(v), False))
1216
        result = result + "</table><h3>lazy items</h3><table>"
1✔
1217
        for k, v in _filterPasswordFields(self._lazies.items()):
1!
1218
            result = result + row % (
×
1219
                html.escape(k, False), html.escape(repr(v), False))
1220
        result = result + "</table><h3>other</h3><table>"
1✔
1221
        for k, v in _filterPasswordFields(self.other.items()):
1✔
1222
            if k in ('PARENTS', 'RESPONSE'):
1✔
1223
                continue
1✔
1224
            result = result + row % (
1✔
1225
                html.escape(k, False), html.escape(repr(v), False))
1226

1227
        for n in "0123456789":
1✔
1228
            key = "URL%s" % n
1✔
1229
            try:
1✔
1230
                result = result + row % (key, html.escape(self[key], False))
1✔
1231
            except KeyError:
1✔
1232
                pass
1✔
1233
        for n in "0123456789":
1✔
1234
            key = "BASE%s" % n
1✔
1235
            try:
1✔
1236
                result = result + row % (key, html.escape(self[key], False))
1✔
1237
            except KeyError:
1✔
1238
                pass
1✔
1239

1240
        result = result + "</table><h3>environ</h3><table>"
1✔
1241
        for k, v in self.environ.items():
1✔
1242
            if k not in hide_key:
1!
1243
                result = result + row % (
1✔
1244
                    html.escape(k, False), html.escape(repr(v), False))
1245
        return result + "</table>"
1✔
1246

1247
    def __repr__(self):
1✔
1248
        return f"<{self.__class__.__name__}, URL={self.get('URL')}>"
1✔
1249

1250
    def text(self):
1✔
1251
        result = "FORM\n\n"
1✔
1252
        row = '%-20s %s\n'
1✔
1253
        for k, v in _filterPasswordFields(self.form.items()):
1✔
1254
            result = result + row % (k, repr(v))
1✔
1255
        result = result + "\nCOOKIES\n\n"
1✔
1256
        for k, v in _filterPasswordFields(self.cookies.items()):
1!
1257
            result = result + row % (k, repr(v))
×
1258
        result = result + "\nLAZY ITEMS\n\n"
1✔
1259
        for k, v in _filterPasswordFields(self._lazies.items()):
1!
1260
            result = result + row % (k, repr(v))
×
1261
        result = result + "\nOTHER\n\n"
1✔
1262
        for k, v in _filterPasswordFields(self.other.items()):
1✔
1263
            if k in ('PARENTS', 'RESPONSE'):
1✔
1264
                continue
1✔
1265
            result = result + row % (k, repr(v))
1✔
1266

1267
        for n in "0123456789":
1✔
1268
            key = "URL%s" % n
1✔
1269
            try:
1✔
1270
                result = result + row % (key, self[key])
1✔
1271
            except KeyError:
1✔
1272
                pass
1✔
1273
        for n in "0123456789":
1✔
1274
            key = "BASE%s" % n
1✔
1275
            try:
1✔
1276
                result = result + row % (key, self[key])
1✔
1277
            except KeyError:
1✔
1278
                pass
1✔
1279

1280
        result = result + "\nENVIRON\n\n"
1✔
1281
        for k, v in self.environ.items():
1✔
1282
            if k not in hide_key:
1!
1283
                result = result + row % (k, v)
1✔
1284
        return result
1✔
1285

1286
    def _authUserPW(self):
1✔
1287
        # Can return None
1288
        return basic_auth_decode(self._auth)
1✔
1289

1290
    def taintWrapper(self, enabled=TAINTING_ENABLED):
1✔
1291
        return enabled and TaintRequestWrapper(self) or self
1✔
1292

1293
    # Original version: zope.publisher.http.HTTPRequest.shiftNameToApplication
1294
    def shiftNameToApplication(self):
1✔
1295
        """see zope.publisher.interfaces.http.IVirtualHostRequest"""
1296
        if len(self._steps) == 1:
1!
1297
            self._script.append(self._steps.pop())
1✔
1298
            self._resetURLS()
1✔
1299
            return
1✔
1300

1301
        raise ValueError("Can only shift leading traversal "
×
1302
                         "names to application names")
1303

1304
    def getURL(self):
1✔
1305
        return self.URL
×
1306

1307

1308
class WSGIRequest(HTTPRequest):
1✔
1309
    # A request object for WSGI, no docstring to avoid being publishable.
1310
    pass
1✔
1311

1312

1313
class TaintRequestWrapper:
1✔
1314

1315
    def __init__(self, req):
1✔
1316
        self._req = req
1✔
1317

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

1321
    def __getattr__(self, key, *args, **kw):
1✔
1322
        if key not in self._req.keys():
1✔
1323
            item = getattr(self._req, key, _marker)
1✔
1324
            if item is not _marker:
1!
1325
                return item
1✔
1326
        return TaintMethodWrapper(self._req.__getattr__)(key, *args, **kw)
1✔
1327

1328
    def __getitem__(self, *args, **kw):
1✔
1329
        return TaintMethodWrapper(self._req.__getitem__)(*args, **kw)
1✔
1330

1331
    def __len__(self):
1✔
1332
        return len(self._req)
1✔
1333

1334
    def get(self, *args, **kw):
1✔
1335
        return TaintMethodWrapper(self._req.get)(*args, **kw)
1✔
1336

1337
    def has_key(self, *args, **kw):
1✔
1338
        return TaintMethodWrapper(self._req.has_key)(*args, **kw)
×
1339

1340
    def keys(self, *args, **kw):
1✔
1341
        return TaintMethodWrapper(self._req.keys)(*args, **kw)
×
1342

1343

1344
class TaintMethodWrapper:
1✔
1345

1346
    def __init__(self, method):
1✔
1347
        self._method = method
1✔
1348

1349
    def __call__(self, *args, **kw):
1✔
1350
        kw['returnTaints'] = 1
1✔
1351
        return self._method(*args, **kw)
1✔
1352

1353

1354
def has_codec(x):
1✔
1355
    try:
1✔
1356
        codecs.lookup(x)
1✔
1357
    except (LookupError, SystemError):
×
1358
        return 0
×
1359
    else:
1360
        return 1
1✔
1361

1362

1363
def sane_environment(env):
1✔
1364
    # return an environment mapping which has been cleaned of
1365
    # funny business such as REDIRECT_ prefixes added by Apache
1366
    # or HTTP_CGI_AUTHORIZATION hacks.
1367
    dict = {}
1✔
1368
    for key, val in env.items():
1✔
1369
        while key[:9] == 'REDIRECT_':
1!
1370
            key = key[9:]
×
1371
        dict[key] = val
1✔
1372
    if 'HTTP_CGI_AUTHORIZATION' in dict:
1!
1373
        dict['HTTP_AUTHORIZATION'] = dict['HTTP_CGI_AUTHORIZATION']
×
1374
        try:
×
1375
            del dict['HTTP_CGI_AUTHORIZATION']
×
1376
        except Exception:
×
1377
            pass
×
1378
    return dict
1✔
1379

1380

1381
class ValueDescriptor:
1✔
1382
    """(non data) descriptor to compute `value` from `file`."""
1383

1384
    def __get__(self, inst, owner=None):
1✔
1385
        if inst is None:
1!
1386
            return self
×
1387
        file = inst.file
1✔
1388
        try:
1✔
1389
            fpos = file.tell()
1✔
1390
        except Exception:
1✔
1391
            fpos = None
1✔
1392
        try:
1✔
1393
            limit = inst.VALUE_LIMIT
1✔
1394
            if limit:
1!
1395
                v = file.read(limit)
1✔
1396
                if file.read(1):
1✔
1397
                    raise BadRequest("data exceeds memory limit")
1✔
1398
            else:
1399
                v = file.read()
×
1400
            if fpos is None:
1✔
1401
                # store the value as we cannot read it again
1402
                inst.value = v
1✔
1403
            return v
1✔
1404
        finally:
1405
            if fpos is not None:
1✔
1406
                file.seek(fpos)
1✔
1407

1408

1409
class Global:
1✔
1410
    """(non data) descriptor to access a (modul) global attribute."""
1411

1412
    def __init__(self, name):
1✔
1413
        """access global *name*."""
1414
        self.name = name
1✔
1415

1416
    def __get__(self, inst, owner=None):
1✔
1417
        return globals()[self.name]
1✔
1418

1419

1420
class ValueAccessor:
1✔
1421
    VALUE_LIMIT = None
1✔
1422

1423
    value = ValueDescriptor()
1✔
1424

1425

1426
class FormField(SimpleNamespace, ValueAccessor):
1✔
1427
    """represent a single form field.
1428

1429
    Typical attributes:
1430
    name
1431
      the field name
1432
    value
1433
      the field value (`bytes`)
1434
    name_charset, value_charset
1435
      the charset for the name and value, respectively, or ``None``
1436
      if no charset has been specified.
1437

1438
    File fields additionally have the attributes:
1439
    file
1440
      a binary file containing the file content
1441
    filename
1442
      the file's name as reported by the client
1443
    headers
1444
      a case insensitive dict with header information;
1445
      usually `content-type` and `content-disposition`.
1446

1447
    Unless otherwise noted, `latin-1` decoded bytes
1448
    are used to represent textual data.
1449
    """
1450

1451
    name_charset = value_charset = None
1✔
1452

1453

1454
class ZopeFieldStorage(ValueAccessor):
1✔
1455
    VALUE_LIMIT = Global("FORM_MEMORY_LIMIT")
1✔
1456

1457
    def __init__(self, fp, environ):
1✔
1458
        self.file = fp
1✔
1459
        method = environ.get("REQUEST_METHOD", "GET").upper()
1✔
1460
        url_qs = environ.get("QUERY_STRING", "")
1✔
1461
        post_qs = ""
1✔
1462
        hl = []
1✔
1463
        content_type = environ.get("CONTENT_TYPE")
1✔
1464
        if content_type is not None:
1✔
1465
            hl.append(("content-type", content_type))
1✔
1466
        else:
1467
            content_type = ""
1✔
1468
        content_type, options = parse_options_header(content_type)
1✔
1469
        content_type = content_type.lower()
1✔
1470
        content_disposition = environ.get("CONTENT_DISPOSITION")
1✔
1471
        if content_disposition is not None:
1!
1472
            hl.append(("content-disposition", content_disposition))
×
1473
        # Note: ``headers`` does not reflect the complete headers.
1474
        #  Likely, it should get removed altogether and accesses be replaced
1475
        #  by a lookup of the corresponding CGI environment keys.
1476
        self.headers = Headers(hl)
1✔
1477
        parts = ()
1✔
1478
        if method in ("POST", "PUT") \
1✔
1479
           and content_type in (
1480
           "multipart/form-data", "application/x-www-form-urlencoded",
1481
           "application/x-url-encoded",
1482
           # ``Testing`` assumes "application/x-www-form-urlencoded"
1483
           # as default content type
1484
           # We have mapped a missing content type to ``""``.
1485
           "",
1486
           ):
1487
            try:
1✔
1488
                fpos = fp.tell()
1✔
1489
            except Exception:
1✔
1490
                fpos = None
1✔
1491
            if content_type == "multipart/form-data":
1✔
1492
                parts = MultipartParser(
1✔
1493
                    fp, options["boundary"],
1494
                    part_limit=FORM_PART_LIMIT,
1495
                    mem_limit=FORM_MEMORY_LIMIT,
1496
                    disk_limit=FORM_DISK_LIMIT,
1497
                    memfile_limit=FORM_MEMFILE_LIMIT,
1498
                    charset="latin-1").parts()
1499
            else:
1500
                post_qs = fp.read(FORM_MEMORY_LIMIT).decode("latin-1")
1✔
1501
                if fp.read(1):
1✔
1502
                    raise BadRequest("form data processing "
1✔
1503
                                     "requires too much memory")
1504
            if fpos is not None:
1✔
1505
                fp.seek(fpos)
1✔
1506
            else:
1507
                # we cannot read the file again
1508
                self.file = None
1✔
1509
        self.list = fl = []
1✔
1510
        add_field = fl.append
1✔
1511
        post_opts = {}
1✔
1512
        if options.get("charset"):
1✔
1513
            post_opts["name_charset"] = post_opts["value_charset"] = \
1✔
1514
                options["charset"]
1515
        for qs, opts in ((url_qs, {}), (post_qs, post_opts)):
1✔
1516
            for name, val in parse_qsl(
1✔
1517
               qs,  # noqa: E121
1518
               keep_blank_values=True, encoding="latin-1"):
1519
                add_field(FormField(
1✔
1520
                    name=name, value=val.encode("latin-1"), **opts))
1521
        for part in parts:
1✔
1522
            if part.filename is not None:
1✔
1523
                # a file
1524
                field = FormField(
1✔
1525
                    name=part.name,
1526
                    file=part.file,
1527
                    filename=part.filename,
1528
                    headers=part.headers)
1529
            else:
1530
                field = FormField(
1✔
1531
                    name=part.name, value=part.raw,
1532
                    value_charset=_mp_charset(part))
1533
            add_field(field)
1✔
1534

1535

1536
def _mp_charset(part):
1✔
1537
    """the charset of *part*."""
1538
    content_type = part.headers.get("Content-Type", "")
1✔
1539
    _, options = parse_options_header(content_type)
1✔
1540
    return options.get("charset")
1✔
1541

1542

1543
# Original version: zope.publisher.browser.FileUpload
1544
class FileUpload:
1✔
1545
    '''File upload objects
1546

1547
    File upload objects are used to represent file-uploaded data.
1548

1549
    File upload objects can be used just like files.
1550

1551
    In addition, they have a 'headers' attribute that is a dictionary
1552
    containing the file-upload headers, and a 'filename' attribute
1553
    containing the name of the uploaded file.
1554

1555
    Note that file names in HTTP/1.1 use latin-1 as charset.  See
1556
    https://github.com/zopefoundation/Zope/pull/1094#issuecomment-1459654636
1557
    '''
1558

1559
    # Allow access to attributes such as headers and filename so
1560
    # that protected code can use DTML to work with FileUploads.
1561
    __allow_access_to_unprotected_subobjects__ = 1
1✔
1562

1563
    def __init__(self, aFieldStorage, charset=None):
1✔
1564
        charset = charset or default_encoding
1✔
1565
        self.file = aFieldStorage.file
1✔
1566
        self.headers = aFieldStorage.headers
1✔
1567
        # Prevent needless encode-decode when both charsets are the same.
1568
        if charset != "latin-1":
1!
1569
            self.filename = aFieldStorage.filename\
1✔
1570
                .encode("latin-1").decode(charset)
1571
            self.name = aFieldStorage.name.encode("latin-1").decode(charset)
1✔
1572
        else:
1573
            self.filename = aFieldStorage.filename
×
1574
            self.name = aFieldStorage.name
×
1575

1576
        # Add an assertion to the rfc822.Message object that implements
1577
        # self.headers so that managed code can access them.
1578
        try:
1✔
1579
            self.headers.__allow_access_to_unprotected_subobjects__ = 1
1✔
1580
        except Exception:
×
1581
            pass
×
1582

1583
    def __getattribute__(self, key):
1✔
1584
        if key in ('close', 'closed', 'detach', 'fileno', 'flush',
1✔
1585
                   'getbuffer', 'getvalue', 'isatty', 'read', 'read1',
1586
                   'readable', 'readinto', 'readline', 'readlines',
1587
                   'seek', 'seekable', 'tell', 'truncate', 'writable',
1588
                   'write', 'writelines', 'name'):
1589
            file = object.__getattribute__(self, 'file')
1✔
1590
            func = getattr(file, key, _marker)
1✔
1591
            if func is not _marker:
1!
1592
                return func
1✔
1593
        # Always fall back to looking things up on self
1594
        return object.__getattribute__(self, key)
1✔
1595

1596
    def __iter__(self):
1✔
1597
        return self.file.__iter__()
1✔
1598

1599
    def __bool__(self):
1✔
1600
        """FileUpload objects are considered false if their
1601
           filename is empty.
1602
        """
1603
        return bool(self.filename)
1✔
1604

1605
    def __next__(self):
1✔
1606
        return self.file.__next__()
1✔
1607

1608

1609
QPARMRE = re.compile(
1✔
1610
    '([\x00- ]*([^\x00- ;,="]+)="([^"]*)"([\x00- ]*[;,])?[\x00- ]*)')
1611
PARMRE = re.compile(
1✔
1612
    '([\x00- ]*([^\x00- ;,="]+)=([^;]*)([\x00- ]*[;,])?[\x00- ]*)')
1613
PARAMLESSRE = re.compile(
1✔
1614
    '([\x00- ]*([^\x00- ;,="]+)[\x00- ]*[;,][\x00- ]*)')
1615

1616

1617
def parse_cookie(text,
1✔
1618
                 result=None,
1619
                 qparmre=QPARMRE,
1620
                 parmre=PARMRE,
1621
                 paramlessre=PARAMLESSRE,
1622
                 ):
1623

1624
    if result is None:
1!
1625
        result = {}
×
1626

1627
    mo_q = qparmre.match(text)
1✔
1628

1629
    if mo_q:
1✔
1630
        # Match quoted correct cookies
1631
        c_len = len(mo_q.group(1))
1✔
1632
        name = mo_q.group(2)
1✔
1633
        value = mo_q.group(3)
1✔
1634

1635
    else:
1636
        # Match evil MSIE cookies ;)
1637
        mo_p = parmre.match(text)
1✔
1638

1639
        if mo_p:
1✔
1640
            c_len = len(mo_p.group(1))
1✔
1641
            name = mo_p.group(2)
1✔
1642
            value = mo_p.group(3)
1✔
1643
        else:
1644
            # Broken Cookie without = nor value.
1645
            broken_p = paramlessre.match(text)
1✔
1646
            if broken_p:
1✔
1647
                c_len = len(broken_p.group(1))
1✔
1648
                name = broken_p.group(2)
1✔
1649
                value = ''
1✔
1650
            else:
1651
                return result
1✔
1652

1653
    if name not in result:
1!
1654
        result[name] = getCookieValuePolicy().load(name, value)
1✔
1655

1656
    return parse_cookie(text[c_len:], result)
1✔
1657

1658

1659
class record:
1✔
1660

1661
    # Allow access to record methods and values from DTML
1662
    __allow_access_to_unprotected_subobjects__ = 1
1✔
1663
    _guarded_writes = 1
1✔
1664

1665
    def __getattr__(self, key, default=None):
1✔
1666
        if key in ('get',
1✔
1667
                   'keys',
1668
                   'items',
1669
                   'values',
1670
                   'copy'):
1671
            return getattr(self.__dict__, key)
1✔
1672
        raise AttributeError(key)
1✔
1673

1674
    def __contains__(self, key):
1✔
1675
        return key in self.__dict__
1✔
1676

1677
    def __getitem__(self, key):
1✔
1678
        return self.__dict__[key]
1✔
1679

1680
    def __iter__(self):
1✔
1681
        return iter(self.__dict__)
1✔
1682

1683
    def __len__(self):
1✔
1684
        return len(self.__dict__)
1✔
1685

1686
    def __str__(self):
1✔
1687
        return ", ".join("%s: %s" % item for item in
1✔
1688
                         sorted(self.__dict__.items()))
1689

1690
    def __repr__(self):
1✔
1691
        # return repr( self.__dict__ )
1692
        return '{%s}' % ', '.join(
1✔
1693
            f"'{key}': {value!r}"
1694
            for key, value in sorted(self.__dict__.items()))
1695

1696
    def __eq__(self, other):
1✔
1697
        if not isinstance(other, record):
1!
1698
            return False
×
1699
        return sorted(self.__dict__.items()) == sorted(other.__dict__.items())
1✔
1700

1701

1702
def _filterPasswordFields(items):
1✔
1703
    # Collector #777:  filter out request fields which contain 'passw'
1704

1705
    result = []
1✔
1706

1707
    for k, v in items:
1✔
1708

1709
        if 'passw' in k.lower():
1✔
1710
            v = '<password obscured>'
1✔
1711

1712
        result.append((k, v))
1✔
1713

1714
    return result
1✔
1715

1716

1717
def use_builtin_xmlrpc(request):
1✔
1718
    checker = queryUtility(IXmlrpcChecker)
1✔
1719
    return checker is None or checker(request)
1✔
1720

1721

1722
def taint(d):
1✔
1723
    """return as ``dict`` the items from *d* which require tainting.
1724

1725
    *d* must be a ``dict`` with ``str`` keys and values recursively
1726
    build from elementary values, ``list``, ``tuple``, ``record`` and
1727
    ``dict``.
1728
    """
1729
    def should_taint(v):
1✔
1730
        """check whether *v* needs tainting."""
1731
        if isinstance(v, (list, tuple)):
1✔
1732
            return any(should_taint(x) for x in v)
1✔
1733
        if isinstance(v, (record, dict)):
1✔
1734
            for k in v:
1✔
1735
                if should_taint(k):
1!
1736
                    return True
×
1737
                if should_taint(v[k]):
1✔
1738
                    return True
1✔
1739
        return should_be_tainted(v)
1✔
1740

1741
    def _taint(v):
1✔
1742
        """return a copy of *v* with tainted replacements.
1743

1744
        In the copy, each ``str`` which requires tainting
1745
        is replaced by the corresponding ``taint_string``.
1746
        """
1747
        __traceback_info__ = v
1✔
1748
        if isinstance(v, (bytes, str)) and should_be_tainted(v):
1✔
1749
            return taint_string(v)
1✔
1750
        if isinstance(v, (list, tuple)):
1✔
1751
            return v.__class__(_taint(x) for x in v)
1✔
1752
        if isinstance(v, record):
1✔
1753
            rn = record()
1✔
1754
            for k in v:
1✔
1755
                __traceback_info__ = v, k
1✔
1756
                if should_be_tainted(k):
1!
1757
                    raise ValueError("Cannot taint `record` attribute names")
×
1758
                setattr(rn, k, _taint(v[k]))
1✔
1759
            return rn
1✔
1760
        if isinstance(v, dict):
1!
1761
            return v.__class__((_taint(k), _taint(v[k])) for k in v)
×
1762
        return v
1✔
1763
    td = {}
1✔
1764
    for k in d:
1✔
1765
        v = d[k]
1✔
1766
        if should_taint(k) or should_taint(v):
1✔
1767
            td[_taint(k)] = _taint(v)
1✔
1768
    return td
1✔
1769

1770

1771
def should_be_tainted(v):
1✔
1772
    """Work around ``AccessControl`` weakness."""
1773
    if isinstance(v, FileUpload):
1✔
1774
        # `base_should_be_tainted` would read `v` as a side effect
1775
        return False
1✔
1776
    try:
1✔
1777
        return base_should_be_tainted(v)
1✔
1778
    except Exception:
1✔
1779
        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

© 2026 Coveralls, Inc