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