1 #!/usr/bin/env python 2 3 """ 4 Generic Web framework interfaces. 5 The WebStack architecture consists of the following layers: 6 7 * Framework - The underlying Web framework implementation. 8 * Adapter - Code operating under the particular framework which creates 9 WebStack abstractions and issues them to the application. 10 * Resources - Units of functionality operating within the hosted Web 11 application. 12 13 Resources can act as both content producers within an application and as request 14 dispatchers to other resources; in the latter role, they may be referred to as 15 directors. 16 """ 17 18 import urllib 19 from WebStack.Helpers.Request import Cookie 20 21 class EndOfResponse(Exception): 22 23 "An exception which signals the end of a response." 24 25 pass 26 27 class HeaderValue: 28 29 "A container for header information." 30 31 def __init__(self, principal_value, **attributes): 32 33 """ 34 Initialise the container with the given 'principal_value' and optional 35 keyword attributes representing the key=value pairs which accompany the 36 'principal_value'. 37 """ 38 39 self.principal_value = principal_value 40 self.attributes = attributes 41 42 def __getattr__(self, name): 43 if self.attributes.has_key(name): 44 return self.attributes[name] 45 else: 46 raise AttributeError, name 47 48 def __str__(self): 49 50 """ 51 Format the header value object, producing a string suitable for the 52 response header field. 53 """ 54 55 l = [] 56 if self.principal_value: 57 l.append(self.principal_value) 58 for name, value in self.attributes.items(): 59 l.append("; ") 60 l.append("%s=%s" % (name, value)) 61 62 # Make sure that only ASCII is used. 63 64 return "".join(l).encode("US-ASCII") 65 66 class ContentType(HeaderValue): 67 68 "A container for content type information." 69 70 def __init__(self, media_type, charset=None, **attributes): 71 72 """ 73 Initialise the container with the given 'media_type', an optional 74 'charset', and optional keyword attributes representing the key=value 75 pairs which qualify content types. 76 """ 77 78 if charset is not None: 79 attributes["charset"] = charset 80 HeaderValue.__init__(self, media_type, **attributes) 81 82 def __getattr__(self, name): 83 if name == "media_type": 84 return self.principal_value 85 elif name == "charset": 86 return self.attributes.get("charset") 87 elif self.attributes.has_key(name): 88 return self.attributes[name] 89 else: 90 raise AttributeError, name 91 92 class Transaction: 93 94 """ 95 A generic transaction interface containing framework-specific methods to be 96 overridden. 97 """ 98 99 # The default charset ties output together with body field interpretation. 100 101 default_charset = "iso-8859-1" 102 103 def commit(self): 104 105 """ 106 A special method, synchronising the transaction with framework-specific 107 objects. 108 """ 109 110 pass 111 112 # Utility methods. 113 114 def parse_header_value(self, header_class, header_value_str): 115 116 """ 117 Create an object of the given 'header_class' by determining the details 118 of the given 'header_value_str' - a string containing the value of a 119 particular header. 120 """ 121 122 if header_value_str is None: 123 return header_class(None) 124 125 l = header_value_str.split(";") 126 attributes = {} 127 128 # Find the attributes. 129 130 principal_value, attributes_str = l[0].strip(), l[1:] 131 132 for attribute_str in attributes_str: 133 t = attribute_str.split("=") 134 if len(t) > 1: 135 name, value = t[0].strip(), t[1].strip() 136 attributes[name] = value 137 138 return header_class(principal_value, **attributes) 139 140 def parse_content_type(self, content_type_field): 141 return self.parse_header_value(ContentType, content_type_field) 142 143 def format_header_value(self, value): 144 145 """ 146 Format the given header 'value'. Typically, this just ensures the usage 147 of US-ASCII. 148 """ 149 150 return value.encode("US-ASCII") 151 152 def encode_cookie_value(self, value): 153 154 """ 155 Encode the given cookie 'value'. This ensures the usage of US-ASCII 156 through the encoding of Unicode objects as URL-encoded UTF-8 text. 157 """ 158 159 return urllib.quote(value.encode("UTF-8")).encode("US-ASCII") 160 161 def decode_cookie_value(self, value): 162 163 """ 164 Decode the given cookie 'value'. 165 """ 166 167 return unicode(urllib.unquote(value), "UTF-8") 168 169 def process_cookies(self, cookie_dict, using_strings=0): 170 171 """ 172 Process the given 'cookie_dict', returning a dictionary mapping cookie names 173 to cookie objects where the names and values have been decoded from the form 174 used in the cookies retrieved from the request. 175 176 The optional 'using_strings', if set to 1, treats the 'cookie_dict' as a 177 mapping of cookie names to values. 178 """ 179 180 cookies = {} 181 for name in cookie_dict.keys(): 182 if using_strings: 183 value = cookie_dict[name] 184 else: 185 cookie = cookie_dict[name] 186 value = cookie.value 187 cookie_name = self.decode_cookie_value(name) 188 cookie_value = self.decode_cookie_value(value) 189 cookies[cookie_name] = Cookie(cookie_name, cookie_value) 190 return cookies 191 192 def parse_content_preferences(self, accept_preference): 193 194 """ 195 Returns the preferences as requested by the user agent. The preferences are 196 returned as a list of codes in the same order as they appeared in the 197 appropriate environment variable. In other words, the explicit weighting 198 criteria are ignored. 199 200 As the 'accept_preference' parameter, values for language and charset 201 preferences are appropriate. 202 """ 203 204 if accept_preference is None: 205 return [] 206 207 accept_defs = accept_preference.split(",") 208 accept_prefs = [] 209 for accept_def in accept_defs: 210 t = accept_def.split(";") 211 if len(t) >= 1: 212 accept_prefs.append(t[0].strip()) 213 return accept_prefs 214 215 def convert_to_list(self, value): 216 217 """ 218 Returns a single element list containing 'value' if it is not itself a list, a 219 tuple, or None. If 'value' is a list then it is itself returned; if 'value' is a 220 tuple then a new list containing the same elements is returned; if 'value' is None 221 then an empty list is returned. 222 """ 223 224 if type(value) == type([]): 225 return value 226 elif type(value) == type(()): 227 return list(value) 228 elif value is None: 229 return [] 230 else: 231 return [value] 232 233 # Request-related methods. 234 235 def get_request_stream(self): 236 237 """ 238 Returns the request stream for the transaction. 239 """ 240 241 raise NotImplementedError, "get_request_stream" 242 243 def get_request_method(self): 244 245 """ 246 Returns the request method. 247 """ 248 249 raise NotImplementedError, "get_request_method" 250 251 def get_headers(self): 252 253 """ 254 Returns all request headers as a dictionary-like object mapping header 255 names to values. 256 """ 257 258 raise NotImplementedError, "get_headers" 259 260 def get_header_values(self, key): 261 262 """ 263 Returns a list of all request header values associated with the given 264 'key'. Note that according to RFC 2616, 'key' is treated as a 265 case-insensitive string. 266 """ 267 268 raise NotImplementedError, "get_header_values" 269 270 def get_content_type(self): 271 272 """ 273 Returns the content type specified on the request, along with the 274 charset employed. 275 """ 276 277 raise NotImplementedError, "get_content_type" 278 279 def get_content_charsets(self): 280 281 """ 282 Returns the character set preferences. 283 """ 284 285 raise NotImplementedError, "get_content_charsets" 286 287 def get_content_languages(self): 288 289 """ 290 Returns extracted language information from the transaction. 291 """ 292 293 raise NotImplementedError, "get_content_languages" 294 295 def get_path(self): 296 297 """ 298 Returns the entire path from the request. 299 """ 300 301 raise NotImplementedError, "get_path" 302 303 def get_path_without_query(self): 304 305 """ 306 Returns the entire path from the request minus the query string. 307 """ 308 309 raise NotImplementedError, "get_path_without_query" 310 311 def get_path_info(self): 312 313 """ 314 Returns the "path info" (the part of the URL after the resource name 315 handling the current request) from the request. 316 """ 317 318 raise NotImplementedError, "get_path_info" 319 320 def get_query_string(self): 321 322 """ 323 Returns the query string from the path in the request. 324 """ 325 326 raise NotImplementedError, "get_query_string" 327 328 # Higher level request-related methods. 329 330 def get_fields_from_path(self): 331 332 """ 333 Extracts fields (or request parameters) from the path specified in the 334 transaction. The underlying framework may refuse to supply fields from 335 the path if handling a POST transaction. 336 337 Returns a dictionary mapping field names to lists of values (even if a 338 single value is associated with any given field name). 339 """ 340 341 raise NotImplementedError, "get_fields_from_path" 342 343 def get_fields_from_body(self, encoding=None): 344 345 """ 346 Extracts fields (or request parameters) from the message body in the 347 transaction. The optional 'encoding' parameter specifies the character 348 encoding of the message body for cases where no such information is 349 available, but where the default encoding is to be overridden. 350 351 Returns a dictionary mapping field names to lists of values (even if a 352 single value is associated with any given field name). Each value is 353 either a Unicode object (representing a simple form field, for example) 354 or a plain string (representing a file upload form field, for example). 355 """ 356 357 raise NotImplementedError, "get_fields_from_body" 358 359 def get_fields(self, encoding=None): 360 361 """ 362 Extracts fields (or request parameters) from both the path specified in 363 the transaction as well as the message body. The optional 'encoding' 364 parameter specifies the character encoding of the message body for cases 365 where no such information is available, but where the default encoding 366 is to be overridden. 367 368 Returns a dictionary mapping field names to lists of values (even if a 369 single value is associated with any given field name). Each value is 370 either a Unicode object (representing a simple form field, for example) 371 or a plain string (representing a file upload form field, for example). 372 373 Where a given field name is used in both the path and message body to 374 specify values, the values from both sources will be combined into a 375 single list associated with that field name. 376 """ 377 378 raise NotImplementedError, "get_fields" 379 380 def get_user(self): 381 382 """ 383 Extracts user information from the transaction. 384 385 Returns a username as a string or None if no user is defined. 386 """ 387 388 raise NotImplementedError, "get_user" 389 390 def get_cookies(self): 391 392 """ 393 Obtains cookie information from the request. 394 395 Returns a dictionary mapping cookie names to cookie objects. 396 """ 397 398 raise NotImplementedError, "get_cookies" 399 400 def get_cookie(self, cookie_name): 401 402 """ 403 Obtains cookie information from the request. 404 405 Returns a cookie object for the given 'cookie_name' or None if no such 406 cookie exists. 407 """ 408 409 raise NotImplementedError, "get_cookie" 410 411 # Response-related methods. 412 413 def get_response_stream(self): 414 415 """ 416 Returns the response stream for the transaction. 417 """ 418 419 raise NotImplementedError, "get_response_stream" 420 421 def get_response_stream_encoding(self): 422 423 """ 424 Returns the response stream encoding. 425 """ 426 427 raise NotImplementedError, "get_response_stream_encoding" 428 429 def get_response_code(self): 430 431 """ 432 Get the response code associated with the transaction. If no response 433 code is defined, None is returned. 434 """ 435 436 raise NotImplementedError, "get_response_code" 437 438 def set_response_code(self, response_code): 439 440 """ 441 Set the 'response_code' using a numeric constant defined in the HTTP 442 specification. 443 """ 444 445 raise NotImplementedError, "set_response_code" 446 447 def set_header_value(self, header, value): 448 449 """ 450 Set the HTTP 'header' with the given 'value'. 451 """ 452 453 raise NotImplementedError, "set_header_value" 454 455 def set_content_type(self, content_type): 456 457 """ 458 Sets the 'content_type' for the response. 459 """ 460 461 raise NotImplementedError, "set_content_type" 462 463 # Higher level response-related methods. 464 465 def set_cookie(self, cookie): 466 467 """ 468 Stores the given 'cookie' object in the response. 469 """ 470 471 raise NotImplementedError, "set_cookie" 472 473 def set_cookie_value(self, name, value, path=None, expires=None): 474 475 """ 476 Stores a cookie with the given 'name' and 'value' in the response. 477 478 The optional 'path' is a string which specifies the scope of the cookie, 479 and the optional 'expires' parameter is a value compatible with the 480 time.time function, and indicates the expiry date/time of the cookie. 481 """ 482 483 raise NotImplementedError, "set_cookie_value" 484 485 def delete_cookie(self, cookie_name): 486 487 """ 488 Adds to the response a request that the cookie with the given 489 'cookie_name' be deleted/discarded by the client. 490 """ 491 492 raise NotImplementedError, "delete_cookie" 493 494 # Session-related methods. 495 496 def get_session(self, create=1): 497 498 """ 499 Gets a session corresponding to an identifier supplied in the 500 transaction. 501 502 If no session has yet been established according to information 503 provided in the transaction then the optional 'create' parameter 504 determines whether a new session will be established. 505 506 Where no session has been established and where 'create' is set to 0 507 then None is returned. In all other cases, a session object is created 508 (where appropriate) and returned. 509 """ 510 511 raise NotImplementedError, "get_session" 512 513 def expire_session(self): 514 515 """ 516 Expires any session established according to information provided in the 517 transaction. 518 """ 519 520 raise NotImplementedError, "expire_session" 521 522 # Application-specific methods. 523 524 def set_user(self, username): 525 526 """ 527 An application-specific method which sets the user information with 528 'username' in the transaction. This affects subsequent calls to 529 'get_user'. 530 """ 531 532 self.user = username 533 534 def set_path_info(self, path_info): 535 536 """ 537 An application-specific method which sets the 'path_info' in the 538 transaction. This affects subsequent calls to 'get_path_info'. 539 """ 540 541 self.path_info = path_info 542 543 class Resource: 544 545 "A generic resource interface." 546 547 def respond(self, trans): 548 549 """ 550 An application-specific method which performs activities on the basis of 551 the transaction object 'trans'. 552 """ 553 554 raise NotImplementedError, "respond" 555 556 class Authenticator: 557 558 "A generic authentication component." 559 560 def authenticate(self, trans): 561 562 """ 563 An application-specific method which authenticates the sender of the 564 request described by the transaction object 'trans'. This method should 565 consider 'trans' to be read-only and not attempt to change the state of 566 the transaction. 567 568 If the sender of the request is authenticated successfully, the result 569 of this method evaluates to true; otherwise the result of this method 570 evaluates to false. 571 """ 572 573 raise NotImplementedError, "authenticate" 574 575 def get_auth_type(self): 576 577 """ 578 An application-specific method which returns the authentication type to 579 be used. An example value is 'Basic' which specifies HTTP basic 580 authentication. 581 """ 582 583 raise NotImplementedError, "get_auth_type" 584 585 def get_realm(self): 586 587 """ 588 An application-specific method which returns the name of the realm for 589 which authentication is taking place. 590 """ 591 592 raise NotImplementedError, "get_realm" 593 594 # vim: tabstop=4 expandtab shiftwidth=4