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