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