Source

tgext.registration2 / tgext / registration2 / controllers.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462

from pylons import request
from tg import expose, redirect, config, validate, require, flash, TGController, url
try:
    import tg.predicates as identity
except:
    # Pre Turbogears 2.2.0 + repoze.who 2.x:
    import repoze.what.predicates as identity
from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin
from pkg_resources import resource_string
import logging
import datetime
import hashlib
import smtplib
import urllib
import string
import random
from email.MIMEText import MIMEText

import tgext.registration2.registration_config as registration_config
cfg = config.get('registration_config', None)
if cfg:
    registration_config.readfile(cfg)
config.reg_config = registration_config.registration

from tgext.registration2.widgets import new_user_form, edit_user_form, lost_password_form, \
        delete_user_form


model = config.model
DBSession = config.DBSession
user_class = config.sa_auth['user_class']
group_class = config.sa_auth['group_class']
#from repoze.what.plugins.sql import SqlGroupsAdapter
#groups = SqlGroupsAdapter(group_class, user_class, DBSession)


try:
    import turbomail
except ImportError:
    turbomail = None

from pylons.i18n import ugettext as _, lazy_ugettext as l_

pendinguser = model.RegistrationPendingUser
emailchange = model.RegistrationUserEmailChange

log = logging.getLogger('registration')


def retrieve_one(class_, **kw):
    r = DBSession.query(class_).filter_by(**kw).all()
    cnt = len(r)
    if cnt == 0:
        return None
    elif cnt == 1:
        return r[0]
    else:
        raise LookupError, 'Received %d results, while up to 1 was expected.' % cnt


class UserRegistration(TGController):

    def __call__(self, environ, start_response):
        """Invoke the Controller, as in a quickstarted tg2 app."""
        # TGController.__call__ dispatches to the Controller method
        # the request is routed to. This routing information is
        # available in environ['pylons.routes_dict']
        
        request.identity = request.environ.get('repoze.who.identity')
        tmpl_context.identity = request.identity
        return TGController.__call__(self, environ, start_response)

    def __init__(self):
        super(UserRegistration, self).__init__()
        random.seed()
        self.hash_salt = ''.join([random.choice(string.printable) for i in range(20)])
    
    @expose()
    def index(self):
        if identity.not_anonymous():
            redir = 'edit_user'
        else:
            redir = 'new'
        return redirect(url([request.path, redir]))
            
    @expose(template=config.reg_config.templates.new)
    def new(self, tg_errors=None, **kw):
        if identity.not_anonymous():
            redirect('./edit_user')
        if tg_errors:
            flash(_('There was a problem with the data submitted.'))
        return dict(form=new_user_form, action='./create', 
                    submit_text=_('Create Account'))
        
    @expose(template=config.reg_config.templates.create)
    @validate(form=new_user_form, error_handler=new)
    def create(self, user_name, email, email2, display_name, password1, password2, 
               **kw):
        if identity.not_anonymous():
            redirect('./edit_user')
        key = self.validation_hash(email + display_name + password1)
        pend = pendinguser(user_name=user_name,
                           email_address=email,
                           display_name=display_name,
                           password=password1,
                           validation_key=key
                          )
        DBSession.add(pend)
        error_msg = None
        try:
            self.mail_new_validation_email(pend)
        except smtplib.SMTPRecipientsRefused, args:
            # catch SMTP error before they can bite
            error_code, error_message = args[0][email]
            
            if error_code == 450:
                # SMTP refused either sender or receiver
                error_msg = _('Your email was refused with error: %s' % error_message)
            else:
                # some other error code ?
                error_msg = '%s' % error_message
    
        if config.reg_config.unverified_user.groups and not error_msg:
            # we have unverified_user.groups.  Add the user to the User table
            # and add the appropriate groups
            user = self.promote_pending_user(pend)
            self.add_unverified_groups(user)
            # log them in
            plugins = request.environ.get('repoze.who.plugins').values()
            rememberers = [auth for auth in plugins if hasattr(auth, 'remember')]
            res = rememberers and \
                rememberers[0].remember(request.environ, dict(login=user_name, password=password1))
            #cookie = AuthTktCookiePlugin(config["repoze.secret"])
            #headers=cookie.remember(request.environ, {"repoze.who.userid" : user_name})
            
        return dict(name=display_name, email=email, error_msg=error_msg)

    @expose(template=config.reg_config.templates.validate)
    def validate_new_user(self, user='', key=''):
        pend = retrieve_one(pendinguser, user_name=user)
        if pend and pend.validation_key == key:
            if config.reg_config.unverified_user.groups:
                # The pending user is already in the Users table
                new_user = retrieve_one(user_class, user_name=user)
                self.remove_all_groups(new_user)
            else:
                # Add the pending user to the Users table
                new_user = self.promote_pending_user(pend)
            
            self.add_standard_groups(new_user)
            DBSession.delete(pend)
            # If you have a protected url that a basic user can log into and see, 
            # set it as login_url (instead of identity.failure_url).  
            # Otherwise, the user will loop back to validate after logging in, and then over 
            # to /login.
            edit_user_url = config.get('edit_user_url', './edit_user')
            login_url = url('/login', __logins=1, came_from=edit_user_url)
            return dict(name=getattr(new_user, 'display_name', new_user.user_name), 
                            login=login_url, 
                            is_valid=True)
        else:
            if identity.not_anonymous():
                #This is probably just someone with an old/stale link
                redirect('./edit_user')
            log.info('%s Bad validation using user=%s validation_key=%s' % 
                        (request.remote_addr, user, key))
            return dict(is_valid=False, 
                        admin_email=config.reg_config.mail.admin_email)
            
    def promote_pending_user(self, pending_user):
        """Copies a pending user from pending and into the official 'users'.
        
        Returns the new user object.
        """
        # Let's try to do this programmatically.  The only thing you should have to modify 
        # if you changed the schema fo RegistrationPendingUser is the 'excluded' list.  All 
        # columns not in this list will be mapped straight to a new user object.
        
        # This list contains the columns from RegistrationPendingUser 
        # that you DON'T want to migrate
        columns = ['user_name', 'email_address', 'display_name', '_password']
        new_columns = dict()
        for c in columns:
            new_columns[c] = getattr(pending_user, c)
        ret = user_class(**new_columns)
        DBSession.add(ret)
        return ret

    def mail_new_validation_email(self, pending_user):
        "Generate the new user validation email."
        reg_base_url = self.registration_base_url()
        queryargs = urllib.urlencode(dict(user=pending_user.user_name, 
                                          key=pending_user.validation_key))
        url = '%s/validate_new_user?%s' % (reg_base_url, queryargs)
        
        _module, _file = config.reg_config.email_body.new
        body = resource_string(_module, _file)
        
        self.send_email(pending_user.email_address, 
                        config.reg_config.mail.admin_email, 
                        _(config.reg_config.mail.new.subject), 
                        body % {'validation_url': url})
     
    @expose(template=config.reg_config.templates.lost_password)
    def lost_password(self, tg_errors=None, **kw):
        "Show the lost password form."
        if identity.not_anonymous():
            redirect('./edit_user')
        policy = config.reg_config.lost_password_policy
        return dict(policy=policy, form=lost_password_form, 
                    action="recover_lost_password")
        
    @expose(template=config.reg_config.templates.recover_lost_password)
    @validate(form=lost_password_form, error_handler=lost_password)
    def recover_lost_password(self, email=None, username=None):
        "Resets (or mails) a user's forgotten password."
        if identity.not_anonymous():
            redirect('./edit_user')
        reset_password = user = user_email = None
        user = retrieve_one(user_class, email_address=email, user_name=username)
        policy = config.reg_config.lost_password_policy

        log.info('%s Recover lost password request for user=%s email=%s' % 
                 (request.remote_addr, username, email))
        # We can't send the password if it is encrypted; must reset.
        if user and policy == 'reset':
            # generate a new password for the user
            chars = string.ascii_letters + string.digits
            random.seed()
            new_pw = ''
            # Compose a new random password  6-9 chars long
            for i in range(0, random.choice((6, 7, 8, 9))):
                new_pw = '%s%s' % (new_pw, random.choice(chars))

            _module, _file = config.reg_config.email_body.reset_password
            body = resource_string(_module, _file)
            reg_base_url = self.registration_base_url()

            key = self.validation_hash(email + user.display_name + new_pw)
            pend = retrieve_one(pendinguser, user_name=user.user_name)
            if pend:
                DBSession.delete(pend)
                DBSession.flush()
            pend = pendinguser(user_name=user.user_name,
                               email_address=email,
                               display_name=user.display_name,
                               password=new_pw,
                               validation_key=key
                              ) 
            DBSession.add(pend)
            queryargs = urllib.urlencode(dict(user=pend.user_name, key=key))
            url = '%s/validate_reset_password?%s' % (reg_base_url, queryargs)
            self.send_email(user.email_address, 
                            config.reg_config.mail.admin_email, 
                            _(config.reg_config.mail.lost_password.subject), 
                            body % {'password': new_pw, 'validation_url': url, 'user_name':user.user_name})
            user_email = user.email_address
            reset_password = True
        elif user and policy == 'send_current':  # sending the current password
            _module, _file = config.reg_config.email_body.lost_password
            body = resource_string(_module, _file)
            self.send_email(user.email_address,
                            config.reg_config.mail.admin_email, 
                            _(config.reg_config.mail.lost_password.subject),
                            body % {'password': user.password, 
                                    'user_name': user.user_name})
            user_email = user.email_address
        else:
            tg_error = _("Bad user/email combination")

        return dict(email=user_email, reset_password=reset_password)
        
    @expose(template=config.reg_config.templates.validate_reset_password)
    def validate_reset_password(self, user='', key=''):
        pend = retrieve_one(pendinguser, user_name=user)
        if pend and pend.validation_key == key:
            user = retrieve_one(user_class, user_name=user)
            user._password = pend.password
            DBSession.delete(pend)
            # If you have a protected url that a basic user can log into and see, 
            # set it as login_url (instead of identity.failure_url).  
            # Otherwise, the user will loop back to validate after logging in, and then over 
            # to /login.
            login_url = config.get('identity.failure_url', '/login')
            return dict(name=getattr(user, 'display_name', user.user_name), 
                        login=login_url, 
                        is_valid=True)
        else:
            log.info('%s Bad validation using user=%s validation_key=%s' % 
                        (request.remote_addr, user, key))
            return dict(is_valid=False, 
                        admin_email=config.reg_config.mail.admin_email)

    @expose(template=config.reg_config.templates.edit_user)
    @require(identity.not_anonymous())
    def edit_user(self, tg_errors=None, **kw):
        "Edit current user information."
        u = dict(request.identity)['user']
        form_values = dict(user_name=u.user_name, email=u.email_address, 
                           display_name=u.display_name,
                           old_password='',
                           password_1='', password_2='')
        return dict(display_name=u.display_name,
                    form=edit_user_form, 
                    form_values=form_values, 
                    action="update_user")
    
    @expose()
    @require(identity.not_anonymous())
    @validate(form=edit_user_form, error_handler=edit_user)                
    def update_user(self, email, display_name, old_password, password1, password2, user_name=None):
        "Updates the users information with new values."
        user = dict(request.identity)['user']
        msg = ""
        user.display_name = display_name
        if password1:
            user.password=password1
            msg = _("Your password was changed. ")
            
        if email and email != user.email_address:
            try:
                self.mail_changed_email_validation(email)
                msg = msg + _("You will receive an email at %s with instructions to complete changing your email address." % email)
            except smtplib.SMTPRecipientsRefused, args:
                msg = _("The provided new email was refused by our server, please provide a vali    d email.")

        if msg:
            flash(msg)
        return redirect('./edit_user')
        
    def mail_changed_email_validation(self, new_email):
        """Sends an email out that has validation information for changed email addresses.
        
        The logic is that we keep the old (verified) email in the User table, and add the
        new information into the RegistrationUserEmailChange table.  When the user eventually
        validates the new address, we delete the information out of RegistrationUserEmailChange 
        and put the new email address into User table.  That way, we always have a "good" email 
        address in the User table.
        """
        user = dict(request.identity)['user']
        unique_str = new_email + user.email_address
        validation_key = self.validation_hash(unique_str)
        email_change = emailchange(
                    user=user,
                    new_email_address=new_email,
                    validation_key=validation_key)
        reg_base_url = self.registration_base_url()
        queryargs = urllib.urlencode(dict(email=new_email, 
                                          key=validation_key))
        url = '%s/validate_email_change?%s' % (reg_base_url, queryargs)
        
        _module, _file = config.reg_config.email_body.changed_email
        body = resource_string(_module, _file)
        self.send_email(new_email,
                    config.reg_config.mail.admin_email, 
                    _(config.reg_config.mail.changed_email.subject),
                    body % {'validation_url': url})
    
    @expose(template=config.reg_config.templates.validate_email)
    def validate_email_change(self, email, key):
        "Validate the email address change and update the database appropriately."
        is_valid = False
        admin_email = config.reg_config.mail.admin_email
        email_change = retrieve_one(emailchange, new_email_address=email)
        if not email_change:
            return dict(is_valid=False, admin_email=admin_email)
        if email_change.validation_key == key:
            is_valid = True
            user = email_change.user
            # change the user's email address and delete the email_change record
            user.email_address = email
            DBSession.delete(email_change)
        return dict(is_valid=is_valid, 
                    email=email, 
                    name=user.display_name,
                    admin_email=admin_email)

    @expose(template=config.reg_config.templates.delete_user)
    @require(identity.not_anonymous())
    def delete_user(self, **kw):
        "Remove a user from the application."
        confirm_msg = _("This account will be immediately and permanently\\n"
                        "deleted.\\n\\nAre you sure you wish to continue?")
        return dict(form=delete_user_form, 
                    confirm_msg=confirm_msg, 
                    submit_text=_('Submit'),
                    action='do_delete')
    
    @expose()
    @require(identity.not_anonymous())
    @validate(form=delete_user_form, error_handler=delete_user)
    def do_delete(self, password):
        "Do the work of deleting a user."
        # The form does the password validation; so we know the user has already
        # given us a valid password.  All that is left to do is delete the user.
        # If you have other cleanup or logging items that need to be done when 
        # a user is deleted, this is the place to do them.
        user = dict(request.identity)['user']
        DBSession.delete(user)
        flash(_('Your account has been deleted.'))
        redirect('/logout_handler')
        
    def add_standard_groups(self, user):
        "Add the user to the groups specified in the config file."
        self.add_groups(user, config.reg_config.verified_user.groups)
    
    def add_unverified_groups(self, user):
        "Adds the user to the unverified user groups specified in the config file."
        self.add_groups(user, config.reg_config.unverified_user.groups)
        
    def add_groups(self, user, group_list):
        "Adds the user to each of the groups in the group_list sequence."
        for group in DBSession.query(group_class) \
            .filter(group_class.group_name.in_(group_list)):
            if group not in user.groups:
                user.groups.append(group)
        DBSession.flush()

    def remove_all_groups(self, user):
        "Removes the user from all groups that a User belongs to."
        user.groups = []
        DBSession.flush()

    def validation_hash(self, unique_input=""):
        "Returns a hash that can be used for validation."
        hash_str =  u" ".join((unique_input, request.remote_addr, 
                             self.hash_salt, datetime.datetime.now().isoformat()))
        return hashlib.new('sha1', unicode(hash_str).encode('ascii', 'replace')).hexdigest()
        
    def registration_base_url(self):
        """Returns the full http://... address of the registration controller.
        
        Does not end with a traling slash.
        """
        # Trying to find the path to the main registration controller.
        # If this has trouble, you may need to hardcode the return value
        # for this function
        last_slash = request.path.rfind('/')
        path = request.path[:last_slash]
        return '%s%s' % (request.host_url, path)
        
    def send_email(self, to_addr, from_addr, subject, body):
        "Send an email."
        # Using turbomail if it exists, 'dumb' method otherwise
        if turbomail and config.get('mail.on'):
            msg = turbomail.Message(from_addr, to_addr, subject)
            msg.plain = body
            turbomail.enqueue(msg)
        else:
            msg = MIMEText (body)
            msg['Subject'] = subject
            msg['From'] = from_addr
            msg['To'] = to_addr
        
            smtp = smtplib.SMTP(config.reg_config.mail.smtp_server, 
                                config.reg_config.mail.smtp_server_port)
            if config.reg_config.mail.smtp_server_username:
                smtp.login(config.reg_config.mail.smtp_server_username, 
                           config.reg_config.mail.smtp_server_password)
            smtp.sendmail(from_addr, to_addr, msg.as_string())
            smtp.quit()