1 #!/usr/bin/env python 2 3 """ 4 Request helper classes. 5 6 Copyright (C) 2004, 2005, 2006 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, n=None): 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 HeaderDict: 91 92 "A dictionary for headers." 93 94 def __init__(self, headers=None): 95 self.headers = {} 96 if headers is not None: 97 self.update(headers) 98 99 # Lower-case-string-coercing methods. 100 101 def __getitem__(self, key): 102 return self.headers[str(key).lower()] 103 104 def __setitem__(self, key, value): 105 self.headers[str(key).lower()] = value 106 107 def get(self, key, default=None): 108 return self.headers.get(str(key).lower(), default) 109 110 def has_key(self, key): 111 return self.headers.has_key(str(key).lower()) 112 113 # Forwarding methods. 114 115 def keys(self): 116 return self.headers.keys() 117 118 def values(self): 119 return self.headers.values() 120 121 def items(self): 122 return self.headers.items() 123 124 # Derived from the above. 125 126 def __contains__(self, key): 127 return self.has_key(key) 128 129 def update(self, other): 130 for k, v in other.items(): 131 self[k] = v 132 133 def __repr__(self): 134 return "HeaderDict(%r)" % self.headers 135 136 class HeaderValue: 137 138 "A container for header information." 139 140 def __init__(self, principal_value, **attributes): 141 142 """ 143 Initialise the container with the given 'principal_value' and optional 144 keyword attributes representing the key=value pairs which accompany the 145 'principal_value'. 146 """ 147 148 self.principal_value = principal_value 149 self.attributes = attributes 150 151 def __getattr__(self, name): 152 if self.attributes.has_key(name): 153 return self.attributes[name] 154 else: 155 raise AttributeError, name 156 157 def __repr__(self): 158 return "HeaderValue(%r)" % str(self) 159 160 def __str__(self): 161 162 """ 163 Format the header value object, producing a string suitable for the 164 response header field. 165 """ 166 167 l = [] 168 if self.principal_value: 169 l.append(self.principal_value) 170 for name, value in self.attributes.items(): 171 l.append("; ") 172 l.append("%s=%s" % (name, value)) 173 174 # Make sure that only ASCII is used. 175 176 return "".join(l).encode("US-ASCII") 177 178 class ContentType(HeaderValue): 179 180 "A container for content type information." 181 182 def __init__(self, media_type, charset=None, **attributes): 183 184 """ 185 Initialise the container with the given 'media_type', an optional 186 'charset', and optional keyword attributes representing the key=value 187 pairs which qualify content types. 188 """ 189 190 if charset is not None: 191 attributes["charset"] = charset 192 HeaderValue.__init__(self, media_type, **attributes) 193 194 def __getattr__(self, name): 195 if name == "media_type": 196 return self.principal_value 197 elif name == "charset": 198 return self.attributes.get("charset") 199 elif self.attributes.has_key(name): 200 return self.attributes[name] 201 else: 202 raise AttributeError, name 203 204 class Cookie: 205 206 """ 207 A simple cookie class for frameworks which do not return cookies in 208 structured form. Instances of this class contain the following attributes: 209 210 * name - the name associated with the cookie 211 * value - the value retained by the cookie 212 """ 213 214 def __init__(self, name, value): 215 self.name = name 216 self.value = value 217 218 class FileContent: 219 220 """ 221 A simple class representing uploaded file content. This is useful in holding 222 metadata as well as being an indicator of such content in environments such 223 as Jython where it is not trivial to differentiate between plain strings and 224 Unicode in a fashion also applicable to CPython. 225 226 Instances of this class contain the following attributes: 227 228 * stream - a stream object through which the content of an uploaded file 229 may be accessed 230 * content - a plain string containing the contents of the uploaded file 231 * filename - a plain string containing the supplied filename of the 232 uploaded file 233 * headers - a dictionary containing the headers associated with the 234 uploaded file 235 """ 236 237 def __init__(self, stream, headers=None): 238 239 """ 240 Initialise the object with a 'stream' through which the file can be 241 read, along with optional 'headers' describing the content. 242 """ 243 244 self.stream = stream 245 self.headers = headers or HeaderDict() 246 self.cache = None 247 248 def __getattr__(self, name): 249 250 """ 251 Provides a property value when 'name' is specified as "content" or as 252 "filename". 253 """ 254 255 if name == "content": 256 257 if self.cache is not None: 258 return self.cache 259 260 if self.reset(): 261 return self.stream.read() 262 else: 263 self.cache = self.stream.read() 264 return self.cache 265 266 elif name == "filename": 267 try: 268 content_disposition = self.headers["Content-Disposition"] 269 return content_disposition.filename 270 except (KeyError, AttributeError): 271 return None 272 273 else: 274 raise AttributeError, name 275 276 def reset(self): 277 278 "Reset the stream providing the data, returning whether this succeeded." 279 280 # Python file objects. 281 282 if hasattr(self.stream, "seek"): 283 self.stream.seek(0) 284 return 1 285 286 # Java input streams. 287 288 elif hasattr(self.stream, "reset"): 289 self.stream.reset() 290 return 1 291 292 # Other streams. 293 294 else: 295 return 0 296 297 def __str__(self): 298 return self.content 299 300 def parse_header_value(header_class, header_value_str): 301 302 """ 303 Create an object of the given 'header_class' by determining the details 304 of the given 'header_value_str' - a string containing the value of a 305 particular header. 306 """ 307 308 if header_value_str is None: 309 return header_class(None) 310 311 l = header_value_str.split(";") 312 attributes = {} 313 314 # Find the attributes. 315 316 principal_value, attributes_str = l[0].strip(), l[1:] 317 318 for attribute_str in attributes_str: 319 t = attribute_str.split("=") 320 if len(t) > 1: 321 name, value = t[0].strip(), t[1].strip() 322 attributes[name] = value 323 324 return header_class(principal_value, **attributes) 325 326 def parse_headers(headers): 327 328 """ 329 Parse the given 'headers' dictionary (containing names mapped to values), 330 returing a dictionary mapping names to HeaderValue objects. 331 """ 332 333 new_headers = HeaderDict() 334 for name, value in headers.items(): 335 new_headers[name] = parse_header_value(HeaderValue, value) 336 return new_headers 337 338 def get_storage_items(storage_body): 339 340 """ 341 Return the items (2-tuples of the form key, values) from the 'storage_body'. 342 This is used in conjunction with FieldStorage objects. 343 """ 344 345 items = [] 346 for key in storage_body.keys(): 347 items.append((key, storage_body[key])) 348 return items 349 350 def get_body_fields(field_items, encoding): 351 352 """ 353 Returns a dictionary mapping field names to lists of field values for all 354 entries in the given 'field_items' (2-tuples of the form key, values) using 355 the given 'encoding'. 356 This is used in conjunction with FieldStorage objects. 357 """ 358 359 fields = {} 360 361 for field_name, field_values in field_items: 362 field_name = decode_value(field_name, encoding) 363 364 if type(field_values) == type([]): 365 fields[field_name] = [] 366 for field_value in field_values: 367 fields[field_name].append(get_body_field_or_file(field_value, encoding)) 368 else: 369 fields[field_name] = [get_body_field_or_file(field_values, encoding)] 370 371 return fields 372 373 def get_body_field_or_file(field_value, encoding): 374 375 """ 376 Returns the appropriate value for the given 'field_value' either for a 377 normal form field (thus employing the given 'encoding') or for a file 378 upload field (returning a plain string). 379 """ 380 381 if hasattr(field_value, "headers") and field_value.headers.has_key("content-type"): 382 383 # Detect stray FileUpload objects (eg. with Zope). 384 385 if hasattr(field_value, "read"): 386 return FileContent(field_value, parse_headers(field_value.headers)) 387 else: 388 return FileContent(field_value.file, parse_headers(field_value.headers)) 389 else: 390 return get_body_field(field_value, encoding) 391 392 def get_body_field(field_str, encoding): 393 394 """ 395 Returns the appropriate value for the given 'field_str' string using the 396 given 'encoding'. 397 """ 398 399 # Detect stray FieldStorage objects (eg. with Webware). 400 401 if hasattr(field_str, "value"): 402 return get_body_field(field_str.value, encoding) 403 else: 404 return decode_value(field_str, encoding) 405 406 def decode_value(s, encoding): 407 if encoding is not None: 408 try: 409 return unicode(s, encoding) 410 except UnicodeError: 411 pass 412 # NOTE: Hacks to permit graceful failure. 413 return unicode(s, "iso-8859-1") 414 415 def get_fields_from_query_string(query_string, decoder): 416 417 """ 418 Returns a dictionary mapping field names to lists of values for the data 419 encoded in the given 'query_string'. Use the given 'decoder' function or 420 method to process the URL-encoded values. 421 """ 422 423 fields = {} 424 425 for pair in query_string.split("&"): 426 t = pair.split("=") 427 name = decoder(t[0]) 428 429 if len(t) == 2: 430 value = decoder(t[1]) 431 else: 432 value = "" 433 434 # NOTE: Remove empty names. 435 436 if name: 437 if not fields.has_key(name): 438 fields[name] = [] 439 fields[name].append(value) 440 441 return fields 442 443 def filter_fields(all_fields, fields_from_path): 444 445 """ 446 Taking items from the 'all_fields' dictionary, produce a new dictionary 447 which does not contain items from the 'fields_from_path' dictionary. 448 Return a new dictionary. 449 """ 450 451 fields = {} 452 for field_name, field_values in all_fields.items(): 453 454 # Find the path values for this field (for filtering below). 455 456 if fields_from_path.has_key(field_name): 457 field_from_path_values = fields_from_path[field_name] 458 if type(field_from_path_values) != type([]): 459 field_from_path_values = [field_from_path_values] 460 else: 461 field_from_path_values = [] 462 463 fields[field_name] = [] 464 for field_value in field_values: 465 466 # Filter path values. 467 468 if field_value not in field_from_path_values: 469 fields[field_name].append(field_value) 470 471 # Remove filtered fields. 472 473 if fields[field_name] == []: 474 del fields[field_name] 475 476 return fields 477 478 # vim: tabstop=4 expandtab shiftwidth=4