Removed almost all of the code from core
[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,
def53bc3 37 user_not_banned, path_subtitle, user_may_delete_media)
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
d88fcb03 50from mediagoblin.db.models import User, LocalUser, Client, AccessToken, Location
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)
0f3bf8d4 76 slug_used = check_media_slug_used(media.actor, 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') \
e75a45c0 94 and media.actor != request.user.id \
96a2c366
CAW
95 and request.method != 'POST':
96 messages.add_message(
5c7b2a63
AB
97 request,
98 messages.WARNING,
4b1adc13 99 _("You are editing another user's media. Proceed with caution."))
96a2c366 100
9038c9f9
CAW
101 return render_to_response(
102 request,
c9c24934
E
103 'mediagoblin/edit/edit.html',
104 {'media': media,
105 'form': form})
46fd661e 106
3a8c3a38 107
825b1362
JW
108# Mimetypes that browsers parse scripts in.
109# Content-sniffing isn't taken into consideration.
110UNSAFE_MIMETYPES = [
111 'text/html',
112 'text/svg+xml']
113
114
954b407c 115@get_media_entry_by_id
630b57a3 116@require_active_login
3a8c3a38
JW
117def edit_attachments(request, media):
118 if mg_globals.app_config['allow_attachments']:
119 form = forms.EditAttachmentsForm()
120
121 # Add any attachements
c43f8c1d
JW
122 if 'attachment_file' in request.files \
123 and request.files['attachment_file']:
3a8c3a38 124
825b1362
JW
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(
c43f8c1d 136 request.files['attachment_file'].filename)[0] in \
825b1362
JW
137 UNSAFE_MIMETYPES:
138 public_filename = secure_filename('{0}.notsafe'.format(
c43f8c1d 139 request.files['attachment_file'].filename))
825b1362
JW
140 else:
141 public_filename = secure_filename(
c43f8c1d 142 request.files['attachment_file'].filename)
825b1362 143
3a8c3a38
JW
144 attachment_public_filepath \
145 = mg_globals.public_store.get_unique_filepath(
e49b7e02 146 ['media_entries', six.text_type(media.id), 'attachment',
825b1362 147 public_filename])
3a8c3a38
JW
148
149 attachment_public_file = mg_globals.public_store.get_file(
150 attachment_public_filepath, 'wb')
151
152 try:
153 attachment_public_file.write(
c43f8c1d 154 request.files['attachment_file'].stream.read())
3a8c3a38 155 finally:
c43f8c1d 156 request.files['attachment_file'].stream.close()
3a8c3a38 157
35029581 158 media.attachment_files.append(dict(
dc03850b 159 name=form.attachment_name.data \
c43f8c1d 160 or request.files['attachment_file'].filename,
3a8c3a38 161 filepath=attachment_public_filepath,
243c3843 162 created=datetime.utcnow(),
3a8c3a38 163 ))
630b57a3 164
3a8c3a38
JW
165 media.save()
166
167 messages.add_message(
5c7b2a63
AB
168 request,
169 messages.SUCCESS,
170 _("You added the attachment %s!") %
171 (form.attachment_name.data or
172 request.files['attachment_file'].filename))
3a8c3a38 173
950124e6
SS
174 return redirect(request,
175 location=media.url_for_self(request.urlgen))
3a8c3a38
JW
176 return render_to_response(
177 request,
178 'mediagoblin/edit/attachments.html',
179 {'media': media,
180 'form': form})
181 else:
cfa92229 182 raise Forbidden("Attachments are disabled")
3a8c3a38 183
abc4da29
SS
184@require_active_login
185def legacy_edit_profile(request):
186 """redirect the old /edit/profile/?username=USER to /u/USER/edit/"""
187 username = request.GET.get('username') or request.user.username
188 return redirect(request, 'mediagoblin.edit.profile', user=username)
189
3a8c3a38
JW
190
191@require_active_login
abc4da29
SS
192@active_user_from_url
193def edit_profile(request, url_user=None):
194 # admins may edit any user profile
195 if request.user.username != url_user.username:
8394febb 196 if not request.user.has_privilege(u'admin'):
abc4da29
SS
197 raise Forbidden(_("You can only edit your own profile."))
198
a0cf14fe
CFD
199 # No need to warn again if admin just submitted an edited profile
200 if request.method != 'POST':
201 messages.add_message(
5c7b2a63
AB
202 request,
203 messages.WARNING,
4b1adc13 204 _("You are editing a user's profile. Proceed with caution."))
abc4da29
SS
205
206 user = url_user
a0cf14fe 207
c0434db4
JT
208 # Get the location name
209 if user.location is None:
210 location = ""
211 else:
212 location = user.get_location.name
213
111a609d 214 form = forms.EditProfileForm(request.form,
066d49b2 215 url=user.url,
c0434db4
JT
216 bio=user.bio,
217 location=location)
630b57a3 218
219 if request.method == 'POST' and form.validate():
e49b7e02
BP
220 user.url = six.text_type(form.url.data)
221 user.bio = six.text_type(form.bio.data)
4c465852 222
c0434db4
JT
223 # Save location
224 if form.location.data and user.location is None:
896d00fb 225 user.get_location = Location(name=six.text_type(form.location.data))
c0434db4 226 elif form.location.data:
c5f258fe 227 location = user.get_location
896d00fb 228 location.name = six.text_type(form.location.data)
c0434db4
JT
229 location.save()
230
c8071fa5 231 user.save()
630b57a3 232
5c7b2a63
AB
233 messages.add_message(
234 request,
235 messages.SUCCESS,
236 _("Profile changes saved"))
c8071fa5
JS
237 return redirect(request,
238 'mediagoblin.user_pages.user_home',
703d09b9 239 user=user.username)
630b57a3 240
241 return render_to_response(
242 request,
243 'mediagoblin/edit/edit_profile.html',
244 {'user': user,
245 'form': form})
c8071fa5 246
89e1563f
RE
247EMAIL_VERIFICATION_TEMPLATE = (
248 u'{uri}?'
249 u'token={verification_key}')
250
c8071fa5
JS
251
252@require_active_login
253def edit_account(request):
c8071fa5 254 user = request.user
111a609d 255 form = forms.EditAccountForm(request.form,
066d49b2 256 wants_comment_notification=user.wants_comment_notification,
93d805ad
RE
257 license_preference=user.license_preference,
258 wants_notifications=user.wants_notifications)
c8071fa5 259
89e1563f 260 if request.method == 'POST' and form.validate():
f670f48d 261 user.wants_comment_notification = form.wants_comment_notification.data
93d805ad 262 user.wants_notifications = form.wants_notifications.data
dc4dfbde 263
f670f48d 264 user.license_preference = form.license_preference.data
dc4dfbde 265
dd57c6c5 266 user.save()
5c7b2a63
AB
267 messages.add_message(
268 request,
269 messages.SUCCESS,
270 _("Account settings saved"))
dd57c6c5
RE
271 return redirect(request,
272 'mediagoblin.user_pages.user_home',
273 user=user.username)
630b57a3 274
275 return render_to_response(
276 request,
c8071fa5 277 'mediagoblin/edit/edit_account.html',
630b57a3 278 {'user': user,
279 'form': form})
be5be115 280
7e15632b
JT
281@require_active_login
282def deauthorize_applications(request):
283 """ Deauthroize OAuth applications """
284 if request.method == 'POST' and "application" in request.form:
285 token = request.form["application"]
286 access_token = AccessToken.query.filter_by(token=token).first()
287 if access_token is None:
288 messages.add_message(
289 request,
290 messages.ERROR,
291 _("Unknown application, not able to deauthorize")
292 )
293 else:
294 access_token.delete()
295 messages.add_message(
296 request,
297 messages.SUCCESS,
298 _("Application has been deauthorized")
299 )
300
f1db51e4 301 access_tokens = AccessToken.query.filter_by(actor=request.user.id)
7e15632b
JT
302 applications = [(a.get_requesttoken, a) for a in access_tokens]
303
304 return render_to_response(
305 request,
306 'mediagoblin/edit/deauthorize_applications.html',
307 {'applications': applications}
308 )
be5be115 309
380f22b8
SS
310@require_active_login
311def delete_account(request):
312 """Delete a user completely"""
313 user = request.user
314 if request.method == 'POST':
315 if request.form.get(u'confirmed'):
316 # Form submitted and confirmed. Actually delete the user account
317 # Log out user and delete cookies etc.
318 # TODO: Should we be using MG.auth.views.py:logout for this?
319 request.session.delete()
320
321 # Delete user account and all related media files etc....
d7f35f6f
JT
322 user = User.query.filter(User.id==user.id).first()
323 user.delete()
380f22b8
SS
324
325 # We should send a message that the user has been deleted
326 # successfully. But we just deleted the session, so we
327 # can't...
328 return redirect(request, 'index')
329
330 else: # Did not check the confirmation box...
331 messages.add_message(
5c7b2a63
AB
332 request,
333 messages.WARNING,
380f22b8
SS
334 _('You need to confirm the deletion of your account.'))
335
336 # No POST submission or not confirmed, just show page
337 return render_to_response(
338 request,
339 'mediagoblin/edit/delete_account.html',
340 {'user': user})
341
342
be5be115
AW
343@require_active_login
344@user_may_alter_collection
345@get_user_collection
346def edit_collection(request, collection):
347 defaults = dict(
348 title=collection.title,
349 slug=collection.slug,
350 description=collection.description)
351
352 form = forms.EditCollectionForm(
111a609d 353 request.form,
be5be115
AW
354 **defaults)
355
356 if request.method == 'POST' and form.validate():
357 # Make sure there isn't already a Collection with such a slug
358 # and userid.
0f3bf8d4 359 slug_used = check_collection_slug_used(collection.actor,
dc03850b 360 form.slug.data, collection.id)
c43f8c1d 361
be5be115 362 # Make sure there isn't already a Collection with this title
44082b12 363 existing_collection = request.db.Collection.query.filter_by(
0f3bf8d4 364 actor=request.user.id,
44082b12 365 title=form.title.data).first()
c43f8c1d 366
be5be115
AW
367 if existing_collection and existing_collection.id != collection.id:
368 messages.add_message(
5c7b2a63
AB
369 request,
370 messages.ERROR,
371 _('You already have a collection called "%s"!') %
dc03850b 372 form.title.data)
be5be115
AW
373 elif slug_used:
374 form.slug.errors.append(
375 _(u'A collection with that slug already exists for this user.'))
376 else:
e49b7e02
BP
377 collection.title = six.text_type(form.title.data)
378 collection.description = six.text_type(form.description.data)
379 collection.slug = six.text_type(form.slug.data)
be5be115
AW
380
381 collection.save()
382
2e6ee596 383 return redirect_obj(request, collection)
be5be115 384
8394febb 385 if request.user.has_privilege(u'admin') \
0f3bf8d4 386 and collection.actor != request.user.id \
be5be115
AW
387 and request.method != 'POST':
388 messages.add_message(
5c7b2a63
AB
389 request,
390 messages.WARNING,
391 _("You are editing another user's collection. "
392 "Proceed with caution."))
be5be115
AW
393
394 return render_to_response(
395 request,
396 'mediagoblin/edit/edit_collection.html',
397 {'collection': collection,
398 'form': form})
39aa1db4
RE
399
400
89e1563f
RE
401def verify_email(request):
402 """
403 Email verification view for changing email address
404 """
405 # If no token, we can't do anything
406 if not 'token' in request.GET:
407 return render_404(request)
408
377db0e7
RE
409 # Catch error if token is faked or expired
410 token = None
411 try:
412 token = get_timed_signer_url("mail_verification_token") \
413 .loads(request.GET['token'], max_age=10*24*3600)
414 except BadSignature:
415 messages.add_message(
416 request,
417 messages.ERROR,
418 _('The verification key or user id is incorrect.'))
419
420 return redirect(
421 request,
422 'index')
89e1563f
RE
423
424 user = User.query.filter_by(id=int(token['user'])).first()
425
426 if user:
427 user.email = token['email']
428 user.save()
429
430 messages.add_message(
431 request,
432 messages.SUCCESS,
433 _('Your email address has been verified.'))
434
435 else:
436 messages.add_message(
437 request,
438 messages.ERROR,
439 _('The verification key or user id is incorrect.'))
440
441 return redirect(
442 request, 'mediagoblin.user_pages.user_home',
443 user=user.username)
5adb906a
RE
444
445
402f4360
RE
446def change_email(request):
447 """ View to change the user's email """
448 form = forms.ChangeEmailForm(request.form)
449 user = request.user
450
451 # If no password authentication, no need to enter a password
452 if 'pass_auth' not in request.template_env.globals or not user.pw_hash:
453 form.__delitem__('password')
454
455 if request.method == 'POST' and form.validate():
456 new_email = form.new_email.data
d88fcb03
JT
457 users_with_email = User.query.filter(
458 LocalUser.email==new_email
459 ).count()
402f4360
RE
460
461 if users_with_email:
462 form.new_email.errors.append(
463 _('Sorry, a user with that email address'
464 ' already exists.'))
465
201ac388 466 if form.password and user.pw_hash and not check_password(
402f4360
RE
467 form.password.data, user.pw_hash):
468 form.password.errors.append(
469 _('Wrong password'))
470
471 if not form.errors:
472 verification_key = get_timed_signer_url(
473 'mail_verification_token').dumps({
474 'user': user.id,
475 'email': new_email})
476
477 rendered_email = render_template(
478 request, 'mediagoblin/edit/verification.txt',
479 {'username': user.username,
480 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
481 uri=request.urlgen('mediagoblin.edit.verify_email',
482 qualified=True),
483 verification_key=verification_key)})
484
485 email_debug_message(request)
486 auth_tools.send_verification_email(user, request, new_email,
487 rendered_email)
488
489 return redirect(request, 'mediagoblin.edit.account')
490
491 return render_to_response(
492 request,
493 'mediagoblin/edit/change_email.html',
494 {'form': form,
495 'user': user})
fffc5dcf 496
497@user_has_privilege(u'admin')
498@require_active_login
499@get_media_entry_by_id
500def edit_metadata(request, media):
9919fb08 501 form = forms.EditMetaDataForm(request.form)
502 if request.method == "POST" and form.validate():
9919fb08 503 metadata_dict = dict([(row['identifier'],row['value'])
504 for row in form.media_metadata.data])
0d6550fb 505 json_ld_metadata = None
6b6b1b07 506 json_ld_metadata = compact_and_validate(metadata_dict)
507 media.media_metadata = json_ld_metadata
508 media.save()
c0434db4 509 return redirect_obj(request, media)
9919fb08 510
1688abbf 511 if len(form.media_metadata) == 0:
13f37e75 512 for identifier, value in six.iteritems(media.media_metadata):
6b6b1b07 513 if identifier == "@context": continue
e80596c8 514 form.media_metadata.append_entry({
515 'identifier':identifier,
516 'value':value})
0d6550fb 517
fffc5dcf 518 return render_to_response(
519 request,
520 'mediagoblin/edit/metadata.html',
e80596c8 521 {'form':form,
6a3fe50e 522 'media':media})