Index: repoze/who/plugins/form.py
===================================================================
--- repoze/who/plugins/form.py	(revision 3445)
+++ repoze/who/plugins/form.py	(working copy)
@@ -72,7 +72,7 @@
 class FormPlugin(FormPluginBase):
 
     implements(IChallenger, IIdentifier)
-    
+
     def __init__(self, login_form_qs, rememberer_name, formbody=None,
                  formcallable=None):
         self.login_form_qs = login_form_qs
@@ -88,7 +88,7 @@
         query = parse_dict_querystring(environ)
         # If the extractor finds a special query string on any request,
         # it will attempt to find the values in the input body.
-        if query.get(self.login_form_qs): 
+        if query.get(self.login_form_qs):
             form = parse_formvars(environ)
             from StringIO import StringIO
             # XXX we need to replace wsgi.input because we've read it
@@ -115,7 +115,7 @@
             if location:
                 headers = list(app_headers) + list(forget_headers)
                 return HTTPFound(headers = headers)
-                
+
         form = self.formbody or _DEFAULT_FORM
         if self.formcallable is not None:
             form = self.formcallable(environ)
@@ -131,7 +131,7 @@
 class RedirectingFormPlugin(FormPluginBase):
 
     implements(IChallenger, IIdentifier)
-    
+
     def __init__(self, login_form_url, login_handler_path, logout_handler_path,
                  rememberer_name, reason_param='reason'):
         self.login_form_url = login_form_url
@@ -146,13 +146,14 @@
     # IIdentifier
     def identify(self, environ):
         path_info = environ['PATH_INFO']
+        script_path = environ.get('SCRIPT_PATH') or '/'
         query = parse_dict_querystring(environ)
 
         if path_info == self.logout_handler_path:
             # we've been asked to perform a logout
             form = parse_formvars(environ)
             form.update(query)
-            referer = environ.get('HTTP_REFERER', '/')
+            referer = environ.get('HTTP_REFERER', script_path)
             came_from = form.get('came_from', referer)
             # set in environ for self.challenge() to find later
             environ['came_from'] = came_from
@@ -172,7 +173,7 @@
                     }
             except KeyError:
                 credentials = None
-            referer = environ.get('HTTP_REFERER', '/')
+            referer = environ.get('HTTP_REFERER', script_path)
             came_from = form.get('came_from', referer)
             environ['repoze.who.application'] = HTTPFound(came_from)
             return credentials
@@ -189,11 +190,23 @@
             query_elements[self.reason_param] = reason
         url_parts[4] = urllib.urlencode(query_elements, doseq=True)
         login_form_url = urlparse.urlunparse(url_parts)
+        login_form_url = self._get_full_path(login_form_url, environ)
         headers = [ ('Location', login_form_url) ]
         cookies = [(h,v) for (h,v) in app_headers if h.lower() == 'set-cookie']
         headers = headers + forget_headers + cookies
         return HTTPFound(headers=headers)
 
+    def _get_full_path(self, path, environ):
+        """
+        Return the full path to ``path`` by prepending the SCRIPT_PATH.
+
+        If ``path`` is a URL, do nothing.
+
+        """
+        if path.startswith('/'):
+            path = environ.get('SCRIPT_PATH', '') + path
+        return path
+
 def make_plugin(login_form_qs='__do_login', rememberer_name=None,
                 form=None):
     if rememberer_name is None:
Index: repoze/who/tests.py
===================================================================
--- repoze/who/tests.py	(revision 3445)
+++ repoze/who/tests.py	(working copy)
@@ -21,7 +21,7 @@
                  authenticators=None,
                  challengers=None,
                  classifier=None,
-                 mdproviders=None,                 
+                 mdproviders=None,
                  challenge_decider=None,
                  log_stream=None,
                  log_level=None,
@@ -53,7 +53,7 @@
                                     log_stream,
                                     log_level=logging.DEBUG)
         return mw
-    
+
     def test_accepts_logger(self):
         import logging
         logger = logging.Logger('something')
@@ -437,7 +437,7 @@
         self.assertEqual(environ['challenged'], app2)
         self.assertEqual(identifier.forgotten, identity)
 
-    def test_add_metadata(self): 
+    def test_add_metadata(self):
         environ = self._makeEnviron()
         plugin1 = DummyMDProvider({'foo':'bar'})
         plugin2 = DummyMDProvider({'fuz':'baz'})
@@ -449,7 +449,7 @@
         self.assertEqual(identity['foo'], 'bar')
         self.assertEqual(identity['fuz'], 'baz')
 
-    def test_add_metadata_w_classification(self): 
+    def test_add_metadata_w_classification(self):
         environ = self._makeEnviron()
         plugin1 = DummyMDProvider({'foo':'bar'})
         plugin2 = DummyMDProvider({'fuz':'baz'})
@@ -461,7 +461,7 @@
         identity = {}
         mw.add_metadata(environ, classification, identity)
         self.assertEqual(identity['foo'], 'bar')
-        self.assertEqual(identity.get('fuz'), None)       
+        self.assertEqual(identity.get('fuz'), None)
 
     def test_call_remoteuser_already_set(self):
         environ = self._makeEnviron({'REMOTE_USER':'admin'})
@@ -504,7 +504,7 @@
         self.assertEqual(result, ['body'])
         self.assertEqual(start_response.status, '200 OK')
         self.assertEqual(start_response.headers, headers)
-        
+
     def test_call_401_no_identifiers(self):
         environ = self._makeEnviron()
         headers = [('a', '1')]
@@ -724,7 +724,7 @@
         self.assertEqual(wrapper.start_response, None)
         self.assertEqual(wrapper.headers, [])
         self.failUnless(wrapper.buffer)
-    
+
     def test_finish_response(self):
         statuses = []
         headerses = []
@@ -736,12 +736,12 @@
         def close():
             closededs.append(True)
         write.close = close
-            
+
         def start_response(status, headers, exc_info=None):
             statuses.append(status)
             headerses.append(headers)
             return write
-            
+
         wrapper = self._makeOne(start_response)
         wrapper.status = '401 Unauthorized'
         wrapper.headers = [('a', '1')]
@@ -782,7 +782,7 @@
             items.append(item)
         response = ''.join(items)
         self.failUnless(response.startswith('401 Unauthorized'))
-        
+
     def test_identify_noauthinfo(self):
         plugin = self._makeOne('realm')
         environ = self._makeEnviron()
@@ -836,7 +836,7 @@
         forget = plugin._get_wwwauth()
         result = plugin.challenge(environ, '401 Unauthorized', [], forget)
         self.assertEqual(result.headers, forget)
-        
+
     def test_challenge_forgetheaders_omits(self):
         plugin = self._makeOne('realm')
         creds = {'login':'foo', 'password':'password'}
@@ -874,7 +874,7 @@
         creds = {}
         result = plugin.authenticate(environ, creds)
         self.assertEqual(result, None)
-        
+
     def test_authenticate_nolines(self):
         from StringIO import StringIO
         io = StringIO()
@@ -883,7 +883,7 @@
         creds = {'login':'chrism', 'password':'pass'}
         result = plugin.authenticate(environ, creds)
         self.assertEqual(result, None)
-        
+
     def test_authenticate_nousermatch(self):
         from StringIO import StringIO
         io = StringIO('nobody:foo')
@@ -987,7 +987,7 @@
         environ = self._makeEnviron()
         result = plugin.identify(environ)
         self.assertEqual(result, None)
-        
+
     def test_identify_badcookies(self):
         plugin = self._makeOne('oatmeal')
         environ = self._makeEnviron({'HTTP_COOKIE':'oatmeal=a'})
@@ -1072,7 +1072,7 @@
             extra['QUERY_STRING'] = '__do_login=true'
         environ = self._makeEnviron(extra)
         return environ
-    
+
     def test_implements(self):
         from zope.interface.verify import verifyClass
         from repoze.who.interfaces import IIdentifier
@@ -1086,7 +1086,7 @@
         environ = self._makeFormEnviron()
         result = plugin.identify(environ)
         self.assertEqual(result, None)
-        
+
     def test_identify_qs_no_values(self):
         plugin = self._makeOne()
         environ = self._makeFormEnviron(do_login=True)
@@ -1098,7 +1098,7 @@
         environ = self._makeFormEnviron(do_login=True, login='chris')
         result = plugin.identify(environ)
         self.assertEqual(result, None)
-    
+
     def test_identify_nopassword(self):
         plugin = self._makeOne()
         environ = self._makeFormEnviron(do_login=True, password='password')
@@ -1217,7 +1217,7 @@
         return plugin
 
     def _makeFormEnviron(self, login=None, password=None, came_from=None,
-                         path_info='/', identifier=None):
+                         path_info='/', identifier=None, script_path=''):
         from StringIO import StringIO
         fields = []
         if login:
@@ -1240,10 +1240,11 @@
                  'repoze.who.plugins': {'cookie':identifier},
                  'QUERY_STRING':'default=1',
                  'PATH_INFO':path_info,
+                 'SCRIPT_PATH':script_path
                  }
         environ = self._makeEnviron(extra)
         return environ
-    
+
     def test_implements(self):
         from zope.interface.verify import verifyClass
         from repoze.who.interfaces import IIdentifier
@@ -1258,7 +1259,7 @@
         result = plugin.identify(environ)
         self.assertEqual(result, None)
         self.failIf(environ.get('repoze.who.application'))
-        
+
     def test_identify_via_login_handler(self):
         plugin = self._makeOne()
         environ = self._makeFormEnviron(path_info='/login_handler',
@@ -1315,6 +1316,16 @@
         self.assertEqual(value, 'http://foo.bar')
         self.assertEqual(app.code, 302)
 
+    def test_identify_via_login_handler_no_came_from_no_referer_spath(self):
+        plugin = self._makeOne()
+        environ = self._makeFormEnviron(path_info='/login_handler',
+                                        script_path='/my-app',
+                                        login='chris',
+                                        password='password')
+        plugin.identify(environ)
+        app = environ['repoze.who.application']
+        self.assertEqual(app.location(), '/my-app')
+
     def test_identify_via_logout_handler(self):
         plugin = self._makeOne()
         environ = self._makeFormEnviron(path_info='/logout_handler',
@@ -1340,6 +1351,16 @@
         self.assertEqual(app.code, 401)
         self.assertEqual(environ['came_from'], '/')
 
+    def test_identify_via_logout_handler_no_came_from_no_referer_spath(self):
+        plugin = self._makeOne()
+        environ = self._makeFormEnviron(path_info='/logout_handler',
+                                        script_path='/my-app',
+                                        login='chris',
+                                        password='password')
+        plugin.identify(environ)
+        app = environ['repoze.who.application']
+        self.assertEqual(environ['came_from'], '/my-app')
+
     def test_identify_via_logout_handler_no_came_from(self):
         plugin = self._makeOne()
         environ = self._makeFormEnviron(path_info='/logout_handler',
@@ -1506,6 +1527,19 @@
         self.assertEqual(sr.headers[2][0], 'set-cookie')
         self.assertEqual(sr.headers[2][1], 'b')
 
+    def test_challenge_with_non_root_script_path(self):
+        """The script path must be taken into account while redirecting."""
+        plugin = self._makeOne(login_form_url='/login')
+        environ = self._makeFormEnviron(script_path='/app',
+                                        path_info='/admin')
+        came_from = 'http://www.example.com/app/admin?default=1'
+        environ['came_from'] = came_from
+        app = plugin.challenge(environ, '401 Unauthorized', [('app', '1')],
+                               [('forget', '1')])
+        from urllib import quote
+        login_url = '/app/login?came_from=%s' % quote(came_from, '')
+        self.assertEqual(app.location(), login_url)
+
 class TestAuthTktCookiePlugin(Base):
     def _getTargetClass(self):
         from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin
@@ -1546,7 +1580,7 @@
         environ = self._makeEnviron()
         result = plugin.identify(environ)
         self.assertEqual(result, None)
-        
+
     def test_identify_good_cookie_include_ip(self):
         plugin = self._makeOne('secret', include_ip=True)
         val = self._makeTicket(remote_addr='1.1.1.1')
@@ -1608,7 +1642,7 @@
         environ = self._makeEnviron({'HTTP_COOKIE':'auth_tkt=bogus'})
         result = plugin.identify(environ)
         self.assertEqual(result, None)
-    
+
     def test_remember_creds_same(self):
         plugin = self._makeOne('secret')
         val = self._makeTicket(userid='userid')
@@ -1642,7 +1676,7 @@
         new_val = self._makeTicket(userid='1', userdata='userid_type:int')
         result = plugin.remember(environ, {'repoze.who.userid':1,
                                            'userdata':''})
-        
+
         self.assertEqual(len(result), 3)
         self.assertEqual(result[0],
                          ('Set-Cookie',
@@ -1717,7 +1751,7 @@
         environ = self._makeEnviron({'HTTP_USER_AGENT':'WebDrive'})
         result = classifier(environ)
         self.assertEqual(result, 'dav')
-        
+
     def test_classify_xmlpost(self):
         classifier = self._getFUT()
         environ = self._makeEnviron({'CONTENT_TYPE':'text/xml',
@@ -1742,7 +1776,7 @@
         iface_reg, name_reg = fn([], [], [], [])
         self.assertEqual(iface_reg, {})
         self.assertEqual(name_reg, {})
-        
+
     def test_brokenimpl(self):
         fn = self._getFUT()
         self.assertRaises(ValueError, fn, [(None, DummyApp())], [], [], [])
@@ -2240,14 +2274,14 @@
 
 IDENTIFIERS_ONLY = """\
 [identifiers]
-plugins = 
+plugins =
     repoze.who.tests:DummyPlugin;klass1
     repoze.who.tests:DummyPlugin
 """
 
 IDENTIFIERS_WITH_PLUGINS = """\
 [identifiers]
-plugins = 
+plugins =
     foo;klass1
     bar
 
@@ -2260,14 +2294,14 @@
 
 AUTHENTICATORS_ONLY = """\
 [authenticators]
-plugins = 
+plugins =
     repoze.who.tests:DummyPlugin;klass1
     repoze.who.tests:DummyPlugin
 """
 
 AUTHENTICATORS_WITH_PLUGINS = """\
 [authenticators]
-plugins = 
+plugins =
     foo;klass1
     bar
 
@@ -2280,14 +2314,14 @@
 
 CHALLENGERS_ONLY = """\
 [challengers]
-plugins = 
+plugins =
     repoze.who.tests:DummyPlugin;klass1
     repoze.who.tests:DummyPlugin
 """
 
 CHALLENGERS_WITH_PLUGINS = """\
 [challengers]
-plugins = 
+plugins =
     foo;klass1
     bar
 
@@ -2300,14 +2334,14 @@
 
 MDPROVIDERS_ONLY = """\
 [mdproviders]
-plugins = 
+plugins =
     repoze.who.tests:DummyPlugin;klass1
     repoze.who.tests:DummyPlugin
 """
 
 MDPROVIDERS_WITH_PLUGINS = """\
 [mdproviders]
-plugins = 
+plugins =
     foo;klass1
     bar
 
@@ -2383,7 +2417,7 @@
 challenge_decider = repoze.who.classifiers:default_challenge_decider
 
 [identifiers]
-plugins = 
+plugins =
     form;browser
     auth_tkt
     basicauth
@@ -2606,7 +2640,7 @@
         environ['repoze.who.identity']['password'] = 'schooled'
         start_response(self.status, self.headers)
         return ['body']
-    
+
 class DummyRequestClassifier:
     def __call__(self, environ):
         return 'browser'
@@ -2648,7 +2682,7 @@
 class DummyAuthenticator:
     def __init__(self, userid=None):
         self.userid = userid
-        
+
     def authenticate(self, environ, credentials):
         if self.userid is None:
             return credentials['login']
@@ -2672,7 +2706,7 @@
 class DummyMDProvider:
     def __init__(self, metadata=None):
         self._metadata = metadata
-        
+
     def add_metadata(self, environ, identity):
         return identity.update(self._metadata)
 

