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