1 #!/usr/bin/env python 2 3 """ 4 Request helper classes. 5 6 Copyright (C) 2004, 2005 Paul Boddie <paul@boddie.org.uk> 7 8 This library is free software; you can redistribute it and/or 9 modify it under the terms of the GNU Lesser General Public 10 License as published by the Free Software Foundation; either 11 version 2.1 of the License, or (at your option) any later version. 12 13 This library is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 Lesser General Public License for more details. 17 18 You should have received a copy of the GNU Lesser General Public 19 License along with this library; if not, write to the Free Software 20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 21 """ 22 23 class MessageBodyStream: 24 25 """ 26 A naive stream class, providing a non-blocking stream for transactions when 27 reading the message body. According to the HTTP standard, the following 28 things decide how long the message is: 29 30 * Use of the Content-Length header field (see 4.4 Message Length). 31 * Use of the Transfer-Coding header field (see 3.6 Transfer Codings), 32 particularly when the "chunked" coding is used. 33 34 NOTE: For now, we don't support the Transfer-Coding business. 35 """ 36 37 def __init__(self, stream, headers): 38 39 """ 40 Initialise the object with the given underlying 'stream'. The supplied 41 'headers' in a dictionary-style object are used to examine the nature of 42 the request. 43 """ 44 45 self.stream = stream 46 self.headers = headers 47 self.length = int(headers.get("Content-Length") or 0) 48 49 def read(self, limit=None): 50 51 "Reads all remaining data from the message body." 52 53 if limit is not None: 54 limit = min(limit, self.length) 55 else: 56 limit = self.length 57 data = self.stream.read(limit) 58 self.length = self.length - len(data) 59 return data 60 61 def readline(self): 62 63 "Reads a single line of data from the message body." 64 65 data = [] 66 while self.length > 0: 67 data.append(self.read(1)) 68 if data[-1] == "\n": 69 break 70 return "".join(data) 71 72 def readlines(self): 73 74 """ 75 Reads all remaining data from the message body, splitting it into lines 76 and returning the data as a list of lines. 77 """ 78 79 lines = self.read().split("\n") 80 for i in range(0, len(lines) - 1): 81 lines[i] = lines[i] + "\n" 82 return lines 83 84 def close(self): 85 86 "Closes the stream." 87 88 self.stream.close() 89 90 class HeaderValue: 91 92 "A container for header information." 93 94 def __init__(self, principal_value, **attributes): 95 96 """ 97 Initialise the container with the given 'principal_value' and optional 98 keyword attributes representing the key=value pairs which accompany the 99 'principal_value'. 100 """ 101 102 self.principal_value = principal_value 103 self.attributes = attributes 104 105 def __getattr__(self, name): 106 if self.attributes.has_key(name): 107 return self.attributes[name] 108 else: 109 raise AttributeError, name 110 111 def __str__(self): 112 113 """ 114 Format the header value object, producing a string suitable for the 115 response header field. 116 """ 117 118 l = [] 119 if self.principal_value: 120 l.append(self.principal_value) 121 for name, value in self.attributes.items(): 122 l.append("; ") 123 l.append("%s=%s" % (name, value)) 124 125 # Make sure that only ASCII is used. 126 127 return "".join(l).encode("US-ASCII") 128 129 class ContentType(HeaderValue): 130 131 "A container for content type information." 132 133 def __init__(self, media_type, charset=None, **attributes): 134 135 """ 136 Initialise the container with the given 'media_type', an optional 137 'charset', and optional keyword attributes representing the key=value 138 pairs which qualify content types. 139 """ 140 141 if charset is not None: 142 attributes["charset"] = charset 143 HeaderValue.__init__(self, media_type, **attributes) 144 145 def __getattr__(self, name): 146 if name == "media_type": 147 return self.principal_value 148 elif name == "charset": 149 return self.attributes.get("charset") 150 elif self.attributes.has_key(name): 151 return self.attributes[name] 152 else: 153 raise AttributeError, name 154 155 class Cookie: 156 157 """ 158 A simple cookie class for frameworks which do not return cookies in 159 structured form. 160 """ 161 162 def __init__(self, name, value): 163 self.name = name 164 self.value = value 165 166 class FileContent: 167 168 """ 169 A simple class representing uploaded file content. This is useful in holding 170 metadata as well as being an indicator of such content in environments such 171 as Jython where it is not trivial to differentiate between plain strings and 172 Unicode in a fashion also applicable to CPython. 173 """ 174 175 def __init__(self, content, headers=None): 176 177 """ 178 Initialise the object with 'content' and optional 'headers' describing 179 the content. 180 """ 181 182 self.content = content 183 self.headers = headers or {} 184 185 def __str__(self): 186 return self.content 187 188 def parse_header_value(header_class, header_value_str): 189 190 """ 191 Create an object of the given 'header_class' by determining the details 192 of the given 'header_value_str' - a string containing the value of a 193 particular header. 194 """ 195 196 if header_value_str is None: 197 return header_class(None) 198 199 l = header_value_str.split(";") 200 attributes = {} 201 202 # Find the attributes. 203 204 principal_value, attributes_str = l[0].strip(), l[1:] 205 206 for attribute_str in attributes_str: 207 t = attribute_str.split("=") 208 if len(t) > 1: 209 name, value = t[0].strip(), t[1].strip() 210 attributes[name] = value 211 212 return header_class(principal_value, **attributes) 213 214 def parse_headers(headers): 215 216 """ 217 Parse the given 'headers' dictionary (containing names mapped to values), 218 returing a dictionary mapping names to HeaderValue objects. 219 """ 220 221 new_headers = {} 222 for name, value in headers.items(): 223 new_headers[name] = parse_header_value(HeaderValue, value) 224 return new_headers 225 226 def get_storage_items(storage_body): 227 228 """ 229 Return the items (2-tuples of the form key, values) from the 'storage_body'. 230 This is used in conjunction with FieldStorage objects. 231 """ 232 233 items = [] 234 for key in storage_body.keys(): 235 items.append((key, storage_body[key])) 236 return items 237 238 def get_body_fields(field_items, encoding): 239 240 """ 241 Returns a dictionary mapping field names to lists of field values for all 242 entries in the given 'field_items' (2-tuples of the form key, values) using 243 the given 'encoding'. 244 This is used in conjunction with FieldStorage objects. 245 """ 246 247 fields = {} 248 249 for field_name, field_values in field_items: 250 field_name = decode_value(field_name, encoding) 251 252 if type(field_values) == type([]): 253 fields[field_name] = [] 254 for field_value in field_values: 255 fields[field_name].append(get_body_field_or_file(field_value, encoding)) 256 else: 257 fields[field_name] = [get_body_field_or_file(field_values, encoding)] 258 259 return fields 260 261 def get_body_field_or_file(field_value, encoding): 262 263 """ 264 Returns the appropriate value for the given 'field_value' either for a 265 normal form field (thus employing the given 'encoding') or for a file 266 upload field (returning a plain string). 267 """ 268 269 if hasattr(field_value, "headers") and field_value.headers.has_key("content-type"): 270 271 # Detect stray FileUpload objects (eg. with Zope). 272 273 if hasattr(field_value, "read"): 274 return FileContent(field_value.read(), parse_headers(field_value.headers)) 275 else: 276 return FileContent(field_value.value, parse_headers(field_value.headers)) 277 else: 278 return get_body_field(field_value, encoding) 279 280 def get_body_field(field_str, encoding): 281 282 """ 283 Returns the appropriate value for the given 'field_str' string using the 284 given 'encoding'. 285 """ 286 287 # Detect stray FieldStorage objects (eg. with Webware). 288 289 if hasattr(field_str, "value"): 290 return get_body_field(field_str.value, encoding) 291 else: 292 return decode_value(field_str, encoding) 293 294 def decode_value(s, encoding): 295 if encoding is not None: 296 try: 297 return unicode(s, encoding) 298 except UnicodeError: 299 pass 300 # NOTE: Hacks to permit graceful failure. 301 return unicode(s, "iso-8859-1") 302 303 def get_fields_from_query_string(query_string, decoder): 304 305 """ 306 Returns a dictionary mapping field names to lists of values for the data 307 encoded in the given 'query_string'. Use the given 'decoder' function or 308 method to process the URL-encoded values. 309 """ 310 311 fields = {} 312 313 for pair in query_string.split("&"): 314 t = pair.split("=") 315 name = decoder(t[0]) 316 317 if len(t) == 2: 318 value = decoder(t[1]) 319 else: 320 value = "" 321 322 # NOTE: Remove empty names. 323 324 if name: 325 if not fields.has_key(name): 326 fields[name] = [] 327 fields[name].append(value) 328 329 return fields 330 331 def filter_fields(all_fields, fields_from_path): 332 333 """ 334 Taking items from the 'all_fields' dictionary, produce a new dictionary 335 which does not contain items from the 'fields_from_path' dictionary. 336 Return a new dictionary. 337 """ 338 339 fields = {} 340 for field_name, field_values in all_fields.items(): 341 342 # Find the path values for this field (for filtering below). 343 344 if fields_from_path.has_key(field_name): 345 field_from_path_values = fields_from_path[field_name] 346 if type(field_from_path_values) != type([]): 347 field_from_path_values = [field_from_path_values] 348 else: 349 field_from_path_values = [] 350 351 fields[field_name] = [] 352 for field_value in field_values: 353 354 # Filter path values. 355 356 if field_value not in field_from_path_values: 357 fields[field_name].append(field_value) 358 359 # Remove filtered fields. 360 361 if fields[field_name] == []: 362 del fields[field_name] 363 364 return fields 365 366 # vim: tabstop=4 expandtab shiftwidth=4