diff options
Diffstat (limited to 'mediagoblin/plugins')
30 files changed, 2024 insertions, 114 deletions
diff --git a/mediagoblin/plugins/api/__init__.py b/mediagoblin/plugins/api/__init__.py index d3fdf2ef..1eddd9e0 100644 --- a/mediagoblin/plugins/api/__init__.py +++ b/mediagoblin/plugins/api/__init__.py @@ -23,11 +23,11 @@ _log = logging.getLogger(__name__) PLUGIN_DIR = os.path.dirname(__file__) -config = pluginapi.get_config(__name__) - def setup_plugin(): _log.info('Setting up API...') + config = pluginapi.get_config(__name__) + _log.debug('API config: {0}'.format(config)) routes = [ diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 2055a663..9159fe65 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -18,7 +18,6 @@ import json import logging from os.path import splitext -from werkzeug.datastructures import FileStorage from werkzeug.exceptions import BadRequest, Forbidden from werkzeug.wrappers import Response @@ -27,7 +26,8 @@ from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.media_types import sniff_media from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \ json_response -from mediagoblin.submit.lib import prepare_queue_task, run_process_media +from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ + run_process_media, new_upload_entry _log = logging.getLogger(__name__) @@ -45,9 +45,7 @@ def post_entry(request): _log.debug('Must POST against post_entry') raise BadRequest() - if not 'file' in request.files \ - or not isinstance(request.files['file'], FileStorage) \ - or not request.files['file'].stream: + if not check_file_field(request, 'file'): _log.debug('File field not found') raise BadRequest() @@ -55,7 +53,7 @@ def post_entry(request): media_type, media_manager = sniff_media(media_file) - entry = request.db.MediaEntry() + 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]) @@ -63,8 +61,6 @@ def post_entry(request): entry.description = unicode(request.form.get('description')) entry.license = unicode(request.form.get('license', '')) - entry.uploader = request.user.id - entry.generate_slug() # queue appropriately diff --git a/mediagoblin/plugins/basic_auth/__init__.py b/mediagoblin/plugins/basic_auth/__init__.py new file mode 100644 index 00000000..33a554b0 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/__init__.py @@ -0,0 +1,88 @@ +# 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.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_ + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.basic_auth') + + +def get_user(**kwargs): + username = kwargs.pop('username', None) + if username: + user = User.query.filter( + or_( + User.username == username, + User.email == username, + )).first() + return user + + +def create_user(registration_form): + user = get_user(username=registration_form.username.data) + if not user and 'password' in registration_form: + user = create_basic_user(registration_form) + user.pw_hash = gen_password_hash( + registration_form.password.data) + user.save() + return user + + +def get_login_form(request): + return auth_forms.LoginForm(request.form) + + +def get_registration_form(request): + return auth_forms.RegistrationForm(request.form) + + +def gen_password_hash(raw_pass, extra_salt=None): + return auth_tools.bcrypt_gen_password_hash(raw_pass, extra_salt) + + +def check_password(raw_pass, stored_hash, extra_salt=None): + if stored_hash: + return auth_tools.bcrypt_check_password(raw_pass, + stored_hash, extra_salt) + return None + + +def auth(): + return True + + +def append_to_global_context(context): + context['pass_auth'] = True + return context + + +hooks = { + 'setup': setup_plugin, + 'authentication': auth, + 'auth_get_user': get_user, + 'auth_create_user': create_user, + 'auth_get_login_form': get_login_form, + 'auth_get_registration_form': get_registration_form, + 'auth_gen_password_hash': gen_password_hash, + 'auth_check_password': check_password, + 'auth_fake_login_attempt': auth_tools.fake_login_attempt, + 'template_global_context': append_to_global_context, +} diff --git a/mediagoblin/plugins/basic_auth/forms.py b/mediagoblin/plugins/basic_auth/forms.py new file mode 100644 index 00000000..6cf01b38 --- /dev/null +++ b/mediagoblin/plugins/basic_auth/forms.py @@ -0,0 +1,46 @@ +# 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 RegistrationForm(wtforms.Form): + username = wtforms.TextField( + _('Username'), + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_email=False)]) + password = wtforms.PasswordField( + _('Password'), + [wtforms.validators.Required(), + wtforms.validators.Length(min=5, max=1024)]) + email = wtforms.TextField( + _('Email address'), + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_user=False)]) + + +class LoginForm(wtforms.Form): + username = wtforms.TextField( + _('Username or Email'), + [wtforms.validators.Required(), + normalize_user_or_email_field()]) + password = wtforms.PasswordField( + _('Password')) + stay_logged_in = wtforms.BooleanField( + label='', + description=_('Stay logged in')) diff --git a/mediagoblin/plugins/basic_auth/tools.py b/mediagoblin/plugins/basic_auth/tools.py new file mode 100644 index 00000000..1300bb9a --- /dev/null +++ b/mediagoblin/plugins/basic_auth/tools.py @@ -0,0 +1,84 @@ +# 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 bcrypt +import random + + +def bcrypt_check_password(raw_pass, stored_hash, extra_salt=None): + """ + Check to see if this password matches. + + Args: + - raw_pass: user submitted password to check for authenticity. + - stored_hash: The hash of the raw password (and possibly extra + salt) to check against + - extra_salt: (optional) If this password is with stored with a + non-database extra salt (probably in the config file) for extra + security, factor this into the check. + + Returns: + True or False depending on success. + """ + if extra_salt: + raw_pass = u"%s:%s" % (extra_salt, raw_pass) + + hashed_pass = bcrypt.hashpw(raw_pass.encode('utf-8'), stored_hash) + + # Reduce risk of timing attacks by hashing again with a random + # number (thx to zooko on this advice, which I hopefully + # incorporated right.) + # + # See also: + rand_salt = bcrypt.gensalt(5) + randplus_stored_hash = bcrypt.hashpw(stored_hash, rand_salt) + randplus_hashed_pass = bcrypt.hashpw(hashed_pass, rand_salt) + + return randplus_stored_hash == randplus_hashed_pass + + +def bcrypt_gen_password_hash(raw_pass, extra_salt=None): + """ + Generate a salt for this new password. + + Args: + - raw_pass: user submitted password + - extra_salt: (optional) If this password is with stored with a + non-database extra salt + """ + if extra_salt: + raw_pass = u"%s:%s" % (extra_salt, raw_pass) + + return unicode( + bcrypt.hashpw(raw_pass.encode('utf-8'), bcrypt.gensalt())) + + +def fake_login_attempt(): + """ + Pretend we're trying to login. + + Nothing actually happens here, we're just trying to take up some + time, approximately the same amount of time as + bcrypt_check_password, so as to avoid figuring out what users are + on the system by intentionally faking logins a bunch of times. + """ + rand_salt = bcrypt.gensalt(5) + + hashed_pass = bcrypt.hashpw(str(random.random()), rand_salt) + + randplus_stored_hash = bcrypt.hashpw(str(random.random()), rand_salt) + randplus_hashed_pass = bcrypt.hashpw(hashed_pass, rand_salt) + + randplus_stored_hash == randplus_hashed_pass diff --git a/mediagoblin/plugins/httpapiauth/__init__.py b/mediagoblin/plugins/httpapiauth/__init__.py index 081b590e..2b2d593c 100644 --- a/mediagoblin/plugins/httpapiauth/__init__.py +++ b/mediagoblin/plugins/httpapiauth/__init__.py @@ -15,10 +15,10 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import logging -import base64 -from werkzeug.exceptions import BadRequest, Unauthorized +from werkzeug.exceptions import Unauthorized +from mediagoblin.auth.tools import check_login_simple from mediagoblin.plugins.api.tools import Auth _log = logging.getLogger(__name__) @@ -40,10 +40,10 @@ class HTTPAuth(Auth): if not request.authorization: return False - user = request.db.User.query.filter_by( - username=unicode(request.authorization['username'])).first() + user = check_login_simple(unicode(request.authorization['username']), + request.authorization['password']) - if user.check_login(request.authorization['password']): + if user: request.user = user return True else: diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py index 4714d95d..5762379d 100644 --- a/mediagoblin/plugins/oauth/__init__.py +++ b/mediagoblin/plugins/oauth/__init__.py @@ -34,7 +34,7 @@ def setup_plugin(): _log.debug('OAuth config: {0}'.format(config)) routes = [ - ('mediagoblin.plugins.oauth.authorize', + ('mediagoblin.plugins.oauth.authorize', '/oauth/authorize', 'mediagoblin.plugins.oauth.views:authorize'), ('mediagoblin.plugins.oauth.authorize_client', diff --git a/mediagoblin/plugins/oauth/forms.py b/mediagoblin/plugins/oauth/forms.py index d0a4e9b8..5edd992a 100644 --- a/mediagoblin/plugins/oauth/forms.py +++ b/mediagoblin/plugins/oauth/forms.py @@ -19,7 +19,7 @@ import wtforms from urlparse import urlparse from mediagoblin.tools.extlib.wtf_html5 import URLField -from mediagoblin.tools.translate import fake_ugettext_passthrough as _ +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ class AuthorizationForm(wtforms.Form): diff --git a/mediagoblin/plugins/oauth/migrations.py b/mediagoblin/plugins/oauth/migrations.py index 6aa0d7cb..d7b89da3 100644 --- a/mediagoblin/plugins/oauth/migrations.py +++ b/mediagoblin/plugins/oauth/migrations.py @@ -102,6 +102,21 @@ class OAuthCode_v0(declarative_base()): client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False) +class OAuthRefreshToken_v0(declarative_base()): + __tablename__ = 'oauth__refresh_tokens' + + id = Column(Integer, primary_key=True) + created = Column(DateTime, nullable=False, + default=datetime.now) + + token = Column(Unicode, index=True) + + user_id = Column(Integer, ForeignKey(User.id), nullable=False) + + # XXX: Is it OK to use OAuthClient_v0.id in this way? + client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False) + + @RegisterMigration(1, MIGRATIONS) def remove_and_replace_token_and_code(db): metadata = MetaData(bind=db.bind) @@ -122,3 +137,22 @@ def remove_and_replace_token_and_code(db): OAuthCode_v0.__table__.create(db.bind) db.commit() + + +@RegisterMigration(2, MIGRATIONS) +def remove_refresh_token_field(db): + metadata = MetaData(bind=db.bind) + + token_table = Table('oauth__tokens', metadata, autoload=True, + autoload_with=db.bind) + + refresh_token = token_table.columns['refresh_token'] + + refresh_token.drop() + db.commit() + +@RegisterMigration(3, MIGRATIONS) +def create_refresh_token_table(db): + OAuthRefreshToken_v0.__table__.create(db.bind) + + db.commit() diff --git a/mediagoblin/plugins/oauth/models.py b/mediagoblin/plugins/oauth/models.py index 695dad31..439424d3 100644 --- a/mediagoblin/plugins/oauth/models.py +++ b/mediagoblin/plugins/oauth/models.py @@ -14,17 +14,17 @@ # 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 uuid -import bcrypt from datetime import datetime, timedelta -from mediagoblin.db.base import Base -from mediagoblin.db.models import User from sqlalchemy import ( Column, Unicode, Integer, DateTime, ForeignKey, Enum) -from sqlalchemy.orm import relationship +from sqlalchemy.orm import relationship, backref +from mediagoblin.db.base import Base +from mediagoblin.db.models import User +from mediagoblin.plugins.oauth.tools import generate_identifier, \ + generate_secret, generate_token, generate_code, generate_refresh_token # Don't remove this, I *think* it applies sqlalchemy-migrate functionality onto # the models. @@ -41,11 +41,14 @@ class OAuthClient(Base): name = Column(Unicode) description = Column(Unicode) - identifier = Column(Unicode, unique=True, index=True) - secret = Column(Unicode, index=True) + identifier = Column(Unicode, unique=True, index=True, + default=generate_identifier) + secret = Column(Unicode, index=True, default=generate_secret) owner_id = Column(Integer, ForeignKey(User.id)) - owner = relationship(User, backref='registered_clients') + owner = relationship( + User, + backref=backref('registered_clients', cascade='all, delete-orphan')) redirect_uri = Column(Unicode) @@ -54,14 +57,8 @@ class OAuthClient(Base): u'public', name=u'oauth__client_type')) - def generate_identifier(self): - self.identifier = unicode(uuid.uuid4()) - - def generate_secret(self): - self.secret = unicode( - bcrypt.hashpw( - unicode(uuid.uuid4()), - bcrypt.gensalt())) + def update_secret(self): + self.secret = generate_secret() def __repr__(self): return '<{0} {1}:{2} ({3})>'.format( @@ -76,10 +73,15 @@ class OAuthUserClient(Base): id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey(User.id)) - user = relationship(User, backref='oauth_clients') + user = relationship( + User, + backref=backref('oauth_client_relations', + cascade='all, delete-orphan')) client_id = Column(Integer, ForeignKey(OAuthClient.id)) - client = relationship(OAuthClient, backref='users') + client = relationship( + OAuthClient, + backref=backref('oauth_user_relations', cascade='all, delete-orphan')) state = Column(Enum( u'approved', @@ -103,15 +105,18 @@ class OAuthToken(Base): default=datetime.now) expires = Column(DateTime, nullable=False, default=lambda: datetime.now() + timedelta(days=30)) - token = Column(Unicode, index=True) - refresh_token = Column(Unicode, index=True) + token = Column(Unicode, index=True, default=generate_token) user_id = Column(Integer, ForeignKey(User.id), nullable=False, index=True) - user = relationship(User) + user = relationship( + User, + backref=backref('oauth_tokens', cascade='all, delete-orphan')) client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False) - client = relationship(OAuthClient) + client = relationship( + OAuthClient, + backref=backref('oauth_tokens', cascade='all, delete-orphan')) def __repr__(self): return '<{0} #{1} expires {2} [{3}, {4}]>'.format( @@ -121,6 +126,34 @@ class OAuthToken(Base): self.user, self.client) +class OAuthRefreshToken(Base): + __tablename__ = 'oauth__refresh_tokens' + + id = Column(Integer, primary_key=True) + created = Column(DateTime, nullable=False, + default=datetime.now) + + token = Column(Unicode, index=True, + default=generate_refresh_token) + + user_id = Column(Integer, ForeignKey(User.id), nullable=False) + + user = relationship(User, backref=backref('oauth_refresh_tokens', + cascade='all, delete-orphan')) + + client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False) + client = relationship(OAuthClient, + backref=backref( + 'oauth_refresh_tokens', + cascade='all, delete-orphan')) + + def __repr__(self): + return '<{0} #{1} [{3}, {4}]>'.format( + self.__class__.__name__, + self.id, + self.user, + self.client) + class OAuthCode(Base): __tablename__ = 'oauth__codes' @@ -130,14 +163,17 @@ class OAuthCode(Base): default=datetime.now) expires = Column(DateTime, nullable=False, default=lambda: datetime.now() + timedelta(minutes=5)) - code = Column(Unicode, index=True) + code = Column(Unicode, index=True, default=generate_code) user_id = Column(Integer, ForeignKey(User.id), nullable=False, index=True) - user = relationship(User) + user = relationship(User, backref=backref('oauth_codes', + cascade='all, delete-orphan')) client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False) - client = relationship(OAuthClient) + client = relationship(OAuthClient, backref=backref( + 'oauth_codes', + cascade='all, delete-orphan')) def __repr__(self): return '<{0} #{1} expires {2} [{3}, {4}]>'.format( @@ -150,6 +186,7 @@ class OAuthCode(Base): MODELS = [ OAuthToken, + OAuthRefreshToken, OAuthCode, OAuthClient, OAuthUserClient] diff --git a/mediagoblin/plugins/oauth/tools.py b/mediagoblin/plugins/oauth/tools.py index d21c8a5b..27ff32b4 100644 --- a/mediagoblin/plugins/oauth/tools.py +++ b/mediagoblin/plugins/oauth/tools.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # GNU MediaGoblin -- federated, autonomous media hosting # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. # @@ -14,13 +15,26 @@ # 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 uuid + +from random import getrandbits + +from datetime import datetime + from functools import wraps -from mediagoblin.plugins.oauth.models import OAuthClient from mediagoblin.plugins.api.tools import json_response def require_client_auth(controller): + ''' + View decorator + + - Requires the presence of ``?client_id`` + ''' + # Avoid circular import + from mediagoblin.plugins.oauth.models import OAuthClient + @wraps(controller) def wrapper(request, *args, **kw): if not request.GET.get('client_id'): @@ -41,3 +55,60 @@ def require_client_auth(controller): return controller(request, client) return wrapper + + +def create_token(client, user): + ''' + Create an OAuthToken and an OAuthRefreshToken entry in the database + + Returns the data structure expected by the OAuth clients. + ''' + from mediagoblin.plugins.oauth.models import OAuthToken, OAuthRefreshToken + + token = OAuthToken() + token.user = user + token.client = client + token.save() + + refresh_token = OAuthRefreshToken() + refresh_token.user = user + refresh_token.client = client + refresh_token.save() + + # expire time of token in full seconds + # timedelta.total_seconds is python >= 2.7 or we would use that + td = token.expires - datetime.now() + exp_in = 86400*td.days + td.seconds # just ignore µsec + + return {'access_token': token.token, 'token_type': 'bearer', + 'refresh_token': refresh_token.token, 'expires_in': exp_in} + + +def generate_identifier(): + ''' Generates a ``uuid.uuid4()`` ''' + return unicode(uuid.uuid4()) + + +def generate_token(): + ''' Uses generate_identifier ''' + return generate_identifier() + + +def generate_refresh_token(): + ''' Uses generate_identifier ''' + return generate_identifier() + + +def generate_code(): + ''' Uses generate_identifier ''' + return generate_identifier() + + +def generate_secret(): + ''' + Generate a long string of pseudo-random characters + ''' + # XXX: We might not want it to use bcrypt, since bcrypt takes its time to + # generate the result. + return unicode(getrandbits(192)) + diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py index c7b2a332..d6fd314f 100644 --- a/mediagoblin/plugins/oauth/views.py +++ b/mediagoblin/plugins/oauth/views.py @@ -16,21 +16,21 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import logging -import json from urllib import urlencode -from uuid import uuid4 -from datetime import datetime + +from werkzeug.exceptions import BadRequest from mediagoblin.tools.response import render_to_response, redirect from mediagoblin.decorators import require_active_login -from mediagoblin.messages import add_message, SUCCESS, ERROR +from mediagoblin.messages import add_message, SUCCESS from mediagoblin.tools.translate import pass_to_ugettext as _ -from mediagoblin.plugins.oauth.models import OAuthCode, OAuthToken, \ - OAuthClient, OAuthUserClient +from mediagoblin.plugins.oauth.models import OAuthCode, OAuthClient, \ + OAuthUserClient, OAuthRefreshToken from mediagoblin.plugins.oauth.forms import ClientRegistrationForm, \ AuthorizationForm -from mediagoblin.plugins.oauth.tools import require_client_auth +from mediagoblin.plugins.oauth.tools import require_client_auth, \ + create_token from mediagoblin.plugins.api.tools import json_response _log = logging.getLogger(__name__) @@ -45,14 +45,11 @@ def register_client(request): if request.method == 'POST' and form.validate(): client = OAuthClient() - client.name = unicode(request.form['name']) - client.description = unicode(request.form['description']) - client.type = unicode(request.form['type']) + client.name = unicode(form.name.data) + client.description = unicode(form.description.data) + client.type = unicode(form.type.data) client.owner_id = request.user.id - client.redirect_uri = unicode(request.form['redirect_uri']) - - client.generate_identifier() - client.generate_secret() + client.redirect_uri = unicode(form.redirect_uri.data) client.save() @@ -92,9 +89,9 @@ def authorize_client(request): form.client_id.data).first() if not client: - _log.error('''No such client id as received from client authorization - form.''') - return BadRequest() + _log.error('No such client id as received from client authorization \ +form.') + raise BadRequest() if form.validate(): relation = OAuthUserClient() @@ -105,7 +102,7 @@ def authorize_client(request): elif form.deny.data: relation.state = u'rejected' else: - return BadRequest + raise BadRequest() relation.save() @@ -136,7 +133,7 @@ def authorize(request, client): return json_response({ 'status': 400, 'errors': - [u'Public clients MUST have a redirect_uri pre-set']}, + [u'Public clients should have a redirect_uri pre-set.']}, _disable_cors=True) redirect_uri = client.redirect_uri @@ -146,11 +143,10 @@ def authorize(request, client): if not redirect_uri: return json_response({ 'status': 400, - 'errors': [u'Can not find a redirect_uri for client: {0}'\ - .format(client.name)]}, _disable_cors=True) + 'errors': [u'No redirect_uri supplied!']}, + _disable_cors=True) code = OAuthCode() - code.code = unicode(uuid4()) code.user = request.user code.client = client code.save() @@ -180,59 +176,79 @@ def authorize(request, client): def access_token(request): + ''' + Access token endpoint provides access tokens to any clients that have the + right grants/credentials + ''' + + client = None + user = None + if request.GET.get('code'): + # Validate the code arg, then get the client object from the db. code = OAuthCode.query.filter(OAuthCode.code == request.GET.get('code')).first() - if code: - if code.client.type == u'confidential': - client_identifier = request.GET.get('client_id') - - if not client_identifier: - return json_response({ - 'error': 'invalid_request', - 'error_description': - 'Missing client_id in request'}) - - client_secret = request.GET.get('client_secret') - - if not client_secret: - return json_response({ - 'error': 'invalid_request', - 'error_description': - 'Missing client_secret in request'}) - - if not client_secret == code.client.secret or \ - not client_identifier == code.client.identifier: - return json_response({ - 'error': 'invalid_client', - 'error_description': - 'The client_id or client_secret does not match the' - ' code'}) - - token = OAuthToken() - token.token = unicode(uuid4()) - token.user = code.user - token.client = code.client - token.save() - - # expire time of token in full seconds - # timedelta.total_seconds is python >= 2.7 or we would use that - td = token.expires - datetime.now() - exp_in = 86400*td.days + td.seconds # just ignore µsec - - access_token_data = { - 'access_token': token.token, - 'token_type': 'bearer', - 'expires_in': exp_in} - return json_response(access_token_data, _disable_cors=True) - else: + if not code: return json_response({ 'error': 'invalid_request', 'error_description': - 'Invalid code'}) - else: - return json_response({ - 'error': 'invalid_request', - 'error_descriptin': - 'Missing `code` parameter in request'}) + 'Invalid code.'}) + + client = code.client + user = code.user + + elif request.args.get('refresh_token'): + # Validate a refresh token, then get the client object from the db. + refresh_token = OAuthRefreshToken.query.filter( + OAuthRefreshToken.token == + request.args.get('refresh_token')).first() + + if not refresh_token: + return json_response({ + 'error': 'invalid_request', + 'error_description': + 'Invalid refresh token.'}) + + client = refresh_token.client + user = refresh_token.user + + if client: + client_identifier = request.GET.get('client_id') + + if not client_identifier: + return json_response({ + 'error': 'invalid_request', + 'error_description': + 'Missing client_id in request.'}) + + if not client_identifier == client.identifier: + return json_response({ + 'error': 'invalid_client', + 'error_description': + 'Mismatching client credentials.'}) + + if client.type == u'confidential': + client_secret = request.GET.get('client_secret') + + if not client_secret: + return json_response({ + 'error': 'invalid_request', + 'error_description': + 'Missing client_secret in request.'}) + + if not client_secret == client.secret: + return json_response({ + 'error': 'invalid_client', + 'error_description': + 'Mismatching client credentials.'}) + + + access_token_data = create_token(client, user) + + return json_response(access_token_data, _disable_cors=True) + + return json_response({ + 'error': 'invalid_request', + 'error_description': + 'Missing `code` or `refresh_token` parameter in request.'}) diff --git a/mediagoblin/plugins/openid/__init__.py b/mediagoblin/plugins/openid/__init__.py new file mode 100644 index 00000000..ee88808c --- /dev/null +++ b/mediagoblin/plugins/openid/__init__.py @@ -0,0 +1,123 @@ +# 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 +import uuid + +from sqlalchemy import or_ + +from mediagoblin.auth.tools import create_basic_user +from mediagoblin.db.models import User +from mediagoblin.plugins.openid.models import OpenIDUserURL +from mediagoblin.tools import pluginapi +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ + +PLUGIN_DIR = os.path.dirname(__file__) + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.openid') + + routes = [ + ('mediagoblin.plugins.openid.register', + '/auth/openid/register/', + 'mediagoblin.plugins.openid.views:register'), + ('mediagoblin.plugins.openid.login', + '/auth/openid/login/', + 'mediagoblin.plugins.openid.views:login'), + ('mediagoblin.plugins.openid.finish_login', + '/auth/openid/login/finish/', + 'mediagoblin.plugins.openid.views:finish_login'), + ('mediagoblin.plugins.openid.edit', + '/edit/openid/', + 'mediagoblin.plugins.openid.views:start_edit'), + ('mediagoblin.plugins.openid.finish_edit', + '/edit/openid/finish/', + 'mediagoblin.plugins.openid.views:finish_edit'), + ('mediagoblin.plugins.openid.delete', + '/edit/openid/delete/', + 'mediagoblin.plugins.openid.views:delete_openid'), + ('mediagoblin.plugins.openid.finish_delete', + '/edit/openid/delete/finish/', + 'mediagoblin.plugins.openid.views:finish_delete')] + + pluginapi.register_routes(routes) + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + pluginapi.register_template_hooks( + {'register_link': 'mediagoblin/plugins/openid/register_link.html', + 'login_link': 'mediagoblin/plugins/openid/login_link.html', + 'edit_link': 'mediagoblin/plugins/openid/edit_link.html'}) + + +def create_user(register_form): + if 'openid' in register_form: + username = register_form.username.data + user = User.query.filter( + or_( + User.username == username, + User.email == username, + )).first() + + if not user: + user = create_basic_user(register_form) + + new_entry = OpenIDUserURL() + new_entry.openid_url = register_form.openid.data + new_entry.user_id = user.id + new_entry.save() + + return user + + +def extra_validation(register_form): + openid = register_form.openid.data if 'openid' in \ + register_form else None + if openid: + openid_url_exists = OpenIDUserURL.query.filter_by( + openid_url=openid + ).count() + + extra_validation_passes = True + + if openid_url_exists: + register_form.openid.errors.append( + _('Sorry, an account is already registered to that OpenID.')) + extra_validation_passes = False + + return extra_validation_passes + + +def no_pass_redirect(): + return 'openid' + + +def add_to_form_context(context): + context['openid_link'] = True + return context + + +def Auth(): + return True + +hooks = { + 'setup': setup_plugin, + 'authentication': Auth, + 'auth_extra_validation': extra_validation, + 'auth_create_user': create_user, + 'auth_no_pass_redirect': no_pass_redirect, + ('mediagoblin.auth.register', + 'mediagoblin/auth/register.html'): add_to_form_context, +} diff --git a/mediagoblin/plugins/openid/forms.py b/mediagoblin/plugins/openid/forms.py new file mode 100644 index 00000000..f26024bd --- /dev/null +++ b/mediagoblin/plugins/openid/forms.py @@ -0,0 +1,41 @@ +# 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 RegistrationForm(wtforms.Form): + openid = wtforms.HiddenField( + '', + [wtforms.validators.Required()]) + username = wtforms.TextField( + _('Username'), + [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): + openid = wtforms.TextField( + _('OpenID'), + [wtforms.validators.Required(), + # Can openid's only be urls? + wtforms.validators.URL(message='Please enter a valid url.')]) diff --git a/mediagoblin/plugins/openid/models.py b/mediagoblin/plugins/openid/models.py new file mode 100644 index 00000000..6773f0ad --- /dev/null +++ b/mediagoblin/plugins/openid/models.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/>. +from sqlalchemy import Column, Integer, Unicode, ForeignKey +from sqlalchemy.orm import relationship, backref + +from mediagoblin.db.models import User +from mediagoblin.db.base import Base + + +class OpenIDUserURL(Base): + __tablename__ = "openid__user_urls" + + id = Column(Integer, primary_key=True) + openid_url = Column(Unicode, nullable=False) + user_id = Column(Integer, ForeignKey(User.id), nullable=False) + + # OpenID's are owned by their user, so do the full thing. + user = relationship(User, backref=backref('openid_urls', + cascade='all, delete-orphan')) + + +# OpenID Store Models +class Nonce(Base): + __tablename__ = "openid__nonce" + + server_url = Column(Unicode, primary_key=True) + timestamp = Column(Integer, primary_key=True) + salt = Column(Unicode, primary_key=True) + + def __unicode__(self): + return u'Nonce: %r, %r' % (self.server_url, self.salt) + + +class Association(Base): + __tablename__ = "openid__association" + + server_url = Column(Unicode, primary_key=True) + handle = Column(Unicode, primary_key=True) + secret = Column(Unicode) + issued = Column(Integer) + lifetime = Column(Integer) + assoc_type = Column(Unicode) + + def __unicode__(self): + return u'Association: %r, %r' % (self.server_url, self.handle) + + +MODELS = [ + OpenIDUserURL, + Nonce, + Association +] diff --git a/mediagoblin/plugins/openid/store.py b/mediagoblin/plugins/openid/store.py new file mode 100644 index 00000000..8f9a7012 --- /dev/null +++ b/mediagoblin/plugins/openid/store.py @@ -0,0 +1,127 @@ +# 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 base64 +import time + +from openid.association import Association as OIDAssociation +from openid.store.interface import OpenIDStore +from openid.store import nonce + +from mediagoblin.plugins.openid.models import Association, Nonce + + +class SQLAlchemyOpenIDStore(OpenIDStore): + def __init__(self): + self.max_nonce_age = 6 * 60 * 60 + + def storeAssociation(self, server_url, association): + assoc = Association.query.filter_by( + server_url=server_url, handle=association.handle + ).first() + + if not assoc: + assoc = Association() + assoc.server_url = unicode(server_url) + assoc.handle = association.handle + + # django uses base64 encoding, python-openid uses a blob field for + # secret + assoc.secret = unicode(base64.encodestring(association.secret)) + assoc.issued = association.issued + assoc.lifetime = association.lifetime + assoc.assoc_type = association.assoc_type + assoc.save() + + def getAssociation(self, server_url, handle=None): + assocs = [] + if handle is not None: + assocs = Association.query.filter_by( + server_url=server_url, handle=handle + ) + else: + assocs = Association.query.filter_by( + server_url=server_url + ) + + if assocs.count() == 0: + return None + else: + associations = [] + for assoc in assocs: + association = OIDAssociation( + assoc.handle, base64.decodestring(assoc.secret), + assoc.issued, assoc.lifetime, assoc.assoc_type + ) + if association.getExpiresIn() == 0: + assoc.delete() + else: + associations.append((association.issued, association)) + + if not associations: + return None + associations.sort() + return associations[-1][1] + + def removeAssociation(self, server_url, handle): + assocs = Association.query.filter_by( + server_url=server_url, handle=handle + ).first() + + assoc_exists = True if assocs else False + for assoc in assocs: + assoc.delete() + return assoc_exists + + def useNonce(self, server_url, timestamp, salt): + if abs(timestamp - time.time()) > nonce.SKEW: + return False + + ononce = Nonce.query.filter_by( + server_url=server_url, + timestamp=timestamp, + salt=salt + ).first() + + if ononce: + return False + else: + ononce = Nonce() + ononce.server_url = server_url + ononce.timestamp = timestamp + ononce.salt = salt + ononce.save() + return True + + def cleanupNonces(self, _now=None): + if _now is None: + _now = int(time.time()) + expired = Nonce.query.filter( + Nonce.timestamp < (_now - nonce.SKEW) + ) + count = expired.count() + for each in expired: + each.delete() + return count + + def cleanupAssociations(self): + now = int(time.time()) + assoc = Association.query.all() + count = 0 + for each in assoc: + if (each.lifetime + each.issued) <= now: + each.delete() + count = count + 1 + return count diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/add.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/add.html new file mode 100644 index 00000000..8d308c81 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/add.html @@ -0,0 +1,44 @@ +{# +# 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 title -%} + {% trans %}Add an OpenID{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + <form action="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}" + method="POST" enctype="multipart/form-data"> + {{ csrf_token }} + <div class="form_box"> + <h1>{% trans %}Add an OpenID{% endtrans %}</h1> + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.openid.delete') }}"> + {% trans %}Delete an OpenID{% endtrans %} + </a> + </p> + {{ wtforms_util.render_divs(form, True) }} + <div class="form_submit_buttons"> + <input type="submit" value="{% trans %}Add{% endtrans %}" class="button_form"/> + </div> + </div> + </form> +{% endblock %} + diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/delete.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/delete.html new file mode 100644 index 00000000..84301b9e --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/delete.html @@ -0,0 +1,43 @@ +{# +# 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 title -%} + {% trans %}Delete an OpenID{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + <form action="{{ request.urlgen('mediagoblin.plugins.openid.delete') }}" + method="POST" enctype="multipart/form-data"> + {{ csrf_token }} + <div class="form_box"> + <h1>{% trans %}Delete an OpenID{% endtrans %}</h1> + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}"> + {% trans %}Add an OpenID{% endtrans %} + </a> + </p> + {{ wtforms_util.render_divs(form, True) }} + <div class="form_submit_buttons"> + <input type="submit" value="{% trans %}Delete{% endtrans %}" class="button_form"/> + </div> + </div> + </form> +{% endblock %} diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/edit_link.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/edit_link.html new file mode 100644 index 00000000..2e63e1f8 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/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 openid_edit_link %} + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.openid.edit') }}"> + {% trans %}Edit your OpenID's{% endtrans %} + </a> + </p> +{% endblock %} diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html new file mode 100644 index 00000000..33df7200 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html @@ -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/>. +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_head %} + <script type="text/javascript" + src="{{ request.staticdirect('/js/autofilledin_password.js') }}"></script> +{% endblock %} + +{% block title -%} + {% trans %}Log in{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} + <form action="{{ post_url }}" + method="POST" enctype="multipart/form-data"> + {{ csrf_token }} + <div class="form_box"> + <h1>{% trans %}Log in{% endtrans %}</h1> + {% if login_failed %} + <div class="form_field_error"> + {% trans %}Logging in failed!{% endtrans %} + </div> + {% endif %} + {% if allow_registration %} + <p> + {% trans %}Log in to create an account!{% endtrans %} + </p> + {% endif %} + {% if pass_auth is defined %} + <p> + <a href="{{ request.urlgen('mediagoblin.auth.login') }}?{{ request.query_string }}"> + {%- trans %}Or login with a password!{% endtrans %} + </a> + </p> + {% endif %} + {{ wtforms_util.render_divs(login_form, True) }} + <div class="form_submit_buttons"> + <input type="submit" value="{% trans %}Log in{% endtrans %}" class="button_form"/> + </div> + {% if next %} + <input type="hidden" name="next" value="{{ next }}" class="button_form" + style="display: none;"/> + {% endif %} + </div> + </form> +{% endblock %} + diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html new file mode 100644 index 00000000..e5e77d01 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_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 openid_login_link %} + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.openid.login') }}?{{ request.query_string }}"> + {%- trans %}Or login with OpenID!{% endtrans %} + </a> + </p> +{% endblock %} diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/register_link.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/register_link.html new file mode 100644 index 00000000..9bccb4d8 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/register_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 openid_register_link %} + {% if openid_link is defined %} + <p> + <a href="{{ request.urlgen('mediagoblin.plugins.openid.login') }}"> + {%- trans %}Or register with OpenID!{% endtrans %} + </a> + </p> + {% endif %} +{% endblock %} diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/request_form.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/request_form.html new file mode 100644 index 00000000..68d028d0 --- /dev/null +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/request_form.html @@ -0,0 +1,24 @@ +{# +# 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" %} + +{% block mediagoblin_content %} + <div onload="document.getElementById('openid_message').submit()"> + {{ html|safe }} + </div> +{% endblock %} diff --git a/mediagoblin/plugins/openid/views.py b/mediagoblin/plugins/openid/views.py new file mode 100644 index 00000000..b639a4cb --- /dev/null +++ b/mediagoblin/plugins/openid/views.py @@ -0,0 +1,404 @@ +# 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 openid.consumer import consumer +from openid.consumer.discover import DiscoveryFailure +from openid.extensions.sreg import SRegRequest, SRegResponse + +from mediagoblin import mg_globals, messages +from mediagoblin.db.models import User +from mediagoblin.decorators import (auth_enabled, allow_registration, + require_active_login) +from mediagoblin.tools.response import redirect, render_to_response +from mediagoblin.tools.translate import pass_to_ugettext as _ +from mediagoblin.plugins.openid import forms as auth_forms +from mediagoblin.plugins.openid.models import OpenIDUserURL +from mediagoblin.plugins.openid.store import SQLAlchemyOpenIDStore +from mediagoblin.auth.tools import register_user + + +def _start_verification(request, form, return_to, sreg=True): + """ + Start OpenID Verification. + + Returns False if verification fails, otherwise, will return either a + redirect or render_to_response object + """ + openid_url = form.openid.data + c = consumer.Consumer(request.session, SQLAlchemyOpenIDStore()) + + # Try to discover provider + try: + auth_request = c.begin(openid_url) + except DiscoveryFailure: + # Discovery failed, return to login page + form.openid.errors.append( + _('Sorry, the OpenID server could not be found')) + + return False + + host = 'http://' + request.host + + if sreg: + # Ask provider for email and nickname + auth_request.addExtension(SRegRequest(required=['email', 'nickname'])) + + # Do we even need this? + if auth_request is None: + form.openid.errors.append( + _('No OpenID service was found for %s' % openid_url)) + + elif auth_request.shouldSendRedirect(): + # Begin the authentication process as a HTTP redirect + redirect_url = auth_request.redirectURL( + host, return_to) + + return redirect( + request, location=redirect_url) + + else: + # Send request as POST + form_html = auth_request.htmlMarkup( + host, host + return_to, + # Is this necessary? + form_tag_attrs={'id': 'openid_message'}) + + # Beware: this renders a template whose content is a form + # and some javascript to submit it upon page load. Non-JS + # users will have to click the form submit button to + # initiate OpenID authentication. + return render_to_response( + request, + 'mediagoblin/plugins/openid/request_form.html', + {'html': form_html}) + + return False + + +def _finish_verification(request): + """ + Complete OpenID Verification Process. + + If the verification failed, will return false, otherwise, will return + the response + """ + c = consumer.Consumer(request.session, SQLAlchemyOpenIDStore()) + + # Check the response from the provider + response = c.complete(request.args, request.base_url) + if response.status == consumer.FAILURE: + messages.add_message( + request, + messages.WARNING, + _('Verification of %s failed: %s' % + (response.getDisplayIdentifier(), response.message))) + + elif response.status == consumer.SUCCESS: + # Verification was successfull + return response + + elif response.status == consumer.CANCEL: + # Verification canceled + messages.add_message( + request, + messages.WARNING, + _('Verification cancelled')) + + return False + + +def _response_email(response): + """ Gets the email from the OpenID providers response""" + sreg_response = SRegResponse.fromSuccessResponse(response) + if sreg_response and 'email' in sreg_response: + return sreg_response.data['email'] + return None + + +def _response_nickname(response): + """ Gets the nickname from the OpenID providers response""" + sreg_response = SRegResponse.fromSuccessResponse(response) + if sreg_response and 'nickname' in sreg_response: + return sreg_response.data['nickname'] + return None + + +@auth_enabled +def login(request): + """OpenID Login View""" + login_form = auth_forms.LoginForm(request.form) + allow_registration = mg_globals.app_config["allow_registration"] + + # Can't store next in request.GET because of redirects to OpenID provider + # Store it in the session + next = request.GET.get('next') + request.session['next'] = next + + login_failed = False + + if request.method == 'POST' and login_form.validate(): + return_to = request.urlgen( + 'mediagoblin.plugins.openid.finish_login') + + success = _start_verification(request, login_form, return_to) + + if success: + return success + + login_failed = True + + return render_to_response( + request, + 'mediagoblin/plugins/openid/login.html', + {'login_form': login_form, + 'next': request.session.get('next'), + 'login_failed': login_failed, + 'post_url': request.urlgen('mediagoblin.plugins.openid.login'), + 'allow_registration': allow_registration}) + + +@auth_enabled +def finish_login(request): + """Complete OpenID Login Process""" + response = _finish_verification(request) + + if not response: + # Verification failed, redirect to login page. + return redirect(request, 'mediagoblin.plugins.openid.login') + + # Verification was successfull + query = OpenIDUserURL.query.filter_by( + openid_url=response.identity_url, + ).first() + user = query.user if query else None + + if user: + # Set up login in session + request.session['user_id'] = unicode(user.id) + request.session.save() + + if request.session.get('next'): + return redirect(request, location=request.session.pop('next')) + else: + return redirect(request, "index") + else: + # No user, need to register + if not mg_globals.app.auth: + messages.add_message( + request, + messages.WARNING, + _('Sorry, authentication is disabled on this instance.')) + return redirect(request, 'index') + + # Get email and nickname from response + email = _response_email(response) + username = _response_nickname(response) + + register_form = auth_forms.RegistrationForm(request.form, + openid=response.identity_url, + email=email, + username=username) + return render_to_response( + request, + 'mediagoblin/auth/register.html', + {'register_form': register_form, + 'post_url': request.urlgen('mediagoblin.plugins.openid.register')}) + + +@allow_registration +@auth_enabled +def register(request): + """OpenID Registration View""" + if request.method == 'GET': + # Need to connect to openid provider before registering a user to + # get the users openid url. If method is 'GET', then this page was + # acessed without logging in first. + return redirect(request, 'mediagoblin.plugins.openid.login') + + register_form = auth_forms.RegistrationForm(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.openid.register')}) + + +@require_active_login +def start_edit(request): + """Starts the process of adding an openid url to a users account""" + form = auth_forms.LoginForm(request.form) + + if request.method == 'POST' and form.validate(): + query = OpenIDUserURL.query.filter_by( + openid_url=form.openid.data + ).first() + user = query.user if query else None + + if not user: + return_to = request.urlgen('mediagoblin.plugins.openid.finish_edit') + success = _start_verification(request, form, return_to, False) + + if success: + return success + else: + form.openid.errors.append( + _('Sorry, an account is already registered to that OpenID.')) + + return render_to_response( + request, + 'mediagoblin/plugins/openid/add.html', + {'form': form, + 'post_url': request.urlgen('mediagoblin.plugins.openid.edit')}) + + +@require_active_login +def finish_edit(request): + """Finishes the process of adding an openid url to a user""" + response = _finish_verification(request) + + if not response: + # Verification failed, redirect to add openid page. + return redirect(request, 'mediagoblin.plugins.openid.edit') + + # Verification was successfull + query = OpenIDUserURL.query.filter_by( + openid_url=response.identity_url, + ).first() + user_exists = query.user if query else None + + if user_exists: + # user exists with that openid url, redirect back to edit page + messages.add_message( + request, + messages.WARNING, + _('Sorry, an account is already registered to that OpenID.')) + return redirect(request, 'mediagoblin.plugins.openid.edit') + + else: + # Save openid to user + user = User.query.filter_by( + id=request.session['user_id'] + ).first() + + new_entry = OpenIDUserURL() + new_entry.openid_url = response.identity_url + new_entry.user_id = user.id + new_entry.save() + + messages.add_message( + request, + messages.SUCCESS, + _('Your OpenID url was saved successfully.')) + + return redirect(request, 'mediagoblin.edit.account') + + +@require_active_login +def delete_openid(request): + """View to remove an openid from a users account""" + form = auth_forms.LoginForm(request.form) + + if request.method == 'POST' and form.validate(): + # Check if a user has this openid + query = OpenIDUserURL.query.filter_by( + openid_url=form.openid.data + ) + user = query.first().user if query.first() else None + + if user and user.id == int(request.session['user_id']): + count = len(user.openid_urls) + if not count > 1 and not user.pw_hash: + # Make sure the user has a pw or another OpenID + messages.add_message( + request, + messages.WARNING, + _("You can't delete your only OpenID URL unless you" + " have a password set")) + elif user: + # There is a user, but not the same user who is logged in + form.openid.errors.append( + _('That OpenID is not registered to this account.')) + + if not form.errors and not request.session.get('messages'): + # Okay to continue with deleting openid + return_to = request.urlgen( + 'mediagoblin.plugins.openid.finish_delete') + success = _start_verification(request, form, return_to, False) + + if success: + return success + + return render_to_response( + request, + 'mediagoblin/plugins/openid/delete.html', + {'form': form, + 'post_url': request.urlgen('mediagoblin.plugins.openid.delete')}) + + +@require_active_login +def finish_delete(request): + """Finishes the deletion of an OpenID from an user's account""" + response = _finish_verification(request) + + if not response: + # Verification failed, redirect to delete openid page. + return redirect(request, 'mediagoblin.plugins.openid.delete') + + query = OpenIDUserURL.query.filter_by( + openid_url=response.identity_url + ) + user = query.first().user if query.first() else None + + # Need to check this again because of generic openid urls such as google's + if user and user.id == int(request.session['user_id']): + count = len(user.openid_urls) + if count > 1 or user.pw_hash: + # User has more then one openid or also has a password. + query.first().delete() + + messages.add_message( + request, + messages.SUCCESS, + _('OpenID was successfully removed.')) + + return redirect(request, 'mediagoblin.edit.account') + + elif not count > 1: + messages.add_message( + request, + messages.WARNING, + _("You can't delete your only OpenID URL unless you have a " + "password set")) + + return redirect(request, 'mediagoblin.plugins.openid.delete') + + else: + messages.add_message( + request, + messages.WARNING, + _('That OpenID is not registered to this account.')) + + return redirect(request, 'mediagoblin.plugins.openid.delete') diff --git a/mediagoblin/plugins/piwigo/README.rst b/mediagoblin/plugins/piwigo/README.rst new file mode 100644 index 00000000..0c71ffbc --- /dev/null +++ b/mediagoblin/plugins/piwigo/README.rst @@ -0,0 +1,23 @@ +=================== + piwigo api plugin +=================== + +.. danger:: + This plugin does not work. + It might make your instance unstable or even insecure. + So do not use it, unless you want to help to develop it. + +.. warning:: + You should not depend on this plugin in any way for now. + It might even go away without any notice. + +Okay, so if you still want to test this plugin, +add the following to your mediagoblin_local.ini: + +.. code-block:: ini + + [plugins] + [[mediagoblin.plugins.piwigo]] + +Then try to connect using some piwigo client. +There should be some logging, that might help. diff --git a/mediagoblin/plugins/piwigo/__init__.py b/mediagoblin/plugins/piwigo/__init__.py new file mode 100644 index 00000000..c4da708a --- /dev/null +++ b/mediagoblin/plugins/piwigo/__init__.py @@ -0,0 +1,42 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 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 logging + +from mediagoblin.tools import pluginapi +from mediagoblin.tools.session import SessionManager +from .tools import PWGSession + +_log = logging.getLogger(__name__) + + +def setup_plugin(): + _log.info('Setting up piwigo...') + + routes = [ + ('mediagoblin.plugins.piwigo.wsphp', + '/api/piwigo/ws.php', + 'mediagoblin.plugins.piwigo.views:ws_php'), + ] + + pluginapi.register_routes(routes) + + PWGSession.session_manager = SessionManager("pwg_id", "plugins.piwigo") + + +hooks = { + 'setup': setup_plugin +} diff --git a/mediagoblin/plugins/piwigo/forms.py b/mediagoblin/plugins/piwigo/forms.py new file mode 100644 index 00000000..fb04aa6a --- /dev/null +++ b/mediagoblin/plugins/piwigo/forms.py @@ -0,0 +1,44 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 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 + + +class AddSimpleForm(wtforms.Form): + image = wtforms.FileField() + name = wtforms.TextField( + validators=[wtforms.validators.Length(min=0, max=500)]) + comment = wtforms.TextField() + # tags = wtforms.FieldList(wtforms.TextField()) + category = wtforms.IntegerField() + level = wtforms.IntegerField() + + +_md5_validator = wtforms.validators.Regexp(r"^[0-9a-fA-F]{32}$") + + +class AddForm(wtforms.Form): + original_sum = wtforms.TextField(None, + [_md5_validator, + wtforms.validators.Required()]) + thumbnail_sum = wtforms.TextField(None, + [wtforms.validators.Optional(), + _md5_validator]) + file_sum = wtforms.TextField(None, [_md5_validator]) + name = wtforms.TextField() + date_creation = wtforms.TextField() + categories = wtforms.TextField() diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py new file mode 100644 index 00000000..484ea531 --- /dev/null +++ b/mediagoblin/plugins/piwigo/tools.py @@ -0,0 +1,165 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 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 collections import namedtuple +import logging + +import six +import lxml.etree as ET +from werkzeug.exceptions import MethodNotAllowed, BadRequest + +from mediagoblin.tools.request import setup_user_in_request +from mediagoblin.tools.response import Response + + +_log = logging.getLogger(__name__) + + +PwgError = namedtuple("PwgError", ["code", "msg"]) + + +class PwgNamedArray(list): + def __init__(self, l, item_name, as_attrib=()): + self.item_name = item_name + self.as_attrib = as_attrib + list.__init__(self, l) + + def fill_element_xml(self, el): + for it in self: + n = ET.SubElement(el, self.item_name) + if isinstance(it, dict): + _fill_element_dict(n, it, self.as_attrib) + else: + _fill_element(n, it) + + +def _fill_element_dict(el, data, as_attr=()): + for k, v in data.iteritems(): + if k in as_attr: + if not isinstance(v, six.string_types): + v = str(v) + el.set(k, v) + else: + n = ET.SubElement(el, k) + _fill_element(n, v) + + +def _fill_element(el, data): + if isinstance(data, bool): + if data: + el.text = "1" + else: + el.text = "0" + elif isinstance(data, six.string_types): + el.text = data + elif isinstance(data, int): + el.text = str(data) + elif isinstance(data, dict): + _fill_element_dict(el, data) + elif isinstance(data, PwgNamedArray): + data.fill_element_xml(el) + else: + _log.warn("Can't convert to xml: %r", data) + + +def response_xml(result): + r = ET.Element("rsp") + r.set("stat", "ok") + status = None + if isinstance(result, PwgError): + r.set("stat", "fail") + err = ET.SubElement(r, "err") + err.set("code", str(result.code)) + err.set("msg", result.msg) + if result.code >= 100 and result.code < 600: + status = result.code + else: + _fill_element(r, result) + return Response(ET.tostring(r, encoding="utf-8", xml_declaration=True), + mimetype='text/xml', status=status) + + +class CmdTable(object): + _cmd_table = {} + + def __init__(self, cmd_name, only_post=False): + assert not cmd_name in self._cmd_table + self.cmd_name = cmd_name + self.only_post = only_post + + def __call__(self, to_be_wrapped): + assert not self.cmd_name in self._cmd_table + self._cmd_table[self.cmd_name] = (to_be_wrapped, self.only_post) + return to_be_wrapped + + @classmethod + def find_func(cls, request): + if request.method == "GET": + cmd_name = request.args.get("method") + else: + cmd_name = request.form.get("method") + entry = cls._cmd_table.get(cmd_name) + if not entry: + return entry + _log.debug("Found method %s", cmd_name) + func, only_post = entry + if only_post and request.method != "POST": + _log.warn("Method %s only allowed for POST", cmd_name) + raise MethodNotAllowed() + return func + + +def check_form(form): + if not form.validate(): + _log.error("form validation failed for form %r", form) + for f in form: + if len(f.errors): + _log.error("Errors for %s: %r", f.name, f.errors) + raise BadRequest() + dump = [] + for f in form: + dump.append("%s=%r" % (f.name, f.data)) + _log.debug("form: %s", " ".join(dump)) + + +class PWGSession(object): + session_manager = None + + def __init__(self, request): + self.request = request + self.in_pwg_session = False + + def __enter__(self): + # Backup old state + self.old_session = self.request.session + self.old_user = self.request.user + # Load piwigo session into state + self.request.session = self.session_manager.load_session_from_cookie( + self.request) + setup_user_in_request(self.request) + self.in_pwg_session = True + return self + + def __exit__(self, *args): + # Restore state + self.request.session = self.old_session + self.request.user = self.old_user + self.in_pwg_session = False + + def save_to_cookie(self, response): + assert self.in_pwg_session + self.session_manager.save_session_to_cookie(self.request.session, + self.request, response) diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py new file mode 100644 index 00000000..ca723189 --- /dev/null +++ b/mediagoblin/plugins/piwigo/views.py @@ -0,0 +1,249 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 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 logging +import re +from os.path import splitext +import shutil + +from werkzeug.exceptions import MethodNotAllowed, BadRequest, NotImplemented +from werkzeug.wrappers import BaseResponse + +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.user_pages.lib import add_media_to_collection +from mediagoblin.db.models import Collection + +from .tools import CmdTable, response_xml, check_form, \ + PWGSession, PwgNamedArray, PwgError +from .forms import AddSimpleForm, AddForm + + +_log = logging.getLogger(__name__) + + +@CmdTable("pwg.session.login", True) +def pwg_login(request): + username = request.form.get("username") + password = request.form.get("password") + user = check_login_simple(username, password) + if not user: + return PwgError(999, 'Invalid username/password') + request.session["user_id"] = user.id + request.session.save() + return True + + +@CmdTable("pwg.session.logout") +def pwg_logout(request): + _log.info("Logout") + request.session.delete() + return True + + +@CmdTable("pwg.getVersion") +def pwg_getversion(request): + return "2.5.0 (MediaGoblin)" + + +@CmdTable("pwg.session.getStatus") +def pwg_session_getStatus(request): + if request.user: + username = request.user.username + else: + username = "guest" + return {'username': username} + + +@CmdTable("pwg.categories.getList") +def pwg_categories_getList(request): + catlist = [{'id': -29711, + 'uppercats': "-29711", + 'name': "All my images"}] + + if request.user: + collections = Collection.query.filter_by( + get_creator=request.user).order_by(Collection.title) + + for c in collections: + catlist.append({'id': c.id, + 'uppercats': str(c.id), + 'name': c.title, + 'comment': c.description + }) + + return { + 'categories': PwgNamedArray( + catlist, + 'category', + ( + 'id', + 'url', + 'nb_images', + 'total_nb_images', + 'nb_categories', + 'date_last', + 'max_date_last', + ) + ) + } + + +@CmdTable("pwg.images.exist") +def pwg_images_exist(request): + return {} + + +@CmdTable("pwg.images.addSimple", True) +def pwg_images_addSimple(request): + form = AddSimpleForm(request.form) + if not form.validate(): + _log.error("addSimple: form failed") + raise BadRequest() + dump = [] + for f in form: + dump.append("%s=%r" % (f.name, f.data)) + _log.info("addSimple: %r %s %r", request.form, " ".join(dump), + request.files) + + 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)} + + +md5sum_matcher = re.compile(r"^[0-9a-fA-F]{32}$") + + +def fetch_md5(request, parm_name, optional_parm=False): + val = request.form.get(parm_name) + if (val is None) and (not optional_parm): + _log.error("Parameter %s missing", parm_name) + raise BadRequest("Parameter %s missing" % parm_name) + if not md5sum_matcher.match(val): + _log.error("Parameter %s=%r has no valid md5 value", parm_name, val) + raise BadRequest("Parameter %s is not md5" % parm_name) + return val + + +@CmdTable("pwg.images.addChunk", True) +def pwg_images_addChunk(request): + o_sum = fetch_md5(request, 'original_sum') + typ = request.form.get('type') + pos = request.form.get('position') + data = request.form.get('data') + + # Validate params: + pos = int(pos) + if not typ in ("file", "thumb"): + _log.error("type %r not allowed for now", typ) + return False + + _log.info("addChunk for %r, type %r, position %d, len: %d", + o_sum, typ, pos, len(data)) + if typ == "thumb": + _log.info("addChunk: Ignoring thumb, because we create our own") + return True + + return True + + +@CmdTable("pwg.images.add", True) +def pwg_images_add(request): + _log.info("add: %r", request.form) + form = AddForm(request.form) + check_form(form) + + return {'image_id': 123456, 'url': ''} + + +@csrf_exempt +def ws_php(request): + if request.method not in ("GET", "POST"): + _log.error("Method %r not supported", request.method) + raise MethodNotAllowed() + + func = CmdTable.find_func(request) + if not func: + _log.warn("wsphp: Unhandled %s %r %r", request.method, + request.args, request.form) + raise NotImplemented() + + with PWGSession(request) as session: + result = func(request) + + if isinstance(result, BaseResponse): + return result + + response = response_xml(result) + session.save_to_cookie(response) + + return response diff --git a/mediagoblin/plugins/raven/README.rst b/mediagoblin/plugins/raven/README.rst index de5fd20d..4006060d 100644 --- a/mediagoblin/plugins/raven/README.rst +++ b/mediagoblin/plugins/raven/README.rst @@ -4,6 +4,8 @@ .. _raven-setup: +Warning: this plugin is somewhat experimental. + Set up the raven plugin ======================= |