Commits

Robert Brewer committed 82bb438

Fix for #562 (Redirect for slash doesn't use tools.proxy.base). Moved slash checking into new tools.trailing_slash(missing=True, extra=False), which is on by default. The core now sets request.is_index to allow such tools to work. In addition, if that tool is off, mismatched slashes will be corrected in cherrypy.url.

Comments (0)

Files changed (8)

cherrypy/__init__.py

         qs = '?' + qs
     
     if request.app:
-        if path == "":
-            path = request.path_info
-        if not path.startswith("/"):
-            path = _urljoin(request.path_info, path)
+        if path[:1] != "/":
+            # Append/remove trailing slash from path_info as needed
+            # (this is to support mistyped URL's without redirecting;
+            # if you want to redirect, use tools.trailing_slash).
+            pi = request.path_info
+            if request.is_index is True:
+                if pi[-1:] != '/':
+                    pi = pi + '/'
+            elif request.is_index is False:
+                if pi[-1:] == '/' and pi != '/':
+                    pi = pi[:-1]
+            
+            if path == "":
+                path = pi
+            else:
+                path = _urljoin(pi, path)
+        
         if script_name is None:
             script_name = request.app.script_name
         if base is None:

cherrypy/_cpconfig.py

     defaults = {
         'tools.log_tracebacks.on': True,
         'tools.log_headers.on': True,
+        'tools.trailing_slash.on': True,
         }
     
     namespaces = {"server": lambda k, v: setattr(cherrypy.server, k, v),

cherrypy/_cprequest.py

                     conf = getattr(defhandler, "_cp_config", {})
                     object_trail.insert(i+1, ("default", defhandler, conf, curpath))
                     request.config = set_conf()
+                    request.is_index = False
                     return defhandler, names[i:-1]
             
             # Uncomment the next line to restrict positional params to "default".
             if getattr(candidate, 'exposed', False):
                 request.config = set_conf()
                 if i == num_candidates:
-                    # We found the extra ".index". Check that path_info
-                    # has a trailing slash (otherwise, do a redirect).
-                    self.check_missing_slash()
+                    # We found the extra ".index". Mark request so tools
+                    # can redirect if path_info has no trailing slash.
+                    request.is_index = True
                 else:
-                    # We're not at an 'index' handler. Check that path_info
-                    # had NO trailing slash (if it did, do a redirect).
-                    self.check_extra_slash()
+                    # We're not at an 'index' handler. Mark request so tools
+                    # can redirect if path_info has NO trailing slash.
+                    # Note that this also includes handlers which take
+                    # positional parameters (virtual paths).
+                    request.is_index = False
                 return candidate, names[i:-1]
         
         # We didn't find anything
         request.config = set_conf()
         return None, []
-    
-    def check_missing_slash(self):
-        """Redirect if path_info has no trailing slash (if configured)."""
-        request = cherrypy.request
-        pi = request.path_info
-        
-        # Must use config here because configure probably hasn't run yet.
-        if request.config.get("request.redirect_on_missing_slash",
-                              request.redirect_on_missing_slash):
-            if pi[-1:] != '/':
-                new_url = cherrypy.url(pi + '/', request.query_string)
-                raise cherrypy.HTTPRedirect(new_url)
-    
-    def check_extra_slash(self):
-        """Redirect if path_info has trailing slash (if configured)."""
-        request = cherrypy.request
-        pi = request.path_info
-        
-        # Must use config here because configure hasn't run yet.
-        if request.config.get("request.redirect_on_extra_slash",
-                              request.redirect_on_extra_slash):
-            # If pi == '/', don't redirect to ''!
-            if pi[-1:] == '/' and pi != '/':
-                new_url = cherrypy.url(pi[:-1], request.query_string)
-                raise cherrypy.HTTPRedirect(new_url)
 
 
 class MethodDispatcher(Dispatcher):
     toolmap = {}
     config = None
     recursive_redirect = False
-    redirect_on_extra_slash = False
-    redirect_on_missing_slash = True
+    is_index = None
     
     hookpoints = ['on_start_resource', 'before_request_body',
                   'before_handler', 'before_finalize',

cherrypy/_cptools.py

 default_toolbox.referer = Tool('before_request_body', cptools.referer)
 default_toolbox.basicauth = Tool('on_start_resource', auth.basic_auth)
 default_toolbox.digestauth = Tool('on_start_resource', auth.digest_auth)
-
+default_toolbox.trailing_slash = Tool('before_handler', cptools.trailing_slash)
 
 del cptools, encoding, auth, static, tidy

cherrypy/lib/cptools.py

         raise cherrypy.InternalRedirect(url)
     else:
         raise cherrypy.HTTPRedirect(url)
+
+def trailing_slash(missing=True, extra=False):
+    """Redirect if path_info has (missing|extra) trailing slash."""
+    request = cherrypy.request
+    pi = request.path_info
+    
+    if request.is_index is True:
+        if missing:
+            if pi[-1:] != '/':
+                new_url = cherrypy.url(pi + '/', request.query_string)
+                raise cherrypy.HTTPRedirect(new_url)
+    elif request.is_index is False:
+        if extra:
+            # If pi == '/', don't redirect to ''!
+            if pi[-1:] == '/' and pi != '/':
+                new_url = cherrypy.url(pi[:-1], request.query_string)
+                raise cherrypy.HTTPRedirect(new_url)
+

cherrypy/test/test_core.py

     
     class URL(Test):
         
+        _cp_config = {'tools.trailing_slash.on': False}
+        
         def index(self, path_info, relative=None):
             return cherrypy.url(path_info, relative=bool(relative))
         
         
         def by_code(self, code):
             raise cherrypy.HTTPRedirect("somewhere else", code)
+        by_code._cp_config = {'tools.trailing_slash.extra': True}
         
         def nomodify(self):
             raise cherrypy.HTTPRedirect("", 304)
         finally:
             ignore.pop()
     
-    def testRedirect(self):
-        self.getPage("/redirect/")
-        self.assertBody('child')
-        self.assertStatus(200)
-        
+    def testSlashes(self):
         # Test that requests for index methods without a trailing slash
         # get redirected to the same URI path with a trailing slash.
         # Make sure GET params are preserved.
             self.assertInBody("<a href='%s/'>%s/</a>" %
                               (self.base(), self.base()))
         
+        # Test that requests for NON-index methods WITH a trailing slash
+        # get redirected to the same URI path WITHOUT a trailing slash.
+        # Make sure GET params are preserved.
+        self.getPage("/redirect/by_code/?code=307")
+        self.assertStatus(('302 Found', '303 See Other'))
+        self.assertInBody("<a href='%s/redirect/by_code?code=307'>"
+                          "%s/redirect/by_code?code=307</a>"
+                          % (self.base(), self.base()))
+        
+        # If the trailing_slash tool is off, CP should just continue
+        # as if the slashes were correct. But it needs some help
+        # inside cherrypy.url to form correct output.
+        self.getPage('/url?path_info=page1')
+        self.assertBody('%s/url/page1' % self.base())
+        self.getPage('/url/leaf/?path_info=page1')
+        self.assertBody('%s/url/page1' % self.base())
+    
+    def testRedirect(self):
+        self.getPage("/redirect/")
+        self.assertBody('child')
+        self.assertStatus(200)
+        
         self.getPage("/redirect/by_code?code=300")
         self.assertMatchesBody(r"<a href='(.*)somewhere else'>\1somewhere else</a>")
         self.assertStatus(300)

cherrypy/test/test_objectmapping.py

         def myMethod(self):
             return "myMethod from dir1, path_info is:" + repr(cherrypy.request.path_info)
         myMethod.exposed = True
-        myMethod._cp_config = {'request.redirect_on_extra_slash': True}
+        myMethod._cp_config = {'tools.trailing_slash.extra': True}
         
         def default(self, *params):
             return "default for dir1, param is:" + repr(params)

cherrypy/test/test_proxy.py

         def xhost(self):
             raise cherrypy.HTTPRedirect('blah')
         xhost.exposed = True
-        xhost._cp_config = {'tools.proxy.local': 'X-Host'}
+        xhost._cp_config = {'tools.proxy.local': 'X-Host',
+                            'tools.trailing_slash.extra': True,
+                            }
         
         def base(self):
             return cherrypy.request.base
             self.assertEqual(cherrypy.url("/this/new/page", script_name=sn),
                              "%s://%s%s%s/this/new/page"
                              % (self.scheme, self.HOST, port, sn))
+        
+        # Test trailing slash (see http://www.cherrypy.org/ticket/562).
+        self.getPage("/xhost/", headers=[('X-Host', 'www.yetanother.com')])
+        self.assertHeader('Location', "%s://www.yetanother.com/xhost"
+                          % self.scheme)
 
 
 if __name__ == '__main__':
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.