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