1 #!/usr/bin/env python 2 3 """ 4 Generic Web framework interfaces. 5 The WebStack architecture consists of the following layers: 6 7 * Framework - The underlying Web framework implementation. 8 * Adapter - Code operating under the particular framework which creates 9 WebStack abstractions and issues them to the application. 10 * Resources - Units of functionality operating within the hosted Web 11 application. 12 13 Resources can act as both content producers within an application and as request 14 dispatchers to other resources; in the latter role, they may be referred to as 15 directors. 16 """ 17 18 import urllib 19 from WebStack.Helpers.Request import Cookie 20 21 class EndOfResponse(Exception): 22 23 "An exception which signals the end of a response." 24 25 pass 26 27 class HeaderValue: 28 29 "A container for header information." 30 31 def __init__(self, principal_value, **attributes): 32 33 """ 34 Initialise the container with the given 'principal_value' and optional 35 keyword attributes representing the key=value pairs which accompany the 36 'principal_value'. 37 """ 38 39 self.principal_value = principal_value 40 self.attributes = attributes 41 42 def __getattr__(self, name): 43 if self.attributes.has_key(name): 44 return self.attributes[name] 45 else: 46 raise AttributeError, name 47 48 def __str__(self): 49 50 """ 51 Format the header value object, producing a string suitable for the 52 response header field. 53 """ 54 55 l = [] 56 if self.principal_value: 57 l.append(self.principal_value) 58 for name, value in self.attributes.items(): 59 l.append("; ") 60 l.append("%s=%s" % (name, value)) 61 62 # Make sure that only ASCII is used. 63 64 return "".join(l).encode("US-ASCII") 65 66 class ContentType(HeaderValue): 67 68 "A container for content type information." 69 70 def __init__(self, media_type, charset=None, **attributes): 71 72 """ 73 Initialise the container with the given 'media_type', an optional 74 'charset', and optional keyword attributes representing the key=value 75 pairs which qualify content types. 76 """ 77 78 if charset is not None: 79 attributes["charset"] = charset 80 HeaderValue.__init__(self, media_type, **attributes) 81 82 def __getattr__(self, name): 83 if name == "media_type": 84 return self.principal_value 85 elif name == "charset": 86 return self.attributes.get("charset") 87 elif self.attributes.has_key(name): 88 return self.attributes[name] 89 else: 90 raise AttributeError, name 91 92 class Transaction: 93 94 """ 95 A generic transaction interface containing framework-specific methods to be 96 overridden. 97 """ 98 99 # The default charset ties output together with body field interpretation. 100 101 default_charset = "iso-8859-1" 102 103 def commit(self): 104 105 """ 106 A special method, synchronising the transaction with framework-specific 107 objects. 108 """ 109 110 pass 111 112 # Utility methods. 113 114 def parse_header_value(self, header_class, header_value_str): 115 116 """ 117 Create an object of the given 'header_class' by determining the details 118 of the given 'header_value_str' - a string containing the value of a 119 particular header. 120 """ 121 122 if header_value_str is None: 123 return header_class(None) 124 125 l = header_value_str.split(";") 126 attributes = {} 127 128 # Find the attributes. 129 130 principal_value, attributes_str = l[0].strip(), l[1:] 131 132 for attribute_str in attributes_str: 133 t = attribute_str.split("=") 134 if len(t) > 1: 135 name, value = t[0].strip(), t[1].strip() 136 attributes[name] = value 137 138 return header_class(principal_value, **attributes) 139 140 def parse_content_type(self, content_type_field): 141 142 """ 143 Parse the given 'content_type_field' - a value found comparable to that 144 found in an HTTP request header for "Content-Type". 145 """ 146 147 return self.parse_header_value(ContentType, content_type_field) 148 149 def format_header_value(self, value): 150 151 """ 152 Format the given header 'value'. Typically, this just ensures the usage 153 of US-ASCII. 154 """ 155 156 return value.encode("US-ASCII") 157 158 def encode_cookie_value(self, value): 159 160 """ 161 Encode the given cookie 'value'. This ensures the usage of US-ASCII 162 through the encoding of Unicode objects as URL-encoded UTF-8 text. 163 """ 164 165 return urllib.quote(value.encode("UTF-8")).encode("US-ASCII") 166 167 def decode_cookie_value(self, value): 168 169 """ 170 Decode the given cookie 'value'. 171 """ 172 173 return unicode(urllib.unquote(value), "UTF-8") 174 175 def process_cookies(self, cookie_dict, using_strings=0): 176 177 """ 178 Process the given 'cookie_dict', returning a dictionary mapping cookie names 179 to cookie objects where the names and values have been decoded from the form 180 used in the cookies retrieved from the request. 181 182 The optional 'using_strings', if set to 1, treats the 'cookie_dict' as a 183 mapping of cookie names to values. 184 """ 185 186 cookies = {} 187 for name in cookie_dict.keys(): 188 if using_strings: 189 value = cookie_dict[name] 190 else: 191 cookie = cookie_dict[name] 192 value = cookie.value 193 cookie_name = self.decode_cookie_value(name) 194 cookie_value = self.decode_cookie_value(value) 195 cookies[cookie_name] = Cookie(cookie_name, cookie_value) 196 return cookies 197 198 def parse_content_preferences(self, accept_preference): 199 200 """ 201 Returns the preferences as requested by the user agent. The preferences are 202 returned as a list of codes in the same order as they appeared in the 203 appropriate environment variable. In other words, the explicit weighting 204 criteria are ignored. 205 206 As the 'accept_preference' parameter, values for language and charset 207 preferences are appropriate. 208 """ 209 210 if accept_preference is None: 211 return [] 212 213 accept_defs = accept_preference.split(",") 214 accept_prefs = [] 215 for accept_def in accept_defs: 216 t = accept_def.split(";") 217 if len(t) >= 1: 218 accept_prefs.append(t[0].strip()) 219 return accept_prefs 220 221 def convert_to_list(self, value): 222 223 """ 224 Returns a single element list containing 'value' if it is not itself a list, a 225 tuple, or None. If 'value' is a list then it is itself returned; if 'value' is a 226 tuple then a new list containing the same elements is returned; if 'value' is None 227 then an empty list is returned. 228 """ 229 230 if type(value) == type([]): 231 return value 232 elif type(value) == type(()): 233 return list(value) 234 elif value is None: 235 return [] 236 else: 237 return [value] 238 239 # Server-related methods. 240 241 def get_server_name(self): 242 243 "Returns the server name." 244 245 raise NotImplementedError, "get_server_name" 246 247 def get_server_port(self): 248 249 "Returns the server port as a string." 250 251 raise NotImplementedError, "get_server_port" 252 253 # Request-related methods. 254 255 def get_request_stream(self): 256 257 """ 258 Returns the request stream for the transaction. 259 """ 260 261 raise NotImplementedError, "get_request_stream" 262 263 def get_request_method(self): 264 265 """ 266 Returns the request method. 267 """ 268 269 raise NotImplementedError, "get_request_method" 270 271 def get_headers(self): 272 273 """ 274 Returns all request headers as a dictionary-like object mapping header 275 names to values. 276 """ 277 278 raise NotImplementedError, "get_headers" 279 280 def get_header_values(self, key): 281 282 """ 283 Returns a list of all request header values associated with the given 284 'key'. Note that according to RFC 2616, 'key' is treated as a 285 case-insensitive string. 286 """ 287 288 raise NotImplementedError, "get_header_values" 289 290 def get_content_type(self): 291 292 """ 293 Returns the content type specified on the request, along with the 294 charset employed. 295 """ 296 297 raise NotImplementedError, "get_content_type" 298 299 def get_content_charsets(self): 300 301 """ 302 Returns the character set preferences. 303 """ 304 305 raise NotImplementedError, "get_content_charsets" 306 307 def get_content_languages(self): 308 309 """ 310 Returns extracted language information from the transaction. 311 """ 312 313 raise NotImplementedError, "get_content_languages" 314 315 def get_path(self): 316 317 """ 318 Returns the entire path from the request. 319 """ 320 321 raise NotImplementedError, "get_path" 322 323 def get_path_without_query(self): 324 325 """ 326 Returns the entire path from the request minus the query string. 327 """ 328 329 raise NotImplementedError, "get_path_without_query" 330 331 def get_path_info(self): 332 333 """ 334 Returns the "path info" (the part of the URL after the resource name 335 handling the current request) from the request. 336 """ 337 338 raise NotImplementedError, "get_path_info" 339 340 def get_query_string(self): 341 342 """ 343 Returns the query string from the path in the request. 344 """ 345 346 raise NotImplementedError, "get_query_string" 347 348 # Higher level request-related methods. 349 350 def get_fields_from_path(self): 351 352 """ 353 Extracts fields (or request parameters) from the path specified in the 354 transaction. The underlying framework may refuse to supply fields from 355 the path if handling a POST transaction. 356 357 Returns a dictionary mapping field names to lists of values (even if a 358 single value is associated with any given field name). 359 """ 360 361 raise NotImplementedError, "get_fields_from_path" 362 363 def get_fields_from_body(self, encoding=None): 364 365 """ 366 Extracts fields (or request parameters) from the message body in the 367 transaction. The optional 'encoding' parameter specifies the character 368 encoding of the message body for cases where no such information is 369 available, but where the default encoding is to be overridden. 370 371 Returns a dictionary mapping field names to lists of values (even if a 372 single value is associated with any given field name). Each value is 373 either a Unicode object (representing a simple form field, for example) 374 or a plain string (representing a file upload form field, for example). 375 """ 376 377 raise NotImplementedError, "get_fields_from_body" 378 379 def get_fields(self, encoding=None): 380 381 """ 382 Extracts fields (or request parameters) from both the path specified in 383 the transaction as well as the message body. The optional 'encoding' 384 parameter specifies the character encoding of the message body for cases 385 where no such information is available, but where the default encoding 386 is to be overridden. 387 388 Returns a dictionary mapping field names to lists of values (even if a 389 single value is associated with any given field name). Each value is 390 either a Unicode object (representing a simple form field, for example) 391 or a plain string (representing a file upload form field, for example). 392 393 Where a given field name is used in both the path and message body to 394 specify values, the values from both sources will be combined into a 395 single list associated with that field name. 396 """ 397 398 raise NotImplementedError, "get_fields" 399 400 def get_user(self): 401 402 """ 403 Extracts user information from the transaction. 404 405 Returns a username as a string or None if no user is defined. 406 """ 407 408 raise NotImplementedError, "get_user" 409 410 def get_cookies(self): 411 412 """ 413 Obtains cookie information from the request. 414 415 Returns a dictionary mapping cookie names to cookie objects. 416 """ 417 418 raise NotImplementedError, "get_cookies" 419 420 def get_cookie(self, cookie_name): 421 422 """ 423 Obtains cookie information from the request. 424 425 Returns a cookie object for the given 'cookie_name' or None if no such 426 cookie exists. 427 """ 428 429 raise NotImplementedError, "get_cookie" 430 431 # Response-related methods. 432 433 def get_response_stream(self): 434 435 """ 436 Returns the response stream for the transaction. 437 """ 438 439 raise NotImplementedError, "get_response_stream" 440 441 def get_response_stream_encoding(self): 442 443 """ 444 Returns the response stream encoding. 445 """ 446 447 raise NotImplementedError, "get_response_stream_encoding" 448 449 def get_response_code(self): 450 451 """ 452 Get the response code associated with the transaction. If no response 453 code is defined, None is returned. 454 """ 455 456 raise NotImplementedError, "get_response_code" 457 458 def set_response_code(self, response_code): 459 460 """ 461 Set the 'response_code' using a numeric constant defined in the HTTP 462 specification. 463 """ 464 465 raise NotImplementedError, "set_response_code" 466 467 def set_header_value(self, header, value): 468 469 """ 470 Set the HTTP 'header' with the given 'value'. 471 """ 472 473 raise NotImplementedError, "set_header_value" 474 475 def set_content_type(self, content_type): 476 477 """ 478 Sets the 'content_type' for the response. 479 """ 480 481 raise NotImplementedError, "set_content_type" 482 483 # Higher level response-related methods. 484 485 def set_cookie(self, cookie): 486 487 """ 488 Stores the given 'cookie' object in the response. 489 """ 490 491 raise NotImplementedError, "set_cookie" 492 493 def set_cookie_value(self, name, value, path=None, expires=None): 494 495 """ 496 Stores a cookie with the given 'name' and 'value' in the response. 497 498 The optional 'path' is a string which specifies the scope of the cookie, 499 and the optional 'expires' parameter is a value compatible with the 500 time.time function, and indicates the expiry date/time of the cookie. 501 """ 502 503 raise NotImplementedError, "set_cookie_value" 504 505 def delete_cookie(self, cookie_name): 506 507 """ 508 Adds to the response a request that the cookie with the given 509 'cookie_name' be deleted/discarded by the client. 510 """ 511 512 raise NotImplementedError, "delete_cookie" 513 514 # Session-related methods. 515 516 def get_session(self, create=1): 517 518 """ 519 Gets a session corresponding to an identifier supplied in the 520 transaction. 521 522 If no session has yet been established according to information 523 provided in the transaction then the optional 'create' parameter 524 determines whether a new session will be established. 525 526 Where no session has been established and where 'create' is set to 0 527 then None is returned. In all other cases, a session object is created 528 (where appropriate) and returned. 529 """ 530 531 raise NotImplementedError, "get_session" 532 533 def expire_session(self): 534 535 """ 536 Expires any session established according to information provided in the 537 transaction. 538 """ 539 540 raise NotImplementedError, "expire_session" 541 542 # Application-specific methods. 543 544 def set_user(self, username): 545 546 """ 547 An application-specific method which sets the user information with 548 'username' in the transaction. This affects subsequent calls to 549 'get_user'. 550 """ 551 552 self.user = username 553 554 def set_virtual_path_info(self, path_info): 555 556 """ 557 An application-specific method which sets the 'path_info' in the 558 transaction. This affects subsequent calls to 'get_virtual_path_info'. 559 """ 560 561 self.path_info = path_info 562 563 def get_virtual_path_info(self): 564 565 """ 566 An application-specific method which either returns path info set in the 567 'set_virtual_path_info' method, or the normal path info found in the 568 request. 569 """ 570 571 return self.path_info or self.get_path_info() 572 573 class Resource: 574 575 "A generic resource interface." 576 577 def respond(self, trans): 578 579 """ 580 An application-specific method which performs activities on the basis of 581 the transaction object 'trans'. 582 """ 583 584 raise NotImplementedError, "respond" 585 586 class Authenticator: 587 588 "A generic authentication component." 589 590 def authenticate(self, trans): 591 592 """ 593 An application-specific method which authenticates the sender of the 594 request described by the transaction object 'trans'. This method should 595 consider 'trans' to be read-only and not attempt to change the state of 596 the transaction. 597 598 If the sender of the request is authenticated successfully, the result 599 of this method evaluates to true; otherwise the result of this method 600 evaluates to false. 601 """ 602 603 raise NotImplementedError, "authenticate" 604 605 def get_auth_type(self): 606 607 """ 608 An application-specific method which returns the authentication type to 609 be used. An example value is 'Basic' which specifies HTTP basic 610 authentication. 611 """ 612 613 raise NotImplementedError, "get_auth_type" 614 615 def get_realm(self): 616 617 """ 618 An application-specific method which returns the name of the realm for 619 which authentication is taking place. 620 """ 621 622 raise NotImplementedError, "get_realm" 623 624 # vim: tabstop=4 expandtab shiftwidth=4