diff options
Diffstat (limited to 'mediagoblin/plugins')
27 files changed, 1077 insertions, 113 deletions
diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index b7e74799..69fae7bb 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -17,17 +17,19 @@ import json import logging -from os.path import splitext -from werkzeug.exceptions import BadRequest, Forbidden +from werkzeug.exceptions import BadRequest from werkzeug.wrappers import Response +from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.response import json_response from mediagoblin.decorators import require_active_login from mediagoblin.meddleware.csrf import csrf_exempt -from mediagoblin.media_types import sniff_media +from mediagoblin.media_types import \ + InvalidFileType, FileTypeNotSupported from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable -from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ - run_process_media, new_upload_entry +from mediagoblin.submit.lib import \ + check_file_field, submit_media, get_upload_file_limits, \ + FileUploadLimit, UserUploadLimit, UserPastUploadLimit _log = logging.getLogger(__name__) @@ -49,45 +51,46 @@ def post_entry(request): _log.debug('File field not found') raise BadRequest() - media_file = request.files['file'] - - media_type, media_manager = sniff_media(media_file) - - entry = new_upload_entry(request.user) - entry.media_type = unicode(media_type) - entry.title = unicode(request.form.get('title') - or splitext(media_file.filename)[0]) - - entry.description = unicode(request.form.get('description')) - entry.license = unicode(request.form.get('license', '')) - - entry.generate_slug() - - # queue appropriately - queue_file = prepare_queue_task(request.app, entry, media_file.filename) - - with queue_file: - queue_file.write(request.files['file'].stream.read()) - - # Save now so we have this data before kicking off processing - entry.save() - - if request.form.get('callback_url'): - metadata = request.db.ProcessingMetaData() - metadata.media_entry = entry - metadata.callback_url = unicode(request.form['callback_url']) - metadata.save() - - # Pass off to processing - # - # (... don't change entry after this point to avoid race - # conditions with changes to the document via processing code) - feed_url = request.urlgen( - 'mediagoblin.user_pages.atom_feed', - qualified=True, user=request.user.username) - run_process_media(entry, feed_url) - - return json_response(get_entry_serializable(entry, request.urlgen)) + upload_limit, max_file_size = get_upload_file_limits(request.user) + + callback_url = request.form.get('callback_url') + if callback_url: + callback_url = unicode(callback_url) + try: + entry = submit_media( + mg_app=request.app, user=request.user, + submitted_file=request.files['file'], + filename=request.files['file'].filename, + title=unicode(request.form.get('title')), + description=unicode(request.form.get('description')), + license=unicode(request.form.get('license', '')), + upload_limit=upload_limit, max_file_size=max_file_size, + callback_url=callback_url) + + return json_response(get_entry_serializable(entry, request.urlgen)) + + # Handle upload limit issues + except FileUploadLimit: + raise BadRequest( + _(u'Sorry, the file size is too big.')) + except UserUploadLimit: + raise BadRequest( + _('Sorry, uploading this file will put you over your' + ' upload limit.')) + except UserPastUploadLimit: + raise BadRequest( + _('Sorry, you have reached your upload limit.')) + + except Exception as e: + ''' + This section is intended to catch exceptions raised in + mediagoblin.media_types + ''' + if isinstance(e, InvalidFileType) or \ + isinstance(e, FileTypeNotSupported): + raise BadRequest(unicode(e)) + else: + raise @api_auth diff --git a/mediagoblin/plugins/basic_auth/__init__.py b/mediagoblin/plugins/basic_auth/__init__.py index 33a554b0..64564c7f 100644 --- a/mediagoblin/plugins/basic_auth/__init__.py +++ b/mediagoblin/plugins/basic_auth/__init__.py @@ -13,17 +13,45 @@ # # 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/>. +from pkg_resources import resource_filename +import os + from mediagoblin.plugins.basic_auth import forms as auth_forms from mediagoblin.plugins.basic_auth import tools as auth_tools from mediagoblin.auth.tools import create_basic_user from mediagoblin.db.models import User from mediagoblin.tools import pluginapi from sqlalchemy import or_ +from mediagoblin.tools.staticdirect import PluginStatic + + +PLUGIN_DIR = os.path.dirname(__file__) def setup_plugin(): config = pluginapi.get_config('mediagoblin.plugins.basic_auth') + routes = [ + ('mediagoblin.plugins.basic_auth.edit.pass', + '/edit/password/', + 'mediagoblin.plugins.basic_auth.views:change_pass'), + ('mediagoblin.plugins.basic_auth.forgot_password', + '/auth/forgot_password/', + 'mediagoblin.plugins.basic_auth.views:forgot_password'), + ('mediagoblin.plugins.basic_auth.verify_forgot_password', + '/auth/forgot_password/verify/', + 'mediagoblin.plugins.basic_auth.views:verify_forgot_password')] + + pluginapi.register_routes(routes) + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + pluginapi.register_template_hooks( + {'edit_link': 'mediagoblin/plugins/basic_auth/edit_link.html', + 'fp_link': 'mediagoblin/plugins/basic_auth/fp_link.html', + 'fp_head': 'mediagoblin/plugins/basic_auth/fp_head.html', + 'create_account': + 'mediagoblin/plugins/basic_auth/create_account_link.html'}) + def get_user(**kwargs): username = kwargs.pop('username', None) @@ -85,4 +113,7 @@ hooks = { 'auth_check_password': check_password, 'auth_fake_login_attempt': auth_tools.fake_login_attempt, 'template_global_context': append_to_global_context, + 'static_setup': lambda: PluginStatic( + 'coreplugin_basic_auth', + resource_filename('mediagoblin.plugins.basic_auth', 'static')) } diff --git a/mediagoblin/plugins/basic_auth/forms.py b/mediagoblin/plugins/basic_auth/forms.py index 6cf01b38..c10496f8 100644 --- a/mediagoblin/plugins/basic_auth/forms.py +++ b/mediagoblin/plugins/basic_auth/forms.py @@ -44,3 +44,33 @@ class LoginForm(wtforms.Form): stay_logged_in = wtforms.BooleanField( label='', description=_('Stay logged in')) + + +class ForgotPassForm(wtforms.Form): + username = wtforms.TextField( + _('Username or email'), + [wtforms.validators.Required(), + normalize_user_or_email_field()]) + + +class ChangeForgotPassForm(wtforms.Form): + password = wtforms.PasswordField( + 'Password', + [wtforms.validators.Required(), + wtforms.validators.Length(min=5, max=1024)]) + token = wtforms.HiddenField( + '', + [wtforms.validators.Required()]) + + +class ChangePassForm(wtforms.Form): + old_password = wtforms.PasswordField( + _('Old password'), + [wtforms.validators.Required()], + description=_( + "Enter your old password to prove you own this account.")) + new_password = wtforms.PasswordField( + _('New password'), + [wtforms.validators.Required(), + wtforms.validators.Length(min=6, max=30)], + id="password") diff --git a/mediagoblin/plugins/basic_auth/static/js/autofilledin_password.js b/mediagoblin/plugins/basic_auth/static/js/autofilledin_password.js new file mode 100644 index 00000000..45e867fe --- /dev/null +++ b/mediagoblin/plugins/basic_auth/static/js/autofilledin_password.js @@ -0,0 +1,25 @@ +/** + * GNU MediaGoblin -- federated, autonomous media hosting + * Copyright (C) 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/>. + */ + +$(document).ready(function(){ + $('#forgot_password').click(function(event){ + event.preventDefault(); + window.location.pathname = $(this).attr('href') + '?username=' + + $('#username').val(); + }); +}); diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/change_fp.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/change_fp.html new file mode 100644 index 00000000..47cd591e --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/change_fp.html @@ -0,0 +1,43 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# 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/>. +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_head %} + <script type="text/javascript" + src="{{ request.staticdirect('/js/show_password.js') }}"></script> +{% endblock mediagoblin_head %} + +{% block title -%} + {% trans %}Set your new password{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + <form action="{{ request.urlgen('mediagoblin.plugins.basic_auth.verify_forgot_password') }}" + method="POST" enctype="multipart/form-data"> + {{ csrf_token }} + <div class="form_box"> + <h1>{% trans %}Set your new password{% endtrans %}</h1> + {{ wtforms_util.render_divs(cp_form, True) }} + <div class="form_submit_buttons"> + <input type="submit" value="{% trans %}Set password{% endtrans %}" class="button_form"/> + </div> + </div> +{% endblock %} + diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/change_pass.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/change_pass.html new file mode 100644 index 00000000..596a4def --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/change_pass.html @@ -0,0 +1,52 @@ +{# +# 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/>. +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_head %} + <script type="text/javascript" + src="{{ request.staticdirect('/js/show_password.js') }}"></script> +{% endblock mediagoblin_head %} + +{% block title -%} + {% trans username=user.username -%} + Changing {{ username }}'s password + {%- endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + <form action="{{ request.urlgen('mediagoblin.plugins.basic_auth.edit.pass') }}" + method="POST" enctype="multipart/form-data"> + <div class="form_box edit_box"> + <h1> + {%- trans username=user.username -%} + Changing {{ username }}'s password + {%- endtrans -%} + </h1> + {{ wtforms_util.render_divs(form, True) }} + {{ csrf_token }} + <div class="form_submit_buttons"> + <input type="submit" value="{% trans %}Save{% endtrans %}" + class="button_form" /> + </div> + </div> + </form> +{% endblock %} + + diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/create_account_link.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/create_account_link.html new file mode 100644 index 00000000..7425630b --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/create_account_link.html @@ -0,0 +1,27 @@ +{# +# 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/>. +#} + +{% block create_account %} + {% if allow_registration %} + <p> + {% trans %}Don't have an account yet?{% endtrans %} + <a href="{{ request.urlgen('mediagoblin.auth.register') }}"> + {%- trans %}Create one here!{% endtrans %}</a> + </p> + {% endif %} +{% endblock %} diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/edit_link.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/edit_link.html new file mode 100644 index 00000000..9fd09ab1 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/edit_link.html @@ -0,0 +1,25 @@ +{# +# 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/>. +#} + +{% block change_pass_link %} + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.basic_auth.edit.pass') }}"> + {% trans %}Change your password.{% endtrans %} + </a> + </p> +{% endblock %} diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/forgot_password.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/forgot_password.html new file mode 100644 index 00000000..b0028ab6 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/forgot_password.html @@ -0,0 +1,38 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# 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/>. +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block title -%} + {% trans %}Recover password{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + <form action="{{ request.urlgen('mediagoblin.plugins.basic_auth.forgot_password') }}" + method="POST" enctype="multipart/form-data"> + {{ csrf_token }} + <div class="form_box"> + <h1>{% trans %}Recover password{% endtrans %}</h1> + {{ wtforms_util.render_divs(fp_form, True) }} + <div class="form_submit_buttons"> + <input type="submit" value="{% trans %}Send instructions{% endtrans %}" class="button_form"/> + </div> + </div> + </form> +{% endblock %} diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_head.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_head.html new file mode 100644 index 00000000..292dce28 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_head.html @@ -0,0 +1,20 @@ +{# +# 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/>. +#} + +<script type="text/javascript" + src="{{ request.staticdirect('/js/autofilledin_password.js', 'coreplugin_basic_auth') }}"></script> diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_link.html b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_link.html new file mode 100644 index 00000000..404358d8 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_link.html @@ -0,0 +1,25 @@ +{# +# 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/>. +#} + +{% block fp_link %} + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.basic_auth.forgot_password') }}" id="forgot_password"> + {% trans %}Forgot your password?{% endtrans %}</a> + </p> +{% endblock %} + diff --git a/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_verification_email.txt b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_verification_email.txt new file mode 100644 index 00000000..fb5e1674 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/templates/mediagoblin/plugins/basic_auth/fp_verification_email.txt @@ -0,0 +1,30 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# 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/>. +#} + +{% trans username=username, verification_url=verification_url|safe -%} +Hi {{ username }}, + +to change your GNU MediaGoblin password, open the following URL in +your web browser: + +{{ verification_url }} + +If you think this is an error, just ignore this email and continue being +a happy goblin! +{%- endtrans %} + diff --git a/mediagoblin/plugins/basic_auth/tools.py b/mediagoblin/plugins/basic_auth/tools.py index 1300bb9a..f943bf39 100644 --- a/mediagoblin/plugins/basic_auth/tools.py +++ b/mediagoblin/plugins/basic_auth/tools.py @@ -16,6 +16,11 @@ import bcrypt import random +from mediagoblin import mg_globals +from mediagoblin.tools.crypto import get_timed_signer_url +from mediagoblin.tools.mail import send_email +from mediagoblin.tools.template import render_template + def bcrypt_check_password(raw_pass, stored_hash, extra_salt=None): """ @@ -82,3 +87,35 @@ def fake_login_attempt(): randplus_hashed_pass = bcrypt.hashpw(hashed_pass, rand_salt) randplus_stored_hash == randplus_hashed_pass + + +EMAIL_FP_VERIFICATION_TEMPLATE = ( + u"{uri}?" + u"token={fp_verification_key}") + + +def send_fp_verification_email(user, request): + """ + Send the verification email to users to change their password. + + Args: + - user: a user object + - request: the request + """ + fp_verification_key = get_timed_signer_url('mail_verification_token') \ + .dumps(user.id) + + rendered_email = render_template( + request, 'mediagoblin/plugins/basic_auth/fp_verification_email.txt', + {'username': user.username, + 'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format( + uri=request.urlgen('mediagoblin.plugins.basic_auth.verify_forgot_password', + qualified=True), + fp_verification_key=fp_verification_key)}) + + # TODO: There is no error handling in place + send_email( + mg_globals.app_config['email_sender_address'], + [user.email], + 'GNU MediaGoblin - Change forgotten password!', + rendered_email) diff --git a/mediagoblin/plugins/basic_auth/views.py b/mediagoblin/plugins/basic_auth/views.py new file mode 100644 index 00000000..02d370f0 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/views.py @@ -0,0 +1,223 @@ +# 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/>. +from itsdangerous import BadSignature + +from mediagoblin import messages +from mediagoblin.db.models import User +from mediagoblin.decorators import require_active_login +from mediagoblin.plugins.basic_auth import forms, tools +from mediagoblin.tools.crypto import get_timed_signer_url +from mediagoblin.tools.mail import email_debug_message +from mediagoblin.tools.response import redirect, render_to_response, render_404 +from mediagoblin.tools.translate import pass_to_ugettext as _ + + +def forgot_password(request): + """ + Forgot password view + + Sends an email with an url to renew forgotten password. + Use GET querystring parameter 'username' to pre-populate the input field + """ + fp_form = forms.ForgotPassForm(request.form, + username=request.args.get('username')) + + if not (request.method == 'POST' and fp_form.validate()): + # Either GET request, or invalid form submitted. Display the template + return render_to_response(request, + 'mediagoblin/plugins/basic_auth/forgot_password.html', + {'fp_form': fp_form}) + + # If we are here: method == POST and form is valid. username casing + # has been sanitized. Store if a user was found by email. We should + # not reveal if the operation was successful then as we don't want to + # leak if an email address exists in the system. + found_by_email = '@' in fp_form.username.data + + if found_by_email: + user = User.query.filter_by( + email=fp_form.username.data).first() + # Don't reveal success in case the lookup happened by email address. + success_message = _("If that email address (case sensitive!) is " + "registered an email has been sent with " + "instructions on how to change your password.") + + else: # found by username + user = User.query.filter_by( + username=fp_form.username.data).first() + + if user is None: + messages.add_message(request, + messages.WARNING, + _("Couldn't find someone with that username.")) + return redirect(request, 'mediagoblin.auth.forgot_password') + + success_message = _("An email has been sent with instructions " + "on how to change your password.") + + if user and user.has_privilege(u'active') is False: + # Don't send reminder because user is inactive or has no verified email + messages.add_message(request, + messages.WARNING, + _("Could not send password recovery email as your username is in" + "active or your account's email address has not been verified.")) + + return redirect(request, 'mediagoblin.user_pages.user_home', + user=user.username) + + # SUCCESS. Send reminder and return to login page + if user: + email_debug_message(request) + tools.send_fp_verification_email(user, request) + + messages.add_message(request, messages.INFO, success_message) + return redirect(request, 'mediagoblin.auth.login') + + +def verify_forgot_password(request): + """ + Check the forgot-password verification and possibly let the user + change their password because of it. + """ + # get form data variables, and specifically check for presence of token + formdata = _process_for_token(request) + if not formdata['has_token']: + return render_404(request) + + formdata_vars = formdata['vars'] + + # Catch error if token is faked or expired + try: + token = get_timed_signer_url("mail_verification_token") \ + .loads(formdata_vars['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') + + # check if it's a valid user id + user = User.query.filter_by(id=int(token)).first() + + # no user in db + if not user: + messages.add_message( + request, messages.ERROR, + _('The user id is incorrect.')) + return redirect( + request, 'index') + + # check if user active and has email verified + if user.has_privilege(u'active'): + cp_form = forms.ChangeForgotPassForm(formdata_vars) + + if request.method == 'POST' and cp_form.validate(): + user.pw_hash = tools.bcrypt_gen_password_hash( + cp_form.password.data) + user.save() + + messages.add_message( + request, + messages.INFO, + _("You can now log in using your new password.")) + return redirect(request, 'mediagoblin.auth.login') + else: + return render_to_response( + request, + 'mediagoblin/plugins/basic_auth/change_fp.html', + {'cp_form': cp_form}) + + ## Commenting this out temporarily because I'm checking into + ## what's going on with user.email_verified. + ## + ## ... if this commit lasts long enough for anyone but me (cwebber) to + ## notice it, they should pester me to remove this or remove it + ## themselves ;) + # + # if not user.email_verified: + # messages.add_message( + # request, messages.ERROR, + # _('You need to verify your email before you can reset your' + # ' password.')) + + if not user.status == 'active': + messages.add_message( + request, messages.ERROR, + _('You are no longer an active user. Please contact the system' + ' admin to reactivate your account.')) + + return redirect( + request, 'index') + + +def _process_for_token(request): + """ + Checks for tokens in formdata without prior knowledge of request method + + For now, returns whether the userid and token formdata variables exist, and + the formdata variables in a hash. Perhaps an object is warranted? + """ + # retrieve the formdata variables + if request.method == 'GET': + formdata_vars = request.GET + else: + formdata_vars = request.form + + formdata = { + 'vars': formdata_vars, + 'has_token': 'token' in formdata_vars} + + return formdata + + +@require_active_login +def change_pass(request): + form = forms.ChangePassForm(request.form) + user = request.user + + if request.method == 'POST' and form.validate(): + + if not tools.bcrypt_check_password( + form.old_password.data, user.pw_hash): + form.old_password.errors.append( + _('Wrong password')) + + return render_to_response( + request, + 'mediagoblin/plugins/basic_auth/change_pass.html', + {'form': form, + 'user': user}) + + # Password matches + user.pw_hash = tools.bcrypt_gen_password_hash( + form.new_password.data) + user.save() + + messages.add_message( + request, messages.SUCCESS, + _('Your password was changed successfully')) + + return redirect(request, 'mediagoblin.edit.account') + + return render_to_response( + request, + 'mediagoblin/plugins/basic_auth/change_pass.html', + {'form': form, + 'user': user}) diff --git a/mediagoblin/plugins/ldap/README.rst b/mediagoblin/plugins/ldap/README.rst new file mode 100644 index 00000000..ea9a34b3 --- /dev/null +++ b/mediagoblin/plugins/ldap/README.rst @@ -0,0 +1,64 @@ +.. MediaGoblin Documentation + + Written in 2012 by MediaGoblin contributors + + To the extent possible under law, the author(s) have dedicated all + copyright and related and neighboring rights to this software to + the public domain worldwide. This software is distributed without + any warranty. + + You should have received a copy of the CC0 Public Domain + Dedication along with this software. If not, see + <http://creativecommons.org/publicdomain/zero/1.0/>. + +.. _ldap-plugin: + +============= + ldap plugin +============= + +.. Warning:: + This plugin is not compatible with the other authentication plugins. + +This plugin allow your GNU Mediagoblin instance to authenticate against an +LDAP server. + +Set up the ldap plugin +====================== + +1. Install the ``python-ldap`` package. + +2. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section:: + + [[mediagoblin.plugins.ldap]] + +Configuring the ldap plugin +=========================== + +This plugin allows you to use multiple ldap servers for authentication. + +In order to configure a server, add the following to you MediaGoblin .ini file +under the ldap plugin:: + + [[mediagoblin.plugins.ldap]] + [[[server1]]] + LDAP_SERVER_URI = 'ldap://ldap.testathon.net:389' + LDAP_USER_DN_TEMPLATE = 'cn={username},ou=users,dc=testathon,dc=net' + [[[server2]]] + ... + +Make any necessary changes to the above to work with your sever. Make sure +``{username}`` is where the username should be in LDAP_USER_DN_TEMPLATE. + +If you would like to fetch the users email from the ldap server upon account +registration, add ``LDAP_SEARCH_BASE = 'ou=users,dc=testathon,dc=net'`` and +``EMAIL_SEARCH_FIELD = 'mail'`` under you server configuration in your +MediaGoblin .ini file. + +.. Warning:: + By default, this plugin provides no encryption when communicating with the + ldap servers. If you would like to use an SSL connection, change + LDAP_SERVER_URI to use ``ldaps://`` and whichever port you use. Default ldap + port for SSL connections is 636. If you would like to use a TLS connection, + add ``LDAP_START_TLS = 'true'`` under your server configuration in your + MediaGoblin .ini file. diff --git a/mediagoblin/plugins/ldap/__init__.py b/mediagoblin/plugins/ldap/__init__.py new file mode 100644 index 00000000..4673acee --- /dev/null +++ b/mediagoblin/plugins/ldap/__init__.py @@ -0,0 +1,59 @@ +# 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 os + +from mediagoblin.auth.tools import create_basic_user +from mediagoblin.tools import pluginapi + +PLUGIN_DIR = os.path.dirname(__file__) + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.ldap') + + routes = [ + ('mediagoblin.plugins.ldap.register', + '/auth/ldap/register/', + 'mediagoblin.plugins.ldap.views:register'), + ('mediagoblin.plugins.ldap.login', + '/auth/ldap/login/', + 'mediagoblin.plugins.ldap.views:login')] + + pluginapi.register_routes(routes) + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + pluginapi.register_template_hooks( + {'create_account': 'mediagoblin/plugins/ldap/create_account_link.html'}) + + +def create_user(register_form): + if 'username' in register_form and 'password' not in register_form: + return create_basic_user(register_form) + + +def no_pass_redirect(): + return 'ldap' + + +def auth(): + return True + +hooks = { + 'setup': setup_plugin, + 'authentication': auth, + 'auth_no_pass_redirect': no_pass_redirect, + 'auth_create_user': create_user, +} diff --git a/mediagoblin/plugins/ldap/forms.py b/mediagoblin/plugins/ldap/forms.py new file mode 100644 index 00000000..7ec1479e --- /dev/null +++ b/mediagoblin/plugins/ldap/forms.py @@ -0,0 +1,40 @@ +# 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 wtforms + +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ +from mediagoblin.auth.tools import normalize_user_or_email_field + + +class RegisterForm(wtforms.Form): + username = wtforms.HiddenField( + '', + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_email=False)]) + email = wtforms.TextField( + _('Email address'), + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_user=False)]) + + +class LoginForm(wtforms.Form): + username = wtforms.TextField( + _('Username'), + [wtforms.validators.Required(), + normalize_user_or_email_field()]) + password = wtforms.PasswordField( + _('Password'), + [wtforms.validators.Required()]) diff --git a/mediagoblin/plugins/ldap/templates/mediagoblin/plugins/ldap/create_account_link.html b/mediagoblin/plugins/ldap/templates/mediagoblin/plugins/ldap/create_account_link.html new file mode 100644 index 00000000..947e4ae2 --- /dev/null +++ b/mediagoblin/plugins/ldap/templates/mediagoblin/plugins/ldap/create_account_link.html @@ -0,0 +1,25 @@ +{# +# 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/>. +#} + +{% block create_account %} + {% if allow_registration %} + <p> + {% trans %}Sign in to create an account!{% endtrans %} + </p> + {% endif %} +{% endblock %} diff --git a/mediagoblin/plugins/ldap/tools.py b/mediagoblin/plugins/ldap/tools.py new file mode 100644 index 00000000..1c436792 --- /dev/null +++ b/mediagoblin/plugins/ldap/tools.py @@ -0,0 +1,65 @@ +# 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 ldap +import logging + +from mediagoblin.tools import pluginapi + +_log = logging.getLogger(__name__) + + +class LDAP(object): + def __init__(self): + self.ldap_settings = pluginapi.get_config('mediagoblin.plugins.ldap') + + def _connect(self, server): + _log.info('Connecting to {0}.'.format(server['LDAP_SERVER_URI'])) + self.conn = ldap.initialize(server['LDAP_SERVER_URI']) + + if server['LDAP_START_TLS'] == 'true': + _log.info('Initiating TLS') + self.conn.start_tls_s() + + def _get_email(self, server, username): + try: + results = self.conn.search_s(server['LDAP_SEARCH_BASE'], + ldap.SCOPE_SUBTREE, 'uid={0}' + .format(username), + [server['EMAIL_SEARCH_FIELD']]) + + email = results[0][1][server['EMAIL_SEARCH_FIELD']][0] + except KeyError: + email = None + + return email + + def login(self, username, password): + for k, v in self.ldap_settings.iteritems(): + try: + self._connect(v) + user_dn = v['LDAP_USER_DN_TEMPLATE'].format(username=username) + self.conn.simple_bind_s(user_dn, password.encode('utf8')) + email = self._get_email(v, username) + return username, email + + except ldap.LDAPError, e: + _log.info(e) + + finally: + _log.info('Unbinding {0}.'.format(v['LDAP_SERVER_URI'])) + self.conn.unbind() + + return False, None diff --git a/mediagoblin/plugins/ldap/views.py b/mediagoblin/plugins/ldap/views.py new file mode 100644 index 00000000..aef1bf56 --- /dev/null +++ b/mediagoblin/plugins/ldap/views.py @@ -0,0 +1,104 @@ +# 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/>. +from mediagoblin import mg_globals, messages +from mediagoblin.auth.tools import register_user +from mediagoblin.db.models import User +from mediagoblin.decorators import allow_registration, auth_enabled +from mediagoblin.plugins.ldap import forms +from mediagoblin.plugins.ldap.tools import LDAP +from mediagoblin.tools.translate import pass_to_ugettext as _ +from mediagoblin.tools.response import redirect, render_to_response + + +@auth_enabled +def login(request): + login_form = forms.LoginForm(request.form) + + login_failed = False + + if request.method == 'POST' and login_form.validate(): + l = LDAP() + username, email = l.login(login_form.username.data, + login_form.password.data) + + if username: + user = User.query.filter_by( + username=username).first() + + if user: + # set up login in session + request.session['user_id'] = unicode(user.id) + request.session.save() + + if request.form.get('next'): + return redirect(request, location=request.form['next']) + else: + return redirect(request, "index") + else: + if not mg_globals.app.auth: + messages.add_message( + request, + messages.WARNING, + _('Sorry, authentication is disabled on this ' + 'instance.')) + return redirect(request, 'index') + + register_form = forms.RegisterForm(username=username, + email=email) + + return render_to_response( + request, + 'mediagoblin/auth/register.html', + {'register_form': register_form, + 'post_url': request.urlgen('mediagoblin.plugins.ldap.register')}) + + login_failed = True + + return render_to_response( + request, + 'mediagoblin/auth/login.html', + {'login_form': login_form, + 'next': request.GET.get('next') or request.form.get('next'), + 'login_failed': login_failed, + 'post_url': request.urlgen('mediagoblin.plugins.ldap.login'), + 'allow_registration': mg_globals.app_config["allow_registration"]}) + + +@allow_registration +@auth_enabled +def register(request): + if request.method == 'GET': + return redirect( + request, + 'mediagoblin.plugins.ldap.login') + + register_form = forms.RegisterForm(request.form) + + if register_form.validate(): + user = register_user(request, register_form) + + if user: + # redirect the user to their homepage... there will be a + # message waiting for them to verify their email + return redirect( + request, 'mediagoblin.user_pages.user_home', + user=user.username) + + return render_to_response( + request, + 'mediagoblin/auth/register.html', + {'register_form': register_form, + 'post_url': request.urlgen('mediagoblin.plugins.ldap.register')}) diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html index 8d74c2b9..193a3b2d 100644 --- a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html @@ -20,8 +20,8 @@ {% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} {% block mediagoblin_head %} - <script type="text/javascript" - src="{{ request.staticdirect('/js/autofilledin_password.js') }}"></script> + {{ super() }} + {% template_hook("fp_head") %} {% endblock %} {% block title -%} diff --git a/mediagoblin/plugins/openid/views.py b/mediagoblin/plugins/openid/views.py index b639a4cb..bb2de7ab 100644 --- a/mediagoblin/plugins/openid/views.py +++ b/mediagoblin/plugins/openid/views.py @@ -195,11 +195,11 @@ def finish_login(request): return redirect(request, "index") else: # No user, need to register - if not mg_globals.app.auth: + if not mg_globals.app_config['allow_registration']: messages.add_message( request, messages.WARNING, - _('Sorry, authentication is disabled on this instance.')) + _('Sorry, registration is disabled on this instance.')) return redirect(request, 'index') # Get email and nickname from response diff --git a/mediagoblin/plugins/persona/static/js/persona.js b/mediagoblin/plugins/persona/static/js/persona.js index a6def398..76a7757a 100644 --- a/mediagoblin/plugins/persona/static/js/persona.js +++ b/mediagoblin/plugins/persona/static/js/persona.js @@ -17,14 +17,20 @@ */ $(document).ready(function () { + var requestParams = { + backgroundColor: "#171717", + siteName: "GNU MediaGoblin", + siteLogo: "data:image/png;base64,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" + }; + var signinLink = document.getElementById('persona_login'); if (signinLink) { - signinLink.onclick = function() { navigator.id.request(); }; + signinLink.onclick = function() { navigator.id.request(requestParams); }; } var signinLink1 = document.getElementById('persona_login1'); if (signinLink1) { - signinLink1.onclick = function() { navigator.id.request(); }; + signinLink1.onclick = function() { navigator.id.request(requestParams); }; } var signoutLink = document.getElementById('logout'); @@ -33,11 +39,13 @@ $(document).ready(function () { } var logout_url = document.getElementById('_logout_url').value; + var persona_user = document.getElementById('_persona_user').value; navigator.id.watch({ + loggedInUser: persona_user || null, onlogin: function(assertion) { document.getElementById('_assertion').value = assertion; - document.getElementById('_persona_login').submit() + document.getElementById('_persona_login').submit(); }, onlogout: function() { $.ajax({ diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html index 08879da5..a022c62a 100644 --- a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html +++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html @@ -17,9 +17,8 @@ #} {% block persona_edit_link %} - <p> - <a href="{{ request.urlgen('mediagoblin.plugins.persona.edit') }}"> - {% trans %}Edit your Persona email addresses{% endtrans %} - </a> - </p> + <a href="{{ request.urlgen('mediagoblin.plugins.persona.edit') }}"> + {% trans %}Persona's{% endtrans %} + </a> + · {% endblock %} diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html index 372bd246..03d63aae 100644 --- a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html +++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html @@ -28,5 +28,11 @@ <input type="hidden" name="assertion" type="text" id="_assertion"/> <input type="hidden" name="_logout_url" type="text" id="_logout_url" value="{{ request.urlgen('mediagoblin.auth.logout') }}"/> + <input type="hidden" type="text" id="_persona_user" + {% if request.session.get('persona_login_email', False) %} + value="{{ request.session['persona_login_email'] }}"/> + {% else %} + value=""/> + {% endif %} </form> {% endblock %} diff --git a/mediagoblin/plugins/persona/views.py b/mediagoblin/plugins/persona/views.py index 4ea16fa7..1bba3b8c 100644 --- a/mediagoblin/plugins/persona/views.py +++ b/mediagoblin/plugins/persona/views.py @@ -64,6 +64,7 @@ def login(request): if user: request.session['user_id'] = unicode(user.id) + request.session['persona_login_email'] = email request.session.save() return redirect(request, "index") @@ -183,6 +184,8 @@ def add(request): new_entry.user_id = request.user.id new_entry.save() + request.session['persona_login_email'] = email + messages.add_message( request, messages.SUCCESS, diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index ca723189..f913a730 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -16,17 +16,17 @@ import logging import re -from os.path import splitext -import shutil from werkzeug.exceptions import MethodNotAllowed, BadRequest, NotImplemented from werkzeug.wrappers import BaseResponse +from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.auth.tools import check_login_simple -from mediagoblin.media_types import sniff_media -from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ - run_process_media, new_upload_entry +from mediagoblin.submit.lib import \ + submit_media, check_file_field, get_upload_file_limits, \ + FileUploadLimit, UserUploadLimit, UserPastUploadLimit + from mediagoblin.user_pages.lib import add_media_to_collection from mediagoblin.db.models import Collection @@ -126,58 +126,40 @@ def pwg_images_addSimple(request): if not check_file_field(request, 'image'): raise BadRequest() - filename = request.files['image'].filename - - # Sniff the submitted media to determine which - # media plugin should handle processing - media_type, media_manager = sniff_media( - request.files['image']) - - # create entry and save in database - entry = new_upload_entry(request.user) - entry.media_type = unicode(media_type) - entry.title = ( - unicode(form.name.data) - or unicode(splitext(filename)[0])) - - entry.description = unicode(form.comment.data) - - ''' - # Process the user's folksonomy "tags" - entry.tags = convert_to_tag_list_of_dicts( - form.tags.data) - ''' - - # Generate a slug from the title - entry.generate_slug() - - queue_file = prepare_queue_task(request.app, entry, filename) - - with queue_file: - shutil.copyfileobj(request.files['image'].stream, - queue_file, - length=4 * 1048576) - - # Save now so we have this data before kicking off processing - entry.save() - - # Pass off to processing - # - # (... don't change entry after this point to avoid race - # conditions with changes to the document via processing code) - feed_url = request.urlgen( - 'mediagoblin.user_pages.atom_feed', - qualified=True, user=request.user.username) - run_process_media(entry, feed_url) - - collection_id = form.category.data - if collection_id > 0: - collection = Collection.query.get(collection_id) - if collection is not None and collection.creator == request.user.id: - add_media_to_collection(collection, entry, "") - - return {'image_id': entry.id, 'url': entry.url_for_self(request.urlgen, - qualified=True)} + upload_limit, max_file_size = get_upload_file_limits(request.user) + + try: + entry = submit_media( + mg_app=request.app, user=request.user, + submitted_file=request.files['image'], + filename=request.files['image'].filename, + title=unicode(form.name.data), + description=unicode(form.comment.data), + upload_limit=upload_limit, max_file_size=max_file_size) + + collection_id = form.category.data + if collection_id > 0: + collection = Collection.query.get(collection_id) + if collection is not None and collection.creator == request.user.id: + add_media_to_collection(collection, entry, "") + + return { + 'image_id': entry.id, + 'url': entry.url_for_self( + request.urlgen, + qualified=True)} + + # Handle upload limit issues + except FileUploadLimit: + raise BadRequest( + _(u'Sorry, the file size is too big.')) + except UserUploadLimit: + raise BadRequest( + _('Sorry, uploading this file will put you over your' + ' upload limit.')) + except UserPastUploadLimit: + raise BadRequest( + _('Sorry, you have reached your upload limit.')) md5sum_matcher = re.compile(r"^[0-9a-fA-F]{32}$") |