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): 369 370 """ 371 Returns the entire path from the request. 372 """ 373 374 raise NotImplementedError, "get_path" 375 376 def get_path_without_query(self): 377 378 """ 379 Returns the entire path from the request minus the query string. 380 """ 381 382 raise NotImplementedError, "get_path_without_query" 383 384 def get_path_info(self): 385 386 """ 387 Returns the "path info" (the part of the URL after the resource name 388 handling the current request) from the request. 389 """ 390 391 raise NotImplementedError, "get_path_info" 392 393 def get_query_string(self): 394 395 """ 396 Returns the query string from the path in the request. 397 """ 398 399 raise NotImplementedError, "get_query_string" 400 401 # Higher level request-related methods. 402 403 def get_fields_from_path(self, encoding=None): 404 405 """ 406 Extracts fields (or request parameters) from the path specified in the 407 transaction. The underlying framework may refuse to supply fields from 408 the path if handling a POST transaction. The optional 'encoding' 409 parameter specifies the character encoding of the query string for cases 410 where the default encoding is to be overridden. 411 412 Returns a dictionary mapping field names to lists of values (even if a 413 single value is associated with any given field name). 414 """ 415 416 raise NotImplementedError, "get_fields_from_path" 417 418 def get_fields_from_body(self, encoding=None): 419 420 """ 421 Extracts fields (or request parameters) from the message body in the 422 transaction. The optional 'encoding' parameter specifies the character 423 encoding of the message body for cases where no such information is 424 available, but where the default encoding is to be overridden. 425 426 Returns a dictionary mapping field names to lists of values (even if a 427 single value is associated with any given field name). Each value is 428 either a Unicode object (representing a simple form field, for example) 429 or a plain string (representing a file upload form field, for example). 430 """ 431 432 raise NotImplementedError, "get_fields_from_body" 433 434 def get_fields(self, encoding=None): 435 436 """ 437 Extracts fields (or request parameters) from both the path specified in 438 the transaction as well as the message body. The optional 'encoding' 439 parameter specifies the character encoding of the message body for cases 440 where no such information is available, but where the default encoding 441 is to be overridden. 442 443 Returns a dictionary mapping field names to lists of values (even if a 444 single value is associated with any given field name). Each value is 445 either a Unicode object (representing a simple form field, for example) 446 or a plain string (representing a file upload form field, for example). 447 448 Where a given field name is used in both the path and message body to 449 specify values, the values from both sources will be combined into a 450 single list associated with that field name. 451 """ 452 453 raise NotImplementedError, "get_fields" 454 455 def get_user(self): 456 457 """ 458 Extracts user information from the transaction. 459 460 Returns a username as a string or None if no user is defined. 461 """ 462 463 raise NotImplementedError, "get_user" 464 465 def get_cookies(self): 466 467 """ 468 Obtains cookie information from the request. 469 470 Returns a dictionary mapping cookie names to cookie objects. 471 """ 472 473 raise NotImplementedError, "get_cookies" 474 475 def get_cookie(self, cookie_name): 476 477 """ 478 Obtains cookie information from the request. 479 480 Returns a cookie object for the given 'cookie_name' or None if no such 481 cookie exists. 482 """ 483 484 raise NotImplementedError, "get_cookie" 485 486 # Response-related methods. 487 488 def get_response_stream(self): 489 490 """ 491 Returns the response stream for the transaction. 492 """ 493 494 raise NotImplementedError, "get_response_stream" 495 496 def get_response_stream_encoding(self): 497 498 """ 499 Returns the response stream encoding. 500 """ 501 502 raise NotImplementedError, "get_response_stream_encoding" 503 504 def get_response_code(self): 505 506 """ 507 Get the response code associated with the transaction. If no response 508 code is defined, None is returned. 509 """ 510 511 raise NotImplementedError, "get_response_code" 512 513 def set_response_code(self, response_code): 514 515 """ 516 Set the 'response_code' using a numeric constant defined in the HTTP 517 specification. 518 """ 519 520 raise NotImplementedError, "set_response_code" 521 522 def set_header_value(self, header, value): 523 524 """ 525 Set the HTTP 'header' with the given 'value'. 526 """ 527 528 raise NotImplementedError, "set_header_value" 529 530 def set_content_type(self, content_type): 531 532 """ 533 Sets the 'content_type' for the response. 534 """ 535 536 raise NotImplementedError, "set_content_type" 537 538 # Higher level response-related methods. 539 540 def set_cookie(self, cookie): 541 542 """ 543 Stores the given 'cookie' object in the response. 544 """ 545 546 raise NotImplementedError, "set_cookie" 547 548 def set_cookie_value(self, name, value, path=None, expires=None): 549 550 """ 551 Stores a cookie with the given 'name' and 'value' in the response. 552 553 The optional 'path' is a string which specifies the scope of the cookie, 554 and the optional 'expires' parameter is a value compatible with the 555 time.time function, and indicates the expiry date/time of the cookie. 556 """ 557 558 raise NotImplementedError, "set_cookie_value" 559 560 def delete_cookie(self, cookie_name): 561 562 """ 563 Adds to the response a request that the cookie with the given 564 'cookie_name' be deleted/discarded by the client. 565 """ 566 567 raise NotImplementedError, "delete_cookie" 568 569 # Session-related methods. 570 571 def get_session(self, create=1): 572 573 """ 574 Gets a session corresponding to an identifier supplied in the 575 transaction. 576 577 If no session has yet been established according to information 578 provided in the transaction then the optional 'create' parameter 579 determines whether a new session will be established. 580 581 Where no session has been established and where 'create' is set to 0 582 then None is returned. In all other cases, a session object is created 583 (where appropriate) and returned. 584 """ 585 586 raise NotImplementedError, "get_session" 587 588 def expire_session(self): 589 590 """ 591 Expires any session established according to information provided in the 592 transaction. 593 """ 594 595 raise NotImplementedError, "expire_session" 596 597 # Application-specific methods. 598 599 def set_user(self, username): 600 601 """ 602 An application-specific method which sets the user information with 603 'username' in the transaction. This affects subsequent calls to 604 'get_user'. 605 """ 606 607 self.user = username 608 609 def set_virtual_path_info(self, path_info): 610 611 """ 612 An application-specific method which sets the 'path_info' in the 613 transaction. This affects subsequent calls to 'get_virtual_path_info'. 614 615 Note that the virtual path info should either be an empty string, or it 616 should begin with "/" and then (optionally) include other details. 617 Virtual path info strings which omit the leading "/" - ie. containing 618 things like "xxx" or even "xxx/yyy" - do not really make sense and may 619 not be handled correctly by various WebStack components. 620 """ 621 622 self.path_info = path_info 623 624 def get_virtual_path_info(self): 625 626 """ 627 An application-specific method which either returns path info set in the 628 'set_virtual_path_info' method, or the normal path info found in the 629 request. 630 """ 631 632 if self.path_info is not None: 633 return self.path_info 634 else: 635 return self.get_path_info() 636 637 def get_processed_virtual_path_info(self): 638 639 """ 640 An application-specific method which returns the virtual path info that 641 is considered "processed"; that is, the part of the path info which is 642 not included in the virtual path info. 643 644 Where the virtual path info is identical to the path info, an empty 645 string is returned. 646 647 Where the virtual path info is a substring of the path info, the path 648 info preceding that substring is returned. 649 650 Where the virtual path info is either an empty string or not a substring 651 of the path info, the entire path info is returned. 652 """ 653 654 real_path_info = self.get_path_info() 655 virtual_path_info = self.get_virtual_path_info() 656 657 if virtual_path_info == "": 658 return real_path_info 659 660 i = real_path_info.find(virtual_path_info) 661 if i == -1: 662 return real_path_info 663 else: 664 return real_path_info[:i] 665 666 class Resource: 667 668 "A generic resource interface." 669 670 def respond(self, trans): 671 672 """ 673 An application-specific method which performs activities on the basis of 674 the transaction object 'trans'. 675 """ 676 677 raise NotImplementedError, "respond" 678 679 class Authenticator: 680 681 "A generic authentication component." 682 683 def authenticate(self, trans): 684 685 """ 686 An application-specific method which authenticates the sender of the 687 request described by the transaction object 'trans'. This method should 688 consider 'trans' to be read-only and not attempt to change the state of 689 the transaction. 690 691 If the sender of the request is authenticated successfully, the result 692 of this method evaluates to true; otherwise the result of this method 693 evaluates to false. 694 """ 695 696 raise NotImplementedError, "authenticate" 697 698 def get_auth_type(self): 699 700 """ 701 An application-specific method which returns the authentication type to 702 be used. An example value is 'Basic' which specifies HTTP basic 703 authentication. 704 """ 705 706 raise NotImplementedError, "get_auth_type" 707 708 def get_realm(self): 709 710 """ 711 An application-specific method which returns the name of the realm for 712 which authentication is taking place. 713 """ 714 715 raise NotImplementedError, "get_realm" 716 717 # vim: tabstop=4 expandtab shiftwidth=4