Commit 5ec8cb5b authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

backport default to stable since 3.6 is the stable release

branch : stable
......@@ -4,11 +4,11 @@
modname = 'registration'
distname = 'cubicweb-registration'
numversion = (0, 2, 1)
numversion = (0, 3, 0)
version = '.'.join(str(num) for num in numversion)
license = 'LGPL'
copyright = '''Copyright (c) 2009 LOGILAB S.A. (Paris, FRANCE).
copyright = '''Copyright (c) 2009-2010 LOGILAB S.A. (Paris, FRANCE). --'''
author = 'LOGILAB S.A. (Paris, FRANCE)'
......@@ -32,7 +32,7 @@ classifiers = [
__depends_cubes__ = {}
__depends__ = {'cubicweb': '>= 3.1.2',
__depends__ = {'cubicweb': '>= 3.6.1',
'python-crypto': None,
'PIL': None,
"""Simple captcha library, based on PIL. Monkey patch functions in this module
if you want something better...
:organization: Logilab
:copyright: 2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: --
:license: GNU Lesser General Public License, v2.1 -
__docformat__ = "restructuredtext en"
from random import randint, choice
from cStringIO import StringIO
import Image, ImageFont, ImageDraw, ImageFilter
def pil_captcha(text, fontfile, fontsize):
"""Generate a captcha image. Return a PIL image object.
adapted from
# randomly select the foreground color
fgcolor = randint(0, 0xffff00)
# make the background color the opposite of fgcolor
bgcolor = fgcolor ^ 0xffffff
# create a font object
font = ImageFont.truetype(fontfile, fontsize)
# determine dimensions of the text
dim = font.getsize(text)
# create a new image slightly larger that the text
img ='RGB', (dim[0]+5, dim[1]+5), bgcolor)
draw = ImageDraw.Draw(img)
# draw 100 random colored boxes on the background
x, y = img.size
for num in range(100):
draw.rectangle((randint(0, x), randint(0, y),
randint(0, x), randint(0, y)),
fill=randint(0, 0xffffff))
# add the text to the image
draw.text((3, 3), text, font=font, fill=fgcolor)
img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)
return img
def captcha(fontfile, fontsize, size=5, format='JPEG'):
"""Generate an arbitrary text, return it together with a buffer containing
the captcha image for the text
text = u''.join(choice('QWERTYUOPASDFGHJKLZXCVBNM') for i in range(size))
img = pil_captcha(text, fontfile, fontsize)
out = StringIO(), format)
return text, out
cubicweb-registration (0.3.0-1) unstable; urgency=low
* new upstream release
-- Sylvain Thénault <> Thu, 25 Feb 2010 09:10:48 +0100
cubicweb-registration (0.2.1-1) unstable; urgency=low
* new upstream release
......@@ -8,7 +8,7 @@ Homepage:
Package: cubicweb-registration
Architecture: all
Depends: cubicweb-common (>= 3.4.0), python-crypto, python-imaging
Depends: cubicweb-common (>= 3.6.1), python-crypto, python-imaging
Description: public registration component for the CubicWeb framework
This CubicWeb component provides a public registration feature (users
can register and create an account without the need for admin
# -*- coding: utf-8 -*-
from os.path import dirname, abspath, join
HERE = abspath(dirname(__file__))
options = (
{'type' : 'string',
'default': u"this is my dummy registration cypher seed",
'help': 'seed used to cypher registration data in confirmation email link',
'group': 'registration', 'inputlevel': 2,
{'type' : 'string',
'default': join(HERE, 'data', 'porkys.ttf'),
'help': 'True type font to use for captcha image generation',
'group': 'registration', 'inputlevel': 2,
{'type' : 'int',
'default': 25,
'help': 'Font size to use for captcha image generation',
'group': 'registration', 'inputlevel': 2,
import re, cgi, urlparse
from cubicweb import ValidationError
from cubicweb.crypto import encrypt, decrypt
from cubicweb.web import Redirect
from cubicweb.devtools.apptest import MAILBOX
from cubicweb.devtools.testlib import WebTest
from cubicweb.devtools.testlib import MAILBOX, CubicWebTC
from cubes.registration.views import RegistrationSendMailController, decrypt, encrypt
from cubes.registration.views import RegistrationSendMailController
class RegistrationTC(WebTest):
class RegistrationTC(CubicWebTC):
captcha_value = u'captcha value'
......@@ -19,7 +19,7 @@ class RegistrationTC(WebTest):
'captcha': captcha_value}
def setup_database(self):
self.config.global_set_option('cypher-seed', u'dummy cypher key')
self.config.global_set_option('registration-cypher-seed', u'dummy cypher key')
super(RegistrationTC, self).setup_database()
def test_registration_form(self):
......@@ -28,7 +28,7 @@ class RegistrationTC(WebTest):
pageinfo = self.view('registration', req=req, rset=None)
ns = {'ns': pageinfo.default_ns}
# check form field names
names = pageinfo.etree.xpath('//ns:form[@id="form"]//ns:input[@type!="hidden"]/@name',
names = pageinfo.etree.xpath('//ns:form[@id="registrationForm"]//ns:input[@type!="hidden"]/@name',
self.assertEquals(set(names), set(
# check form field value
......@@ -38,7 +38,7 @@ class RegistrationTC(WebTest):
def _check_redirect(self, ctrl, urlpath, **urlparams):
ctrl.req.set_session_data('captcha', self.captcha_value)
ctrl._cw.set_session_data('captcha', self.captcha_value)
ctrl.publish()'should redirect to ' + urlpath)
except Redirect, ex:
......@@ -50,8 +50,8 @@ class RegistrationTC(WebTest):
req = self.request()
req.form =
ctrl = self.env.vreg['controllers'].select('registration_sendmail', req,
ctrl = self.vreg['controllers'].select('registration_sendmail', req,
# check redirect
self._check_redirect(ctrl, '', __message='Your registration email has been sent. Follow instructions in there to activate your account.')
# check email contains activation url...
......@@ -62,7 +62,7 @@ class RegistrationTC(WebTest):
key = dict(cgi.parse_qsl(urlparse.urlsplit(url)[3]))['key']
d =
self.assertDictEquals(decrypt(key, self.config['cypher-seed']), d)
self.assertDictEquals(decrypt(key, self.config['registration-cypher-seed']), d)
def test_send_mail_failure(self):
req = self.request()
......@@ -70,29 +70,29 @@ class RegistrationTC(WebTest):
('captcha', 'incorrect captcha value')):
req.form =
ctrl = self.env.vreg['controllers'].select('registration_sendmail',
ctrl.req.set_session_data('captcha', self.captcha_value)
ctrl = self.vreg['controllers'].select('registration_sendmail',
ctrl._cw.set_session_data('captcha', self.captcha_value)
ctrl.publish()'should raise ValidationError')
except ValidationError, e:
self.assertDictEquals(e.errors, {param: msg})
# check captcha has expired
self.assertEquals(ctrl.req.get_session_data('captcha'), None)
self.assertEquals(ctrl._cw.get_session_data('captcha'), None)
def _confirm_ctrl(self, key=None):
req = self.request()
req.form = {'key': key or encrypt(, self.config['cypher-seed'])}
return self.env.vreg['controllers'].select('registration_confirm', req,
req.form = {'key': key or encrypt(, self.config['registration-cypher-seed'])}
return self.vreg['controllers'].select('registration_confirm', req,
def test_confirm_ok(self):
ctrl = self._confirm_ctrl()
self._check_redirect(ctrl, '', __message=u'Congratulations, your registration is complete. Welcome %(firstname)s %(surname)s !' %
rset = self.execute('Any U WHERE U login %(login)s, U firstname %(firstname)s, '
'U surname %(surname)s, U use_email M, M address %(use_email)s',
# -*- coding: utf-8 -*-
from pickle import dumps, loads
from base64 import b64encode, b64decode
from time import time
from Crypto.Cipher import Blowfish
from logilab.common.decorators import clear_cache
from cubicweb.common import tags, mail
from cubicweb import mail, crypto
from cubicweb.view import StartupView
from cubicweb.web import Redirect, ValidationError
from cubicweb.web import (controller, httpcache, form,
formwidgets as wdg, formfields as ff)
from cubicweb.web import controller, form, captcha
from cubicweb.web import formwidgets as fw, formfields as ff
from cubicweb.web.views import forms, basecomponents, urlrewrite
from cubes.registration import captcha
def _cypherer(seed):
return _CYPHERERS[seed]
except KeyError:
_CYPHERERS[seed] =, Blowfish.MODE_ECB)
return _CYPHERERS[seed]
def encrypt(data, seed):
string = dumps(data)
string = string + '*' * (8 - len(string) % 8)
string = b64encode(_cypherer(seed).encrypt(string))
return unicode(string)
def decrypt(string, seed):
# pickle ignores trailing characters so we do not need to strip them off
string = _cypherer(seed).decrypt(b64decode(string))
return loads(string)
class CaptchaView(StartupView):
http_cache_manager = httpcache.NoHTTPCacheManager
id = 'captcha'
binary = True
templatable = False
content_type = 'image/jpg'
def call(self):
text, data = captcha.captcha(self.config['captcha-font-file'],
self.req.set_session_data('captcha', text)
class CaptchaWidget(wdg.TextInput):
def render(self, form, field, renderer=None):
name, curvalues, attrs = self._render_attrs(form, field)
# t=int(time()*100) to make sure img is not cached
src = form.req.build_url('view', vid='captcha', t=int(time()*100))
img = tags.img(src=src, alt=u'captcha')
img = u'<div class="captcha">%s</div>' % img
if renderer is None: # XXX cw < 3.4
return img + super(CaptchaWidget, self).render(form, field)
return img + super(CaptchaWidget, self).render(form, field, renderer)
class RegistrationForm(forms.FieldsForm):
id = 'registration'
form_buttons = [wdg.SubmitButton()]
__regid__ = 'registration'
domid = 'registrationForm'
form_buttons = [fw.SubmitButton()]
def action(self):
return self.req.build_url(u'registration_sendmail')
login = ff.StringField(widget=wdg.TextInput(), required=True)
upassword = ff.StringField(widget=wdg.PasswordInput(), required=True)
use_email = ff.StringField(widget=wdg.TextInput(), required=True)
firstname = ff.StringField(widget=wdg.TextInput())
surname = ff.StringField(widget=wdg.TextInput())
captcha = ff.StringField(widget=CaptchaWidget(), required=True,
return self._cw.build_url(u'registration_sendmail')
login = ff.StringField(widget=fw.TextInput(), required=True)
upassword = ff.StringField(widget=fw.PasswordInput(), required=True)
use_email = ff.StringField(widget=fw.TextInput(), required=True)
firstname = ff.StringField(widget=fw.TextInput())
surname = ff.StringField(widget=fw.TextInput())
captcha = ff.StringField(widget=captcha.CaptchaWidget(), required=True,
help=_('please copy the letters from the image'))
# XXX move captcha validation to a CaptchaField
class RegistrationFormView(form.FormViewMixIn, StartupView):
id = 'registration'
__regid__ = 'registration'
def call(self):
form = self.vreg['forms'].select('registration', self.req)
form = self._cw.vreg['forms'].select('registration', self._cw)
class RegistrationSendMailController(controller.Controller):
id = 'registration_sendmail'
__regid__ = 'registration_sendmail'
content = _(u'''
Hello %(firstname)s %(surname)s,
......@@ -110,29 +56,29 @@ See you soon on %(base_url)s !
data = self.checked_data()
recipient = data['use_email']
msg = self.build_email(recipient, data)
self.config.sendmails([(msg, (recipient,))])
self._cw.vreg.config.sendmails([(msg, (recipient,))])
raise Redirect(self.success_redirect_url())
def checked_data(self):
'''only basic data check here (required attributes and password
confirmation check)
fieldsform = self.vreg['forms'].select('registration', self.req)
fieldsform = self._cw.vreg['forms'].select('registration', self._cw)
data = {}
errors = {}
for field in fieldsform._fields_:
value = self.req.form.get(, u'').strip()
value = self._cw.form.get(, u'').strip()
if not value:
if field.required:
errors[] = self.req._('required attribute')
errors[] = self._cw._('required attribute')
data[] = value
if data['upassword'] != self.req.form.get('upassword-confirm'):
if data['upassword'] != self._cw.form.get('upassword-confirm'):
errors['upassword'] = _('passwords are different')
captcha = self.req.get_session_data('captcha', None, pop=True)
captcha = self._cw.get_session_data('captcha', None, pop=True)
if captcha is None:
errors[None] = self.req._('unable to check captcha, please try again')
errors[None] = self._cw._('unable to check captcha, please try again')
elif data['captcha'].lower() != captcha.lower():
errors['captcha'] = self.req._('incorrect captcha value')
errors['captcha'] = self._cw._('incorrect captcha value')
if errors:
raise ValidationError(None, errors)
return data
......@@ -140,30 +86,31 @@ See you soon on %(base_url)s !
def build_email(self, recipient, data):
if not data.get('firstname') or data.get('surtname'):
data['firstname'] = data['login']
data.update({'base_url': self.config['base-url'],
data.update({'base_url': self._cw.vreg.config['base-url'],
'url': self.activation_url(data)})
content = self.req._(self.content) % data
subject = self.req._(self.subject) % data
content = self._cw._(self.content) % data
subject = self._cw._(self.subject) % data
return mail.format_mail({}, [recipient], content=content,
subject=subject, config=self.config)
subject=subject, config=self._cw.vreg.config)
def activation_url(self, data):
key = encrypt(data, self.config['cypher-seed'])
return self.build_url('registration_confirm', key=key)
key = crypto.encrypt(data, self._cw.vreg.config['registration-cypher-seed'])
return self._cw.build_url('registration_confirm', key=key)
def success_redirect_url(self):
msg = self.req._(u'Your registration email has been sent. Follow '
msg = self._cw._(u'Your registration email has been sent. Follow '
'instructions in there to activate your account.')
return self.build_url('', __message=msg)
return self._cw.build_url('', __message=msg)
class RegistrationConfirmController(controller.Controller):
id = 'registration_confirm'
__regid__ = 'registration_confirm'
def publish(self, rset=None):
req = self.req
req = self._cw
data = decrypt(req.form['key'], self.vreg.config['cypher-seed'])
data = crypto.decrypt(req.form['key'],
self.debug('registration data: %s', data)
msg = req._(u'Invalid registration data. Please try registering again.')
......@@ -194,19 +141,19 @@ class RegistrationConfirmController(controller.Controller):
data.pop('upassword-confirm', None)
data['__message'] = u'<br/>'.join(u'%s : %s' % (k,v)
for (k,v) in errors.iteritems())
return self.build_url(u'register', **data)
return self._cw.build_url(u'register', **data)
def success_redirect_url(self, data):
msg = self.req._(u'Congratulations, your registration is complete. '
msg = self._cw._(u'Congratulations, your registration is complete. '
'Welcome %(firstname)s %(surname)s !')
return self.build_url('', __message=msg%data)
return self._cw.build_url('', __message=msg%data)
class UserLink(basecomponents.UserLink):
def anon_user_link(self):
super(UserLink, self).anon_user_link()
self.w(u'&nbsp;[<a class="logout" href="%s">%s</a>]' % (
self.build_url('register'), self.req._('i18n_register_user')))
self._cw.build_url('register'), self._cw._('i18n_register_user')))
## urls #######################################################################
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment