Merge lp:~bac/launchpad/bug-341935-captcha into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~bac/launchpad/bug-341935-captcha
Merge into: lp:launchpad
Diff against target: 319 lines
7 files modified
lib/canonical/launchpad/browser/tests/registration.py (+2/-0)
lib/canonical/launchpad/pagetests/standalone/xx-new-account-redirection-url.txt (+2/-0)
lib/canonical/launchpad/templates/launchpad-login.pt (+23/-5)
lib/canonical/launchpad/webapp/login.py (+47/-0)
lib/lp/registry/stories/foaf/xx-createaccount.txt (+17/-4)
lib/lp/registry/stories/foaf/xx-reg-with-existing-email.txt (+5/-1)
lib/lp/testing/registration.py (+32/-0)
To merge this branch: bzr merge lp:~bac/launchpad/bug-341935-captcha
Reviewer Review Type Date Requested Status
Martin Albisetti (community) ui Approve
Michael Nelson (community) ui* Approve
Edwin Grubbs (community) code ui* Approve
Review via email: mp+13022@code.launchpad.net

Commit message

Add a simple math-based captcha to the registration pages to thwart dumb bots.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

= Summary =

Bug 341935 addresses the need to do *something* to disrupt the ability for bots to
trick Launchpad into sending registration email to unsuspecting people. It was
agreed to do a simple, text-based math problem captcha as a first step to try to
defeat the bots. While it would be trivial for bots to defeat this captcha (see the
pagetest!) it is our belief that we are not a real target so no one would invest in a
custom solution for use against Launchpad.

== Proposed fix ==

Add a simple captcha into the form.

== Pre-implementation notes ==

Discussions with Barry and Curtis.

== Implementation details ==

The view for the login page is a mess. It's not a LaunchpadFormView so the additions
were done 'by hand'.

In order to make it a low bar the math question is simple addition with the answer
being in the range [10, 20].

Screenshot at http://people.canonical.com/~bac/captcha.png

== Tests ==

bin/test xx-createaccount.txt

== Demo and Q/A ==

https://launchpad.dev/+login

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/canonical/launchpad/templates/launchpad-login.pt
  lib/lp/registry/stories/foaf/xx-createaccount.txt
  lib/canonical/launchpad/webapp/login.py

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Hi Brad,

This looks good. I just have one minor comment.

merge-approved

-Edwin

>=== modified file 'lib/lp/registry/stories/foaf/xx-createaccount.txt'
>--- lib/lp/registry/stories/foaf/xx-createaccount.txt 2009-09-11 18:45:31 +0000
>+++ lib/lp/registry/stories/foaf/xx-createaccount.txt 2009-10-07 21:39:02 +0000
>@@ -21,12 +21,34 @@
> The email address you provided isn't valid. Please verify it and try
> again.
>
>-Jane tries again, providing a valid email address and asking to create a new
>-account.
>+Next she enters a valid email address but enters a wrong answer for
>+the incredibly trivial match captcha. She gets an error message.

Did you intend "math captcha" instead of "match captcha"?

> >>> browser.getControl(name='loginpage_email', index=1).value = (
> ... '<email address hidden>')
>- >>> browser.getControl('Register').click()
>+ >>> browser.getControl(name='loginpage_captcha_submission').value = '-1'
>+ >>> browser.getControl('Register').click()
>+ >>> print_feedback_messages(browser.contents)
>+ The answer to the simple math question was incorrect or missing.
>+ Please try again.
>+
>+Jane tries again, providing a the correct captcha answer. Her valid
>+email address from before has been retained in the form.
>+
>+ >>> import re
>+ >>> def get_captcha_answer(contents):
>+ ... expr = re.compile("What is (\d+ .{1} \d+)?")
>+ ... match = expr.search(contents)
>+ ... if match:
>+ ... question = match.group(1)
>+ ... answer = eval(question)
>+ ... return str(answer)
>+ ... return ''
>+
>+ >>> browser.getControl(name='loginpage_captcha_submission').value = (
>+ ... get_captcha_answer(browser.contents))
>+ >>> browser.getControl('Register').click()
>+ >>> print_feedback_messages(browser.contents)
> >>> print extract_text(find_main_content(browser.contents))
> Registration mail sent
> Instructions on completing your registration have been sent to
>@@ -36,7 +58,7 @@
> Launchpad sends Jane an email message containing a token she must use to
> complete the registration process.
>
>- >>> import email, re
>+ >>> import email
> >>> from lp.services.mail import stub
> >>> from canonical.launchpad.ftests.logintoken import (
> ... get_token_url_from_email)
>@@ -91,6 +113,8 @@
> >>> browser.open('http://launchpad.dev/+login')
> >>> browser.getControl(name='loginpage_email', index=1).value = (
> ... '<email address hidden>')
>+ >>> browser.getControl(name='loginpage_captcha_submission').value = (
>+ ... get_captcha_answer(browser.contents))
> >>> browser.getControl('Register').click()
>
> >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
>

review: Approve (code ui*)
Revision history for this message
Michael Nelson (michael.nelson) wrote :

Hi bac,

Thanks for getting this done! Looks very straight-forward.

I've noticed that many sites have at least a casual explanation for why they're asking people to answer a maths question. Something like: "We like to make sure that you're not a bot." as a description for the form field, but I'm assuming you've made the decision that there's no benefit?

review: Approve (ui*)
Revision history for this message
Martin Albisetti (beuno) wrote :

08:11 < beuno> noodles775, good morning
08:11 < noodles775> hi beuno
08:11 < beuno> noodles775, looking at the captcha review
08:12 < beuno> it's a great point about telling people why we ask that
08:12 < beuno> I wonder if we could have a (?) icon, that is clickable, and mrevell's help popup comes up
08:12 < beuno> so we don't have so much text on the page
08:12 < noodles775> Yeah, that'd be a good way to keep it minimal :)
08:13 < beuno> the other thing that came to mind was, maybe the field shouldn't be that length
08:13 < beuno> to make it distincctivly different than password
08:13 < beuno> if you don't read labels, it's easy to be fooled
08:16 < noodles775> yeah, true - I hadn't thought of that - mistaking it for a password field simply because it's following the email field.
08:16 < noodles775> I had a quick look at a few other sites (gmail signup and ubuntu forums) and both define a label like "Random question" or "Word
                    verification", and are visually quite different...
08:17 < beuno> right, you may also be able to get away with a very short description
08:17 < beuno> I think the key here is that it look visually different enough
08:17 < beuno> this is where "don't read the merge proposal" comes in useful :)
08:18 < noodles775> So, laying it out like the gmail signup:
08:19 < noodles775> hmm... not sure what we'd use for a label, but having the actual simple math question in the right section (ie. aligned with the inputs)
                    and the input below it could do that.
08:20 < beuno> yeap, that would work
08:21 < noodles775> perhaps the label could simply be 'Random question'.
08:21 < beuno> yeah, or "spam test"
08:21 < beuno> something that gets the message across
08:21 < noodles775> I'll play with it and do a diff.
08:21 < beuno> it's a great point that the label is not a good place to put the question

review: Needs Fixing (ui)
Revision history for this message
Michael Nelson (michael.nelson) wrote :

> Hi bac,
>
> Thanks for getting this done! Looks very straight-forward.
>
> I've noticed that many sites have at least a casual explanation for why
> they're asking people to answer a maths question. Something like: "We like to
> make sure that you're not a bot." as a description for the form field, but I'm
> assuming you've made the decision that there's no benefit?

I was just chatting with Martin, and a few things came out of it:

1. It would be good to ensure that the captcha field is visually different (it's possible that some people won't read the label but will simply type their password in there...being so used to entering their email followed by a password to create accounts etc.)
2. It might be good to have some indication of why we're asking for a maths question (as above) - but it doesn't need to be prominent.

So along those lines, here's two thoughts - see what you think:

1. http://people.canonical.com/~michaeln/tmp/captcha_eg1.png
This deals with (1) by giving the field a normal label and moving the question into the right-hand column (similar to what gmail does with their 'Word verification' when you try to register an account). That said, I don't particularly like the smaller input all on its own... so,

2. http://people.canonical.com/~michaeln/tmp/captcha_eg2.png
shows another idea - modifying the question to simply 8 + 8 = and having the input inline there. This looks much simpler to me and is very hard to mistake as a password field ;). I'll see what you guys think.

Note, in the second screenshot, you'll see that I also added the explanation of the random question to the steps above the fields - this seems like a good place to put it (as it is a required step) and it keeps the form clean. Another thought Martin had was to have a small '?' help link next to the question that would pop-up an explanation.

Anyway, let me know what you think.

Revision history for this message
Brad Crittenden (bac) wrote :

Thanks for the review and suggestions Michael and Martin.

Michael I like your second screenshot and have implemented it. A new screenshot is at http://people.canonical.com/~bac/captcha-2.png and a diff of the changes at http://pastebin.ubuntu.com/288678/

Revision history for this message
Martin Albisetti (beuno) :
review: Approve (ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/browser/tests/registration.py'
--- lib/canonical/launchpad/browser/tests/registration.py 2009-08-05 18:52:52 +0000
+++ lib/canonical/launchpad/browser/tests/registration.py 2009-10-08 20:25:21 +0000
@@ -8,6 +8,7 @@
8from canonical.launchpad.ftests import logout8from canonical.launchpad.ftests import logout
9from canonical.launchpad.testing.pages import setupBrowser9from canonical.launchpad.testing.pages import setupBrowser
10from canonical.launchpad.webapp import canonical_url10from canonical.launchpad.webapp import canonical_url
11from lp.testing.registration import set_captcha_answer
1112
1213
13def start_registration_through_the_web(email):14def start_registration_through_the_web(email):
@@ -20,6 +21,7 @@
20 browser = setupBrowser()21 browser = setupBrowser()
21 browser.open('http://launchpad.dev/+login')22 browser.open('http://launchpad.dev/+login')
22 browser.getControl(name='loginpage_email', index=1).value = email23 browser.getControl(name='loginpage_email', index=1).value = email
24 set_captcha_answer(browser)
23 browser.getControl('Register').click()25 browser.getControl('Register').click()
24 return browser26 return browser
2527
2628
=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-new-account-redirection-url.txt'
--- lib/canonical/launchpad/pagetests/standalone/xx-new-account-redirection-url.txt 2009-05-12 01:39:29 +0000
+++ lib/canonical/launchpad/pagetests/standalone/xx-new-account-redirection-url.txt 2009-10-08 20:25:21 +0000
@@ -39,8 +39,10 @@
39If she now creates a new account, we'll certainly redirect her back to39If she now creates a new account, we'll certainly redirect her back to
40whatever we had stored on the redirection_url hidden field.40whatever we had stored on the redirection_url hidden field.
4141
42 >>> from lp.testing.registration import set_captcha_answer
42 >>> anon_browser.getControl('E-mail address:', index=1).value = (43 >>> anon_browser.getControl('E-mail address:', index=1).value = (
43 ... 'granny@canonical.com')44 ... 'granny@canonical.com')
45 >>> set_captcha_answer(anon_browser)
44 >>> anon_browser.getControl('Register').click()46 >>> anon_browser.getControl('Register').click()
45 >>> print anon_browser.contents47 >>> print anon_browser.contents
46 <...48 <...
4749
=== modified file 'lib/canonical/launchpad/templates/launchpad-login.pt'
--- lib/canonical/launchpad/templates/launchpad-login.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/launchpad-login.pt 2009-10-08 20:25:21 +0000
@@ -154,10 +154,11 @@
154 <!-- Preserve extra query parameters as hidden fields. -->154 <!-- Preserve extra query parameters as hidden fields. -->
155 <tal:replacement tal:replace="structure view/preserve_query" />155 <tal:replacement tal:replace="structure view/preserve_query" />
156 </form>156 </form>
157 <p><strong>Note:</strong> You must enable cookies in your browser to log into or register for Launchpad.</p>157 <p><strong>Note:</strong> You must enable cookies in your browser
158 to log into or register for Launchpad.</p>
158 <!-- 5a. didn't try to register last time: -->159 <!-- 5a. didn't try to register last time: -->
159 <h1 tal:condition="not: view/registration_error">160 <h1 tal:condition="not: view/registration_error">
160 Not registered yet? 161 Not registered yet?
161 </h1>162 </h1>
162 <!-- 5b. just tried to register and failed: -->163 <!-- 5b. just tried to register and failed: -->
163 <tal:block condition="view/registration_error">164 <tal:block condition="view/registration_error">
@@ -168,19 +169,24 @@
168 <!-- 5. might want to register: -->169 <!-- 5. might want to register: -->
169 <p>170 <p>
170 Creating your Launchpad account is easy. Here's what to do:</p>171 Creating your Launchpad account is easy. Here's what to do:</p>
171 172
172 <ol class="subordinate">173 <ol class="subordinate">
173 <li>Make sure cookies are enabled in your browser.</li>174 <li>Make sure cookies are enabled in your browser.</li>
174 <li>Enter your e-mail address.</li>175 <li>Enter your e-mail address and answer our random question
176 so we know that you're human.
177 </li>
175 <li>Follow the URL in the confirmation e-mail that Launchpad sends and you're done!</li>178 <li>Follow the URL in the confirmation e-mail that Launchpad sends and you're done!</li>
176 </ol>179 </ol>
177 180
178181
179182
180 <form name="join" method="POST">183 <form name="join" method="POST">
181 <input type="hidden" name="redirection_url"184 <input type="hidden" name="redirection_url"
182 tal:condition="not: request/form/redirection_url|nothing"185 tal:condition="not: request/form/redirection_url|nothing"
183 tal:attributes="value view/getRedirectionURL" />186 tal:attributes="value view/getRedirectionURL" />
187 <input type="hidden"
188 tal:attributes="name view/captcha_hash;
189 value view/get_captcha_hash" />
184 <table>190 <table>
185 <tr>191 <tr>
186 <th>192 <th>
@@ -194,6 +200,18 @@
194 </td>200 </td>
195 </tr>201 </tr>
196 <tr>202 <tr>
203 <th>
204 <label>Random question:</label>
205 </th>
206 <td>
207 <span id="problem" tal:content="view/captcha_problem" />
208 <input type="text" size="5"
209 id="captcha_submission" value=""
210 tal:attributes="name view/captcha_submission"
211 />
212 </td>
213 </tr>
214 <tr>
197 <th></th>215 <th></th>
198 <td>216 <td>
199 <input217 <input
200218
=== modified file 'lib/canonical/launchpad/webapp/login.py'
--- lib/canonical/launchpad/webapp/login.py 2009-07-31 19:56:48 +0000
+++ lib/canonical/launchpad/webapp/login.py 2009-10-08 20:25:21 +0000
@@ -8,6 +8,8 @@
8import cgi8import cgi
9import urllib9import urllib
10from datetime import datetime, timedelta10from datetime import datetime, timedelta
11import md5
12import random
1113
12from BeautifulSoup import UnicodeDammit14from BeautifulSoup import UnicodeDammit
1315
@@ -18,6 +20,7 @@
1820
19from z3c.ptcompat import ViewPageTemplateFile21from z3c.ptcompat import ViewPageTemplateFile
2022
23from canonical.cachedproperty import cachedproperty
21from canonical.config import config24from canonical.config import config
22from canonical.launchpad import _25from canonical.launchpad import _
23from canonical.launchpad.interfaces.account import AccountStatus26from canonical.launchpad.interfaces.account import AccountStatus
@@ -167,6 +170,8 @@
167 submit_registration = form_prefix + 'submit_registration'170 submit_registration = form_prefix + 'submit_registration'
168 input_email = form_prefix + 'email'171 input_email = form_prefix + 'email'
169 input_password = form_prefix + 'password'172 input_password = form_prefix + 'password'
173 captcha_submission = form_prefix + 'captcha_submission'
174 captcha_hash = form_prefix + 'captcha_hash'
170175
171 # Instance variables that represent the state of the form.176 # Instance variables that represent the state of the form.
172 login_error = None177 login_error = None
@@ -292,12 +297,20 @@
292 redirection_url = redirection_url_list[0]297 redirection_url = redirection_url_list[0]
293298
294 self.email = request.form.get(self.input_email).strip()299 self.email = request.form.get(self.input_email).strip()
300
295 if not valid_email(self.email):301 if not valid_email(self.email):
296 self.registration_error = (302 self.registration_error = (
297 "The email address you provided isn't valid. "303 "The email address you provided isn't valid. "
298 "Please verify it and try again.")304 "Please verify it and try again.")
299 return305 return
300306
307 # Validate the user is human, more or less.
308 if not self.validateCaptcha():
309 self.registration_error = (
310 "The answer to the simple math question was incorrect "
311 "or missing. Please try again.")
312 return
313
301 registered_email = getUtility(IEmailAddressSet).getByEmail(self.email)314 registered_email = getUtility(IEmailAddressSet).getByEmail(self.email)
302 if registered_email is not None:315 if registered_email is not None:
303 person = registered_email.person316 person = registered_email.person
@@ -386,6 +399,40 @@
386 L.append(html % (name, cgi.escape(value, quote=True)))399 L.append(html % (name, cgi.escape(value, quote=True)))
387 return '\n'.join(L)400 return '\n'.join(L)
388401
402 def validateCaptcha(self):
403 """Validate the submitted captcha value matches what we expect."""
404 expected = self.request.form.get(self.captcha_hash)
405 submitted = self.request.form.get(self.captcha_submission)
406 if expected is not None and submitted is not None:
407 return md5.new(submitted).hexdigest() == expected
408 return False
409
410 @cachedproperty
411 def captcha_answer(self):
412 """Get the answer for the current captcha challenge.
413
414 With each failed attempt a new challenge will be given. Our answer
415 space is acknowledged to be ridiculously small but is chosen in the
416 interest of ease-of-use. We're not trying to create an iron-clad
417 challenge but only a minimal obstacle to dumb bots.
418 """
419 return random.randint(10, 20)
420
421 @property
422 def get_captcha_hash(self):
423 """Get the captcha hash.
424
425 The hash is the value we put in the form for later comparison.
426 """
427 return md5.new(str(self.captcha_answer)).hexdigest()
428
429 @property
430 def captcha_problem(self):
431 """Create the captcha challenge."""
432 op1 = random.randint(1, self.captcha_answer)
433 op2 = self.captcha_answer - op1
434 return '%d + %d =' % (op1, op2)
435
389436
390def logInPrincipal(request, principal, email):437def logInPrincipal(request, principal, email):
391 """Log the principal in. Password validation must be done in callsites."""438 """Log the principal in. Password validation must be done in callsites."""
392439
=== modified file 'lib/lp/registry/stories/foaf/xx-createaccount.txt'
--- lib/lp/registry/stories/foaf/xx-createaccount.txt 2009-09-11 18:45:31 +0000
+++ lib/lp/registry/stories/foaf/xx-createaccount.txt 2009-10-08 20:25:21 +0000
@@ -21,12 +21,24 @@
21 The email address you provided isn't valid. Please verify it and try21 The email address you provided isn't valid. Please verify it and try
22 again.22 again.
2323
24Jane tries again, providing a valid email address and asking to create a new24Next she enters a valid email address but enters a wrong answer for
25account.25the incredibly trivial math captcha. She gets an error message.
2626
27 >>> browser.getControl(name='loginpage_email', index=1).value = (27 >>> browser.getControl(name='loginpage_email', index=1).value = (
28 ... 'jane@example.com')28 ... 'jane@example.com')
29 >>> browser.getControl('Register').click()29 >>> browser.getControl(name='loginpage_captcha_submission').value = '-1'
30 >>> browser.getControl('Register').click()
31 >>> print_feedback_messages(browser.contents)
32 The answer to the simple math question was incorrect or missing.
33 Please try again.
34
35Jane tries again, providing a the correct captcha answer. Her valid
36email address from before has been retained in the form.
37
38 >>> from lp.testing.registration import set_captcha_answer
39 >>> set_captcha_answer(browser)
40 >>> browser.getControl('Register').click()
41 >>> print_feedback_messages(browser.contents)
30 >>> print extract_text(find_main_content(browser.contents))42 >>> print extract_text(find_main_content(browser.contents))
31 Registration mail sent43 Registration mail sent
32 Instructions on completing your registration have been sent to44 Instructions on completing your registration have been sent to
@@ -36,7 +48,7 @@
36Launchpad sends Jane an email message containing a token she must use to48Launchpad sends Jane an email message containing a token she must use to
37complete the registration process.49complete the registration process.
3850
39 >>> import email, re51 >>> import email
40 >>> from lp.services.mail import stub52 >>> from lp.services.mail import stub
41 >>> from canonical.launchpad.ftests.logintoken import (53 >>> from canonical.launchpad.ftests.logintoken import (
42 ... get_token_url_from_email)54 ... get_token_url_from_email)
@@ -91,6 +103,7 @@
91 >>> browser.open('http://launchpad.dev/+login')103 >>> browser.open('http://launchpad.dev/+login')
92 >>> browser.getControl(name='loginpage_email', index=1).value = (104 >>> browser.getControl(name='loginpage_email', index=1).value = (
93 ... 'jperson@example.com')105 ... 'jperson@example.com')
106 >>> set_captcha_answer(browser)
94 >>> browser.getControl('Register').click()107 >>> browser.getControl('Register').click()
95108
96 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()109 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
97110
=== modified file 'lib/lp/registry/stories/foaf/xx-reg-with-existing-email.txt'
--- lib/lp/registry/stories/foaf/xx-reg-with-existing-email.txt 2009-05-12 01:39:29 +0000
+++ lib/lp/registry/stories/foaf/xx-reg-with-existing-email.txt 2009-10-08 20:25:21 +0000
@@ -15,6 +15,8 @@
1515
16 >>> browser.getControl('E-mail address:', index=1).value = (16 >>> browser.getControl('E-mail address:', index=1).value = (
17 ... 'test@canonical.com')17 ... 'test@canonical.com')
18 >>> from lp.testing.registration import set_captcha_answer
19 >>> set_captcha_answer(browser)
18 >>> browser.getControl('Register').click()20 >>> browser.getControl('Register').click()
1921
20 >>> for message in get_feedback_messages(browser.contents):22 >>> for message in get_feedback_messages(browser.contents):
@@ -31,6 +33,7 @@
31on with the registration process.33on with the registration process.
3234
33 >>> browser.getControl('E-mail address:', index=1).value = 'mpo@iki.fi'35 >>> browser.getControl('E-mail address:', index=1).value = 'mpo@iki.fi'
36 >>> set_captcha_answer(browser)
34 >>> browser.getControl('Register').click()37 >>> browser.getControl('Register').click()
3538
36 >>> print extract_text(find_tag_by_id(browser.contents, 'address'))39 >>> print extract_text(find_tag_by_id(browser.contents, 'address'))
@@ -82,6 +85,7 @@
8285
83 >>> anon_browser.getControl('E-mail address:', index=1).value = (86 >>> anon_browser.getControl('E-mail address:', index=1).value = (
84 ... 'christian.reis@ubuntulinux.com')87 ... 'christian.reis@ubuntulinux.com')
88 >>> set_captcha_answer(anon_browser)
85 >>> anon_browser.getControl('Register').click()89 >>> anon_browser.getControl('Register').click()
8690
87 >>> from_addr, to_addr, msg = stub.test_emails.pop()91 >>> from_addr, to_addr, msg = stub.test_emails.pop()
@@ -143,6 +147,7 @@
143147
144 >>> browser.getControl('E-mail address:', index=1).value = (148 >>> browser.getControl('E-mail address:', index=1).value = (
145 ... 'bad-user@canonical.com')149 ... 'bad-user@canonical.com')
150 >>> set_captcha_answer(browser)
146 >>> browser.getControl('Register').click()151 >>> browser.getControl('Register').click()
147152
148 >>> print extract_text(find_main_content(browser.contents).p)153 >>> print extract_text(find_main_content(browser.contents).p)
@@ -179,4 +184,3 @@
179 <div ...>This profile cannot be claimed because the account is suspended.184 <div ...>This profile cannot be claimed because the account is suspended.
180 Contact a <a href="mailto:feedback@launchpad.net?subject=SU...">Launchpad185 Contact a <a href="mailto:feedback@launchpad.net?subject=SU...">Launchpad
181 admin</a> about this issue.</div>186 admin</a> about this issue.</div>
182
183187
=== added file 'lib/lp/testing/registration.py'
--- lib/lp/testing/registration.py 1970-01-01 00:00:00 +0000
+++ lib/lp/testing/registration.py 2009-10-08 20:25:21 +0000
@@ -0,0 +1,32 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Helper functions dealing with registration in tests.
5"""
6__metaclass__ = type
7
8__all__ = [
9 'get_captcha_answer',
10 'set_captcha_answer',
11 ]
12
13import re
14
15
16def get_captcha_answer(contents):
17 """Search the browser contents and get the captcha answer."""
18 expr = re.compile("(\d+ .{1} \d+) =")
19 match = expr.search(contents)
20 if match:
21 question = match.group(1)
22 answer = eval(question)
23 return str(answer)
24 return ''
25
26
27def set_captcha_answer(browser, answer=None):
28 """Given a browser, set the login captcha with the correct answer."""
29 if answer is None:
30 answer = get_captcha_answer(browser.contents)
31 browser.getControl(name='loginpage_captcha_submission').value = (
32 answer)