| 1 | # |
|---|
| 2 | # Copyright 2004 Apache Software Foundation |
|---|
| 3 | # |
|---|
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you |
|---|
| 5 | # may not use this file except in compliance with the License. You |
|---|
| 6 | # may obtain a copy of the License at |
|---|
| 7 | # |
|---|
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
|---|
| 9 | # |
|---|
| 10 | # Unless required by applicable law or agreed to in writing, software |
|---|
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, |
|---|
| 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
|---|
| 13 | # implied. See the License for the specific language governing |
|---|
| 14 | # permissions and limitations under the License. |
|---|
| 15 | # |
|---|
| 16 | # Originally developed by Gregory Trubetskoy. |
|---|
| 17 | # |
|---|
| 18 | # $Id: util.py,v 1.21 2004/02/16 19:47:27 grisha Exp $ |
|---|
| 19 | |
|---|
| 20 | import _apache |
|---|
| 21 | import apache |
|---|
| 22 | import cStringIO |
|---|
| 23 | import tempfile |
|---|
| 24 | |
|---|
| 25 | from types import * |
|---|
| 26 | from exceptions import * |
|---|
| 27 | |
|---|
| 28 | parse_qs = _apache.parse_qs |
|---|
| 29 | parse_qsl = _apache.parse_qsl |
|---|
| 30 | |
|---|
| 31 | """ The classes below are a (almost) a drop-in replacement for the |
|---|
| 32 | standard cgi.py FieldStorage class. They should have pretty much the |
|---|
| 33 | same functionality. |
|---|
| 34 | |
|---|
| 35 | These classes differ in that unlike cgi.FieldStorage, they are not |
|---|
| 36 | recursive. The class FieldStorage contains a list of instances of |
|---|
| 37 | Field class. Field class is incapable of storing anything in it. |
|---|
| 38 | |
|---|
| 39 | These objects should be considerably faster than the ones in cgi.py |
|---|
| 40 | because they do not expect CGI environment, and are |
|---|
| 41 | optimized specifically for Apache and mod_python. |
|---|
| 42 | """ |
|---|
| 43 | |
|---|
| 44 | class Field: |
|---|
| 45 | |
|---|
| 46 | filename = None |
|---|
| 47 | headers = {} |
|---|
| 48 | |
|---|
| 49 | def __init__(self, name, file, ctype, type_options, |
|---|
| 50 | disp, disp_options): |
|---|
| 51 | self.name = name |
|---|
| 52 | self.file = file |
|---|
| 53 | self.type = ctype |
|---|
| 54 | self.type_options = type_options |
|---|
| 55 | self.disposition = disp |
|---|
| 56 | self.disposition_options = disp_options |
|---|
| 57 | |
|---|
| 58 | def __repr__(self): |
|---|
| 59 | """Return printable representation.""" |
|---|
| 60 | return "Field(%s, %s)" % (`self.name`, `self.value`) |
|---|
| 61 | |
|---|
| 62 | def __getattr__(self, name): |
|---|
| 63 | if name != 'value': |
|---|
| 64 | raise AttributeError, name |
|---|
| 65 | if self.file: |
|---|
| 66 | self.file.seek(0) |
|---|
| 67 | value = self.file.read() |
|---|
| 68 | self.file.seek(0) |
|---|
| 69 | else: |
|---|
| 70 | value = None |
|---|
| 71 | return value |
|---|
| 72 | |
|---|
| 73 | def __del__(self): |
|---|
| 74 | self.file.close() |
|---|
| 75 | |
|---|
| 76 | class StringField(str): |
|---|
| 77 | """ This class is basically a string with |
|---|
| 78 | a value attribute for compatibility with std lib cgi.py |
|---|
| 79 | """ |
|---|
| 80 | |
|---|
| 81 | def __init__(self, str=""): |
|---|
| 82 | str.__init__(self, str) |
|---|
| 83 | self.value = self.__str__() |
|---|
| 84 | |
|---|
| 85 | class FieldStorage: |
|---|
| 86 | |
|---|
| 87 | def __init__(self, req, keep_blank_values=0, strict_parsing=0): |
|---|
| 88 | |
|---|
| 89 | self.list = [] |
|---|
| 90 | |
|---|
| 91 | # always process GET-style parameters |
|---|
| 92 | if req.args: |
|---|
| 93 | pairs = parse_qsl(req.args, keep_blank_values) |
|---|
| 94 | for pair in pairs: |
|---|
| 95 | file = cStringIO.StringIO(pair[1]) |
|---|
| 96 | self.list.append(Field(pair[0], file, "text/plain", {}, |
|---|
| 97 | None, {})) |
|---|
| 98 | |
|---|
| 99 | if req.method == "POST": |
|---|
| 100 | |
|---|
| 101 | try: |
|---|
| 102 | clen = int(req.headers_in["content-length"]) |
|---|
| 103 | except (KeyError, ValueError): |
|---|
| 104 | # absent content-length is not acceptable |
|---|
| 105 | raise apache.SERVER_RETURN, apache.HTTP_LENGTH_REQUIRED |
|---|
| 106 | |
|---|
| 107 | if not req.headers_in.has_key("content-type"): |
|---|
| 108 | ctype = "application/x-www-form-urlencoded" |
|---|
| 109 | else: |
|---|
| 110 | ctype = req.headers_in["content-type"] |
|---|
| 111 | |
|---|
| 112 | if ctype == "application/x-www-form-urlencoded": |
|---|
| 113 | |
|---|
| 114 | pairs = parse_qsl(req.read(clen), keep_blank_values) |
|---|
| 115 | for pair in pairs: |
|---|
| 116 | file = cStringIO.StringIO(pair[1]) |
|---|
| 117 | self.list.append(Field(pair[0], file, "text/plain", |
|---|
| 118 | {}, None, {})) |
|---|
| 119 | |
|---|
| 120 | elif ctype[:10] == "multipart/": |
|---|
| 121 | |
|---|
| 122 | # figure out boundary |
|---|
| 123 | try: |
|---|
| 124 | i = ctype.lower().rindex("boundary=") |
|---|
| 125 | boundary = ctype[i+9:] |
|---|
| 126 | if len(boundary) >= 2 and boundary[0] == boundary[-1] == '"': |
|---|
| 127 | boundary = boundary[1:-1] |
|---|
| 128 | boundary = "--" + boundary |
|---|
| 129 | except ValueError: |
|---|
| 130 | raise apache.SERVER_RETURN, apache.HTTP_BAD_REQUEST |
|---|
| 131 | |
|---|
| 132 | #read until boundary |
|---|
| 133 | line = req.readline() |
|---|
| 134 | sline = line.strip() |
|---|
| 135 | while line and sline != boundary: |
|---|
| 136 | line = req.readline() |
|---|
| 137 | sline = line.strip() |
|---|
| 138 | |
|---|
| 139 | while 1: |
|---|
| 140 | |
|---|
| 141 | ## parse headers |
|---|
| 142 | |
|---|
| 143 | ctype, type_options = "text/plain", {} |
|---|
| 144 | disp, disp_options = None, {} |
|---|
| 145 | headers = apache.make_table() |
|---|
| 146 | |
|---|
| 147 | line = req.readline() |
|---|
| 148 | sline = line.strip() |
|---|
| 149 | if not line or sline == (boundary + "--"): |
|---|
| 150 | break |
|---|
| 151 | |
|---|
| 152 | while line and line not in ["\n", "\r\n"]: |
|---|
| 153 | h, v = line.split(":", 1) |
|---|
| 154 | headers.add(h, v) |
|---|
| 155 | h = h.lower() |
|---|
| 156 | if h == "content-disposition": |
|---|
| 157 | disp, disp_options = parse_header(v) |
|---|
| 158 | elif h == "content-type": |
|---|
| 159 | ctype, type_options = parse_header(v) |
|---|
| 160 | line = req.readline() |
|---|
| 161 | sline = line.strip() |
|---|
| 162 | |
|---|
| 163 | if disp_options.has_key("name"): |
|---|
| 164 | name = disp_options["name"] |
|---|
| 165 | else: |
|---|
| 166 | name = None |
|---|
| 167 | |
|---|
| 168 | # is this a file? |
|---|
| 169 | if disp_options.has_key("filename"): |
|---|
| 170 | file = self.make_file() |
|---|
| 171 | else: |
|---|
| 172 | file = cStringIO.StringIO() |
|---|
| 173 | |
|---|
| 174 | # read it in |
|---|
| 175 | self.read_to_boundary(req, boundary, file) |
|---|
| 176 | file.seek(0) |
|---|
| 177 | |
|---|
| 178 | # make a Field |
|---|
| 179 | field = Field(name, file, ctype, type_options, |
|---|
| 180 | disp, disp_options) |
|---|
| 181 | field.headers = headers |
|---|
| 182 | if disp_options.has_key("filename"): |
|---|
| 183 | field.filename = disp_options["filename"] |
|---|
| 184 | |
|---|
| 185 | self.list.append(field) |
|---|
| 186 | |
|---|
| 187 | else: |
|---|
| 188 | # we don't understand this content-type |
|---|
| 189 | raise apache.SERVER_RETURN, apache.HTTP_NOT_IMPLEMENTED |
|---|
| 190 | |
|---|
| 191 | |
|---|
| 192 | def make_file(self): |
|---|
| 193 | return tempfile.TemporaryFile("w+b") |
|---|
| 194 | |
|---|
| 195 | def skip_to_boundary(self, req, boundary): |
|---|
| 196 | line = req.readline() |
|---|
| 197 | sline = line.strip() |
|---|
| 198 | last_bound = boundary + "--" |
|---|
| 199 | while line and sline != boundary and sline != last_bound: |
|---|
| 200 | line = req.readline() |
|---|
| 201 | sline = line.strip() |
|---|
| 202 | |
|---|
| 203 | def read_to_boundary(self, req, boundary, file): |
|---|
| 204 | delim = "" |
|---|
| 205 | line = req.readline() |
|---|
| 206 | sline = line.strip() |
|---|
| 207 | last_bound = boundary + "--" |
|---|
| 208 | while line and sline != boundary and sline != last_bound: |
|---|
| 209 | odelim = delim |
|---|
| 210 | if line[-2:] == "\r\n": |
|---|
| 211 | delim = "\r\n" |
|---|
| 212 | line = line[:-2] |
|---|
| 213 | elif line[-1:] == "\n": |
|---|
| 214 | delim = "\n" |
|---|
| 215 | line = line[:-1] |
|---|
| 216 | file.write(odelim + line) |
|---|
| 217 | line = req.readline() |
|---|
| 218 | sline = line.strip() |
|---|
| 219 | |
|---|
| 220 | def __getitem__(self, key): |
|---|
| 221 | """Dictionary style indexing.""" |
|---|
| 222 | if self.list is None: |
|---|
| 223 | raise TypeError, "not indexable" |
|---|
| 224 | found = [] |
|---|
| 225 | for item in self.list: |
|---|
| 226 | if item.name == key: |
|---|
| 227 | if isinstance(item.file, FileType): |
|---|
| 228 | found.append(item) |
|---|
| 229 | else: |
|---|
| 230 | found.append(StringField(item.value)) |
|---|
| 231 | if not found: |
|---|
| 232 | raise KeyError, key |
|---|
| 233 | if len(found) == 1: |
|---|
| 234 | return found[0] |
|---|
| 235 | else: |
|---|
| 236 | return found |
|---|
| 237 | |
|---|
| 238 | def get(self, key, default): |
|---|
| 239 | try: |
|---|
| 240 | return self.__getitem__(key) |
|---|
| 241 | except KeyError: |
|---|
| 242 | return default |
|---|
| 243 | |
|---|
| 244 | def keys(self): |
|---|
| 245 | """Dictionary style keys() method.""" |
|---|
| 246 | if self.list is None: |
|---|
| 247 | raise TypeError, "not indexable" |
|---|
| 248 | keys = [] |
|---|
| 249 | for item in self.list: |
|---|
| 250 | if item.name not in keys: keys.append(item.name) |
|---|
| 251 | return keys |
|---|
| 252 | |
|---|
| 253 | def has_key(self, key): |
|---|
| 254 | """Dictionary style has_key() method.""" |
|---|
| 255 | if self.list is None: |
|---|
| 256 | raise TypeError, "not indexable" |
|---|
| 257 | for item in self.list: |
|---|
| 258 | if item.name == key: return 1 |
|---|
| 259 | return 0 |
|---|
| 260 | |
|---|
| 261 | __contains__ = has_key |
|---|
| 262 | |
|---|
| 263 | def __len__(self): |
|---|
| 264 | """Dictionary style len(x) support.""" |
|---|
| 265 | return len(self.keys()) |
|---|
| 266 | |
|---|
| 267 | def getfirst(self, key, default=None): |
|---|
| 268 | """ return the first value received """ |
|---|
| 269 | for item in self.list: |
|---|
| 270 | if item.name == key: |
|---|
| 271 | if isinstance(item.file, FileType): |
|---|
| 272 | return item |
|---|
| 273 | else: |
|---|
| 274 | return StringField(item.value) |
|---|
| 275 | return default |
|---|
| 276 | |
|---|
| 277 | def getlist(self, key): |
|---|
| 278 | """ return a list of received values """ |
|---|
| 279 | if self.list is None: |
|---|
| 280 | raise TypeError, "not indexable" |
|---|
| 281 | found = [] |
|---|
| 282 | for item in self.list: |
|---|
| 283 | if item.name == key: |
|---|
| 284 | if isinstance(item.file, FileType): |
|---|
| 285 | found.append(item) |
|---|
| 286 | else: |
|---|
| 287 | found.append(StringField(item.value)) |
|---|
| 288 | return found |
|---|
| 289 | |
|---|
| 290 | def parse_header(line): |
|---|
| 291 | """Parse a Content-type like header. |
|---|
| 292 | |
|---|
| 293 | Return the main content-type and a dictionary of options. |
|---|
| 294 | |
|---|
| 295 | """ |
|---|
| 296 | |
|---|
| 297 | plist = map(lambda a: a.strip(), line.split(';')) |
|---|
| 298 | key = plist[0].lower() |
|---|
| 299 | del plist[0] |
|---|
| 300 | pdict = {} |
|---|
| 301 | for p in plist: |
|---|
| 302 | i = p.find('=') |
|---|
| 303 | if i >= 0: |
|---|
| 304 | name = p[:i].strip().lower() |
|---|
| 305 | value = p[i+1:].strip() |
|---|
| 306 | if len(value) >= 2 and value[0] == value[-1] == '"': |
|---|
| 307 | value = value[1:-1] |
|---|
| 308 | pdict[name] = value |
|---|
| 309 | return key, pdict |
|---|
| 310 | |
|---|
| 311 | def apply_fs_data(object, fs, **args): |
|---|
| 312 | """ |
|---|
| 313 | Apply FieldStorage data to an object - the object must be |
|---|
| 314 | callable. Examine the args, and match then with fs data, |
|---|
| 315 | then call the object, return the result. |
|---|
| 316 | """ |
|---|
| 317 | |
|---|
| 318 | # add form data to args |
|---|
| 319 | for field in fs.list: |
|---|
| 320 | if field.filename: |
|---|
| 321 | val = field |
|---|
| 322 | else: |
|---|
| 323 | val = field.value |
|---|
| 324 | args.setdefault(field.name, []).append(val) |
|---|
| 325 | |
|---|
| 326 | # replace lists with single values |
|---|
| 327 | for arg in args: |
|---|
| 328 | if ((type(args[arg]) is ListType) and |
|---|
| 329 | (len(args[arg]) == 1)): |
|---|
| 330 | args[arg] = args[arg][0] |
|---|
| 331 | |
|---|
| 332 | # we need to weed out unexpected keyword arguments |
|---|
| 333 | # and for that we need to get a list of them. There |
|---|
| 334 | # are a few options for callable objects here: |
|---|
| 335 | |
|---|
| 336 | if type(object) is InstanceType: |
|---|
| 337 | # instances are callable when they have __call__() |
|---|
| 338 | object = object.__call__ |
|---|
| 339 | |
|---|
| 340 | expected = [] |
|---|
| 341 | if hasattr(object, "func_code"): |
|---|
| 342 | # function |
|---|
| 343 | fc = object.func_code |
|---|
| 344 | expected = fc.co_varnames[0:fc.co_argcount] |
|---|
| 345 | elif hasattr(object, 'im_func'): |
|---|
| 346 | # method |
|---|
| 347 | fc = object.im_func.func_code |
|---|
| 348 | expected = fc.co_varnames[1:fc.co_argcount] |
|---|
| 349 | elif type(object) is ClassType: |
|---|
| 350 | # class |
|---|
| 351 | fc = object.__init__.im_func.func_code |
|---|
| 352 | expected = fc.co_varnames[1:fc.co_argcount] |
|---|
| 353 | |
|---|
| 354 | # remove unexpected args unless co_flags & 0x08, |
|---|
| 355 | # meaning function accepts **kw syntax |
|---|
| 356 | if not (fc.co_flags & 0x08): |
|---|
| 357 | for name in args.keys(): |
|---|
| 358 | if name not in expected: |
|---|
| 359 | del args[name] |
|---|
| 360 | |
|---|
| 361 | return object(**args) |
|---|
| 362 | |
|---|
| 363 | def redirect(req, location, permanent=0, text=None): |
|---|
| 364 | """ |
|---|
| 365 | A convenience function to provide redirection |
|---|
| 366 | """ |
|---|
| 367 | |
|---|
| 368 | if req.sent_bodyct: |
|---|
| 369 | raise IOError, "Cannot redirect after headers have already been sent." |
|---|
| 370 | |
|---|
| 371 | req.err_headers_out["Location"] = location |
|---|
| 372 | if permanent: |
|---|
| 373 | req.status = apache.HTTP_MOVED_PERMANENTLY |
|---|
| 374 | else: |
|---|
| 375 | req.status = apache.HTTP_MOVED_TEMPORARILY |
|---|
| 376 | |
|---|
| 377 | if text is None: |
|---|
| 378 | req.write('<p>The document has moved' |
|---|
| 379 | ' <a href="%s">here</a></p>\n' |
|---|
| 380 | % location) |
|---|
| 381 | else: |
|---|
| 382 | req.write(text) |
|---|
| 383 | |
|---|
| 384 | raise apache.SERVER_RETURN, apache.OK |
|---|