aboutsummaryrefslogtreecommitdiffstats
path: root/mediagoblin/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'mediagoblin/plugins')
-rw-r--r--mediagoblin/plugins/api/__init__.py4
-rw-r--r--mediagoblin/plugins/api/views.py12
-rw-r--r--mediagoblin/plugins/basic_auth/__init__.py88
-rw-r--r--mediagoblin/plugins/basic_auth/forms.py46
-rw-r--r--mediagoblin/plugins/basic_auth/tools.py84
-rw-r--r--mediagoblin/plugins/httpapiauth/__init__.py10
-rw-r--r--mediagoblin/plugins/oauth/__init__.py2
-rw-r--r--mediagoblin/plugins/oauth/forms.py2
-rw-r--r--mediagoblin/plugins/oauth/migrations.py34
-rw-r--r--mediagoblin/plugins/oauth/models.py87
-rw-r--r--mediagoblin/plugins/oauth/tools.py73
-rw-r--r--mediagoblin/plugins/oauth/views.py158
-rw-r--r--mediagoblin/plugins/openid/__init__.py123
-rw-r--r--mediagoblin/plugins/openid/forms.py41
-rw-r--r--mediagoblin/plugins/openid/models.py65
-rw-r--r--mediagoblin/plugins/openid/store.py127
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/add.html44
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/delete.html43
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/edit_link.html25
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html65
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html25
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/register_link.html27
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/request_form.html24
-rw-r--r--mediagoblin/plugins/openid/views.py404
-rw-r--r--mediagoblin/plugins/piwigo/README.rst23
-rw-r--r--mediagoblin/plugins/piwigo/__init__.py42
-rw-r--r--mediagoblin/plugins/piwigo/forms.py44
-rw-r--r--mediagoblin/plugins/piwigo/tools.py165
-rw-r--r--mediagoblin/plugins/piwigo/views.py249
-rw-r--r--mediagoblin/plugins/raven/README.rst2
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 %} &mdash; {{ 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 %} &mdash; {{ 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 %} &mdash; {{ 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
=======================