Merge remote-tracking branch 'refs/remotes/rodney757-github/mail'
[mediagoblin.git] / mediagoblin / auth / tools.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 import uuid
18 import logging
19
20 import wtforms
21 from sqlalchemy import or_
22
23 from mediagoblin import mg_globals
24 from mediagoblin.auth import lib as auth_lib
25 from mediagoblin.tools.crypto import get_timed_signer_url
26 from mediagoblin.db.models import User
27 from mediagoblin.tools.mail import (normalize_email, send_email,
28 email_debug_message)
29 from mediagoblin.tools.template import render_template
30 from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
31
32 _log = logging.getLogger(__name__)
33
34
35 def normalize_user_or_email_field(allow_email=True, allow_user=True):
36 """
37 Check if we were passed a field that matches a username and/or email
38 pattern.
39
40 This is useful for fields that can take either a username or email
41 address. Use the parameters if you want to only allow a username for
42 instance"""
43 message = _(u'Invalid User name or email address.')
44 nomail_msg = _(u"This field does not take email addresses.")
45 nouser_msg = _(u"This field requires an email address.")
46
47 def _normalize_field(form, field):
48 email = u'@' in field.data
49 if email: # normalize email address casing
50 if not allow_email:
51 raise wtforms.ValidationError(nomail_msg)
52 wtforms.validators.Email()(form, field)
53 field.data = normalize_email(field.data)
54 else: # lower case user names
55 if not allow_user:
56 raise wtforms.ValidationError(nouser_msg)
57 wtforms.validators.Length(min=3, max=30)(form, field)
58 wtforms.validators.Regexp(r'^\w+$')(form, field)
59 field.data = field.data.lower()
60 if field.data is None: # should not happen, but be cautious anyway
61 raise wtforms.ValidationError(message)
62 return _normalize_field
63
64
65 EMAIL_VERIFICATION_TEMPLATE = (
66 u"{uri}?"
67 u"token={verification_key}")
68
69
70 def send_verification_email(user, request, email=None,
71 rendered_email=None):
72 """
73 Send the verification email to users to activate their accounts.
74
75 Args:
76 - user: a user object
77 - request: the request
78 """
79 if not email:
80 email = user.email
81
82 if not rendered_email:
83 verification_key = get_timed_signer_url('mail_verification_token') \
84 .dumps(user.id)
85 rendered_email = render_template(
86 request, 'mediagoblin/auth/verification_email.txt',
87 {'username': user.username,
88 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
89 uri=request.urlgen('mediagoblin.auth.verify_email',
90 qualified=True),
91 verification_key=verification_key)})
92
93 # TODO: There is no error handling in place
94 send_email(
95 mg_globals.app_config['email_sender_address'],
96 [email],
97 # TODO
98 # Due to the distributed nature of GNU MediaGoblin, we should
99 # find a way to send some additional information about the
100 # specific GNU MediaGoblin instance in the subject line. For
101 # example "GNU MediaGoblin @ Wandborg - [...]".
102 'GNU MediaGoblin - Verify your email!',
103 rendered_email)
104
105
106 def basic_extra_validation(register_form, *args):
107 users_with_username = User.query.filter_by(
108 username=register_form.data['username']).count()
109 users_with_email = User.query.filter_by(
110 email=register_form.data['email']).count()
111
112 extra_validation_passes = True
113
114 if users_with_username:
115 register_form.username.errors.append(
116 _(u'Sorry, a user with that name already exists.'))
117 extra_validation_passes = False
118 if users_with_email:
119 register_form.email.errors.append(
120 _(u'Sorry, a user with that email address already exists.'))
121 extra_validation_passes = False
122
123 return extra_validation_passes
124
125
126 def register_user(request, register_form):
127 """ Handle user registration """
128 extra_validation_passes = basic_extra_validation(register_form)
129
130 if extra_validation_passes:
131 # Create the user
132 user = User()
133 user.username = register_form.data['username']
134 user.email = register_form.data['email']
135 user.pw_hash = auth_lib.bcrypt_gen_password_hash(
136 register_form.password.data)
137 user.verification_key = unicode(uuid.uuid4())
138 user.save()
139
140 # log the user in
141 request.session['user_id'] = unicode(user.id)
142 request.session.save()
143
144 # send verification email
145 email_debug_message(request)
146 send_verification_email(user, request)
147
148 return user
149
150 return None
151
152
153 def check_login_simple(username, password, username_might_be_email=False):
154 search = (User.username == username)
155 if username_might_be_email and ('@' in username):
156 search = or_(search, User.email == username)
157 user = User.query.filter(search).first()
158 if not user:
159 _log.info("User %r not found", username)
160 auth_lib.fake_login_attempt()
161 return None
162 if not auth_lib.bcrypt_check_password(password, user.pw_hash):
163 _log.warn("Wrong password for %r", username)
164 return None
165 _log.info("Logging %r in", username)
166 return user