Repository: plone/Products.CMFPlone

Issue:
Login: Malformed url redirecting to external login
## BUG/PROBLEM REPORT

### What I did:

On a new Plone site (Plone 5.2rc2-pending) with an external login addon (pas.plugins.authomatic), the login redirection uses a malformed url.

This issue happens if no **next** parameter is provided in the redirect but a **came_from** parameter is. [Code producing the error](https://github.com/plone/Products.CMFPlone/blob/master/Products/CMFPlone/browser/login/login.py#L68)

### What I expect to happen:
Redirection to **http://localhost:8080/Plone/authomatic-handler?came_from=http://localhost:8080/Plone/**

### What actually happened:
Redirected to **http://localhost:8080/Plone/authomatic-handler&came_from=http://localhost:8080/Plone/**

### What version of Plone/ Addons I am using:
Plone 5.2-rc2-pending
pas.plugin.authomatic 1.0b2.dev0



Discussion:

Resolving patch:
diff --git a/Products/CMFPlone/browser/login/login.py b/Products/CMFPlone/browser/login/login.py
--- a/Products/CMFPlone/browser/login/login.py
+++ b/Products/CMFPlone/browser/login/login.py
@@ -68,15 +68,18 @@ def render(self):
     def _handle_external_login(self, url):
         """Handle login on this portal where login is external."""
         next_ = self.request.get('next', None)
+        came_from = self.request.get('came_from')
+        urlparts = [part for part in parse.urlparse(url)]
+        qs = dict(parse.parse_qsl(urlparts[4]))
         portal_url = getToolByName(self.context, 'portal_url')
         if next_ is not None and not portal_url.isURLInPortal(next_):
             next_ = None
         if next_ is not None:
-            url = '{0}?next={1}'.format(url, next_)
-        came_from = self.request.get('came_from')
+            qs['next'] = next_
         if came_from:
-            url = '{0}&came_from={1}'.format(url, came_from)
-        self.request.response.redirect(url)
+            qs['came_from'] = came_from
+        urlparts[4] = parse.urlencode(qs)
+        self.request.response.redirect(parse.urlunparse(urlparts))
 
     def _get_auth(self):
         try:


Test patch:
diff --git a/Products/CMFPlone/tests/test_login_form.py b/Products/CMFPlone/tests/test_login_form.py
--- a/Products/CMFPlone/tests/test_login_form.py
+++ b/Products/CMFPlone/tests/test_login_form.py
@@ -79,9 +79,9 @@ def test_login_external(self):
             form.request.response.getHeader('Location'),
         )
 
-    def test_login_external_with_params(self):
+    def test_login_external_with_all_params(self):
         registry = self.layer['portal'].portal_registry
-        registry['plone.external_login_url'] = 'http://testurl/extlogin'
+        registry['plone.external_login_url'] = 'http://testurl/extlogin?level=debug'
         self.request['came_from'] = 'foo'
         self.request['next'] = 'bar'
         form = self.portal.restrictedTraverse('login')
@@ -94,6 +94,22 @@ def test_login_external_with_params(self):
             'next=bar',
             form.request.response.getHeader('Location'),
         )
+        # Keep the original query string
+        self.assertIn(
+            'level=debug',
+            form.request.response.getHeader('Location'),
+        )
+
+    def test_login_external_without_next_param(self):
+        registry = self.layer['portal'].portal_registry
+        registry['plone.external_login_url'] = 'http://testurl/extlogin'
+        self.request['came_from'] = 'foo'
+        form = self.portal.restrictedTraverse('login')
+        form()
+        self.assertIn(
+            'came_from=foo',
+            form.request.response.getHeader('Location'),
+        )
 
     def test_failsafe_login_external(self):
         registry = self.layer['portal'].portal_registry