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 # Server-related methods. 234 235 def get_server_name(self): 236 237 "Returns the server name." 238 239 raise NotImplementedError, "get_server_name" 240 241 def get_server_port(self): 242 243 "Returns the server port as a string." 244 245 raise NotImplementedError, "get_server_port" 246 247 # Request-related methods. 248 249 def get_request_stream(self): 250 251 """ 252 Returns the request stream for the transaction. 253 """ 254 255 raise NotImplementedError, "get_request_stream" 256 257 def get_request_method(self): 258 259 """ 260 Returns the request method. 261 """ 262 263 raise NotImplementedError, "get_request_method" 264 265 def get_headers(self): 266 267 """ 268 Returns all request headers as a dictionary-like object mapping header 269 names to values. 270 """ 271 272 raise NotImplementedError, "get_headers" 273 274 def get_header_values(self, key): 275 276 """ 277 Returns a list of all request header values associated with the given 278 'key'. Note that according to RFC 2616, 'key' is treated as a 279 case-insensitive string. 280 """ 281 282 raise NotImplementedError, "get_header_values" 283 284 def get_content_type(self): 285 286 """ 287 Returns the content type specified on the request, along with the 288 charset employed. 289 """ 290 291 raise NotImplementedError, "get_content_type" 292 293 def get_content_charsets(self): 294 295 """ 296 Returns the character set preferences. 297 """ 298 299 raise NotImplementedError, "get_content_charsets" 300 301 def get_content_languages(self): 302 303 """ 304 Returns extracted language information from the transaction. 305 """ 306 307 raise NotImplementedError, "get_content_languages" 308 309 def get_path(self): 310 311 """ 312 Returns the entire path from the request. 313 """ 314 315 raise NotImplementedError, "get_path" 316 317 def get_path_without_query(self): 318 319 """ 320 Returns the entire path from the request minus the query string. 321 """ 322 323 raise NotImplementedError, "get_path_without_query" 324 325 def get_path_info(self): 326 327 """ 328 Returns the "path info" (the part of the URL after the resource name 329 handling the current request) from the request. 330 """ 331 332 raise NotImplementedError, "get_path_info" 333 334 def get_query_string(self): 335 336 """ 337 Returns the query string from the path in the request. 338 """ 339 340 raise NotImplementedError, "get_query_string" 341 342 # Higher level request-related methods. 343 344 def get_fields_from_path(self): 345 346 """ 347 Extracts fields (or request parameters) from the path specified in the 348 transaction. The underlying framework may refuse to supply fields from 349 the path if handling a POST transaction. 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). 353 """ 354 355 raise NotImplementedError, "get_fields_from_path" 356 357 def get_fields_from_body(self, encoding=None): 358 359 """ 360 Extracts fields (or request parameters) from the message body in the 361 transaction. The optional 'encoding' parameter specifies the character 362 encoding of the message body for cases where no such information is 363 available, but where the default encoding is to be overridden. 364 365 Returns a dictionary mapping field names to lists of values (even if a 366 single value is associated with any given field name). Each value is 367 either a Unicode object (representing a simple form field, for example) 368 or a plain string (representing a file upload form field, for example). 369 """ 370 371 raise NotImplementedError, "get_fields_from_body" 372 373 def get_fields(self, encoding=None): 374 375 """ 376 Extracts fields (or request parameters) from both the path specified in 377 the transaction as well as the message body. The optional 'encoding' 378 parameter specifies the character encoding of the message body for cases 379 where no such information is available, but where the default encoding 380 is to be overridden. 381 382 Returns a dictionary mapping field names to lists of values (even if a 383 single value is associated with any given field name). Each value is 384 either a Unicode object (representing a simple form field, for example) 385 or a plain string (representing a file upload form field, for example). 386 387 Where a given field name is used in both the path and message body to 388 specify values, the values from both sources will be combined into a 389 single list associated with that field name. 390 """ 391 392 raise NotImplementedError, "get_fields" 393 394 def get_user(self): 395 396 """ 397 Extracts user information from the transaction. 398 399 Returns a username as a string or None if no user is defined. 400 """ 401 402 raise NotImplementedError, "get_user" 403 404 def get_cookies(self): 405 406 """ 407 Obtains cookie information from the request. 408 409 Returns a dictionary mapping cookie names to cookie objects. 410 """ 411 412 raise NotImplementedError, "get_cookies" 413 414 def get_cookie(self, cookie_name): 415 416 """ 417 Obtains cookie information from the request. 418 419 Returns a cookie object for the given 'cookie_name' or None if no such 420 cookie exists. 421 """ 422 423 raise NotImplementedError, "get_cookie" 424 425 # Response-related methods. 426 427 def get_response_stream(self): 428 429 """ 430 Returns the response stream for the transaction. 431 """ 432 433 raise NotImplementedError, "get_response_stream" 434 435 def get_response_stream_encoding(self): 436 437 """ 438 Returns the response stream encoding. 439 """ 440 441 raise NotImplementedError, "get_response_stream_encoding" 442 443 def get_response_code(self): 444 445 """ 446 Get the response code associated with the transaction. If no response 447 code is defined, None is returned. 448 """ 449 450 raise NotImplementedError, "get_response_code" 451 452 def set_response_code(self, response_code): 453 454 """ 455 Set the 'response_code' using a numeric constant defined in the HTTP 456 specification. 457 """ 458 459 raise NotImplementedError, "set_response_code" 460 461 def set_header_value(self, header, value): 462 463 """ 464 Set the HTTP 'header' with the given 'value'. 465 """ 466 467 raise NotImplementedError, "set_header_value" 468 469 def set_content_type(self, content_type): 470 471 """ 472 Sets the 'content_type' for the response. 473 """ 474 475 raise NotImplementedError, "set_content_type" 476 477 # Higher level response-related methods. 478 479 def set_cookie(self, cookie): 480 481 """ 482 Stores the given 'cookie' object in the response. 483 """ 484 485 raise NotImplementedError, "set_cookie" 486 487 def set_cookie_value(self, name, value, path=None, expires=None): 488 489 """ 490 Stores a cookie with the given 'name' and 'value' in the response. 491 492 The optional 'path' is a string which specifies the scope of the cookie, 493 and the optional 'expires' parameter is a value compatible with the 494 time.time function, and indicates the expiry date/time of the cookie. 495 """ 496 497 raise NotImplementedError, "set_cookie_value" 498 499 def delete_cookie(self, cookie_name): 500 501 """ 502 Adds to the response a request that the cookie with the given 503 'cookie_name' be deleted/discarded by the client. 504 """ 505 506 raise NotImplementedError, "delete_cookie" 507 508 # Session-related methods. 509 510 def get_session(self, create=1): 511 512 """ 513 Gets a session corresponding to an identifier supplied in the 514 transaction. 515 516 If no session has yet been established according to information 517 provided in the transaction then the optional 'create' parameter 518 determines whether a new session will be established. 519 520 Where no session has been established and where 'create' is set to 0 521 then None is returned. In all other cases, a session object is created 522 (where appropriate) and returned. 523 """ 524 525 raise NotImplementedError, "get_session" 526 527 def expire_session(self): 528 529 """ 530 Expires any session established according to information provided in the 531 transaction. 532 """ 533 534 raise NotImplementedError, "expire_session" 535 536 # Application-specific methods. 537 538 def set_user(self, username): 539 540 """ 541 An application-specific method which sets the user information with 542 'username' in the transaction. This affects subsequent calls to 543 'get_user'. 544 """ 545 546 self.user = username 547 548 def set_virtual_path_info(self, path_info): 549 550 """ 551 An application-specific method which sets the 'path_info' in the 552 transaction. This affects subsequent calls to 'get_virtual_path_info'. 553 """ 554 555 self.path_info = path_info 556 557 def get_virtual_path_info(self): 558 559 """ 560 An application-specific method which either returns path info set in the 561 'set_virtual_path_info' method, or the normal path info found in the 562 request. 563 """ 564 565 return self.path_info or self.get_path_info() 566 567 class Resource: 568 569 "A generic resource interface." 570 571 def respond(self, trans): 572 573 """ 574 An application-specific method which performs activities on the basis of 575 the transaction object 'trans'. 576 """ 577 578 raise NotImplementedError, "respond" 579 580 class Authenticator: 581 582 "A generic authentication component." 583 584 def authenticate(self, trans): 585 586 """ 587 An application-specific method which authenticates the sender of the 588 request described by the transaction object 'trans'. This method should 589 consider 'trans' to be read-only and not attempt to change the state of 590 the transaction. 591 592 If the sender of the request is authenticated successfully, the result 593 of this method evaluates to true; otherwise the result of this method 594 evaluates to false. 595 """ 596 597 raise NotImplementedError, "authenticate" 598 599 def get_auth_type(self): 600 601 """ 602 An application-specific method which returns the authentication type to 603 be used. An example value is 'Basic' which specifies HTTP basic 604 authentication. 605 """ 606 607 raise NotImplementedError, "get_auth_type" 608 609 def get_realm(self): 610 611 """ 612 An application-specific method which returns the name of the realm for 613 which authentication is taking place. 614 """ 615 616 raise NotImplementedError, "get_realm" 617 618 # vim: tabstop=4 expandtab shiftwidth=4