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