WebStack

Annotated WebStack/CGI.py

434:47a4fa8e0104
2005-08-24 paulb [project @ 2005-08-24 18:50:07 by paulb] Introduced decoding of path information with an optional explicit encoding.
paulb@108 1
#!/usr/bin/env python
paulb@108 2
paulb@108 3
"""
paulb@108 4
CGI classes.
paulb@403 5
paulb@403 6
Copyright (C) 2004, 2005 Paul Boddie <paul@boddie.org.uk>
paulb@403 7
paulb@403 8
This library is free software; you can redistribute it and/or
paulb@403 9
modify it under the terms of the GNU Lesser General Public
paulb@403 10
License as published by the Free Software Foundation; either
paulb@403 11
version 2.1 of the License, or (at your option) any later version.
paulb@403 12
paulb@403 13
This library is distributed in the hope that it will be useful,
paulb@403 14
but WITHOUT ANY WARRANTY; without even the implied warranty of
paulb@403 15
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
paulb@403 16
Lesser General Public License for more details.
paulb@403 17
paulb@403 18
You should have received a copy of the GNU Lesser General Public
paulb@403 19
License along with this library; if not, write to the Free Software
paulb@403 20
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
paulb@108 21
"""
paulb@108 22
paulb@108 23
import Generic
paulb@108 24
import os, sys
paulb@258 25
from Helpers.Request import MessageBodyStream, get_body_fields, get_storage_items, Cookie
paulb@167 26
from Helpers.Response import ConvertingStream
paulb@108 27
from Helpers.Auth import UserInfo
paulb@239 28
from Helpers.Session import SessionStore
paulb@108 29
from Helpers import Environment
paulb@108 30
from cgi import parse_qs, FieldStorage
paulb@258 31
from Cookie import SimpleCookie
paulb@108 32
from StringIO import StringIO
paulb@108 33
paulb@108 34
class Transaction(Generic.Transaction):
paulb@108 35
paulb@108 36
    """
paulb@108 37
    CGI transaction interface.
paulb@108 38
    """
paulb@108 39
paulb@108 40
    def __init__(self, input=None, output=None, env=None):
paulb@108 41
paulb@108 42
        """
paulb@108 43
        Initialise the transaction using the CGI 'input' and 'output' streams.
paulb@108 44
        These streams are optional and default to standard input and standard
paulb@108 45
        output respectively.
paulb@108 46
        """
paulb@108 47
paulb@108 48
        self.input = input or sys.stdin
paulb@108 49
        self.output = output or sys.stdout
paulb@108 50
        self.env = env or os.environ
paulb@108 51
paulb@108 52
        # Other attributes of interest in instances of this class.
paulb@108 53
paulb@108 54
        self.content_type = None
paulb@108 55
        self.response_code = 200
paulb@108 56
        self.content = StringIO()
paulb@108 57
        self.headers_out = {}
paulb@258 58
        self.cookies_out = SimpleCookie()
paulb@128 59
        self.user = None
paulb@311 60
        self.path_info = None
paulb@108 61
paulb@108 62
        # Define the incoming cookies.
paulb@108 63
paulb@258 64
        self.cookies_in = SimpleCookie(self.env.get("HTTP_COOKIE"))
paulb@108 65
paulb@133 66
        # Cached information.
paulb@133 67
paulb@133 68
        self.storage_body = None
paulb@133 69
paulb@239 70
        # Special objects retained throughout the transaction.
paulb@239 71
paulb@239 72
        self.session_store = None
paulb@239 73
paulb@108 74
    def commit(self):
paulb@108 75
paulb@108 76
        """
paulb@108 77
        A special method, synchronising the transaction with framework-specific
paulb@108 78
        objects.
paulb@108 79
paulb@108 80
        See draft-coar-cgi-v11-03, section 7.
paulb@108 81
        """
paulb@108 82
paulb@239 83
        # Close the session store.
paulb@239 84
paulb@239 85
        if self.session_store is not None:
paulb@239 86
            self.session_store.close()
paulb@239 87
paulb@108 88
        # NOTE: Provide sensible messages.
paulb@108 89
paulb@108 90
        self.output.write("Status: %s %s\n" % (self.response_code, "WebStack status"))
paulb@108 91
        if self.content_type is not None:
paulb@224 92
            self.output.write("Content-type: %s\n" % str(self.content_type))
paulb@108 93
        for header, value in self.headers_out.items():
paulb@108 94
            self.output.write("%s: %s\n" %
paulb@108 95
                (self.format_header_value(header), self.format_header_value(value))
paulb@108 96
            )
paulb@108 97
        self.output.write(str(self.cookies_out))
paulb@108 98
        self.output.write("\n")
paulb@108 99
paulb@108 100
        self.content.seek(0)
paulb@108 101
        self.output.write(self.content.read())
paulb@108 102
paulb@338 103
    # Server-related methods.
paulb@338 104
paulb@338 105
    def get_server_name(self):
paulb@338 106
paulb@338 107
        "Returns the server name."
paulb@338 108
paulb@338 109
        return self.env.get("SERVER_NAME")
paulb@338 110
paulb@338 111
    def get_server_port(self):
paulb@338 112
paulb@338 113
        "Returns the server port as a string."
paulb@338 114
paulb@338 115
        return self.env.get("SERVER_PORT")
paulb@338 116
paulb@108 117
    # Request-related methods.
paulb@108 118
paulb@108 119
    def get_request_stream(self):
paulb@108 120
paulb@108 121
        """
paulb@186 122
        Returns the request stream for the transaction.
paulb@108 123
        """
paulb@108 124
paulb@108 125
        return self.input
paulb@108 126
paulb@108 127
    def get_request_method(self):
paulb@108 128
paulb@108 129
        """
paulb@186 130
        Returns the request method.
paulb@108 131
        """
paulb@108 132
paulb@108 133
        return self.env.get("REQUEST_METHOD")
paulb@108 134
paulb@108 135
    def get_headers(self):
paulb@108 136
paulb@108 137
        """
paulb@186 138
        Returns all request headers as a dictionary-like object mapping header
paulb@186 139
        names to values.
paulb@108 140
        """
paulb@108 141
paulb@108 142
        return Environment.get_headers(self.env)
paulb@108 143
paulb@108 144
    def get_header_values(self, key):
paulb@108 145
paulb@108 146
        """
paulb@186 147
        Returns a list of all request header values associated with the given
paulb@186 148
        'key'. Note that according to RFC 2616, 'key' is treated as a
paulb@186 149
        case-insensitive string.
paulb@108 150
        """
paulb@108 151
paulb@108 152
        return self.convert_to_list(self.get_headers().get(key))
paulb@108 153
paulb@108 154
    def get_content_type(self):
paulb@108 155
paulb@108 156
        """
paulb@186 157
        Returns the content type specified on the request, along with the
paulb@186 158
        charset employed.
paulb@108 159
        """
paulb@108 160
paulb@108 161
        return self.parse_content_type(self.env.get("CONTENT_TYPE"))
paulb@108 162
paulb@108 163
    def get_content_charsets(self):
paulb@108 164
paulb@108 165
        """
paulb@108 166
        Returns the character set preferences.
paulb@108 167
        """
paulb@108 168
paulb@108 169
        return self.parse_content_preferences(None)
paulb@108 170
paulb@108 171
    def get_content_languages(self):
paulb@108 172
paulb@108 173
        """
paulb@186 174
        Returns extracted language information from the transaction.
paulb@108 175
        """
paulb@108 176
paulb@108 177
        return self.parse_content_preferences(None)
paulb@108 178
paulb@434 179
    def get_path(self, encoding=None):
paulb@108 180
paulb@108 181
        """
paulb@434 182
        Returns the entire path from the request as a Unicode object. Any "URL
paulb@434 183
        encoded" character values in the part of the path before the query
paulb@434 184
        string will be decoded and presented as genuine characters; the query
paulb@434 185
        string will remain "URL encoded", however.
paulb@434 186
paulb@434 187
        If the optional 'encoding' is set, use that in preference to the default
paulb@434 188
        encoding to convert the path into a form not containing "URL encoded"
paulb@434 189
        character values.
paulb@108 190
        """
paulb@108 191
paulb@434 192
        path = self.get_path_without_query(encoding)
paulb@162 193
        qs = self.get_query_string()
paulb@162 194
        if qs:
paulb@434 195
            return path + "?" + qs
paulb@434 196
        else:
paulb@434 197
            return path
paulb@434 198
paulb@434 199
    def get_path_without_query(self, encoding=None):
paulb@434 200
paulb@434 201
        """
paulb@434 202
        Returns the entire path from the request minus the query string as a
paulb@434 203
        Unicode object containing genuine characters (as opposed to "URL
paulb@434 204
        encoded" character values).
paulb@434 205
paulb@434 206
        If the optional 'encoding' is set, use that in preference to the default
paulb@434 207
        encoding to convert the path into a form not containing "URL encoded"
paulb@434 208
        character values.
paulb@434 209
        """
paulb@434 210
paulb@434 211
        path = self.decode_path(self.env.get("SCRIPT_NAME") or "", encoding)
paulb@434 212
        path += self.get_path_info(encoding)
paulb@162 213
        return path
paulb@162 214
paulb@434 215
    def get_path_info(self, encoding=None):
paulb@108 216
paulb@108 217
        """
paulb@186 218
        Returns the "path info" (the part of the URL after the resource name
paulb@434 219
        handling the current request) from the request as a Unicode object
paulb@434 220
        containing genuine characters (as opposed to "URL encoded" character
paulb@434 221
        values).
paulb@434 222
paulb@434 223
        If the optional 'encoding' is set, use that in preference to the default
paulb@434 224
        encoding to convert the path into a form not containing "URL encoded"
paulb@434 225
        character values.
paulb@108 226
        """
paulb@108 227
paulb@434 228
        return self.decode_path(self.env.get("PATH_INFO") or "", encoding)
paulb@108 229
paulb@108 230
    def get_query_string(self):
paulb@108 231
paulb@108 232
        """
paulb@186 233
        Returns the query string from the path in the request.
paulb@108 234
        """
paulb@108 235
paulb@108 236
        return self.env.get("QUERY_STRING") or ""
paulb@108 237
paulb@108 238
    # Higher level request-related methods.
paulb@108 239
paulb@430 240
    def get_fields_from_path(self, encoding=None):
paulb@108 241
paulb@108 242
        """
paulb@249 243
        Extracts fields (or request parameters) from the path specified in the
paulb@249 244
        transaction. The underlying framework may refuse to supply fields from
paulb@430 245
        the path if handling a POST transaction. The optional 'encoding'
paulb@430 246
        parameter specifies the character encoding of the query string for cases
paulb@430 247
        where the default encoding is to be overridden.
paulb@108 248
paulb@108 249
        Returns a dictionary mapping field names to lists of values (even if a
paulb@108 250
        single value is associated with any given field name).
paulb@108 251
        """
paulb@108 252
paulb@249 253
        fields = {}
paulb@249 254
        for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items():
paulb@430 255
            name = self.decode_path(name, encoding)
paulb@249 256
            fields[name] = []
paulb@249 257
            for value in values:
paulb@430 258
                value = self.decode_path(value, encoding)
paulb@430 259
                fields[name].append(value)
paulb@249 260
        return fields
paulb@108 261
paulb@167 262
    def get_fields_from_body(self, encoding=None):
paulb@108 263
paulb@108 264
        """
paulb@249 265
        Extracts fields (or request parameters) from the message body in the
paulb@249 266
        transaction. The optional 'encoding' parameter specifies the character
paulb@249 267
        encoding of the message body for cases where no such information is
paulb@249 268
        available, but where the default encoding is to be overridden.
paulb@108 269
paulb@108 270
        Returns a dictionary mapping field names to lists of values (even if a
paulb@193 271
        single value is associated with any given field name). Each value is
paulb@193 272
        either a Unicode object (representing a simple form field, for example)
paulb@198 273
        or a plain string (representing a file upload form field, for example).
paulb@108 274
        """
paulb@108 275
paulb@224 276
        encoding = encoding or self.get_content_type().charset or self.default_charset
paulb@167 277
paulb@133 278
        if self.storage_body is None:
paulb@249 279
            self.storage_body = FieldStorage(fp=self.get_request_stream(),
paulb@249 280
                headers={"content-type" : str(self.get_content_type())},
paulb@249 281
                environ={"REQUEST_METHOD" : self.get_request_method()},
paulb@249 282
                keep_blank_values=1)
paulb@108 283
paulb@108 284
        # Avoid strange design issues with FieldStorage by checking the internal
paulb@108 285
        # field list directly.
paulb@108 286
paulb@108 287
        fields = {}
paulb@133 288
        if self.storage_body.list is not None:
paulb@108 289
paulb@108 290
            # Traverse the storage, finding each field value.
paulb@108 291
paulb@198 292
            fields = get_body_fields(get_storage_items(self.storage_body), encoding)
paulb@198 293
paulb@108 294
        return fields
paulb@108 295
paulb@249 296
    def get_fields(self, encoding=None):
paulb@249 297
paulb@249 298
        """
paulb@249 299
        Extracts fields (or request parameters) from both the path specified in
paulb@249 300
        the transaction as well as the message body. The optional 'encoding'
paulb@249 301
        parameter specifies the character encoding of the message body for cases
paulb@249 302
        where no such information is available, but where the default encoding
paulb@249 303
        is to be overridden.
paulb@249 304
paulb@249 305
        Returns a dictionary mapping field names to lists of values (even if a
paulb@249 306
        single value is associated with any given field name). Each value is
paulb@249 307
        either a Unicode object (representing a simple form field, for example)
paulb@249 308
        or a plain string (representing a file upload form field, for example).
paulb@249 309
paulb@249 310
        Where a given field name is used in both the path and message body to
paulb@249 311
        specify values, the values from both sources will be combined into a
paulb@249 312
        single list associated with that field name.
paulb@249 313
        """
paulb@249 314
paulb@249 315
        # Combine the two sources.
paulb@249 316
paulb@249 317
        fields = {}
paulb@249 318
        fields.update(self.get_fields_from_path())
paulb@249 319
        for name, values in self.get_fields_from_body(encoding).items():
paulb@249 320
            if not fields.has_key(name):
paulb@249 321
                fields[name] = values
paulb@249 322
            else:
paulb@249 323
                fields[name] += values
paulb@249 324
        return fields
paulb@249 325
paulb@108 326
    def get_user(self):
paulb@108 327
paulb@108 328
        """
paulb@186 329
        Extracts user information from the transaction.
paulb@108 330
paulb@108 331
        Returns a username as a string or None if no user is defined.
paulb@108 332
        """
paulb@108 333
paulb@128 334
        if self.user is not None:
paulb@128 335
            return self.user
paulb@128 336
        else:
paulb@128 337
            return self.env.get("REMOTE_USER")
paulb@108 338
paulb@108 339
    def get_cookies(self):
paulb@108 340
paulb@108 341
        """
paulb@186 342
        Obtains cookie information from the request.
paulb@108 343
paulb@108 344
        Returns a dictionary mapping cookie names to cookie objects.
paulb@108 345
        """
paulb@108 346
paulb@258 347
        return self.process_cookies(self.cookies_in)
paulb@108 348
paulb@108 349
    def get_cookie(self, cookie_name):
paulb@108 350
paulb@108 351
        """
paulb@186 352
        Obtains cookie information from the request.
paulb@108 353
paulb@108 354
        Returns a cookie object for the given 'cookie_name' or None if no such
paulb@108 355
        cookie exists.
paulb@108 356
        """
paulb@108 357
paulb@258 358
        cookie = self.cookies_in.get(self.encode_cookie_value(cookie_name))
paulb@258 359
        if cookie is not None:
paulb@258 360
            return Cookie(cookie_name, self.decode_cookie_value(cookie.value))
paulb@258 361
        else:
paulb@258 362
            return None
paulb@108 363
paulb@108 364
    # Response-related methods.
paulb@108 365
paulb@108 366
    def get_response_stream(self):
paulb@108 367
paulb@108 368
        """
paulb@186 369
        Returns the response stream for the transaction.
paulb@108 370
        """
paulb@108 371
paulb@108 372
        # Return a stream which is later emptied into the real stream.
paulb@224 373
        # Unicode can upset this operation. Using either the specified charset
paulb@224 374
        # or a default encoding.
paulb@108 375
paulb@252 376
        encoding = self.get_response_stream_encoding()
paulb@252 377
        return ConvertingStream(self.content, encoding)
paulb@252 378
paulb@252 379
    def get_response_stream_encoding(self):
paulb@252 380
paulb@252 381
        """
paulb@252 382
        Returns the response stream encoding.
paulb@252 383
        """
paulb@252 384
paulb@167 385
        if self.content_type:
paulb@224 386
            encoding = self.content_type.charset
paulb@252 387
        else:
paulb@252 388
            encoding = None
paulb@252 389
        return encoding or self.default_charset
paulb@108 390
paulb@108 391
    def get_response_code(self):
paulb@108 392
paulb@108 393
        """
paulb@108 394
        Get the response code associated with the transaction. If no response
paulb@108 395
        code is defined, None is returned.
paulb@108 396
        """
paulb@108 397
paulb@108 398
        return self.response_code
paulb@108 399
paulb@108 400
    def set_response_code(self, response_code):
paulb@108 401
paulb@108 402
        """
paulb@108 403
        Set the 'response_code' using a numeric constant defined in the HTTP
paulb@108 404
        specification.
paulb@108 405
        """
paulb@108 406
paulb@108 407
        self.response_code = response_code
paulb@108 408
paulb@108 409
    def set_header_value(self, header, value):
paulb@108 410
paulb@108 411
        """
paulb@108 412
        Set the HTTP 'header' with the given 'value'.
paulb@108 413
        """
paulb@108 414
paulb@108 415
        # The header is not written out immediately due to the buffering in use.
paulb@108 416
paulb@108 417
        self.headers_out[header] = value
paulb@108 418
paulb@108 419
    def set_content_type(self, content_type):
paulb@108 420
paulb@108 421
        """
paulb@186 422
        Sets the 'content_type' for the response.
paulb@108 423
        """
paulb@108 424
paulb@108 425
        # The content type has to be written as a header, before actual content,
paulb@108 426
        # but after the response line. This means that some kind of buffering is
paulb@108 427
        # required. Hence, we don't write the header out immediately.
paulb@108 428
paulb@108 429
        self.content_type = content_type
paulb@108 430
paulb@108 431
    # Higher level response-related methods.
paulb@108 432
paulb@108 433
    def set_cookie(self, cookie):
paulb@108 434
paulb@108 435
        """
paulb@186 436
        Stores the given 'cookie' object in the response.
paulb@108 437
        """
paulb@108 438
paulb@108 439
        # NOTE: If multiple cookies of the same name could be specified, this
paulb@108 440
        # NOTE: could need changing.
paulb@108 441
paulb@258 442
        self.set_cookie_value(cookie.name, cookie.value)
paulb@108 443
paulb@108 444
    def set_cookie_value(self, name, value, path=None, expires=None):
paulb@108 445
paulb@108 446
        """
paulb@186 447
        Stores a cookie with the given 'name' and 'value' in the response.
paulb@108 448
paulb@108 449
        The optional 'path' is a string which specifies the scope of the cookie,
paulb@108 450
        and the optional 'expires' parameter is a value compatible with the
paulb@108 451
        time.time function, and indicates the expiry date/time of the cookie.
paulb@108 452
        """
paulb@108 453
paulb@258 454
        name = self.encode_cookie_value(name)
paulb@258 455
        self.cookies_out[name] = self.encode_cookie_value(value)
paulb@108 456
        if path is not None:
paulb@108 457
            self.cookies_out[name]["path"] = path
paulb@108 458
        if expires is not None:
paulb@108 459
            self.cookies_out[name]["expires"] = expires
paulb@108 460
paulb@108 461
    def delete_cookie(self, cookie_name):
paulb@108 462
paulb@108 463
        """
paulb@186 464
        Adds to the response a request that the cookie with the given
paulb@186 465
        'cookie_name' be deleted/discarded by the client.
paulb@108 466
        """
paulb@108 467
paulb@108 468
        # Create a special cookie, given that we do not know whether the browser
paulb@108 469
        # has been sent the cookie or not.
paulb@108 470
        # NOTE: Magic discovered in Webware.
paulb@108 471
paulb@258 472
        name = self.encode_cookie_value(cookie_name)
paulb@258 473
        self.cookies_out[name] = ""
paulb@258 474
        self.cookies_out[name]["path"] = "/"
paulb@258 475
        self.cookies_out[name]["expires"] = 0
paulb@258 476
        self.cookies_out[name]["max-age"] = 0
paulb@108 477
paulb@239 478
    # Session-related methods.
paulb@239 479
paulb@239 480
    def get_session(self, create=1):
paulb@239 481
paulb@239 482
        """
paulb@239 483
        Gets a session corresponding to an identifier supplied in the
paulb@239 484
        transaction.
paulb@239 485
paulb@239 486
        If no session has yet been established according to information
paulb@239 487
        provided in the transaction then the optional 'create' parameter
paulb@239 488
        determines whether a new session will be established.
paulb@239 489
paulb@239 490
        Where no session has been established and where 'create' is set to 0
paulb@239 491
        then None is returned. In all other cases, a session object is created
paulb@239 492
        (where appropriate) and returned.
paulb@239 493
        """
paulb@239 494
paulb@239 495
        # NOTE: Requires configuration.
paulb@239 496
paulb@239 497
        if self.session_store is None:
paulb@239 498
            self.session_store = SessionStore(self, "WebStack-sessions")
paulb@239 499
        return self.session_store.get_session(create)
paulb@239 500
paulb@239 501
    def expire_session(self):
paulb@239 502
paulb@239 503
        """
paulb@239 504
        Expires any session established according to information provided in the
paulb@239 505
        transaction.
paulb@239 506
        """
paulb@239 507
paulb@239 508
        # NOTE: Requires configuration.
paulb@239 509
paulb@239 510
        if self.session_store is None:
paulb@239 511
            self.session_store = SessionStore(self, "WebStack-sessions")
paulb@239 512
        self.session_store.expire_session()
paulb@239 513
paulb@108 514
# vim: tabstop=4 expandtab shiftwidth=4