Replace raw_input with six.moves.input
[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
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(
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
c0434db4
JT
205 # Get the location name
206 if user.location is None:
207 location = ""
208 else:
209 location = user.get_location.name
210
111a609d 211 form = forms.EditProfileForm(request.form,
066d49b2 212 url=user.url,
c0434db4
JT
213 bio=user.bio,
214 location=location)
630b57a3 215
216 if request.method == 'POST' and form.validate():
e49b7e02
BP
217 user.url = six.text_type(form.url.data)
218 user.bio = six.text_type(form.bio.data)
4c465852 219
c0434db4
JT
220 # Save location
221 if form.location.data and user.location is None:
896d00fb 222 user.get_location = Location(name=six.text_type(form.location.data))
c0434db4 223 elif form.location.data:
c5f258fe 224 location = user.get_location
896d00fb 225 location.name = six.text_type(form.location.data)
c0434db4
JT
226 location.save()
227
c8071fa5 228 user.save()
630b57a3 229
c8071fa5
JS
230 messages.add_message(request,
231 messages.SUCCESS,
232 _("Profile changes saved"))
233 return redirect(request,
234 'mediagoblin.user_pages.user_home',
703d09b9 235 user=user.username)
630b57a3 236
237 return render_to_response(
238 request,
239 'mediagoblin/edit/edit_profile.html',
240 {'user': user,
241 'form': form})
c8071fa5 242
89e1563f
RE
243EMAIL_VERIFICATION_TEMPLATE = (
244 u'{uri}?'
245 u'token={verification_key}')
246
c8071fa5
JS
247
248@require_active_login
249def edit_account(request):
c8071fa5 250 user = request.user
111a609d 251 form = forms.EditAccountForm(request.form,
066d49b2 252 wants_comment_notification=user.wants_comment_notification,
93d805ad
RE
253 license_preference=user.license_preference,
254 wants_notifications=user.wants_notifications)
c8071fa5 255
89e1563f 256 if request.method == 'POST' and form.validate():
f670f48d 257 user.wants_comment_notification = form.wants_comment_notification.data
93d805ad 258 user.wants_notifications = form.wants_notifications.data
dc4dfbde 259
f670f48d 260 user.license_preference = form.license_preference.data
dc4dfbde 261
dd57c6c5
RE
262 user.save()
263 messages.add_message(request,
264 messages.SUCCESS,
265 _("Account settings saved"))
266 return redirect(request,
267 'mediagoblin.user_pages.user_home',
268 user=user.username)
630b57a3 269
270 return render_to_response(
271 request,
c8071fa5 272 'mediagoblin/edit/edit_account.html',
630b57a3 273 {'user': user,
274 'form': form})
be5be115 275
7e15632b
JT
276@require_active_login
277def deauthorize_applications(request):
278 """ Deauthroize OAuth applications """
279 if request.method == 'POST' and "application" in request.form:
280 token = request.form["application"]
281 access_token = AccessToken.query.filter_by(token=token).first()
282 if access_token is None:
283 messages.add_message(
284 request,
285 messages.ERROR,
286 _("Unknown application, not able to deauthorize")
287 )
288 else:
289 access_token.delete()
290 messages.add_message(
291 request,
292 messages.SUCCESS,
293 _("Application has been deauthorized")
294 )
295
f1db51e4 296 access_tokens = AccessToken.query.filter_by(actor=request.user.id)
7e15632b
JT
297 applications = [(a.get_requesttoken, a) for a in access_tokens]
298
299 return render_to_response(
300 request,
301 'mediagoblin/edit/deauthorize_applications.html',
302 {'applications': applications}
303 )
be5be115 304
380f22b8
SS
305@require_active_login
306def delete_account(request):
307 """Delete a user completely"""
308 user = request.user
309 if request.method == 'POST':
310 if request.form.get(u'confirmed'):
311 # Form submitted and confirmed. Actually delete the user account
312 # Log out user and delete cookies etc.
313 # TODO: Should we be using MG.auth.views.py:logout for this?
314 request.session.delete()
315
316 # Delete user account and all related media files etc....
d7f35f6f
JT
317 user = User.query.filter(User.id==user.id).first()
318 user.delete()
380f22b8
SS
319
320 # We should send a message that the user has been deleted
321 # successfully. But we just deleted the session, so we
322 # can't...
323 return redirect(request, 'index')
324
325 else: # Did not check the confirmation box...
326 messages.add_message(
327 request, messages.WARNING,
328 _('You need to confirm the deletion of your account.'))
329
330 # No POST submission or not confirmed, just show page
331 return render_to_response(
332 request,
333 'mediagoblin/edit/delete_account.html',
334 {'user': user})
335
336
be5be115
AW
337@require_active_login
338@user_may_alter_collection
339@get_user_collection
340def edit_collection(request, collection):
341 defaults = dict(
342 title=collection.title,
343 slug=collection.slug,
344 description=collection.description)
345
346 form = forms.EditCollectionForm(
111a609d 347 request.form,
be5be115
AW
348 **defaults)
349
350 if request.method == 'POST' and form.validate():
351 # Make sure there isn't already a Collection with such a slug
352 # and userid.
0f3bf8d4 353 slug_used = check_collection_slug_used(collection.actor,
dc03850b 354 form.slug.data, collection.id)
c43f8c1d 355
be5be115 356 # Make sure there isn't already a Collection with this title
44082b12 357 existing_collection = request.db.Collection.query.filter_by(
0f3bf8d4 358 actor=request.user.id,
44082b12 359 title=form.title.data).first()
c43f8c1d 360
be5be115
AW
361 if existing_collection and existing_collection.id != collection.id:
362 messages.add_message(
a6481028
CAW
363 request, messages.ERROR,
364 _('You already have a collection called "%s"!') % \
dc03850b 365 form.title.data)
be5be115
AW
366 elif slug_used:
367 form.slug.errors.append(
368 _(u'A collection with that slug already exists for this user.'))
369 else:
e49b7e02
BP
370 collection.title = six.text_type(form.title.data)
371 collection.description = six.text_type(form.description.data)
372 collection.slug = six.text_type(form.slug.data)
be5be115
AW
373
374 collection.save()
375
2e6ee596 376 return redirect_obj(request, collection)
be5be115 377
8394febb 378 if request.user.has_privilege(u'admin') \
0f3bf8d4 379 and collection.actor != request.user.id \
be5be115
AW
380 and request.method != 'POST':
381 messages.add_message(
382 request, messages.WARNING,
383 _("You are editing another user's collection. Proceed with caution."))
384
385 return render_to_response(
386 request,
387 'mediagoblin/edit/edit_collection.html',
388 {'collection': collection,
389 'form': form})
39aa1db4
RE
390
391
89e1563f
RE
392def verify_email(request):
393 """
394 Email verification view for changing email address
395 """
396 # If no token, we can't do anything
397 if not 'token' in request.GET:
398 return render_404(request)
399
377db0e7
RE
400 # Catch error if token is faked or expired
401 token = None
402 try:
403 token = get_timed_signer_url("mail_verification_token") \
404 .loads(request.GET['token'], max_age=10*24*3600)
405 except BadSignature:
406 messages.add_message(
407 request,
408 messages.ERROR,
409 _('The verification key or user id is incorrect.'))
410
411 return redirect(
412 request,
413 'index')
89e1563f
RE
414
415 user = User.query.filter_by(id=int(token['user'])).first()
416
417 if user:
418 user.email = token['email']
419 user.save()
420
421 messages.add_message(
422 request,
423 messages.SUCCESS,
424 _('Your email address has been verified.'))
425
426 else:
427 messages.add_message(
428 request,
429 messages.ERROR,
430 _('The verification key or user id is incorrect.'))
431
432 return redirect(
433 request, 'mediagoblin.user_pages.user_home',
434 user=user.username)
5adb906a
RE
435
436
402f4360
RE
437def change_email(request):
438 """ View to change the user's email """
439 form = forms.ChangeEmailForm(request.form)
440 user = request.user
441
442 # If no password authentication, no need to enter a password
443 if 'pass_auth' not in request.template_env.globals or not user.pw_hash:
444 form.__delitem__('password')
445
446 if request.method == 'POST' and form.validate():
447 new_email = form.new_email.data
d88fcb03
JT
448 users_with_email = User.query.filter(
449 LocalUser.email==new_email
450 ).count()
402f4360
RE
451
452 if users_with_email:
453 form.new_email.errors.append(
454 _('Sorry, a user with that email address'
455 ' already exists.'))
456
201ac388 457 if form.password and user.pw_hash and not check_password(
402f4360
RE
458 form.password.data, user.pw_hash):
459 form.password.errors.append(
460 _('Wrong password'))
461
462 if not form.errors:
463 verification_key = get_timed_signer_url(
464 'mail_verification_token').dumps({
465 'user': user.id,
466 'email': new_email})
467
468 rendered_email = render_template(
469 request, 'mediagoblin/edit/verification.txt',
470 {'username': user.username,
471 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
472 uri=request.urlgen('mediagoblin.edit.verify_email',
473 qualified=True),
474 verification_key=verification_key)})
475
476 email_debug_message(request)
477 auth_tools.send_verification_email(user, request, new_email,
478 rendered_email)
479
480 return redirect(request, 'mediagoblin.edit.account')
481
482 return render_to_response(
483 request,
484 'mediagoblin/edit/change_email.html',
485 {'form': form,
486 'user': user})
fffc5dcf 487
488@user_has_privilege(u'admin')
489@require_active_login
490@get_media_entry_by_id
491def edit_metadata(request, media):
9919fb08 492 form = forms.EditMetaDataForm(request.form)
493 if request.method == "POST" and form.validate():
9919fb08 494 metadata_dict = dict([(row['identifier'],row['value'])
495 for row in form.media_metadata.data])
0d6550fb 496 json_ld_metadata = None
6b6b1b07 497 json_ld_metadata = compact_and_validate(metadata_dict)
498 media.media_metadata = json_ld_metadata
499 media.save()
c0434db4 500 return redirect_obj(request, media)
9919fb08 501
1688abbf 502 if len(form.media_metadata) == 0:
13f37e75 503 for identifier, value in six.iteritems(media.media_metadata):
6b6b1b07 504 if identifier == "@context": continue
e80596c8 505 form.media_metadata.append_entry({
506 'identifier':identifier,
507 'value':value})
0d6550fb 508
fffc5dcf 509 return render_to_response(
510 request,
511 'mediagoblin/edit/metadata.html',
e80596c8 512 {'form':form,
513 'media':media})