1# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
2# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
3"""Routines to generate WSGI responses"""
4
5############################################################
6## Headers
7############################################################
8import warnings
9
10class HeaderDict(dict):
11
12    """
13    This represents response headers.  It handles the headers as a
14    dictionary, with case-insensitive keys.
15
16    Also there is an ``.add(key, value)`` method, which sets the key,
17    or adds the value to the current value (turning it into a list if
18    necessary).
19
20    For passing to WSGI there is a ``.headeritems()`` method which is
21    like ``.items()`` but unpacks value that are lists.  It also
22    handles encoding -- all headers are encoded in ASCII (if they are
23    unicode).
24
25    @@: Should that encoding be ISO-8859-1 or UTF-8?  I'm not sure
26    what the spec says.
27    """
28
29    def __getitem__(self, key):
30        return dict.__getitem__(self, self.normalize(key))
31
32    def __setitem__(self, key, value):
33        dict.__setitem__(self, self.normalize(key), value)
34
35    def __delitem__(self, key):
36        dict.__delitem__(self, self.normalize(key))
37
38    def __contains__(self, key):
39        return dict.__contains__(self, self.normalize(key))
40
41    has_key = __contains__
42
43    def get(self, key, failobj=None):
44        return dict.get(self, self.normalize(key), failobj)
45
46    def setdefault(self, key, failobj=None):
47        return dict.setdefault(self, self.normalize(key), failobj)
48
49    def pop(self, key, *args):
50        return dict.pop(self, self.normalize(key), *args)
51
52    def update(self, other):
53        for key in other:
54            self[self.normalize(key)] = other[key]
55
56    def normalize(self, key):
57        return str(key).lower().strip()
58
59    def add(self, key, value):
60        key = self.normalize(key)
61        if key in self:
62            if isinstance(self[key], list):
63                self[key].append(value)
64            else:
65                self[key] = [self[key], value]
66        else:
67            self[key] = value
68
69    def headeritems(self):
70        result = []
71        for key, value in self.items():
72            if isinstance(value, list):
73                for v in value:
74                    result.append((key, str(v)))
75            else:
76                result.append((key, str(value)))
77        return result
78
79    #@classmethod
80    def fromlist(cls, seq):
81        self = cls()
82        for name, value in seq:
83            self.add(name, value)
84        return self
85
86    fromlist = classmethod(fromlist)
87
88def has_header(headers, name):
89    """
90    Is header named ``name`` present in headers?
91    """
92    name = name.lower()
93    for header, value in headers:
94        if header.lower() == name:
95            return True
96    return False
97
98def header_value(headers, name):
99    """
100    Returns the header's value, or None if no such header.  If a
101    header appears more than once, all the values of the headers
102    are joined with ','.   Note that this is consistent /w RFC 2616
103    section 4.2 which states:
104
105        It MUST be possible to combine the multiple header fields
106        into one "field-name: field-value" pair, without changing
107        the semantics of the message, by appending each subsequent
108        field-value to the first, each separated by a comma.
109
110    However, note that the original netscape usage of 'Set-Cookie',
111    especially in MSIE which contains an 'expires' date will is not
112    compatible with this particular concatination method.
113    """
114    name = name.lower()
115    result = [value for header, value in headers
116              if header.lower() == name]
117    if result:
118        return ','.join(result)
119    else:
120        return None
121
122def remove_header(headers, name):
123    """
124    Removes the named header from the list of headers.  Returns the
125    value of that header, or None if no header found.  If multiple
126    headers are found, only the last one is returned.
127    """
128    name = name.lower()
129    i = 0
130    result = None
131    while i < len(headers):
132        if headers[i][0].lower() == name:
133            result = headers[i][1]
134            del headers[i]
135            continue
136        i += 1
137    return result
138
139def replace_header(headers, name, value):
140    """
141    Updates the headers replacing the first occurance of the given name
142    with the value provided; asserting that no further occurances
143    happen. Note that this is _not_ the same as remove_header and then
144    append, as two distinct operations (del followed by an append) are
145    not atomic in a threaded environment. Returns the previous header
146    value for the provided name, if any.   Clearly one should not use
147    this function with ``set-cookie`` or other names that may have more
148    than one occurance in the headers.
149    """
150    name = name.lower()
151    i = 0
152    result = None
153    while i < len(headers):
154        if headers[i][0].lower() == name:
155            assert not result, "two values for the header '%s' found" % name
156            result = headers[i][1]
157            headers[i] = (name, value)
158        i += 1
159    if not result:
160        headers.append((name, value))
161    return result
162
163
164############################################################
165## Deprecated methods
166############################################################
167
168def error_body_response(error_code, message, __warn=True):
169    """
170    Returns a standard HTML response page for an HTTP error.
171    **Note:** Deprecated
172    """
173    if __warn:
174        warnings.warn(
175            'wsgilib.error_body_response is deprecated; use the '
176            'wsgi_application method on an HTTPException object '
177            'instead', DeprecationWarning, 2)
178    return '''\
179<html>
180  <head>
181    <title>%(error_code)s</title>
182  </head>
183  <body>
184  <h1>%(error_code)s</h1>
185  %(message)s
186  </body>
187</html>''' % {
188        'error_code': error_code,
189        'message': message,
190        }
191
192
193def error_response(environ, error_code, message,
194                   debug_message=None, __warn=True):
195    """
196    Returns the status, headers, and body of an error response.
197
198    Use like:
199
200    .. code-block:: python
201
202        status, headers, body = wsgilib.error_response(
203            '301 Moved Permanently', 'Moved to <a href="%s">%s</a>'
204            % (url, url))
205        start_response(status, headers)
206        return [body]
207
208    **Note:** Deprecated
209    """
210    if __warn:
211        warnings.warn(
212            'wsgilib.error_response is deprecated; use the '
213            'wsgi_application method on an HTTPException object '
214            'instead', DeprecationWarning, 2)
215    if debug_message and environ.get('paste.config', {}).get('debug'):
216        message += '\n\n<!-- %s -->' % debug_message
217    body = error_body_response(error_code, message, __warn=False)
218    headers = [('content-type', 'text/html'),
219               ('content-length', str(len(body)))]
220    return error_code, headers, body
221
222def error_response_app(error_code, message, debug_message=None,
223                       __warn=True):
224    """
225    An application that emits the given error response.
226
227    **Note:** Deprecated
228    """
229    if __warn:
230        warnings.warn(
231            'wsgilib.error_response_app is deprecated; use the '
232            'wsgi_application method on an HTTPException object '
233            'instead', DeprecationWarning, 2)
234    def application(environ, start_response):
235        status, headers, body = error_response(
236            environ, error_code, message,
237            debug_message=debug_message, __warn=False)
238        start_response(status, headers)
239        return [body]
240    return application
241