Skip to content

Commit 7959ecb

Browse files
committed
issue2550785: Using login from search (or logout) fails. when
logging in from a search page or after a logout it fails with an error. The fix also keeps the user on the same page they started from (e.g. search results) before the login. There are two parts to this: 1) changes to the templates to properly define the __came_from form element. 2) code changes to the LoginAction code in roundup/cgi/actions.py. New test code added. Needed some additional functions from urllib so urllib_.py got a change.
1 parent c6de943 commit 7959ecb

File tree

9 files changed

+207
-10
lines changed

9 files changed

+207
-10
lines changed

CHANGES.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@ Fixed:
239239
Diagnosis and fix with patch by R David Murray. Support for
240240
restoring retired but active queries, html layout changes and doc
241241
by John Rouillard.
242+
- issue2550785: Using login from search (or logout) fails. when
243+
logging in from a search page or after a logout it fails with an
244+
error. These failures have been fixed. The fix also keeps the user
245+
on the same page they started from before the login. There are two
246+
parts to this: 1) changes to the templates to properly define the
247+
__came_from form element. See ``upgrading.txt``. 2) code changes
248+
to the LoginAction code in roundup/cgi/actions.py. (John Rouillard)
242249

243250
2016-01-11: 1.5.1
244251

doc/upgrading.txt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,44 @@ the email entry in the tracker section::
107107
# Default:
108108
replyto_address =
109109

110+
Login from a search or after logout works better
111+
------------------------------------------------
112+
113+
The login form has been improved to work with some back end code
114+
changes. Now when a user logs in they stay on the same page where they
115+
started the login. To make this work, you must change the tal that is
116+
used to set the ``__came_from`` form variable.
117+
118+
Replace the existing code in the tracker's html/page.html page that
119+
looks similar to (look for name="__came_from")::
120+
121+
<input type="hidden" name="__came_from" tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
122+
123+
with the following::
124+
125+
<input type="hidden" name="__came_from"
126+
tal:condition="exists:request/env/QUERY_STRING"
127+
tal:attributes="value string:${request/base}${request/env/PATH_INFO}?${request/env/QUERY_STRING}">
128+
<input type="hidden" name="__came_from"
129+
tal:condition="not:exists:request/env/QUERY_STRING"
130+
tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
131+
132+
Now search backwards for the nearest form statement before the code
133+
that sets __came_from. If it looks like::
134+
135+
<form method="post" action="#">
136+
137+
replace it with::
138+
139+
<form method="post" tal:attributes="action request/base">
140+
141+
or with::
142+
143+
<form method="post" tal:attributes="action string:${request/env/PATH_INFO}">
144+
145+
the important part is that the action field **must not** include any query
146+
parameters ('#' includes query params).
147+
110148
html/_generic.404.html in trackers use page template
111149
----------------------------------------------------
112150

roundup/anypy/urllib_.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11

22
try:
33
# Python 3+
4-
from urllib.parse import quote, urlparse
4+
from urllib.parse import quote, urlencode, urlparse, parse_qs, urlunparse
55
except:
66
# Python 2.5-2.7
7-
from urllib import quote
8-
from urlparse import urlparse
7+
from urllib import quote, urlencode
8+
from urlparse import urlparse, parse_qs, urlunparse

roundup/cgi/actions.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1029,12 +1029,63 @@ def handle(self):
10291029
else:
10301030
password = ''
10311031

1032+
if '__came_from' in self.form:
1033+
# On valid or invalid login, redirect the user back to the page
1034+
# the started on. Searches, issue and other pages
1035+
# are all preserved in __came_from. Clean out any old feedback
1036+
# @error_message, @ok_message from the __came_from url.
1037+
#
1038+
# 1. Split the url into components.
1039+
# 2. Split the query string into parts.
1040+
# 3. Delete @error_message and @ok_message if present.
1041+
# 4. Define a new redirect_url missing the @...message entries.
1042+
# This will be redefined if there is a login error to include
1043+
# a new error message
1044+
redirect_url_tuple = urllib_.urlparse(self.form['__came_from'].value)
1045+
# now I have a tuple form for the __came_from url
1046+
try:
1047+
query=urllib_.parse_qs(redirect_url_tuple.query)
1048+
if "@error_message" in query:
1049+
del query["@error_message"]
1050+
if "@ok_message" in query:
1051+
del query["@ok_message"]
1052+
if "@action" in query:
1053+
# also remove the logout action from the redirect
1054+
# there is only ever one @action value.
1055+
if query['@action'] == ["logout"]:
1056+
del query["@action"]
1057+
except AttributeError:
1058+
# no query param so nothing to remove. Just define.
1059+
query = {}
1060+
pass
1061+
1062+
redirect_url = urllib_.urlunparse( (redirect_url_tuple.scheme,
1063+
redirect_url_tuple.netloc,
1064+
redirect_url_tuple.path,
1065+
redirect_url_tuple.params,
1066+
urllib_.urlencode(query, doseq=True),
1067+
redirect_url_tuple.fragment)
1068+
)
1069+
10321070
try:
10331071
self.verifyLogin(self.client.user, password)
10341072
except exceptions.LoginError, err:
10351073
self.client.make_user_anonymous()
10361074
for arg in err.args:
10371075
self.client.add_error_message(arg)
1076+
1077+
if '__came_from' in self.form:
1078+
# set a new error
1079+
query['@error_message'] = err.args
1080+
redirect_url = urllib_.urlunparse( (redirect_url_tuple.scheme,
1081+
redirect_url_tuple.netloc,
1082+
redirect_url_tuple.path,
1083+
redirect_url_tuple.params,
1084+
urllib_.urlencode(query, doseq=True),
1085+
redirect_url_tuple.fragment )
1086+
)
1087+
raise exceptions.Redirect(redirect_url)
1088+
# if no __came_from, send back to base url with error
10381089
return
10391090

10401091
# now we're OK, re-open the database for real, using the user
@@ -1047,7 +1098,7 @@ def handle(self):
10471098

10481099
# If we came from someplace, go back there
10491100
if '__came_from' in self.form:
1050-
raise exceptions.Redirect(self.form['__came_from'].value)
1101+
raise exceptions.Redirect(redirect_url)
10511102

10521103
def verifyLogin(self, username, password):
10531104
# make sure the user exists

share/roundup/templates/classic/html/page.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,12 @@ <h2><span metal:define-slot="body_title">body title</span></h2>
133133
<input type="checkbox" name="remember" id="remember">
134134
<label for="remember" i18n:translate="">Remember me?</label><br>
135135
<input type="submit" value="Login" i18n:attributes="value"><br>
136-
<input type="hidden" name="__came_from" tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
136+
<input type="hidden" name="__came_from"
137+
tal:condition="exists:request/env/QUERY_STRING"
138+
tal:attributes="value string:${request/base}${request/env/PATH_INFO}?${request/env/QUERY_STRING}">
139+
<input type="hidden" name="__came_from"
140+
tal:condition="not:exists:request/env/QUERY_STRING"
141+
tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
137142
<span tal:replace="structure request/indexargs_form" />
138143
<a href="user?@template=register"
139144
tal:condition="python:request.user.hasPermission('Register', 'user')"

share/roundup/templates/devel/html/page.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ <h1><a href="/">Roundup Demo Tracker</a></h1>
153153
<ul class="user">
154154
<li tal:condition="python:request.user.username=='anonymous'" class="submenu">
155155
<b i18n:translate="">User</b>
156-
<form method="post" action="#">
156+
<form method="post" tal:attributes="action request/base">
157157
<ul>
158158
<li>
159159
<tal:span i18n:translate="">Login</tal:span><br/>
@@ -163,7 +163,12 @@ <h1><a href="/">Roundup Demo Tracker</a></h1>
163163
<input type="checkbox" name="remember" id="remember"/>
164164
<label for="remember" i18n:translate="">Remember me?</label><br/>
165165
<input class="form-small" type="submit" value="Login" i18n:attributes="value"/><br/>
166-
<input type="hidden" name="__came_from" tal:attributes="value string:${request/env/PATH_INFO}"/>
166+
<input type="hidden" name="__came_from"
167+
tal:condition="exists:request/env/QUERY_STRING"
168+
tal:attributes="value string:${request/base}${request/env/PATH_INFO}?${request/env/QUERY_STRING}"/>
169+
<input type="hidden" name="__came_from"
170+
tal:condition="not:exists:request/env/QUERY_STRING"
171+
tal:attributes="value string:${request/base}${request/env/PATH_INFO}"/>
167172
<span tal:replace="structure request/indexargs_form" />
168173
</li>
169174
<li>

share/roundup/templates/minimal/html/page.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,12 @@ <h2><span metal:define-slot="body_title">body title</span></h2>
133133
<input type="checkbox" name="remember" id="remember">
134134
<label for="remember" i18n:translate="">Remember me?</label><br>
135135
<input type="submit" value="Login" i18n:attributes="value"><br>
136-
<input type="hidden" name="__came_from" tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
136+
<input type="hidden" name="__came_from"
137+
tal:condition="exists:request/env/QUERY_STRING"
138+
tal:attributes="value string:${request/base}${request/env/PATH_INFO}?${request/env/QUERY_STRING}">
139+
<input type="hidden" name="__came_from"
140+
tal:condition="not:exists:request/env/QUERY_STRING"
141+
tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
137142
<span tal:replace="structure request/indexargs_form" />
138143
<a href="user?@template=register"
139144
tal:condition="python:request.user.hasPermission('Register', 'user')"

share/roundup/templates/responsive/html/page.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@
170170
<div tal:condition="python:request.user.username=='anonymous'">
171171
<ul class='nav nav-list'>
172172
<li>
173-
<form method="post" action="#">
173+
<form method="post" tal:attributes="action request/base">
174174
<fieldset>
175175
<legend><i class='icon-user'></i>Login form</legend>
176176
<input name="__login_name" type='text' placeholder='Username' i18n:attributes="placeholder">
@@ -180,7 +180,12 @@
180180
<input type="checkbox" name="remember" id="remember">Remember me?
181181
</label>
182182
<input type="submit" value="Login" i18n:attributes="value" class='btn'>
183-
<input type="hidden" name="__came_from" tal:attributes="value string:${request/env/PATH_INFO}"/>
183+
<input type="hidden" name="__came_from"
184+
tal:condition="exists:request/env/QUERY_STRING"
185+
tal:attributes="value string:${request/base}${request/env/PATH_INFO}?${request/env/QUERY_STRING}">
186+
<input type="hidden" name="__came_from"
187+
tal:condition="not:exists:request/env/QUERY_STRING"
188+
tal:attributes="value string:${request/base}${request/env/PATH_INFO}">
184189
<span tal:replace="structure request/indexargs_form" />
185190
</fieldset>
186191
</form>

test/test_actions.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,23 @@ def setUp(self):
237237
self.client.opendb = lambda a: self.fail(
238238
"Logged in, but we shouldn't be.")
239239

240+
def assertRaisesMessage(self, exception, callable, message, *args,
241+
**kwargs):
242+
"""An extension of assertRaises, which also checks the exception
243+
message. We need this because we rely on exception messages when
244+
redirecting.
245+
"""
246+
try:
247+
callable(*args, **kwargs)
248+
except exception, msg:
249+
self.assertEqual(str(msg), message)
250+
else:
251+
if hasattr(exception, '__name__'):
252+
excName = exception.__name__
253+
else:
254+
excName = str(exception)
255+
raise self.failureException, excName
256+
240257
def assertLoginLeavesMessages(self, messages, username=None, password=None):
241258
if username is not None:
242259
self.form.value.append(MiniFieldStorage('__login_name', username))
@@ -247,6 +264,18 @@ def assertLoginLeavesMessages(self, messages, username=None, password=None):
247264
LoginAction(self.client).handle()
248265
self.assertEqual(self.client._error_message, messages)
249266

267+
def assertLoginRaisesRedirect(self, message, username=None, password=None, came_from=None):
268+
if username is not None:
269+
self.form.value.append(MiniFieldStorage('__login_name', username))
270+
if password is not None:
271+
self.form.value.append(
272+
MiniFieldStorage('__login_password', password))
273+
if came_from is not None:
274+
self.form.value.append(
275+
MiniFieldStorage('__came_from', came_from))
276+
277+
self.assertRaisesMessage(Redirect, LoginAction(self.client).handle, message)
278+
250279
def testNoUsername(self):
251280
self.assertLoginLeavesMessages(['Username required'])
252281

@@ -272,6 +301,58 @@ def opendb(username):
272301

273302
self.assertLoginLeavesMessages([], 'foo', 'right')
274303

304+
def testCorrectLoginRedirect(self):
305+
self.client.db.security.hasPermission = lambda *args, **kwargs: True
306+
def opendb(username):
307+
self.assertEqual(username, 'foo')
308+
self.client.opendb = opendb
309+
310+
# basic test with query
311+
self.assertLoginRaisesRedirect("http://whoami.com/path/issue?%40action=search",
312+
'foo', 'right', "http://whoami.com/path/issue?@action=search")
313+
314+
# test that old messages are removed
315+
self.form.value[:] = [] # clear out last test's setup values
316+
self.assertLoginRaisesRedirect("http://whoami.com/path/issue?%40action=search",
317+
'foo', 'right', "http://whoami.com/path/issue?@action=search&@ok_messagehurrah+we+win&@error_message=blam")
318+
319+
# test when there is no query
320+
self.form.value[:] = [] # clear out last test's setup values
321+
self.assertLoginRaisesRedirect("http://whoami.com/path/issue255",
322+
'foo', 'right', "http://whoami.com/path/issue255")
323+
324+
# test if we are logged out; should kill the @action=logout
325+
self.form.value[:] = [] # clear out last test's setup values
326+
self.assertLoginRaisesRedirect("http://localhost:9017/demo/issue39?%40startwith=0&%40pagesize=50",
327+
'foo', 'right', "http://localhost:9017/demo/issue39?@action=logout&@pagesize=50&@startwith=0")
328+
329+
def testInvalidLoginRedirect(self):
330+
self.client.db.security.hasPermission = lambda *args, **kwargs: True
331+
332+
def opendb(username):
333+
self.assertEqual(username, 'foo')
334+
self.client.opendb = opendb
335+
336+
# basic test with query
337+
self.assertLoginRaisesRedirect("http://whoami.com/path/issue?%40error_message=Invalid+login&%40action=search",
338+
'foo', 'wrong', "http://whoami.com/path/issue?@action=search")
339+
340+
# test that old messages are removed
341+
self.form.value[:] = [] # clear out last test's setup values
342+
self.assertLoginRaisesRedirect("http://whoami.com/path/issue?%40error_message=Invalid+login&%40action=search",
343+
'foo', 'wrong', "http://whoami.com/path/issue?@action=search&@ok_messagehurrah+we+win&@error_message=blam")
344+
345+
# test when there is no __came_from specified
346+
self.form.value[:] = [] # clear out last test's setup values
347+
# I am not sure why this produces three copies of the same error.
348+
# only one copy of the error is displayed to the user in the web interface.
349+
self.assertLoginLeavesMessages(['Invalid login', 'Invalid login', 'Invalid login'], 'foo', 'wrong')
350+
351+
# test when there is no query
352+
self.form.value[:] = [] # clear out last test's setup values
353+
self.assertLoginRaisesRedirect("http://whoami.com/path/issue255?%40error_message=Invalid+login",
354+
'foo', 'wrong', "http://whoami.com/path/issue255")
355+
275356
class EditItemActionTestCase(ActionTestCase):
276357
def setUp(self):
277358
ActionTestCase.setUp(self)

0 commit comments

Comments
 (0)