moved change_pass to basic_auth and fixed some typos with the moving of forgot pass
[mediagoblin.git] / mediagoblin / edit / views.py
CommitLineData
9bfe1d8e 1# GNU MediaGoblin -- federated, autonomous media hosting
cf29e8a8 2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
9bfe1d8e
E
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/>.
aba81c9f 16
3a8c3a38
JW
17from datetime import datetime
18
377db0e7 19from itsdangerous import BadSignature
62d14bf5 20from werkzeug.exceptions import Forbidden
3a8c3a38 21from werkzeug.utils import secure_filename
aba81c9f 22
d9ed098e 23from mediagoblin import messages
10d7496d 24from mediagoblin import mg_globals
152a3bfa 25
61741697 26from mediagoblin.auth import tools as auth_tools
aba81c9f 27from mediagoblin.edit import forms
b5a64f78 28from mediagoblin.edit.lib import may_edit_media
abc4da29 29from mediagoblin.decorators import (require_active_login, active_user_from_url,
89e1563f
RE
30 get_media_entry_by_id, user_may_alter_collection,
31 get_user_collection)
32from mediagoblin.tools.crypto import get_timed_signer_url
af4414a8 33from mediagoblin.tools.mail import email_debug_message
89e1563f
RE
34from mediagoblin.tools.response import (render_to_response,
35 redirect, redirect_obj, render_404)
5ae0cbaa 36from mediagoblin.tools.translate import pass_to_ugettext as _
89e1563f 37from mediagoblin.tools.template import render_template
152a3bfa 38from mediagoblin.tools.text import (
a855e92a 39 convert_to_tag_list_of_dicts, media_tags_as_string)
9e9d9083 40from mediagoblin.tools.url import slugify
be5be115 41from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used
89e1563f 42from mediagoblin.db.models import User
c849e690 43
825b1362
JW
44import mimetypes
45
97ec97db 46
461dd971 47@get_media_entry_by_id
aba81c9f
E
48@require_active_login
49def edit_media(request, media):
c849e690 50 if not may_edit_media(request, media):
cfa92229 51 raise Forbidden("User may not edit this media")
c849e690 52
2c437493 53 defaults = dict(
ec82fbd8 54 title=media.title,
5da0bf90 55 slug=media.slug,
1d939966 56 description=media.description,
a6c49d49 57 tags=media_tags_as_string(media.tags),
97ec97db 58 license=media.license)
aba81c9f 59
2c437493 60 form = forms.EditForm(
111a609d 61 request.form,
2c437493
JW
62 **defaults)
63
98857207 64 if request.method == 'POST' and form.validate():
d5e90fe4
CAW
65 # Make sure there isn't already a MediaEntry with such a slug
66 # and userid.
dc03850b 67 slug = slugify(form.slug.data)
9e9d9083 68 slug_used = check_media_slug_used(media.uploader, slug, media.id)
3a8c3a38 69
b62b3b98 70 if slug_used:
d5e90fe4 71 form.slug.errors.append(
4b1adc13 72 _(u'An entry with that slug already exists for this user.'))
d5e90fe4 73 else:
dc03850b
HL
74 media.title = form.title.data
75 media.description = form.description.data
de917303 76 media.tags = convert_to_tag_list_of_dicts(
dc03850b 77 form.tags.data)
3a8c3a38 78
dc03850b 79 media.license = unicode(form.license.data) or None
9e9d9083 80 media.slug = slug
747623cc 81 media.save()
d5e90fe4 82
2e6ee596 83 return redirect_obj(request, media)
98857207 84
bec591d8 85 if request.user.is_admin \
5c2b8486 86 and media.uploader != request.user.id \
96a2c366
CAW
87 and request.method != 'POST':
88 messages.add_message(
89 request, messages.WARNING,
4b1adc13 90 _("You are editing another user's media. Proceed with caution."))
96a2c366 91
9038c9f9
CAW
92 return render_to_response(
93 request,
c9c24934
E
94 'mediagoblin/edit/edit.html',
95 {'media': media,
96 'form': form})
46fd661e 97
3a8c3a38 98
825b1362
JW
99# Mimetypes that browsers parse scripts in.
100# Content-sniffing isn't taken into consideration.
101UNSAFE_MIMETYPES = [
102 'text/html',
103 'text/svg+xml']
104
105
954b407c 106@get_media_entry_by_id
630b57a3 107@require_active_login
3a8c3a38
JW
108def edit_attachments(request, media):
109 if mg_globals.app_config['allow_attachments']:
110 form = forms.EditAttachmentsForm()
111
112 # Add any attachements
c43f8c1d
JW
113 if 'attachment_file' in request.files \
114 and request.files['attachment_file']:
3a8c3a38 115
825b1362
JW
116 # Security measure to prevent attachments from being served as
117 # text/html, which will be parsed by web clients and pose an XSS
118 # threat.
119 #
120 # TODO
121 # This method isn't flawless as some browsers may perform
122 # content-sniffing.
123 # This method isn't flawless as we do the mimetype lookup on the
124 # machine parsing the upload form, and not necessarily the machine
125 # serving the attachments.
126 if mimetypes.guess_type(
c43f8c1d 127 request.files['attachment_file'].filename)[0] in \
825b1362
JW
128 UNSAFE_MIMETYPES:
129 public_filename = secure_filename('{0}.notsafe'.format(
c43f8c1d 130 request.files['attachment_file'].filename))
825b1362
JW
131 else:
132 public_filename = secure_filename(
c43f8c1d 133 request.files['attachment_file'].filename)
825b1362 134
3a8c3a38
JW
135 attachment_public_filepath \
136 = mg_globals.public_store.get_unique_filepath(
5c2b8486 137 ['media_entries', unicode(media.id), 'attachment',
825b1362 138 public_filename])
3a8c3a38
JW
139
140 attachment_public_file = mg_globals.public_store.get_file(
141 attachment_public_filepath, 'wb')
142
143 try:
144 attachment_public_file.write(
c43f8c1d 145 request.files['attachment_file'].stream.read())
3a8c3a38 146 finally:
c43f8c1d 147 request.files['attachment_file'].stream.close()
3a8c3a38 148
35029581 149 media.attachment_files.append(dict(
dc03850b 150 name=form.attachment_name.data \
c43f8c1d 151 or request.files['attachment_file'].filename,
3a8c3a38 152 filepath=attachment_public_filepath,
243c3843 153 created=datetime.utcnow(),
3a8c3a38 154 ))
630b57a3 155
3a8c3a38
JW
156 media.save()
157
158 messages.add_message(
159 request, messages.SUCCESS,
32255ec0 160 _("You added the attachment %s!") \
dc03850b 161 % (form.attachment_name.data
c43f8c1d 162 or request.files['attachment_file'].filename))
3a8c3a38 163
950124e6
SS
164 return redirect(request,
165 location=media.url_for_self(request.urlgen))
3a8c3a38
JW
166 return render_to_response(
167 request,
168 'mediagoblin/edit/attachments.html',
169 {'media': media,
170 'form': form})
171 else:
cfa92229 172 raise Forbidden("Attachments are disabled")
3a8c3a38 173
abc4da29
SS
174@require_active_login
175def legacy_edit_profile(request):
176 """redirect the old /edit/profile/?username=USER to /u/USER/edit/"""
177 username = request.GET.get('username') or request.user.username
178 return redirect(request, 'mediagoblin.edit.profile', user=username)
179
3a8c3a38
JW
180
181@require_active_login
abc4da29
SS
182@active_user_from_url
183def edit_profile(request, url_user=None):
184 # admins may edit any user profile
185 if request.user.username != url_user.username:
186 if not request.user.is_admin:
187 raise Forbidden(_("You can only edit your own profile."))
188
a0cf14fe
CFD
189 # No need to warn again if admin just submitted an edited profile
190 if request.method != 'POST':
191 messages.add_message(
192 request, messages.WARNING,
4b1adc13 193 _("You are editing a user's profile. Proceed with caution."))
abc4da29
SS
194
195 user = url_user
a0cf14fe 196
111a609d 197 form = forms.EditProfileForm(request.form,
066d49b2
SS
198 url=user.url,
199 bio=user.bio)
630b57a3 200
201 if request.method == 'POST' and form.validate():
dc03850b
HL
202 user.url = unicode(form.url.data)
203 user.bio = unicode(form.bio.data)
4c465852 204
c8071fa5 205 user.save()
630b57a3 206
c8071fa5
JS
207 messages.add_message(request,
208 messages.SUCCESS,
209 _("Profile changes saved"))
210 return redirect(request,
211 'mediagoblin.user_pages.user_home',
703d09b9 212 user=user.username)
630b57a3 213
214 return render_to_response(
215 request,
216 'mediagoblin/edit/edit_profile.html',
217 {'user': user,
218 'form': form})
c8071fa5 219
89e1563f
RE
220EMAIL_VERIFICATION_TEMPLATE = (
221 u'{uri}?'
222 u'token={verification_key}')
223
c8071fa5
JS
224
225@require_active_login
226def edit_account(request):
c8071fa5 227 user = request.user
111a609d 228 form = forms.EditAccountForm(request.form,
066d49b2
SS
229 wants_comment_notification=user.wants_comment_notification,
230 license_preference=user.license_preference)
c8071fa5 231
89e1563f 232 if request.method == 'POST' and form.validate():
f670f48d 233 user.wants_comment_notification = form.wants_comment_notification.data
dc4dfbde 234
f670f48d 235 user.license_preference = form.license_preference.data
dc4dfbde 236
89e1563f 237 if form.new_email.data:
5adb906a 238 _update_email(request, form, user)
89e1563f
RE
239
240 if not form.errors:
dc4dfbde
MH
241 user.save()
242 messages.add_message(request,
89e1563f
RE
243 messages.SUCCESS,
244 _("Account settings saved"))
dc4dfbde 245 return redirect(request,
89e1563f
RE
246 'mediagoblin.user_pages.user_home',
247 user=user.username)
630b57a3 248
249 return render_to_response(
250 request,
c8071fa5 251 'mediagoblin/edit/edit_account.html',
630b57a3 252 {'user': user,
253 'form': form})
be5be115
AW
254
255
380f22b8
SS
256@require_active_login
257def delete_account(request):
258 """Delete a user completely"""
259 user = request.user
260 if request.method == 'POST':
261 if request.form.get(u'confirmed'):
262 # Form submitted and confirmed. Actually delete the user account
263 # Log out user and delete cookies etc.
264 # TODO: Should we be using MG.auth.views.py:logout for this?
265 request.session.delete()
266
267 # Delete user account and all related media files etc....
268 request.user.delete()
269
270 # We should send a message that the user has been deleted
271 # successfully. But we just deleted the session, so we
272 # can't...
273 return redirect(request, 'index')
274
275 else: # Did not check the confirmation box...
276 messages.add_message(
277 request, messages.WARNING,
278 _('You need to confirm the deletion of your account.'))
279
280 # No POST submission or not confirmed, just show page
281 return render_to_response(
282 request,
283 'mediagoblin/edit/delete_account.html',
284 {'user': user})
285
286
be5be115
AW
287@require_active_login
288@user_may_alter_collection
289@get_user_collection
290def edit_collection(request, collection):
291 defaults = dict(
292 title=collection.title,
293 slug=collection.slug,
294 description=collection.description)
295
296 form = forms.EditCollectionForm(
111a609d 297 request.form,
be5be115
AW
298 **defaults)
299
300 if request.method == 'POST' and form.validate():
301 # Make sure there isn't already a Collection with such a slug
302 # and userid.
455fd36f 303 slug_used = check_collection_slug_used(collection.creator,
dc03850b 304 form.slug.data, collection.id)
c43f8c1d 305
be5be115 306 # Make sure there isn't already a Collection with this title
44082b12
RE
307 existing_collection = request.db.Collection.query.filter_by(
308 creator=request.user.id,
309 title=form.title.data).first()
c43f8c1d 310
be5be115
AW
311 if existing_collection and existing_collection.id != collection.id:
312 messages.add_message(
a6481028
CAW
313 request, messages.ERROR,
314 _('You already have a collection called "%s"!') % \
dc03850b 315 form.title.data)
be5be115
AW
316 elif slug_used:
317 form.slug.errors.append(
318 _(u'A collection with that slug already exists for this user.'))
319 else:
dc03850b
HL
320 collection.title = unicode(form.title.data)
321 collection.description = unicode(form.description.data)
322 collection.slug = unicode(form.slug.data)
be5be115
AW
323
324 collection.save()
325
2e6ee596 326 return redirect_obj(request, collection)
be5be115
AW
327
328 if request.user.is_admin \
5c2b8486 329 and collection.creator != request.user.id \
be5be115
AW
330 and request.method != 'POST':
331 messages.add_message(
332 request, messages.WARNING,
333 _("You are editing another user's collection. Proceed with caution."))
334
335 return render_to_response(
336 request,
337 'mediagoblin/edit/edit_collection.html',
338 {'collection': collection,
339 'form': form})
39aa1db4
RE
340
341
89e1563f
RE
342def verify_email(request):
343 """
344 Email verification view for changing email address
345 """
346 # If no token, we can't do anything
347 if not 'token' in request.GET:
348 return render_404(request)
349
377db0e7
RE
350 # Catch error if token is faked or expired
351 token = None
352 try:
353 token = get_timed_signer_url("mail_verification_token") \
354 .loads(request.GET['token'], max_age=10*24*3600)
355 except BadSignature:
356 messages.add_message(
357 request,
358 messages.ERROR,
359 _('The verification key or user id is incorrect.'))
360
361 return redirect(
362 request,
363 'index')
89e1563f
RE
364
365 user = User.query.filter_by(id=int(token['user'])).first()
366
367 if user:
368 user.email = token['email']
369 user.save()
370
371 messages.add_message(
372 request,
373 messages.SUCCESS,
374 _('Your email address has been verified.'))
375
376 else:
377 messages.add_message(
378 request,
379 messages.ERROR,
380 _('The verification key or user id is incorrect.'))
381
382 return redirect(
383 request, 'mediagoblin.user_pages.user_home',
384 user=user.username)
5adb906a
RE
385
386
387def _update_email(request, form, user):
388 new_email = form.new_email.data
389 users_with_email = User.query.filter_by(
390 email=new_email).count()
391
392 if users_with_email:
393 form.new_email.errors.append(
394 _('Sorry, a user with that email address'
395 ' already exists.'))
396
397 elif not users_with_email:
398 verification_key = get_timed_signer_url(
399 'mail_verification_token').dumps({
400 'user': user.id,
401 'email': new_email})
402
403 rendered_email = render_template(
404 request, 'mediagoblin/edit/verification.txt',
405 {'username': user.username,
406 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
407 uri=request.urlgen('mediagoblin.edit.verify_email',
408 qualified=True),
409 verification_key=verification_key)})
410
411 email_debug_message(request)
412 auth_tools.send_verification_email(user, request, new_email,
413 rendered_email)