Commits

Robert Brewer committed 4e078bb

2.x backport of [1330] (new cptools.referer) and [1422] (new cptools.accept(media)). See test_misc_tools.py for usage.

Comments (0)

Files changed (3)

cherrypy/lib/cptools.py

 mimetypes.types_map['.ico']='image/x-icon'
 
 import os
+import re
 import stat as _stat
 import sys
 import time
         return s
     return Builder().build(getObj(s))
 
+
+def referer(pattern, accept=True, accept_missing=False, error=403,
+            message='Forbidden Referer header.'):
+    """Raise HTTPError if Referer header does not pass our test.
+    
+    pattern: a regular expression pattern to test against the Referer.
+    accept: if True, the Referer must match the pattern; if False,
+        the Referer must NOT match the pattern.
+    accept_missing: if True, permit requests with no Referer header.
+    error: the HTTP error code to return to the client on failure.
+    message: a string to include in the response body on failure.
+    """
+    try:
+        match = bool(re.match(pattern, cherrypy.request.headers['Referer']))
+        if accept == match:
+            return
+    except KeyError:
+        if accept_missing:
+            return
+    
+    raise cherrypy.HTTPError(error, message)
+
+def accept(media=None):
+    """Return the client's preferred media-type (from the given Content-Types).
+    
+    If 'media' is None (the default), no test will be performed.
+    
+    If 'media' is provided, it should be the Content-Type value (as a string)
+    or values (as a list or tuple of strings) which the current request
+    can emit. The client's acceptable media ranges (as declared in the
+    Accept request header) will be matched in order to these Content-Type
+    values; the first such string is returned. That is, the return value
+    will always be one of the strings provided in the 'media' arg (or None
+    if 'media' is None).
+    
+    If no match is found, then HTTPError 406 (Not Acceptable) is raised.
+    Note that most web browsers send */* as a (low-quality) acceptable
+    media range, which should match any Content-Type. In addition, "...if
+    no Accept header field is present, then it is assumed that the client
+    accepts all media types."
+    
+    Matching types are checked in order of client preference first,
+    and then in the order of the given 'media' values.
+    
+    Note that this function does not honor accept-params (other than "q").
+    """
+    if not media:
+        return
+    if isinstance(media, basestring):
+        media = [media]
+    
+    # Parse the Accept request header, and try to match one
+    # of the requested media-ranges (in order of preference).
+    ranges = cherrypy.request.headers.elements('Accept')
+    if not ranges:
+        # Any media type is acceptable.
+        return media[0]
+    else:
+        # Note that 'ranges' is sorted in order of preference
+        for element in ranges:
+            if element.qvalue > 0:
+                if element.value == "*/*":
+                    # Matches any type or subtype
+                    return media[0]
+                elif element.value.endswith("/*"):
+                    # Matches any subtype
+                    mtype = element.value[:-1]  # Keep the slash
+                    for m in media:
+                        if m.startswith(mtype):
+                            return m
+                else:
+                    # Matches exact value
+                    if element.value in media:
+                        return element.value
+    
+    # No suitable media-range found.
+    ah = cherrypy.request.headers.get('Accept')
+    if ah is None:
+        msg = "Your client did not send an Accept header."
+    else:
+        msg = "Your client sent this Accept header: %s." % ah
+    msg += (" But this resource only emits these media types: %s." %
+            ", ".join(media))
+    raise cherrypy.HTTPError(406, msg)
+

cherrypy/test/test.py

         'test_etags',
         'test_gzip_filter',
         'test_logdebuginfo_filter',
+        'test_misc_tools',
         'test_objectmapping',
         'test_response_headers_filter',
         'test_static_filter',

cherrypy/test/test_misc_tools.py

+import test
+test.prefer_parent_path()
+
+import cherrypy
+from cherrypy.lib import cptools
+
+
+def setup_server():
+    class Root:
+        pass
+    
+    
+    class Accept:
+        def index(self):
+            cptools.accept()
+            return '<a href="feed">Atom feed</a>'
+        index.exposed = True
+        
+        def feed(self):
+            cptools.accept(media='application/atom+xml')
+            return """<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+    <title>Unknown Blog</title>
+</feed>"""
+        feed.exposed = True
+        
+        def select(self):
+            mtype = cptools.accept(['text/html', 'text/plain'])
+            if mtype == 'text/html':
+                return "<h2>Page Title</h2>"
+            else:
+                return "PAGE TITLE"
+        select.exposed = True
+    
+    class Referer:
+        def accept(self):
+            cptools.referer(pattern=r'http://[^/]*thisdomain\.com')
+            return "Accepted!"
+        accept.exposed = True
+        
+        def reject(self):
+            cptools.referer(pattern=r'http://[^/]*thisdomain\.com',
+                            accept=False, accept_missing=True)
+            return "Accepted!"
+        reject.exposed = True
+    
+    root = Root()
+    root.referer = Referer()
+    root.accept = Accept()
+    cherrypy.root = root
+    cherrypy.config.update({
+            'server.log_to_screen': False,
+            'server.environment': 'production',
+    })
+
+
+import helper
+
+
+class RefererTest(helper.CPWebCase):
+    
+    def testReferer(self):
+        self.getPage('/referer/accept')
+        self.assertErrorPage(403, 'Forbidden Referer header.')
+        
+        self.getPage('/referer/accept',
+                     headers=[('Referer', 'http://www.thisdomain.com/')])
+        self.assertStatus(200)
+        self.assertBody('Accepted!')
+        
+        # Reject
+        self.getPage('/referer/reject')
+        self.assertStatus(200)
+        self.assertBody('Accepted!')
+        
+        self.getPage('/referer/reject',
+                     headers=[('Referer', 'http://www.thisdomain.com/')])
+        self.assertErrorPage(403, 'Forbidden Referer header.')
+
+
+class AcceptTest(helper.CPWebCase):
+    
+    def test_Accept_Tool(self):
+        # Test with no header provided
+        self.getPage('/accept/feed')
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+        
+        # Specify exact media type
+        self.getPage('/accept/feed', headers=[('Accept', 'application/atom+xml')])
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+        
+        # Specify matching media range
+        self.getPage('/accept/feed', headers=[('Accept', 'application/*')])
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+        
+        # Specify all media ranges
+        self.getPage('/accept/feed', headers=[('Accept', '*/*')])
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+        
+        # Specify unacceptable media types
+        self.getPage('/accept/feed', headers=[('Accept', 'text/html')])
+        self.assertErrorPage(406,
+                             "Your client sent this Accept header: text/html. "
+                             "But this resource only emits these media types: "
+                             "application/atom+xml.")
+        
+        # Test resource where tool is 'on' but media is None (not set).
+        self.getPage('/accept/')
+        self.assertStatus(200)
+        self.assertBody('<a href="feed">Atom feed</a>')
+    
+    def test_accept_selection(self):
+        # Try both our expected media types
+        self.getPage('/accept/select', [('Accept', 'text/html')])
+        self.assertStatus(200)
+        self.assertBody('<h2>Page Title</h2>')
+        self.getPage('/accept/select', [('Accept', 'text/plain')])
+        self.assertStatus(200)
+        self.assertBody('PAGE TITLE')
+        self.getPage('/accept/select', [('Accept', 'text/plain, text/*;q=0.5')])
+        self.assertStatus(200)
+        self.assertBody('PAGE TITLE')
+        
+        # text/* and */* should prefer text/html since it comes first
+        # in our 'media' argument to tools.accept
+        self.getPage('/accept/select', [('Accept', 'text/*')])
+        self.assertStatus(200)
+        self.assertBody('<h2>Page Title</h2>')
+        self.getPage('/accept/select', [('Accept', '*/*')])
+        self.assertStatus(200)
+        self.assertBody('<h2>Page Title</h2>')
+        
+        # Try unacceptable media types
+        self.getPage('/accept/select', [('Accept', 'application/xml')])
+        self.assertErrorPage(406,
+                             "Your client sent this Accept header: application/xml. "
+                             "But this resource only emits these media types: "
+                             "text/html, text/plain.")
+
+
+
+if __name__ == "__main__":
+    setup_server()
+    helper.testmain()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.