Merge branch 'Federation'

This commit is contained in:
Jessica Tallon 2014-07-23 00:23:23 +01:00
commit aab6239477
25 changed files with 1389 additions and 228 deletions

155
docs/source/api/media.rst Normal file
View File

@ -0,0 +1,155 @@
.. MediaGoblin Documentation
Written in 2011, 2012 by MediaGoblin contributors
To the extent possible under law, the author(s) have dedicated all
copyright and related and neighboring rights to this software to
the public domain worldwide. This software is distributed without
any warranty.
You should have received a copy of the CC0 Public Domain
Dedication along with this software. If not, see
<http://creativecommons.org/publicdomain/zero/1.0/>.
.. info:: Currently only image uploading is supported.
===============
Uploading Media
===============
To use any the APIs mentioned in this document you will required :doc:`oauth`
Uploading and posting an media requiest you to make two to three requests:
1) Uploads the data to the server
2) Post media to feed
3) Update media to have title, description, license, etc. (optional)
These steps could be condenced in the future however currently this is how the
pump.io API works. There is currently an issue open, if you would like to change
how this works please contribute upstream: https://github.com/e14n/pump.io/issues/657
----------------------
Upload Media to Server
----------------------
To upload media you should use the URI `/api/user/<username>/uploads`.
A POST request should be made to the media upload URI submitting at least two header:
* `Content-Type` - This being a valid mimetype for the media.
* `Content-Length` - size in bytes of the media.
The media data should be submitted as POST data to the image upload URI.
You will get back a JSON encoded response which will look similiar to::
{
"updated": "2014-01-11T09:45:48Z",
"links": {
"self": {
"href": "https://<server>/image/4wiBUV1HT8GRqseyvX8m-w"
}
},
"fullImage": {
"url": "https://<server>//uploads/<username>/2014/1/11/V3cBMw.jpg",
"width": 505,
"height": 600
},
"replies": {
"url": "https://<server>//api/image/4wiBUV1HT8GRqseyvX8m-w/replies"
},
"image": {
"url": "https://<server>/uploads/<username>/2014/1/11/V3cBMw_thumb.jpg",
"width": 269,
"height": 320
},
"author": {
"preferredUsername": "<username>",
"displayName": "<username>",
"links": {
"activity-outbox": {
"href": "https://<server>/api/user/<username>/feed"
},
"self": {
"href": "https://<server>/api/user/<username>/profile"
},
"activity-inbox": {
"href": "https://<server>/api/user/<username>/inbox"
}
},
"url": "https://<server>/<username>",
"updated": "2013-08-14T10:01:21Z",
"id": "acct:<username>@<server>",
"objectType": "person"
},
"url": "https://<server>/<username>/image/4wiBUV1HT8GRqseyvX8m-w",
"published": "2014-01-11T09:45:48Z",
"id": "https://<server>/api/image/4wiBUV1HT8GRqseyvX8m-w",
"objectType": "image"
}
The main things in this response is `fullImage` which contains `url` (the URL
of the original image - i.e. fullsize) and `image` which contains `url` (the URL
of a thumbnail version).
.. warning:: Media which have been uploaded but not submitted to a feed will
periodically be deleted.
--------------
Submit to feed
--------------
This is submitting the media to appear on the website. This will create an
object in your feed which will then appear on the GNU MediaGoblin website so the
user and others can view and interact with the media.
The URL you need to POST to is `/api/user/<username>/feed`
You first should do a post to the feed URI with some of the information you got
back from the above request (which uploaded the media). The request should look
something like::
{
"verb": "post",
"object": {
"id": "https://<server>/api/image/6_K9m-2NQFi37je845c83w",
"objectType": "image"
}
}
.. warning:: Any other data submitted **will** be ignored
-------------------
Submitting Metadata
-------------------
Finally if you wish to set a title, description and license you will need to do
and update request to the endpoint, the following attributes can be submitted:
+--------------+---------------------------------------+-------------------+
| Name | Description | Required/Optional |
+==============+=======================================+===================+
| displayName | This is the title for the media | Optional |
+--------------+---------------------------------------+-------------------+
| content | This is the description for the media | Optional |
+--------------+---------------------------------------+-------------------+
| license | This is the license to be used | Optional |
+--------------+---------------------------------------+-------------------+
.. note:: license attribute is mediagoblin specific, pump.io does not support this attribute
The update request should look something similiar to::
{
"verb": "update",
"object": {
"displayName": "My super awesome image!",
"content": "The awesome image I took while backpacking to modor",
"license": "creativecommons.org/licenses/by-sa/3.0/",
"id": "https://<server>/api/image/6_K9m-2NQFi37je845c83w",
"objectType": "image"
}
}
.. warning:: Any other data submitted **will** be ignored.

View File

@ -0,0 +1,65 @@
.. MediaGoblin Documentation
Written in 2011, 2012 by MediaGoblin contributors
To the extent possible under law, the author(s) have dedicated all
copyright and related and neighboring rights to this software to
the public domain worldwide. This software is distributed without
any warranty.
You should have received a copy of the CC0 Public Domain
Dedication along with this software. If not, see
<http://creativecommons.org/publicdomain/zero/1.0/>.
Pump.io supports a number of different interactions that can happen against
media. Theser are commenting, liking/favoriting and (re-)sharing. Currently
MediaGoblin supports just commenting although other interactions will come at
a later date.
--------------
How to comment
--------------
.. warning:: Commenting on a comment currently is NOT supported.
Commenting is done by posting a comment activity to the users feed. The
activity should look similiar to::
{
"verb": "post",
"object": {
"objectType": "comment",
"inReplyTo": <media>
}
}
This is where `<media>` is the media object you have got with from the server.
----------------
Getting comments
----------------
The media object you get back should have a `replies` section. This should
be an object which contains the number of replies and if there are any (i.e.
number of replies > 0) then `items` will include an array of every item::
{
"totalItems": 2,
"items: [
{
"id": 1,
"objectType": "comment",
"content": "I'm a comment ^_^",
"author": <author user object>
},
{
"id": 4,
"objectType": "comment",
"content": "Another comment! Blimey!",
"author": <author user object>
}
],
"url": "http://some.server/api/images/1/comments/"
}

View File

@ -76,7 +76,7 @@ case "$selfname" in
lazycelery.sh)
MEDIAGOBLIN_CONFIG="${ini_file}" \
CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_celery \
$starter "$@"
$starter -B "$@"
;;
*) exit 1 ;;
esac

View File

@ -23,6 +23,10 @@ allow_registration = true
# Set to false to disable the ability for users to report offensive content
allow_reporting = true
# Frequency garbage collection will run (setting to 0 or false to disable)
# Setting units are minutes.
garbage_collection = 60
## Uncomment this to put some user-overriding templates here
# local_templates = %(here)s/user_dev/templates/

View File

@ -25,14 +25,14 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import and_
from migrate.changeset.constraint import UniqueConstraint
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
from mediagoblin.db.migration_tools import (
RegisterMigration, inspect_table, replace_table_hack)
from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
Privilege)
from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
Privilege)
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
MIGRATIONS = {}
@ -466,7 +466,6 @@ def create_oauth1_tables(db):
db.commit()
@RegisterMigration(15, MIGRATIONS)
def wants_notifications(db):
"""Add a wants_notifications field to User model"""
@ -660,8 +659,8 @@ def create_moderation_tables(db):
# admin, an active user or an inactive user ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
for admin_user in admin_users_ids:
admin_user_id = admin_user['id']
for privilege_id in [admin_privilege_id, uploader_privilege_id,
reporter_privilege_id, commenter_privilege_id,
for privilege_id in [admin_privilege_id, uploader_privilege_id,
reporter_privilege_id, commenter_privilege_id,
active_privilege_id]:
db.execute(user_privilege_assoc.insert().values(
core__privilege_id=admin_user_id,
@ -669,7 +668,7 @@ def create_moderation_tables(db):
for active_user in active_users_ids:
active_user_id = active_user['id']
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
commenter_privilege_id, active_privilege_id]:
db.execute(user_privilege_assoc.insert().values(
core__privilege_id=active_user_id,
@ -677,7 +676,7 @@ def create_moderation_tables(db):
for inactive_user in inactive_users_ids:
inactive_user_id = inactive_user['id']
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
commenter_privilege_id]:
db.execute(user_privilege_assoc.insert().values(
core__privilege_id=inactive_user_id,

View File

@ -202,6 +202,17 @@ class MediaEntryMixin(GenerateSlugMixin):
thumb_url = mg_globals.app.staticdirector(manager[u'default_thumb'])
return thumb_url
@property
def original_url(self):
""" Returns the URL for the original image
will return self.thumb_url if original url doesn't exist"""
if u"original" not in self.media_files:
return self.thumb_url
return mg_globals.app.public_store.file_url(
self.media_files[u"original"]
)
@cached_property
def media_manager(self):
"""Returns the MEDIA_MANAGER of the media's media_type

View File

@ -20,6 +20,7 @@ TODO: indexes on foreignkeys, where useful.
import logging
import datetime
import base64
from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
@ -136,6 +137,48 @@ class User(Base, UserMixin):
return UserBan.query.get(self.id) is not None
def serialize(self, request):
user = {
"id": "acct:{0}@{1}".format(self.username, request.host),
"preferredUsername": self.username,
"displayName": "{0}@{1}".format(self.username, request.host),
"objectType": "person",
"pump_io": {
"shared": False,
"followed": False,
},
"links": {
"self": {
"href": request.urlgen(
"mediagoblin.federation.user.profile",
username=self.username,
qualified=True
),
},
"activity-inbox": {
"href": request.urlgen(
"mediagoblin.federation.inbox",
username=self.username,
qualified=True
)
},
"activity-outbox": {
"href": request.urlgen(
"mediagoblin.federation.feed",
username=self.username,
qualified=True
)
},
},
}
if self.bio:
user.update({"summary": self.bio})
if self.url:
user.update({"url": self.url})
return user
class Client(Base):
"""
Model representing a client - Used for API Auth
@ -201,7 +244,6 @@ class NonceTimestamp(Base):
nonce = Column(Unicode, nullable=False, primary_key=True)
timestamp = Column(DateTime, nullable=False, primary_key=True)
class MediaEntry(Base, MediaEntryMixin):
"""
TODO: Consider fetching the media_files using join
@ -388,6 +430,87 @@ class MediaEntry(Base, MediaEntryMixin):
# pass through commit=False/True in kwargs
super(MediaEntry, self).delete(**kwargs)
@property
def objectType(self):
""" Converts media_type to pump-like type - don't use internally """
return self.media_type.split(".")[-1]
def serialize(self, request, show_comments=True):
""" Unserialize MediaEntry to object """
author = self.get_uploader
url = request.urlgen(
"mediagoblin.user_pages.media_home",
user=author.username,
media=self.slug,
qualified=True
)
context = {
"id": self.id,
"author": author.serialize(request),
"objectType": self.objectType,
"url": url,
"image": {
"url": request.host_url + self.thumb_url[1:],
},
"fullImage":{
"url": request.host_url + self.original_url[1:],
},
"published": self.created.isoformat(),
"updated": self.created.isoformat(),
"pump_io": {
"shared": False,
},
"links": {
"self": {
"href": request.urlgen(
"mediagoblin.federation.object",
objectType=self.objectType,
id=self.id,
qualified=True
),
},
}
}
if self.title:
context["displayName"] = self.title
if self.description:
context["content"] = self.description
if self.license:
context["license"] = self.license
if show_comments:
comments = [comment.serialize(request) for comment in self.get_comments()]
total = len(comments)
context["replies"] = {
"totalItems": total,
"items": comments,
"url": request.urlgen(
"mediagoblin.federation.object.comments",
objectType=self.objectType,
id=self.id,
qualified=True
),
}
return context
def unserialize(self, data):
""" Takes API objects and unserializes on existing MediaEntry """
if "displayName" in data:
self.title = data["displayName"]
if "content" in data:
self.description = data["content"]
if "license" in data:
self.license = data["license"]
return True
class FileKeynames(Base):
"""
@ -534,6 +657,37 @@ class MediaComment(Base, MediaCommentMixin):
lazy="dynamic",
cascade="all, delete-orphan"))
def serialize(self, request):
""" Unserialize to python dictionary for API """
media = MediaEntry.query.filter_by(id=self.media_entry).first()
author = self.get_author
context = {
"id": self.id,
"objectType": "comment",
"content": self.content,
"inReplyTo": media.serialize(request, show_comments=False),
"author": author.serialize(request)
}
return context
def unserialize(self, data):
""" Takes API objects and unserializes on existing comment """
# Do initial checks to verify the object is correct
required_attributes = ["content", "inReplyTo"]
for attr in required_attributes:
if attr not in data:
return False
# Validate inReplyTo has ID
if "id" not in data["inReplyTo"]:
return False
self.media_entry = data["inReplyTo"]["id"]
self.content = data["content"]
return True
class Collection(Base, CollectionMixin):
"""An 'album' or 'set' of media by a user.

View File

@ -22,7 +22,7 @@ from oauthlib.oauth1 import ResourceEndpoint
from mediagoblin import mg_globals as mgg
from mediagoblin import messages
from mediagoblin.db.models import MediaEntry, User, MediaComment
from mediagoblin.db.models import MediaEntry, User, MediaComment, AccessToken
from mediagoblin.tools.response import (
redirect, render_404,
render_user_banned, json_response)
@ -401,10 +401,10 @@ def oauth_required(controller):
request_validator = GMGRequestValidator()
resource_endpoint = ResourceEndpoint(request_validator)
valid, request = resource_endpoint.validate_protected_resource_request(
valid, r = resource_endpoint.validate_protected_resource_request(
uri=request.url,
http_method=request.method,
body=request.get_data(),
body=request.data,
headers=dict(request.headers),
)
@ -412,6 +412,13 @@ def oauth_required(controller):
error = "Invalid oauth prarameter."
return json_response({"error": error}, status=400)
# Fill user if not already
token = authorization[u"oauth_token"]
access_token = AccessToken.query.filter_by(token=token).first()
if access_token is not None and request.user is None:
user_id = access_token.user
request.user = User.query.filter_by(id=user_id).first()
return controller(request, *args, **kwargs)
return wrapper

View File

@ -13,11 +13,3 @@
#
# 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.routing import add_route
add_route('mediagoblin.webfinger.host_meta', '/.well-known/host-meta',
'mediagoblin.webfinger.views:host_meta')
add_route('mediagoblin.webfinger.xrd', '/webfinger/xrd',
'mediagoblin.webfinger.views:xrd')

View File

@ -0,0 +1,51 @@
# 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 functools import wraps
from mediagoblin.db.models import User
from mediagoblin.decorators import require_active_login
from mediagoblin.tools.response import json_response
def user_has_privilege(privilege_name):
"""
Requires that a user have a particular privilege in order to access a page.
In order to require that a user have multiple privileges, use this
decorator twice on the same view. This decorator also makes sure that the
user is not banned, or else it redirects them to the "You are Banned" page.
:param privilege_name A unicode object that is that represents
the privilege object. This object is
the name of the privilege, as assigned
in the Privilege.privilege_name column
"""
def user_has_privilege_decorator(controller):
@wraps(controller)
@require_active_login
def wrapper(request, *args, **kwargs):
user_id = request.user.id
if not request.user.has_privilege(privilege_name):
error = "User '{0}' needs '{1}' privilege".format(
request.user.username,
privilege_name
)
return json_response({"error": error}, status=403)
return controller(request, *args, **kwargs)
return wrapper
return user_has_privilege_decorator

View File

@ -0,0 +1,79 @@
# 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.routing import add_route
# Add user profile
add_route(
"mediagoblin.federation.user",
"/api/user/<string:username>/",
"mediagoblin.federation.views:user"
)
add_route(
"mediagoblin.federation.user.profile",
"/api/user/<string:username>/profile",
"mediagoblin.federation.views:profile"
)
# Inbox and Outbox (feed)
add_route(
"mediagoblin.federation.feed",
"/api/user/<string:username>/feed",
"mediagoblin.federation.views:feed"
)
add_route(
"mediagoblin.federation.user.uploads",
"/api/user/<string:username>/uploads",
"mediagoblin.federation.views:uploads"
)
add_route(
"mediagoblin.federation.inbox",
"/api/user/<string:username>/inbox",
"mediagoblin.federation.views:feed"
)
# object endpoints
add_route(
"mediagoblin.federation.object",
"/api/<string:objectType>/<string:id>",
"mediagoblin.federation.views:object"
)
add_route(
"mediagoblin.federation.object.comments",
"/api/<string:objectType>/<string:id>/comments",
"mediagoblin.federation.views:object_comments"
)
add_route(
"mediagoblin.webfinger.well-known.host-meta",
"/.well-known/host-meta",
"mediagoblin.federation.views:host_meta"
)
add_route(
"mediagoblin.webfinger.well-known.host-meta.json",
"/.well-known/host-meta.json",
"mediagoblin.federation.views:host_meta"
)
add_route(
"mediagoblin.webfinger.whoami",
"/api/whoami",
"mediagoblin.federation.views:whoami"
)

View File

@ -13,13 +13,37 @@
#
# 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/>.
'''
mediagoblin.webfinger_ provides an LRDD discovery service and
a web host meta information file
Links:
- `LRDD Discovery Draft
<http://tools.ietf.org/html/draft-hammer-discovery-06>`_.
- `RFC 6415 - Web Host Metadata
<http://tools.ietf.org/html/rfc6415>`_.
'''
import celery
import datetime
import logging
import pytz
from mediagoblin.db.models import MediaEntry
_log = logging.getLogger(__name__)
logging.basicConfig()
_log.setLevel(logging.DEBUG)
@celery.task()
def collect_garbage():
"""
Garbage collection to clean up media
This will look for all critera on models to clean
up. This is primerally written to clean up media that's
entered a erroneous state.
"""
_log.info("Garbage collection is running.")
now = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=1)
garbage = MediaEntry.query.filter(MediaEntry.created > now)
garbage = garbage.filter(MediaEntry.state == "unprocessed")
for entry in garbage.all():
_log.info("Garbage media found with ID '{0}'".format(entry.id))
entry.delete()

View File

@ -0,0 +1,380 @@
# 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 json
import io
import mimetypes
from werkzeug.datastructures import FileStorage
from mediagoblin.media_types import sniff_media
from mediagoblin.decorators import oauth_required
from mediagoblin.federation.decorators import user_has_privilege
from mediagoblin.db.models import User, MediaEntry, MediaComment
from mediagoblin.tools.response import redirect, json_response
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.submit.lib import new_upload_entry
@oauth_required
def profile(request, raw=False):
""" This is /api/user/<username>/profile - This will give profile info """
user = request.matchdict["username"]
requested_user = User.query.filter_by(username=user)
# check if the user exists
if requested_user is None:
error = "No such 'user' with id '{0}'".format(user)
return json_response({"error": error}, status=404)
user = requested_user[0]
if raw:
return (user, user.serialize(request))
# user profiles are public so return information
return json_response(user.serialize(request))
@oauth_required
def user(request):
""" This is /api/user/<username> - This will get the user """
user, user_profile = profile(request, raw=True)
data = {
"nickname": user.username,
"updated": user.created.isoformat(),
"published": user.created.isoformat(),
"profile": user_profile,
}
return json_response(data)
@oauth_required
@csrf_exempt
@user_has_privilege(u'uploader')
def uploads(request):
""" Endpoint for file uploads """
user = request.matchdict["username"]
requested_user = User.query.filter_by(username=user)
if requested_user is None:
error = "No such 'user' with id '{0}'".format(user)
return json_response({"error": error}, status=404)
request.user = requested_user[0]
if request.method == "POST":
# Wrap the data in the werkzeug file wrapper
if "Content-Type" not in request.headers:
error = "Must supply 'Content-Type' header to upload media."
return json_response({"error": error}, status=400)
mimetype = request.headers["Content-Type"]
filename = mimetypes.guess_all_extensions(mimetype)
filename = 'unknown' + filename[0] if filename else filename
file_data = FileStorage(
stream=io.BytesIO(request.data),
filename=filename,
content_type=mimetype
)
# Find media manager
media_type, media_manager = sniff_media(file_data, filename)
entry = new_upload_entry(request.user)
if hasattr(media_manager, "api_upload_request"):
return media_manager.api_upload_request(request, file_data, entry)
else:
return json_response({"error": "Not yet implemented"}, status=501)
return json_response({"error": "Not yet implemented"}, status=501)
@oauth_required
@csrf_exempt
def feed(request):
""" Handles the user's outbox - /api/user/<username>/feed """
user = request.matchdict["username"]
requested_user = User.query.filter_by(username=user)
# check if the user exists
if requested_user is None:
error = "No such 'user' with id '{0}'".format(user)
return json_response({"error": error}, status=404)
request.user = requested_user[0]
if request.data:
data = json.loads(request.data)
else:
data = {"verb": None, "object": {}}
if request.method == "POST" and data["verb"] == "post":
obj = data.get("object", None)
if obj is None:
error = {"error": "Could not find 'object' element."}
return json_response(error, status=400)
if obj.get("objectType", None) == "comment":
# post a comment
comment = MediaComment(author=request.user.id)
comment.unserialize(data["object"])
comment.save()
data = {"verb": "post", "object": comment.serialize(request)}
return json_response(data)
elif obj.get("objectType", None) == "image":
# Posting an image to the feed
media_id = int(data["object"]["id"])
media = MediaEntry.query.filter_by(id=media_id)
if media is None:
error = "No such 'image' with id '{0}'".format(id=media_id)
return json_response(error, status=404)
media = media.first()
if not media.unserialize(data["object"]):
error = "Invalid 'image' with id '{0}'".format(media_id)
return json_response({"error": error}, status=400)
media.save()
media.media_manager.api_add_to_feed(request, media)
return json_response({
"verb": "post",
"object": media.serialize(request)
})
elif obj.get("objectType", None) is None:
# They need to tell us what type of object they're giving us.
error = {"error": "No objectType specified."}
return json_response(error, status=400)
else:
# Oh no! We don't know about this type of object (yet)
error_message = "Unknown object type '{0}'.".format(
obj.get("objectType", None)
)
error = {"error": error_message}
return json_response(error, status=400)
elif request.method in ["PUT", "POST"] and data["verb"] == "update":
# Check we've got a valid object
obj = data.get("object", None)
if obj is None:
error = {"error": "Could not find 'object' element."}
return json_response(error, status=400)
if "objectType" not in obj:
error = {"error": "No objectType specified."}
return json_response(error, status=400)
if "id" not in obj:
error = {"error": "Object ID has not been specified."}
return json_response(error, status=400)
obj_id = obj["id"]
# Now try and find object
if obj["objectType"] == "comment":
comment = MediaComment.query.filter_by(id=obj_id)
if comment is None:
error = "No such 'comment' with id '{0}'.".format(obj_id)
return json_response({"error": error}, status=400)
comment = comment[0]
if not comment.unserialize(data["object"]):
error = "Invalid 'comment' with id '{0}'".format(obj_id)
return json_response({"error": error}, status=400)
comment.save()
activity = {
"verb": "update",
"object": comment.serialize(request),
}
return json_response(activity)
elif obj["objectType"] == "image":
image = MediaEntry.query.filter_by(id=obj_id)
if image is None:
error = "No such 'image' with the id '{0}'.".format(obj_id)
return json_response({"error": error}, status=400)
image = image[0]
if not image.unserialize(obj):
"Invalid 'image' with id '{0}'".format(obj_id)
return json_response({"error": error}, status=400)
image.save()
activity = {
"verb": "update",
"object": image.serialize(request),
}
return json_response(activity)
elif request.method != "GET":
# Currently unsupported
error = "Unsupported HTTP method {0}".format(request.method)
return json_response({"error": error}, status=501)
feed_url = request.urlgen(
"mediagoblin.federation.feed",
username=request.user.username,
qualified=True
)
feed = {
"displayName": "Activities by {user}@{host}".format(
user=request.user.username,
host=request.host
),
"objectTypes": ["activity"],
"url": feed_url,
"links": {
"first": {
"href": feed_url,
},
"self": {
"href": request.url,
},
"prev": {
"href": feed_url,
},
"next": {
"href": feed_url,
}
},
"author": request.user.serialize(request),
"items": [],
}
# Now lookup the user's feed.
for media in MediaEntry.query.all():
item = {
"verb": "post",
"object": media.serialize(request),
"actor": request.user.serialize(request),
"content": "{0} posted a picture".format(request.user.username),
"id": 1,
}
item["updated"] = item["object"]["updated"]
item["published"] = item["object"]["published"]
item["url"] = item["object"]["url"]
feed["items"].append(item)
feed["totalItems"] = len(feed["items"])
return json_response(feed)
@oauth_required
def object(request, raw_obj=False):
""" Lookup for a object type """
object_type = request.matchdict["objectType"]
try:
object_id = int(request.matchdict["id"])
except ValueError:
error = "Invalid object ID '{0}' for '{1}'".format(
request.matchdict["id"],
object_type
)
return json_response({"error": error}, status=400)
if object_type not in ["image"]:
error = "Unknown type: {0}".format(object_type)
# not sure why this is 404, maybe ask evan. Maybe 400?
return json_response({"error": error}, status=404)
media = MediaEntry.query.filter_by(id=object_id).first()
if media is None:
# no media found with that uuid
error = "Can't find '{0}' with ID '{1}'".format(
object_type,
object_id
)
return json_response({"error": error}, status=404)
if raw_obj:
return media
return json_response(media.serialize(request))
@oauth_required
def object_comments(request):
""" Looks up for the comments on a object """
media = object(request, raw_obj=True)
response = media
if isinstance(response, MediaEntry):
comments = response.serialize(request)
comments = comments.get("replies", {
"totalItems": 0,
"items": [],
"url": request.urlgen(
"mediagoblin.federation.object.comments",
objectType=media.objectType,
uuid=media.id,
qualified=True
)
})
comments["displayName"] = "Replies to {0}".format(comments["url"])
comments["links"] = {
"first": comments["url"],
"self": comments["url"],
}
response = json_response(comments)
return response
##
# Well known
##
def host_meta(request):
""" /.well-known/host-meta - provide URLs to resources """
links = []
links.append({
"ref": "registration_endpoint",
"href": request.urlgen(
"mediagoblin.oauth.client_register",
qualified=True
),
})
links.append({
"ref": "http://apinamespace.org/oauth/request_token",
"href": request.urlgen(
"mediagoblin.oauth.request_token",
qualified=True
),
})
links.append({
"ref": "http://apinamespace.org/oauth/authorize",
"href": request.urlgen(
"mediagoblin.oauth.authorize",
qualified=True
),
})
links.append({
"ref": "http://apinamespace.org/oauth/access_token",
"href": request.urlgen(
"mediagoblin.oauth.access_token",
qualified=True
),
})
return json_response({"links": links})
def whoami(request):
""" /api/whoami - HTTP redirect to API profile """
profile = request.urlgen(
"mediagoblin.federation.user.profile",
username=request.user.username,
qualified=True
)
return redirect(request, location=profile)

View File

@ -16,6 +16,7 @@
import os
import sys
import datetime
import logging
from celery import Celery
@ -58,6 +59,18 @@ def get_celery_settings_dict(app_config, global_config,
celery_settings['CELERY_ALWAYS_EAGER'] = True
celery_settings['CELERY_EAGER_PROPAGATES_EXCEPTIONS'] = True
# Garbage collection periodic task
frequency = app_config.get('garbage_collection', 60)
if frequency:
frequency = int(frequency)
celery_settings['CELERYBEAT_SCHEDULE'] = {
'garbage-collection': {
'task': 'mediagoblin.federation.task.garbage_collection',
'schedule': datetime.timedelta(minutes=frequency),
}
}
celery_settings['BROKER_HEARTBEAT'] = 1
return celery_settings

View File

@ -19,12 +19,14 @@ import logging
from mediagoblin.media_types import MediaManagerBase
from mediagoblin.media_types.image.processing import sniff_handler, \
ImageProcessingManager
from mediagoblin.tools.response import json_response
from mediagoblin.submit.lib import prepare_queue_task, run_process_media
from mediagoblin.notifications import add_comment_subscription
_log = logging.getLogger(__name__)
ACCEPTED_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "tiff"]
ACCEPTED_EXTENSIONS = ["jpe", "jpg", "jpeg", "png", "gif", "tiff"]
MEDIA_TYPE = 'mediagoblin.media_types.image'
@ -56,6 +58,37 @@ class ImageMediaManager(MediaManagerBase):
except (KeyError, ValueError):
return None
@staticmethod
def api_upload_request(request, file_data, entry):
""" This handles a image upload request """
# Use the same kind of method from mediagoblin/submit/views:submit_start
entry.media_type = unicode(MEDIA_TYPE)
entry.title = file_data.filename
entry.generate_slug()
queue_file = prepare_queue_task(request.app, entry, file_data.filename)
with queue_file:
queue_file.write(request.data)
entry.save()
return json_response(entry.serialize(request))
@staticmethod
def api_add_to_feed(request, entry):
""" Add media to Feed """
if entry.title:
# Shame we have to do this here but we didn't have the data in
# api_upload_request as no filename is usually specified.
entry.slug = None
entry.generate_slug()
feed_url = request.urlgen(
'mediagoblin.user_pages.atom_feed',
qualified=True, user=request.user.username)
run_process_media(entry, feed_url)
add_comment_subscription(request.user, entry)
return json_response(entry.serialize(request))
def get_media_type_and_manager(ext):
if ext in ACCEPTED_EXTENSIONS:

View File

@ -15,12 +15,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from oauthlib.common import Request
from oauthlib.oauth1 import RequestValidator
from oauthlib.oauth1 import RequestValidator
from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
class GMGRequestValidator(RequestValidator):
enforce_ssl = False
@ -63,14 +61,14 @@ class GMGRequestValidator(RequestValidator):
""" Currently a stub - called when making AccessTokens """
return list()
def validate_timestamp_and_nonce(self, client_key, timestamp,
nonce, request, request_token=None,
def validate_timestamp_and_nonce(self, client_key, timestamp,
nonce, request, request_token=None,
access_token=None):
nc = NonceTimestamp.query.filter_by(timestamp=timestamp, nonce=nonce)
nc = nc.first()
if nc is None:
return True
return False
def validate_client_key(self, client_key, request):
@ -78,7 +76,7 @@ class GMGRequestValidator(RequestValidator):
client = Client.query.filter_by(id=client_key).first()
if client is None:
return False
return True
def validate_access_token(self, client_key, token, request):
@ -119,14 +117,14 @@ class GMGRequest(Request):
"""
def __init__(self, request, *args, **kwargs):
"""
"""
:param request: werkzeug request object
any extra params are passed to oauthlib.common.Request object
"""
kwargs["uri"] = kwargs.get("uri", request.url)
kwargs["http_method"] = kwargs.get("http_method", request.method)
kwargs["body"] = kwargs.get("body", request.get_data())
kwargs["body"] = kwargs.get("body", request.data)
kwargs["headers"] = kwargs.get("headers", dict(request.headers))
super(GMGRequest, self).__init__(*args, **kwargs)

View File

@ -18,25 +18,25 @@ from mediagoblin.tools.routing import add_route
# client registration & oauth
add_route(
"mediagoblin.oauth",
"mediagoblin.oauth.client_register",
"/api/client/register",
"mediagoblin.oauth.views:client_register"
)
add_route(
"mediagoblin.oauth",
"mediagoblin.oauth.request_token",
"/oauth/request_token",
"mediagoblin.oauth.views:request_token"
)
add_route(
"mediagoblin.oauth",
"mediagoblin.oauth.authorize",
"/oauth/authorize",
"mediagoblin.oauth.views:authorize",
)
add_route(
"mediagoblin.oauth",
"mediagoblin.oauth.access_token",
"/oauth/access_token",
"mediagoblin.oauth.views:access_token"
)

View File

@ -252,6 +252,7 @@ def authorize(request):
if oauth_request.verifier is None:
orequest = GMGRequest(request)
orequest.resource_owner_key = token
request_validator = GMGRequestValidator()
auth_endpoint = AuthorizationEndpoint(request_validator)
verifier = auth_endpoint.create_verifier(orequest, {})
@ -333,7 +334,7 @@ def access_token(request):
error = "Missing required parameter."
return json_response({"error": error}, status=400)
request.resource_owner_key = parsed_tokens["oauth_consumer_key"]
request.oauth_token = parsed_tokens["oauth_token"]
request_validator = GMGRequestValidator(data)
av = AccessTokenEndpoint(request_validator)

View File

@ -35,12 +35,11 @@ def get_url_map():
import mediagoblin.submit.routing
import mediagoblin.user_pages.routing
import mediagoblin.edit.routing
import mediagoblin.webfinger.routing
import mediagoblin.listings.routing
import mediagoblin.notifications.routing
import mediagoblin.oauth.routing
import mediagoblin.federation.routing
for route in PluginManager().get_routes():
add_route(*route)

View File

@ -13,81 +13,284 @@
#
# 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 json
import datetime
import logging
import base64
import mock
import pytz
import pytest
from webtest import AppError
from werkzeug.datastructures import FileStorage
from .resources import GOOD_JPG
from mediagoblin import mg_globals
from mediagoblin.tools import template, pluginapi
from mediagoblin.media_types import sniff_media
from mediagoblin.db.models import User, MediaEntry
from mediagoblin.submit.lib import new_upload_entry
from mediagoblin.tests.tools import fixture_add_user
from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \
BIG_BLUE
_log = logging.getLogger(__name__)
from mediagoblin.federation.task import collect_garbage
from mediagoblin.moderation.tools import take_away_privileges
class TestAPI(object):
def setup(self):
""" Test mediagoblin's pump.io complient APIs """
@pytest.fixture(autouse=True)
def setup(self, test_app):
self.test_app = test_app
self.db = mg_globals.database
self.user_password = u'4cc355_70k3N'
self.user = fixture_add_user(u'joapi', self.user_password,
privileges=[u'active',u'uploader'])
self.user = fixture_add_user(privileges=[u'active', u'uploader'])
def login(self, test_app):
test_app.post(
'/auth/login/', {
'username': self.user.username,
'password': self.user_password})
def _activity_to_feed(self, test_app, activity, headers=None):
""" Posts an activity to the user's feed """
if headers:
headers.setdefault("Content-Type", "application/json")
else:
headers = {"Content-Type": "application/json"}
def get_context(self, template_name):
return template.TEMPLATE_TEST_CONTEXT[template_name]
with mock.patch("mediagoblin.decorators.oauth_required",
new_callable=self.mocked_oauth_required):
response = test_app.post(
"/api/user/{0}/feed".format(self.user.username),
json.dumps(activity),
headers=headers
)
def http_auth_headers(self):
return {'Authorization': 'Basic {0}'.format(
base64.b64encode(':'.join([
self.user.username,
self.user_password])))}
return response, json.loads(response.body)
def do_post(self, data, test_app, **kwargs):
url = kwargs.pop('url', '/api/submit')
do_follow = kwargs.pop('do_follow', False)
def _upload_image(self, test_app, image):
""" Uploads and image to MediaGoblin via pump.io API """
data = open(image, "rb").read()
headers = {
"Content-Type": "image/jpeg",
"Content-Length": str(len(data))
}
if not 'headers' in kwargs.keys():
kwargs['headers'] = self.http_auth_headers()
response = test_app.post(url, data, **kwargs)
with mock.patch("mediagoblin.decorators.oauth_required",
new_callable=self.mocked_oauth_required):
response = test_app.post(
"/api/user/{0}/uploads".format(self.user.username),
data,
headers=headers
)
image = json.loads(response.body)
if do_follow:
response.follow()
return response, image
return response
def _post_image_to_feed(self, test_app, image):
""" Posts an already uploaded image to feed """
activity = {
"verb": "post",
"object": image,
}
def upload_data(self, filename):
return {'upload_files': [('file', filename)]}
return self._activity_to_feed(test_app, activity)
def test_1_test_test_view(self, test_app):
self.login(test_app)
response = test_app.get(
'/api/test',
headers=self.http_auth_headers())
def mocked_oauth_required(self, *args, **kwargs):
""" Mocks mediagoblin.decorator.oauth_required to always validate """
assert response.body == \
'{"username": "joapi", "email": "joapi@example.com"}'
def fake_controller(controller, request, *args, **kwargs):
request.user = User.query.filter_by(id=self.user.id).first()
return controller(request, *args, **kwargs)
def test_2_test_submission(self, test_app):
self.login(test_app)
def oauth_required(c):
return lambda *args, **kwargs: fake_controller(c, *args, **kwargs)
response = self.do_post(
{'title': 'Great JPG!'},
test_app,
**self.upload_data(GOOD_JPG))
return oauth_required
assert response.status_int == 200
def test_can_post_image(self, test_app):
""" Tests that an image can be posted to the API """
# First request we need to do is to upload the image
response, image = self._upload_image(test_app, GOOD_JPG)
assert self.db.MediaEntry.query.filter_by(title=u'Great JPG!').first()
# I should have got certain things back
assert response.status_code == 200
assert "id" in image
assert "fullImage" in image
assert "url" in image["fullImage"]
assert "url" in image
assert "author" in image
assert "published" in image
assert "updated" in image
assert image["objectType"] == "image"
# Check that we got the response we're expecting
response, _ = self._post_image_to_feed(test_app, image)
assert response.status_code == 200
def test_upload_image_with_filename(self, test_app):
""" Tests that you can upload an image with filename and description """
response, data = self._upload_image(test_app, GOOD_JPG)
response, data = self._post_image_to_feed(test_app, data)
image = data["object"]
# Now we need to add a title and description
title = "My image ^_^"
description = "This is my super awesome image :D"
license = "CC-BY-SA"
image["displayName"] = title
image["content"] = description
image["license"] = license
activity = {"verb": "update", "object": image}
with mock.patch("mediagoblin.decorators.oauth_required",
new_callable=self.mocked_oauth_required):
response = test_app.post(
"/api/user/{0}/feed".format(self.user.username),
json.dumps(activity),
headers={"Content-Type": "application/json"}
)
image = json.loads(response.body)["object"]
# Check everything has been set on the media correctly
media = MediaEntry.query.filter_by(id=image["id"]).first()
assert media.title == title
assert media.description == description
assert media.license == license
# Check we're being given back everything we should on an update
assert image["id"] == media.id
assert image["displayName"] == title
assert image["content"] == description
assert image["license"] == license
def test_only_uploaders_post_image(self, test_app):
""" Test that only uploaders can upload images """
# Remove uploader permissions from user
take_away_privileges(self.user.username, u"uploader")
# Now try and upload a image
data = open(GOOD_JPG, "rb").read()
headers = {
"Content-Type": "image/jpeg",
"Content-Length": str(len(data)),
}
with mock.patch("mediagoblin.decorators.oauth_required",
new_callable=self.mocked_oauth_required):
with pytest.raises(AppError) as excinfo:
test_app.post(
"/api/user/{0}/uploads".format(self.user.username),
data,
headers=headers
)
# Assert that we've got a 403
assert "403 FORBIDDEN" in excinfo.value.message
def test_object_endpoint(self, test_app):
""" Tests that object can be looked up at endpoint """
# Post an image
response, data = self._upload_image(test_app, GOOD_JPG)
response, data = self._post_image_to_feed(test_app, data)
# Now lookup image to check that endpoint works.
image = data["object"]
assert "links" in image
assert "self" in image["links"]
# Get URI and strip testing host off
object_uri = image["links"]["self"]["href"]
object_uri = object_uri.replace("http://localhost:80", "")
with mock.patch("mediagoblin.decorators.oauth_required",
new_callable=self.mocked_oauth_required):
request = test_app.get(object_uri)
image = json.loads(request.body)
entry = MediaEntry.query.filter_by(id=image["id"]).first()
assert request.status_code == 200
assert entry.id == image["id"]
assert "image" in image
assert "fullImage" in image
assert "pump_io" in image
assert "links" in image
def test_post_comment(self, test_app):
""" Tests that I can post an comment media """
# Upload some media to comment on
response, data = self._upload_image(test_app, GOOD_JPG)
response, data = self._post_image_to_feed(test_app, data)
content = "Hai this is a comment on this lovely picture ^_^"
activity = {
"verb": "post",
"object": {
"objectType": "comment",
"content": content,
"inReplyTo": data["object"],
}
}
response, comment_data = self._activity_to_feed(test_app, activity)
assert response.status_code == 200
# Find the objects in the database
media = MediaEntry.query.filter_by(id=data["object"]["id"]).first()
comment = media.get_comments()[0]
# Tests that it matches in the database
assert comment.author == self.user.id
assert comment.content == content
# Test that the response is what we should be given
assert comment.id == comment_data["object"]["id"]
assert comment.content == comment_data["object"]["content"]
def test_profile(self, test_app):
""" Tests profile endpoint """
uri = "/api/user/{0}/profile".format(self.user.username)
with mock.patch("mediagoblin.decorators.oauth_required",
new_callable=self.mocked_oauth_required):
response = test_app.get(uri)
profile = json.loads(response.body)
assert response.status_code == 200
assert profile["preferredUsername"] == self.user.username
assert profile["objectType"] == "person"
assert "links" in profile
def test_garbage_collection_task(self, test_app):
""" Test old media entry are removed by GC task """
# Create a media entry that's unprocessed and over an hour old.
entry_id = 72
now = datetime.datetime.now(pytz.UTC)
file_data = FileStorage(
stream=open(GOOD_JPG, "rb"),
filename="mah_test.jpg",
content_type="image/jpeg"
)
# Find media manager
media_type, media_manager = sniff_media(file_data, "mah_test.jpg")
entry = new_upload_entry(self.user)
entry.id = entry_id
entry.title = "Mah Image"
entry.slug = "slugy-slug-slug"
entry.media_type = 'image'
entry.uploaded = now - datetime.timedelta(days=2)
entry.save()
# Validate the model exists
assert MediaEntry.query.filter_by(id=entry_id).first() is not None
# Call the garbage collection task
collect_garbage()
# Now validate the image has been deleted
assert MediaEntry.query.filter_by(id=entry_id).first() is None

View File

@ -0,0 +1,93 @@
# 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 logging
import base64
import pytest
from mediagoblin import mg_globals
from mediagoblin.tools import template, pluginapi
from mediagoblin.tests.tools import fixture_add_user
from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \
BIG_BLUE
_log = logging.getLogger(__name__)
class TestAPI(object):
def setup(self):
self.db = mg_globals.database
self.user_password = u'4cc355_70k3N'
self.user = fixture_add_user(u'joapi', self.user_password,
privileges=[u'active',u'uploader'])
def login(self, test_app):
test_app.post(
'/auth/login/', {
'username': self.user.username,
'password': self.user_password})
def get_context(self, template_name):
return template.TEMPLATE_TEST_CONTEXT[template_name]
def http_auth_headers(self):
return {'Authorization': 'Basic {0}'.format(
base64.b64encode(':'.join([
self.user.username,
self.user_password])))}
def do_post(self, data, test_app, **kwargs):
url = kwargs.pop('url', '/api/submit')
do_follow = kwargs.pop('do_follow', False)
if not 'headers' in kwargs.keys():
kwargs['headers'] = self.http_auth_headers()
response = test_app.post(url, data, **kwargs)
if do_follow:
response.follow()
return response
def upload_data(self, filename):
return {'upload_files': [('file', filename)]}
def test_1_test_test_view(self, test_app):
self.login(test_app)
response = test_app.get(
'/api/test',
headers=self.http_auth_headers())
assert response.body == \
'{"username": "joapi", "email": "joapi@example.com"}'
def test_2_test_submission(self, test_app):
self.login(test_app)
response = self.do_post(
{'title': 'Great JPG!'},
test_app,
**self.upload_data(GOOD_JPG))
assert response.status_int == 200
assert self.db.MediaEntry.query.filter_by(title=u'Great JPG!').first()

View File

@ -52,8 +52,8 @@ class TestOAuth(object):
def register_client(self, **kwargs):
""" Regiters a client with the API """
kwargs["type"] = "client_associate"
kwargs["type"] = "client_associate"
kwargs["application_type"] = kwargs.get("application_type", "native")
return self.test_app.post("/api/client/register", kwargs)
@ -63,7 +63,7 @@ class TestOAuth(object):
client_info = response.json
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
assert response.status_int == 200
assert client is not None
@ -81,7 +81,7 @@ class TestOAuth(object):
client_info = response.json
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
assert client is not None
assert client.secret == client_info["client_secret"]
assert client.application_type == query["application_type"]
@ -163,4 +163,3 @@ class TestOAuth(object):
assert request_token.client == client.id
assert request_token.used == False
assert request_token.callback == request_query["oauth_callback"]

View File

@ -25,13 +25,16 @@ from webtest import TestApp
from mediagoblin import mg_globals
from mediagoblin.db.models import User, MediaEntry, Collection, MediaComment, \
CommentSubscription, CommentNotification, Privilege, CommentReport
CommentSubscription, CommentNotification, Privilege, CommentReport, Client, \
RequestToken, AccessToken
from mediagoblin.tools import testing
from mediagoblin.init.config import read_mediagoblin_config
from mediagoblin.db.base import Session
from mediagoblin.meddleware import BaseMeddleware
from mediagoblin.auth import gen_password_hash
from mediagoblin.gmg_commands.dbupdate import run_dbupdate
from mediagoblin.oauth.views import OAUTH_ALPHABET
from mediagoblin.tools.crypto import random_string
from datetime import datetime
@ -343,3 +346,4 @@ def fixture_add_comment_report(comment=None, reported_user=None,
Session.expunge(comment_report)
return comment_report

View File

@ -16,7 +16,9 @@
import json
import logging
from mediagoblin.db.models import User
from mediagoblin.db.models import User, AccessToken
from mediagoblin.oauth.tools.request import decode_authorization_header
_log = logging.getLogger(__name__)
@ -31,6 +33,18 @@ def setup_user_in_request(request):
Examine a request and tack on a request.user parameter if that's
appropriate.
"""
# If API request the user will be associated with the access token
authorization = decode_authorization_header(request.headers)
if authorization.get(u"access_token"):
# Check authorization header.
token = authorization[u"oauth_token"]
token = AccessToken.query.filter_by(token=token).first()
if token is not None:
request.user = token.user
return
if 'user_id' not in request.session:
request.user = None
return
@ -45,8 +59,8 @@ def setup_user_in_request(request):
def decode_request(request):
""" Decodes a request based on MIME-Type """
data = request.get_data()
data = request.data
if request.content_type == json_encoded:
data = json.loads(data)
elif request.content_type == form_encoded or request.content_type == "":

View File

@ -1,117 +0,0 @@
# 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/>.
'''
For references, see docstring in mediagoblin/webfinger/__init__.py
'''
import re
from urlparse import urlparse
from mediagoblin.tools.response import render_to_response, render_404
def host_meta(request):
'''
Webfinger host-meta
'''
placeholder = 'MG_LRDD_PLACEHOLDER'
lrdd_title = 'GNU MediaGoblin - User lookup'
lrdd_template = request.urlgen(
'mediagoblin.webfinger.xrd',
uri=placeholder,
qualified=True)
return render_to_response(
request,
'mediagoblin/webfinger/host-meta.xml',
{'request': request,
'lrdd_template': lrdd_template,
'lrdd_title': lrdd_title,
'placeholder': placeholder})
MATCH_SCHEME_PATTERN = re.compile(r'^acct:')
def xrd(request):
'''
Find user data based on a webfinger URI
'''
param_uri = request.GET.get('uri')
if not param_uri:
return render_404(request)
'''
:py:module:`urlparse` does not recognize usernames in URIs of the
form ``acct:user@example.org`` or ``user@example.org``.
'''
if not MATCH_SCHEME_PATTERN.search(param_uri):
# Assume the URI is in the form ``user@example.org``
uri = 'acct://' + param_uri
else:
# Assumes the URI looks like ``acct:user@example.org
uri = MATCH_SCHEME_PATTERN.sub(
'acct://', param_uri)
parsed = urlparse(uri)
xrd_subject = param_uri
# TODO: Verify that the user exists
# Q: Does webfinger support error handling in this case?
# Returning 404 seems intuitive, need to check.
if parsed.username:
# The user object
# TODO: Fetch from database instead of using the MockUser
user = MockUser()
user.username = parsed.username
xrd_links = [
{'attrs': {
'rel': 'http://microformats.org/profile/hcard',
'href': request.urlgen(
'mediagoblin.user_pages.user_home',
user=user.username,
qualified=True)}},
{'attrs': {
'rel': 'http://schemas.google.com/g/2010#updates-from',
'href': request.urlgen(
'mediagoblin.user_pages.atom_feed',
user=user.username,
qualified=True)}}]
xrd_alias = request.urlgen(
'mediagoblin.user_pages.user_home',
user=user.username,
qualified=True)
return render_to_response(
request,
'mediagoblin/webfinger/xrd.xml',
{'request': request,
'subject': xrd_subject,
'alias': xrd_alias,
'links': xrd_links })
else:
return render_404(request)
class MockUser(object):
'''
TEMPORARY user object
'''
username = None