| 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 datetime |
| 19 | |
| 20 | from mediagoblin import messages, mg_globals |
| 21 | from mediagoblin.db.models import User |
| 22 | from mediagoblin.tools.response import render_to_response, redirect, render_404 |
| 23 | from mediagoblin.tools.translate import pass_to_ugettext as _ |
| 24 | from mediagoblin.auth import lib as auth_lib |
| 25 | from mediagoblin.auth import forms as auth_forms |
| 26 | from mediagoblin.auth.lib import send_verification_email, \ |
| 27 | send_fp_verification_email |
| 28 | from sqlalchemy import or_ |
| 29 | |
| 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 | |
| 43 | def register(request): |
| 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. |
| 48 | """ |
| 49 | # Redirects to indexpage if registrations are disabled |
| 50 | if not mg_globals.app_config["allow_registration"]: |
| 51 | messages.add_message( |
| 52 | request, |
| 53 | messages.WARNING, |
| 54 | _('Sorry, registration is disabled on this instance.')) |
| 55 | return redirect(request, "index") |
| 56 | |
| 57 | register_form = auth_forms.RegistrationForm(request.form) |
| 58 | |
| 59 | if request.method == 'POST' and register_form.validate(): |
| 60 | # TODO: Make sure the user doesn't exist already |
| 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() |
| 63 | |
| 64 | extra_validation_passes = True |
| 65 | |
| 66 | if users_with_username: |
| 67 | register_form.username.errors.append( |
| 68 | _(u'Sorry, a user with that name already exists.')) |
| 69 | extra_validation_passes = False |
| 70 | if users_with_email: |
| 71 | register_form.email.errors.append( |
| 72 | _(u'Sorry, a user with that email address already exists.')) |
| 73 | extra_validation_passes = False |
| 74 | |
| 75 | if extra_validation_passes: |
| 76 | # Create the user |
| 77 | user = User() |
| 78 | user.username = register_form.data['username'] |
| 79 | user.email = register_form.data['email'] |
| 80 | user.pw_hash = auth_lib.bcrypt_gen_password_hash( |
| 81 | register_form.password.data) |
| 82 | user.verification_key = unicode(uuid.uuid4()) |
| 83 | user.save() |
| 84 | |
| 85 | # log the user in |
| 86 | request.session['user_id'] = unicode(user.id) |
| 87 | request.session.save() |
| 88 | |
| 89 | # send verification email |
| 90 | email_debug_message(request) |
| 91 | send_verification_email(user, request) |
| 92 | |
| 93 | # redirect the user to their homepage... there will be a |
| 94 | # message waiting for them to verify their email |
| 95 | return redirect( |
| 96 | request, 'mediagoblin.user_pages.user_home', |
| 97 | user=user.username) |
| 98 | |
| 99 | return render_to_response( |
| 100 | request, |
| 101 | 'mediagoblin/auth/register.html', |
| 102 | {'register_form': register_form}) |
| 103 | |
| 104 | |
| 105 | def login(request): |
| 106 | """ |
| 107 | MediaGoblin login view. |
| 108 | |
| 109 | If you provide the POST with 'next', it'll redirect to that view. |
| 110 | """ |
| 111 | login_form = auth_forms.LoginForm(request.form) |
| 112 | |
| 113 | login_failed = False |
| 114 | |
| 115 | if request.method == 'POST': |
| 116 | |
| 117 | username = login_form.data['username'] |
| 118 | |
| 119 | if login_form.validate(): |
| 120 | user = User.query.filter( |
| 121 | or_( |
| 122 | User.username == username, |
| 123 | User.email == username, |
| 124 | |
| 125 | )).first() |
| 126 | |
| 127 | if user and user.check_login(login_form.password.data): |
| 128 | # set up login in session |
| 129 | request.session['user_id'] = unicode(user.id) |
| 130 | request.session.save() |
| 131 | |
| 132 | if request.form.get('next'): |
| 133 | return redirect(request, location=request.form['next']) |
| 134 | else: |
| 135 | return redirect(request, "index") |
| 136 | |
| 137 | # Some failure during login occured if we are here! |
| 138 | # Prevent detecting who's on this system by testing login |
| 139 | # attempt timings |
| 140 | auth_lib.fake_login_attempt() |
| 141 | login_failed = True |
| 142 | |
| 143 | return render_to_response( |
| 144 | request, |
| 145 | 'mediagoblin/auth/login.html', |
| 146 | {'login_form': login_form, |
| 147 | 'next': request.GET.get('next') or request.form.get('next'), |
| 148 | 'login_failed': login_failed, |
| 149 | 'allow_registration': mg_globals.app_config["allow_registration"]}) |
| 150 | |
| 151 | |
| 152 | def logout(request): |
| 153 | # Maybe deleting the user_id parameter would be enough? |
| 154 | request.session.delete() |
| 155 | |
| 156 | return redirect(request, "index") |
| 157 | |
| 158 | |
| 159 | def verify_email(request): |
| 160 | """ |
| 161 | Email verification view |
| 162 | |
| 163 | validates GET parameters against database and unlocks the user account, if |
| 164 | you are lucky :) |
| 165 | """ |
| 166 | # If we don't have userid and token parameters, we can't do anything; 404 |
| 167 | if not 'userid' in request.GET or not 'token' in request.GET: |
| 168 | return render_404(request) |
| 169 | |
| 170 | user = User.query.filter_by(id=request.args['userid']).first() |
| 171 | |
| 172 | if user and user.verification_key == unicode(request.GET['token']): |
| 173 | user.status = u'active' |
| 174 | user.email_verified = True |
| 175 | user.verification_key = None |
| 176 | |
| 177 | user.save() |
| 178 | |
| 179 | messages.add_message( |
| 180 | request, |
| 181 | messages.SUCCESS, |
| 182 | _("Your email address has been verified. " |
| 183 | "You may now login, edit your profile, and submit images!")) |
| 184 | else: |
| 185 | messages.add_message( |
| 186 | request, |
| 187 | messages.ERROR, |
| 188 | _('The verification key or user id is incorrect')) |
| 189 | |
| 190 | return redirect( |
| 191 | request, 'mediagoblin.user_pages.user_home', |
| 192 | user=user.username) |
| 193 | |
| 194 | |
| 195 | def resend_activation(request): |
| 196 | """ |
| 197 | The reactivation view |
| 198 | |
| 199 | Resend the activation email. |
| 200 | """ |
| 201 | |
| 202 | if request.user is None: |
| 203 | messages.add_message( |
| 204 | request, |
| 205 | messages.ERROR, |
| 206 | _('You must be logged in so we know who to send the email to!')) |
| 207 | |
| 208 | return redirect(request, 'mediagoblin.auth.login') |
| 209 | |
| 210 | if request.user.email_verified: |
| 211 | messages.add_message( |
| 212 | request, |
| 213 | messages.ERROR, |
| 214 | _("You've already verified your email address!")) |
| 215 | |
| 216 | return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username']) |
| 217 | |
| 218 | request.user.verification_key = unicode(uuid.uuid4()) |
| 219 | request.user.save() |
| 220 | |
| 221 | email_debug_message(request) |
| 222 | send_verification_email(request.user, request) |
| 223 | |
| 224 | messages.add_message( |
| 225 | request, |
| 226 | messages.INFO, |
| 227 | _('Resent your verification email.')) |
| 228 | return redirect( |
| 229 | request, 'mediagoblin.user_pages.user_home', |
| 230 | user=request.user.username) |
| 231 | |
| 232 | |
| 233 | def forgot_password(request): |
| 234 | """ |
| 235 | Forgot password view |
| 236 | |
| 237 | Sends an email with an url to renew forgotten password. |
| 238 | Use GET querystring parameter 'username' to pre-populate the input field |
| 239 | """ |
| 240 | fp_form = auth_forms.ForgotPassForm(request.form, |
| 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. |
| 252 | found_by_email = '@' in fp_form.username.data |
| 253 | |
| 254 | if found_by_email: |
| 255 | user = User.query.filter_by( |
| 256 | email = fp_form.username.data).first() |
| 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( |
| 264 | username = fp_form.username.data).first() |
| 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') |
| 271 | |
| 272 | success_message=_("An email has been sent with instructions " |
| 273 | "on how to change your password.") |
| 274 | |
| 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.")) |
| 281 | |
| 282 | return redirect(request, 'mediagoblin.user_pages.user_home', |
| 283 | user=user.username) |
| 284 | |
| 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() |
| 291 | |
| 292 | email_debug_message(request) |
| 293 | send_fp_verification_email(user, request) |
| 294 | |
| 295 | messages.add_message(request, messages.INFO, success_message) |
| 296 | return redirect(request, 'mediagoblin.auth.login') |
| 297 | |
| 298 | |
| 299 | def verify_forgot_password(request): |
| 300 | """ |
| 301 | Check the forgot-password verification and possibly let the user |
| 302 | change their password because of it. |
| 303 | """ |
| 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']: |
| 307 | return render_404(request) |
| 308 | |
| 309 | formdata_token = formdata['vars']['token'] |
| 310 | formdata_userid = formdata['vars']['userid'] |
| 311 | formdata_vars = formdata['vars'] |
| 312 | |
| 313 | # check if it's a valid user id |
| 314 | user = User.query.filter_by(id=formdata_userid).first() |
| 315 | if not user: |
| 316 | return render_404(request) |
| 317 | |
| 318 | # check if we have a real user and correct token |
| 319 | if ((user and user.fp_verification_key and |
| 320 | user.fp_verification_key == unicode(formdata_token) and |
| 321 | datetime.datetime.now() < user.fp_token_expire |
| 322 | and user.email_verified and user.status == 'active')): |
| 323 | |
| 324 | cp_form = auth_forms.ChangePassForm(formdata_vars) |
| 325 | |
| 326 | if request.method == 'POST' and cp_form.validate(): |
| 327 | user.pw_hash = auth_lib.bcrypt_gen_password_hash( |
| 328 | cp_form.password.data) |
| 329 | user.fp_verification_key = None |
| 330 | user.fp_token_expire = None |
| 331 | user.save() |
| 332 | |
| 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') |
| 338 | else: |
| 339 | return render_to_response( |
| 340 | request, |
| 341 | 'mediagoblin/auth/change_fp.html', |
| 342 | {'cp_form': cp_form}) |
| 343 | |
| 344 | # in case there is a valid id but no user with that id in the db |
| 345 | # or the token expired |
| 346 | else: |
| 347 | return render_404(request) |
| 348 | |
| 349 | |
| 350 | def _process_for_token(request): |
| 351 | """ |
| 352 | Checks for tokens in formdata without prior knowledge of request method |
| 353 | |
| 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? |
| 356 | """ |
| 357 | # retrieve the formdata variables |
| 358 | if request.method == 'GET': |
| 359 | formdata_vars = request.GET |
| 360 | else: |
| 361 | formdata_vars = request.form |
| 362 | |
| 363 | formdata = { |
| 364 | 'vars': formdata_vars, |
| 365 | 'has_userid_and_token': |
| 366 | 'userid' in formdata_vars and 'token' in formdata_vars} |
| 367 | |
| 368 | return formdata |