Merge branch 'master' into OPW-Moderation-Update

Conflicts:
	mediagoblin/db/models.py
	mediagoblin/decorators.py
	mediagoblin/routing.py
	mediagoblin/user_pages/views.py
This commit is contained in:
tilly-Q
2013-08-20 12:21:13 -04:00
91 changed files with 4357 additions and 200 deletions

View File

@@ -39,7 +39,6 @@ from mediagoblin.init import (get_jinja_loader, get_staticdirector,
from mediagoblin.tools.pluginapi import PluginManager, hook_transform
from mediagoblin.tools.crypto import setup_crypto
from mediagoblin.auth.tools import check_auth_enabled, no_auth_logout
from mediagoblin import notifications
_log = logging.getLogger(__name__)
@@ -199,8 +198,6 @@ class MediaGoblinApp(object):
# Log user out if authentication_disabled
no_auth_logout(request)
request.notifications = notifications
mg_request.setup_user_in_request(request)
request.controller_name = None

View File

@@ -41,8 +41,11 @@ def register(request):
"""
if 'pass_auth' not in request.template_env.globals:
redirect_name = hook_handle('auth_no_pass_redirect')
return redirect(request, 'mediagoblin.plugins.{0}.register'.format(
redirect_name))
if redirect_name:
return redirect(request, 'mediagoblin.plugins.{0}.register'.format(
redirect_name))
else:
return redirect(request, 'index')
register_form = hook_handle("auth_get_registration_form", request)
@@ -73,8 +76,11 @@ def login(request):
"""
if 'pass_auth' not in request.template_env.globals:
redirect_name = hook_handle('auth_no_pass_redirect')
return redirect(request, 'mediagoblin.plugins.{0}.login'.format(
redirect_name))
if redirect_name:
return redirect(request, 'mediagoblin.plugins.{0}.login'.format(
redirect_name))
else:
return redirect(request, 'index')
login_form = hook_handle("auth_get_login_form", request)

View File

@@ -75,6 +75,12 @@ theme = string()
plugin_web_path = string(default="/plugin_static/")
plugin_linked_assets_dir = string(default="%(here)s/user_dev/plugin_static/")
[jinja2]
# Jinja2 supports more directives than the minimum required by mediagoblin.
# This setting allows users creating custom templates to specify a list of
# additional extensions they want to use. example value:
# extensions = jinja2.ext.loopcontrols , jinja2.ext.with_
extensions = string_list(default=list())
[storage:publicstore]
storage_class = string(default="mediagoblin.storage.filestorage:BasicFileStorage")
@@ -116,7 +122,7 @@ vp8_quality = integer(default=8)
vorbis_quality = float(default=0.3)
# Autoplay the video when page is loaded?
auto_play = boolean(default=True)
auto_play = boolean(default=False)
[[skip_transcode]]
mime_types = string_list(default=list("video/webm"))
@@ -146,7 +152,7 @@ CELERY_RESULT_DBURI = string(default="sqlite:///%(here)s/celery.db")
# default kombu stuff
BROKER_TRANSPORT = string(default="sqlalchemy")
BROKER_HOST = string(default="sqlite:///%(here)s/kombu.db")
BROKER_URL = string(default="sqlite:///%(here)s/kombu.db")
# known booleans
CELERY_RESULT_PERSISTENT = boolean()

View File

@@ -25,6 +25,8 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import and_
from migrate.changeset.constraint import UniqueConstraint
from mediagoblin.db.extratypes import JSONEncoded
from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
from mediagoblin.db.models import (MediaEntry, Collection, User,
MediaComment, Privilege, ReportBase)
@@ -452,3 +454,82 @@ def create_moderation_tables(db):
Privilege_v0.__table__.create(db.bind)
PrivilegeUserAssociation_v0.__table__.create(db.bind)
db.commit()
# oauth1 migrations
class Client_v0(declarative_base()):
"""
Model representing a client - Used for API Auth
"""
__tablename__ = "core__clients"
id = Column(Unicode, nullable=True, primary_key=True)
secret = Column(Unicode, nullable=False)
expirey = Column(DateTime, nullable=True)
application_type = Column(Unicode, nullable=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
# optional stuff
redirect_uri = Column(JSONEncoded, nullable=True)
logo_url = Column(Unicode, nullable=True)
application_name = Column(Unicode, nullable=True)
contacts = Column(JSONEncoded, nullable=True)
def __repr__(self):
if self.application_name:
return "<Client {0} - {1}>".format(self.application_name, self.id)
else:
return "<Client {0}>".format(self.id)
class RequestToken_v0(declarative_base()):
"""
Model for representing the request tokens
"""
__tablename__ = "core__request_tokens"
token = Column(Unicode, primary_key=True)
secret = Column(Unicode, nullable=False)
client = Column(Unicode, ForeignKey(Client_v0.id))
user = Column(Integer, ForeignKey(User.id), nullable=True)
used = Column(Boolean, default=False)
authenticated = Column(Boolean, default=False)
verifier = Column(Unicode, nullable=True)
callback = Column(Unicode, nullable=False, default=u"oob")
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
class AccessToken_v0(declarative_base()):
"""
Model for representing the access tokens
"""
__tablename__ = "core__access_tokens"
token = Column(Unicode, nullable=False, primary_key=True)
secret = Column(Unicode, nullable=False)
user = Column(Integer, ForeignKey(User.id))
request_token = Column(Unicode, ForeignKey(RequestToken_v0.token))
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
class NonceTimestamp_v0(declarative_base()):
"""
A place the timestamp and nonce can be stored - this is for OAuth1
"""
__tablename__ = "core__nonce_timestamps"
nonce = Column(Unicode, nullable=False, primary_key=True)
timestamp = Column(DateTime, nullable=False, primary_key=True)
@RegisterMigration(14, MIGRATIONS)
def create_oauth1_tables(db):
""" Creates the OAuth1 tables """
Client_v0.__table__.create(db.bind)
RequestToken_v0.__table__.create(db.bind)
AccessToken_v0.__table__.create(db.bind)
NonceTimestamp_v0.__table__.create(db.bind)
db.commit()

View File

@@ -30,6 +30,7 @@ from sqlalchemy.sql.expression import desc
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.util import memoized_property
from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded
from mediagoblin.db.base import Base, DictReadAttrProxy
from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \
@@ -61,20 +62,17 @@ class User(Base, UserMixin):
# the RFC) and because it would be a mess to implement at this
# point.
email = Column(Unicode, nullable=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
pw_hash = Column(Unicode, nullable=False)
pw_hash = Column(Unicode)
email_verified = Column(Boolean, default=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
status = Column(Unicode, default=u"needs_email_verification", nullable=False)
# Intented to be nullable=False, but migrations would not work for it
# set to nullable=True implicitly.
wants_comment_notification = Column(Boolean, default=True)
license_preference = Column(Unicode)
verification_key = Column(Unicode)
is_admin = Column(Boolean, default=False, nullable=False)
url = Column(Unicode)
bio = Column(UnicodeText) # ??
fp_verification_key = Column(Unicode)
fp_token_expire = Column(DateTime)
## TODO
# plugin data would be in a separate model
@@ -116,6 +114,71 @@ class User(Base, UserMixin):
self.has_privilege(*priv_names[1:])
return False
class Client(Base):
"""
Model representing a client - Used for API Auth
"""
__tablename__ = "core__clients"
id = Column(Unicode, nullable=True, primary_key=True)
secret = Column(Unicode, nullable=False)
expirey = Column(DateTime, nullable=True)
application_type = Column(Unicode, nullable=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
# optional stuff
redirect_uri = Column(JSONEncoded, nullable=True)
logo_url = Column(Unicode, nullable=True)
application_name = Column(Unicode, nullable=True)
contacts = Column(JSONEncoded, nullable=True)
def __repr__(self):
if self.application_name:
return "<Client {0} - {1}>".format(self.application_name, self.id)
else:
return "<Client {0}>".format(self.id)
class RequestToken(Base):
"""
Model for representing the request tokens
"""
__tablename__ = "core__request_tokens"
token = Column(Unicode, primary_key=True)
secret = Column(Unicode, nullable=False)
client = Column(Unicode, ForeignKey(Client.id))
user = Column(Integer, ForeignKey(User.id), nullable=True)
used = Column(Boolean, default=False)
authenticated = Column(Boolean, default=False)
verifier = Column(Unicode, nullable=True)
callback = Column(Unicode, nullable=False, default=u"oob")
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
class AccessToken(Base):
"""
Model for representing the access tokens
"""
__tablename__ = "core__access_tokens"
token = Column(Unicode, nullable=False, primary_key=True)
secret = Column(Unicode, nullable=False)
user = Column(Integer, ForeignKey(User.id))
request_token = Column(Unicode, ForeignKey(RequestToken.token))
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
class NonceTimestamp(Base):
"""
A place the timestamp and nonce can be stored - this is for OAuth1
"""
__tablename__ = "core__nonce_timestamps"
nonce = Column(Unicode, nullable=False, primary_key=True)
timestamp = Column(DateTime, nullable=False, primary_key=True)
class MediaEntry(Base, MediaEntryMixin):
"""
@@ -498,6 +561,7 @@ class ProcessingMetaData(Base):
"""A dict like view on this object"""
return DictReadAttrProxy(self)
class CommentSubscription(Base):
__tablename__ = 'core__comment_subscriptions'
id = Column(Integer, primary_key=True)
@@ -768,16 +832,15 @@ with_polymorphic(
Notification,
[ProcessingNotification, CommentNotification])
MODELS = [
User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData, ReportBase,
CommentReport, MediaReport, UserBan, Privilege, PrivilegeUserAssociation,
ArchivedReport, Notification, CommentNotification,
ProcessingNotification, CommentSubscription]
User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
Notification, CommentNotification, ProcessingNotification,
CommentSubscription, ReportBase, CommentReport, MediaReport, UserBan,
Privilege, PrivilegeUserAssociation, ArchivedReport, ArchivedReport]
"""
Foundations are the default rows that are created immediately after the tables
Foundations are the default rows that are created immediately after the tables
are initialized. Each entry to this dictionary should be in the format of:
ModelConstructorObject:List of Dictionaries
(Each Dictionary represents a row on the Table to be created, containing each

View File

@@ -18,15 +18,18 @@ from functools import wraps
from urlparse import urljoin
from werkzeug.exceptions import Forbidden, NotFound
from werkzeug.urls import url_quote
from oauthlib.oauth1 import ResourceEndpoint
from mediagoblin import mg_globals as mgg
from mediagoblin import messages
from mediagoblin.db.models import (MediaEntry, User, MediaComment,
UserBan, Privilege)
from mediagoblin.tools.response import redirect, render_404, render_user_banned
from mediagoblin.tools.response import (redirect, render_404,
render_user_banned, json_response)
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.oauth.tools.request import decode_authorization_header
from mediagoblin.oauth.oauth import GMGRequestValidator
def require_active_login(controller):
"""
@@ -245,6 +248,17 @@ def get_media_entry_by_id(controller):
return wrapper
def get_workbench(func):
"""Decorator, passing in a workbench as kwarg which is cleaned up afterwards"""
@wraps(func)
def new_func(*args, **kwargs):
with mgg.workbench_manager.create() as workbench:
return func(*args, workbench=workbench, **kwargs)
return new_func
def allow_registration(controller):
""" Decorator for if registration is enabled"""
@wraps(controller)
@@ -287,18 +301,11 @@ def auth_enabled(controller):
messages.WARNING,
_('Sorry, authentication is disabled on this instance.'))
return redirect(request, 'index')
return controller(request, *args, **kwargs)
return wrapper
def get_workbench(func):
"""Decorator, passing in a workbench as kwarg which is cleaned up afterwards"""
@wraps(func)
def new_func(*args, **kwargs):
with mgg.workbench_manager.create() as workbench:
return func(*args, workbench=workbench, **kwargs)
return new_func
def require_admin_or_moderator_login(controller):
"""
Require an login from an administrator or a moderator.
@@ -322,6 +329,7 @@ def require_admin_or_moderator_login(controller):
return new_controller_func
def user_not_banned(controller):
"""
Requires that the user has not been banned. Otherwise redirects to the page
@@ -337,3 +345,33 @@ def user_not_banned(controller):
return wrapper
def oauth_required(controller):
""" Used to wrap API endpoints where oauth is required """
@wraps(controller)
def wrapper(request, *args, **kwargs):
data = request.headers
authorization = decode_authorization_header(data)
if authorization == dict():
error = "Missing required parameter."
return json_response({"error": error}, status=400)
request_validator = GMGRequestValidator()
resource_endpoint = ResourceEndpoint(request_validator)
valid, request = resource_endpoint.validate_protected_resource_request(
uri=request.url,
http_method=request.method,
body=request.get_data(),
headers=dict(request.headers),
)
if not valid:
error = "Invalid oauth prarameter."
return json_response({"error": error}, status=400)
return controller(request, *args, **kwargs)
return wrapper

View File

@@ -96,7 +96,7 @@ forgotten to add it? ({1})'.format(plugin, exc))
plugin,
exc))
foundations = []
foundations = {}
except AttributeError as exc:
_log.debug('Could not find FOUNDATIONS in {0}.models, have you \
forgotten to add it? ({1})'.format(plugin, exc))
@@ -126,7 +126,7 @@ def run_dbupdate(app_config, global_config):
def run_all_migrations(db, app_config, global_config):
"""
Initializes or migrates a database that already has a
Initializes or migrates a database that already has a
connection setup and also initializes or migrates all
extensions based on the config files.

View File

@@ -19,6 +19,9 @@ from mediagoblin.media_types.audio.processing import process_audio, \
sniff_handler
from mediagoblin.tools import pluginapi
# Why isn't .ogg in this list? It's still detected, but via sniffing,
# .ogg files could be either video or audio... sniffing determines which.
ACCEPTED_EXTENSIONS = ["mp3", "flac", "wav", "m4a"]
MEDIA_TYPE = 'mediagoblin.media_types.audio'

View File

@@ -14,7 +14,7 @@
# 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 tempfile import NamedTemporaryFile
import os.path
import logging
import datetime
@@ -73,79 +73,77 @@ def process_video(proc_state):
queued_filename = proc_state.get_queued_filename()
name_builder = FilenameBuilder(queued_filename)
medium_filepath = create_pub_filepath(
entry, name_builder.fill('{basename}-640p.webm'))
medium_basename = name_builder.fill('{basename}-640p.webm')
medium_filepath = create_pub_filepath(entry, medium_basename)
thumbnail_filepath = create_pub_filepath(
entry, name_builder.fill('{basename}.thumbnail.jpg'))
thumbnail_basename = name_builder.fill('{basename}.thumbnail.jpg')
thumbnail_filepath = create_pub_filepath(entry, thumbnail_basename)
# Create a temporary file for the video destination (cleaned up with workbench)
tmp_dst = NamedTemporaryFile(dir=workbench.dir, delete=False)
with tmp_dst:
# Transcode queued file to a VP8/vorbis file that fits in a 640x640 square
progress_callback = ProgressCallback(entry)
tmp_dst = os.path.join(workbench.dir, medium_basename)
# Transcode queued file to a VP8/vorbis file that fits in a 640x640 square
progress_callback = ProgressCallback(entry)
dimensions = (
mgg.global_config['media:medium']['max_width'],
mgg.global_config['media:medium']['max_height'])
dimensions = (
mgg.global_config['media:medium']['max_width'],
mgg.global_config['media:medium']['max_height'])
# Extract metadata and keep a record of it
metadata = transcoders.VideoTranscoder().discover(queued_filename)
store_metadata(entry, metadata)
# Extract metadata and keep a record of it
metadata = transcoders.VideoTranscoder().discover(queued_filename)
store_metadata(entry, metadata)
# Figure out whether or not we need to transcode this video or
# if we can skip it
if skip_transcode(metadata):
_log.debug('Skipping transcoding')
# Figure out whether or not we need to transcode this video or
# if we can skip it
if skip_transcode(metadata):
_log.debug('Skipping transcoding')
dst_dimensions = metadata['videowidth'], metadata['videoheight']
dst_dimensions = metadata['videowidth'], metadata['videoheight']
# Push original file to public storage
_log.debug('Saving original...')
proc_state.copy_original(queued_filepath[-1])
_log.debug('Saving original...')
proc_state.copy_original(queued_filepath[-1])
did_transcode = False
else:
transcoder = transcoders.VideoTranscoder()
did_transcode = False
else:
transcoder = transcoders.VideoTranscoder()
transcoder.transcode(queued_filename, tmp_dst.name,
vp8_quality=video_config['vp8_quality'],
vp8_threads=video_config['vp8_threads'],
vorbis_quality=video_config['vorbis_quality'],
progress_callback=progress_callback,
dimensions=dimensions)
transcoder.transcode(queued_filename, tmp_dst,
vp8_quality=video_config['vp8_quality'],
vp8_threads=video_config['vp8_threads'],
vorbis_quality=video_config['vorbis_quality'],
progress_callback=progress_callback,
dimensions=dimensions)
dst_dimensions = transcoder.dst_data.videowidth,\
transcoder.dst_data.videoheight
dst_dimensions = transcoder.dst_data.videowidth,\
transcoder.dst_data.videoheight
# Push transcoded video to public storage
_log.debug('Saving medium...')
mgg.public_store.copy_local_to_storage(tmp_dst.name, medium_filepath)
_log.debug('Saved medium')
# Push transcoded video to public storage
_log.debug('Saving medium...')
mgg.public_store.copy_local_to_storage(tmp_dst, medium_filepath)
_log.debug('Saved medium')
entry.media_files['webm_640'] = medium_filepath
entry.media_files['webm_640'] = medium_filepath
did_transcode = True
did_transcode = True
# Save the width and height of the transcoded video
entry.media_data_init(
width=dst_dimensions[0],
height=dst_dimensions[1])
# Save the width and height of the transcoded video
entry.media_data_init(
width=dst_dimensions[0],
height=dst_dimensions[1])
# Temporary file for the video thumbnail (cleaned up with workbench)
tmp_thumb = NamedTemporaryFile(dir=workbench.dir, suffix='.jpg', delete=False)
tmp_thumb = os.path.join(workbench.dir, thumbnail_basename)
with tmp_thumb:
# Create a thumbnail.jpg that fits in a 180x180 square
transcoders.VideoThumbnailerMarkII(
queued_filename,
tmp_thumb.name,
180)
# Create a thumbnail.jpg that fits in a 180x180 square
transcoders.VideoThumbnailerMarkII(
queued_filename,
tmp_thumb,
180)
# Push the thumbnail to public storage
_log.debug('Saving thumbnail...')
mgg.public_store.copy_local_to_storage(tmp_thumb.name, thumbnail_filepath)
entry.media_files['thumb'] = thumbnail_filepath
# Push the thumbnail to public storage
_log.debug('Saving thumbnail...')
mgg.public_store.copy_local_to_storage(tmp_thumb, thumbnail_filepath)
entry.media_files['thumb'] = thumbnail_filepath
# save the original... but only if we did a transcoding
# (if we skipped transcoding and just kept the original anyway as the main

View File

@@ -18,7 +18,6 @@ import logging
from mediagoblin.db.models import Notification, \
CommentNotification, CommentSubscription
from mediagoblin.notifications.task import email_notification_task
from mediagoblin.notifications.tools import generate_comment_message
_log = logging.getLogger(__name__)
@@ -50,6 +49,7 @@ def trigger_notification(comment, media_entry, request):
media_entry,
request)
from mediagoblin.notifications.task import email_notification_task
email_notification_task.apply_async([cn.id, message])

View File

@@ -0,0 +1,16 @@
# 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/>.

View File

@@ -0,0 +1,18 @@
# 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/>.
class ValidationException(Exception):
pass

View File

@@ -0,0 +1,7 @@
import wtforms
class AuthorizeForm(wtforms.Form):
""" Form used to authorize the request token """
oauth_token = wtforms.HiddenField("oauth_token")
oauth_verifier = wtforms.HiddenField("oauth_verifier")

132
mediagoblin/oauth/oauth.py Normal file
View File

@@ -0,0 +1,132 @@
# 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 oauthlib.common import Request
from oauthlib.oauth1 import RequestValidator
from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
class GMGRequestValidator(RequestValidator):
enforce_ssl = False
def __init__(self, data=None, *args, **kwargs):
self.POST = data
super(GMGRequestValidator, self).__init__(*args, **kwargs)
def save_request_token(self, token, request):
""" Saves request token in db """
client_id = self.POST[u"oauth_consumer_key"]
request_token = RequestToken(
token=token["oauth_token"],
secret=token["oauth_token_secret"],
)
request_token.client = client_id
if u"oauth_callback" in self.POST:
request_token.callback = self.POST[u"oauth_callback"]
request_token.save()
def save_verifier(self, token, verifier, request):
""" Saves the oauth request verifier """
request_token = RequestToken.query.filter_by(token=token).first()
request_token.verifier = verifier["oauth_verifier"]
request_token.save()
def save_access_token(self, token, request):
""" Saves access token in db """
access_token = AccessToken(
token=token["oauth_token"],
secret=token["oauth_token_secret"],
)
access_token.request_token = request.oauth_token
request_token = RequestToken.query.filter_by(token=request.oauth_token).first()
access_token.user = request_token.user
access_token.save()
def get_realms(*args, **kwargs):
""" Currently a stub - called when making AccessTokens """
return list()
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):
""" Verifies client exists with id of client_key """
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):
""" Verifies token exists for client with id of client_key """
client = Client.query.filter_by(id=client_key).first()
token = AccessToken.query.filter_by(token=token)
token = token.first()
if token is None:
return False
request_token = RequestToken.query.filter_by(token=token.request_token)
request_token = request_token.first()
if client.id != request_token.client:
return False
return True
def validate_realms(self, *args, **kwargs):
""" Would validate reals however not using these yet. """
return True # implement when realms are implemented
def get_client_secret(self, client_key, request):
""" Retrives a client secret with from a client with an id of client_key """
client = Client.query.filter_by(id=client_key).first()
return client.secret
def get_access_token_secret(self, client_key, token, request):
access_token = AccessToken.query.filter_by(token=token).first()
return access_token.secret
class GMGRequest(Request):
"""
Fills in data to produce a oauth.common.Request object from a
werkzeug Request object
"""
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["headers"] = kwargs.get("headers", dict(request.headers))
super(GMGRequest, self).__init__(*args, **kwargs)

View File

@@ -0,0 +1,43 @@
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from mediagoblin.tools.routing import add_route
# client registration & oauth
add_route(
"mediagoblin.oauth",
"/api/client/register",
"mediagoblin.oauth.views:client_register"
)
add_route(
"mediagoblin.oauth",
"/oauth/request_token",
"mediagoblin.oauth.views:request_token"
)
add_route(
"mediagoblin.oauth",
"/oauth/authorize",
"mediagoblin.oauth.views:authorize",
)
add_route(
"mediagoblin.oauth",
"/oauth/access_token",
"mediagoblin.oauth.views:access_token"
)

View File

View File

@@ -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/>.
class WTFormData(dict):
"""
Provides a WTForm usable dictionary
"""
def getlist(self, key):
v = self[key]
if not isinstance(v, (list, tuple)):
v = [v]
return v

View File

@@ -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/>.
def decode_authorization_header(header):
""" Decodes a HTTP Authorization Header to python dictionary """
authorization = header.get("Authorization", "").lstrip(" ").lstrip("OAuth")
tokens = {}
for param in authorization.split(","):
try:
key, value = param.split("=")
except ValueError:
continue
key = key.lstrip(" ")
value = value.lstrip(" ").lstrip('"')
value = value.rstrip(" ").rstrip('"')
tokens[key] = value
return tokens

339
mediagoblin/oauth/views.py Normal file
View File

@@ -0,0 +1,339 @@
# 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 datetime
from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint,
AccessTokenEndpoint)
from mediagoblin.decorators import require_active_login
from mediagoblin.tools.translate import pass_to_ugettext
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.tools.request import decode_request
from mediagoblin.tools.response import (render_to_response, redirect,
json_response, render_400,
form_response)
from mediagoblin.tools.crypto import random_string
from mediagoblin.tools.validator import validate_email, validate_url
from mediagoblin.oauth.forms import AuthorizeForm
from mediagoblin.oauth.oauth import GMGRequestValidator, GMGRequest
from mediagoblin.oauth.tools.request import decode_authorization_header
from mediagoblin.oauth.tools.forms import WTFormData
from mediagoblin.db.models import NonceTimestamp, Client, RequestToken
# possible client types
client_types = ["web", "native"] # currently what pump supports
@csrf_exempt
def client_register(request):
""" Endpoint for client registration """
try:
data = decode_request(request)
except ValueError:
error = "Could not decode data."
return json_response({"error": error}, status=400)
if data is "":
error = "Unknown Content-Type"
return json_response({"error": error}, status=400)
if "type" not in data:
error = "No registration type provided."
return json_response({"error": error}, status=400)
if data.get("application_type", None) not in client_types:
error = "Unknown application_type."
return json_response({"error": error}, status=400)
client_type = data["type"]
if client_type == "client_update":
# updating a client
if "client_id" not in data:
error = "client_id is requried to update."
return json_response({"error": error}, status=400)
elif "client_secret" not in data:
error = "client_secret is required to update."
return json_response({"error": error}, status=400)
client = Client.query.filter_by(
id=data["client_id"],
secret=data["client_secret"]
).first()
if client is None:
error = "Unauthorized."
return json_response({"error": error}, status=403)
client.application_name = data.get(
"application_name",
client.application_name
)
client.application_type = data.get(
"application_type",
client.application_type
)
app_name = ("application_type", client.application_name)
if app_name in client_types:
client.application_name = app_name
elif client_type == "client_associate":
# registering
if "client_id" in data:
error = "Only set client_id for update."
return json_response({"error": error}, status=400)
elif "access_token" in data:
error = "access_token not needed for registration."
return json_response({"error": error}, status=400)
elif "client_secret" in data:
error = "Only set client_secret for update."
return json_response({"error": error}, status=400)
# generate the client_id and client_secret
client_id = random_string(22) # seems to be what pump uses
client_secret = random_string(43) # again, seems to be what pump uses
expirey = 0 # for now, lets not have it expire
expirey_db = None if expirey == 0 else expirey
application_type = data["application_type"]
# save it
client = Client(
id=client_id,
secret=client_secret,
expirey=expirey_db,
application_type=application_type,
)
else:
error = "Invalid registration type"
return json_response({"error": error}, status=400)
logo_url = data.get("logo_url", client.logo_url)
if logo_url is not None and not validate_url(logo_url):
error = "Logo URL {0} is not a valid URL.".format(logo_url)
return json_response(
{"error": error},
status=400
)
else:
client.logo_url = logo_url
client.application_name = data.get("application_name", None)
contacts = data.get("contacts", None)
if contacts is not None:
if type(contacts) is not unicode:
error = "Contacts must be a string of space-seporated email addresses."
return json_response({"error": error}, status=400)
contacts = contacts.split()
for contact in contacts:
if not validate_email(contact):
# not a valid email
error = "Email {0} is not a valid email.".format(contact)
return json_response({"error": error}, status=400)
client.contacts = contacts
redirect_uris = data.get("redirect_uris", None)
if redirect_uris is not None:
if type(redirect_uris) is not unicode:
error = "redirect_uris must be space-seporated URLs."
return json_response({"error": error}, status=400)
redirect_uris = redirect_uris.split()
for uri in redirect_uris:
if not validate_url(uri):
# not a valid uri
error = "URI {0} is not a valid URI".format(uri)
return json_response({"error": error}, status=400)
client.redirect_uri = redirect_uris
client.save()
expirey = 0 if client.expirey is None else client.expirey
return json_response(
{
"client_id": client.id,
"client_secret": client.secret,
"expires_at": expirey,
})
@csrf_exempt
def request_token(request):
""" Returns request token """
try:
data = decode_request(request)
except ValueError:
error = "Could not decode data."
return json_response({"error": error}, status=400)
if data == "":
error = "Unknown Content-Type"
return json_response({"error": error}, status=400)
if not data and request.headers:
data = request.headers
data = dict(data) # mutableifying
authorization = decode_authorization_header(data)
if authorization == dict() or u"oauth_consumer_key" not in authorization:
error = "Missing required parameter."
return json_response({"error": error}, status=400)
# check the client_id
client_id = authorization[u"oauth_consumer_key"]
client = Client.query.filter_by(id=client_id).first()
if client == None:
# client_id is invalid
error = "Invalid client_id"
return json_response({"error": error}, status=400)
# make request token and return to client
request_validator = GMGRequestValidator(authorization)
rv = RequestTokenEndpoint(request_validator)
tokens = rv.create_request_token(request, authorization)
# store the nonce & timestamp before we return back
nonce = authorization[u"oauth_nonce"]
timestamp = authorization[u"oauth_timestamp"]
timestamp = datetime.datetime.fromtimestamp(float(timestamp))
nc = NonceTimestamp(nonce=nonce, timestamp=timestamp)
nc.save()
return form_response(tokens)
@require_active_login
def authorize(request):
""" Displays a page for user to authorize """
if request.method == "POST":
return authorize_finish(request)
_ = pass_to_ugettext
token = request.args.get("oauth_token", None)
if token is None:
# no token supplied, display a html 400 this time
err_msg = _("Must provide an oauth_token.")
return render_400(request, err_msg=err_msg)
oauth_request = RequestToken.query.filter_by(token=token).first()
if oauth_request is None:
err_msg = _("No request token found.")
return render_400(request, err_msg)
if oauth_request.used:
return authorize_finish(request)
if oauth_request.verifier is None:
orequest = GMGRequest(request)
request_validator = GMGRequestValidator()
auth_endpoint = AuthorizationEndpoint(request_validator)
verifier = auth_endpoint.create_verifier(orequest, {})
oauth_request.verifier = verifier["oauth_verifier"]
oauth_request.user = request.user.id
oauth_request.save()
# find client & build context
client = Client.query.filter_by(id=oauth_request.client).first()
authorize_form = AuthorizeForm(WTFormData({
"oauth_token": oauth_request.token,
"oauth_verifier": oauth_request.verifier
}))
context = {
"user": request.user,
"oauth_request": oauth_request,
"client": client,
"authorize_form": authorize_form,
}
# AuthorizationEndpoint
return render_to_response(
request,
"mediagoblin/api/authorize.html",
context
)
def authorize_finish(request):
""" Finishes the authorize """
_ = pass_to_ugettext
token = request.form["oauth_token"]
verifier = request.form["oauth_verifier"]
oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier)
oauth_request = oauth_request.first()
if oauth_request is None:
# invalid token or verifier
err_msg = _("No request token found.")
return render_400(request, err_msg)
oauth_request.used = True
oauth_request.updated = datetime.datetime.now()
oauth_request.save()
if oauth_request.callback == "oob":
# out of bounds
context = {"oauth_request": oauth_request}
return render_to_response(
request,
"mediagoblin/api/oob.html",
context
)
# okay we need to redirect them then!
querystring = "?oauth_token={0}&oauth_verifier={1}".format(
oauth_request.token,
oauth_request.verifier
)
return redirect(
request,
querystring=querystring,
location=oauth_request.callback
)
@csrf_exempt
def access_token(request):
""" Provides an access token based on a valid verifier and request token """
data = request.headers
parsed_tokens = decode_authorization_header(data)
if parsed_tokens == dict() or "oauth_token" not in parsed_tokens:
error = "Missing required parameter."
return json_response({"error": error}, status=400)
request.oauth_token = parsed_tokens["oauth_token"]
request_validator = GMGRequestValidator(data)
av = AccessTokenEndpoint(request_validator)
tokens = av.create_access_token(request, {})
return form_response(tokens)

View File

@@ -51,30 +51,6 @@ class Auth(object):
def __call__(self, request, *args, **kw):
raise NotImplemented()
def json_response(serializable, _disable_cors=False, *args, **kw):
'''
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
Response.__init__ method.
'''
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'}
for key, value in cors_headers.iteritems():
response.headers.set(key, value)
return response
def get_entry_serializable(entry, urlgen):
'''
Returns a serializable dict() of a MediaEntry instance.

View File

@@ -21,11 +21,11 @@ from os.path import splitext
from werkzeug.exceptions import BadRequest, Forbidden
from werkzeug.wrappers import Response
from mediagoblin.tools.response import json_response
from mediagoblin.decorators import require_active_login
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.media_types import sniff_media
from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \
json_response
from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable
from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \
run_process_media, new_upload_entry

View File

@@ -0,0 +1,24 @@
.. _basic_auth-chapter:
===================
basic_auth plugin
===================
The basic_auth plugin is enabled by default in mediagoblin.ini. This plugin
provides basic username and password authentication for GNU Mediagoblin.
This plugin can be enabled alongside :ref:`openid-chapter` and
:ref:`persona-chapter`.
Set up the basic_auth plugin
============================
1. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section::
[[mediagoblin.plugins.basic_auth]]
2. Run::
gmg assetlink
in order to link basic_auth's static assets

View File

@@ -35,22 +35,22 @@ def setup_plugin():
routes = [
('mediagoblin.plugins.oauth.authorize',
'/oauth/authorize',
'/oauth-2/authorize',
'mediagoblin.plugins.oauth.views:authorize'),
('mediagoblin.plugins.oauth.authorize_client',
'/oauth/client/authorize',
'/oauth-2/client/authorize',
'mediagoblin.plugins.oauth.views:authorize_client'),
('mediagoblin.plugins.oauth.access_token',
'/oauth/access_token',
'/oauth-2/access_token',
'mediagoblin.plugins.oauth.views:access_token'),
('mediagoblin.plugins.oauth.list_connections',
'/oauth/client/connections',
'/oauth-2/client/connections',
'mediagoblin.plugins.oauth.views:list_connections'),
('mediagoblin.plugins.oauth.register_client',
'/oauth/client/register',
'/oauth-2/client/register',
'mediagoblin.plugins.oauth.views:register_client'),
('mediagoblin.plugins.oauth.list_clients',
'/oauth/client/list',
'/oauth-2/client/list',
'mediagoblin.plugins.oauth.views:list_clients')]
pluginapi.register_routes(routes)

View File

@@ -23,7 +23,7 @@ from datetime import datetime
from functools import wraps
from mediagoblin.plugins.api.tools import json_response
from mediagoblin.tools.response import json_response
def require_client_auth(controller):

View File

@@ -21,7 +21,7 @@ from urllib import urlencode
from werkzeug.exceptions import BadRequest
from mediagoblin.tools.response import render_to_response, redirect
from mediagoblin.tools.response import render_to_response, redirect, json_response
from mediagoblin.decorators import require_active_login
from mediagoblin.messages import add_message, SUCCESS
from mediagoblin.tools.translate import pass_to_ugettext as _
@@ -31,7 +31,6 @@ from mediagoblin.plugins.oauth.forms import ClientRegistrationForm, \
AuthorizationForm
from mediagoblin.plugins.oauth.tools import require_client_auth, \
create_token
from mediagoblin.plugins.api.tools import json_response
_log = logging.getLogger(__name__)

View File

@@ -0,0 +1,34 @@
.. _openid-chapter:
===================
openid plugin
===================
The openid plugin allows user to login to your GNU Mediagoblin instance using
their openid url.
This plugin can be enabled alongside :ref:`basic_auth-chapter` and
:ref:`persona-chapter`.
.. note::
When :ref:`basic_auth-chapter` is enabled alongside this openid plugin, and
a user creates an account using their openid. If they would like to add a
password to their account, they can use the forgot password feature to do
so.
Set up the openid plugin
============================
1. Install the ``python-openid`` package.
2. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section::
[[mediagoblin.plugins.openid]]
3. Run::
gmg dbupdate
in order to create and apply migrations to any database tables that the
plugin requires.

View File

@@ -120,4 +120,6 @@ hooks = {
'auth_no_pass_redirect': no_pass_redirect,
('mediagoblin.auth.register',
'mediagoblin/auth/register.html'): add_to_form_context,
('mediagoblin.auth.login',
'mediagoblin/auth/login.html'): add_to_form_context
}

View File

@@ -44,6 +44,7 @@
{% trans %}Log in to create an account!{% endtrans %}
</p>
{% endif %}
{% template_hook('login_link') %}
{% if pass_auth is defined %}
<p>
<a href="{{ request.urlgen('mediagoblin.auth.login') }}?{{ request.query_string }}">

View File

@@ -17,9 +17,11 @@
#}
{% block openid_login_link %}
{% if openid_link is defined %}
<p>
<a href="{{ request.urlgen('mediagoblin.plugins.openid.login') }}?{{ request.query_string }}">
{%- trans %}Or login with OpenID!{% endtrans %}
</a>
</p>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,41 @@
.. _persona-chapter:
================
persona plugin
================
The persona plugin allows users to login to you GNU MediaGoblin instance using
`Mozilla Persona`_.
This plugin can be enabled alongside :ref:`openid-chapter` and
:ref:`basic_auth-chapter`.
.. note::
When :ref:`basic_auth-chapter` is enabled alongside this persona plugin, and
a user creates an account using their persona. If they would like to add a
password to their account, they can use the forgot password feature to do
so.
.. _Mozilla Persona: https://www.mozilla.org/en-US/persona/
Set up the persona plugin
=========================
1. Install the ``requests`` package.
2. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section::
[[mediagoblin.plugins.persona]]
3. Run::
gmg dbupdate
in order to create and apply migrations to any database tables that the
plugin requires.
4. Run::
gmg assetlink
in order to persona's static assets.

View File

@@ -0,0 +1,116 @@
# 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 pkg_resources import resource_filename
import os
from sqlalchemy import or_
from mediagoblin.auth.tools import create_basic_user
from mediagoblin.db.models import User
from mediagoblin.plugins.persona.models import PersonaUserEmails
from mediagoblin.tools import pluginapi
from mediagoblin.tools.staticdirect import PluginStatic
from mediagoblin.tools.translate import pass_to_ugettext as _
PLUGIN_DIR = os.path.dirname(__file__)
def setup_plugin():
config = pluginapi.get_config('mediagoblin.plugins.persona')
routes = [
('mediagoblin.plugins.persona.login',
'/auth/persona/login/',
'mediagoblin.plugins.persona.views:login'),
('mediagoblin.plugins.persona.register',
'/auth/persona/register/',
'mediagoblin.plugins.persona.views:register'),
('mediagoblin.plugins.persona.edit',
'/edit/persona/',
'mediagoblin.plugins.persona.views:edit'),
('mediagoblin.plugins.persona.add',
'/edit/persona/add/',
'mediagoblin.plugins.persona.views:add')]
pluginapi.register_routes(routes)
pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
pluginapi.register_template_hooks(
{'persona_end': 'mediagoblin/plugins/persona/persona_js_end.html',
'persona_form': 'mediagoblin/plugins/persona/persona.html',
'edit_link': 'mediagoblin/plugins/persona/edit_link.html',
'login_link': 'mediagoblin/plugins/persona/login_link.html',
'register_link': 'mediagoblin/plugins/persona/register_link.html'})
def create_user(register_form):
if 'persona_email' in register_form:
username = register_form.username.data
user = User.query.filter(
or_(
User.username == username,
User.email == username,
)).first()
if not user:
user = create_basic_user(register_form)
new_entry = PersonaUserEmails()
new_entry.persona_email = register_form.persona_email.data
new_entry.user_id = user.id
new_entry.save()
return user
def extra_validation(register_form):
persona_email = register_form.persona_email.data if 'persona_email' in \
register_form else None
if persona_email:
persona_email_exists = PersonaUserEmails.query.filter_by(
persona_email=persona_email
).count()
extra_validation_passes = True
if persona_email_exists:
register_form.persona_email.errors.append(
_('Sorry, an account is already registered to that Persona'
' email.'))
extra_validation_passes = False
return extra_validation_passes
def Auth():
return True
def add_to_global_context(context):
if len(pluginapi.hook_runall('authentication')) == 1:
context['persona_auth'] = True
context['persona'] = True
return context
hooks = {
'setup': setup_plugin,
'authentication': Auth,
'auth_extra_validation': extra_validation,
'auth_create_user': create_user,
'template_global_context': add_to_global_context,
'static_setup': lambda: PluginStatic(
'coreplugin_persona',
resource_filename('mediagoblin.plugins.persona', 'static'))
}

View File

@@ -0,0 +1,41 @@
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import wtforms
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from mediagoblin.auth.tools import normalize_user_or_email_field
class RegistrationForm(wtforms.Form):
username = wtforms.TextField(
_('Username'),
[wtforms.validators.Required(),
normalize_user_or_email_field(allow_email=False)])
email = wtforms.TextField(
_('Email address'),
[wtforms.validators.Required(),
normalize_user_or_email_field(allow_user=False)])
persona_email = wtforms.HiddenField(
'',
[wtforms.validators.Required(),
normalize_user_or_email_field(allow_user=False)])
class EditForm(wtforms.Form):
email = wtforms.TextField(
_('Email address'),
[wtforms.validators.Required(),
normalize_user_or_email_field(allow_user=False)])

View File

@@ -0,0 +1,36 @@
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sqlalchemy import Column, Integer, Unicode, ForeignKey
from sqlalchemy.orm import relationship, backref
from mediagoblin.db.models import User
from mediagoblin.db.base import Base
class PersonaUserEmails(Base):
__tablename__ = "persona__user_emails"
id = Column(Integer, primary_key=True)
persona_email = Column(Unicode, nullable=False)
user_id = Column(Integer, ForeignKey(User.id), nullable=False)
# Persona's are owned by their user, so do the full thing.
user = relationship(User, backref=backref('persona_emails',
cascade='all, delete-orphan'))
MODELS = [
PersonaUserEmails
]

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/>.
*/
$(document).ready(function () {
var signinLink = document.getElementById('persona_login');
if (signinLink) {
signinLink.onclick = function() { navigator.id.request(); };
}
var signinLink1 = document.getElementById('persona_login1');
if (signinLink1) {
signinLink1.onclick = function() { navigator.id.request(); };
}
var signoutLink = document.getElementById('logout');
if (signoutLink) {
signoutLink.onclick = function() { navigator.id.logout(); };
}
var logout_url = document.getElementById('_logout_url').value;
navigator.id.watch({
onlogin: function(assertion) {
document.getElementById('_assertion').value = assertion;
document.getElementById('_persona_login').submit()
},
onlogout: function() {
$.ajax({
type: 'GET',
url: logout_url,
success: function(res, status, xhr) { window.location.reload(); },
error: function(xhr, status, err) { alert("Logout failure: " + err); }
});
}
});
});

View File

@@ -0,0 +1,43 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block title -%}
{% trans %}Add an OpenID{% endtrans %} &mdash; {{ super() }}
{%- endblock %}
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.plugins.persona.edit') }}"
method="POST" enctype="multipart/form-data">
{{ csrf_token }}
<div class="form_box">
<h1>{% trans %}Delete a Persona email address{% endtrans %}</h1>
<p>
<a href="javascript:;" id="persona_login">
{% trans %}Add a Persona email address{% endtrans %}
</a>
</p>
{{ wtforms_util.render_divs(form, True) }}
<div class="form_submit_buttons">
<input type="submit" value="{% trans %}Delete{% endtrans %}" class="button_form"/>
</div>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% block persona_edit_link %}
<p>
<a href="{{ request.urlgen('mediagoblin.plugins.persona.edit') }}">
{% trans %}Edit your Persona email addresses{% endtrans %}
</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% block person_login_link %}
<p>
<a href="javascript:;" id="persona_login">
{% trans %}Or login with Persona!{% endtrans %}
</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,32 @@
{#
# 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 persona %}
<form id="_persona_login"
action=
{%- if edit_persona is defined -%}
"{{ request.urlgen('mediagoblin.plugins.persona.add') }}"
{%- else -%}
"{{ request.urlgen('mediagoblin.plugins.persona.login') }}"
{%- endif %}
method="POST">
{{ csrf_token }}
<input type="hidden" name="assertion" type="text" id="_assertion"/>
<input type="hidden" name="_logout_url" type="text" id="_logout_url"
value="{{ request.urlgen('mediagoblin.auth.logout') }}"/>
</form>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
<script src="https://login.persona.org/include.js"></script>
<script type="text/javascript"
src="{{ request.staticdirect('/js/persona.js', 'coreplugin_persona') }}"></script>

View File

@@ -0,0 +1,25 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% block persona_register_link %}
<p>
<a href="javascript:;" id="persona_login">
{% trans %}Or register with Persona!{% endtrans %}
</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,191 @@
# 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 logging
import requests
from werkzeug.exceptions import BadRequest
from mediagoblin import messages, mg_globals
from mediagoblin.auth.tools import register_user
from mediagoblin.decorators import (auth_enabled, allow_registration,
require_active_login)
from mediagoblin.tools.response import render_to_response, redirect
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.plugins.persona import forms
from mediagoblin.plugins.persona.models import PersonaUserEmails
_log = logging.getLogger(__name__)
def _get_response(request):
if 'assertion' not in request.form:
_log.debug('assertion not in request.form')
raise BadRequest()
data = {'assertion': request.form['assertion'],
'audience': request.urlgen('index', qualified=True)}
resp = requests.post('https://verifier.login.persona.org/verify',
data=data, verify=True)
if resp.ok:
verification_data = json.loads(resp.content)
if verification_data['status'] == 'okay':
return verification_data['email']
return None
@auth_enabled
def login(request):
if request.method == 'GET':
return redirect(request, 'mediagoblin.auth.login')
email = _get_response(request)
if email:
query = PersonaUserEmails.query.filter_by(
persona_email=email
).first()
user = query.user if query else None
if user:
request.session['user_id'] = unicode(user.id)
request.session.save()
return redirect(request, "index")
else:
if not mg_globals.app.auth:
messages.add_message(
request,
messages.WARNING,
_('Sorry, authentication is disabled on this instance.'))
return redirect(request, 'index')
register_form = forms.RegistrationForm(email=email,
persona_email=email)
return render_to_response(
request,
'mediagoblin/auth/register.html',
{'register_form': register_form,
'post_url': request.urlgen(
'mediagoblin.plugins.persona.register')})
return redirect(request, 'mediagoblin.auth.login')
@allow_registration
@auth_enabled
def register(request):
if request.method == 'GET':
# Need to connect to persona before registering a user. If method is
# 'GET', then this page was acessed without logging in first.
return redirect(request, 'mediagoblin.auth.login')
register_form = forms.RegistrationForm(request.form)
if register_form.validate():
user = register_user(request, register_form)
if user:
# redirect the user to their homepage... there will be a
# message waiting for them to verify their email
return redirect(
request, 'mediagoblin.user_pages.user_home',
user=user.username)
return render_to_response(
request,
'mediagoblin/auth/register.html',
{'register_form': register_form,
'post_url': request.urlgen('mediagoblin.plugins.persona.register')})
@require_active_login
def edit(request):
form = forms.EditForm(request.form)
if request.method == 'POST' and form.validate():
query = PersonaUserEmails.query.filter_by(
persona_email=form.email.data)
user = query.first().user if query.first() else None
if user and user.id == int(request.user.id):
count = len(user.persona_emails)
if count > 1 or user.pw_hash:
# User has more then one Persona email or also has a password.
query.first().delete()
messages.add_message(
request,
messages.SUCCESS,
_('The Persona email address was successfully removed.'))
return redirect(request, 'mediagoblin.edit.account')
elif not count > 1:
form.email.errors.append(
_("You can't delete your only Persona email address unless"
" you have a password set."))
else:
form.email.errors.append(
_('That Persona email address is not registered to this'
' account.'))
return render_to_response(
request,
'mediagoblin/plugins/persona/edit.html',
{'form': form,
'edit_persona': True})
@require_active_login
def add(request):
if request.method == 'GET':
return redirect(request, 'mediagoblin.plugins.persona.edit')
email = _get_response(request)
if email:
query = PersonaUserEmails.query.filter_by(
persona_email=email
).first()
user_exists = query.user if query else None
if user_exists:
messages.add_message(
request,
messages.WARNING,
_('Sorry, an account is already registered with that Persona'
' email address.'))
return redirect(request, 'mediagoblin.plugins.persona.edit')
else:
# Save the Persona Email to the user
new_entry = PersonaUserEmails()
new_entry.persona_email = email
new_entry.user_id = request.user.id
new_entry.save()
messages.add_message(
request,
messages.SUCCESS,
_('Your Person email address was saved successfully.'))
return redirect(request, 'mediagoblin.edit.account')

View File

@@ -184,7 +184,6 @@ class BaseProcessingFail(Exception):
def __init__(self, **metadata):
self.metadata = metadata or {}
class BadMediaFail(BaseProcessingFail):
"""
Error that should be raised when an inappropriate file was given

View File

@@ -18,11 +18,13 @@ import logging
import urllib
import urllib2
from celery import registry, task
import celery
from celery.registry import tasks
from mediagoblin import mg_globals as mgg
from mediagoblin.db.models import MediaEntry
from . import mark_entry_failed, BaseProcessingFail, ProcessingState
from mediagoblin.processing import (mark_entry_failed, BaseProcessingFail,
ProcessingState)
from mediagoblin.tools.processing import json_processing_callback
_log = logging.getLogger(__name__)
@@ -30,7 +32,7 @@ logging.basicConfig()
_log.setLevel(logging.DEBUG)
@task.task(default_retry_delay=2 * 60)
@celery.task(default_retry_delay=2 * 60)
def handle_push_urls(feed_url):
"""Subtask, notifying the PuSH servers of new content
@@ -60,14 +62,16 @@ def handle_push_urls(feed_url):
'Giving up.'.format(feed_url))
return False
################################
# Media processing initial steps
################################
class ProcessMedia(task.Task):
class ProcessMedia(celery.Task):
"""
Pass this entry off for processing.
"""
track_started=True
def run(self, media_id, feed_url):
"""
Pass the media entry off to the appropriate processing function
@@ -140,6 +144,4 @@ class ProcessMedia(task.Task):
entry = mgg.database.MediaEntry.query.filter_by(id=entry_id).first()
json_processing_callback(entry)
# Register the task
process_media = registry.tasks[ProcessMedia.name]
tasks.register(ProcessMedia)

View File

@@ -38,8 +38,8 @@ def get_url_map():
import mediagoblin.webfinger.routing
import mediagoblin.listings.routing
import mediagoblin.notifications.routing
import mediagoblin.oauth.routing
for route in PluginManager().get_routes():
add_route(*route)

View File

@@ -812,3 +812,10 @@ pre {
#exif_additional_info table tr {
margin-bottom: 10px;
}
p.verifier {
text-align:center;
font-size:50px;
none repeat scroll 0% 0% rgb(221, 221, 221);
padding: 1em 0px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -15,12 +15,25 @@
* 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/>.
*/
var content="";
function previewComment(){
if ($('#comment_content').val() && (content != $('#comment_content').val())) {
content = $('#comment_content').val();
$.post($('#previewURL').val(),$('#form_comment').serialize(),
function(data){
preview = JSON.parse(data)
$('#comment_preview').replaceWith("<div id=comment_preview><h3>" + $('#previewText').val() +"</h3><br />" + preview.content +
"<hr style='border: 1px solid #333;' /></div>");
});
}
}
$(document).ready(function(){
$('#form_comment').hide();
$('#button_addcomment').click(function(){
$(this).fadeOut('fast');
$('#form_comment').slideDown(function(){
setInterval("previewComment()",1000);
$('#comment_content').focus();
});
});

View File

@@ -21,7 +21,7 @@ from werkzeug.datastructures import FileStorage
from mediagoblin.db.models import MediaEntry
from mediagoblin.processing import mark_entry_failed
from mediagoblin.processing.task import process_media
from mediagoblin.processing.task import ProcessMedia
_log = logging.getLogger(__name__)
@@ -85,7 +85,7 @@ def run_process_media(entry, feed_url=None):
'mediagoblin.user_pages.atom_feed',qualified=True,
user=request.user.username)`"""
try:
process_media.apply_async(
ProcessMedia().apply_async(
[entry.id, feed_url], {},
task_id=entry.queued_task_id)
except BaseException as exc:

View File

@@ -90,7 +90,7 @@ def submit_start(request):
# Save now so we have this data before kicking off processing
entry.save()
# Pass off to processing
# Pass off to async processing
#
# (... don't change entry after this point to avoid race
# conditions with changes to the document via processing code)
@@ -98,6 +98,7 @@ def submit_start(request):
'mediagoblin.user_pages.atom_feed',
qualified=True, user=request.user.username)
run_process_media(entry, feed_url)
add_message(request, SUCCESS, _('Woohoo! Submitted!'))
add_comment_subscription(request.user, entry)

View File

@@ -0,0 +1,56 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "mediagoblin/base.html" %}
{% block title -%}
{% trans %}Authorization{% endtrans %} &mdash; {{ super() }}
{%- endblock %}
{% block mediagoblin_content %}
<h1>{% trans %}Authorize{% endtrans %}</h1>
<p>
{% trans %}You are logged in as{% endtrans %}
<strong>{{user.username}}</strong>
<br /><br />
{% trans %}Do you want to authorize {% endtrans %}
{% if client.application_name -%}
<em>{{ client.application_name }}</em>
{%- else -%}
<em>{% trans %}an unknown application{% endtrans %}</em>
{%- endif %}
{% trans %} to access your account? {% endtrans %}
<br /><br />
{% trans %}Applications with access to your account can: {% endtrans %}
<ul>
<li>{% trans %}Post new media as you{% endtrans %}</li>
<li>{% trans %}See your information (e.g profile, meida, etc...){% endtrans %}</li>
<li>{% trans %}Change your information{% endtrans %}</li>
</ul>
<br />
<form method="POST">
{{ csrf_token }}
{{ authorize_form.oauth_token }}
{{ authorize_form.oauth_verifier }}
<input type="submit" value="{% trans %}Authorize{% endtrans %}">
</form>
</p>
{% endblock %}

View File

@@ -0,0 +1,33 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "mediagoblin/base.html" %}
{% block title -%}
{% trans %}Authorization Finished{% endtrans %} &mdash; {{ super() }}
{%- endblock %}
{% block mediagoblin_content %}
<h1>{% trans %}Authorization Complete{% endtrans %}</h1>
<h4>{% trans %}Copy and paste this into your client:{% endtrans %}</h4>
<p class="verifier">
{{ oauth_request.verifier }}
</p>
{% endblock %}

View File

@@ -23,6 +23,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<title>{% block title %}{{ app_config['html_title'] }}{% endblock %}</title>
<link rel="stylesheet" type="text/css"
href="{{ request.staticdirect('/css/extlib/reset.css') }}"/>
@@ -60,24 +61,35 @@
{%- if request.user %}
{% if request.user and request.user.status == 'active' %}
{% set notification_count = request.notifications.get_notification_count(request.user.id) %}
{% set notification_count = get_notification_count(request.user.id) %}
{% if notification_count %}
<a href="#notifications" class="notification-gem button_action" title="Notifications">
{{ notification_count }}</a>
<a href="javascript:;" class="notification-gem button_action" title="Notifications">
{{ notification_count }}</a>
{% endif %}
<div class="button_action header_dropdown_down">&#9660;</div>
<div class="button_action header_dropdown_up">&#9650;</div>
<a href="javascript:;" class="button_action header_dropdown_down">&#9660;</a>
<a href="javascript:;" class="button_action header_dropdown_up">&#9650;</a>
{% elif request.user and request.user.status == "needs_email_verification" %}
{# the following link should only appear when verification is needed #}
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
user=request.user.username) }}"
class="button_action_highlight">
{% trans %}Verify your email!{% endtrans %}</a>
or <a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a>
or <a id="logout" href=
{% if persona is not defined %}
"{{ request.urlgen('mediagoblin.auth.logout') }}"
{% else %}
"javascript:;"
{% endif %}
>{% trans %}log out{% endtrans %}</a>
{% endif %}
{%- elif auth %}
<a href="{{ request.urlgen('mediagoblin.auth.login') }}?next={{
request.base_url|urlencode }}">
<a href=
{% if persona_auth is defined %}
"javascript:;" id="persona_login"
{% else %}
"{{ request.urlgen('mediagoblin.auth.login') }}"
{% endif %}
>
{%- trans %}Log in{% endtrans -%}
</a>
{%- endif %}
@@ -101,7 +113,13 @@
{%- trans %}Media processing panel{% endtrans -%}
</a>
&middot;
<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}Log out{% endtrans %}</a>
<a id="logout" href=
{% if persona is not defined %}
"{{ request.urlgen('mediagoblin.auth.logout') }}"
{% else %}
"javascript:;"
{% endif %}
>{% trans %}Log out{% endtrans %}</a>
</p>
<a class="button_action" href="{{ request.urlgen('mediagoblin.submit.start') }}">
{%- trans %}Add media{% endtrans -%}
@@ -134,6 +152,9 @@
{% include "mediagoblin/utils/messages.html" %}
{% block mediagoblin_content %}
{% endblock mediagoblin_content %}
{% if csrf_token is defined %}
{% template_hook("persona_form") %}
{% endif %}
</div>
{%- include "mediagoblin/bits/base_footer.html" %}
</div>

View File

@@ -15,3 +15,5 @@
# 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/>.
-#}
{% template_hook("persona_end") %}

View File

@@ -19,21 +19,27 @@
{% if request.user %}
<h1>{% trans %}Explore{% endtrans %}</h1>
{% else %}
<img class="right_align" src="{{ request.staticdirect('/images/home_goblin.png') }}" />
<h1>{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}</h1>
<img class="right_align" src="{{ request.staticdirect('/images/frontpage_image.png') }}" />
<p>{% trans %}This site is running <a href="http://mediagoblin.org">MediaGoblin</a>, an extraordinarily great piece of media hosting software.{% endtrans %}</p>
{% if auth %}
<p>{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}</p>
{% if allow_registration %}
<p>{% trans %}Don't have one yet? It's easy!{% endtrans %}</p>
{% trans register_url=request.urlgen('mediagoblin.auth.register') -%}
<a class="button_action_highlight" href="{{ register_url }}">Create an account at this site</a>
<a class="button_action_highlight" href=
{% if persona_auth is defined %}
"javascript:;" id="persona_login1"
{% else %}
"{{ request.urlgen('mediagoblin.auth.register') }}"
{% endif %}
{% trans %}
>Create an account at this site</a>
or
{%- endtrans %}
{% endif %}
{% endif %}
{% trans %}
<a class="button_action" href="http://wiki.mediagoblin.org/HackingHowto">Set up MediaGoblin on your own server</a>
<a class="button_action" href="http://mediagoblin.readthedocs.org/">Set up MediaGoblin on your own server</a>
{%- endtrans %}
<div class="clear"></div>

View File

@@ -1,4 +1,4 @@
{% set notifications = request.notifications.get_notifications(request.user.id) %}
{% set notifications = get_notifications(request.user.id) %}
{% if notifications %}
<div class="header_notifications">
<h3>{% trans %}New comments{% endtrans %}</h3>

View File

@@ -90,7 +90,8 @@
{% if app_config['allow_comments'] %}
<a
{% if not request.user %}
href="{{ request.urlgen('mediagoblin.auth.login') }}"
href="{{ request.urlgen('mediagoblin.auth.login') }}?next={{
request.base_url|urlencode }}"
{% endif %}
class="button_action" id="button_addcomment" title="Add a comment">
{% trans %}Add a comment{% endtrans %}
@@ -107,7 +108,10 @@
<input type="submit" value="{% trans %}Add this comment{% endtrans %}" class="button_action" />
{{ csrf_token }}
</div>
<input type="hidden" value="{{ request.urlgen('mediagoblin.user_pages.media_preview_comment') }}" id="previewURL" />
<input type="hidden" value="{% trans %}Comment Preview{% endtrans %}" id="previewText"/>
</form>
<div id="comment_preview"></div>
{% endif %}
<ul style="list-style:none">
{% for comment in comments %}

View File

@@ -16,8 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{%- if request.user %}
{% set subscription = request.notifications.get_comment_subscription(
request.user.id, media.id) %}
{% set subscription = get_comment_subscription(request.user.id, media.id) %}
{% if not subscription or not subscription.notify %}
<a type="submit" href="{{ request.urlgen('mediagoblin.notifications.subscribe_comments',
user=media.get_uploader.username,

View File

@@ -0,0 +1,42 @@
# 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/>.
[mediagoblin]
direct_remote_path = /test_static/
email_sender_address = "notice@mediagoblin.example.org"
email_debug_mode = true
# TODO: Switch to using an in-memory database
sql_engine = "sqlite:///%(here)s/user_dev/mediagoblin.db"
# Celery shouldn't be set up by the application as it's setup via
# mediagoblin.init.celery.from_celery
celery_setup_elsewhere = true
[storage:publicstore]
base_dir = %(here)s/user_dev/media/public
base_url = /mgoblin_media/
[storage:queuestore]
base_dir = %(here)s/user_dev/media/queue
[celery]
CELERY_ALWAYS_EAGER = true
CELERY_RESULT_DBURI = "sqlite:///%(here)s/user_dev/celery.db"
BROKER_HOST = "sqlite:///%(here)s/user_dev/kombu.db"
[plugins]
[[mediagoblin.plugins.persona]]

View File

@@ -55,6 +55,6 @@ def test_setup_celery_from_config():
pkg_resources.resource_filename('mediagoblin.tests', 'celery.db'))
assert fake_celery_module.BROKER_TRANSPORT == 'sqlalchemy'
assert fake_celery_module.BROKER_HOST == (
assert fake_celery_module.BROKER_URL == (
'sqlite:///' +
pkg_resources.resource_filename('mediagoblin.tests', 'kombu.db'))

View File

@@ -23,7 +23,7 @@ from mediagoblin import mg_globals
from mediagoblin.tools import processing
from mediagoblin.tests.tools import fixture_add_user
from mediagoblin.tests.test_submission import GOOD_PNG
from mediagoblin.tests import test_oauth as oauth
from mediagoblin.tests import test_oauth2 as oauth
class TestHTTPCallback(object):
@@ -44,7 +44,7 @@ class TestHTTPCallback(object):
'password': self.user_password})
def get_access_token(self, client_id, client_secret, code):
response = self.test_app.get('/oauth/access_token', {
response = self.test_app.get('/oauth-2/access_token', {
'code': code,
'client_id': client_id,
'client_secret': client_secret})

View File

@@ -23,7 +23,7 @@ base_dir = %(here)s/user_dev/media/queue
[celery]
CELERY_ALWAYS_EAGER = true
CELERY_RESULT_DBURI = "sqlite:///%(here)s/user_dev/celery.db"
BROKER_HOST = "sqlite:///%(here)s/user_dev/kombu.db"
BROKER_URL = "sqlite:///%(here)s/test_user_dev/kombu.db"
[plugins]
[[mediagoblin.plugins.api]]

View File

@@ -0,0 +1,166 @@
# 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 cgi
import pytest
from urlparse import parse_qs, urlparse
from oauthlib.oauth1 import Client
from mediagoblin import mg_globals
from mediagoblin.tools import template, pluginapi
from mediagoblin.tests.tools import fixture_add_user
class TestOAuth(object):
MIME_FORM = "application/x-www-form-urlencoded"
MIME_JSON = "application/json"
@pytest.fixture(autouse=True)
def setup(self, test_app):
self.test_app = test_app
self.db = mg_globals.database
self.pman = pluginapi.PluginManager()
self.user_password = "AUserPassword123"
self.user = fixture_add_user("OAuthy", self.user_password)
self.login()
def login(self):
self.test_app.post(
"/auth/login/", {
"username": self.user.username,
"password": self.user_password})
def register_client(self, **kwargs):
""" Regiters a client with the API """
kwargs["type"] = "client_associate"
kwargs["application_type"] = kwargs.get("application_type", "native")
return self.test_app.post("/api/client/register", kwargs)
def test_client_client_register_limited_info(self):
""" Tests that a client can be registered with limited information """
response = self.register_client()
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
def test_client_register_full_info(self):
""" Provides every piece of information possible to register client """
query = {
"application_name": "Testificate MD",
"application_type": "web",
"contacts": "someone@someplace.com tuteo@tsengeo.lu",
"logo_url": "http://ayrel.com/utral.png",
"redirect_uris": "http://navi-kosman.lu http://gmg-yawne-oeru.lu",
}
response = self.register_client(**query)
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"]
assert client.redirect_uri == query["redirect_uris"].split()
assert client.logo_url == query["logo_url"]
assert client.contacts == query["contacts"].split()
def test_client_update(self):
""" Tests that you can update a client """
# first we need to register a client
response = self.register_client()
client_info = response.json
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
# Now update
update_query = {
"type": "client_update",
"application_name": "neytiri",
"contacts": "someone@someplace.com abc@cba.com",
"logo_url": "http://place.com/picture.png",
"application_type": "web",
"redirect_uris": "http://blah.gmg/whatever https://inboxen.org/",
}
update_response = self.register_client(**update_query)
assert update_response.status_int == 200
client_info = update_response.json
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
assert client.secret == client_info["client_secret"]
assert client.application_type == update_query["application_type"]
assert client.application_name == update_query["application_name"]
assert client.contacts == update_query["contacts"].split()
assert client.logo_url == update_query["logo_url"]
assert client.redirect_uri == update_query["redirect_uris"].split()
def to_authorize_headers(self, data):
headers = ""
for key, value in data.items():
headers += '{0}="{1}",'.format(key, value)
return {"Authorization": "OAuth " + headers[:-1]}
def test_request_token(self):
""" Test a request for a request token """
response = self.register_client()
client_id = response.json["client_id"]
endpoint = "/oauth/request_token"
request_query = {
"oauth_consumer_key": client_id,
"oauth_nonce": "abcdefghij",
"oauth_timestamp": 123456789.0,
"oauth_callback": "https://some.url/callback",
}
headers = self.to_authorize_headers(request_query)
headers["Content-Type"] = self.MIME_FORM
response = self.test_app.post(endpoint, headers=headers)
response = cgi.parse_qs(response.body)
# each element is a list, reduce it to a string
for key, value in response.items():
response[key] = value[0]
request_token = self.db.RequestToken.query.filter_by(
token=response["oauth_token"]
).first()
client = self.db.Client.query.filter_by(id=client_id).first()
assert request_token is not None
assert request_token.secret == response["oauth_token_secret"]
assert request_token.client == client.id
assert request_token.used == False
assert request_token.callback == request_query["oauth_callback"]

View File

@@ -52,7 +52,7 @@ class TestOAuth(object):
def register_client(self, name, client_type, description=None,
redirect_uri=''):
return self.test_app.post(
'/oauth/client/register', {
'/oauth-2/client/register', {
'name': name,
'description': description,
'type': client_type,
@@ -116,7 +116,7 @@ class TestOAuth(object):
client_identifier = client.identifier
redirect_uri = 'https://foo.example'
response = self.test_app.get('/oauth/authorize', {
response = self.test_app.get('/oauth-2/authorize', {
'client_id': client.identifier,
'scope': 'all',
'redirect_uri': redirect_uri})
@@ -130,7 +130,7 @@ class TestOAuth(object):
# Short for client authorization post reponse
capr = self.test_app.post(
'/oauth/client/authorize', {
'/oauth-2/client/authorize', {
'client_id': form.client_id.data,
'allow': 'Allow',
'next': form.next.data})
@@ -156,7 +156,7 @@ class TestOAuth(object):
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == unicode(client_id)).first()
token_res = self.test_app.get('/oauth/access_token?client_id={0}&\
token_res = self.test_app.get('/oauth-2/access_token?client_id={0}&\
code={1}&client_secret={2}'.format(client_id, code, client.secret))
assert token_res.status_int == 200
@@ -184,7 +184,7 @@ code={1}&client_secret={2}'.format(client_id, code, client.secret))
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == unicode(client_id)).first()
token_res = self.test_app.get('/oauth/access_token?\
token_res = self.test_app.get('/oauth-2/access_token?\
code={0}&client_secret={1}'.format(code, client.secret))
assert token_res.status_int == 200
@@ -205,7 +205,7 @@ code={0}&client_secret={1}'.format(code, client.secret))
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == client_id).first()
token_res = self.test_app.get('/oauth/access_token',
token_res = self.test_app.get('/oauth-2/access_token',
{'refresh_token': token_data['refresh_token'],
'client_id': client_id,
'client_secret': client.secret

View File

@@ -0,0 +1,212 @@
# 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 urlparse
import pkg_resources
import pytest
import mock
pytest.importorskip("requests")
from mediagoblin import mg_globals
from mediagoblin.db.base import Session
from mediagoblin.tests.tools import get_app
from mediagoblin.tools import template
# App with plugin enabled
@pytest.fixture()
def persona_plugin_app(request):
return get_app(
request,
mgoblin_config=pkg_resources.resource_filename(
'mediagoblin.tests.auth_configs',
'persona_appconfig.ini'))
class TestPersonaPlugin(object):
def test_authentication_views(self, persona_plugin_app):
res = persona_plugin_app.get('/auth/login/')
assert urlparse.urlsplit(res.location)[2] == '/'
res = persona_plugin_app.get('/auth/register/')
assert urlparse.urlsplit(res.location)[2] == '/'
res = persona_plugin_app.get('/auth/persona/login/')
assert urlparse.urlsplit(res.location)[2] == '/auth/login/'
res = persona_plugin_app.get('/auth/persona/register/')
assert urlparse.urlsplit(res.location)[2] == '/auth/login/'
@mock.patch('mediagoblin.plugins.persona.views._get_response', mock.Mock(return_value=u'test@example.com'))
def _test_registration():
# No register users
template.clear_test_template_context()
res = persona_plugin_app.post(
'/auth/persona/login/', {})
assert 'mediagoblin/auth/register.html' in template.TEMPLATE_TEST_CONTEXT
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
register_form = context['register_form']
assert register_form.email.data == u'test@example.com'
assert register_form.persona_email.data == u'test@example.com'
template.clear_test_template_context()
res = persona_plugin_app.post(
'/auth/persona/register/', {})
assert 'mediagoblin/auth/register.html' in template.TEMPLATE_TEST_CONTEXT
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
register_form = context['register_form']
assert register_form.username.errors == [u'This field is required.']
assert register_form.email.errors == [u'This field is required.']
assert register_form.persona_email.errors == [u'This field is required.']
# Successful register
template.clear_test_template_context()
res = persona_plugin_app.post(
'/auth/persona/register/',
{'username': 'chris',
'email': 'chris@example.com',
'persona_email': 'test@example.com'})
res.follow()
assert urlparse.urlsplit(res.location)[2] == '/u/chris/'
assert 'mediagoblin/user_pages/user.html' in template.TEMPLATE_TEST_CONTEXT
# Try to register same Persona email address
template.clear_test_template_context()
res = persona_plugin_app.post(
'/auth/persona/register/',
{'username': 'chris1',
'email': 'chris1@example.com',
'persona_email': 'test@example.com'})
assert 'mediagoblin/auth/register.html' in template.TEMPLATE_TEST_CONTEXT
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
register_form = context['register_form']
assert register_form.persona_email.errors == [u'Sorry, an account is already registered to that Persona email.']
# Logout
persona_plugin_app.get('/auth/logout/')
# Get user and detach from session
test_user = mg_globals.database.User.query.filter_by(
username=u'chris').first()
test_user.email_verified = True
test_user.status = u'active'
test_user.save()
test_user = mg_globals.database.User.query.filter_by(
username=u'chris').first()
Session.expunge(test_user)
# Add another user for _test_edit_persona
persona_plugin_app.post(
'/auth/persona/register/',
{'username': 'chris1',
'email': 'chris1@example.com',
'persona_email': 'test1@example.com'})
# Log back in
template.clear_test_template_context()
res = persona_plugin_app.post(
'/auth/persona/login/')
res.follow()
assert urlparse.urlsplit(res.location)[2] == '/'
assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT
# Make sure user is in the session
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
session = context['request'].session
assert session['user_id'] == unicode(test_user.id)
_test_registration()
@mock.patch('mediagoblin.plugins.persona.views._get_response', mock.Mock(return_value=u'new@example.com'))
def _test_edit_persona():
# Try and delete only Persona email address
template.clear_test_template_context()
res = persona_plugin_app.post(
'/edit/persona/',
{'email': 'test@example.com'})
assert 'mediagoblin/plugins/persona/edit.html' in template.TEMPLATE_TEST_CONTEXT
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/persona/edit.html']
form = context['form']
assert form.email.errors == [u"You can't delete your only Persona email address unless you have a password set."]
template.clear_test_template_context()
res = persona_plugin_app.post(
'/edit/persona/', {})
assert 'mediagoblin/plugins/persona/edit.html' in template.TEMPLATE_TEST_CONTEXT
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/persona/edit.html']
form = context['form']
assert form.email.errors == [u'This field is required.']
# Try and delete Persona not owned by the user
template.clear_test_template_context()
res = persona_plugin_app.post(
'/edit/persona/',
{'email': 'test1@example.com'})
assert 'mediagoblin/plugins/persona/edit.html' in template.TEMPLATE_TEST_CONTEXT
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/persona/edit.html']
form = context['form']
assert form.email.errors == [u'That Persona email address is not registered to this account.']
res = persona_plugin_app.get('/edit/persona/add/')
assert urlparse.urlsplit(res.location)[2] == '/edit/persona/'
# Add Persona email address
template.clear_test_template_context()
res = persona_plugin_app.post(
'/edit/persona/add/')
res.follow()
assert urlparse.urlsplit(res.location)[2] == '/edit/account/'
# Delete a Persona
res = persona_plugin_app.post(
'/edit/persona/',
{'email': 'test@example.com'})
res.follow()
assert urlparse.urlsplit(res.location)[2] == '/edit/account/'
_test_edit_persona()
@mock.patch('mediagoblin.plugins.persona.views._get_response', mock.Mock(return_value=u'test1@example.com'))
def _test_add_existing():
template.clear_test_template_context()
res = persona_plugin_app.post(
'/edit/persona/add/')
res.follow()
assert urlparse.urlsplit(res.location)[2] == '/edit/persona/'
_test_add_existing()

View File

@@ -14,6 +14,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import string
import errno
import itsdangerous
import logging
@@ -24,6 +26,9 @@ from mediagoblin import mg_globals
_log = logging.getLogger(__name__)
# produces base64 alphabet
alphabet = string.ascii_letters + "-_"
base = len(alphabet)
# Use the system (hardware-based) random number generator if it exists.
# -- this optimization is lifted from Django
@@ -111,3 +116,13 @@ def get_timed_signer_url(namespace):
assert __itsda_secret is not None
return itsdangerous.URLSafeTimedSerializer(__itsda_secret,
salt=namespace)
def random_string(length):
""" Returns a URL safe base64 encoded crypographically strong string """
rstring = ""
for i in range(length):
n = getrandbits(6) # 6 bytes = 2^6 = 64
n = divmod(n, base)[1]
rstring += alphabet[n]
return rstring

View File

@@ -14,12 +14,18 @@
# 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 logging
from mediagoblin.db.models import User
_log = logging.getLogger(__name__)
# MIME-Types
form_encoded = "application/x-www-form-urlencoded"
json_encoded = "application/json"
def setup_user_in_request(request):
"""
Examine a request and tack on a request.user parameter if that's
@@ -36,3 +42,15 @@ def setup_user_in_request(request):
# this session.
_log.warn("Killing session for user id %r", request.session['user_id'])
request.session.delete()
def decode_request(request):
""" Decodes a request based on MIME-Type """
data = request.get_data()
if request.content_type == json_encoded:
data = json.loads(data)
elif request.content_type == form_encoded or request.content_type == "":
data = request.form
else:
data = ""
return data

View File

@@ -14,6 +14,8 @@
# 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 werkzeug.utils
from werkzeug.wrappers import Response as wz_Response
from mediagoblin.tools.template import render_template
@@ -33,7 +35,6 @@ def render_to_response(request, template, context, status=200):
render_template(request, template, context),
status=status)
def render_error(request, status=500, title=_('Oops!'),
err_msg=_('An error occured')):
"""Render any error page with a given error code, title and text body
@@ -46,6 +47,14 @@ def render_error(request, status=500, title=_('Oops!'),
{'err_code': status, 'title': title, 'err_msg': err_msg}),
status=status)
def render_400(request, err_msg=None):
""" Render a standard 400 page"""
_ = pass_to_ugettext
title = _("Bad Request")
if err_msg is None:
err_msg = _("The request sent to the server is invalid, please double check it")
return render_error(request, 400, title, err_msg)
def render_403(request):
"""Render a standard 403 page"""
@@ -121,3 +130,45 @@ def redirect_obj(request, obj):
Requires obj to have a .url_for_self method."""
return redirect(request, location=obj.url_for_self(request.urlgen))
def json_response(serializable, _disable_cors=False, *args, **kw):
'''
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
Response.__init__ method.
'''
response = wz_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'}
for key, value in cors_headers.iteritems():
response.headers.set(key, value)
return response
def form_response(data, *args, **kwargs):
"""
Responds using application/x-www-form-urlencoded and returns a werkzeug
Response object with the data argument as the body
and 'application/x-www-form-urlencoded' as the Content-Type.
Any extra arguments and keyword arguments are passed to the
Response.__init__ method.
"""
response = wz_Response(
data,
content_type="application/x-www-form-urlencoded",
*args,
**kwargs
)
return response

View File

@@ -32,7 +32,6 @@ from mediagoblin.tools.timesince import timesince
from mediagoblin.meddleware.csrf import render_csrf_form_token
SETUP_JINJA_ENVS = {}
@@ -50,6 +49,12 @@ def get_jinja_env(template_loader, locale):
if locale in SETUP_JINJA_ENVS:
return SETUP_JINJA_ENVS[locale]
# The default config does not require a [jinja2] block.
# You may create one if you wish to enable additional jinja2 extensions,
# see example in config_spec.ini
jinja2_config = mg_globals.global_config.get('jinja2', {})
local_exts = jinja2_config.get('extensions', [])
# jinja2.StrictUndefined will give exceptions on references
# to undefined/unknown variables in templates.
template_env = jinja2.Environment(
@@ -57,7 +62,7 @@ def get_jinja_env(template_loader, locale):
undefined=jinja2.StrictUndefined,
extensions=[
'jinja2.ext.i18n', 'jinja2.ext.autoescape',
TemplateHookExtension])
TemplateHookExtension] + local_exts)
template_env.install_gettext_callables(
mg_globals.thread_scope.translations.ugettext,
@@ -84,6 +89,16 @@ def get_jinja_env(template_loader, locale):
template_env.globals = hook_transform(
'template_global_context', template_env.globals)
#### THIS IS TEMPORARY, PLEASE FIX IT
## Notifications stuff is not yet a plugin (and we're not sure it will be),
## but it needs to add stuff to the context. This is THE WRONG WAY TO DO IT
from mediagoblin import notifications
template_env.globals['get_notifications'] = notifications.get_notifications
template_env.globals[
'get_notification_count'] = notifications.get_notification_count
template_env.globals[
'get_comment_subscription'] = notifications.get_comment_subscription
if exists(locale):
SETUP_JINJA_ENVS[locale] = template_env

View File

@@ -0,0 +1,46 @@
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from wtforms.validators import Email, URL
def validate_email(email):
"""
Validates an email
Returns True if valid and False if invalid
"""
email_re = Email().regex
result = email_re.match(email)
if result is None:
return False
else:
return result.string
def validate_url(url):
"""
Validates a url
Returns True if valid and False if invalid
"""
url_re = URL().regex
result = url_re.match(url)
if result is None:
return False
else:
return result.string

View File

@@ -23,7 +23,7 @@ class MediaCommentForm(wtforms.Form):
_('Comment'),
[wtforms.validators.Required()],
description=_(u'You can use '
u'<a href="http://daringfireball.net/projects/markdown/basics">'
u'<a href="http://daringfireball.net/projects/markdown/basics" target="_blank">'
u'Markdown</a> for formatting.'))
class ConfirmDeleteForm(wtforms.Form):
@@ -47,7 +47,7 @@ class MediaCollectForm(wtforms.Form):
collection_description = wtforms.TextAreaField(
_('Description of this collection'),
description=_("""You can use
<a href="http://daringfireball.net/projects/markdown/basics">
<a href="http://daringfireball.net/projects/markdown/basics" target="_blank">
Markdown</a> for formatting."""))
class CommentReportForm(wtforms.Form):

View File

@@ -36,6 +36,10 @@ add_route('mediagoblin.user_pages.media_post_comment',
'/u/<string:user>/m/<int:media_id>/comment/add/',
'mediagoblin.user_pages.views:media_post_comment')
add_route('mediagoblin.user_pages.media_preview_comment',
'/ajax/comment/preview/',
'mediagoblin.user_pages.views:media_preview_comment')
add_route('mediagoblin.user_pages.user_gallery',
'/u/<string:user>/gallery/',
'mediagoblin.user_pages.views:user_gallery')

View File

@@ -16,6 +16,7 @@
import logging
import datetime
import json
from mediagoblin import messages, mg_globals
from mediagoblin.db.models import (MediaEntry, MediaTag, Collection,
@@ -23,6 +24,7 @@ from mediagoblin.db.models import (MediaEntry, MediaTag, Collection,
CommentReport, MediaReport)
from mediagoblin.tools.response import render_to_response, render_404, \
redirect, redirect_obj
from mediagoblin.tools.text import cleaned_markdown_conversion
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.pagination import Pagination
from mediagoblin.user_pages import forms as user_forms
@@ -39,6 +41,7 @@ from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
from werkzeug.contrib.atom import AtomFeed
from werkzeug.exceptions import MethodNotAllowed
from werkzeug.wrappers import Response
_log = logging.getLogger(__name__)
@@ -159,7 +162,6 @@ def media_home(request, media, page, **kwargs):
@get_media_entry_by_id
@require_active_login
@user_has_privilege(u'commenter')
def media_post_comment(request, media):
"""
recieves POST from a MediaEntry() comment form, saves the comment.
@@ -291,8 +293,13 @@ def media_confirm_delete(request, media):
messages.add_message(
request, messages.SUCCESS, _('You deleted the media.'))
return redirect(request, "mediagoblin.user_pages.user_home",
user=username)
location = media.url_to_next(request.urlgen)
if not location:
location=media.url_to_prev(request.urlgen)
if not location:
location=request.urlgen("mediagoblin.user_pages.user_home",
user=username)
return redirect(request, location=location)
else:
messages.add_message(
request, messages.ERROR,