Commit | Line | Data |
---|---|---|
8e1e744d | 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
cf29e8a8 | 2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. |
24181820 CAW |
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 | ||
a77d952a | 17 | import uuid |
25ba955e | 18 | import datetime |
a77d952a | 19 | |
70f8b2d0 | 20 | from mediagoblin import messages, mg_globals |
b0c8328e | 21 | from mediagoblin.db.models import User |
152a3bfa | 22 | from mediagoblin.tools.response import render_to_response, redirect, render_404 |
ae3bc7fa | 23 | from mediagoblin.tools.translate import pass_to_ugettext as _ |
24181820 CAW |
24 | from mediagoblin.auth import lib as auth_lib |
25 | from mediagoblin.auth import forms as auth_forms | |
25ba955e AV |
26 | from mediagoblin.auth.lib import send_verification_email, \ |
27 | send_fp_verification_email | |
b2c8dbcf | 28 | from sqlalchemy import or_ |
24181820 | 29 | |
bf33272f E |
30 | def email_debug_message(request): |
31 | """ | |
32 | If the server is running in email debug mode (which is | |
33 | the current default), give a debug message to the user | |
34 | so that they have an idea where to find their email. | |
35 | """ | |
36 | if mg_globals.app_config['email_debug_mode']: | |
37 | # DEBUG message, no need to translate | |
38 | messages.add_message(request, messages.DEBUG, | |
39 | u"This instance is running in email debug mode. " | |
40 | u"The email will be on the console of the server process.") | |
41 | ||
42 | ||
24181820 | 43 | def register(request): |
a89df961 SS |
44 | """The registration view. |
45 | ||
46 | Note that usernames will always be lowercased. Email domains are lowercased while | |
47 | the first part remains case-sensitive. | |
24181820 | 48 | """ |
13677ef9 RL |
49 | # Redirects to indexpage if registrations are disabled |
50 | if not mg_globals.app_config["allow_registration"]: | |
166dc91a CAW |
51 | messages.add_message( |
52 | request, | |
53 | messages.WARNING, | |
4b1adc13 | 54 | _('Sorry, registration is disabled on this instance.')) |
13677ef9 RL |
55 | return redirect(request, "index") |
56 | ||
111a609d | 57 | register_form = auth_forms.RegistrationForm(request.form) |
24181820 CAW |
58 | |
59 | if request.method == 'POST' and register_form.validate(): | |
60 | # TODO: Make sure the user doesn't exist already | |
a89df961 SS |
61 | users_with_username = User.query.filter_by(username=register_form.data['username']).count() |
62 | users_with_email = User.query.filter_by(email=register_form.data['email']).count() | |
24181820 | 63 | |
9f6ea475 CAW |
64 | extra_validation_passes = True |
65 | ||
24181820 CAW |
66 | if users_with_username: |
67 | register_form.username.errors.append( | |
4b1adc13 | 68 | _(u'Sorry, a user with that name already exists.')) |
9f6ea475 CAW |
69 | extra_validation_passes = False |
70 | if users_with_email: | |
0bf099d7 | 71 | register_form.email.errors.append( |
5ab3855e | 72 | _(u'Sorry, a user with that email address already exists.')) |
9f6ea475 | 73 | extra_validation_passes = False |
24181820 | 74 | |
9f6ea475 | 75 | if extra_validation_passes: |
24181820 | 76 | # Create the user |
70f8b2d0 | 77 | user = User() |
a89df961 SS |
78 | user.username = register_form.data['username'] |
79 | user.email = register_form.data['email'] | |
9047b254 | 80 | user.pw_hash = auth_lib.bcrypt_gen_password_hash( |
9d140cb8 | 81 | register_form.password.data) |
479e8a83 | 82 | user.verification_key = unicode(uuid.uuid4()) |
b39d1f23 | 83 | user.save() |
0bc03620 | 84 | |
f73f4c4b | 85 | # log the user in |
5c2b8486 | 86 | request.session['user_id'] = unicode(user.id) |
f73f4c4b CAW |
87 | request.session.save() |
88 | ||
89 | # send verification email | |
bf33272f | 90 | email_debug_message(request) |
0bc03620 CAW |
91 | send_verification_email(user, request) |
92 | ||
dce5c9cb CAW |
93 | # redirect the user to their homepage... there will be a |
94 | # message waiting for them to verify their email | |
0bc03620 CAW |
95 | return redirect( |
96 | request, 'mediagoblin.user_pages.user_home', | |
5a4e3ff1 | 97 | user=user.username) |
24181820 | 98 | |
9038c9f9 CAW |
99 | return render_to_response( |
100 | request, | |
c9c24934 E |
101 | 'mediagoblin/auth/register.html', |
102 | {'register_form': register_form}) | |
24181820 CAW |
103 | |
104 | ||
692fd1c9 | 105 | def login(request): |
a3776717 | 106 | """ |
8e1e744d | 107 | MediaGoblin login view. |
a3776717 CAW |
108 | |
109 | If you provide the POST with 'next', it'll redirect to that view. | |
110 | """ | |
111a609d | 111 | login_form = auth_forms.LoginForm(request.form) |
692fd1c9 | 112 | |
a3776717 CAW |
113 | login_failed = False |
114 | ||
69b56235 | 115 | if request.method == 'POST': |
b2c8dbcf J |
116 | |
117 | username = login_form.data['username'] | |
118 | ||
69b56235 | 119 | if login_form.validate(): |
b2c8dbcf J |
120 | user = User.query.filter( |
121 | or_( | |
122 | User.username == username, | |
123 | User.email == username, | |
124 | ||
125 | )).first() | |
692fd1c9 | 126 | |
9d140cb8 | 127 | if user and user.check_login(login_form.password.data): |
69b56235 SS |
128 | # set up login in session |
129 | request.session['user_id'] = unicode(user.id) | |
130 | request.session.save() | |
692fd1c9 | 131 | |
69b56235 SS |
132 | if request.form.get('next'): |
133 | return redirect(request, location=request.form['next']) | |
134 | else: | |
135 | return redirect(request, "index") | |
692fd1c9 | 136 | |
69b56235 | 137 | # Some failure during login occured if we are here! |
692fd1c9 CAW |
138 | # Prevent detecting who's on this system by testing login |
139 | # attempt timings | |
140 | auth_lib.fake_login_attempt() | |
a3776717 | 141 | login_failed = True |
692fd1c9 | 142 | |
9038c9f9 CAW |
143 | return render_to_response( |
144 | request, | |
c9c24934 E |
145 | 'mediagoblin/auth/login.html', |
146 | {'login_form': login_form, | |
111a609d | 147 | 'next': request.GET.get('next') or request.form.get('next'), |
13bb1d67 RL |
148 | 'login_failed': login_failed, |
149 | 'allow_registration': mg_globals.app_config["allow_registration"]}) | |
692fd1c9 CAW |
150 | |
151 | ||
152 | def logout(request): | |
b97232fa CAW |
153 | # Maybe deleting the user_id parameter would be enough? |
154 | request.session.delete() | |
7b31a11c | 155 | |
9150244a | 156 | return redirect(request, "index") |
db1a438f | 157 | |
5866d1a8 | 158 | |
db1a438f | 159 | def verify_email(request): |
4c093e85 JW |
160 | """ |
161 | Email verification view | |
162 | ||
163 | validates GET parameters against database and unlocks the user account, if | |
164 | you are lucky :) | |
165 | """ | |
155f24f9 | 166 | # If we don't have userid and token parameters, we can't do anything; 404 |
285ffedd | 167 | if not 'userid' in request.GET or not 'token' in request.GET: |
de12b4e7 | 168 | return render_404(request) |
155f24f9 | 169 | |
70f8b2d0 | 170 | user = User.query.filter_by(id=request.args['userid']).first() |
db1a438f | 171 | |
00bb9550 | 172 | if user and user.verification_key == unicode(request.GET['token']): |
7a3d00ec | 173 | user.status = u'active' |
4facc7a0 | 174 | user.email_verified = True |
00bb9550 | 175 | user.verification_key = None |
daf02964 | 176 | |
db1a438f | 177 | user.save() |
daf02964 | 178 | |
fe80cb06 | 179 | messages.add_message( |
7b31a11c CAW |
180 | request, |
181 | messages.SUCCESS, | |
4b1adc13 CAW |
182 | _("Your email address has been verified. " |
183 | "You may now login, edit your profile, and submit images!")) | |
db1a438f | 184 | else: |
4b1adc13 CAW |
185 | messages.add_message( |
186 | request, | |
187 | messages.ERROR, | |
188 | _('The verification key or user id is incorrect')) | |
7b31a11c | 189 | |
269943a6 CAW |
190 | return redirect( |
191 | request, 'mediagoblin.user_pages.user_home', | |
5a4e3ff1 | 192 | user=user.username) |
28afb47c | 193 | |
5866d1a8 | 194 | |
b93a6a22 AM |
195 | def resend_activation(request): |
196 | """ | |
197 | The reactivation view | |
198 | ||
199 | Resend the activation email. | |
200 | """ | |
84a7e770 | 201 | |
2fe69916 | 202 | if request.user is None: |
7903a14f AW |
203 | messages.add_message( |
204 | request, | |
205 | messages.ERROR, | |
2fe69916 | 206 | _('You must be logged in so we know who to send the email to!')) |
dfa6994d | 207 | |
5dbeda8a | 208 | return redirect(request, 'mediagoblin.auth.login') |
7903a14f | 209 | |
0ab21f98 | 210 | if request.user.email_verified: |
84a7e770 AW |
211 | messages.add_message( |
212 | request, | |
213 | messages.ERROR, | |
2fe69916 | 214 | _("You've already verified your email address!")) |
dfa6994d | 215 | |
2fe69916 | 216 | return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username']) |
84a7e770 | 217 | |
00bb9550 | 218 | request.user.verification_key = unicode(uuid.uuid4()) |
a77d952a | 219 | request.user.save() |
dfa6994d | 220 | |
bf33272f | 221 | email_debug_message(request) |
02d80437 | 222 | send_verification_email(request.user, request) |
b93a6a22 | 223 | |
61927e6e CAW |
224 | messages.add_message( |
225 | request, | |
226 | messages.INFO, | |
4b1adc13 | 227 | _('Resent your verification email.')) |
61927e6e CAW |
228 | return redirect( |
229 | request, 'mediagoblin.user_pages.user_home', | |
5a4e3ff1 | 230 | user=request.user.username) |
25ba955e AV |
231 | |
232 | ||
233 | def forgot_password(request): | |
234 | """ | |
235 | Forgot password view | |
236 | ||
a89df961 SS |
237 | Sends an email with an url to renew forgotten password. |
238 | Use GET querystring parameter 'username' to pre-populate the input field | |
25ba955e | 239 | """ |
111a609d | 240 | fp_form = auth_forms.ForgotPassForm(request.form, |
a89df961 SS |
241 | username=request.args.get('username')) |
242 | ||
243 | if not (request.method == 'POST' and fp_form.validate()): | |
244 | # Either GET request, or invalid form submitted. Display the template | |
245 | return render_to_response(request, | |
246 | 'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form}) | |
247 | ||
248 | # If we are here: method == POST and form is valid. username casing | |
249 | # has been sanitized. Store if a user was found by email. We should | |
250 | # not reveal if the operation was successful then as we don't want to | |
251 | # leak if an email address exists in the system. | |
9d140cb8 | 252 | found_by_email = '@' in fp_form.username.data |
a89df961 SS |
253 | |
254 | if found_by_email: | |
255 | user = User.query.filter_by( | |
9d140cb8 | 256 | email = fp_form.username.data).first() |
a89df961 SS |
257 | # Don't reveal success in case the lookup happened by email address. |
258 | success_message=_("If that email address (case sensitive!) is " | |
259 | "registered an email has been sent with instructions " | |
260 | "on how to change your password.") | |
261 | ||
262 | else: # found by username | |
263 | user = User.query.filter_by( | |
9d140cb8 | 264 | username = fp_form.username.data).first() |
a89df961 SS |
265 | |
266 | if user is None: | |
267 | messages.add_message(request, | |
268 | messages.WARNING, | |
269 | _("Couldn't find someone with that username.")) | |
270 | return redirect(request, 'mediagoblin.auth.forgot_password') | |
25ba955e | 271 | |
a89df961 SS |
272 | success_message=_("An email has been sent with instructions " |
273 | "on how to change your password.") | |
bf33272f | 274 | |
a89df961 SS |
275 | if user and not(user.email_verified and user.status == 'active'): |
276 | # Don't send reminder because user is inactive or has no verified email | |
277 | messages.add_message(request, | |
278 | messages.WARNING, | |
279 | _("Could not send password recovery email as your username is in" | |
280 | "active or your account's email address has not been verified.")) | |
25ba955e | 281 | |
a89df961 SS |
282 | return redirect(request, 'mediagoblin.user_pages.user_home', |
283 | user=user.username) | |
a85a2110 | 284 | |
a89df961 SS |
285 | # SUCCESS. Send reminder and return to login page |
286 | if user: | |
287 | user.fp_verification_key = unicode(uuid.uuid4()) | |
288 | user.fp_token_expire = datetime.datetime.now() + \ | |
289 | datetime.timedelta(days=10) | |
290 | user.save() | |
992e4f80 | 291 | |
a89df961 SS |
292 | email_debug_message(request) |
293 | send_fp_verification_email(user, request) | |
992e4f80 | 294 | |
a89df961 SS |
295 | messages.add_message(request, messages.INFO, success_message) |
296 | return redirect(request, 'mediagoblin.auth.login') | |
25ba955e AV |
297 | |
298 | ||
299 | def verify_forgot_password(request): | |
961fe381 CAW |
300 | """ |
301 | Check the forgot-password verification and possibly let the user | |
302 | change their password because of it. | |
303 | """ | |
f7ab6670 CAW |
304 | # get form data variables, and specifically check for presence of token |
305 | formdata = _process_for_token(request) | |
306 | if not formdata['has_userid_and_token']: | |
8d1c9863 CFD |
307 | return render_404(request) |
308 | ||
f7ab6670 CAW |
309 | formdata_token = formdata['vars']['token'] |
310 | formdata_userid = formdata['vars']['userid'] | |
311 | formdata_vars = formdata['vars'] | |
8d1c9863 | 312 | |
70f8b2d0 SS |
313 | # check if it's a valid user id |
314 | user = User.query.filter_by(id=formdata_userid).first() | |
315 | if not user: | |
8d1c9863 CFD |
316 | return render_404(request) |
317 | ||
318 | # check if we have a real user and correct token | |
dc39e455 E |
319 | if ((user and user.fp_verification_key and |
320 | user.fp_verification_key == unicode(formdata_token) and | |
2d540fed | 321 | datetime.datetime.now() < user.fp_token_expire |
7a3d00ec | 322 | and user.email_verified and user.status == 'active')): |
73fffbb8 | 323 | |
f7ab6670 | 324 | cp_form = auth_forms.ChangePassForm(formdata_vars) |
8d1c9863 CFD |
325 | |
326 | if request.method == 'POST' and cp_form.validate(): | |
9047b254 | 327 | user.pw_hash = auth_lib.bcrypt_gen_password_hash( |
9d140cb8 | 328 | cp_form.password.data) |
dc39e455 | 329 | user.fp_verification_key = None |
2d540fed | 330 | user.fp_token_expire = None |
8d1c9863 CFD |
331 | user.save() |
332 | ||
35149b11 JS |
333 | messages.add_message( |
334 | request, | |
335 | messages.INFO, | |
336 | _("You can now log in using your new password.")) | |
337 | return redirect(request, 'mediagoblin.auth.login') | |
25ba955e | 338 | else: |
73fffbb8 CAW |
339 | return render_to_response( |
340 | request, | |
341 | 'mediagoblin/auth/change_fp.html', | |
342 | {'cp_form': cp_form}) | |
343 | ||
70f8b2d0 | 344 | # in case there is a valid id but no user with that id in the db |
8d1c9863 CFD |
345 | # or the token expired |
346 | else: | |
347 | return render_404(request) | |
348 | ||
349 | ||
350 | def _process_for_token(request): | |
351 | """ | |
f7ab6670 | 352 | Checks for tokens in formdata without prior knowledge of request method |
8d1c9863 | 353 | |
f7ab6670 CAW |
354 | For now, returns whether the userid and token formdata variables exist, and |
355 | the formdata variables in a hash. Perhaps an object is warranted? | |
8d1c9863 | 356 | """ |
f7ab6670 | 357 | # retrieve the formdata variables |
8d1c9863 | 358 | if request.method == 'GET': |
f7ab6670 | 359 | formdata_vars = request.GET |
8d1c9863 | 360 | else: |
111a609d | 361 | formdata_vars = request.form |
8d1c9863 | 362 | |
f7ab6670 CAW |
363 | formdata = { |
364 | 'vars': formdata_vars, | |
2c9e8184 | 365 | 'has_userid_and_token': |
285ffedd | 366 | 'userid' in formdata_vars and 'token' in formdata_vars} |
2c9e8184 | 367 | |
f7ab6670 | 368 | return formdata |