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 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. Instances of this class contain the following attributes: 160 161 * name - the name associated with the cookie 162 * value - the value retained by the cookie 163 """ 164 165 def __init__(self, name, value): 166 self.name = name 167 self.value = value 168 169 class FileContent: 170 171 """ 172 A simple class representing uploaded file content. This is useful in holding 173 metadata as well as being an indicator of such content in environments such 174 as Jython where it is not trivial to differentiate between plain strings and 175 Unicode in a fashion also applicable to CPython. 176 177 Instances of this class contain the following attributes: 178 179 * stream - a stream object through which the content of an uploaded file 180 may be accessed 181 * content - a plain string containing the contents of the uploaded file 182 * headers - a dictionary containing the headers associated with the 183 uploaded file 184 """ 185 186 def __init__(self, stream, headers=None): 187 188 """ 189 Initialise the object with a 'stream' through which the file can be 190 read, along with optional 'headers' describing the content. 191 """ 192 193 self.stream = stream 194 self.headers = headers or {} 195 self.cache = None 196 197 def __getattr__(self, name): 198 199 """ 200 Provides a property value when 'name' is specified as "content". 201 """ 202 203 if name != "content": 204 raise AttributeError, name 205 206 if self.cache is not None: 207 return self.cache 208 209 if self.reset(): 210 return self.stream.read() 211 else: 212 self.cache = self.stream.read() 213 return self.cache 214 215 def reset(self): 216 217 "Reset the stream providing the data, returning whether this succeeded." 218 219 # Python file objects. 220 221 if hasattr(self.stream, "seek"): 222 self.stream.seek(0) 223 return 1 224 225 # Java input streams. 226 227 elif hasattr(self.stream, "reset"): 228 self.stream.reset() 229 return 1 230 231 # Other streams. 232 233 else: 234 return 0 235 236 def __str__(self): 237 return self.content 238 239 def parse_header_value(header_class, header_value_str): 240 241 """ 242 Create an object of the given 'header_class' by determining the details 243 of the given 'header_value_str' - a string containing the value of a 244 particular header. 245 """ 246 247 if header_value_str is None: 248 return header_class(None) 249 250 l = header_value_str.split(";") 251 attributes = {} 252 253 # Find the attributes. 254 255 principal_value, attributes_str = l[0].strip(), l[1:] 256 257 for attribute_str in attributes_str: 258 t = attribute_str.split("=") 259 if len(t) > 1: 260 name, value = t[0].strip(), t[1].strip() 261 attributes[name] = value 262 263 return header_class(principal_value, **attributes) 264 265 def parse_headers(headers): 266 267 """ 268 Parse the given 'headers' dictionary (containing names mapped to values), 269 returing a dictionary mapping names to HeaderValue objects. 270 """ 271 272 new_headers = {} 273 for name, value in headers.items(): 274 new_headers[name] = parse_header_value(HeaderValue, value) 275 return new_headers 276 277 def get_storage_items(storage_body): 278 279 """ 280 Return the items (2-tuples of the form key, values) from the 'storage_body'. 281 This is used in conjunction with FieldStorage objects. 282 """ 283 284 items = [] 285 for key in storage_body.keys(): 286 items.append((key, storage_body[key])) 287 return items 288 289 def get_body_fields(field_items, encoding): 290 291 """ 292 Returns a dictionary mapping field names to lists of field values for all 293 entries in the given 'field_items' (2-tuples of the form key, values) using 294 the given 'encoding'. 295 This is used in conjunction with FieldStorage objects. 296 """ 297 298 fields = {} 299 300 for field_name, field_values in field_items: 301 field_name = decode_value(field_name, encoding) 302 303 if type(field_values) == type([]): 304 fields[field_name] = [] 305 for field_value in field_values: 306 fields[field_name].append(get_body_field_or_file(field_value, encoding)) 307 else: 308 fields[field_name] = [get_body_field_or_file(field_values, encoding)] 309 310 return fields 311 312 def get_body_field_or_file(field_value, encoding): 313 314 """ 315 Returns the appropriate value for the given 'field_value' either for a 316 normal form field (thus employing the given 'encoding') or for a file 317 upload field (returning a plain string). 318 """ 319 320 if hasattr(field_value, "headers") and field_value.headers.has_key("content-type"): 321 322 # Detect stray FileUpload objects (eg. with Zope). 323 324 if hasattr(field_value, "read"): 325 return FileContent(field_value, parse_headers(field_value.headers)) 326 else: 327 return FileContent(field_value.file, parse_headers(field_value.headers)) 328 else: 329 return get_body_field(field_value, encoding) 330 331 def get_body_field(field_str, encoding): 332 333 """ 334 Returns the appropriate value for the given 'field_str' string using the 335 given 'encoding'. 336 """ 337 338 # Detect stray FieldStorage objects (eg. with Webware). 339 340 if hasattr(field_str, "value"): 341 return get_body_field(field_str.value, encoding) 342 else: 343 return decode_value(field_str, encoding) 344 345 def decode_value(s, encoding): 346 if encoding is not None: 347 try: 348 return unicode(s, encoding) 349 except UnicodeError: 350 pass 351 # NOTE: Hacks to permit graceful failure. 352 return unicode(s, "iso-8859-1") 353 354 def get_fields_from_query_string(query_string, decoder): 355 356 """ 357 Returns a dictionary mapping field names to lists of values for the data 358 encoded in the given 'query_string'. Use the given 'decoder' function or 359 method to process the URL-encoded values. 360 """ 361 362 fields = {} 363 364 for pair in query_string.split("&"): 365 t = pair.split("=") 366 name = decoder(t[0]) 367 368 if len(t) == 2: 369 value = decoder(t[1]) 370 else: 371 value = "" 372 373 # NOTE: Remove empty names. 374 375 if name: 376 if not fields.has_key(name): 377 fields[name] = [] 378 fields[name].append(value) 379 380 return fields 381 382 def filter_fields(all_fields, fields_from_path): 383 384 """ 385 Taking items from the 'all_fields' dictionary, produce a new dictionary 386 which does not contain items from the 'fields_from_path' dictionary. 387 Return a new dictionary. 388 """ 389 390 fields = {} 391 for field_name, field_values in all_fields.items(): 392 393 # Find the path values for this field (for filtering below). 394 395 if fields_from_path.has_key(field_name): 396 field_from_path_values = fields_from_path[field_name] 397 if type(field_from_path_values) != type([]): 398 field_from_path_values = [field_from_path_values] 399 else: 400 field_from_path_values = [] 401 402 fields[field_name] = [] 403 for field_value in field_values: 404 405 # Filter path values. 406 407 if field_value not in field_from_path_values: 408 fields[field_name].append(field_value) 409 410 # Remove filtered fields. 411 412 if fields[field_name] == []: 413 del fields[field_name] 414 415 return fields 416 417 # vim: tabstop=4 expandtab shiftwidth=4