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 * headers - a dictionary containing the headers associated with the 232 uploaded file 233 """ 234 235 def __init__(self, stream, headers=None): 236 237 """ 238 Initialise the object with a 'stream' through which the file can be 239 read, along with optional 'headers' describing the content. 240 """ 241 242 self.stream = stream 243 self.headers = headers or HeaderDict() 244 self.cache = None 245 246 def __getattr__(self, name): 247 248 """ 249 Provides a property value when 'name' is specified as "content". 250 """ 251 252 if name != "content": 253 raise AttributeError, name 254 255 if self.cache is not None: 256 return self.cache 257 258 if self.reset(): 259 return self.stream.read() 260 else: 261 self.cache = self.stream.read() 262 return self.cache 263 264 def reset(self): 265 266 "Reset the stream providing the data, returning whether this succeeded." 267 268 # Python file objects. 269 270 if hasattr(self.stream, "seek"): 271 self.stream.seek(0) 272 return 1 273 274 # Java input streams. 275 276 elif hasattr(self.stream, "reset"): 277 self.stream.reset() 278 return 1 279 280 # Other streams. 281 282 else: 283 return 0 284 285 def __str__(self): 286 return self.content 287 288 def parse_header_value(header_class, header_value_str): 289 290 """ 291 Create an object of the given 'header_class' by determining the details 292 of the given 'header_value_str' - a string containing the value of a 293 particular header. 294 """ 295 296 if header_value_str is None: 297 return header_class(None) 298 299 l = header_value_str.split(";") 300 attributes = {} 301 302 # Find the attributes. 303 304 principal_value, attributes_str = l[0].strip(), l[1:] 305 306 for attribute_str in attributes_str: 307 t = attribute_str.split("=") 308 if len(t) > 1: 309 name, value = t[0].strip(), t[1].strip() 310 attributes[name] = value 311 312 return header_class(principal_value, **attributes) 313 314 def parse_headers(headers): 315 316 """ 317 Parse the given 'headers' dictionary (containing names mapped to values), 318 returing a dictionary mapping names to HeaderValue objects. 319 """ 320 321 new_headers = HeaderDict() 322 for name, value in headers.items(): 323 new_headers[name] = parse_header_value(HeaderValue, value) 324 return new_headers 325 326 def get_storage_items(storage_body): 327 328 """ 329 Return the items (2-tuples of the form key, values) from the 'storage_body'. 330 This is used in conjunction with FieldStorage objects. 331 """ 332 333 items = [] 334 for key in storage_body.keys(): 335 items.append((key, storage_body[key])) 336 return items 337 338 def get_body_fields(field_items, encoding): 339 340 """ 341 Returns a dictionary mapping field names to lists of field values for all 342 entries in the given 'field_items' (2-tuples of the form key, values) using 343 the given 'encoding'. 344 This is used in conjunction with FieldStorage objects. 345 """ 346 347 fields = {} 348 349 for field_name, field_values in field_items: 350 field_name = decode_value(field_name, encoding) 351 352 if type(field_values) == type([]): 353 fields[field_name] = [] 354 for field_value in field_values: 355 fields[field_name].append(get_body_field_or_file(field_value, encoding)) 356 else: 357 fields[field_name] = [get_body_field_or_file(field_values, encoding)] 358 359 return fields 360 361 def get_body_field_or_file(field_value, encoding): 362 363 """ 364 Returns the appropriate value for the given 'field_value' either for a 365 normal form field (thus employing the given 'encoding') or for a file 366 upload field (returning a plain string). 367 """ 368 369 if hasattr(field_value, "headers") and field_value.headers.has_key("content-type"): 370 371 # Detect stray FileUpload objects (eg. with Zope). 372 373 if hasattr(field_value, "read"): 374 return FileContent(field_value, parse_headers(field_value.headers)) 375 else: 376 return FileContent(field_value.file, parse_headers(field_value.headers)) 377 else: 378 return get_body_field(field_value, encoding) 379 380 def get_body_field(field_str, encoding): 381 382 """ 383 Returns the appropriate value for the given 'field_str' string using the 384 given 'encoding'. 385 """ 386 387 # Detect stray FieldStorage objects (eg. with Webware). 388 389 if hasattr(field_str, "value"): 390 return get_body_field(field_str.value, encoding) 391 else: 392 return decode_value(field_str, encoding) 393 394 def decode_value(s, encoding): 395 if encoding is not None: 396 try: 397 return unicode(s, encoding) 398 except UnicodeError: 399 pass 400 # NOTE: Hacks to permit graceful failure. 401 return unicode(s, "iso-8859-1") 402 403 def get_fields_from_query_string(query_string, decoder): 404 405 """ 406 Returns a dictionary mapping field names to lists of values for the data 407 encoded in the given 'query_string'. Use the given 'decoder' function or 408 method to process the URL-encoded values. 409 """ 410 411 fields = {} 412 413 for pair in query_string.split("&"): 414 t = pair.split("=") 415 name = decoder(t[0]) 416 417 if len(t) == 2: 418 value = decoder(t[1]) 419 else: 420 value = "" 421 422 # NOTE: Remove empty names. 423 424 if name: 425 if not fields.has_key(name): 426 fields[name] = [] 427 fields[name].append(value) 428 429 return fields 430 431 def filter_fields(all_fields, fields_from_path): 432 433 """ 434 Taking items from the 'all_fields' dictionary, produce a new dictionary 435 which does not contain items from the 'fields_from_path' dictionary. 436 Return a new dictionary. 437 """ 438 439 fields = {} 440 for field_name, field_values in all_fields.items(): 441 442 # Find the path values for this field (for filtering below). 443 444 if fields_from_path.has_key(field_name): 445 field_from_path_values = fields_from_path[field_name] 446 if type(field_from_path_values) != type([]): 447 field_from_path_values = [field_from_path_values] 448 else: 449 field_from_path_values = [] 450 451 fields[field_name] = [] 452 for field_value in field_values: 453 454 # Filter path values. 455 456 if field_value not in field_from_path_values: 457 fields[field_name].append(field_value) 458 459 # Remove filtered fields. 460 461 if fields[field_name] == []: 462 del fields[field_name] 463 464 return fields 465 466 # vim: tabstop=4 expandtab shiftwidth=4