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