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