WebStack

Annotated WebStack/CGI.py

193:6448ed817132
2004-08-27 paulb [project @ 2004-08-27 23:51:51 by paulb] Fixed content type interpretation. Fixed file upload support (not currently working for JavaServlet).
paulb@108 1
#!/usr/bin/env python
paulb@108 2
paulb@108 3
"""
paulb@108 4
CGI classes.
paulb@108 5
"""
paulb@108 6
paulb@108 7
import Generic
paulb@108 8
import os, sys
paulb@108 9
from Helpers.Request import MessageBodyStream
paulb@167 10
from Helpers.Response import ConvertingStream
paulb@108 11
from Helpers.Auth import UserInfo
paulb@108 12
from Helpers import Environment
paulb@108 13
from cgi import parse_qs, FieldStorage
paulb@108 14
import Cookie
paulb@108 15
from StringIO import StringIO
paulb@108 16
paulb@108 17
class Transaction(Generic.Transaction):
paulb@108 18
paulb@108 19
    """
paulb@108 20
    CGI transaction interface.
paulb@108 21
    """
paulb@108 22
paulb@108 23
    def __init__(self, input=None, output=None, env=None):
paulb@108 24
paulb@108 25
        """
paulb@108 26
        Initialise the transaction using the CGI 'input' and 'output' streams.
paulb@108 27
        These streams are optional and default to standard input and standard
paulb@108 28
        output respectively.
paulb@108 29
        """
paulb@108 30
paulb@108 31
        self.input = input or sys.stdin
paulb@108 32
        self.output = output or sys.stdout
paulb@108 33
        self.env = env or os.environ
paulb@108 34
paulb@108 35
        # Other attributes of interest in instances of this class.
paulb@108 36
paulb@108 37
        self.content_type = None
paulb@108 38
        self.response_code = 200
paulb@108 39
        self.content = StringIO()
paulb@108 40
        self.headers_out = {}
paulb@108 41
        self.cookies_out = Cookie.SimpleCookie()
paulb@128 42
        self.user = None
paulb@108 43
paulb@108 44
        # Define the incoming cookies.
paulb@108 45
paulb@108 46
        self.cookies_in = Cookie.SimpleCookie(self.env.get("HTTP_COOKIE"))
paulb@108 47
paulb@133 48
        # Cached information.
paulb@133 49
paulb@133 50
        self.storage_body = None
paulb@133 51
paulb@108 52
    def commit(self):
paulb@108 53
paulb@108 54
        """
paulb@108 55
        A special method, synchronising the transaction with framework-specific
paulb@108 56
        objects.
paulb@108 57
paulb@108 58
        See draft-coar-cgi-v11-03, section 7.
paulb@108 59
        """
paulb@108 60
paulb@108 61
        # NOTE: Provide sensible messages.
paulb@108 62
paulb@108 63
        self.output.write("Status: %s %s\n" % (self.response_code, "WebStack status"))
paulb@108 64
        if self.content_type is not None:
paulb@108 65
            self.output.write("Content-type: %s\n" % self.format_content_type(self.content_type))
paulb@108 66
        for header, value in self.headers_out.items():
paulb@108 67
            self.output.write("%s: %s\n" %
paulb@108 68
                (self.format_header_value(header), self.format_header_value(value))
paulb@108 69
            )
paulb@108 70
        self.output.write(str(self.cookies_out))
paulb@108 71
        self.output.write("\n")
paulb@133 72
        self.output.write("\n")
paulb@108 73
paulb@108 74
        self.content.seek(0)
paulb@108 75
        self.output.write(self.content.read())
paulb@108 76
paulb@108 77
    # Request-related methods.
paulb@108 78
paulb@108 79
    def get_request_stream(self):
paulb@108 80
paulb@108 81
        """
paulb@186 82
        Returns the request stream for the transaction.
paulb@108 83
        """
paulb@108 84
paulb@108 85
        return self.input
paulb@108 86
paulb@108 87
    def get_request_method(self):
paulb@108 88
paulb@108 89
        """
paulb@186 90
        Returns the request method.
paulb@108 91
        """
paulb@108 92
paulb@108 93
        return self.env.get("REQUEST_METHOD")
paulb@108 94
paulb@108 95
    def get_headers(self):
paulb@108 96
paulb@108 97
        """
paulb@186 98
        Returns all request headers as a dictionary-like object mapping header
paulb@186 99
        names to values.
paulb@108 100
        """
paulb@108 101
paulb@108 102
        return Environment.get_headers(self.env)
paulb@108 103
paulb@108 104
    def get_header_values(self, key):
paulb@108 105
paulb@108 106
        """
paulb@186 107
        Returns a list of all request header values associated with the given
paulb@186 108
        'key'. Note that according to RFC 2616, 'key' is treated as a
paulb@186 109
        case-insensitive string.
paulb@108 110
        """
paulb@108 111
paulb@108 112
        return self.convert_to_list(self.get_headers().get(key))
paulb@108 113
paulb@108 114
    def get_content_type(self):
paulb@108 115
paulb@108 116
        """
paulb@186 117
        Returns the content type specified on the request, along with the
paulb@186 118
        charset employed.
paulb@108 119
        """
paulb@108 120
paulb@108 121
        return self.parse_content_type(self.env.get("CONTENT_TYPE"))
paulb@108 122
paulb@108 123
    def get_content_charsets(self):
paulb@108 124
paulb@108 125
        """
paulb@108 126
        Returns the character set preferences.
paulb@108 127
        """
paulb@108 128
paulb@108 129
        return self.parse_content_preferences(None)
paulb@108 130
paulb@108 131
    def get_content_languages(self):
paulb@108 132
paulb@108 133
        """
paulb@186 134
        Returns extracted language information from the transaction.
paulb@108 135
        """
paulb@108 136
paulb@108 137
        return self.parse_content_preferences(None)
paulb@108 138
paulb@108 139
    def get_path(self):
paulb@108 140
paulb@108 141
        """
paulb@186 142
        Returns the entire path from the request.
paulb@108 143
        """
paulb@108 144
paulb@162 145
        path = self.get_path_without_query()
paulb@162 146
        qs = self.get_query_string()
paulb@162 147
        if qs:
paulb@162 148
            path += "?"
paulb@162 149
            path += qs
paulb@162 150
        return path
paulb@162 151
paulb@162 152
    def get_path_without_query(self):
paulb@162 153
paulb@162 154
        """
paulb@186 155
        Returns the entire path from the request minus the query string.
paulb@162 156
        """
paulb@162 157
paulb@108 158
        path = self.env.get("SCRIPT_NAME") or ""
paulb@108 159
        if self.env.has_key("PATH_INFO"):
paulb@108 160
            path += self.env["PATH_INFO"]
paulb@108 161
        return path
paulb@108 162
paulb@108 163
    def get_path_info(self):
paulb@108 164
paulb@108 165
        """
paulb@186 166
        Returns the "path info" (the part of the URL after the resource name
paulb@186 167
        handling the current request) from the request.
paulb@108 168
        """
paulb@108 169
paulb@108 170
        return self.env.get("PATH_INFO") or ""
paulb@108 171
paulb@108 172
    def get_query_string(self):
paulb@108 173
paulb@108 174
        """
paulb@186 175
        Returns the query string from the path in the request.
paulb@108 176
        """
paulb@108 177
paulb@108 178
        return self.env.get("QUERY_STRING") or ""
paulb@108 179
paulb@108 180
    # Higher level request-related methods.
paulb@108 181
paulb@108 182
    def get_fields_from_path(self):
paulb@108 183
paulb@108 184
        """
paulb@186 185
        Extracts the form fields from the path specified in the transaction. The
paulb@186 186
        underlying framework may refuse to supply fields from the path if
paulb@186 187
        handling a POST transaction.
paulb@108 188
paulb@108 189
        Returns a dictionary mapping field names to lists of values (even if a
paulb@108 190
        single value is associated with any given field name).
paulb@108 191
        """
paulb@108 192
paulb@108 193
        return parse_qs(self.get_query_string(), keep_blank_values=1)
paulb@108 194
paulb@167 195
    def get_fields_from_body(self, encoding=None):
paulb@108 196
paulb@108 197
        """
paulb@186 198
        Extracts the form fields from the message body in the transaction. The
paulb@186 199
        optional 'encoding' parameter specifies the character encoding of the
paulb@186 200
        message body for cases where no such information is available, but where
paulb@186 201
        the default encoding is to be overridden.
paulb@108 202
paulb@108 203
        Returns a dictionary mapping field names to lists of values (even if a
paulb@193 204
        single value is associated with any given field name). Each value is
paulb@193 205
        either a Unicode object (representing a simple form field, for example)
paulb@193 206
        or a file-like object (representing a file upload form field, for
paulb@193 207
        example).
paulb@108 208
        """
paulb@108 209
paulb@167 210
        encoding = self.get_content_type().charset or encoding or "iso-8859-1"
paulb@167 211
paulb@133 212
        if self.storage_body is None:
paulb@133 213
            self.storage_body = FieldStorage(fp=self.get_request_stream(), keep_blank_values=1)
paulb@108 214
paulb@108 215
        # Avoid strange design issues with FieldStorage by checking the internal
paulb@108 216
        # field list directly.
paulb@108 217
paulb@108 218
        fields = {}
paulb@133 219
        if self.storage_body.list is not None:
paulb@108 220
paulb@108 221
            # Traverse the storage, finding each field value.
paulb@108 222
paulb@133 223
            for field_name in self.storage_body.keys():
paulb@167 224
                fields[field_name] = []
paulb@167 225
                for field_value in self.storage_body.getlist(field_name):
paulb@193 226
                    if hasattr(field_value, "file"):
paulb@193 227
                        fields[field_name].append(field_value.file)
paulb@193 228
                    else:
paulb@193 229
                        fields[field_name].append(unicode(field_value, encoding))
paulb@108 230
        return fields
paulb@108 231
paulb@108 232
    def get_user(self):
paulb@108 233
paulb@108 234
        """
paulb@186 235
        Extracts user information from the transaction.
paulb@108 236
paulb@108 237
        Returns a username as a string or None if no user is defined.
paulb@108 238
        """
paulb@108 239
paulb@128 240
        if self.user is not None:
paulb@128 241
            return self.user
paulb@128 242
        else:
paulb@128 243
            return self.env.get("REMOTE_USER")
paulb@108 244
paulb@108 245
    def get_cookies(self):
paulb@108 246
paulb@108 247
        """
paulb@186 248
        Obtains cookie information from the request.
paulb@108 249
paulb@108 250
        Returns a dictionary mapping cookie names to cookie objects.
paulb@108 251
        """
paulb@108 252
paulb@108 253
        return self.cookies_in
paulb@108 254
paulb@108 255
    def get_cookie(self, cookie_name):
paulb@108 256
paulb@108 257
        """
paulb@186 258
        Obtains cookie information from the request.
paulb@108 259
paulb@108 260
        Returns a cookie object for the given 'cookie_name' or None if no such
paulb@108 261
        cookie exists.
paulb@108 262
        """
paulb@108 263
paulb@108 264
        return self.cookies_in.get(cookie_name)
paulb@108 265
paulb@108 266
    # Response-related methods.
paulb@108 267
paulb@108 268
    def get_response_stream(self):
paulb@108 269
paulb@108 270
        """
paulb@186 271
        Returns the response stream for the transaction.
paulb@108 272
        """
paulb@108 273
paulb@108 274
        # Return a stream which is later emptied into the real stream.
paulb@167 275
        # Unicode can upset this operation. Using either the specified charset,
paulb@167 276
        # the same charset as that used in the request, or a default encoding.
paulb@108 277
paulb@167 278
        encoding = self.get_content_type().charset or "utf-8"
paulb@167 279
        if self.content_type:
paulb@167 280
            encoding = self.content_type.charset or encoding
paulb@167 281
        return ConvertingStream(self.content, encoding)
paulb@108 282
paulb@108 283
    def get_response_code(self):
paulb@108 284
paulb@108 285
        """
paulb@108 286
        Get the response code associated with the transaction. If no response
paulb@108 287
        code is defined, None is returned.
paulb@108 288
        """
paulb@108 289
paulb@108 290
        return self.response_code
paulb@108 291
paulb@108 292
    def set_response_code(self, response_code):
paulb@108 293
paulb@108 294
        """
paulb@108 295
        Set the 'response_code' using a numeric constant defined in the HTTP
paulb@108 296
        specification.
paulb@108 297
        """
paulb@108 298
paulb@108 299
        self.response_code = response_code
paulb@108 300
paulb@108 301
    def set_header_value(self, header, value):
paulb@108 302
paulb@108 303
        """
paulb@108 304
        Set the HTTP 'header' with the given 'value'.
paulb@108 305
        """
paulb@108 306
paulb@108 307
        # The header is not written out immediately due to the buffering in use.
paulb@108 308
paulb@108 309
        self.headers_out[header] = value
paulb@108 310
paulb@108 311
    def set_content_type(self, content_type):
paulb@108 312
paulb@108 313
        """
paulb@186 314
        Sets the 'content_type' for the response.
paulb@108 315
        """
paulb@108 316
paulb@108 317
        # The content type has to be written as a header, before actual content,
paulb@108 318
        # but after the response line. This means that some kind of buffering is
paulb@108 319
        # required. Hence, we don't write the header out immediately.
paulb@108 320
paulb@108 321
        self.content_type = content_type
paulb@108 322
paulb@108 323
    # Higher level response-related methods.
paulb@108 324
paulb@108 325
    def set_cookie(self, cookie):
paulb@108 326
paulb@108 327
        """
paulb@186 328
        Stores the given 'cookie' object in the response.
paulb@108 329
        """
paulb@108 330
paulb@108 331
        # NOTE: If multiple cookies of the same name could be specified, this
paulb@108 332
        # NOTE: could need changing.
paulb@108 333
paulb@108 334
        self.cookies_out[cookie.name] = cookie.value
paulb@108 335
paulb@108 336
    def set_cookie_value(self, name, value, path=None, expires=None):
paulb@108 337
paulb@108 338
        """
paulb@186 339
        Stores a cookie with the given 'name' and 'value' in the response.
paulb@108 340
paulb@108 341
        The optional 'path' is a string which specifies the scope of the cookie,
paulb@108 342
        and the optional 'expires' parameter is a value compatible with the
paulb@108 343
        time.time function, and indicates the expiry date/time of the cookie.
paulb@108 344
        """
paulb@108 345
paulb@108 346
        self.cookies_out[name] = value
paulb@108 347
        if path is not None:
paulb@108 348
            self.cookies_out[name]["path"] = path
paulb@108 349
        if expires is not None:
paulb@108 350
            self.cookies_out[name]["expires"] = expires
paulb@108 351
paulb@108 352
    def delete_cookie(self, cookie_name):
paulb@108 353
paulb@108 354
        """
paulb@186 355
        Adds to the response a request that the cookie with the given
paulb@186 356
        'cookie_name' be deleted/discarded by the client.
paulb@108 357
        """
paulb@108 358
paulb@108 359
        # Create a special cookie, given that we do not know whether the browser
paulb@108 360
        # has been sent the cookie or not.
paulb@108 361
        # NOTE: Magic discovered in Webware.
paulb@108 362
paulb@108 363
        self.cookies_out[cookie_name] = ""
paulb@108 364
        self.cookies_out[cookie_name]["path"] = "/"
paulb@108 365
        self.cookies_out[cookie_name]["expires"] = 0
paulb@108 366
        self.cookies_out[cookie_name]["max-age"] = 0
paulb@108 367
paulb@128 368
    # Application-specific methods.
paulb@128 369
paulb@128 370
    def set_user(self, username):
paulb@128 371
paulb@128 372
        """
paulb@128 373
        An application-specific method which sets the user information with
paulb@128 374
        'username' in the transaction. This affects subsequent calls to
paulb@128 375
        'get_user'.
paulb@128 376
        """
paulb@128 377
paulb@128 378
        self.user = username
paulb@128 379
paulb@108 380
# vim: tabstop=4 expandtab shiftwidth=4