Moved common, translation, template, and url code out of util.py and into tools/...
[mediagoblin.git] / mediagoblin / auth / views.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 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 datetime
19
20 from webob import exc
21
22 from mediagoblin import messages
23 from mediagoblin import mg_globals
24 from mediagoblin.util import render_to_response, redirect, render_404
25 from mediagoblin.tools.translate import pass_to_ugettext as _
26 from mediagoblin.db.util import ObjectId, InvalidId
27 from mediagoblin.auth import lib as auth_lib
28 from mediagoblin.auth import forms as auth_forms
29 from mediagoblin.auth.lib import send_verification_email, \
30 send_fp_verification_email
31
32
33 def register(request):
34 """
35 Your classic registration view!
36 """
37 # Redirects to indexpage if registrations are disabled
38 if not mg_globals.app_config["allow_registration"]:
39 messages.add_message(
40 request,
41 messages.WARNING,
42 _('Sorry, registration is disabled on this instance.'))
43 return redirect(request, "index")
44
45 register_form = auth_forms.RegistrationForm(request.POST)
46
47 if request.method == 'POST' and register_form.validate():
48 # TODO: Make sure the user doesn't exist already
49 username = unicode(request.POST['username'].lower())
50 email = unicode(request.POST['email'].lower())
51 users_with_username = request.db.User.find(
52 {'username': username}).count()
53 users_with_email = request.db.User.find(
54 {'email': email}).count()
55
56 extra_validation_passes = True
57
58 if users_with_username:
59 register_form.username.errors.append(
60 _(u'Sorry, a user with that name already exists.'))
61 extra_validation_passes = False
62 if users_with_email:
63 register_form.email.errors.append(
64 _(u'Sorry, that email address has already been taken.'))
65 extra_validation_passes = False
66
67 if extra_validation_passes:
68 # Create the user
69 user = request.db.User()
70 user['username'] = username
71 user['email'] = email
72 user['pw_hash'] = auth_lib.bcrypt_gen_password_hash(
73 request.POST['password'])
74 user.save(validate=True)
75
76 # log the user in
77 request.session['user_id'] = unicode(user['_id'])
78 request.session.save()
79
80 # send verification email
81 send_verification_email(user, request)
82
83 # redirect the user to their homepage... there will be a
84 # message waiting for them to verify their email
85 return redirect(
86 request, 'mediagoblin.user_pages.user_home',
87 user=user['username'])
88
89 return render_to_response(
90 request,
91 'mediagoblin/auth/register.html',
92 {'register_form': register_form})
93
94
95 def login(request):
96 """
97 MediaGoblin login view.
98
99 If you provide the POST with 'next', it'll redirect to that view.
100 """
101 login_form = auth_forms.LoginForm(request.POST)
102
103 login_failed = False
104
105 if request.method == 'POST' and login_form.validate():
106 user = request.db.User.one(
107 {'username': request.POST['username'].lower()})
108
109 if user and user.check_login(request.POST['password']):
110 # set up login in session
111 request.session['user_id'] = unicode(user['_id'])
112 request.session.save()
113
114 if request.POST.get('next'):
115 return exc.HTTPFound(location=request.POST['next'])
116 else:
117 return redirect(request, "index")
118
119 else:
120 # Prevent detecting who's on this system by testing login
121 # attempt timings
122 auth_lib.fake_login_attempt()
123 login_failed = True
124
125 return render_to_response(
126 request,
127 'mediagoblin/auth/login.html',
128 {'login_form': login_form,
129 'next': request.GET.get('next') or request.POST.get('next'),
130 'login_failed': login_failed,
131 'allow_registration': mg_globals.app_config["allow_registration"]})
132
133
134 def logout(request):
135 # Maybe deleting the user_id parameter would be enough?
136 request.session.delete()
137
138 return redirect(request, "index")
139
140
141 def verify_email(request):
142 """
143 Email verification view
144
145 validates GET parameters against database and unlocks the user account, if
146 you are lucky :)
147 """
148 # If we don't have userid and token parameters, we can't do anything; 404
149 if not request.GET.has_key('userid') or not request.GET.has_key('token'):
150 return render_404(request)
151
152 user = request.db.User.find_one(
153 {'_id': ObjectId(unicode(request.GET['userid']))})
154
155 if user and user['verification_key'] == unicode(request.GET['token']):
156 user[u'status'] = u'active'
157 user[u'email_verified'] = True
158 user[u'verification_key'] = None
159
160 user.save()
161
162 messages.add_message(
163 request,
164 messages.SUCCESS,
165 _("Your email address has been verified. "
166 "You may now login, edit your profile, and submit images!"))
167 else:
168 messages.add_message(
169 request,
170 messages.ERROR,
171 _('The verification key or user id is incorrect'))
172
173 return redirect(
174 request, 'mediagoblin.user_pages.user_home',
175 user=user['username'])
176
177
178 def resend_activation(request):
179 """
180 The reactivation view
181
182 Resend the activation email.
183 """
184 request.user[u'verification_key'] = unicode(uuid.uuid4())
185 request.user.save()
186
187 send_verification_email(request.user, request)
188
189 messages.add_message(
190 request,
191 messages.INFO,
192 _('Resent your verification email.'))
193 return redirect(
194 request, 'mediagoblin.user_pages.user_home',
195 user=request.user['username'])
196
197
198 def forgot_password(request):
199 """
200 Forgot password view
201
202 Sends an email whit an url to renew forgoten password
203 """
204 fp_form = auth_forms.ForgotPassForm(request.POST)
205
206 if request.method == 'POST' and fp_form.validate():
207 # '$or' not available till mongodb 1.5.3
208 user = request.db.User.find_one(
209 {'username': request.POST['username']})
210 if not user:
211 user = request.db.User.find_one(
212 {'email': request.POST['username']})
213
214 if user:
215 if user['email_verified'] and user['status'] == 'active':
216 user[u'fp_verification_key'] = unicode(uuid.uuid4())
217 user[u'fp_token_expire'] = datetime.datetime.now() + \
218 datetime.timedelta(days=10)
219 user.save()
220
221 send_fp_verification_email(user, request)
222 else:
223 # special case... we can't send the email because the
224 # username is inactive / hasn't verified their email
225 messages.add_message(
226 request,
227 messages.WARNING,
228 _("Could not send password recovery email as "
229 "your username is inactive or your account's "
230 "email address has not been verified."))
231
232 return redirect(
233 request, 'mediagoblin.user_pages.user_home',
234 user=user['username'])
235
236
237 # do not reveal whether or not there is a matching user, just move along
238 return redirect(request, 'mediagoblin.auth.fp_email_sent')
239
240 return render_to_response(
241 request,
242 'mediagoblin/auth/forgot_password.html',
243 {'fp_form': fp_form})
244
245
246 def verify_forgot_password(request):
247 """
248 Check the forgot-password verification and possibly let the user
249 change their password because of it.
250 """
251 # get form data variables, and specifically check for presence of token
252 formdata = _process_for_token(request)
253 if not formdata['has_userid_and_token']:
254 return render_404(request)
255
256 formdata_token = formdata['vars']['token']
257 formdata_userid = formdata['vars']['userid']
258 formdata_vars = formdata['vars']
259
260 # check if it's a valid Id
261 try:
262 user = request.db.User.find_one(
263 {'_id': ObjectId(unicode(formdata_userid))})
264 except InvalidId:
265 return render_404(request)
266
267 # check if we have a real user and correct token
268 if ((user and user['fp_verification_key'] and
269 user['fp_verification_key'] == unicode(formdata_token) and
270 datetime.datetime.now() < user['fp_token_expire']
271 and user['email_verified'] and user['status'] == 'active')):
272
273 cp_form = auth_forms.ChangePassForm(formdata_vars)
274
275 if request.method == 'POST' and cp_form.validate():
276 user[u'pw_hash'] = auth_lib.bcrypt_gen_password_hash(
277 request.POST['password'])
278 user[u'fp_verification_key'] = None
279 user[u'fp_token_expire'] = None
280 user.save()
281
282 return redirect(request, 'mediagoblin.auth.fp_changed_success')
283 else:
284 return render_to_response(
285 request,
286 'mediagoblin/auth/change_fp.html',
287 {'cp_form': cp_form})
288
289 # in case there is a valid id but no user whit that id in the db
290 # or the token expired
291 else:
292 return render_404(request)
293
294
295 def _process_for_token(request):
296 """
297 Checks for tokens in formdata without prior knowledge of request method
298
299 For now, returns whether the userid and token formdata variables exist, and
300 the formdata variables in a hash. Perhaps an object is warranted?
301 """
302 # retrieve the formdata variables
303 if request.method == 'GET':
304 formdata_vars = request.GET
305 else:
306 formdata_vars = request.POST
307
308 formdata = {
309 'vars': formdata_vars,
310 'has_userid_and_token':
311 formdata_vars.has_key('userid') and formdata_vars.has_key('token')}
312
313 return formdata