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