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