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): 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 * content - a plain string containing the contents of the uploaded file 180 * headers - a dictionary containing the headers associated with the 181 uploaded file 182 """ 183 184 def __init__(self, content, headers=None): 185 186 """ 187 Initialise the object with 'content' and optional 'headers' describing 188 the content. 189 """ 190 191 self.content = content 192 self.headers = headers or {} 193 194 def __str__(self): 195 return self.content 196 197 def parse_header_value(header_class, header_value_str): 198 199 """ 200 Create an object of the given 'header_class' by determining the details 201 of the given 'header_value_str' - a string containing the value of a 202 particular header. 203 """ 204 205 if header_value_str is None: 206 return header_class(None) 207 208 l = header_value_str.split(";") 209 attributes = {} 210 211 # Find the attributes. 212 213 principal_value, attributes_str = l[0].strip(), l[1:] 214 215 for attribute_str in attributes_str: 216 t = attribute_str.split("=") 217 if len(t) > 1: 218 name, value = t[0].strip(), t[1].strip() 219 attributes[name] = value 220 221 return header_class(principal_value, **attributes) 222 223 def parse_headers(headers): 224 225 """ 226 Parse the given 'headers' dictionary (containing names mapped to values), 227 returing a dictionary mapping names to HeaderValue objects. 228 """ 229 230 new_headers = {} 231 for name, value in headers.items(): 232 new_headers[name] = parse_header_value(HeaderValue, value) 233 return new_headers 234 235 def get_storage_items(storage_body): 236 237 """ 238 Return the items (2-tuples of the form key, values) from the 'storage_body'. 239 This is used in conjunction with FieldStorage objects. 240 """ 241 242 items = [] 243 for key in storage_body.keys(): 244 items.append((key, storage_body[key])) 245 return items 246 247 def get_body_fields(field_items, encoding): 248 249 """ 250 Returns a dictionary mapping field names to lists of field values for all 251 entries in the given 'field_items' (2-tuples of the form key, values) using 252 the given 'encoding'. 253 This is used in conjunction with FieldStorage objects. 254 """ 255 256 fields = {} 257 258 for field_name, field_values in field_items: 259 field_name = decode_value(field_name, encoding) 260 261 if type(field_values) == type([]): 262 fields[field_name] = [] 263 for field_value in field_values: 264 fields[field_name].append(get_body_field_or_file(field_value, encoding)) 265 else: 266 fields[field_name] = [get_body_field_or_file(field_values, encoding)] 267 268 return fields 269 270 def get_body_field_or_file(field_value, encoding): 271 272 """ 273 Returns the appropriate value for the given 'field_value' either for a 274 normal form field (thus employing the given 'encoding') or for a file 275 upload field (returning a plain string). 276 """ 277 278 if hasattr(field_value, "headers") and field_value.headers.has_key("content-type"): 279 280 # Detect stray FileUpload objects (eg. with Zope). 281 282 if hasattr(field_value, "read"): 283 return FileContent(field_value.read(), parse_headers(field_value.headers)) 284 else: 285 return FileContent(field_value.value, parse_headers(field_value.headers)) 286 else: 287 return get_body_field(field_value, encoding) 288 289 def get_body_field(field_str, encoding): 290 291 """ 292 Returns the appropriate value for the given 'field_str' string using the 293 given 'encoding'. 294 """ 295 296 # Detect stray FieldStorage objects (eg. with Webware). 297 298 if hasattr(field_str, "value"): 299 return get_body_field(field_str.value, encoding) 300 else: 301 return decode_value(field_str, encoding) 302 303 def decode_value(s, encoding): 304 if encoding is not None: 305 try: 306 return unicode(s, encoding) 307 except UnicodeError: 308 pass 309 # NOTE: Hacks to permit graceful failure. 310 return unicode(s, "iso-8859-1") 311 312 def get_fields_from_query_string(query_string, decoder): 313 314 """ 315 Returns a dictionary mapping field names to lists of values for the data 316 encoded in the given 'query_string'. Use the given 'decoder' function or 317 method to process the URL-encoded values. 318 """ 319 320 fields = {} 321 322 for pair in query_string.split("&"): 323 t = pair.split("=") 324 name = decoder(t[0]) 325 326 if len(t) == 2: 327 value = decoder(t[1]) 328 else: 329 value = "" 330 331 # NOTE: Remove empty names. 332 333 if name: 334 if not fields.has_key(name): 335 fields[name] = [] 336 fields[name].append(value) 337 338 return fields 339 340 def filter_fields(all_fields, fields_from_path): 341 342 """ 343 Taking items from the 'all_fields' dictionary, produce a new dictionary 344 which does not contain items from the 'fields_from_path' dictionary. 345 Return a new dictionary. 346 """ 347 348 fields = {} 349 for field_name, field_values in all_fields.items(): 350 351 # Find the path values for this field (for filtering below). 352 353 if fields_from_path.has_key(field_name): 354 field_from_path_values = fields_from_path[field_name] 355 if type(field_from_path_values) != type([]): 356 field_from_path_values = [field_from_path_values] 357 else: 358 field_from_path_values = [] 359 360 fields[field_name] = [] 361 for field_value in field_values: 362 363 # Filter path values. 364 365 if field_value not in field_from_path_values: 366 fields[field_name].append(field_value) 367 368 # Remove filtered fields. 369 370 if fields[field_name] == []: 371 del fields[field_name] 372 373 return fields 374 375 # vim: tabstop=4 expandtab shiftwidth=4