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