Replace raw_input with six.moves.input
[mediagoblin.git] / mediagoblin / edit / views.py
index 9db1c3f9d5cdd5c9f771f22ce5ae68338e312103..00c611573dcf020d41b3e243961aa2385f2af822 100644 (file)
 # 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 import auth
+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)
-from mediagoblin.tools.response import render_to_response, \
-    redirect, redirect_obj
+                            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, LocalUser, Client, AccessToken, Location
 
 import mimetypes
 
@@ -60,7 +73,7 @@ def edit_media(request, media):
         # 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)
+        slug_used = check_media_slug_used(media.actor, slug, media.id)
 
         if slug_used:
             form.slug.errors.append(
@@ -71,14 +84,14 @@ def edit_media(request, media):
             media.tags = convert_to_tag_list_of_dicts(
                                    form.tags.data)
 
-            media.license = unicode(form.license.data) or None
+            media.license = six.text_type(form.license.data) or None
             media.slug = slug
             media.save()
 
             return redirect_obj(request, media)
 
-    if request.user.is_admin \
-            and media.uploader != request.user.id \
+    if request.user.has_privilege(u'admin') \
+            and media.actor != request.user.id \
             and request.method != 'POST':
         messages.add_message(
             request, messages.WARNING,
@@ -129,7 +142,7 @@ def edit_attachments(request, media):
 
             attachment_public_filepath \
                 = mg_globals.public_store.get_unique_filepath(
-                ['media_entries', unicode(media.id), 'attachment',
+                ['media_entries', six.text_type(media.id), 'attachment',
                  public_filename])
 
             attachment_public_file = mg_globals.public_store.get_file(
@@ -178,7 +191,7 @@ def legacy_edit_profile(request):
 def edit_profile(request, url_user=None):
     # admins may edit any user profile
     if request.user.username != url_user.username:
-        if not request.user.is_admin:
+        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
@@ -189,13 +202,28 @@ def edit_profile(request, url_user=None):
 
     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)
+        bio=user.bio,
+        location=location)
 
     if request.method == 'POST' and form.validate():
-        user.url = unicode(form.url.data)
-        user.bio = unicode(form.bio.data)
+        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()
 
@@ -212,35 +240,32 @@ def edit_profile(request, url_user=None):
         {'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)
-
-    if request.method == 'POST':
-        form_validated = form.validate()
+        license_preference=user.license_preference,
+        wants_notifications=user.wants_notifications)
 
-        if form_validated and \
-                form.wants_comment_notification.validate(form):
-            user.wants_comment_notification = \
-                form.wants_comment_notification.data
+    if request.method == 'POST' and form.validate():
+        user.wants_comment_notification = form.wants_comment_notification.data
+        user.wants_notifications = form.wants_notifications.data
 
-        if form_validated and \
-                form.license_preference.validate(form):
-            user.license_preference = \
-                form.license_preference.data
+        user.license_preference = form.license_preference.data
 
-        if form_validated and not form.errors:
-            user.save()
-            messages.add_message(request,
-                messages.SUCCESS,
-                _("Account settings saved"))
-            return redirect(request,
-                'mediagoblin.user_pages.user_home',
-                user=user.username)
+        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,
@@ -248,6 +273,34 @@ def edit_account(request):
         {'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(actor=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):
@@ -261,7 +314,8 @@ def delete_account(request):
             request.session.delete()
 
             # Delete user account and all related media files etc....
-            request.user.delete()
+            user = User.query.filter(User.id==user.id).first()
+            user.delete()
 
             # We should send a message that the user has been deleted
             # successfully. But we just deleted the session, so we
@@ -296,13 +350,13 @@ def edit_collection(request, collection):
     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,
+        slug_used = check_collection_slug_used(collection.actor,
                 form.slug.data, collection.id)
 
         # Make sure there isn't already a Collection with this title
-        existing_collection = request.db.Collection.find_one({
-                'creator': request.user.id,
-                'title':form.title.data})
+        existing_collection = request.db.Collection.query.filter_by(
+                actor=request.user.id,
+                title=form.title.data).first()
 
         if existing_collection and existing_collection.id != collection.id:
             messages.add_message(
@@ -313,16 +367,16 @@ def edit_collection(request, collection):
             form.slug.errors.append(
                 _(u'A collection with that slug already exists for this user.'))
         else:
-            collection.title = unicode(form.title.data)
-            collection.description = unicode(form.description.data)
-            collection.slug = unicode(form.slug.data)
+            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.is_admin \
-            and collection.creator != request.user.id \
+    if request.user.has_privilege(u'admin') \
+            and collection.actor != request.user.id \
             and request.method != 'POST':
         messages.add_message(
             request, messages.WARNING,
@@ -335,37 +389,125 @@ def edit_collection(request, collection):
          'form': form})
 
 
-@require_active_login
-def change_pass(request):
-    form = forms.ChangePassForm(request.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 request.method == 'POST' and form.validate():
+    # 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 not auth_lib.bcrypt_check_password(
-                form.old_password.data, user.pw_hash):
-            form.old_password.errors.append(
+    if request.method == 'POST' and form.validate():
+        new_email = form.new_email.data
+        users_with_email = User.query.filter(
+            LocalUser.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'))
 
-            return render_to_response(
-                request,
-                'mediagoblin/edit/change_pass.html',
-                {'form': form,
-                 'user': user})
+        if not form.errors:
+            verification_key = get_timed_signer_url(
+                'mail_verification_token').dumps({
+                    'user': user.id,
+                    'email': new_email})
 
-        # Password matches
-        user.pw_hash = auth_lib.bcrypt_gen_password_hash(
-            form.new_password.data)
-        user.save()
+            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)})
 
-        messages.add_message(
-            request, messages.SUCCESS,
-            _('Your password was changed successfully'))
+            email_debug_message(request)
+            auth_tools.send_verification_email(user, request, new_email,
+                                            rendered_email)
 
-        return redirect(request, 'mediagoblin.edit.account')
+            return redirect(request, 'mediagoblin.edit.account')
 
     return render_to_response(
         request,
-        'mediagoblin/edit/change_pass.html',
+        '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})