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

zopefoundation / zope.testbrowser / 16098871825

05 Mar 2025 04:01PM UTC coverage: 91.403%. Remained the same
16098871825

push

github

icemac
Back to development: 7.1

427 of 520 branches covered (82.12%)

Branch coverage included in aggregate %.

1859 of 1981 relevant lines covered (93.84%)

0.94 hits per line

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

92.05
/src/zope/testbrowser/cookies.py
1
##############################################################################
2
#
3
# Copyright (c) 2008 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

15
import datetime
1✔
16
import http.cookies
1✔
17
import time
1✔
18
import urllib.parse
1✔
19
import urllib.request
1✔
20
from collections.abc import MutableMapping
1✔
21
from urllib.parse import quote as url_quote
1✔
22

23
import pytz
1✔
24

25
import zope.interface
1✔
26

27
from zope.testbrowser import interfaces
1✔
28
from zope.testbrowser import utils
1✔
29

30

31
# Cookies class helpers
32

33

34
class BrowserStateError(Exception):
1✔
35
    pass
1✔
36

37

38
class _StubHTTPMessage:
1✔
39
    def __init__(self, cookies):
1✔
40
        self._cookies = cookies
1✔
41

42
    def getheaders(self, name, default=[]):
1✔
43
        if name.lower() != 'set-cookie':
1✔
44
            return default
1✔
45
        else:
46
            return self._cookies
1✔
47

48
    get_all = getheaders
1✔
49

50

51
class _StubResponse:
1✔
52
    def __init__(self, cookies):
1✔
53
        self.message = _StubHTTPMessage(cookies)
1✔
54

55
    def info(self):
1✔
56
        return self.message
1✔
57

58

59
DAY_OF_WEEK = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
1✔
60

61
MONTH = [
1✔
62
    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
63
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
64
]
65

66

67
def expiration_string(expires):  # this is not protected so usable in tests.
1✔
68
    if isinstance(expires, datetime.datetime):
1✔
69
        if expires.tzinfo is not None:
1✔
70
            expires = expires.astimezone(pytz.UTC)
1✔
71
        expires = expires.strftime(
1✔
72
            '{dow}, %d {mon} %Y %H:%M:%S GMT'.format(
73
                dow=DAY_OF_WEEK[expires.weekday()],
74
                mon=MONTH[expires.month - 1],
75
            )
76
        )
77
    return expires
1✔
78

79
# end Cookies class helpers
80

81

82
@zope.interface.implementer(interfaces.ICookies)
1✔
83
class Cookies(MutableMapping):
1✔
84
    """Cookies for testbrowser.
85
    """
86

87
    def __init__(self, testapp, url=None, req_headers=None):
1✔
88
        self.testapp = testapp
1✔
89
        self._url = url
1✔
90
        self._jar = testapp.cookiejar
1✔
91
        self._req_headers = req_headers if req_headers is not None else {}
1✔
92

93
    @property
1✔
94
    def strict_domain_policy(self):
1✔
95
        policy = self._jar._policy
1✔
96
        flags = (policy.DomainStrictNoDots | policy.DomainRFC2965Match |
1✔
97
                 policy.DomainStrictNonDomain)
98
        return policy.strict_ns_domain & flags == flags
1✔
99

100
    @strict_domain_policy.setter
1✔
101
    def strict_domain_policy(self, value):
1✔
102
        jar = self._jar
1✔
103
        policy = jar._policy
1✔
104
        flags = (policy.DomainStrictNoDots | policy.DomainRFC2965Match |
1✔
105
                 policy.DomainStrictNonDomain)
106
        policy.strict_ns_domain |= flags
1✔
107
        if not value:
1✔
108
            policy.strict_ns_domain ^= flags
1✔
109

110
    def forURL(self, url):
1✔
111
        return self.__class__(self.testapp, url)
1✔
112

113
    @property
1✔
114
    def url(self):
1✔
115
        return self._url
1✔
116

117
    @property
1✔
118
    def _request(self):
1✔
119
        return urllib.request.Request(self._url)
1✔
120

121
    @property
1✔
122
    def header(self):
1✔
123
        request = self._request
1✔
124
        self._jar.add_cookie_header(request)
1✔
125

126
        if not request.has_header('Cookie'):
1✔
127
            return ''
1✔
128

129
        hdr = request.get_header('Cookie')
1✔
130
        # We need a predictable order of cookies for tests, so we reparse and
131
        # sort the header here.
132
        return '; '.join(sorted(hdr.split('; ')))
1✔
133

134
    def __str__(self):
1✔
135
        return self.header
1✔
136

137
    def __repr__(self):
1✔
138
        # get the cookies for the current url
139
        return '<{}.{} object at {!r} for {} ({})>'.format(
1✔
140
            self.__class__.__module__, self.__class__.__name__,
141
            id(self), self.url, self.header)
142

143
    def _raw_cookies(self):
1✔
144
        # We are using protected cookielib _cookies_for_request() here to avoid
145
        # code duplication.
146

147
        # Comply with cookielib internal protocol
148
        self._jar._policy._now = self._jar._now = int(time.time())
1✔
149
        cookies = self._jar._cookies_for_request(self._request)
1✔
150

151
        # Sort cookies so that the longer paths would come first. This allows
152
        # masking parent cookies.
153
        cookies.sort(key=lambda c: (len(c.path), len(c.domain)), reverse=True)
1✔
154
        return cookies
1✔
155

156
    def _get_cookies(self, key=None):
1✔
157
        if key is None:
1✔
158
            seen = set()
1✔
159
            for ck in self._raw_cookies():
1✔
160
                if ck.name not in seen:
1✔
161
                    yield ck
1✔
162
                    seen.add(ck.name)
1✔
163
        else:
164
            for ck in self._raw_cookies():
1✔
165
                if ck.name == key:
1✔
166
                    yield ck
1✔
167

168
    _marker = object()
1✔
169

170
    def _get(self, key, default=_marker):
1✔
171
        for ck in self._raw_cookies():
1✔
172
            if ck.name == key:
1✔
173
                return ck
1✔
174
        if default is self._marker:
1✔
175
            raise KeyError(key)
1✔
176
        return default
1✔
177

178
    def __getitem__(self, key):
1✔
179
        return self._get(key).value
1✔
180

181
    def getinfo(self, key):
1✔
182
        return self._getinfo(self._get(key))
1✔
183

184
    def _getinfo(self, ck):
1✔
185
        res = {'name': ck.name,
1✔
186
               'value': ck.value,
187
               'port': ck.port,
188
               'domain': ck.domain,
189
               'path': ck.path,
190
               'secure': ck.secure,
191
               'expires': None,
192
               'comment': ck.comment,
193
               'commenturl': ck.comment_url}
194
        if ck.expires is not None:
1✔
195
            res['expires'] = datetime.datetime.fromtimestamp(
1✔
196
                ck.expires, pytz.UTC)
197
        return res
1✔
198

199
    def keys(self):
1✔
200
        return [ck.name for ck in self._get_cookies()]
1✔
201

202
    def __iter__(self):
1✔
203
        return (ck.name for ck in self._get_cookies())
1✔
204

205
    def iterinfo(self, key=None):
1✔
206
        return (self._getinfo(ck) for ck in self._get_cookies(key))
1✔
207

208
    def iteritems(self):
1✔
209
        return ((ck.name, ck.value) for ck in self._get_cookies())
×
210

211
    def has_key(self, key):
1✔
212
        return self._get(key, None) is not None
1✔
213

214
    __contains__ = has_key
1✔
215

216
    def __len__(self):
1✔
217
        return len(list(self._get_cookies()))
1✔
218

219
    def __delitem__(self, key):
1✔
220
        ck = self._get(key)
1✔
221
        self._jar.clear(ck.domain, ck.path, ck.name)
1✔
222

223
    def create(self, name, value,
1✔
224
               domain=None, expires=None, path=None, secure=None, comment=None,
225
               commenturl=None, port=None):
226
        if value is None:
1!
227
            raise ValueError('must provide value')
×
228
        ck = self._get(name, None)
1✔
229
        if (ck is not None and
1!
230
            (path is None or ck.path == path) and
231
            (domain is None or ck.domain == domain or
232
             ck.domain == domain) and (port is None or ck.port == port)):
233
            # cookie already exists
234
            raise ValueError('cookie already exists')
×
235
        if domain is not None:
1✔
236
            self._verifyDomain(domain, ck)
1✔
237
        if path is not None:
1✔
238
            self._verifyPath(path, ck)
1✔
239
        now = int(time.time())
1✔
240
        if expires is not None and self._is_expired(expires, now):
1✔
241
            raise zope.testbrowser.interfaces.AlreadyExpiredError(
1✔
242
                'May not create a cookie that is immediately expired')
243
        self._setCookie(name, value, domain, expires, path, secure, comment,
1✔
244
                        commenturl, port, now=now)
245

246
    def change(self, name, value=None, domain=None, expires=None, path=None,
1✔
247
               secure=None, comment=None, commenturl=None, port=None):
248
        now = int(time.time())
1✔
249
        if expires is not None and self._is_expired(expires, now):
1✔
250
            # shortcut
251
            del self[name]
1✔
252
        else:
253
            self._change(self._get(name), value, domain, expires, path, secure,
1✔
254
                         comment, commenturl, port, now)
255

256
    def _change(self, ck, value=None,
1✔
257
                domain=None, expires=None, path=None, secure=None,
258
                comment=None, commenturl=None, port=None, now=None):
259
        if value is None:
1✔
260
            value = ck.value
1✔
261
        if domain is None:
1✔
262
            domain = ck.domain
1✔
263
        else:
264
            self._verifyDomain(domain, None)
1✔
265
        if expires is None:
1✔
266
            expires = ck.expires
1✔
267
        if path is None:
1!
268
            path = ck.path
1✔
269
        else:
270
            self._verifyPath(domain, None)
×
271
        if secure is None:
1!
272
            secure = ck.secure
1✔
273
        if comment is None:
1!
274
            comment = ck.comment
1✔
275
        if commenturl is None:
1!
276
            commenturl = ck.comment_url
1✔
277
        if port is None:
1!
278
            port = ck.port
1✔
279
        self._setCookie(ck.name, value, domain, expires, path, secure, comment,
1✔
280
                        commenturl, port, ck.version, ck=ck, now=now)
281

282
    def _verifyDomain(self, domain, ck):
1✔
283
        tmp_domain = domain
1✔
284
        if domain is not None and domain.startswith('.'):
1✔
285
            tmp_domain = domain[1:]
1✔
286
        self_host = utils.effective_request_host(self._request)
1✔
287
        if (self_host != tmp_domain and
1✔
288
                not self_host.endswith('.' + tmp_domain)):
289
            raise ValueError('current url must match given domain')
1✔
290
        if (ck is not None and ck.domain != tmp_domain and
1✔
291
                ck.domain.endswith(tmp_domain)):
292
            raise ValueError(
1✔
293
                'cannot set a cookie that will be hidden by another '
294
                'cookie for this url (%s)' % (self.url,))
295

296
    def _verifyPath(self, path, ck):
1✔
297
        self_path = urllib.parse.urlparse(self.url)[2]
1✔
298
        if not self_path.startswith(path):
1!
299
            raise ValueError('current url must start with path, if given')
×
300
        if ck is not None and ck.path != path and ck.path.startswith(path):
1✔
301
            raise ValueError(
1✔
302
                'cannot set a cookie that will be hidden by another '
303
                'cookie for this url (%s)' % (self.url,))
304

305
    def _setCookie(self, name, value, domain, expires, path, secure, comment,
1✔
306
                   commenturl, port, version=None, ck=None, now=None):
307
        for nm, val in self._req_headers.items():
1✔
308
            if nm.lower() in ('cookie', 'cookie2'):
1✔
309
                raise ValueError('cookies are already set in `Cookie` header')
1✔
310

311
        if domain and not domain.startswith('.'):
1✔
312
            # we do a dance here so that we keep names that have been passed
313
            # in consistent (i.e., if we get an explicit 'example.com' it stays
314
            # 'example.com', rather than converting to '.example.com').
315
            tmp_domain = domain
1✔
316
            domain = None
1✔
317
            if secure:
1✔
318
                protocol = 'https'
1✔
319
            else:
320
                protocol = 'http'
1✔
321
            url = '{}://{}{}'.format(protocol, tmp_domain, path or '/')
1✔
322
            request = urllib.request.Request(url)
1✔
323
        else:
324
            request = self._request
1✔
325
            if request is None:
1!
326
                # TODO: fix exception
327
                raise BrowserStateError(
×
328
                    'cannot create cookie without request or domain')
329
        c = http.cookies.SimpleCookie()
1✔
330
        name = str(name)
1✔
331
        # Cookie value must be native string
332
        c[name] = value
1✔
333
        if secure:
1✔
334
            c[name]['secure'] = True
1✔
335
        if domain:
1✔
336
            c[name]['domain'] = domain
1✔
337
        if path:
1✔
338
            c[name]['path'] = path
1✔
339
        if expires:
1✔
340
            c[name]['expires'] = expiration_string(expires)
1✔
341
        if comment:
1✔
342
            c[name]['comment'] = url_quote(
1✔
343
                comment.encode('utf-8'), safe="/?:@&+")
344
        if port:
1!
345
            c[name]['port'] = port
×
346
        if commenturl:
1!
347
            c[name]['commenturl'] = commenturl
×
348
        if version:
1!
349
            c[name]['version'] = version
×
350
        # this use of objects like _StubResponse and _StubHTTPMessage is in
351
        # fact supported by the documented client cookie API.
352
        cookies = self._jar.make_cookies(
1✔
353
            _StubResponse([c.output(header='').strip()]), request)
354
        assert len(cookies) == 1, (
1✔
355
            'programmer error: %d cookies made' % (len(cookies),))
356
        policy = self._jar._policy
1✔
357
        if now is None:
1✔
358
            now = int(time.time())
1✔
359
        policy._now = self._jar._now = now
1✔
360
        if not policy.set_ok(cookies[0], request):
1✔
361
            raise ValueError('policy does not allow this cookie')
1✔
362
        if ck is not None:
1✔
363
            self._jar.clear(ck.domain, ck.path, ck.name)
1✔
364
        self._jar.set_cookie(cookies[0])
1✔
365

366
    def __setitem__(self, key, value):
1✔
367
        ck = self._get(key, None)
1✔
368
        if ck is None:
1✔
369
            self.create(key, value)
1✔
370
        else:
371
            self._change(ck, value)
1✔
372

373
    def _is_expired(self, value, now):  # now = int(time.time())
1✔
374
        dnow = datetime.datetime.fromtimestamp(now, pytz.UTC)
1✔
375
        if isinstance(value, datetime.datetime):
1✔
376
            if value.tzinfo is None:
1✔
377
                if value <= dnow.replace(tzinfo=None):
1✔
378
                    return True
1✔
379
            elif value <= dnow:
1!
380
                return True
×
381
        elif isinstance(value, str):
1!
382
            if datetime.datetime.fromtimestamp(
1!
383
                    utils.http2time(value), pytz.UTC) <= dnow:
384
                return True
×
385
        return False
1✔
386

387
    def clear(self):
1✔
388
        # to give expected mapping behavior of resulting in an empty dict,
389
        # we use _raw_cookies rather than _get_cookies.
390
        for ck in self._raw_cookies():
1✔
391
            self._jar.clear(ck.domain, ck.path, ck.name)
1✔
392

393
    def clearAllSession(self):
1✔
394
        self._jar.clear_session_cookies()
1✔
395

396
    def clearAll(self):
1✔
397
        self._jar.clear()
1✔
398

399
    def pop(self, k, *args):
1✔
400
        """See zope.interface.common.mapping.IExtendedWriteMapping
401
        """
402
        # Python3' MutableMapping doesn't offer pop() with variable arguments,
403
        # so we are reimplementing it here as defined in IExtendedWriteMapping
404
        super().pop(k, *args)
×
405

406
    def itervalues(self):
1✔
407
        # Method, missing in Py3' MutableMapping, but required by
408
        # IIterableMapping
409
        return self.values()
×
410

411
    def iterkeys(self):
1✔
412
        # Method, missing in Py3' MutableMapping, but required by
413
        # IIterableMapping
414
        return self.keys()
×
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