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