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 ############################################################ 8 import warnings 9 10 class 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 88 def 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 98 def 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 122 def 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 139 def 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 168 def 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 193 def 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 222 def 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