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/tools.py20
-rw-r--r--mediagoblin/plugins/api/views.py66
-rw-r--r--mediagoblin/plugins/geolocation/__init__.py35
-rw-r--r--mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html59
-rw-r--r--mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map_js_head.html25
-rw-r--r--mediagoblin/plugins/httpapiauth/__init__.py5
-rw-r--r--mediagoblin/plugins/oauth/README.rst4
-rw-r--r--mediagoblin/plugins/oauth/__init__.py2
-rw-r--r--mediagoblin/plugins/oauth/forms.py9
-rw-r--r--mediagoblin/plugins/oauth/migrations.py128
-rw-r--r--mediagoblin/plugins/oauth/models.py87
-rw-r--r--mediagoblin/plugins/oauth/templates/oauth/authorize.html2
-rw-r--r--mediagoblin/plugins/oauth/tools.py73
-rw-r--r--mediagoblin/plugins/oauth/views.py161
-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.py152
-rw-r--r--mediagoblin/plugins/piwigo/views.py183
-rw-r--r--mediagoblin/plugins/raven/README.rst17
-rw-r--r--mediagoblin/plugins/raven/__init__.py92
-rw-r--r--mediagoblin/plugins/trim_whitespace/README.rst25
-rw-r--r--mediagoblin/plugins/trim_whitespace/__init__.py73
24 files changed, 1154 insertions, 177 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/tools.py b/mediagoblin/plugins/api/tools.py
index ecc50364..92411f4b 100644
--- a/mediagoblin/plugins/api/tools.py
+++ b/mediagoblin/plugins/api/tools.py
@@ -18,9 +18,9 @@ import logging
import json
from functools import wraps
-from webob import exc, Response
from urlparse import urljoin
-
+from werkzeug.exceptions import Forbidden
+from werkzeug.wrappers import Response
from mediagoblin import mg_globals
from mediagoblin.tools.pluginapi import PluginManager
from mediagoblin.storage.filestorage import BasicFileStorage
@@ -54,23 +54,23 @@ class Auth(object):
def json_response(serializable, _disable_cors=False, *args, **kw):
'''
- Serializes a json objects and returns a webob.Response object with the
+ Serializes a json objects and returns a werkzeug Response object with the
serialized value as the response body and Content-Type: application/json.
:param serializable: A json-serializable object
Any extra arguments and keyword arguments are passed to the
- webob.Response.__init__ method.
+ Response.__init__ method.
'''
- response = Response(json.dumps(serializable), *args, **kw)
- response.headers['Content-Type'] = 'application/json'
+ response = Response(json.dumps(serializable), *args, content_type='application/json', **kw)
if not _disable_cors:
cors_headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'}
- response.headers.update(cors_headers)
+ for key, value in cors_headers.iteritems():
+ response.headers.set(key, value)
return response
@@ -136,14 +136,14 @@ def api_auth(controller):
auth_candidates = []
for auth in PluginManager().get_hook_callables('auth'):
- _log.debug('Plugin auth: {0}'.format(auth))
if auth.trigger(request):
+ _log.debug('{0} believes it is capable of authenticating this request.'.format(auth))
auth_candidates.append(auth)
# If we can't find any authentication methods, we should not let them
# pass.
if not auth_candidates:
- return exc.HTTPForbidden()
+ raise Forbidden()
# For now, just select the first one in the list
auth = auth_candidates[0]
@@ -157,7 +157,7 @@ def api_auth(controller):
'status': 403,
'errors': auth.errors})
- return exc.HTTPForbidden()
+ raise Forbidden()
return controller(request, *args, **kw)
diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py
index a1b1bcac..fde76fe4 100644
--- a/mediagoblin/plugins/api/views.py
+++ b/mediagoblin/plugins/api/views.py
@@ -16,22 +16,18 @@
import json
import logging
-import uuid
from os.path import splitext
-from webob import exc, Response
-from werkzeug.utils import secure_filename
-from werkzeug.datastructures import FileStorage
-from celery import registry
+from werkzeug.exceptions import BadRequest, Forbidden
+from werkzeug.wrappers import Response
-from mediagoblin.db.util import ObjectId
from mediagoblin.decorators import require_active_login
-from mediagoblin.processing import mark_entry_failed
-from mediagoblin.processing.task import ProcessMedia
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 check_file_field, prepare_queue_task, \
+ run_process_media
_log = logging.getLogger(__name__)
@@ -47,20 +43,17 @@ def post_entry(request):
if request.method != 'POST':
_log.debug('Must POST against post_entry')
- return exc.HTTPBadRequest()
+ 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')
- return exc.HTTPBadRequest()
+ raise BadRequest()
media_file = request.files['file']
media_type, media_manager = sniff_media(media_file)
entry = request.db.MediaEntry()
- entry.id = ObjectId()
entry.media_type = unicode(media_type)
entry.title = unicode(request.form.get('title')
or splitext(media_file.filename)[0])
@@ -72,28 +65,14 @@ def post_entry(request):
entry.generate_slug()
- task_id = unicode(uuid.uuid4())
-
- # Now store generate the queueing related filename
- queue_filepath = request.app.queue_store.get_unique_filepath(
- ['media_entries',
- task_id,
- secure_filename(media_file.filename)])
-
# queue appropriately
- queue_file = request.app.queue_store.get_file(
- queue_filepath, 'wb')
+ queue_file = prepare_queue_task(request.app, entry, media_file.filename)
with queue_file:
queue_file.write(request.files['file'].stream.read())
- # Add queued filename to the entry
- entry.queued_media_file = queue_filepath
-
- entry.queued_task_id = task_id
-
# Save now so we have this data before kicking off processing
- entry.save(validate=True)
+ entry.save()
if request.form.get('callback_url'):
metadata = request.db.ProcessingMetaData()
@@ -105,36 +84,23 @@ def post_entry(request):
#
# (... don't change entry after this point to avoid race
# conditions with changes to the document via processing code)
- process_media = registry.tasks[ProcessMedia.name]
- try:
- process_media.apply_async(
- [unicode(entry._id)], {},
- task_id=task_id)
- except BaseException as e:
- # The purpose of this section is because when running in "lazy"
- # or always-eager-with-exceptions-propagated celery mode that
- # the failure handling won't happen on Celery end. Since we
- # expect a lot of users to run things in this way we have to
- # capture stuff here.
- #
- # ... not completely the diaper pattern because the
- # exception is re-raised :)
- mark_entry_failed(entry._id, e)
- # re-raise the exception
- raise
+ feed_url = request.urlgen(
+ 'mediagoblin.user_pages.atom_feed',
+ qualified=True, user=request.user.username)
+ run_process_media(entry, feed_url)
return json_response(get_entry_serializable(entry, request.urlgen))
@api_auth
+@require_active_login
def api_test(request):
- if not request.user:
- return exc.HTTPForbidden()
-
user_data = {
'username': request.user.username,
'email': request.user.email}
+ # TODO: This is the *only* thing using Response() here, should that
+ # not simply use json_response()?
return Response(json.dumps(user_data))
diff --git a/mediagoblin/plugins/geolocation/__init__.py b/mediagoblin/plugins/geolocation/__init__.py
new file mode 100644
index 00000000..5d14590e
--- /dev/null
+++ b/mediagoblin/plugins/geolocation/__init__.py
@@ -0,0 +1,35 @@
+# 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.tools import pluginapi
+import os
+
+PLUGIN_DIR = os.path.dirname(__file__)
+
+def setup_plugin():
+ config = pluginapi.get_config('mediagoblin.plugins.geolocation')
+
+ # Register the template path.
+ pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
+
+ pluginapi.register_template_hooks(
+ {"image_sideinfo": "mediagoblin/plugins/geolocation/map.html",
+ "image_head": "mediagoblin/plugins/geolocation/map_js_head.html"})
+
+
+hooks = {
+ 'setup': setup_plugin
+ }
diff --git a/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html
new file mode 100644
index 00000000..70f837ff
--- /dev/null
+++ b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html
@@ -0,0 +1,59 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#}
+
+{% block geolocation_map %}
+ {% if media.media_data.gps_latitude is defined
+ and media.media_data.gps_latitude
+ and media.media_data.gps_longitude is defined
+ and media.media_data.gps_longitude %}
+ <h3>{% trans %}Location{% endtrans %}</h3>
+ <div>
+ {%- set lon = media.media_data.gps_longitude %}
+ {%- set lat = media.media_data.gps_latitude %}
+ {%- set osm_url = "http://openstreetmap.org/?mlat={lat}&mlon={lon}".format(lat=lat, lon=lon) %}
+ <div id="tile-map" style="width: 100%; height: 196px;">
+ <input type="hidden" id="gps-longitude"
+ value="{{ lon }}" />
+ <input type="hidden" id="gps-latitude"
+ value="{{ lat }}" />
+ </div>
+ <script> <!-- pop up full OSM license when clicked -->
+ $(document).ready(function(){
+ $("#osm_license_link").click(function () {
+ $("#osm_attrib").slideToggle("slow");
+ });
+ });
+ </script>
+ <div id="osm_attrib" class="hidden"><ul><li>
+ Data &copy;<a
+ href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>
+ contributors
+ </li><li>Imaging &copy;<a
+ href="http://mapquest.com">MapQuest</a></li><li>Maps powered by
+ <a href="http://leafletjs.com/"> Leaflet</a></li></ul>
+ </div>
+ <p>
+ <small>
+ {% trans -%}
+ View on <a href="{{ osm_url }}">OpenStreetMap</a>
+ {%- endtrans %}
+ </small>
+ </p>
+ </div>
+ {% endif %}
+{% endblock %}
diff --git a/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map_js_head.html b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map_js_head.html
new file mode 100644
index 00000000..aca0f730
--- /dev/null
+++ b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map_js_head.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/>.
+#}
+
+<link rel="stylesheet"
+ href="{{ request.staticdirect('/extlib/leaflet/leaflet.css') }}" />
+
+<script type="text/javascript"
+ src="{{ request.staticdirect('/extlib/leaflet/leaflet.js') }}"></script>
+<script type="text/javascript"
+ src="{{ request.staticdirect('/js/geolocation-map.js') }}"></script>
diff --git a/mediagoblin/plugins/httpapiauth/__init__.py b/mediagoblin/plugins/httpapiauth/__init__.py
index d3d2065e..99b6a4b0 100644
--- a/mediagoblin/plugins/httpapiauth/__init__.py
+++ b/mediagoblin/plugins/httpapiauth/__init__.py
@@ -15,9 +15,8 @@
# 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.plugins.api.tools import Auth
@@ -41,7 +40,7 @@ class HTTPAuth(Auth):
return False
user = request.db.User.query.filter_by(
- username=request.authorization['username']).first()
+ username=unicode(request.authorization['username'])).first()
if user.check_login(request.authorization['password']):
request.user = user
diff --git a/mediagoblin/plugins/oauth/README.rst b/mediagoblin/plugins/oauth/README.rst
index 405a67e2..753b180f 100644
--- a/mediagoblin/plugins/oauth/README.rst
+++ b/mediagoblin/plugins/oauth/README.rst
@@ -7,6 +7,10 @@
Development has been entirely focused on Making It Work(TM). Use this
plugin with caution.
+ Additionally, this and the API may break... consider it pre-alpha.
+ There's also a known issue that the OAuth client doesn't do
+ refresh tokens so this might result in issues for users.
+
The OAuth plugin enables third party web applications to authenticate as one or
more GNU MediaGoblin users in a safe way in order retrieve, create and update
content stored on the GNU MediaGoblin instance.
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 2a956dad..5edd992a 100644
--- a/mediagoblin/plugins/oauth/forms.py
+++ b/mediagoblin/plugins/oauth/forms.py
@@ -19,14 +19,13 @@ 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):
- client_id = wtforms.HiddenField(_(u'Client ID'),
- [wtforms.validators.Required()])
- next = wtforms.HiddenField(_(u'Next URL'),
- [wtforms.validators.Required()])
+ client_id = wtforms.HiddenField(u'',
+ validators=[wtforms.validators.Required()])
+ next = wtforms.HiddenField(u'', validators=[wtforms.validators.Required()])
allow = wtforms.SubmitField(_(u'Allow'))
deny = wtforms.SubmitField(_(u'Deny'))
diff --git a/mediagoblin/plugins/oauth/migrations.py b/mediagoblin/plugins/oauth/migrations.py
index f2af3907..d7b89da3 100644
--- a/mediagoblin/plugins/oauth/migrations.py
+++ b/mediagoblin/plugins/oauth/migrations.py
@@ -14,16 +14,109 @@
# 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 MetaData, Table
+from datetime import datetime, timedelta
+from sqlalchemy import (MetaData, Table, Column,
+ Integer, Unicode, Enum, DateTime, ForeignKey)
+from sqlalchemy.ext.declarative import declarative_base
-from mediagoblin.db.sql.util import RegisterMigration
+from mediagoblin.db.migration_tools import RegisterMigration
+from mediagoblin.db.models import User
-from mediagoblin.plugins.oauth.models import OAuthClient, OAuthToken, \
- OAuthUserClient, OAuthCode
MIGRATIONS = {}
+class OAuthClient_v0(declarative_base()):
+ __tablename__ = 'oauth__client'
+
+ id = Column(Integer, primary_key=True)
+ created = Column(DateTime, nullable=False,
+ default=datetime.now)
+
+ name = Column(Unicode)
+ description = Column(Unicode)
+
+ identifier = Column(Unicode, unique=True, index=True)
+ secret = Column(Unicode, index=True)
+
+ owner_id = Column(Integer, ForeignKey(User.id))
+ redirect_uri = Column(Unicode)
+
+ type = Column(Enum(
+ u'confidential',
+ u'public',
+ name=u'oauth__client_type'))
+
+
+class OAuthUserClient_v0(declarative_base()):
+ __tablename__ = 'oauth__user_client'
+ id = Column(Integer, primary_key=True)
+
+ user_id = Column(Integer, ForeignKey(User.id))
+ client_id = Column(Integer, ForeignKey(OAuthClient_v0.id))
+
+ state = Column(Enum(
+ u'approved',
+ u'rejected',
+ name=u'oauth__relation_state'))
+
+
+class OAuthToken_v0(declarative_base()):
+ __tablename__ = 'oauth__tokens'
+
+ id = Column(Integer, primary_key=True)
+ created = Column(DateTime, nullable=False,
+ 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)
+
+ user_id = Column(Integer, ForeignKey(User.id), nullable=False,
+ index=True)
+
+ client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False)
+
+ def __repr__(self):
+ return '<{0} #{1} expires {2} [{3}, {4}]>'.format(
+ self.__class__.__name__,
+ self.id,
+ self.expires.isoformat(),
+ self.user,
+ self.client)
+
+
+class OAuthCode_v0(declarative_base()):
+ __tablename__ = 'oauth__codes'
+
+ id = Column(Integer, primary_key=True)
+ created = Column(DateTime, nullable=False,
+ default=datetime.now)
+ expires = Column(DateTime, nullable=False,
+ default=lambda: datetime.now() + timedelta(minutes=5))
+ code = Column(Unicode, index=True)
+
+ user_id = Column(Integer, ForeignKey(User.id), nullable=False,
+ index=True)
+
+ 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)
@@ -38,9 +131,28 @@ def remove_and_replace_token_and_code(db):
code_table.drop()
- OAuthClient.__table__.create(db.bind)
- OAuthUserClient.__table__.create(db.bind)
- OAuthToken.__table__.create(db.bind)
- OAuthCode.__table__.create(db.bind)
+ OAuthClient_v0.__table__.create(db.bind)
+ OAuthUserClient_v0.__table__.create(db.bind)
+ OAuthToken_v0.__table__.create(db.bind)
+ 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 7e247c1a..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.sql.base import Base
-from mediagoblin.db.sql.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/templates/oauth/authorize.html b/mediagoblin/plugins/oauth/templates/oauth/authorize.html
index 647fa41f..8a00c925 100644
--- a/mediagoblin/plugins/oauth/templates/oauth/authorize.html
+++ b/mediagoblin/plugins/oauth/templates/oauth/authorize.html
@@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#}
{% extends "mediagoblin/base.html" %}
-{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+{% import "mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.plugins.oauth.authorize_client') }}"
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 cf605fd2..d6fd314f 100644
--- a/mediagoblin/plugins/oauth/views.py
+++ b/mediagoblin/plugins/oauth/views.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
@@ -15,22 +16,21 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
-import json
-from webob import exc, Response
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 exc.HTTPBadRequest()
+ _log.error('No such client id as received from client authorization \
+form.')
+ raise BadRequest()
if form.validate():
relation = OAuthUserClient()
@@ -105,11 +102,11 @@ def authorize_client(request):
elif form.deny.data:
relation.state = u'rejected'
else:
- return exc.HTTPBadRequest
+ raise BadRequest()
relation.save()
- return exc.HTTPFound(location=form.next.data)
+ return redirect(request, location=form.next.data)
return render_to_response(
request,
@@ -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()
@@ -162,7 +158,7 @@ def authorize(request, client):
_log.debug('Redirecting to {0}'.format(redirect_uri))
- return exc.HTTPFound(location=redirect_uri)
+ return redirect(request, location=redirect_uri)
else:
# Show prompt to allow client to access data
# - on accept: send the user agent back to the redirect_uri with the
@@ -180,56 +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()
-
- access_token_data = {
- 'access_token': token.token,
- 'token_type': 'bearer',
- 'expires_in': int(
- round(
- (token.expires - datetime.now()).total_seconds()))}
- 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/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..18cbd5c5
--- /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(False),
+ _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..400be615
--- /dev/null
+++ b/mediagoblin/plugins/piwigo/tools.py
@@ -0,0 +1,152 @@
+# 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 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__)
+
+
+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")
+ _fill_element(r, result)
+ return Response(ET.tostring(r, encoding="utf-8", xml_declaration=True),
+ mimetype='text/xml')
+
+
+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.error):
+ _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..b59247ad
--- /dev/null
+++ b/mediagoblin/plugins/piwigo/views.py
@@ -0,0 +1,183 @@
+# 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 werkzeug.exceptions import MethodNotAllowed, BadRequest, NotImplemented
+from werkzeug.wrappers import BaseResponse
+
+from mediagoblin.meddleware.csrf import csrf_exempt
+from mediagoblin.submit.lib import check_file_field
+from mediagoblin.auth.lib import fake_login_attempt
+from .tools import CmdTable, PwgNamedArray, response_xml, check_form, \
+ PWGSession
+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")
+ _log.debug("Login for %r/%r...", username, password)
+ user = request.db.User.query.filter_by(username=username).first()
+ if not user:
+ _log.info("User %r not found", username)
+ fake_login_attempt()
+ return False
+ if not user.check_login(password):
+ _log.warn("Wrong password for %r", username)
+ return False
+ _log.info("Logging %r in", username)
+ 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"},)
+ 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("addimple: %r %s %r", request.form, " ".join(dump), request.files)
+
+ if not check_file_field(request, 'image'):
+ raise BadRequest()
+
+ return {'image_id': 123456, 'url': ''}
+
+
+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
new file mode 100644
index 00000000..4006060d
--- /dev/null
+++ b/mediagoblin/plugins/raven/README.rst
@@ -0,0 +1,17 @@
+==============
+ raven plugin
+==============
+
+.. _raven-setup:
+
+Warning: this plugin is somewhat experimental.
+
+Set up the raven plugin
+=======================
+
+1. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section::
+
+ [[mediagoblin.plugins.raven]]
+ sentry_dsn = <YOUR SENTRY DSN>
+ # Logging is very high-volume, set to 0 if you want to turn off logging
+ setup_logging = 1
diff --git a/mediagoblin/plugins/raven/__init__.py b/mediagoblin/plugins/raven/__init__.py
new file mode 100644
index 00000000..8cfaed0a
--- /dev/null
+++ b/mediagoblin/plugins/raven/__init__.py
@@ -0,0 +1,92 @@
+# 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 logging
+
+from mediagoblin.tools import pluginapi
+
+_log = logging.getLogger(__name__)
+
+
+def get_client():
+ from raven import Client
+ config = pluginapi.get_config('mediagoblin.plugins.raven')
+
+ sentry_dsn = config.get('sentry_dsn')
+
+ client = None
+
+ if sentry_dsn:
+ _log.info('Setting up raven from plugin config: {0}'.format(
+ sentry_dsn))
+ client = Client(sentry_dsn)
+ elif os.environ.get('SENTRY_DSN'):
+ _log.info('Setting up raven from SENTRY_DSN environment variable: {0}'\
+ .format(os.environ.get('SENTRY_DSN')))
+ client = Client() # Implicitly looks for SENTRY_DSN
+
+ if not client:
+ _log.error('Could not set up client, missing sentry DSN')
+ return None
+
+ return client
+
+
+def setup_celery():
+ from raven.contrib.celery import register_signal
+
+ client = get_client()
+
+ register_signal(client)
+
+
+def setup_logging():
+ config = pluginapi.get_config('mediagoblin.plugins.raven')
+
+ conf_setup_logging = False
+ if config.get('setup_logging'):
+ conf_setup_logging = bool(int(config.get('setup_logging')))
+
+ if not conf_setup_logging:
+ return
+
+ from raven.handlers.logging import SentryHandler
+ from raven.conf import setup_logging
+
+ client = get_client()
+
+ _log.info('Setting up raven logging handler')
+
+ setup_logging(SentryHandler(client))
+
+
+def wrap_wsgi(app):
+ from raven.middleware import Sentry
+
+ client = get_client()
+
+ _log.info('Attaching raven middleware...')
+
+ return Sentry(app, client)
+
+
+hooks = {
+ 'setup': setup_logging,
+ 'wrap_wsgi': wrap_wsgi,
+ 'celery_logging_setup': setup_logging,
+ 'celery_setup': setup_celery,
+ }
diff --git a/mediagoblin/plugins/trim_whitespace/README.rst b/mediagoblin/plugins/trim_whitespace/README.rst
new file mode 100644
index 00000000..b55ce35e
--- /dev/null
+++ b/mediagoblin/plugins/trim_whitespace/README.rst
@@ -0,0 +1,25 @@
+=======================
+ Trim whitespace plugin
+=======================
+
+Mediagoblin templates are written with 80 char limit for better
+readability. However that means that the html output is very verbose
+containing LOTS of whitespace. This plugin inserts a Middleware that
+filters out whitespace from the returned HTML in the Response() objects.
+
+Simply enable this plugin by putting it somewhere where python can reach it and put it's path into the [plugins] section of your mediagoblin.ini or mediagoblin_local.ini like for example this:
+
+ [plugins]
+ [[mediagoblin.plugins.trim_whitespace]]
+
+There is no further configuration required. If this plugin is enabled,
+all text/html documents should not have lots of whitespace in between
+elements, although it does a very naive filtering right now (just keep
+the first whitespace and delete all subsequent ones).
+
+Nonetheless, it is a useful plugin that might serve as inspiration for
+other plugin writers.
+
+It was originally conceived by Sebastian Spaeth. It is licensed under
+the GNU AGPL v3 (or any later version) license.
+
diff --git a/mediagoblin/plugins/trim_whitespace/__init__.py b/mediagoblin/plugins/trim_whitespace/__init__.py
new file mode 100644
index 00000000..3da1e8b4
--- /dev/null
+++ b/mediagoblin/plugins/trim_whitespace/__init__.py
@@ -0,0 +1,73 @@
+# 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 __future__ import unicode_literals
+import logging
+import re
+
+from mediagoblin import meddleware
+
+_log = logging.getLogger(__name__)
+
+class TrimWhiteSpaceMeddleware(meddleware.BaseMeddleware):
+ _setup_plugin_called = 0
+ RE_MULTI_WHITESPACE = re.compile(b'(\s)\s+', re.M)
+
+ def process_response(self, request, response):
+ """Perform very naive html tidying by removing multiple whitespaces"""
+ # werkzeug.BaseResponse has no content_type attr, this comes via
+ # werkzeug.wrappers.CommonRequestDescriptorsMixin (part of
+ # wrappers.Response)
+ if getattr(response ,'content_type', None) != 'text/html':
+ return
+
+ # This is a tad more complex than needed to be able to handle
+ # response.data and response.body, depending on whether we have
+ # a werkzeug Resonse or a webob one. Let's kill webob soon!
+ if hasattr(response, 'body') and not hasattr(response, 'data'):
+ # Old-style webob Response object.
+ # TODO: Remove this once we transition away from webob
+ resp_attr = 'body'
+ else:
+ resp_attr = 'data'
+ # Don't flatten iterator to list when we fudge the response body
+ # (see werkzeug.Response documentation)
+ response.implicit_sequence_conversion = False
+
+ # Set the tidied text. Very naive tidying for now, just strip all
+ # subsequent whitespaces (this preserves most newlines)
+ setattr(response, resp_attr, re.sub(
+ TrimWhiteSpaceMeddleware.RE_MULTI_WHITESPACE, br'\1',
+ getattr(response, resp_attr)))
+
+ @classmethod
+ def setup_plugin(cls):
+ """Set up this meddleware as a plugin during 'setup' hook"""
+ global _log
+ if cls._setup_plugin_called:
+ _log.info('Trim whitespace plugin was already set up.')
+ return
+
+ _log.debug('Trim whitespace plugin set up.')
+ cls._setup_plugin_called += 1
+
+ # Append ourselves to the list of enabled Meddlewares
+ meddleware.ENABLED_MEDDLEWARE.append(
+ '{0}:{1}'.format(cls.__module__, cls.__name__))
+
+
+hooks = {
+ 'setup': TrimWhiteSpaceMeddleware.setup_plugin
+ }