Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/branches/cherrypy-2.x/cherrypy/lib/cptools.py

Revision 2662 (checked in by fs, 5 months ago)

remove DeprecationWarnings? on Python 2.6+ by importing hashlib first (instead of the md5/sha modules)

Patch by Toshio Kuratomi, imported from Fedora CVS, licensed under the same terms as CherryPy 2.3

  • Property svn:eol-style set to native
Line 
1 """Tools which both CherryPy and application developers may invoke."""
2 try:
3     from hashlib import md5
4 except ImportError:
5     from md5 import new as md5
6 import mimetools
7 import mimetypes
8 mimetypes.init()
9 mimetypes.types_map['.dwg']='image/x-dwg'
10 mimetypes.types_map['.ico']='image/x-icon'
11
12 import os
13 import re
14 import stat as _stat
15 import sys
16 import time
17
18 import cherrypy
19 import httptools
20
21 from cherrypy.filters.wsgiappfilter import WSGIAppFilter
22
23
24 def decorate(func, decorator):
25     """
26     Return the decorated func. This will automatically copy all
27     non-standard attributes (like exposed) to the newly decorated function.
28     """
29     newfunc = decorator(func)
30     for key in dir(func):
31         if not hasattr(newfunc, key):
32             setattr(newfunc, key, getattr(func, key))
33     return newfunc
34
35 def decorateAll(obj, decorator):
36     """
37     Recursively decorate all exposed functions of obj and all of its children,
38     grandchildren, etc. If you used to use aspects, you might want to look
39     into these. This function modifies obj; there is no return value.
40     """
41     obj_type = type(obj)
42     for key in dir(obj):
43         # only deal with user-defined attributes
44         if hasattr(obj_type, key):
45             value = getattr(obj, key)
46             if callable(value) and getattr(value, "exposed", False):
47                 setattr(obj, key, decorate(value, decorator))
48             decorateAll(value, decorator)
49
50
51 class ExposeItems:
52     """
53     Utility class that exposes a getitem-aware object. It does not provide
54     index() or default() methods, and it does not expose the individual item
55     objects - just the list or dict that contains them. User-specific index()
56     and default() methods can be implemented by inheriting from this class.
57     
58     Use case:
59     
60     from cherrypy.lib.cptools import ExposeItems
61     ...
62     cherrypy.root.foo = ExposeItems(mylist)
63     cherrypy.root.bar = ExposeItems(mydict)
64     """
65     exposed = True
66     def __init__(self, items):
67         self.items = items
68     def __getattr__(self, key):
69         return self.items[key]
70
71
72 #                     Conditional HTTP request support                     #
73
74 def validate_etags(autotags=False):
75     """Validate the current ETag against If-Match, If-None-Match headers.
76     
77     If autotags is True, an ETag response-header value will be provided
78     from an MD5 hash of the response body (unless some other code has
79     already provided an ETag header). If False (the default), the ETag
80     will not be automatic.
81     
82     WARNING: the autotags feature is not designed for URL's which allow
83     methods other than GET. For example, if a POST to the same URL returns
84     no content, the automatic ETag will be incorrect, breaking a fundamental
85     use for entity tags in a possibly destructive fashion. Likewise, if you
86     raise 304 Not Modified, the response body will be empty, the ETag hash
87     will be incorrect, and your application will break.
88     See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24
89     """
90     response = cherrypy.response
91    
92     # Guard against being run twice.
93     if hasattr(response, "ETag"):
94         return
95    
96     status, reason, msg = httptools.validStatus(response.status)
97    
98     etag = response.headers.get('ETag')
99    
100     # Automatic ETag generation. See warning in docstring.
101     if (not etag) and autotags:
102         if status == 200:
103             etag = response.collapse_body()
104             etag = '"%s"' % md5(etag).hexdigest()
105             response.headers['ETag'] = etag
106    
107     response.ETag = etag
108    
109     # "If the request would, without the If-Match header field, result in
110     # anything other than a 2xx or 412 status, then the If-Match header
111     # MUST be ignored."
112     if status >= 200 and status <= 299:
113         request = cherrypy.request
114        
115         conditions = request.headers.elements('If-Match') or []
116         conditions = [str(x) for x in conditions]
117         if conditions and not (conditions == ["*"] or etag in conditions):
118             raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did "
119                                      "not match %r" % (etag, conditions))
120        
121         conditions = request.headers.elements('If-None-Match') or []
122         conditions = [str(x) for x in conditions]
123         if conditions == ["*"] or etag in conditions:
124             if request.method in ("GET", "HEAD"):
125                 raise cherrypy.HTTPRedirect([], 304)
126             else:
127                 raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r "
128                                          "matched %r" % (etag, conditions))
129
130 def validate_since():
131     """Validate the current Last-Modified against If-Modified-Since headers.
132     
133     If no code has set the Last-Modified response header, then no validation
134     will be performed.
135     """
136     response = cherrypy.response
137     lastmod = response.headers.get('Last-Modified')
138     if lastmod:
139         status, reason, msg = httptools.validStatus(response.status)
140        
141         request = cherrypy.request
142        
143         since = request.headers.get('If-Unmodified-Since')
144         if since and since != lastmod:
145             if (status >= 200 and status <= 299) or status == 412:
146                 raise cherrypy.HTTPError(412)
147        
148         since = request.headers.get('If-Modified-Since')
149         if since and since == lastmod:
150             if (status >= 200 and status <= 299) or status == 304:
151                 if request.method in ("GET", "HEAD"):
152                     raise cherrypy.HTTPRedirect([], 304)
153                 else:
154                     raise cherrypy.HTTPError(412)
155
156
157 def modified_since(path, stat=None):
158     """Check whether a file has been modified since the date
159     provided in 'If-Modified-Since'
160     It doesn't check if the file exists or not
161     Return True if has been modified, False otherwise
162     """
163     # serveFile already creates a stat object so let's not
164     # waste our energy to do it again
165     if not stat:
166         try:
167             stat = os.stat(path)
168         except OSError:
169             if cherrypy.config.get('server.log_file_not_found', False):
170                 cherrypy.log("    NOT FOUND file: %s" % path, "DEBUG")
171             raise cherrypy.NotFound()
172    
173     response = cherrypy.response
174     strModifTime = httptools.HTTPDate(time.gmtime(stat.st_mtime))
175     if cherrypy.request.headers.has_key('If-Modified-Since'):
176         if cherrypy.request.headers['If-Modified-Since'] == strModifTime:
177             raise cherrypy.HTTPRedirect([], 304)
178     response.headers['Last-Modified'] = strModifTime
179     return True
180
181 def serveFile(path, contentType=None, disposition=None, name=None):
182     """Set status, headers, and body in order to serve the given file.
183     
184     The Content-Type header will be set to the contentType arg, if provided.
185     If not provided, the Content-Type will be guessed by its extension.
186     
187     If disposition is not None, the Content-Disposition header will be set
188     to "<disposition>; filename=<name>". If name is None, it will be set
189     to the basename of path. If disposition is None, no Content-Disposition
190     header will be written.
191     """
192    
193     response = cherrypy.response
194    
195     # If path is relative, users should fix it by making path absolute.
196     # That is, CherryPy should not guess where the application root is.
197     # It certainly should *not* use cwd (since CP may be invoked from a
198     # variety of paths). If using static_filter, you can make your relative
199     # paths become absolute by supplying a value for "static_filter.root".
200     if not os.path.isabs(path):
201         raise ValueError("'%s' is not an absolute path." % path)
202    
203     try:
204         stat = os.stat(path)
205     except OSError:
206         if cherrypy.config.get('server.log_file_not_found', False):
207             cherrypy.log("    NOT FOUND file: %s" % path, "DEBUG")
208         raise cherrypy.NotFound()
209    
210     # Check if path is a directory.
211     if _stat.S_ISDIR(stat.st_mode):
212         # Let the caller deal with it as they like.
213         raise cherrypy.NotFound()
214    
215     if contentType is None:
216         # Set content-type based on filename extension
217         ext = ""
218         i = path.rfind('.')
219         if i != -1:
220             ext = path[i:].lower()
221         contentType = mimetypes.types_map.get(ext, "text/plain")
222     response.headers['Content-Type'] = contentType
223    
224     # Set the Last-Modified response header, so that
225     # modified-since validation code can work.
226     response.headers['Last-Modified'] = httptools.HTTPDate(time.gmtime(stat.st_mtime))
227     validate_since()
228    
229     if disposition is not None:
230         if name is None:
231             name = os.path.basename(path)
232         cd = '%s; filename="%s"' % (disposition, name)
233         response.headers["Content-Disposition"] = cd
234    
235     # Set Content-Length and use an iterable (file object)
236     #   this way CP won't load the whole file in memory
237     c_len = stat.st_size
238     bodyfile = open(path, 'rb')
239     if getattr(cherrypy, "debug", None):
240         cherrypy.log("    Found file: %s" % path, "DEBUG")
241    
242     # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
243     if cherrypy.response.version >= "1.1":
244         response.headers["Accept-Ranges"] = "bytes"
245         r = httptools.getRanges(cherrypy.request.headers.get('Range'), c_len)
246         if r == []:
247             response.headers['Content-Range'] = "bytes */%s" % c_len
248             message = "Invalid Range (first-byte-pos greater than Content-Length)"
249             raise cherrypy.HTTPError(416, message)
250         if r:
251             if len(r) == 1:
252                 # Return a single-part response.
253                 start, stop = r[0]
254                 r_len = stop - start
255                 response.status = "206 Partial Content"
256                 response.headers['Content-Range'] = ("bytes %s-%s/%s" %
257                                                        (start, stop - 1, c_len))
258                 response.headers['Content-Length'] = r_len
259                 bodyfile.seek(start)
260                 response.body = bodyfile.read(r_len)
261             else:
262                 # Return a multipart/byteranges response.
263                 response.status = "206 Partial Content"
264                 boundary = mimetools.choose_boundary()
265                 ct = "multipart/byteranges; boundary=%s" % boundary
266                 response.headers['Content-Type'] = ct
267 ##                del response.headers['Content-Length']
268                
269                 def fileRanges():
270                     # Apache compatibility:
271                     yield "\r\n"
272                    
273                     for start, stop in r:
274                         yield "--" + boundary
275                         yield "\r\nContent-type: %s" % contentType
276                         yield ("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
277                                % (start, stop - 1, c_len))
278                         bodyfile.seek(start)
279                         yield bodyfile.read(stop - start)
280                         yield "\r\n"
281                     # Final boundary
282                     yield "--" + boundary + "--"
283                    
284                     # Apache compatibility:
285                     yield "\r\n"
286                 response.body = fileRanges()
287         else:
288             response.headers['Content-Length'] = c_len
289             response.body = bodyfile
290     else:
291         response.headers['Content-Length'] = c_len
292         response.body = bodyfile
293     return response.body
294 serve_file = serveFile
295
296 def serve_download(path, name=None):
297     """Serve 'path' as an application/x-download attachment."""
298     # This is such a common idiom I felt it deserved its own wrapper.
299     return serve_file(path, "application/x-download", "attachment", name)
300
301 def fileGenerator(input, chunkSize=65536):
302     """Yield the given input (a file object) in chunks (default 64k)."""
303     chunk = input.read(chunkSize)
304     while chunk:
305         yield chunk
306         chunk = input.read(chunkSize)
307     input.close()
308
309 def modules(modulePath):
310     """Load a module and retrieve a reference to that module."""
311     try:
312         mod = sys.modules[modulePath]
313         if mod is None:
314             raise KeyError()
315     except KeyError:
316         # The last [''] is important.
317         mod = __import__(modulePath, globals(), locals(), [''])
318     return mod
319
320 def attributes(fullAttributeName):
321     """Load a module and retrieve an attribute of that module."""
322    
323     # Parse out the path, module, and attribute
324     lastDot = fullAttributeName.rfind(u".")
325     attrName = fullAttributeName[lastDot + 1:]
326     modPath = fullAttributeName[:lastDot]
327    
328     aMod = modules(modPath)
329     # Let an AttributeError propagate outward.
330     try:
331         attr = getattr(aMod, attrName)
332     except AttributeError:
333         raise AttributeError("'%s' object has no attribute '%s'"
334                              % (modPath, attrName))
335    
336     # Return a reference to the attribute.
337     return attr
338
339
340 class WSGIApp(object):
341     """a convenience class that uses the WSGIAppFilter
342     
343     to easily add a WSGI application to the CP object tree.
344
345     example:
346     cherrypy.tree.mount(SomeRoot(), '/')
347     cherrypy.tree.mount(WSGIApp(other_wsgi_app), '/ext_app')
348     """
349     def __init__(self, app, env_update=None):
350         self._cpFilterList = [WSGIAppFilter(app, env_update)]
351
352
353 # public domain "unrepr" implementation, found on the web and then improved.
354
355 def getObj(s):
356     try:
357         import compiler
358     except ImportError:
359         # Fallback to eval when compiler package is not available,
360         # e.g. IronPython 1.0.
361         return eval(s)
362    
363     s = "a=" + s
364     p = compiler.parse(s)
365     return p.getChildren()[1].getChildren()[0].getChildren()[1]
366
367
368 class UnknownType(Exception):
369     pass
370
371
372 class Builder:
373    
374     def build(self, o):
375         m = getattr(self, 'build_' + o.__class__.__name__, None)
376         if m is None:
377             raise UnknownType(o.__class__.__name__)
378         return m(o)
379    
380     def build_CallFunc(self, o):
381         callee, args, starargs, kwargs = map(self.build, o.getChildren())
382         return callee(args, *(starargs or ()), **(kwargs or {}))
383    
384     def build_List(self, o):
385         return map(self.build, o.getChildren())
386    
387     def build_Const(self, o):
388         return o.value
389    
390     def build_Dict(self, o):
391         d = {}
392         i = iter(map(self.build, o.getChildren()))
393         for el in i:
394             d[el] = i.next()
395         return d
396    
397     def build_Tuple(self, o):
398         return tuple(self.build_List(o))
399    
400     def build_Name(self, o):
401         if o.name == 'None':
402             return None
403         if o.name == 'True':
404             return True
405         if o.name == 'False':
406             return False
407        
408         # See if the Name is a package or module
409         try:
410             return modules(o.name)
411         except ImportError:
412             pass
413        
414         raise UnknownType(o.name)
415    
416     def build_Add(self, o):
417         real, imag = map(self.build_Const, o.getChildren())
418         try:
419             real = float(real)
420         except TypeError:
421             raise UnknownType('Add')
422         if not isinstance(imag, complex) or imag.real != 0.0:
423             raise UnknownType('Add')
424         return real+imag
425    
426     def build_Getattr(self, o):
427         parent = self.build(o.expr)
428         return getattr(parent, o.attrname)
429    
430     def build_NoneType(self, o):
431         return None
432    
433     def build_UnarySub(self, o):
434         return -self.build_Const(o.getChildren()[0])
435    
436     def build_UnaryAdd(self, o):
437         return self.build_Const(o.getChildren()[0])
438
439
440 def unrepr(s):
441     if not s:
442         return s
443     return Builder().build(getObj(s))
444
445
446 def referer(pattern, accept=True, accept_missing=False, error=403,
447             message='Forbidden Referer header.'):
448     """Raise HTTPError if Referer header does not pass our test.
449     
450     pattern: a regular expression pattern to test against the Referer.
451     accept: if True, the Referer must match the pattern; if False,
452         the Referer must NOT match the pattern.
453     accept_missing: if True, permit requests with no Referer header.
454     error: the HTTP error code to return to the client on failure.
455     message: a string to include in the response body on failure.
456     """
457     try:
458         match = bool(re.match(pattern, cherrypy.request.headers['Referer']))
459         if accept == match:
460             return
461     except KeyError:
462         if accept_missing:
463             return
464    
465     raise cherrypy.HTTPError(error, message)
466
467 def accept(media=None):
468     """Return the client's preferred media-type (from the given Content-Types).
469     
470     If 'media' is None (the default), no test will be performed.
471     
472     If 'media' is provided, it should be the Content-Type value (as a string)
473     or values (as a list or tuple of strings) which the current request
474     can emit. The client's acceptable media ranges (as declared in the
475     Accept request header) will be matched in order to these Content-Type
476     values; the first such string is returned. That is, the return value
477     will always be one of the strings provided in the 'media' arg (or None
478     if 'media' is None).
479     
480     If no match is found, then HTTPError 406 (Not Acceptable) is raised.
481     Note that most web browsers send */* as a (low-quality) acceptable
482     media range, which should match any Content-Type. In addition, "...if
483     no Accept header field is present, then it is assumed that the client
484     accepts all media types."
485     
486     Matching types are checked in order of client preference first,
487     and then in the order of the given 'media' values.
488     
489     Note that this function does not honor accept-params (other than "q").
490     """
491     if not media:
492         return
493     if isinstance(media, basestring):
494         media = [media]
495    
496     # Parse the Accept request header, and try to match one
497     # of the requested media-ranges (in order of preference).
498     ranges = cherrypy.request.headers.elements('Accept')
499     if not ranges:
500         # Any media type is acceptable.
501         return media[0]
502     else:
503         # Note that 'ranges' is sorted in order of preference
504         for element in ranges:
505             if element.qvalue > 0:
506                 if element.value == "*/*":
507                     # Matches any type or subtype
508                     return media[0]
509                 elif element.value.endswith("/*"):
510                     # Matches any subtype
511                     mtype = element.value[:-1]  # Keep the slash
512                     for m in media:
513                         if m.startswith(mtype):
514                             return m
515                 else:
516                     # Matches exact value
517                     if element.value in media:
518                         return element.value
519    
520     # No suitable media-range found.
521     ah = cherrypy.request.headers.get('Accept')
522     if ah is None:
523         msg = "Your client did not send an Accept header."
524     else:
525         msg = "Your client sent this Accept header: %s." % ah
526     msg += (" But this resource only emits these media types: %s." %
527             ", ".join(media))
528     raise cherrypy.HTTPError(406, msg)
529
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets