123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512 |
- # GNU MediaGoblin -- federated, autonomous media hosting
- # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU Affero General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU Affero General Public License for more details.
- #
- # You should have received a copy of the GNU Affero General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- import six
- from datetime import datetime
- from itsdangerous import BadSignature
- from pyld import jsonld
- from werkzeug.exceptions import Forbidden
- from werkzeug.utils import secure_filename
- from jsonschema import ValidationError, Draft4Validator
- from mediagoblin import messages
- from mediagoblin import mg_globals
- from mediagoblin.auth import (check_password,
- tools as auth_tools)
- from mediagoblin.edit import forms
- from mediagoblin.edit.lib import may_edit_media
- from mediagoblin.decorators import (require_active_login, active_user_from_url,
- get_media_entry_by_id, user_may_alter_collection,
- get_user_collection, user_has_privilege,
- user_not_banned)
- from mediagoblin.tools.crypto import get_timed_signer_url
- from mediagoblin.tools.metadata import (compact_and_validate, DEFAULT_CHECKER,
- DEFAULT_SCHEMA)
- from mediagoblin.tools.mail import email_debug_message
- from mediagoblin.tools.response import (render_to_response,
- redirect, redirect_obj, render_404)
- from mediagoblin.tools.translate import pass_to_ugettext as _
- from mediagoblin.tools.template import render_template
- from mediagoblin.tools.text import (
- convert_to_tag_list_of_dicts, media_tags_as_string)
- from mediagoblin.tools.url import slugify
- from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used
- from mediagoblin.db.models import User, Client, AccessToken, Location
- import mimetypes
- @get_media_entry_by_id
- @require_active_login
- def edit_media(request, media):
- if not may_edit_media(request, media):
- raise Forbidden("User may not edit this media")
- defaults = dict(
- title=media.title,
- slug=media.slug,
- description=media.description,
- tags=media_tags_as_string(media.tags),
- license=media.license)
- form = forms.EditForm(
- request.form,
- **defaults)
- if request.method == 'POST' and form.validate():
- # Make sure there isn't already a MediaEntry with such a slug
- # and userid.
- slug = slugify(form.slug.data)
- slug_used = check_media_slug_used(media.uploader, slug, media.id)
- if slug_used:
- form.slug.errors.append(
- _(u'An entry with that slug already exists for this user.'))
- else:
- media.title = form.title.data
- media.description = form.description.data
- media.tags = convert_to_tag_list_of_dicts(
- form.tags.data)
- media.license = six.text_type(form.license.data) or None
- media.slug = slug
- media.save()
- return redirect_obj(request, media)
- if request.user.has_privilege(u'admin') \
- and media.uploader != request.user.id \
- and request.method != 'POST':
- messages.add_message(
- request, messages.WARNING,
- _("You are editing another user's media. Proceed with caution."))
- return render_to_response(
- request,
- 'mediagoblin/edit/edit.html',
- {'media': media,
- 'form': form})
- # Mimetypes that browsers parse scripts in.
- # Content-sniffing isn't taken into consideration.
- UNSAFE_MIMETYPES = [
- 'text/html',
- 'text/svg+xml']
- @get_media_entry_by_id
- @require_active_login
- def edit_attachments(request, media):
- if mg_globals.app_config['allow_attachments']:
- form = forms.EditAttachmentsForm()
- # Add any attachements
- if 'attachment_file' in request.files \
- and request.files['attachment_file']:
- # Security measure to prevent attachments from being served as
- # text/html, which will be parsed by web clients and pose an XSS
- # threat.
- #
- # TODO
- # This method isn't flawless as some browsers may perform
- # content-sniffing.
- # This method isn't flawless as we do the mimetype lookup on the
- # machine parsing the upload form, and not necessarily the machine
- # serving the attachments.
- if mimetypes.guess_type(
- request.files['attachment_file'].filename)[0] in \
- UNSAFE_MIMETYPES:
- public_filename = secure_filename('{0}.notsafe'.format(
- request.files['attachment_file'].filename))
- else:
- public_filename = secure_filename(
- request.files['attachment_file'].filename)
- attachment_public_filepath \
- = mg_globals.public_store.get_unique_filepath(
- ['media_entries', six.text_type(media.id), 'attachment',
- public_filename])
- attachment_public_file = mg_globals.public_store.get_file(
- attachment_public_filepath, 'wb')
- try:
- attachment_public_file.write(
- request.files['attachment_file'].stream.read())
- finally:
- request.files['attachment_file'].stream.close()
- media.attachment_files.append(dict(
- name=form.attachment_name.data \
- or request.files['attachment_file'].filename,
- filepath=attachment_public_filepath,
- created=datetime.utcnow(),
- ))
- media.save()
- messages.add_message(
- request, messages.SUCCESS,
- _("You added the attachment %s!") \
- % (form.attachment_name.data
- or request.files['attachment_file'].filename))
- return redirect(request,
- location=media.url_for_self(request.urlgen))
- return render_to_response(
- request,
- 'mediagoblin/edit/attachments.html',
- {'media': media,
- 'form': form})
- else:
- raise Forbidden("Attachments are disabled")
- @require_active_login
- def legacy_edit_profile(request):
- """redirect the old /edit/profile/?username=USER to /u/USER/edit/"""
- username = request.GET.get('username') or request.user.username
- return redirect(request, 'mediagoblin.edit.profile', user=username)
- @require_active_login
- @active_user_from_url
- def edit_profile(request, url_user=None):
- # admins may edit any user profile
- if request.user.username != url_user.username:
- if not request.user.has_privilege(u'admin'):
- raise Forbidden(_("You can only edit your own profile."))
- # No need to warn again if admin just submitted an edited profile
- if request.method != 'POST':
- messages.add_message(
- request, messages.WARNING,
- _("You are editing a user's profile. Proceed with caution."))
- user = url_user
- # Get the location name
- if user.location is None:
- location = ""
- else:
- location = user.get_location.name
- form = forms.EditProfileForm(request.form,
- url=user.url,
- bio=user.bio,
- location=location)
- if request.method == 'POST' and form.validate():
- user.url = six.text_type(form.url.data)
- user.bio = six.text_type(form.bio.data)
- # Save location
- if form.location.data and user.location is None:
- user.get_location = Location(name=six.text_type(form.location.data))
- elif form.location.data:
- location = user.get_location
- location.name = six.text_type(form.location.data)
- location.save()
- user.save()
- messages.add_message(request,
- messages.SUCCESS,
- _("Profile changes saved"))
- return redirect(request,
- 'mediagoblin.user_pages.user_home',
- user=user.username)
- return render_to_response(
- request,
- 'mediagoblin/edit/edit_profile.html',
- {'user': user,
- 'form': form})
- EMAIL_VERIFICATION_TEMPLATE = (
- u'{uri}?'
- u'token={verification_key}')
- @require_active_login
- def edit_account(request):
- user = request.user
- form = forms.EditAccountForm(request.form,
- wants_comment_notification=user.wants_comment_notification,
- license_preference=user.license_preference,
- wants_notifications=user.wants_notifications)
- if request.method == 'POST' and form.validate():
- user.wants_comment_notification = form.wants_comment_notification.data
- user.wants_notifications = form.wants_notifications.data
- user.license_preference = form.license_preference.data
- user.save()
- messages.add_message(request,
- messages.SUCCESS,
- _("Account settings saved"))
- return redirect(request,
- 'mediagoblin.user_pages.user_home',
- user=user.username)
- return render_to_response(
- request,
- 'mediagoblin/edit/edit_account.html',
- {'user': user,
- 'form': form})
- @require_active_login
- def deauthorize_applications(request):
- """ Deauthroize OAuth applications """
- if request.method == 'POST' and "application" in request.form:
- token = request.form["application"]
- access_token = AccessToken.query.filter_by(token=token).first()
- if access_token is None:
- messages.add_message(
- request,
- messages.ERROR,
- _("Unknown application, not able to deauthorize")
- )
- else:
- access_token.delete()
- messages.add_message(
- request,
- messages.SUCCESS,
- _("Application has been deauthorized")
- )
- access_tokens = AccessToken.query.filter_by(user=request.user.id)
- applications = [(a.get_requesttoken, a) for a in access_tokens]
- return render_to_response(
- request,
- 'mediagoblin/edit/deauthorize_applications.html',
- {'applications': applications}
- )
- @require_active_login
- def delete_account(request):
- """Delete a user completely"""
- user = request.user
- if request.method == 'POST':
- if request.form.get(u'confirmed'):
- # Form submitted and confirmed. Actually delete the user account
- # Log out user and delete cookies etc.
- # TODO: Should we be using MG.auth.views.py:logout for this?
- request.session.delete()
- # Delete user account and all related media files etc....
- request.user.delete()
- # We should send a message that the user has been deleted
- # successfully. But we just deleted the session, so we
- # can't...
- return redirect(request, 'index')
- else: # Did not check the confirmation box...
- messages.add_message(
- request, messages.WARNING,
- _('You need to confirm the deletion of your account.'))
- # No POST submission or not confirmed, just show page
- return render_to_response(
- request,
- 'mediagoblin/edit/delete_account.html',
- {'user': user})
- @require_active_login
- @user_may_alter_collection
- @get_user_collection
- def edit_collection(request, collection):
- defaults = dict(
- title=collection.title,
- slug=collection.slug,
- description=collection.description)
- form = forms.EditCollectionForm(
- request.form,
- **defaults)
- if request.method == 'POST' and form.validate():
- # Make sure there isn't already a Collection with such a slug
- # and userid.
- slug_used = check_collection_slug_used(collection.creator,
- form.slug.data, collection.id)
- # Make sure there isn't already a Collection with this title
- existing_collection = request.db.Collection.query.filter_by(
- creator=request.user.id,
- title=form.title.data).first()
- if existing_collection and existing_collection.id != collection.id:
- messages.add_message(
- request, messages.ERROR,
- _('You already have a collection called "%s"!') % \
- form.title.data)
- elif slug_used:
- form.slug.errors.append(
- _(u'A collection with that slug already exists for this user.'))
- else:
- collection.title = six.text_type(form.title.data)
- collection.description = six.text_type(form.description.data)
- collection.slug = six.text_type(form.slug.data)
- collection.save()
- return redirect_obj(request, collection)
- if request.user.has_privilege(u'admin') \
- and collection.creator != request.user.id \
- and request.method != 'POST':
- messages.add_message(
- request, messages.WARNING,
- _("You are editing another user's collection. Proceed with caution."))
- return render_to_response(
- request,
- 'mediagoblin/edit/edit_collection.html',
- {'collection': collection,
- 'form': form})
- def verify_email(request):
- """
- Email verification view for changing email address
- """
- # If no token, we can't do anything
- if not 'token' in request.GET:
- return render_404(request)
- # Catch error if token is faked or expired
- token = None
- try:
- token = get_timed_signer_url("mail_verification_token") \
- .loads(request.GET['token'], max_age=10*24*3600)
- except BadSignature:
- messages.add_message(
- request,
- messages.ERROR,
- _('The verification key or user id is incorrect.'))
- return redirect(
- request,
- 'index')
- user = User.query.filter_by(id=int(token['user'])).first()
- if user:
- user.email = token['email']
- user.save()
- messages.add_message(
- request,
- messages.SUCCESS,
- _('Your email address has been verified.'))
- else:
- messages.add_message(
- request,
- messages.ERROR,
- _('The verification key or user id is incorrect.'))
- return redirect(
- request, 'mediagoblin.user_pages.user_home',
- user=user.username)
- def change_email(request):
- """ View to change the user's email """
- form = forms.ChangeEmailForm(request.form)
- user = request.user
- # If no password authentication, no need to enter a password
- if 'pass_auth' not in request.template_env.globals or not user.pw_hash:
- form.__delitem__('password')
- if request.method == 'POST' and form.validate():
- new_email = form.new_email.data
- users_with_email = User.query.filter_by(
- email=new_email).count()
- if users_with_email:
- form.new_email.errors.append(
- _('Sorry, a user with that email address'
- ' already exists.'))
- if form.password and user.pw_hash and not check_password(
- form.password.data, user.pw_hash):
- form.password.errors.append(
- _('Wrong password'))
- if not form.errors:
- verification_key = get_timed_signer_url(
- 'mail_verification_token').dumps({
- 'user': user.id,
- 'email': new_email})
- rendered_email = render_template(
- request, 'mediagoblin/edit/verification.txt',
- {'username': user.username,
- 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
- uri=request.urlgen('mediagoblin.edit.verify_email',
- qualified=True),
- verification_key=verification_key)})
- email_debug_message(request)
- auth_tools.send_verification_email(user, request, new_email,
- rendered_email)
- return redirect(request, 'mediagoblin.edit.account')
- return render_to_response(
- request,
- 'mediagoblin/edit/change_email.html',
- {'form': form,
- 'user': user})
- @user_has_privilege(u'admin')
- @require_active_login
- @get_media_entry_by_id
- def edit_metadata(request, media):
- form = forms.EditMetaDataForm(request.form)
- if request.method == "POST" and form.validate():
- metadata_dict = dict([(row['identifier'],row['value'])
- for row in form.media_metadata.data])
- json_ld_metadata = None
- json_ld_metadata = compact_and_validate(metadata_dict)
- media.media_metadata = json_ld_metadata
- media.save()
- return redirect_obj(request, media)
- if len(form.media_metadata) == 0:
- for identifier, value in six.iteritems(media.media_metadata):
- if identifier == "@context": continue
- form.media_metadata.append_entry({
- 'identifier':identifier,
- 'value':value})
- return render_to_response(
- request,
- 'mediagoblin/edit/metadata.html',
- {'form':form,
- 'media':media})
|