Edgewall Software

Ticket #554: util.py

File util.py, 12.1 KB (added by anonymous, 3 years ago)
Line 
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
20import _apache
21import apache
22import cStringIO
23import tempfile
24
25from types import *
26from exceptions import *
27
28parse_qs = _apache.parse_qs
29parse_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
44class 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
76class 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
85class 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
290def 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
311def 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
363def 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