Only video uploader can edit their subtitles
[mediagoblin.git] / mediagoblin / edit / views.py
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 six
18
19 from datetime import datetime
20
21 from itsdangerous import BadSignature
22 from pyld import jsonld
23 from werkzeug.exceptions import Forbidden
24 from werkzeug.utils import secure_filename
25 from jsonschema import ValidationError, Draft4Validator
26
27 from mediagoblin import messages
28 from mediagoblin import mg_globals
29
30 from mediagoblin.auth import (check_password,
31 tools as auth_tools)
32 from mediagoblin.edit import forms
33 from mediagoblin.edit.lib import may_edit_media
34 from mediagoblin.decorators import (require_active_login, active_user_from_url,
35 get_media_entry_by_id, user_may_alter_collection,
36 get_user_collection, user_has_privilege,
37 user_not_banned, path_subtitle, user_may_delete_media)
38 from mediagoblin.tools.crypto import get_timed_signer_url
39 from mediagoblin.tools.metadata import (compact_and_validate, DEFAULT_CHECKER,
40 DEFAULT_SCHEMA)
41 from mediagoblin.tools.mail import email_debug_message
42 from mediagoblin.tools.response import (render_to_response,
43 redirect, redirect_obj, render_404)
44 from mediagoblin.tools.translate import pass_to_ugettext as _
45 from mediagoblin.tools.template import render_template
46 from mediagoblin.tools.text import (
47 convert_to_tag_list_of_dicts, media_tags_as_string)
48 from mediagoblin.tools.url import slugify
49 from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used
50 from mediagoblin.db.models import User, LocalUser, Client, AccessToken, Location
51
52 import mimetypes
53
54
55 @get_media_entry_by_id
56 @require_active_login
57 def edit_media(request, media):
58 if not may_edit_media(request, media):
59 raise Forbidden("User may not edit this media")
60
61 defaults = dict(
62 title=media.title,
63 slug=media.slug,
64 description=media.description,
65 tags=media_tags_as_string(media.tags),
66 license=media.license)
67
68 form = forms.EditForm(
69 request.form,
70 **defaults)
71
72 if request.method == 'POST' and form.validate():
73 # Make sure there isn't already a MediaEntry with such a slug
74 # and userid.
75 slug = slugify(form.slug.data)
76 slug_used = check_media_slug_used(media.actor, slug, media.id)
77
78 if slug_used:
79 form.slug.errors.append(
80 _(u'An entry with that slug already exists for this user.'))
81 else:
82 media.title = form.title.data
83 media.description = form.description.data
84 media.tags = convert_to_tag_list_of_dicts(
85 form.tags.data)
86
87 media.license = six.text_type(form.license.data) or None
88 media.slug = slug
89 media.save()
90
91 return redirect_obj(request, media)
92
93 if request.user.has_privilege(u'admin') \
94 and media.actor != request.user.id \
95 and request.method != 'POST':
96 messages.add_message(
97 request,
98 messages.WARNING,
99 _("You are editing another user's media. Proceed with caution."))
100
101 return render_to_response(
102 request,
103 'mediagoblin/edit/edit.html',
104 {'media': media,
105 'form': form})
106
107
108 # Mimetypes that browsers parse scripts in.
109 # Content-sniffing isn't taken into consideration.
110 UNSAFE_MIMETYPES = [
111 'text/html',
112 'text/svg+xml']
113
114
115 @get_media_entry_by_id
116 @require_active_login
117 def edit_attachments(request, media):
118 if mg_globals.app_config['allow_attachments']:
119 form = forms.EditAttachmentsForm()
120
121 # Add any attachements
122 if 'attachment_file' in request.files \
123 and request.files['attachment_file']:
124
125 # Security measure to prevent attachments from being served as
126 # text/html, which will be parsed by web clients and pose an XSS
127 # threat.
128 #
129 # TODO
130 # This method isn't flawless as some browsers may perform
131 # content-sniffing.
132 # This method isn't flawless as we do the mimetype lookup on the
133 # machine parsing the upload form, and not necessarily the machine
134 # serving the attachments.
135 if mimetypes.guess_type(
136 request.files['attachment_file'].filename)[0] in \
137 UNSAFE_MIMETYPES:
138 public_filename = secure_filename('{0}.notsafe'.format(
139 request.files['attachment_file'].filename))
140 else:
141 public_filename = secure_filename(
142 request.files['attachment_file'].filename)
143
144 attachment_public_filepath \
145 = mg_globals.public_store.get_unique_filepath(
146 ['media_entries', six.text_type(media.id), 'attachment',
147 public_filename])
148
149 attachment_public_file = mg_globals.public_store.get_file(
150 attachment_public_filepath, 'wb')
151
152 try:
153 attachment_public_file.write(
154 request.files['attachment_file'].stream.read())
155 finally:
156 request.files['attachment_file'].stream.close()
157
158 media.attachment_files.append(dict(
159 name=form.attachment_name.data \
160 or request.files['attachment_file'].filename,
161 filepath=attachment_public_filepath,
162 created=datetime.utcnow(),
163 ))
164
165 media.save()
166
167 messages.add_message(
168 request,
169 messages.SUCCESS,
170 _("You added the attachment %s!") %
171 (form.attachment_name.data or
172 request.files['attachment_file'].filename))
173
174 return redirect(request,
175 location=media.url_for_self(request.urlgen))
176 return render_to_response(
177 request,
178 'mediagoblin/edit/attachments.html',
179 {'media': media,
180 'form': form})
181 else:
182 raise Forbidden("Attachments are disabled")
183
184 @get_media_entry_by_id
185 @user_may_delete_media
186 @require_active_login
187 def edit_subtitles(request, media):
188 if mg_globals.app_config['allow_subtitles']:
189 form = forms.EditSubtitlesForm(request.form)
190
191 # Add any subtitles
192 if 'subtitle_file' in request.files \
193 and request.files['subtitle_file']:
194 if mimetypes.guess_type(
195 request.files['subtitle_file'].filename)[0] in \
196 UNSAFE_MIMETYPES:
197 public_filename = secure_filename('{0}.notsafe'.format(
198 request.files['subtitle_file'].filename))
199 else:
200 public_filename = secure_filename(
201 request.files['subtitle_file'].filename)
202
203 subtitle_public_filepath \
204 = mg_globals.public_store.get_unique_filepath(
205 ['media_entries', six.text_type(media.id), 'subtitle',
206 public_filename])
207
208 subtitle_public_file = mg_globals.public_store.get_file(
209 subtitle_public_filepath, 'wb')
210
211 try:
212 subtitle_public_file.write(
213 request.files['subtitle_file'].stream.read())
214 finally:
215 request.files['subtitle_file'].stream.close()
216
217 media.subtitle_files.append(dict(
218 name=form.subtitle_language.data \
219 or request.files['subtitle_file'].filename,
220 filepath=subtitle_public_filepath,
221 created=datetime.utcnow(),
222 ))
223
224 media.save()
225
226 messages.add_message(
227 request,
228 messages.SUCCESS,
229 _("You added the subttile %s!") %
230 (form.subtitle_language.data or
231 request.files['subtitle_file'].filename))
232
233 return redirect(request,
234 location=media.url_for_self(request.urlgen))
235 return render_to_response(
236 request,
237 'mediagoblin/edit/subtitles.html',
238 {'media': media,
239 'form': form})
240 else:
241 raise Forbidden("Subtitles are disabled")
242
243 @require_active_login
244 def legacy_edit_profile(request):
245 """redirect the old /edit/profile/?username=USER to /u/USER/edit/"""
246 username = request.GET.get('username') or request.user.username
247 return redirect(request, 'mediagoblin.edit.profile', user=username)
248
249
250 @require_active_login
251 @active_user_from_url
252 def edit_profile(request, url_user=None):
253 # admins may edit any user profile
254 if request.user.username != url_user.username:
255 if not request.user.has_privilege(u'admin'):
256 raise Forbidden(_("You can only edit your own profile."))
257
258 # No need to warn again if admin just submitted an edited profile
259 if request.method != 'POST':
260 messages.add_message(
261 request,
262 messages.WARNING,
263 _("You are editing a user's profile. Proceed with caution."))
264
265 user = url_user
266
267 # Get the location name
268 if user.location is None:
269 location = ""
270 else:
271 location = user.get_location.name
272
273 form = forms.EditProfileForm(request.form,
274 url=user.url,
275 bio=user.bio,
276 location=location)
277
278 if request.method == 'POST' and form.validate():
279 user.url = six.text_type(form.url.data)
280 user.bio = six.text_type(form.bio.data)
281
282 # Save location
283 if form.location.data and user.location is None:
284 user.get_location = Location(name=six.text_type(form.location.data))
285 elif form.location.data:
286 location = user.get_location
287 location.name = six.text_type(form.location.data)
288 location.save()
289
290 user.save()
291
292 messages.add_message(
293 request,
294 messages.SUCCESS,
295 _("Profile changes saved"))
296 return redirect(request,
297 'mediagoblin.user_pages.user_home',
298 user=user.username)
299
300 return render_to_response(
301 request,
302 'mediagoblin/edit/edit_profile.html',
303 {'user': user,
304 'form': form})
305
306 EMAIL_VERIFICATION_TEMPLATE = (
307 u'{uri}?'
308 u'token={verification_key}')
309
310
311 @require_active_login
312 def edit_account(request):
313 user = request.user
314 form = forms.EditAccountForm(request.form,
315 wants_comment_notification=user.wants_comment_notification,
316 license_preference=user.license_preference,
317 wants_notifications=user.wants_notifications)
318
319 if request.method == 'POST' and form.validate():
320 user.wants_comment_notification = form.wants_comment_notification.data
321 user.wants_notifications = form.wants_notifications.data
322
323 user.license_preference = form.license_preference.data
324
325 user.save()
326 messages.add_message(
327 request,
328 messages.SUCCESS,
329 _("Account settings saved"))
330 return redirect(request,
331 'mediagoblin.user_pages.user_home',
332 user=user.username)
333
334 return render_to_response(
335 request,
336 'mediagoblin/edit/edit_account.html',
337 {'user': user,
338 'form': form})
339
340 @require_active_login
341 def deauthorize_applications(request):
342 """ Deauthroize OAuth applications """
343 if request.method == 'POST' and "application" in request.form:
344 token = request.form["application"]
345 access_token = AccessToken.query.filter_by(token=token).first()
346 if access_token is None:
347 messages.add_message(
348 request,
349 messages.ERROR,
350 _("Unknown application, not able to deauthorize")
351 )
352 else:
353 access_token.delete()
354 messages.add_message(
355 request,
356 messages.SUCCESS,
357 _("Application has been deauthorized")
358 )
359
360 access_tokens = AccessToken.query.filter_by(actor=request.user.id)
361 applications = [(a.get_requesttoken, a) for a in access_tokens]
362
363 return render_to_response(
364 request,
365 'mediagoblin/edit/deauthorize_applications.html',
366 {'applications': applications}
367 )
368
369 @require_active_login
370 def delete_account(request):
371 """Delete a user completely"""
372 user = request.user
373 if request.method == 'POST':
374 if request.form.get(u'confirmed'):
375 # Form submitted and confirmed. Actually delete the user account
376 # Log out user and delete cookies etc.
377 # TODO: Should we be using MG.auth.views.py:logout for this?
378 request.session.delete()
379
380 # Delete user account and all related media files etc....
381 user = User.query.filter(User.id==user.id).first()
382 user.delete()
383
384 # We should send a message that the user has been deleted
385 # successfully. But we just deleted the session, so we
386 # can't...
387 return redirect(request, 'index')
388
389 else: # Did not check the confirmation box...
390 messages.add_message(
391 request,
392 messages.WARNING,
393 _('You need to confirm the deletion of your account.'))
394
395 # No POST submission or not confirmed, just show page
396 return render_to_response(
397 request,
398 'mediagoblin/edit/delete_account.html',
399 {'user': user})
400
401
402 @require_active_login
403 @user_may_alter_collection
404 @get_user_collection
405 def edit_collection(request, collection):
406 defaults = dict(
407 title=collection.title,
408 slug=collection.slug,
409 description=collection.description)
410
411 form = forms.EditCollectionForm(
412 request.form,
413 **defaults)
414
415 if request.method == 'POST' and form.validate():
416 # Make sure there isn't already a Collection with such a slug
417 # and userid.
418 slug_used = check_collection_slug_used(collection.actor,
419 form.slug.data, collection.id)
420
421 # Make sure there isn't already a Collection with this title
422 existing_collection = request.db.Collection.query.filter_by(
423 actor=request.user.id,
424 title=form.title.data).first()
425
426 if existing_collection and existing_collection.id != collection.id:
427 messages.add_message(
428 request,
429 messages.ERROR,
430 _('You already have a collection called "%s"!') %
431 form.title.data)
432 elif slug_used:
433 form.slug.errors.append(
434 _(u'A collection with that slug already exists for this user.'))
435 else:
436 collection.title = six.text_type(form.title.data)
437 collection.description = six.text_type(form.description.data)
438 collection.slug = six.text_type(form.slug.data)
439
440 collection.save()
441
442 return redirect_obj(request, collection)
443
444 if request.user.has_privilege(u'admin') \
445 and collection.actor != request.user.id \
446 and request.method != 'POST':
447 messages.add_message(
448 request,
449 messages.WARNING,
450 _("You are editing another user's collection. "
451 "Proceed with caution."))
452
453 return render_to_response(
454 request,
455 'mediagoblin/edit/edit_collection.html',
456 {'collection': collection,
457 'form': form})
458
459
460 def verify_email(request):
461 """
462 Email verification view for changing email address
463 """
464 # If no token, we can't do anything
465 if not 'token' in request.GET:
466 return render_404(request)
467
468 # Catch error if token is faked or expired
469 token = None
470 try:
471 token = get_timed_signer_url("mail_verification_token") \
472 .loads(request.GET['token'], max_age=10*24*3600)
473 except BadSignature:
474 messages.add_message(
475 request,
476 messages.ERROR,
477 _('The verification key or user id is incorrect.'))
478
479 return redirect(
480 request,
481 'index')
482
483 user = User.query.filter_by(id=int(token['user'])).first()
484
485 if user:
486 user.email = token['email']
487 user.save()
488
489 messages.add_message(
490 request,
491 messages.SUCCESS,
492 _('Your email address has been verified.'))
493
494 else:
495 messages.add_message(
496 request,
497 messages.ERROR,
498 _('The verification key or user id is incorrect.'))
499
500 return redirect(
501 request, 'mediagoblin.user_pages.user_home',
502 user=user.username)
503
504
505 def change_email(request):
506 """ View to change the user's email """
507 form = forms.ChangeEmailForm(request.form)
508 user = request.user
509
510 # If no password authentication, no need to enter a password
511 if 'pass_auth' not in request.template_env.globals or not user.pw_hash:
512 form.__delitem__('password')
513
514 if request.method == 'POST' and form.validate():
515 new_email = form.new_email.data
516 users_with_email = User.query.filter(
517 LocalUser.email==new_email
518 ).count()
519
520 if users_with_email:
521 form.new_email.errors.append(
522 _('Sorry, a user with that email address'
523 ' already exists.'))
524
525 if form.password and user.pw_hash and not check_password(
526 form.password.data, user.pw_hash):
527 form.password.errors.append(
528 _('Wrong password'))
529
530 if not form.errors:
531 verification_key = get_timed_signer_url(
532 'mail_verification_token').dumps({
533 'user': user.id,
534 'email': new_email})
535
536 rendered_email = render_template(
537 request, 'mediagoblin/edit/verification.txt',
538 {'username': user.username,
539 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
540 uri=request.urlgen('mediagoblin.edit.verify_email',
541 qualified=True),
542 verification_key=verification_key)})
543
544 email_debug_message(request)
545 auth_tools.send_verification_email(user, request, new_email,
546 rendered_email)
547
548 return redirect(request, 'mediagoblin.edit.account')
549
550 return render_to_response(
551 request,
552 'mediagoblin/edit/change_email.html',
553 {'form': form,
554 'user': user})
555
556 @user_has_privilege(u'admin')
557 @require_active_login
558 @get_media_entry_by_id
559 def edit_metadata(request, media):
560 form = forms.EditMetaDataForm(request.form)
561 if request.method == "POST" and form.validate():
562 metadata_dict = dict([(row['identifier'],row['value'])
563 for row in form.media_metadata.data])
564 json_ld_metadata = None
565 json_ld_metadata = compact_and_validate(metadata_dict)
566 media.media_metadata = json_ld_metadata
567 media.save()
568 return redirect_obj(request, media)
569
570 if len(form.media_metadata) == 0:
571 for identifier, value in six.iteritems(media.media_metadata):
572 if identifier == "@context": continue
573 form.media_metadata.append_entry({
574 'identifier':identifier,
575 'value':value})
576
577 return render_to_response(
578 request,
579 'mediagoblin/edit/metadata.html',
580 {'form':form,
581 'media':media})
582
583
584 from mediagoblin.tools.subtitles import open_subtitle,save_subtitle
585
586 @require_active_login
587 @get_media_entry_by_id
588 @user_may_delete_media
589 @path_subtitle
590 def custom_subtitles(request,media,path=None):
591 text = open_subtitle(path)
592 form = forms.CustomizeSubtitlesForm(request.form,
593 subtitle=text)
594 if request.method == 'POST' and form.validate():
595 subtitle_data = form.subtitle.data
596 save_subtitle(path,subtitle_data)
597
598 return render_to_response(
599 request,
600 "mediagoblin/edit/custom_subtitles.html",
601 {"path": path,
602 "form": form })