1# -*- coding: utf-8 -*-
2
3"""
4requests.cookies
5~~~~~~~~~~~~~~~~
6
7Compatibility code to be able to use `cookielib.CookieJar` with requests.
8
9requests.utils imports from here, so be careful with imports.
10"""
11
12import copy
13import time
14import calendar
15
16from ._internal_utils import to_native_string
17from .compat import cookielib, urlparse, urlunparse, Morsel, MutableMapping
18
19try:
20    import threading
21except ImportError:
22    import dummy_threading as threading
23
24
25class MockRequest(object):
26    """Wraps a `requests.Request` to mimic a `urllib2.Request`.
27
28    The code in `cookielib.CookieJar` expects this interface in order to correctly
29    manage cookie policies, i.e., determine whether a cookie can be set, given the
30    domains of the request and the cookie.
31
32    The original request object is read-only. The client is responsible for collecting
33    the new headers via `get_new_headers()` and interpreting them appropriately. You
34    probably want `get_cookie_header`, defined below.
35    """
36
37    def __init__(self, request):
38        self._r = request
39        self._new_headers = {}
40        self.type = urlparse(self._r.url).scheme
41
42    def get_type(self):
43        return self.type
44
45    def get_host(self):
46        return urlparse(self._r.url).netloc
47
48    def get_origin_req_host(self):
49        return self.get_host()
50
51    def get_full_url(self):
52        # Only return the response's URL if the user hadn't set the Host
53        # header
54        if not self._r.headers.get('Host'):
55            return self._r.url
56        # If they did set it, retrieve it and reconstruct the expected domain
57        host = to_native_string(self._r.headers['Host'], encoding='utf-8')
58        parsed = urlparse(self._r.url)
59        # Reconstruct the URL as we expect it
60        return urlunparse([
61            parsed.scheme, host, parsed.path, parsed.params, parsed.query,
62            parsed.fragment
63        ])
64
65    def is_unverifiable(self):
66        return True
67
68    def has_header(self, name):
69        return name in self._r.headers or name in self._new_headers
70
71    def get_header(self, name, default=None):
72        return self._r.headers.get(name, self._new_headers.get(name, default))
73
74    def add_header(self, key, val):
75        """cookielib has no legitimate use for this method; add it back if you find one."""
76        raise NotImplementedError("Cookie headers should be added with add_unredirected_header()")
77
78    def add_unredirected_header(self, name, value):
79        self._new_headers[name] = value
80
81    def get_new_headers(self):
82        return self._new_headers
83
84    @property
85    def unverifiable(self):
86        return self.is_unverifiable()
87
88    @property
89    def origin_req_host(self):
90        return self.get_origin_req_host()
91
92    @property
93    def host(self):
94        return self.get_host()
95
96
97class MockResponse(object):
98    """Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.
99
100    ...what? Basically, expose the parsed HTTP headers from the server response
101    the way `cookielib` expects to see them.
102    """
103
104    def __init__(self, headers):
105        """Make a MockResponse for `cookielib` to read.
106
107        :param headers: a httplib.HTTPMessage or analogous carrying the headers
108        """
109        self._headers = headers
110
111    def info(self):
112        return self._headers
113
114    def getheaders(self, name):
115        self._headers.getheaders(name)
116
117
118def extract_cookies_to_jar(jar, request, response):
119    """Extract the cookies from the response into a CookieJar.
120
121    :param jar: cookielib.CookieJar (not necessarily a RequestsCookieJar)
122    :param request: our own requests.Request object
123    :param response: urllib3.HTTPResponse object
124    """
125    if not (hasattr(response, '_original_response') and
126            response._original_response):
127        return
128    # the _original_response field is the wrapped httplib.HTTPResponse object,
129    req = MockRequest(request)
130    # pull out the HTTPMessage with the headers and put it in the mock:
131    res = MockResponse(response._original_response.msg)
132    jar.extract_cookies(res, req)
133
134
135def get_cookie_header(jar, request):
136    """
137    Produce an appropriate Cookie header string to be sent with `request`, or None.
138
139    :rtype: str
140    """
141    r = MockRequest(request)
142    jar.add_cookie_header(r)
143    return r.get_new_headers().get('Cookie')
144
145
146def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
147    """Unsets a cookie by name, by default over all domains and paths.
148
149    Wraps CookieJar.clear(), is O(n).
150    """
151    clearables = []
152    for cookie in cookiejar:
153        if cookie.name != name:
154            continue
155        if domain is not None and domain != cookie.domain:
156            continue
157        if path is not None and path != cookie.path:
158            continue
159        clearables.append((cookie.domain, cookie.path, cookie.name))
160
161    for domain, path, name in clearables:
162        cookiejar.clear(domain, path, name)
163
164
165class CookieConflictError(RuntimeError):
166    """There are two cookies that meet the criteria specified in the cookie jar.
167    Use .get and .set and include domain and path args in order to be more specific.
168    """
169
170
171class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
172    """Compatibility class; is a cookielib.CookieJar, but exposes a dict
173    interface.
174
175    This is the CookieJar we create by default for requests and sessions that
176    don't specify one, since some clients may expect response.cookies and
177    session.cookies to support dict operations.
178
179    Requests does not use the dict interface internally; it's just for
180    compatibility with external client code. All requests code should work
181    out of the box with externally provided instances of ``CookieJar``, e.g.
182    ``LWPCookieJar`` and ``FileCookieJar``.
183
184    Unlike a regular CookieJar, this class is pickleable.
185
186    .. warning:: dictionary operations that are normally O(1) may be O(n).
187    """
188
189    def get(self, name, default=None, domain=None, path=None):
190        """Dict-like get() that also supports optional domain and path args in
191        order to resolve naming collisions from using one cookie jar over
192        multiple domains.
193
194        .. warning:: operation is O(n), not O(1).
195        """
196        try:
197            return self._find_no_duplicates(name, domain, path)
198        except KeyError:
199            return default
200
201    def set(self, name, value, **kwargs):
202        """Dict-like set() that also supports optional domain and path args in
203        order to resolve naming collisions from using one cookie jar over
204        multiple domains.
205        """
206        # support client code that unsets cookies by assignment of a None value:
207        if value is None:
208            remove_cookie_by_name(self, name, domain=kwargs.get('domain'), path=kwargs.get('path'))
209            return
210
211        if isinstance(value, Morsel):
212            c = morsel_to_cookie(value)
213        else:
214            c = create_cookie(name, value, **kwargs)
215        self.set_cookie(c)
216        return c
217
218    def iterkeys(self):
219        """Dict-like iterkeys() that returns an iterator of names of cookies
220        from the jar.
221
222        .. seealso:: itervalues() and iteritems().
223        """
224        for cookie in iter(self):
225            yield cookie.name
226
227    def keys(self):
228        """Dict-like keys() that returns a list of names of cookies from the
229        jar.
230
231        .. seealso:: values() and items().
232        """
233        return list(self.iterkeys())
234
235    def itervalues(self):
236        """Dict-like itervalues() that returns an iterator of values of cookies
237        from the jar.
238
239        .. seealso:: iterkeys() and iteritems().
240        """
241        for cookie in iter(self):
242            yield cookie.value
243
244    def values(self):
245        """Dict-like values() that returns a list of values of cookies from the
246        jar.
247
248        .. seealso:: keys() and items().
249        """
250        return list(self.itervalues())
251
252    def iteritems(self):
253        """Dict-like iteritems() that returns an iterator of name-value tuples
254        from the jar.
255
256        .. seealso:: iterkeys() and itervalues().
257        """
258        for cookie in iter(self):
259            yield cookie.name, cookie.value
260
261    def items(self):
262        """Dict-like items() that returns a list of name-value tuples from the
263        jar. Allows client-code to call ``dict(RequestsCookieJar)`` and get a
264        vanilla python dict of key value pairs.
265
266        .. seealso:: keys() and values().
267        """
268        return list(self.iteritems())
269
270    def list_domains(self):
271        """Utility method to list all the domains in the jar."""
272        domains = []
273        for cookie in iter(self):
274            if cookie.domain not in domains:
275                domains.append(cookie.domain)
276        return domains
277
278    def list_paths(self):
279        """Utility method to list all the paths in the jar."""
280        paths = []
281        for cookie in iter(self):
282            if cookie.path not in paths:
283                paths.append(cookie.path)
284        return paths
285
286    def multiple_domains(self):
287        """Returns True if there are multiple domains in the jar.
288        Returns False otherwise.
289
290        :rtype: bool
291        """
292        domains = []
293        for cookie in iter(self):
294            if cookie.domain is not None and cookie.domain in domains:
295                return True
296            domains.append(cookie.domain)
297        return False  # there is only one domain in jar
298
299    def get_dict(self, domain=None, path=None):
300        """Takes as an argument an optional domain and path and returns a plain
301        old Python dict of name-value pairs of cookies that meet the
302        requirements.
303
304        :rtype: dict
305        """
306        dictionary = {}
307        for cookie in iter(self):
308            if (
309                (domain is None or cookie.domain == domain) and
310                (path is None or cookie.path == path)
311            ):
312                dictionary[cookie.name] = cookie.value
313        return dictionary
314
315    def __contains__(self, name):
316        try:
317            return super(RequestsCookieJar, self).__contains__(name)
318        except CookieConflictError:
319            return True
320
321    def __getitem__(self, name):
322        """Dict-like __getitem__() for compatibility with client code. Throws
323        exception if there are more than one cookie with name. In that case,
324        use the more explicit get() method instead.
325
326        .. warning:: operation is O(n), not O(1).
327        """
328        return self._find_no_duplicates(name)
329
330    def __setitem__(self, name, value):
331        """Dict-like __setitem__ for compatibility with client code. Throws
332        exception if there is already a cookie of that name in the jar. In that
333        case, use the more explicit set() method instead.
334        """
335        self.set(name, value)
336
337    def __delitem__(self, name):
338        """Deletes a cookie given a name. Wraps ``cookielib.CookieJar``'s
339        ``remove_cookie_by_name()``.
340        """
341        remove_cookie_by_name(self, name)
342
343    def set_cookie(self, cookie, *args, **kwargs):
344        if hasattr(cookie.value, 'startswith') and cookie.value.startswith('"') and cookie.value.endswith('"'):
345            cookie.value = cookie.value.replace('\\"', '')
346        return super(RequestsCookieJar, self).set_cookie(cookie, *args, **kwargs)
347
348    def update(self, other):
349        """Updates this jar with cookies from another CookieJar or dict-like"""
350        if isinstance(other, cookielib.CookieJar):
351            for cookie in other:
352                self.set_cookie(copy.copy(cookie))
353        else:
354            super(RequestsCookieJar, self).update(other)
355
356    def _find(self, name, domain=None, path=None):
357        """Requests uses this method internally to get cookie values.
358
359        If there are conflicting cookies, _find arbitrarily chooses one.
360        See _find_no_duplicates if you want an exception thrown if there are
361        conflicting cookies.
362
363        :param name: a string containing name of cookie
364        :param domain: (optional) string containing domain of cookie
365        :param path: (optional) string containing path of cookie
366        :return: cookie.value
367        """
368        for cookie in iter(self):
369            if cookie.name == name:
370                if domain is None or cookie.domain == domain:
371                    if path is None or cookie.path == path:
372                        return cookie.value
373
374        raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))
375
376    def _find_no_duplicates(self, name, domain=None, path=None):
377        """Both ``__get_item__`` and ``get`` call this function: it's never
378        used elsewhere in Requests.
379
380        :param name: a string containing name of cookie
381        :param domain: (optional) string containing domain of cookie
382        :param path: (optional) string containing path of cookie
383        :raises KeyError: if cookie is not found
384        :raises CookieConflictError: if there are multiple cookies
385            that match name and optionally domain and path
386        :return: cookie.value
387        """
388        toReturn = None
389        for cookie in iter(self):
390            if cookie.name == name:
391                if domain is None or cookie.domain == domain:
392                    if path is None or cookie.path == path:
393                        if toReturn is not None:  # if there are multiple cookies that meet passed in criteria
394                            raise CookieConflictError('There are multiple cookies with name, %r' % (name))
395                        toReturn = cookie.value  # we will eventually return this as long as no cookie conflict
396
397        if toReturn:
398            return toReturn
399        raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))
400
401    def __getstate__(self):
402        """Unlike a normal CookieJar, this class is pickleable."""
403        state = self.__dict__.copy()
404        # remove the unpickleable RLock object
405        state.pop('_cookies_lock')
406        return state
407
408    def __setstate__(self, state):
409        """Unlike a normal CookieJar, this class is pickleable."""
410        self.__dict__.update(state)
411        if '_cookies_lock' not in self.__dict__:
412            self._cookies_lock = threading.RLock()
413
414    def copy(self):
415        """Return a copy of this RequestsCookieJar."""
416        new_cj = RequestsCookieJar()
417        new_cj.set_policy(self.get_policy())
418        new_cj.update(self)
419        return new_cj
420
421    def get_policy(self):
422        """Return the CookiePolicy instance used."""
423        return self._policy
424
425
426def _copy_cookie_jar(jar):
427    if jar is None:
428        return None
429
430    if hasattr(jar, 'copy'):
431        # We're dealing with an instance of RequestsCookieJar
432        return jar.copy()
433    # We're dealing with a generic CookieJar instance
434    new_jar = copy.copy(jar)
435    new_jar.clear()
436    for cookie in jar:
437        new_jar.set_cookie(copy.copy(cookie))
438    return new_jar
439
440
441def create_cookie(name, value, **kwargs):
442    """Make a cookie from underspecified parameters.
443
444    By default, the pair of `name` and `value` will be set for the domain ''
445    and sent on every request (this is sometimes called a "supercookie").
446    """
447    result = {
448        'version': 0,
449        'name': name,
450        'value': value,
451        'port': None,
452        'domain': '',
453        'path': '/',
454        'secure': False,
455        'expires': None,
456        'discard': True,
457        'comment': None,
458        'comment_url': None,
459        'rest': {'HttpOnly': None},
460        'rfc2109': False,
461    }
462
463    badargs = set(kwargs) - set(result)
464    if badargs:
465        err = 'create_cookie() got unexpected keyword arguments: %s'
466        raise TypeError(err % list(badargs))
467
468    result.update(kwargs)
469    result['port_specified'] = bool(result['port'])
470    result['domain_specified'] = bool(result['domain'])
471    result['domain_initial_dot'] = result['domain'].startswith('.')
472    result['path_specified'] = bool(result['path'])
473
474    return cookielib.Cookie(**result)
475
476
477def morsel_to_cookie(morsel):
478    """Convert a Morsel object into a Cookie containing the one k/v pair."""
479
480    expires = None
481    if morsel['max-age']:
482        try:
483            expires = int(time.time() + int(morsel['max-age']))
484        except ValueError:
485            raise TypeError('max-age: %s must be integer' % morsel['max-age'])
486    elif morsel['expires']:
487        time_template = '%a, %d-%b-%Y %H:%M:%S GMT'
488        expires = calendar.timegm(
489            time.strptime(morsel['expires'], time_template)
490        )
491    return create_cookie(
492        comment=morsel['comment'],
493        comment_url=bool(morsel['comment']),
494        discard=False,
495        domain=morsel['domain'],
496        expires=expires,
497        name=morsel.key,
498        path=morsel['path'],
499        port=None,
500        rest={'HttpOnly': morsel['httponly']},
501        rfc2109=False,
502        secure=bool(morsel['secure']),
503        value=morsel.value,
504        version=morsel['version'] or 0,
505    )
506
507
508def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True):
509    """Returns a CookieJar from a key/value dictionary.
510
511    :param cookie_dict: Dict of key/values to insert into CookieJar.
512    :param cookiejar: (optional) A cookiejar to add the cookies to.
513    :param overwrite: (optional) If False, will not replace cookies
514        already in the jar with new ones.
515    :rtype: CookieJar
516    """
517    if cookiejar is None:
518        cookiejar = RequestsCookieJar()
519
520    if cookie_dict is not None:
521        names_from_jar = [cookie.name for cookie in cookiejar]
522        for name in cookie_dict:
523            if overwrite or (name not in names_from_jar):
524                cookiejar.set_cookie(create_cookie(name, cookie_dict[name]))
525
526    return cookiejar
527
528
529def merge_cookies(cookiejar, cookies):
530    """Add cookies to cookiejar and returns a merged CookieJar.
531
532    :param cookiejar: CookieJar object to add the cookies to.
533    :param cookies: Dictionary or CookieJar object to be added.
534    :rtype: CookieJar
535    """
536    if not isinstance(cookiejar, cookielib.CookieJar):
537        raise ValueError('You can only merge into CookieJar')
538
539    if isinstance(cookies, dict):
540        cookiejar = cookiejar_from_dict(
541            cookies, cookiejar=cookiejar, overwrite=False)
542    elif isinstance(cookies, cookielib.CookieJar):
543        try:
544            cookiejar.update(cookies)
545        except AttributeError:
546            for cookie_in_jar in cookies:
547                cookiejar.set_cookie(cookie_in_jar)
548
549    return cookiejar
550