1from base64 import b64encode
2from datetime import (
3    datetime,
4    timedelta,
5    )
6from hashlib import md5
7import re
8import struct
9import zlib
10try:
11    import simplejson as json
12except ImportError:
13    import json
14
15from webob.byterange import ContentRange
16
17from webob.cachecontrol import (
18    CacheControl,
19    serialize_cache_control,
20    )
21
22from webob.compat import (
23    PY3,
24    bytes_,
25    native_,
26    text_type,
27    url_quote,
28    urlparse,
29    )
30
31from webob.cookies import (
32    Cookie,
33    make_cookie,
34    )
35
36from webob.datetime_utils import (
37    parse_date_delta,
38    serialize_date_delta,
39    timedelta_to_seconds,
40    )
41
42from webob.descriptors import (
43    CHARSET_RE,
44    SCHEME_RE,
45    converter,
46    date_header,
47    header_getter,
48    list_header,
49    parse_auth,
50    parse_content_range,
51    parse_etag_response,
52    parse_int,
53    parse_int_safe,
54    serialize_auth,
55    serialize_content_range,
56    serialize_etag_response,
57    serialize_int,
58    )
59
60from webob.headers import ResponseHeaders
61from webob.request import BaseRequest
62from webob.util import status_reasons, status_generic_reasons
63
64__all__ = ['Response']
65
66_PARAM_RE = re.compile(r'([a-z0-9]+)=(?:"([^"]*)"|([a-z0-9_.-]*))', re.I)
67_OK_PARAM_RE = re.compile(r'^[a-z0-9_.-]+$', re.I)
68
69_gzip_header = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff'
70
71class Response(object):
72    """
73        Represents a WSGI response
74    """
75
76    default_content_type = 'text/html'
77    default_charset = 'UTF-8' # TODO: deprecate
78    unicode_errors = 'strict' # TODO: deprecate (why would response body have errors?)
79    default_conditional_response = False
80    request = None
81    environ = None
82
83    #
84    # __init__, from_file, copy
85    #
86
87    def __init__(self, body=None, status=None, headerlist=None, app_iter=None,
88                 content_type=None, conditional_response=None,
89                 **kw):
90        if app_iter is None and body is None and ('json_body' in kw or 'json' in kw):
91            if 'json_body' in kw:
92                json_body = kw.pop('json_body')
93            else:
94                json_body = kw.pop('json')
95            body = json.dumps(json_body, separators=(',', ':'))
96            if content_type is None:
97                content_type = 'application/json'
98        if app_iter is None:
99            if body is None:
100                body = b''
101        elif body is not None:
102            raise TypeError(
103                "You may only give one of the body and app_iter arguments")
104        if status is None:
105            self._status = '200 OK'
106        else:
107            self.status = status
108        if headerlist is None:
109            self._headerlist = []
110        else:
111            self._headerlist = headerlist
112        self._headers = None
113        if content_type is None:
114            content_type = self.default_content_type
115        charset = None
116        if 'charset' in kw:
117            charset = kw.pop('charset')
118        elif self.default_charset:
119            if (content_type
120                and 'charset=' not in content_type
121                and (content_type == 'text/html'
122                    or content_type.startswith('text/')
123                    or content_type.startswith('application/xml')
124                    or content_type.startswith('application/json')
125                    or (content_type.startswith('application/')
126                         and (content_type.endswith('+xml') or content_type.endswith('+json'))))):
127                charset = self.default_charset
128        if content_type and charset:
129            content_type += '; charset=' + charset
130        elif self._headerlist and charset:
131            self.charset = charset
132        if not self._headerlist and content_type:
133            self._headerlist.append(('Content-Type', content_type))
134        if conditional_response is None:
135            self.conditional_response = self.default_conditional_response
136        else:
137            self.conditional_response = bool(conditional_response)
138        if app_iter is None:
139            if isinstance(body, text_type):
140                if charset is None:
141                    raise TypeError(
142                        "You cannot set the body to a text value without a "
143                        "charset")
144                body = body.encode(charset)
145            app_iter = [body]
146            if headerlist is None:
147                self._headerlist.append(('Content-Length', str(len(body))))
148            else:
149                self.headers['Content-Length'] = str(len(body))
150        self._app_iter = app_iter
151        for name, value in kw.items():
152            if not hasattr(self.__class__, name):
153                # Not a basic attribute
154                raise TypeError(
155                    "Unexpected keyword: %s=%r" % (name, value))
156            setattr(self, name, value)
157
158
159    @classmethod
160    def from_file(cls, fp):
161        """Reads a response from a file-like object (it must implement
162        ``.read(size)`` and ``.readline()``).
163
164        It will read up to the end of the response, not the end of the
165        file.
166
167        This reads the response as represented by ``str(resp)``; it
168        may not read every valid HTTP response properly.  Responses
169        must have a ``Content-Length``"""
170        headerlist = []
171        status = fp.readline().strip()
172        is_text = isinstance(status, text_type)
173        if is_text:
174            _colon = ':'
175        else:
176            _colon = b':'
177        while 1:
178            line = fp.readline().strip()
179            if not line:
180                # end of headers
181                break
182            try:
183                header_name, value = line.split(_colon, 1)
184            except ValueError:
185                raise ValueError('Bad header line: %r' % line)
186            value = value.strip()
187            headerlist.append((
188                native_(header_name, 'latin-1'),
189                native_(value, 'latin-1')
190            ))
191        r = cls(
192            status=status,
193            headerlist=headerlist,
194            app_iter=(),
195        )
196        body = fp.read(r.content_length or 0)
197        if is_text:
198            r.text = body
199        else:
200            r.body = body
201        return r
202
203    def copy(self):
204        """Makes a copy of the response"""
205        # we need to do this for app_iter to be reusable
206        app_iter = list(self._app_iter)
207        iter_close(self._app_iter)
208        # and this to make sure app_iter instances are different
209        self._app_iter = list(app_iter)
210        return self.__class__(
211            content_type=False,
212            status=self._status,
213            headerlist=self._headerlist[:],
214            app_iter=app_iter,
215            conditional_response=self.conditional_response)
216
217
218    #
219    # __repr__, __str__
220    #
221
222    def __repr__(self):
223        return '<%s at 0x%x %s>' % (self.__class__.__name__, abs(id(self)),
224                                    self.status)
225
226    def __str__(self, skip_body=False):
227        parts = [self.status]
228        if not skip_body:
229            # Force enumeration of the body (to set content-length)
230            self.body
231        parts += map('%s: %s'.__mod__, self.headerlist)
232        if not skip_body and self.body:
233            parts += ['', self.text if PY3 else self.body]
234        return '\r\n'.join(parts)
235
236    #
237    # status, status_code/status_int
238    #
239
240    def _status__get(self):
241        """
242        The status string
243        """
244        return self._status
245
246    def _status__set(self, value):
247        try:
248            code = int(value)
249        except (ValueError, TypeError):
250            pass
251        else:
252            self.status_code = code
253            return
254        if PY3: # pragma: no cover
255            if isinstance(value, bytes):
256                value = value.decode('ascii')
257        elif isinstance(value, text_type):
258            value = value.encode('ascii')
259        if not isinstance(value, str):
260            raise TypeError(
261                "You must set status to a string or integer (not %s)"
262                % type(value))
263
264        # Attempt to get the status code itself, if this fails we should fail
265        status_code = int(value.split()[0])
266        self._status = value
267
268    status = property(_status__get, _status__set, doc=_status__get.__doc__)
269
270    def _status_code__get(self):
271        """
272        The status as an integer
273        """
274        return int(self._status.split()[0])
275
276    def _status_code__set(self, code):
277        try:
278            self._status = '%d %s' % (code, status_reasons[code])
279        except KeyError:
280            self._status = '%d %s' % (code, status_generic_reasons[code // 100])
281
282    status_code = status_int = property(_status_code__get, _status_code__set,
283                           doc=_status_code__get.__doc__)
284
285
286    #
287    # headerslist, headers
288    #
289
290    def _headerlist__get(self):
291        """
292        The list of response headers
293        """
294        return self._headerlist
295
296    def _headerlist__set(self, value):
297        self._headers = None
298        if not isinstance(value, list):
299            if hasattr(value, 'items'):
300                value = value.items()
301            value = list(value)
302        self._headerlist = value
303
304    def _headerlist__del(self):
305        self.headerlist = []
306
307    headerlist = property(_headerlist__get, _headerlist__set,
308                          _headerlist__del, doc=_headerlist__get.__doc__)
309
310    def _headers__get(self):
311        """
312        The headers in a dictionary-like object
313        """
314        if self._headers is None:
315            self._headers = ResponseHeaders.view_list(self.headerlist)
316        return self._headers
317
318    def _headers__set(self, value):
319        if hasattr(value, 'items'):
320            value = value.items()
321        self.headerlist = value
322        self._headers = None
323
324    headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__)
325
326
327    #
328    # body
329    #
330
331    def _body__get(self):
332        """
333        The body of the response, as a ``str``.  This will read in the
334        entire app_iter if necessary.
335        """
336        app_iter = self._app_iter
337#         try:
338#             if len(app_iter) == 1:
339#                 return app_iter[0]
340#         except:
341#             pass
342        if isinstance(app_iter, list) and len(app_iter) == 1:
343            return app_iter[0]
344        if app_iter is None:
345            raise AttributeError("No body has been set")
346        try:
347            body = b''.join(app_iter)
348        finally:
349            iter_close(app_iter)
350        if isinstance(body, text_type):
351            raise _error_unicode_in_app_iter(app_iter, body)
352        self._app_iter = [body]
353        if len(body) == 0:
354            # if body-length is zero, we assume it's a HEAD response and
355            # leave content_length alone
356            pass # pragma: no cover (no idea why necessary, it's hit)
357        elif self.content_length is None:
358            self.content_length = len(body)
359        elif self.content_length != len(body):
360            raise AssertionError(
361                "Content-Length is different from actual app_iter length "
362                "(%r!=%r)"
363                % (self.content_length, len(body))
364            )
365        return body
366
367    def _body__set(self, value=b''):
368        if not isinstance(value, bytes):
369            if isinstance(value, text_type):
370                msg = ("You cannot set Response.body to a text object "
371                       "(use Response.text)")
372            else:
373                msg = ("You can only set the body to a binary type (not %s)" %
374                       type(value))
375            raise TypeError(msg)
376        if self._app_iter is not None:
377            self.content_md5 = None
378        self._app_iter = [value]
379        self.content_length = len(value)
380
381#     def _body__del(self):
382#         self.body = ''
383#         #self.content_length = None
384
385    body = property(_body__get, _body__set, _body__set)
386
387    def _json_body__get(self):
388        """Access the body of the response as JSON"""
389        # Note: UTF-8 is a content-type specific default for JSON:
390        return json.loads(self.body.decode(self.charset or 'UTF-8'))
391
392    def _json_body__set(self, value):
393        self.body = json.dumps(value, separators=(',', ':')).encode(self.charset or 'UTF-8')
394
395    def _json_body__del(self):
396        del self.body
397
398    json = json_body = property(_json_body__get, _json_body__set, _json_body__del)
399
400
401    #
402    # text, unicode_body, ubody
403    #
404
405    def _text__get(self):
406        """
407        Get/set the text value of the body (using the charset of the
408        Content-Type)
409        """
410        if not self.charset:
411            raise AttributeError(
412                "You cannot access Response.text unless charset is set")
413        body = self.body
414        return body.decode(self.charset, self.unicode_errors)
415
416    def _text__set(self, value):
417        if not self.charset:
418            raise AttributeError(
419                "You cannot access Response.text unless charset is set")
420        if not isinstance(value, text_type):
421            raise TypeError(
422                "You can only set Response.text to a unicode string "
423                "(not %s)" % type(value))
424        self.body = value.encode(self.charset)
425
426    def _text__del(self):
427        del self.body
428
429    text = property(_text__get, _text__set, _text__del, doc=_text__get.__doc__)
430
431    unicode_body = ubody = property(_text__get, _text__set, _text__del,
432        "Deprecated alias for .text")
433
434    #
435    # body_file, write(text)
436    #
437
438    def _body_file__get(self):
439        """
440        A file-like object that can be used to write to the
441        body.  If you passed in a list app_iter, that app_iter will be
442        modified by writes.
443        """
444        return ResponseBodyFile(self)
445
446    def _body_file__set(self, file):
447        self.app_iter = iter_file(file)
448
449    def _body_file__del(self):
450        del self.body
451
452    body_file = property(_body_file__get, _body_file__set, _body_file__del,
453                         doc=_body_file__get.__doc__)
454
455    def write(self, text):
456        if not isinstance(text, bytes):
457            if not isinstance(text, text_type):
458                msg = "You can only write str to a Response.body_file, not %s"
459                raise TypeError(msg % type(text))
460            if not self.charset:
461                msg = ("You can only write text to Response if charset has "
462                       "been set")
463                raise TypeError(msg)
464            text = text.encode(self.charset)
465        app_iter = self._app_iter
466        if not isinstance(app_iter, list):
467            try:
468                new_app_iter = self._app_iter = list(app_iter)
469            finally:
470                iter_close(app_iter)
471            app_iter = new_app_iter
472            self.content_length = sum(len(chunk) for chunk in app_iter)
473        app_iter.append(text)
474        if self.content_length is not None:
475            self.content_length += len(text)
476
477
478
479    #
480    # app_iter
481    #
482
483    def _app_iter__get(self):
484        """
485        Returns the app_iter of the response.
486
487        If body was set, this will create an app_iter from that body
488        (a single-item list)
489        """
490        return self._app_iter
491
492    def _app_iter__set(self, value):
493        if self._app_iter is not None:
494            # Undo the automatically-set content-length
495            self.content_length = None
496            self.content_md5 = None
497        self._app_iter = value
498
499    def _app_iter__del(self):
500        self._app_iter = []
501        self.content_length = None
502
503    app_iter = property(_app_iter__get, _app_iter__set, _app_iter__del,
504                        doc=_app_iter__get.__doc__)
505
506
507
508    #
509    # headers attrs
510    #
511
512    allow = list_header('Allow', '14.7')
513    # TODO: (maybe) support response.vary += 'something'
514    # TODO: same thing for all listy headers
515    vary = list_header('Vary', '14.44')
516
517    content_length = converter(
518        header_getter('Content-Length', '14.17'),
519        parse_int, serialize_int, 'int')
520
521    content_encoding = header_getter('Content-Encoding', '14.11')
522    content_language = list_header('Content-Language', '14.12')
523    content_location = header_getter('Content-Location', '14.14')
524    content_md5 = header_getter('Content-MD5', '14.14')
525    content_disposition = header_getter('Content-Disposition', '19.5.1')
526
527    accept_ranges = header_getter('Accept-Ranges', '14.5')
528    content_range = converter(
529        header_getter('Content-Range', '14.16'),
530        parse_content_range, serialize_content_range, 'ContentRange object')
531
532    date = date_header('Date', '14.18')
533    expires = date_header('Expires', '14.21')
534    last_modified = date_header('Last-Modified', '14.29')
535
536    _etag_raw = header_getter('ETag', '14.19')
537    etag = converter(_etag_raw,
538        parse_etag_response, serialize_etag_response,
539        'Entity tag'
540    )
541    @property
542    def etag_strong(self):
543        return parse_etag_response(self._etag_raw, strong=True)
544
545    location = header_getter('Location', '14.30')
546    pragma = header_getter('Pragma', '14.32')
547    age = converter(
548        header_getter('Age', '14.6'),
549        parse_int_safe, serialize_int, 'int')
550
551    retry_after = converter(
552        header_getter('Retry-After', '14.37'),
553        parse_date_delta, serialize_date_delta, 'HTTP date or delta seconds')
554
555    server = header_getter('Server', '14.38')
556
557    # TODO: the standard allows this to be a list of challenges
558    www_authenticate = converter(
559        header_getter('WWW-Authenticate', '14.47'),
560        parse_auth, serialize_auth,
561    )
562
563
564    #
565    # charset
566    #
567
568    def _charset__get(self):
569        """
570        Get/set the charset (in the Content-Type)
571        """
572        header = self.headers.get('Content-Type')
573        if not header:
574            return None
575        match = CHARSET_RE.search(header)
576        if match:
577            return match.group(1)
578        return None
579
580    def _charset__set(self, charset):
581        if charset is None:
582            del self.charset
583            return
584        header = self.headers.pop('Content-Type', None)
585        if header is None:
586            raise AttributeError("You cannot set the charset when no "
587                                 "content-type is defined")
588        match = CHARSET_RE.search(header)
589        if match:
590            header = header[:match.start()] + header[match.end():]
591        header += '; charset=%s' % charset
592        self.headers['Content-Type'] = header
593
594    def _charset__del(self):
595        header = self.headers.pop('Content-Type', None)
596        if header is None:
597            # Don't need to remove anything
598            return
599        match = CHARSET_RE.search(header)
600        if match:
601            header = header[:match.start()] + header[match.end():]
602        self.headers['Content-Type'] = header
603
604    charset = property(_charset__get, _charset__set, _charset__del,
605                       doc=_charset__get.__doc__)
606
607
608    #
609    # content_type
610    #
611
612    def _content_type__get(self):
613        """
614        Get/set the Content-Type header (or None), *without* the
615        charset or any parameters.
616
617        If you include parameters (or ``;`` at all) when setting the
618        content_type, any existing parameters will be deleted;
619        otherwise they will be preserved.
620        """
621        header = self.headers.get('Content-Type')
622        if not header:
623            return None
624        return header.split(';', 1)[0]
625
626    def _content_type__set(self, value):
627        if not value:
628            self._content_type__del()
629            return
630        if ';' not in value:
631            header = self.headers.get('Content-Type', '')
632            if ';' in header:
633                params = header.split(';', 1)[1]
634                value += ';' + params
635        self.headers['Content-Type'] = value
636
637    def _content_type__del(self):
638        self.headers.pop('Content-Type', None)
639
640    content_type = property(_content_type__get, _content_type__set,
641                            _content_type__del, doc=_content_type__get.__doc__)
642
643
644    #
645    # content_type_params
646    #
647
648    def _content_type_params__get(self):
649        """
650        A dictionary of all the parameters in the content type.
651
652        (This is not a view, set to change, modifications of the dict would not
653        be applied otherwise)
654        """
655        params = self.headers.get('Content-Type', '')
656        if ';' not in params:
657            return {}
658        params = params.split(';', 1)[1]
659        result = {}
660        for match in _PARAM_RE.finditer(params):
661            result[match.group(1)] = match.group(2) or match.group(3) or ''
662        return result
663
664    def _content_type_params__set(self, value_dict):
665        if not value_dict:
666            del self.content_type_params
667            return
668        params = []
669        for k, v in sorted(value_dict.items()):
670            if not _OK_PARAM_RE.search(v):
671                v = '"%s"' % v.replace('"', '\\"')
672            params.append('; %s=%s' % (k, v))
673        ct = self.headers.pop('Content-Type', '').split(';', 1)[0]
674        ct += ''.join(params)
675        self.headers['Content-Type'] = ct
676
677    def _content_type_params__del(self):
678        self.headers['Content-Type'] = self.headers.get(
679            'Content-Type', '').split(';', 1)[0]
680
681    content_type_params = property(
682        _content_type_params__get,
683        _content_type_params__set,
684        _content_type_params__del,
685        _content_type_params__get.__doc__
686    )
687
688
689
690
691    #
692    # set_cookie, unset_cookie, delete_cookie, merge_cookies
693    #
694
695    def set_cookie(self, name, value='', max_age=None,
696                   path='/', domain=None, secure=False, httponly=False,
697                   comment=None, expires=None, overwrite=False):
698        """
699        Set (add) a cookie for the response.
700
701        Arguments are:
702
703        ``name``
704
705           The cookie name.
706
707        ``value``
708
709           The cookie value, which should be a string or ``None``.  If
710           ``value`` is ``None``, it's equivalent to calling the
711           :meth:`webob.response.Response.unset_cookie` method for this
712           cookie key (it effectively deletes the cookie on the client).
713
714        ``max_age``
715
716           An integer representing a number of seconds, ``datetime.timedelta``,
717           or ``None``. This value is used as the ``Max-Age`` of the generated
718           cookie.  If ``expires`` is not passed and this value is not
719           ``None``, the ``max_age`` value will also influence the ``Expires``
720           value of the cookie (``Expires`` will be set to now + max_age).  If
721           this value is ``None``, the cookie will not have a ``Max-Age`` value
722           (unless ``expires`` is set). If both ``max_age`` and ``expires`` are
723           set, this value takes precedence.
724
725        ``path``
726
727           A string representing the cookie ``Path`` value.  It defaults to
728           ``/``.
729
730        ``domain``
731
732           A string representing the cookie ``Domain``, or ``None``.  If
733           domain is ``None``, no ``Domain`` value will be sent in the
734           cookie.
735
736        ``secure``
737
738           A boolean.  If it's ``True``, the ``secure`` flag will be sent in
739           the cookie, if it's ``False``, the ``secure`` flag will not be
740           sent in the cookie.
741
742        ``httponly``
743
744           A boolean.  If it's ``True``, the ``HttpOnly`` flag will be sent
745           in the cookie, if it's ``False``, the ``HttpOnly`` flag will not
746           be sent in the cookie.
747
748        ``comment``
749
750           A string representing the cookie ``Comment`` value, or ``None``.
751           If ``comment`` is ``None``, no ``Comment`` value will be sent in
752           the cookie.
753
754        ``expires``
755
756           A ``datetime.timedelta`` object representing an amount of time,
757           ``datetime.datetime`` or ``None``. A non-``None`` value is used to
758           generate the ``Expires`` value of the generated cookie. If
759           ``max_age`` is not passed, but this value is not ``None``, it will
760           influence the ``Max-Age`` header. If this value is ``None``, the
761           ``Expires`` cookie value will be unset (unless ``max_age`` is set).
762           If ``max_age`` is set, it will be used to generate the ``expires``
763           and this value is ignored.
764
765        ``overwrite``
766
767           If this key is ``True``, before setting the cookie, unset any
768           existing cookie.
769
770        """
771        if overwrite:
772            self.unset_cookie(name, strict=False)
773
774        # If expires is set, but not max_age we set max_age to expires
775        if not max_age and isinstance(expires, timedelta):
776            max_age = expires
777
778        # expires can also be a datetime
779        if not max_age and isinstance(expires, datetime):
780            max_age = expires - datetime.utcnow()
781
782        value = bytes_(value, 'utf-8')
783
784        cookie = make_cookie(name, value, max_age=max_age, path=path,
785                domain=domain, secure=secure, httponly=httponly,
786                comment=comment)
787
788        self.headerlist.append(('Set-Cookie', cookie))
789
790    def delete_cookie(self, name, path='/', domain=None):
791        """
792        Delete a cookie from the client.  Note that path and domain must match
793        how the cookie was originally set.
794
795        This sets the cookie to the empty string, and max_age=0 so
796        that it should expire immediately.
797        """
798        self.set_cookie(name, None, path=path, domain=domain)
799
800    def unset_cookie(self, name, strict=True):
801        """
802        Unset a cookie with the given name (remove it from the
803        response).
804        """
805        existing = self.headers.getall('Set-Cookie')
806        if not existing and not strict:
807            return
808        cookies = Cookie()
809        for header in existing:
810            cookies.load(header)
811        if isinstance(name, text_type):
812            name = name.encode('utf8')
813        if name in cookies:
814            del cookies[name]
815            del self.headers['Set-Cookie']
816            for m in cookies.values():
817                self.headerlist.append(('Set-Cookie', m.serialize()))
818        elif strict:
819            raise KeyError("No cookie has been set with the name %r" % name)
820
821
822    def merge_cookies(self, resp):
823        """Merge the cookies that were set on this response with the
824        given `resp` object (which can be any WSGI application).
825
826        If the `resp` is a :class:`webob.Response` object, then the
827        other object will be modified in-place.
828        """
829        if not self.headers.get('Set-Cookie'):
830            return resp
831        if isinstance(resp, Response):
832            for header in self.headers.getall('Set-Cookie'):
833                resp.headers.add('Set-Cookie', header)
834            return resp
835        else:
836            c_headers = [h for h in self.headerlist if
837                         h[0].lower() == 'set-cookie']
838            def repl_app(environ, start_response):
839                def repl_start_response(status, headers, exc_info=None):
840                    return start_response(status, headers+c_headers,
841                                          exc_info=exc_info)
842                return resp(environ, repl_start_response)
843            return repl_app
844
845
846    #
847    # cache_control
848    #
849
850    _cache_control_obj = None
851
852    def _cache_control__get(self):
853        """
854        Get/set/modify the Cache-Control header (`HTTP spec section 14.9
855        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_)
856        """
857        value = self.headers.get('cache-control', '')
858        if self._cache_control_obj is None:
859            self._cache_control_obj = CacheControl.parse(
860                value, updates_to=self._update_cache_control, type='response')
861            self._cache_control_obj.header_value = value
862        if self._cache_control_obj.header_value != value:
863            new_obj = CacheControl.parse(value, type='response')
864            self._cache_control_obj.properties.clear()
865            self._cache_control_obj.properties.update(new_obj.properties)
866            self._cache_control_obj.header_value = value
867        return self._cache_control_obj
868
869    def _cache_control__set(self, value):
870        # This actually becomes a copy
871        if not value:
872            value = ""
873        if isinstance(value, dict):
874            value = CacheControl(value, 'response')
875        if isinstance(value, text_type):
876            value = str(value)
877        if isinstance(value, str):
878            if self._cache_control_obj is None:
879                self.headers['Cache-Control'] = value
880                return
881            value = CacheControl.parse(value, 'response')
882        cache = self.cache_control
883        cache.properties.clear()
884        cache.properties.update(value.properties)
885
886    def _cache_control__del(self):
887        self.cache_control = {}
888
889    def _update_cache_control(self, prop_dict):
890        value = serialize_cache_control(prop_dict)
891        if not value:
892            if 'Cache-Control' in self.headers:
893                del self.headers['Cache-Control']
894        else:
895            self.headers['Cache-Control'] = value
896
897    cache_control = property(
898        _cache_control__get, _cache_control__set,
899        _cache_control__del, doc=_cache_control__get.__doc__)
900
901
902    #
903    # cache_expires
904    #
905
906    def _cache_expires(self, seconds=0, **kw):
907        """
908            Set expiration on this request.  This sets the response to
909            expire in the given seconds, and any other attributes are used
910            for cache_control (e.g., private=True, etc).
911        """
912        if seconds is True:
913            seconds = 0
914        elif isinstance(seconds, timedelta):
915            seconds = timedelta_to_seconds(seconds)
916        cache_control = self.cache_control
917        if seconds is None:
918            pass
919        elif not seconds:
920            # To really expire something, you have to force a
921            # bunch of these cache control attributes, and IE may
922            # not pay attention to those still so we also set
923            # Expires.
924            cache_control.no_store = True
925            cache_control.no_cache = True
926            cache_control.must_revalidate = True
927            cache_control.max_age = 0
928            cache_control.post_check = 0
929            cache_control.pre_check = 0
930            self.expires = datetime.utcnow()
931            if 'last-modified' not in self.headers:
932                self.last_modified = datetime.utcnow()
933            self.pragma = 'no-cache'
934        else:
935            cache_control.properties.clear()
936            cache_control.max_age = seconds
937            self.expires = datetime.utcnow() + timedelta(seconds=seconds)
938            self.pragma = None
939        for name, value in kw.items():
940            setattr(cache_control, name, value)
941
942    cache_expires = property(lambda self: self._cache_expires, _cache_expires)
943
944
945
946    #
947    # encode_content, decode_content, md5_etag
948    #
949
950    def encode_content(self, encoding='gzip', lazy=False):
951        """
952        Encode the content with the given encoding (only gzip and
953        identity are supported).
954        """
955        assert encoding in ('identity', 'gzip'), \
956               "Unknown encoding: %r" % encoding
957        if encoding == 'identity':
958            self.decode_content()
959            return
960        if self.content_encoding == 'gzip':
961            return
962        if lazy:
963            self.app_iter = gzip_app_iter(self._app_iter)
964            self.content_length = None
965        else:
966            self.app_iter = list(gzip_app_iter(self._app_iter))
967            self.content_length = sum(map(len, self._app_iter))
968        self.content_encoding = 'gzip'
969
970    def decode_content(self):
971        content_encoding = self.content_encoding or 'identity'
972        if content_encoding == 'identity':
973            return
974        if content_encoding not in ('gzip', 'deflate'):
975            raise ValueError(
976                "I don't know how to decode the content %s" % content_encoding)
977        if content_encoding == 'gzip':
978            from gzip import GzipFile
979            from io import BytesIO
980            gzip_f = GzipFile(filename='', mode='r', fileobj=BytesIO(self.body))
981            self.body = gzip_f.read()
982            self.content_encoding = None
983            gzip_f.close()
984        else:
985            # Weird feature: http://bugs.python.org/issue5784
986            self.body = zlib.decompress(self.body, -15)
987            self.content_encoding = None
988
989    def md5_etag(self, body=None, set_content_md5=False):
990        """
991        Generate an etag for the response object using an MD5 hash of
992        the body (the body parameter, or ``self.body`` if not given)
993
994        Sets ``self.etag``
995        If ``set_content_md5`` is True sets ``self.content_md5`` as well
996        """
997        if body is None:
998            body = self.body
999        md5_digest = md5(body).digest()
1000        md5_digest = b64encode(md5_digest)
1001        md5_digest = md5_digest.replace(b'\n', b'')
1002        md5_digest = native_(md5_digest)
1003        self.etag = md5_digest.strip('=')
1004        if set_content_md5:
1005            self.content_md5 = md5_digest
1006
1007
1008
1009    #
1010    # __call__, conditional_response_app
1011    #
1012
1013    def __call__(self, environ, start_response):
1014        """
1015        WSGI application interface
1016        """
1017        if self.conditional_response:
1018            return self.conditional_response_app(environ, start_response)
1019        headerlist = self._abs_headerlist(environ)
1020        start_response(self.status, headerlist)
1021        if environ['REQUEST_METHOD'] == 'HEAD':
1022            # Special case here...
1023            return EmptyResponse(self._app_iter)
1024        return self._app_iter
1025
1026    def _abs_headerlist(self, environ):
1027        """Returns a headerlist, with the Location header possibly
1028        made absolute given the request environ.
1029        """
1030        headerlist = list(self.headerlist)
1031        for i, (name, value) in enumerate(headerlist):
1032            if name.lower() == 'location':
1033                if SCHEME_RE.search(value):
1034                    break
1035                new_location = urlparse.urljoin(_request_uri(environ), value)
1036                headerlist[i] = (name, new_location)
1037                break
1038        return headerlist
1039
1040    _safe_methods = ('GET', 'HEAD')
1041
1042    def conditional_response_app(self, environ, start_response):
1043        """
1044        Like the normal __call__ interface, but checks conditional headers:
1045
1046        * If-Modified-Since   (304 Not Modified; only on GET, HEAD)
1047        * If-None-Match       (304 Not Modified; only on GET, HEAD)
1048        * Range               (406 Partial Content; only on GET, HEAD)
1049        """
1050        req = BaseRequest(environ)
1051        headerlist = self._abs_headerlist(environ)
1052        method = environ.get('REQUEST_METHOD', 'GET')
1053        if method in self._safe_methods:
1054            status304 = False
1055            if req.if_none_match and self.etag:
1056                status304 = self.etag in req.if_none_match
1057            elif req.if_modified_since and self.last_modified:
1058                status304 = self.last_modified <= req.if_modified_since
1059            if status304:
1060                start_response('304 Not Modified', filter_headers(headerlist))
1061                return EmptyResponse(self._app_iter)
1062        if (req.range and self in req.if_range
1063            and self.content_range is None
1064            and method in ('HEAD', 'GET')
1065            and self.status_code == 200
1066            and self.content_length is not None
1067        ):
1068            content_range = req.range.content_range(self.content_length)
1069            if content_range is None:
1070                iter_close(self._app_iter)
1071                body = bytes_("Requested range not satisfiable: %s" % req.range)
1072                headerlist = [
1073                    ('Content-Length', str(len(body))),
1074                    ('Content-Range', str(ContentRange(None, None,
1075                                                       self.content_length))),
1076                    ('Content-Type', 'text/plain'),
1077                ] + filter_headers(headerlist)
1078                start_response('416 Requested Range Not Satisfiable',
1079                               headerlist)
1080                if method == 'HEAD':
1081                    return ()
1082                return [body]
1083            else:
1084                app_iter = self.app_iter_range(content_range.start,
1085                                               content_range.stop)
1086                if app_iter is not None:
1087                    # the following should be guaranteed by
1088                    # Range.range_for_length(length)
1089                    assert content_range.start is not None
1090                    headerlist = [
1091                        ('Content-Length',
1092                         str(content_range.stop - content_range.start)),
1093                        ('Content-Range', str(content_range)),
1094                    ] + filter_headers(headerlist, ('content-length',))
1095                    start_response('206 Partial Content', headerlist)
1096                    if method == 'HEAD':
1097                        return EmptyResponse(app_iter)
1098                    return app_iter
1099
1100        start_response(self.status, headerlist)
1101        if method  == 'HEAD':
1102            return EmptyResponse(self._app_iter)
1103        return self._app_iter
1104
1105    def app_iter_range(self, start, stop):
1106        """
1107        Return a new app_iter built from the response app_iter, that
1108        serves up only the given ``start:stop`` range.
1109        """
1110        app_iter = self._app_iter
1111        if hasattr(app_iter, 'app_iter_range'):
1112            return app_iter.app_iter_range(start, stop)
1113        return AppIterRange(app_iter, start, stop)
1114
1115
1116def filter_headers(hlist, remove_headers=('content-length', 'content-type')):
1117    return [h for h in hlist if (h[0].lower() not in remove_headers)]
1118
1119
1120def iter_file(file, block_size=1<<18): # 256Kb
1121    while True:
1122        data = file.read(block_size)
1123        if not data:
1124            break
1125        yield data
1126
1127class ResponseBodyFile(object):
1128    mode = 'wb'
1129    closed = False
1130
1131    def __init__(self, response):
1132        self.response = response
1133        self.write = response.write
1134
1135    def __repr__(self):
1136        return '<body_file for %r>' % self.response
1137
1138    encoding = property(
1139        lambda self: self.response.charset,
1140        doc="The encoding of the file (inherited from response.charset)"
1141    )
1142
1143    def writelines(self, seq):
1144        for item in seq:
1145            self.write(item)
1146
1147    def close(self):
1148        raise NotImplementedError("Response bodies cannot be closed")
1149
1150    def flush(self):
1151        pass
1152
1153
1154
1155class AppIterRange(object):
1156    """
1157    Wraps an app_iter, returning just a range of bytes
1158    """
1159
1160    def __init__(self, app_iter, start, stop):
1161        assert start >= 0, "Bad start: %r" % start
1162        assert stop is None or (stop >= 0 and stop >= start), (
1163            "Bad stop: %r" % stop)
1164        self.app_iter = iter(app_iter)
1165        self._pos = 0 # position in app_iter
1166        self.start = start
1167        self.stop = stop
1168
1169    def __iter__(self):
1170        return self
1171
1172    def _skip_start(self):
1173        start, stop = self.start, self.stop
1174        for chunk in self.app_iter:
1175            self._pos += len(chunk)
1176            if self._pos < start:
1177                continue
1178            elif self._pos == start:
1179                return b''
1180            else:
1181                chunk = chunk[start-self._pos:]
1182                if stop is not None and self._pos > stop:
1183                    chunk = chunk[:stop-self._pos]
1184                    assert len(chunk) == stop - start
1185                return chunk
1186        else:
1187            raise StopIteration()
1188
1189
1190    def next(self):
1191        if self._pos < self.start:
1192            # need to skip some leading bytes
1193            return self._skip_start()
1194        stop = self.stop
1195        if stop is not None and self._pos >= stop:
1196            raise StopIteration
1197
1198        chunk = next(self.app_iter)
1199        self._pos += len(chunk)
1200
1201        if stop is None or self._pos <= stop:
1202            return chunk
1203        else:
1204            return chunk[:stop-self._pos]
1205
1206    __next__ = next # py3
1207
1208    def close(self):
1209        iter_close(self.app_iter)
1210
1211
1212class EmptyResponse(object):
1213    """An empty WSGI response.
1214
1215    An iterator that immediately stops. Optionally provides a close
1216    method to close an underlying app_iter it replaces.
1217    """
1218
1219    def __init__(self, app_iter=None):
1220        if app_iter is not None and hasattr(app_iter, 'close'):
1221            self.close = app_iter.close
1222
1223    def __iter__(self):
1224        return self
1225
1226    def __len__(self):
1227        return 0
1228
1229    def next(self):
1230        raise StopIteration()
1231
1232    __next__ = next # py3
1233
1234def _request_uri(environ):
1235    """Like wsgiref.url.request_uri, except eliminates :80 ports
1236
1237    Return the full request URI"""
1238    url = environ['wsgi.url_scheme']+'://'
1239
1240    if environ.get('HTTP_HOST'):
1241        url += environ['HTTP_HOST']
1242    else:
1243        url += environ['SERVER_NAME'] + ':' + environ['SERVER_PORT']
1244    if url.endswith(':80') and environ['wsgi.url_scheme'] == 'http':
1245        url = url[:-3]
1246    elif url.endswith(':443') and environ['wsgi.url_scheme'] == 'https':
1247        url = url[:-4]
1248
1249    if PY3: # pragma: no cover
1250        script_name = bytes_(environ.get('SCRIPT_NAME', '/'), 'latin-1')
1251        path_info = bytes_(environ.get('PATH_INFO', ''), 'latin-1')
1252    else:
1253        script_name = environ.get('SCRIPT_NAME', '/')
1254        path_info = environ.get('PATH_INFO', '')
1255
1256    url += url_quote(script_name)
1257    qpath_info = url_quote(path_info)
1258    if not 'SCRIPT_NAME' in environ:
1259        url += qpath_info[1:]
1260    else:
1261        url += qpath_info
1262    return url
1263
1264
1265def iter_close(iter):
1266    if hasattr(iter, 'close'):
1267        iter.close()
1268
1269def gzip_app_iter(app_iter):
1270    size = 0
1271    crc = zlib.crc32(b"") & 0xffffffff
1272    compress = zlib.compressobj(9, zlib.DEFLATED, -zlib.MAX_WBITS,
1273                                zlib.DEF_MEM_LEVEL, 0)
1274
1275    yield _gzip_header
1276    for item in app_iter:
1277        size += len(item)
1278        crc = zlib.crc32(item, crc) & 0xffffffff
1279
1280        # The compress function may return zero length bytes if the input is
1281        # small enough; it buffers the input for the next iteration or for a
1282        # flush.
1283        result = compress.compress(item)
1284        if result:
1285            yield result
1286
1287    # Similarly, flush may also not yield a value.
1288    result = compress.flush()
1289    if result:
1290        yield result
1291    yield struct.pack("<2L", crc, size & 0xffffffff)
1292
1293def _error_unicode_in_app_iter(app_iter, body):
1294    app_iter_repr = repr(app_iter)
1295    if len(app_iter_repr) > 50:
1296        app_iter_repr = (
1297            app_iter_repr[:30] + '...' + app_iter_repr[-10:])
1298    raise TypeError(
1299        'An item of the app_iter (%s) was text, causing a '
1300        'text body: %r' % (app_iter_repr, body))
1301