From 29b6f91740e68d804612ff68295020f6cfa16071 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Mon, 12 Mar 2012 21:17:08 -0400 Subject: 401. Plugin infrastructure * implements installing, loading and setup for plugins * codifies configuration * has a sample plugin * docs * tests --- mediagoblin/plugins/README | 6 ++++ mediagoblin/plugins/__init__.py | 16 +++++++++++ mediagoblin/plugins/sampleplugin/README | 6 ++++ mediagoblin/plugins/sampleplugin/__init__.py | 20 +++++++++++++ mediagoblin/plugins/sampleplugin/main.py | 42 ++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+) create mode 100644 mediagoblin/plugins/README create mode 100644 mediagoblin/plugins/__init__.py create mode 100644 mediagoblin/plugins/sampleplugin/README create mode 100644 mediagoblin/plugins/sampleplugin/__init__.py create mode 100644 mediagoblin/plugins/sampleplugin/main.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/README b/mediagoblin/plugins/README new file mode 100644 index 00000000..a2b92334 --- /dev/null +++ b/mediagoblin/plugins/README @@ -0,0 +1,6 @@ +======== + README +======== + +This directory holds the MediaGoblin core plugins. These plugins are not +enabled by default. See documentation for enabling plugins. diff --git a/mediagoblin/plugins/__init__.py b/mediagoblin/plugins/__init__.py new file mode 100644 index 00000000..719b56e7 --- /dev/null +++ b/mediagoblin/plugins/__init__.py @@ -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 . + diff --git a/mediagoblin/plugins/sampleplugin/README b/mediagoblin/plugins/sampleplugin/README new file mode 100644 index 00000000..15411d8e --- /dev/null +++ b/mediagoblin/plugins/sampleplugin/README @@ -0,0 +1,6 @@ +======== + README +======== + +This is a sample plugin. It does nothing interesting other than show +one way to structure a MediaGoblin plugin. \ No newline at end of file diff --git a/mediagoblin/plugins/sampleplugin/__init__.py b/mediagoblin/plugins/sampleplugin/__init__.py new file mode 100644 index 00000000..b87348af --- /dev/null +++ b/mediagoblin/plugins/sampleplugin/__init__.py @@ -0,0 +1,20 @@ +# 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 . + + +# This imports the module that has the Plugin subclass in it which +# causes that module to get imported and that class to get registered. +import mediagoblin.plugins.sampleplugin.main diff --git a/mediagoblin/plugins/sampleplugin/main.py b/mediagoblin/plugins/sampleplugin/main.py new file mode 100644 index 00000000..67cc70a5 --- /dev/null +++ b/mediagoblin/plugins/sampleplugin/main.py @@ -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 . + +import logging +from mediagoblin.tools.pluginapi import Plugin, get_config + + +_log = logging.getLogger(__name__) + + +class SamplePlugin(Plugin): + """ + This is a sample plugin class. It automatically registers itself + with mediagoblin when this module is imported. + + The setup_plugin method prints configuration for this plugin if + it exists. + """ + def __init__(self): + self._setup_plugin_called = 0 + + def setup_plugin(self): + _log.info('Sample plugin set up!') + config = get_config('mediagoblin.plugins.sampleplugin') + if config: + _log.info('%r' % config) + else: + _log.info('There is no configuration set.') + self._setup_plugin_called += 1 -- cgit v1.2.3 From 8545dd50f0cd588d505c217d367450198199a2b0 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Sun, 10 Jun 2012 11:50:14 -0400 Subject: Flatpages first pass This fixes the template loader so that it can load plugin templates. This adds code for registering template paths so that plugins can add their own templates. This adds the base code for the flatpagesfile plugin. It doesn't serve pages, yet, but it's pretty close. --- mediagoblin/plugins/flatpagesfile/README | 50 ++++++++++++++++++++++ mediagoblin/plugins/flatpagesfile/__init__.py | 20 +++++++++ mediagoblin/plugins/flatpagesfile/main.py | 41 ++++++++++++++++++ .../templates/flatpagesfile/base.html | 18 ++++++++ 4 files changed, 129 insertions(+) create mode 100644 mediagoblin/plugins/flatpagesfile/README create mode 100644 mediagoblin/plugins/flatpagesfile/__init__.py create mode 100644 mediagoblin/plugins/flatpagesfile/main.py create mode 100644 mediagoblin/plugins/flatpagesfile/templates/flatpagesfile/base.html (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/flatpagesfile/README b/mediagoblin/plugins/flatpagesfile/README new file mode 100644 index 00000000..23a675e1 --- /dev/null +++ b/mediagoblin/plugins/flatpagesfile/README @@ -0,0 +1,50 @@ +======== + README +======== + +This is the flatpages file plugin. It allows you to add pages to your +MediaGoblin instance which are not generated from user content. For +example, this is useful for these pages: + +* About this site +* Terms of service +* Privacy policy +* How to get an account here +* ... + + +How to add pages +================ + +To add pages, you must edit template files on the file system in your +`local_templates` directory. + +The directory structure looks kind of like this:: + + local_templates + |- flatpagesfile + |- flatpage1.html + |- flatpage2.html + |- ... + + +The ``.html`` file contains the content of your page. It's just a +template like all the other templates you have. + +Here's an example:: + + {% extends "flatpagesfile/base.html" %} + {% block mediagoblin_content %} +

About this site

+

+ This site is a MediaGoblin instance set up to host media for + me, my family and my friends. +

+ {% endblock %} + + +.. Note:: + + If you have a bunch of flatpages that kind of look like one + another, take advantage of Jinja2 template extending and create a + base template that the others extend. diff --git a/mediagoblin/plugins/flatpagesfile/__init__.py b/mediagoblin/plugins/flatpagesfile/__init__.py new file mode 100644 index 00000000..69c40a77 --- /dev/null +++ b/mediagoblin/plugins/flatpagesfile/__init__.py @@ -0,0 +1,20 @@ +# 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 . + + +# This imports the main module which has the FlatPagesPlugin class +# which does all the work. +import mediagoblin.plugins.flatpagesfile.main diff --git a/mediagoblin/plugins/flatpagesfile/main.py b/mediagoblin/plugins/flatpagesfile/main.py new file mode 100644 index 00000000..b73f9b90 --- /dev/null +++ b/mediagoblin/plugins/flatpagesfile/main.py @@ -0,0 +1,41 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import logging +import os + +from mediagoblin.tools import pluginapi + + +PLUGIN_DIR = os.path.dirname(__file__) + + +_log = logging.getLogger(__name__) + + +class FlatpagesPlugin(pluginapi.Plugin): + """ + This is the flatpages plugin class. See the README for how to use + flatpages. + """ + def __init__(self): + self._setup_plugin_called = 0 + + def setup_plugin(self): + self.config = pluginapi.get_config('mediagoblin.plugins.flatpagesfile') + + _log.info('Setting up flatpages....') + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) diff --git a/mediagoblin/plugins/flatpagesfile/templates/flatpagesfile/base.html b/mediagoblin/plugins/flatpagesfile/templates/flatpagesfile/base.html new file mode 100644 index 00000000..2b72a477 --- /dev/null +++ b/mediagoblin/plugins/flatpagesfile/templates/flatpagesfile/base.html @@ -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 . +-#} +{% extend "'mediagoblin/base.html" %} -- cgit v1.2.3 From 4bd65f69c710268404e1b1fdaac68db069558584 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Sun, 10 Jun 2012 14:51:13 -0400 Subject: Finish flatpagesplugin; add plugin docs --- mediagoblin/plugins/flatpagesfile/README | 50 ---------- mediagoblin/plugins/flatpagesfile/README.rst | 132 +++++++++++++++++++++++++++ mediagoblin/plugins/flatpagesfile/main.py | 42 ++++++++- mediagoblin/plugins/sampleplugin/README | 6 -- mediagoblin/plugins/sampleplugin/README.rst | 8 ++ 5 files changed, 177 insertions(+), 61 deletions(-) delete mode 100644 mediagoblin/plugins/flatpagesfile/README create mode 100644 mediagoblin/plugins/flatpagesfile/README.rst delete mode 100644 mediagoblin/plugins/sampleplugin/README create mode 100644 mediagoblin/plugins/sampleplugin/README.rst (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/flatpagesfile/README b/mediagoblin/plugins/flatpagesfile/README deleted file mode 100644 index 23a675e1..00000000 --- a/mediagoblin/plugins/flatpagesfile/README +++ /dev/null @@ -1,50 +0,0 @@ -======== - README -======== - -This is the flatpages file plugin. It allows you to add pages to your -MediaGoblin instance which are not generated from user content. For -example, this is useful for these pages: - -* About this site -* Terms of service -* Privacy policy -* How to get an account here -* ... - - -How to add pages -================ - -To add pages, you must edit template files on the file system in your -`local_templates` directory. - -The directory structure looks kind of like this:: - - local_templates - |- flatpagesfile - |- flatpage1.html - |- flatpage2.html - |- ... - - -The ``.html`` file contains the content of your page. It's just a -template like all the other templates you have. - -Here's an example:: - - {% extends "flatpagesfile/base.html" %} - {% block mediagoblin_content %} -

About this site

-

- This site is a MediaGoblin instance set up to host media for - me, my family and my friends. -

- {% endblock %} - - -.. Note:: - - If you have a bunch of flatpages that kind of look like one - another, take advantage of Jinja2 template extending and create a - base template that the others extend. diff --git a/mediagoblin/plugins/flatpagesfile/README.rst b/mediagoblin/plugins/flatpagesfile/README.rst new file mode 100644 index 00000000..b31f6017 --- /dev/null +++ b/mediagoblin/plugins/flatpagesfile/README.rst @@ -0,0 +1,132 @@ +====================== + flatpagesfile plugin +====================== + +This is the flatpages file plugin. It allows you to add pages to your +MediaGoblin instance which are not generated from user content. For +example, this is useful for these pages: + +* About this site +* Terms of service +* Privacy policy +* How to get an account here +* ... + + +How to configure +================ + +Add the following to your MediaGoblin .ini file in the ``[plugins]`` +section:: + + [[mediagoblin.plugins.flatpagesfile]] + + +This tells MediaGoblin to load the flatpagesfile plugin. This is the +subsection that you'll do all flatpagesfile plugin configuration in. + + +How to add pages +================ + +To add a new page to your site, you need to do two things: + +1. add a route to the MediaGoblin .ini file in the flatpagesfile + subsection + +2. write a template that will get served when that route is requested + + +Routes +------ + +First, let's talk about the route. + +A route is a key/value in your configuration file. + +The key starts with ``path`` and then has a number after that. For +example: ``path1``, ``path2``, ... ``path15``, ... + +The value has three parts separated by commas: + +1. **route name**: You can use this with `url()` in templates to have + MediaGoblin automatically build the urls for you. It's very handy. + + It should be "unique" and it should be alphanumeric characters and + hyphens. I wouldn't put spaces in there. + + Examples: ``flatpages-about``, ``about-view``, ``contact-view``, ... + +2. **route path**: This is the url that this route matches. + + Examples: ``/about``, ``/contact``, ``/pages/about``, ... + + Technically, this is a regular expression and you can do anything + with this that you can do with the routepath parameter of + `routes.Route`. For more details, see `the routes documentation + `_. + + Example: ``/siteadmin/{adminname:\w+}`` + + .. Note:: + + If you're doing something fancy, enclose the route in single + quotes. + + For example: ``'/siteadmin/{adminname:\w+}'`` + +3. **template**: The template to use for this url. The template is in + the flatpagesfile template directory, so you just need to specify + the file name. + + Like with other templates, if it's an HTML file, it's good to use + the ``.html`` extensions. + + Examples: ``index.html``, ``about.html``, ``contact.html``, ... + + +Here's an example configuration that adds two flat pages: one for an +"About this site" page and one for a "Terms of service" page:: + + [[mediagoblin.plugins.flatpagesfile]] + page1 = about-view, '/about', about.html + page2 = terms-view, '/terms', terms.html + + +Templates +--------- + +To add pages, you must edit template files on the file system in your +`local_templates` directory. + +The directory structure looks kind of like this:: + + local_templates + |- flatpagesfile + |- flatpage1.html + |- flatpage2.html + |- ... + + +The ``.html`` file contains the content of your page. It's just a +template like all the other templates you have. + +Here's an example that extends the `flatpagesfile/base.html` +template:: + + {% extends "flatpagesfile/base.html" %} + {% block mediagoblin_content %} +

About this site

+

+ This site is a MediaGoblin instance set up to host media for + me, my family and my friends. +

+ {% endblock %} + + +.. Note:: + + If you have a bunch of flatpages that kind of look like one + another, take advantage of Jinja2 template extending and create a + base template that the others extend. + diff --git a/mediagoblin/plugins/flatpagesfile/main.py b/mediagoblin/plugins/flatpagesfile/main.py index b73f9b90..b0f5ea42 100644 --- a/mediagoblin/plugins/flatpagesfile/main.py +++ b/mediagoblin/plugins/flatpagesfile/main.py @@ -17,7 +17,10 @@ import logging import os +from routes.route import Route + from mediagoblin.tools import pluginapi +from mediagoblin.tools.response import render_to_response PLUGIN_DIR = os.path.dirname(__file__) @@ -26,16 +29,45 @@ PLUGIN_DIR = os.path.dirname(__file__) _log = logging.getLogger(__name__) -class FlatpagesPlugin(pluginapi.Plugin): +def flatpage_handler(template): + """Flatpage view generator + + Given a template, generates the controller function for handling that + route. + + """ + def _flatpage_handler(request, *args, **kwargs): + return render_to_response( + request, 'flatpagesfile/%s' % template, + {'args': args, 'kwargs': kwargs}) + return _flatpage_handler + + +class FlatpagesFilePlugin(pluginapi.Plugin): """ This is the flatpages plugin class. See the README for how to use flatpages. """ - def __init__(self): - self._setup_plugin_called = 0 - def setup_plugin(self): self.config = pluginapi.get_config('mediagoblin.plugins.flatpagesfile') - _log.info('Setting up flatpages....') + _log.info('Setting up flatpagesfile....') + + # Register the template path. pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + # Set up and register routes. + pages = [(int(key.replace('page', '')), val) + for key, val in self.config.items() + if key.startswith('page')] + + pages = [mapping for index, mapping in sorted(pages)] + routes = [] + for name, url, template in pages: + name = 'flatpagesfile.%s' % name.strip() + controller = flatpage_handler(template) + routes.append( + Route(name, url, controller=controller)) + + pluginapi.register_routes(routes) + _log.info('Done setting up flatpagesfile!') diff --git a/mediagoblin/plugins/sampleplugin/README b/mediagoblin/plugins/sampleplugin/README deleted file mode 100644 index 15411d8e..00000000 --- a/mediagoblin/plugins/sampleplugin/README +++ /dev/null @@ -1,6 +0,0 @@ -======== - README -======== - -This is a sample plugin. It does nothing interesting other than show -one way to structure a MediaGoblin plugin. \ No newline at end of file diff --git a/mediagoblin/plugins/sampleplugin/README.rst b/mediagoblin/plugins/sampleplugin/README.rst new file mode 100644 index 00000000..73897133 --- /dev/null +++ b/mediagoblin/plugins/sampleplugin/README.rst @@ -0,0 +1,8 @@ +============== + sampleplugin +============== + +This is a sample plugin. It does nothing interesting other than show +one way to structure a MediaGoblin plugin. + +The code for this plugin is in ``mediagoblin/plugins/sampleplugin/``. -- cgit v1.2.3 From 8815541c2628c40190319c70f6227b32e0e35bf2 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Thu, 12 Jul 2012 18:32:45 -0400 Subject: Fix template --- mediagoblin/plugins/flatpagesfile/templates/flatpagesfile/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/flatpagesfile/templates/flatpagesfile/base.html b/mediagoblin/plugins/flatpagesfile/templates/flatpagesfile/base.html index 2b72a477..1cf9dd9d 100644 --- a/mediagoblin/plugins/flatpagesfile/templates/flatpagesfile/base.html +++ b/mediagoblin/plugins/flatpagesfile/templates/flatpagesfile/base.html @@ -15,4 +15,4 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -#} -{% extend "'mediagoblin/base.html" %} +{% extends "mediagoblin/base.html" %} -- cgit v1.2.3 From 54b42ee5645ff40aa572f89e60184bf540b50041 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Thu, 12 Jul 2012 19:18:15 -0400 Subject: Overhaul flatpages * move contents of main.py to __init__.py * update documentation in README * change the key/value configuration specification * added a recipe for passing values from the url to the template * removed some unused code --- mediagoblin/plugins/flatpagesfile/README.rst | 63 ++++++++++++++++------- mediagoblin/plugins/flatpagesfile/__init__.py | 65 ++++++++++++++++++++++-- mediagoblin/plugins/flatpagesfile/main.py | 73 --------------------------- 3 files changed, 108 insertions(+), 93 deletions(-) delete mode 100644 mediagoblin/plugins/flatpagesfile/main.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/flatpagesfile/README.rst b/mediagoblin/plugins/flatpagesfile/README.rst index b31f6017..525fb9d9 100644 --- a/mediagoblin/plugins/flatpagesfile/README.rst +++ b/mediagoblin/plugins/flatpagesfile/README.rst @@ -44,27 +44,24 @@ First, let's talk about the route. A route is a key/value in your configuration file. -The key starts with ``path`` and then has a number after that. For -example: ``path1``, ``path2``, ... ``path15``, ... +The key for the route is the route name You can use this with `url()` +in templates to have MediaGoblin automatically build the urls for +you. It's very handy. -The value has three parts separated by commas: +It should be "unique" and it should be alphanumeric characters and +hyphens. I wouldn't put spaces in there. -1. **route name**: You can use this with `url()` in templates to have - MediaGoblin automatically build the urls for you. It's very handy. +Examples: ``flatpages-about``, ``about-view``, ``contact-view``, ... - It should be "unique" and it should be alphanumeric characters and - hyphens. I wouldn't put spaces in there. +The value has two parts separated by commas: - Examples: ``flatpages-about``, ``about-view``, ``contact-view``, ... - -2. **route path**: This is the url that this route matches. +1. **route path**: This is the url that this route matches. Examples: ``/about``, ``/contact``, ``/pages/about``, ... - Technically, this is a regular expression and you can do anything - with this that you can do with the routepath parameter of - `routes.Route`. For more details, see `the routes documentation - `_. + You can do anything with this that you can do with the routepath + parameter of `routes.Route`. For more details, see `the routes + documentation `_. Example: ``/siteadmin/{adminname:\w+}`` @@ -75,7 +72,7 @@ The value has three parts separated by commas: For example: ``'/siteadmin/{adminname:\w+}'`` -3. **template**: The template to use for this url. The template is in +2. **template**: The template to use for this url. The template is in the flatpagesfile template directory, so you just need to specify the file name. @@ -89,8 +86,14 @@ Here's an example configuration that adds two flat pages: one for an "About this site" page and one for a "Terms of service" page:: [[mediagoblin.plugins.flatpagesfile]] - page1 = about-view, '/about', about.html - page2 = terms-view, '/terms', terms.html + about-view = '/about', about.html + terms-view = '/terms', terms.html + + +.. Note:: + + The order in which you define the routes in the config file is the + order in which they're checked for incoming requests. Templates @@ -130,3 +133,29 @@ template:: another, take advantage of Jinja2 template extending and create a base template that the others extend. + +Recipes +======= + +Url variables +------------- + +You can handle urls like ``/about/{name}`` and access the name that's +passed in in the template. + +Sample route:: + + about-page = '/about/{name}', about.html + +Sample template:: + + {% extends "flatpagesfile/base.html" %} + {% block mediagoblin_content %} + +

About page for {{ request.matchdict['name'] }}

+ + {% endblock %} + +See the `the routes documentation +`_ for syntax details for +the route. Values will end up in the ``request.matchdict`` dict. diff --git a/mediagoblin/plugins/flatpagesfile/__init__.py b/mediagoblin/plugins/flatpagesfile/__init__.py index 69c40a77..9ed26102 100644 --- a/mediagoblin/plugins/flatpagesfile/__init__.py +++ b/mediagoblin/plugins/flatpagesfile/__init__.py @@ -15,6 +15,65 @@ # along with this program. If not, see . -# This imports the main module which has the FlatPagesPlugin class -# which does all the work. -import mediagoblin.plugins.flatpagesfile.main +import logging +import os + +import jinja2 +from routes.route import Route + +from mediagoblin.tools import pluginapi +from mediagoblin.tools.response import render_to_response + + +PLUGIN_DIR = os.path.dirname(__file__) + + +_log = logging.getLogger(__name__) + + +@jinja2.contextfunction +def print_context(c): + s = [] + for key, val in c.items(): + s.append('%s: %s' % (key, repr(val))) + return '\n'.join(s) + + +def flatpage_handler_builder(template): + """Flatpage view generator + + Given a template, generates the controller function for handling that + route. + + """ + def _flatpage_handler_builder(request): + return render_to_response( + request, 'flatpagesfile/%s' % template, + {'request': request}) + return _flatpage_handler_builder + + +class FlatpagesFilePlugin(pluginapi.Plugin): + """ + This is the flatpages plugin class. See the README for how to use + flatpages. + """ + def setup_plugin(self): + self.config = pluginapi.get_config('mediagoblin.plugins.flatpagesfile') + + _log.info('Setting up flatpagesfile....') + + # Register the template path. + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + pages = self.config.items() + + routes = [] + for name, (url, template) in pages: + name = 'flatpagesfile.%s' % name.strip() + controller = flatpage_handler_builder(template) + routes.append( + Route(name, url, controller=controller)) + + pluginapi.register_routes(routes) + _log.info('Done setting up flatpagesfile!') diff --git a/mediagoblin/plugins/flatpagesfile/main.py b/mediagoblin/plugins/flatpagesfile/main.py deleted file mode 100644 index b0f5ea42..00000000 --- a/mediagoblin/plugins/flatpagesfile/main.py +++ /dev/null @@ -1,73 +0,0 @@ -# GNU MediaGoblin -- federated, autonomous media hosting -# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import logging -import os - -from routes.route import Route - -from mediagoblin.tools import pluginapi -from mediagoblin.tools.response import render_to_response - - -PLUGIN_DIR = os.path.dirname(__file__) - - -_log = logging.getLogger(__name__) - - -def flatpage_handler(template): - """Flatpage view generator - - Given a template, generates the controller function for handling that - route. - - """ - def _flatpage_handler(request, *args, **kwargs): - return render_to_response( - request, 'flatpagesfile/%s' % template, - {'args': args, 'kwargs': kwargs}) - return _flatpage_handler - - -class FlatpagesFilePlugin(pluginapi.Plugin): - """ - This is the flatpages plugin class. See the README for how to use - flatpages. - """ - def setup_plugin(self): - self.config = pluginapi.get_config('mediagoblin.plugins.flatpagesfile') - - _log.info('Setting up flatpagesfile....') - - # Register the template path. - pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) - - # Set up and register routes. - pages = [(int(key.replace('page', '')), val) - for key, val in self.config.items() - if key.startswith('page')] - - pages = [mapping for index, mapping in sorted(pages)] - routes = [] - for name, url, template in pages: - name = 'flatpagesfile.%s' % name.strip() - controller = flatpage_handler(template) - routes.append( - Route(name, url, controller=controller)) - - pluginapi.register_routes(routes) - _log.info('Done setting up flatpagesfile!') -- cgit v1.2.3 From 05e007c1dbe7b5b8a092f1a99ed361c4e6b71f26 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Tue, 17 Jul 2012 21:02:12 -0400 Subject: Rework plugin infrastructure to nix side-effects This reworks the plugin infrastructure so as to remove module-loading side-effects which were making things a pain in the ass to test. With the new system, there's no auto-registering meta class. Instead plugins do whatever they want and then specify a hooks dict that maps hook names to callables for the things they're tying into. The most common one (and the only one we've implemented so far) is "setup". This also simplifies the sampleplugin a little by moving the code to __init__.py. --- mediagoblin/plugins/flatpagesfile/__init__.py | 38 ++++++++++++------------ mediagoblin/plugins/sampleplugin/__init__.py | 28 ++++++++++++++++-- mediagoblin/plugins/sampleplugin/main.py | 42 --------------------------- 3 files changed, 44 insertions(+), 64 deletions(-) delete mode 100644 mediagoblin/plugins/sampleplugin/main.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/flatpagesfile/__init__.py b/mediagoblin/plugins/flatpagesfile/__init__.py index 9ed26102..b9b52012 100644 --- a/mediagoblin/plugins/flatpagesfile/__init__.py +++ b/mediagoblin/plugins/flatpagesfile/__init__.py @@ -53,27 +53,27 @@ def flatpage_handler_builder(template): return _flatpage_handler_builder -class FlatpagesFilePlugin(pluginapi.Plugin): - """ - This is the flatpages plugin class. See the README for how to use - flatpages. - """ - def setup_plugin(self): - self.config = pluginapi.get_config('mediagoblin.plugins.flatpagesfile') +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.flatpagesfile') + + _log.info('Setting up flatpagesfile....') + + # Register the template path. + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) - _log.info('Setting up flatpagesfile....') + pages = config.items() - # Register the template path. - pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + routes = [] + for name, (url, template) in pages: + name = 'flatpagesfile.%s' % name.strip() + controller = flatpage_handler_builder(template) + routes.append( + Route(name, url, controller=controller)) - pages = self.config.items() + pluginapi.register_routes(routes) + _log.info('Done setting up flatpagesfile!') - routes = [] - for name, (url, template) in pages: - name = 'flatpagesfile.%s' % name.strip() - controller = flatpage_handler_builder(template) - routes.append( - Route(name, url, controller=controller)) - pluginapi.register_routes(routes) - _log.info('Done setting up flatpagesfile!') +hooks = { + 'setup': setup_plugin + } diff --git a/mediagoblin/plugins/sampleplugin/__init__.py b/mediagoblin/plugins/sampleplugin/__init__.py index b87348af..2cd077a2 100644 --- a/mediagoblin/plugins/sampleplugin/__init__.py +++ b/mediagoblin/plugins/sampleplugin/__init__.py @@ -15,6 +15,28 @@ # along with this program. If not, see . -# This imports the module that has the Plugin subclass in it which -# causes that module to get imported and that class to get registered. -import mediagoblin.plugins.sampleplugin.main +import logging + +from mediagoblin.tools.pluginapi import get_config + + +_log = logging.getLogger(__name__) + + +_setup_plugin_called = 0 + +def setup_plugin(): + global _setup_plugin_called + + _log.info('Sample plugin set up!') + config = get_config('mediagoblin.plugins.sampleplugin') + if config: + _log.info('%r' % config) + else: + _log.info('There is no configuration set.') + _setup_plugin_called += 1 + + +hooks = { + 'setup': setup_plugin + } diff --git a/mediagoblin/plugins/sampleplugin/main.py b/mediagoblin/plugins/sampleplugin/main.py deleted file mode 100644 index 67cc70a5..00000000 --- a/mediagoblin/plugins/sampleplugin/main.py +++ /dev/null @@ -1,42 +0,0 @@ -# GNU MediaGoblin -- federated, autonomous media hosting -# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import logging -from mediagoblin.tools.pluginapi import Plugin, get_config - - -_log = logging.getLogger(__name__) - - -class SamplePlugin(Plugin): - """ - This is a sample plugin class. It automatically registers itself - with mediagoblin when this module is imported. - - The setup_plugin method prints configuration for this plugin if - it exists. - """ - def __init__(self): - self._setup_plugin_called = 0 - - def setup_plugin(self): - _log.info('Sample plugin set up!') - config = get_config('mediagoblin.plugins.sampleplugin') - if config: - _log.info('%r' % config) - else: - _log.info('There is no configuration set.') - self._setup_plugin_called += 1 -- cgit v1.2.3 From 7a690a5ae541f26321be98625a3df34847b44d5b Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Mon, 20 Aug 2012 08:54:09 -0500 Subject: Updated flatpages example in plugins.rst to reflect reality & point to flatpages docs --- mediagoblin/plugins/flatpagesfile/README.rst | 2 ++ 1 file changed, 2 insertions(+) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/flatpagesfile/README.rst b/mediagoblin/plugins/flatpagesfile/README.rst index 525fb9d9..59cd6217 100644 --- a/mediagoblin/plugins/flatpagesfile/README.rst +++ b/mediagoblin/plugins/flatpagesfile/README.rst @@ -1,3 +1,5 @@ +.. _flatpagesfile-chapter: + ====================== flatpagesfile plugin ====================== -- cgit v1.2.3 From f46e2a4db9e70aba473bec537300103c9102ef1a Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Wed, 12 Sep 2012 22:41:04 +0200 Subject: Add OAuth models, plugin DB migrations, api_auth --- mediagoblin/plugins/oauth/__init__.py | 90 +++++++++++++++++++++++++++++ mediagoblin/plugins/oauth/models.py | 58 +++++++++++++++++++ mediagoblin/plugins/oauth/views.py | 105 ++++++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+) create mode 100644 mediagoblin/plugins/oauth/__init__.py create mode 100644 mediagoblin/plugins/oauth/models.py create mode 100644 mediagoblin/plugins/oauth/views.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py new file mode 100644 index 00000000..af39ae93 --- /dev/null +++ b/mediagoblin/plugins/oauth/__init__.py @@ -0,0 +1,90 @@ +# 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 . + +import os +import logging + +from routes.route import Route +from webob import exc + +from mediagoblin.tools import pluginapi +from mediagoblin.tools.response import render_to_response +from mediagoblin.plugins.oauth.models import OAuthToken + +_log = logging.getLogger(__name__) + +PLUGIN_DIR = os.path.dirname(__file__) + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.oauth') + + _log.info('Setting up OAuth...') + _log.debug('OAuth config: {0}'.format(config)) + + routes = [ + Route('mediagoblin.plugins.oauth.authorize', '/oauth/authorize', + controller='mediagoblin.plugins.oauth.views:authorize'), + Route('mediagoblin.plugins.oauth.test', '/api/test', + controller='mediagoblin.plugins.oauth.views:api_test'), + Route('mediagoblin.plugins.oauth.access_token', '/oauth/access_token', + controller='mediagoblin.plugins.oauth.views:access_token')] + + pluginapi.register_routes(routes) + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + +class OAuthAuth(object): + ''' + An object with two significant methods, 'trigger' and 'run'. + + Using a similar object to this, plugins can register specific + authentication logic, for example the GET param 'access_token' for OAuth. + + - trigger: Analyze the 'request' argument, return True if you think you + can handle the request, otherwise return False + - run: The authentication logic, set the request.user object to the user + you intend to authenticate and return True, otherwise return False. + + If run() returns False, an HTTP 403 Forbidden error will be shown. + + You may also display custom errors, just raise them within the run() + method. + ''' + def __init__(self): + pass + + def trigger(self, request): + return True + + def __call__(self, request, *args, **kw): + access_token = request.GET.get('access_token') + if access_token: + token = OAuthToken.query.filter(OAuthToken.token == access_token)\ + .first() + + if not token: + return False + + request.user = token.user + + return True + + +hooks = { + 'setup': setup_plugin, + 'auth': OAuthAuth() + } diff --git a/mediagoblin/plugins/oauth/models.py b/mediagoblin/plugins/oauth/models.py new file mode 100644 index 00000000..295987c8 --- /dev/null +++ b/mediagoblin/plugins/oauth/models.py @@ -0,0 +1,58 @@ +# 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 . + +from datetime import datetime, timedelta + +from mediagoblin.db.sql.base import Base +from mediagoblin.db.sql.models import User + +from sqlalchemy import ( + Column, Unicode, Integer, DateTime, ForeignKey) +from sqlalchemy.orm import relationship + + +class OAuthToken(Base): + __tablename__ = 'oauth__tokens' + + id = Column(Integer, primary_key=True) + created = Column(DateTime, nullable=False, + default=datetime.now) + expires = Column(DateTime, nullable=False, + default=lambda: datetime.now() + timedelta(days=30)) + token = Column(Unicode, index=True) + refresh_token = Column(Unicode, index=True) + + user_id = Column(Integer, ForeignKey(User.id), nullable=False, + index=True) + user = relationship(User) + + +class OAuthCode(Base): + __tablename__ = 'oauth__codes' + + id = Column(Integer, primary_key=True) + created = Column(DateTime, nullable=False, + default=datetime.now) + expires = Column(DateTime, nullable=False, + default=lambda: datetime.now() + timedelta(minutes=5)) + code = Column(Unicode, index=True) + + user_id = Column(Integer, ForeignKey(User.id), nullable=False, + index=True) + user = relationship(User) + + +MODELS = [OAuthToken, OAuthCode] diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py new file mode 100644 index 00000000..7627b97a --- /dev/null +++ b/mediagoblin/plugins/oauth/views.py @@ -0,0 +1,105 @@ +# 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 . + +import logging +import json + +from webob import exc, Response +from urllib import urlencode +from uuid import uuid4 +from datetime import datetime +from functools import wraps + +from mediagoblin.tools import pluginapi +from mediagoblin.tools.response import render_to_response +from mediagoblin.decorators import require_active_login +from mediagoblin.messages import add_message, SUCCESS, ERROR +from mediagoblin.tools.translate import pass_to_ugettext as _ +from mediagoblin.plugins.oauth.models import OAuthCode, OAuthToken + +_log = logging.getLogger(__name__) + + +@require_active_login +def authorize(request): + # TODO: Check if allowed + + # Client is allowed by the user + if True or already_authorized: + # Generate a code + # Save the code, the client will later use it to obtain an access token + # Redirect the user agent to the redirect_uri with the code + + if not 'redirect_uri' in request.GET: + add_message(request, ERROR, _('No redirect_uri found')) + + code = OAuthCode() + code.code = unicode(uuid4()) + code.user = request.user + code.save() + + redirect_uri = ''.join([ + request.GET.get('redirect_uri'), + '?', + urlencode({'code': code.code})]) + + _log.debug('Redirecting to {0}'.format(redirect_uri)) + + return exc.HTTPFound(location=redirect_uri) + else: + # Show prompt to allow client to access data + # - on accept: send the user agent back to the redirect_uri with the + # code parameter + # - on deny: send the user agent back to the redirect uri with error + # information + pass + return render_to_response(request, 'oauth/base.html', {}) + + +def access_token(request): + if request.GET.get('code'): + code = OAuthCode.query.filter(OAuthCode.code == request.GET.get('code'))\ + .first() + + if code: + token = OAuthToken() + token.token = unicode(uuid4()) + token.user = code.user + token.save() + + access_token_data = { + 'access_token': token.token, + 'token_type': 'what_do_i_use_this_for', # TODO + 'expires_in': + (token.expires - datetime.now()).total_seconds(), + 'refresh_token': 'This should probably be safe'} + return Response(json.dumps(access_token_data)) + + error_data = { + 'error': 'Incorrect code'} + return Response(json.dumps(error_data)) + + +@pluginapi.api_auth +def api_test(request): + if not request.user: + return exc.HTTPForbidden() + + user_data = { + 'username': request.user.username, + 'email': request.user.email} + + return Response(json.dumps(user_data)) -- cgit v1.2.3 From a062149e90731cfd730d8a539a32354065a8c9e8 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Thu, 13 Sep 2012 20:59:00 +0200 Subject: Created API plugin, moved api_auth to the API plugin --- mediagoblin/plugins/api/__init__.py | 41 +++++++++++++++++++++++++++ mediagoblin/plugins/api/tools.py | 53 +++++++++++++++++++++++++++++++++++ mediagoblin/plugins/api/views.py | 32 +++++++++++++++++++++ mediagoblin/plugins/oauth/__init__.py | 2 -- mediagoblin/plugins/oauth/views.py | 12 -------- 5 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 mediagoblin/plugins/api/__init__.py create mode 100644 mediagoblin/plugins/api/tools.py create mode 100644 mediagoblin/plugins/api/views.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/__init__.py b/mediagoblin/plugins/api/__init__.py new file mode 100644 index 00000000..f3526aa1 --- /dev/null +++ b/mediagoblin/plugins/api/__init__.py @@ -0,0 +1,41 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os +import logging + +from routes.route import Route + +from mediagoblin.tools import pluginapi + +_log = logging.getLogger(__name__) + +PLUGIN_DIR = os.path.dirname(__file__) + + +def setup_plugin(): + config = pluginapi.get_config(__name__) + + _log.info('Setting up API...') + + routes = [ + Route('mediagoblin.plugins.api.test', '/api/test', + controller='mediagoblin.plugins.api.views:api_test')] + + pluginapi.register_routes(routes) + +hooks = { + 'setup': setup_plugin} diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py new file mode 100644 index 00000000..a8873b06 --- /dev/null +++ b/mediagoblin/plugins/api/tools.py @@ -0,0 +1,53 @@ +# 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 . + +import logging + +from functools import wraps +from webob import exc + +from mediagoblin.tools.pluginapi import PluginManager + +_log = logging.getLogger(__name__) + + +def api_auth(controller): + @wraps(controller) + def wrapper(request, *args, **kw): + auth_candidates = [] + + for auth in PluginManager().get_hook_callables('auth'): + _log.debug('Plugin auth: {0}'.format(auth)) + if auth.trigger(request): + auth_candidates.append(auth) + + # If we can't find any authentication methods, we should not let them + # pass. + if not auth_candidates: + return exc.HTTPForbidden() + + # For now, just select the first one in the list + auth = auth_candidates[0] + + _log.debug('Using {0} to authorize request {1}'.format( + auth, request.url)) + + if not auth(request, *args, **kw): + return exc.HTTPForbidden() + + return controller(request, *args, **kw) + + return wrapper diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py new file mode 100644 index 00000000..7d6ef572 --- /dev/null +++ b/mediagoblin/plugins/api/views.py @@ -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 . + +import json +from webob import exc, Response + +from mediagoblin.plugins.api.tools import api_auth + + +@api_auth +def api_test(request): + if not request.user: + return exc.HTTPForbidden() + + user_data = { + 'username': request.user.username, + 'email': request.user.email} + + return Response(json.dumps(user_data)) diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py index af39ae93..04aa7815 100644 --- a/mediagoblin/plugins/oauth/__init__.py +++ b/mediagoblin/plugins/oauth/__init__.py @@ -38,8 +38,6 @@ def setup_plugin(): routes = [ Route('mediagoblin.plugins.oauth.authorize', '/oauth/authorize', controller='mediagoblin.plugins.oauth.views:authorize'), - Route('mediagoblin.plugins.oauth.test', '/api/test', - controller='mediagoblin.plugins.oauth.views:api_test'), Route('mediagoblin.plugins.oauth.access_token', '/oauth/access_token', controller='mediagoblin.plugins.oauth.views:access_token')] diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py index 7627b97a..70be3039 100644 --- a/mediagoblin/plugins/oauth/views.py +++ b/mediagoblin/plugins/oauth/views.py @@ -91,15 +91,3 @@ def access_token(request): error_data = { 'error': 'Incorrect code'} return Response(json.dumps(error_data)) - - -@pluginapi.api_auth -def api_test(request): - if not request.user: - return exc.HTTPForbidden() - - user_data = { - 'username': request.user.username, - 'email': request.user.email} - - return Response(json.dumps(user_data)) -- cgit v1.2.3 From 42c837523e5ac70a03fb310dbb15bec03d4108cd Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 15 Sep 2012 15:25:26 +0200 Subject: Added /api/entries view --- mediagoblin/plugins/api/__init__.py | 4 ++- mediagoblin/plugins/api/tools.py | 61 ++++++++++++++++++++++++++++++++++- mediagoblin/plugins/api/views.py | 16 ++++++++- mediagoblin/plugins/oauth/__init__.py | 22 ++----------- 4 files changed, 80 insertions(+), 23 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/__init__.py b/mediagoblin/plugins/api/__init__.py index f3526aa1..9d4b58df 100644 --- a/mediagoblin/plugins/api/__init__.py +++ b/mediagoblin/plugins/api/__init__.py @@ -33,7 +33,9 @@ def setup_plugin(): routes = [ Route('mediagoblin.plugins.api.test', '/api/test', - controller='mediagoblin.plugins.api.views:api_test')] + controller='mediagoblin.plugins.api.views:api_test'), + Route('mediagoblin.plugins.api.entries', '/api/entries', + controller='mediagoblin.plugins.api.views:get_entries')] pluginapi.register_routes(routes) diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index a8873b06..4d306274 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -15,15 +15,74 @@ # along with this program. If not, see . import logging +import json from functools import wraps -from webob import exc +from webob import exc, Response +from mediagoblin import mg_globals from mediagoblin.tools.pluginapi import PluginManager +from mediagoblin.storage.filestorage import BasicFileStorage _log = logging.getLogger(__name__) +class Auth(object): + ''' + An object with two significant methods, 'trigger' and 'run'. + + Using a similar object to this, plugins can register specific + authentication logic, for example the GET param 'access_token' for OAuth. + + - trigger: Analyze the 'request' argument, return True if you think you + can handle the request, otherwise return False + - run: The authentication logic, set the request.user object to the user + you intend to authenticate and return True, otherwise return False. + + If run() returns False, an HTTP 403 Forbidden error will be shown. + + You may also display custom errors, just raise them within the run() + method. + ''' + def trigger(self, request): + raise NotImplemented() + + def __call__(self, request, *args, **kw): + raise NotImplemented() + + +def json_response(serializable): + response = Response(json.dumps(serializable)) + response.headers['Content-Type'] = 'application/json' + return response + + +def get_entry_serializable(entry): + return { + 'user': entry.get_uploader.username, + 'user_id': entry.get_uploader.id, + 'id': entry.id, + 'created': entry.created.isoformat(), + 'title': entry.title, + 'license': entry.license, + 'description': entry.description, + 'description_html': entry.description_html, + 'media_type': entry.media_type, + 'media_files': get_media_file_paths(entry.media_files)} + + +def get_media_file_paths(media_files): + if isinstance(mg_globals.public_store, BasicFileStorage): + pass # TODO + + media_urls = {} + + for key, val in media_files.items(): + media_urls[key] = mg_globals.public_store.file_url(val) + + return media_urls + + def api_auth(controller): @wraps(controller) def wrapper(request, *args, **kw): diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 7d6ef572..49c16ee6 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -17,7 +17,8 @@ import json from webob import exc, Response -from mediagoblin.plugins.api.tools import api_auth +from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \ + json_response @api_auth @@ -30,3 +31,16 @@ def api_test(request): 'email': request.user.email} return Response(json.dumps(user_data)) + + +def get_entries(request): + entries = request.db.MediaEntry.query + + entries = entries.filter_by(state=u'processed') + + entries_serializable = [] + + for entry in entries: + entries_serializable.append(get_entry_serializable(entry)) + + return json_response(entries_serializable) diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py index 04aa7815..95919728 100644 --- a/mediagoblin/plugins/oauth/__init__.py +++ b/mediagoblin/plugins/oauth/__init__.py @@ -23,6 +23,7 @@ from webob import exc from mediagoblin.tools import pluginapi from mediagoblin.tools.response import render_to_response from mediagoblin.plugins.oauth.models import OAuthToken +from mediagoblin.plugins.api.tools import Auth _log = logging.getLogger(__name__) @@ -45,26 +46,7 @@ def setup_plugin(): pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) -class OAuthAuth(object): - ''' - An object with two significant methods, 'trigger' and 'run'. - - Using a similar object to this, plugins can register specific - authentication logic, for example the GET param 'access_token' for OAuth. - - - trigger: Analyze the 'request' argument, return True if you think you - can handle the request, otherwise return False - - run: The authentication logic, set the request.user object to the user - you intend to authenticate and return True, otherwise return False. - - If run() returns False, an HTTP 403 Forbidden error will be shown. - - You may also display custom errors, just raise them within the run() - method. - ''' - def __init__(self): - pass - +class OAuthAuth(Auth): def trigger(self, request): return True -- cgit v1.2.3 From 85726f7360e2a7dbc74764c26501ac633bc10049 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 15 Sep 2012 15:54:22 +0200 Subject: Added fields to /api/entries, wrote docstrings for api.tools --- mediagoblin/plugins/api/tools.py | 44 +++++++++++++++++++++++++++++++++++----- mediagoblin/plugins/api/views.py | 2 +- 2 files changed, 40 insertions(+), 6 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index 4d306274..e3b15b23 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -51,16 +51,37 @@ class Auth(object): raise NotImplemented() -def json_response(serializable): - response = Response(json.dumps(serializable)) +def json_response(serializable, *args, **kw): + ''' + Serializes a json objects and returns a webob.Response object with the + serialized value as the response body and Content-Type: application/json. + + :param serializable: A json-serializable object + + Any extra arguments and keyword arguments are passed to the + webob.Response.__init__ method. + ''' + response = Response(json.dumps(serializable), *args, **kw) response.headers['Content-Type'] = 'application/json' return response -def get_entry_serializable(entry): +def get_entry_serializable(entry, urlgen): + ''' + Returns a serializable dict() of a MediaEntry instance. + + :param entry: A MediaEntry instance + :param urlgen: An urlgen instance, can be found on the request object passed + to views. + ''' return { 'user': entry.get_uploader.username, 'user_id': entry.get_uploader.id, + 'user_bio': entry.get_uploader.bio, + 'user_bio_html': entry.get_uploader.bio_html, + 'user_permalink': urlgen('mediagoblin.user_pages.user_home', + user=entry.get_uploader.username, + qualified=True), 'id': entry.id, 'created': entry.created.isoformat(), 'title': entry.title, @@ -68,10 +89,18 @@ def get_entry_serializable(entry): 'description': entry.description, 'description_html': entry.description_html, 'media_type': entry.media_type, - 'media_files': get_media_file_paths(entry.media_files)} + 'permalink': entry.url_for_self(urlgen, qualified=True), + 'media_files': get_media_file_paths(entry.media_files, urlgen)} + +def get_media_file_paths(media_files, urlgen): + ''' + Returns a dictionary of media files with `file_handle` => `qualified URL` -def get_media_file_paths(media_files): + :param media_files: dict-like object consisting of `file_handle => `listy + filepath` pairs. + :param urlgen: An urlgen object, usually found on request.urlgen. + ''' if isinstance(mg_globals.public_store, BasicFileStorage): pass # TODO @@ -84,6 +113,11 @@ def get_media_file_paths(media_files): def api_auth(controller): + ''' + Decorator, allows plugins to register auth methods that will then be + evaluated against the request, finally a worthy authenticator object is + chosen and used to decide whether to grant or deny access. + ''' @wraps(controller) def wrapper(request, *args, **kw): auth_candidates = [] diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 49c16ee6..feeac09a 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -41,6 +41,6 @@ def get_entries(request): entries_serializable = [] for entry in entries: - entries_serializable.append(get_entry_serializable(entry)) + entries_serializable.append(get_entry_serializable(entry, request.urlgen)) return json_response(entries_serializable) -- cgit v1.2.3 From 4504dbba9b111a0f6648f94c970c429b644f1ab4 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 15 Sep 2012 16:51:29 +0200 Subject: Added post_entry at /api/submit --- mediagoblin/plugins/api/__init__.py | 4 +- mediagoblin/plugins/api/views.py | 91 +++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/__init__.py b/mediagoblin/plugins/api/__init__.py index 9d4b58df..6a127b6e 100644 --- a/mediagoblin/plugins/api/__init__.py +++ b/mediagoblin/plugins/api/__init__.py @@ -35,7 +35,9 @@ def setup_plugin(): Route('mediagoblin.plugins.api.test', '/api/test', controller='mediagoblin.plugins.api.views:api_test'), Route('mediagoblin.plugins.api.entries', '/api/entries', - controller='mediagoblin.plugins.api.views:get_entries')] + controller='mediagoblin.plugins.api.views:get_entries'), + Route('mediagoblin.plugins.api.post_entry', '/api/submit', + controller='mediagoblin.plugins.api.views:post_entry')] pluginapi.register_routes(routes) diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index feeac09a..5508f8b0 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -15,11 +15,102 @@ # along with this program. If not, see . import json +import logging +import uuid + +from os.path import splitext from webob import exc, Response +from werkzeug.utils import secure_filename +from celery import registry +from mediagoblin.db.util import ObjectId +from mediagoblin.decorators import require_active_login +from mediagoblin.processing import mark_entry_failed +from mediagoblin.processing.task import ProcessMedia +from mediagoblin.meddleware.csrf import csrf_exempt +from mediagoblin.media_types import sniff_media, InvalidFileType, \ + FileTypeNotSupported from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \ json_response +_log = logging.getLogger(__name__) + + +@csrf_exempt +@api_auth +@require_active_login +def post_entry(request): + _log.debug('Posting entry') + if request.method != 'POST': + return exc.HTTPBadRequest() + + if not 'file' in request.POST or not hasattr(request.POST['file'], 'file'): + return exc.HTTPBadRequest() + + media_file = request.POST['file'] + + media_type, media_manager = sniff_media(media_file) + + entry = request.db.MediaEntry() + entry.id = ObjectId() + entry.media_type = unicode(media_type) + entry.title = unicode(request.POST.get('title') + or splitext(media_file.filename)[0]) + + entry.descriptions = unicode(request.POST.get('description')) + entry.license = unicode(request.POST.get('license', '')) + + entry.uploader = request.user.id + + entry.generate_slug() + + task_id = unicode(uuid.uuid4()) + + # Now store generate the queueing related filename + queue_filepath = request.app.queue_store.get_unique_filepath( + ['media_entries', + task_id, + secure_filename(media_file.filename)]) + + # queue appropriately + queue_file = request.app.queue_store.get_file( + queue_filepath, 'wb') + + with queue_file: + queue_file.write(request.POST['file'].file.read()) + + # Add queued filename to the entry + entry.queued_media_file = queue_filepath + + entry.queued_task_id = task_id + + # Save now so we have this data before kicking off processing + entry.save(validate=True) + + # Pass off to processing + # + # (... don't change entry after this point to avoid race + # conditions with changes to the document via processing code) + process_media = registry.tasks[ProcessMedia.name] + try: + process_media.apply_async( + [unicode(entry._id)], {}, + task_id=task_id) + except BaseException as e: + # The purpose of this section is because when running in "lazy" + # or always-eager-with-exceptions-propagated celery mode that + # the failure handling won't happen on Celery end. Since we + # expect a lot of users to run things in this way we have to + # capture stuff here. + # + # ... not completely the diaper pattern because the + # exception is re-raised :) + mark_entry_failed(entry._id, e) + # re-raise the exception + raise + + return json_response(get_entry_serializable(entry, request.urlgen)) + @api_auth def api_test(request): -- cgit v1.2.3 From 999f91eb8a220ee9a69c0678aa2803c63e80c903 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 15 Sep 2012 16:54:14 +0200 Subject: Fixed typo in API post_entry --- mediagoblin/plugins/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 5508f8b0..6a58fabf 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -57,7 +57,7 @@ def post_entry(request): entry.title = unicode(request.POST.get('title') or splitext(media_file.filename)[0]) - entry.descriptions = unicode(request.POST.get('description')) + entry.description = unicode(request.POST.get('description')) entry.license = unicode(request.POST.get('license', '')) entry.uploader = request.user.id -- cgit v1.2.3 From 3a1993288ffefe790208b719d6bd5ea42af79c49 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 15 Sep 2012 21:07:24 +0200 Subject: Fixed ?next= argument for require_active_login It now includes the full URI, including GET args, not just the path. --- mediagoblin/plugins/api/__init__.py | 5 +++-- mediagoblin/plugins/api/views.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/__init__.py b/mediagoblin/plugins/api/__init__.py index 6a127b6e..40722088 100644 --- a/mediagoblin/plugins/api/__init__.py +++ b/mediagoblin/plugins/api/__init__.py @@ -25,12 +25,13 @@ _log = logging.getLogger(__name__) PLUGIN_DIR = os.path.dirname(__file__) +config = pluginapi.get_config(__name__) def setup_plugin(): - config = pluginapi.get_config(__name__) - _log.info('Setting up API...') + _log.debug('API config: {0}'.format(config)) + routes = [ Route('mediagoblin.plugins.api.test', '/api/test', controller='mediagoblin.plugins.api.views:api_test'), diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 6a58fabf..ff177e29 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -33,6 +33,8 @@ from mediagoblin.media_types import sniff_media, InvalidFileType, \ from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \ json_response +from mediagoblin.plugins.api import config + _log = logging.getLogger(__name__) -- cgit v1.2.3 From 965b39a84fc240a24ae1bd7215bb06361b6df98f Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 15 Sep 2012 22:18:49 +0200 Subject: Added CORS headers to API json_response --- mediagoblin/plugins/api/tools.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index e3b15b23..5488e515 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -63,6 +63,11 @@ def json_response(serializable, *args, **kw): ''' response = Response(json.dumps(serializable), *args, **kw) response.headers['Content-Type'] = 'application/json' + cors_headers = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} + response.headers.update(cors_headers) return response -- cgit v1.2.3 From c92aa0d0b21e01223db7eeaa2fcea4d961b512d9 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 15 Sep 2012 22:34:34 +0200 Subject: API: Fixed media file URLs, limits - Added default limit and limit arg to get_entries - Fixed URL generation for BasicFileStorage files in API --- mediagoblin/plugins/api/tools.py | 12 ++++++++---- mediagoblin/plugins/api/views.py | 7 +++++++ 2 files changed, 15 insertions(+), 4 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index 5488e515..e5aca29b 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -19,6 +19,7 @@ import json from functools import wraps from webob import exc, Response +from urlparse import urljoin from mediagoblin import mg_globals from mediagoblin.tools.pluginapi import PluginManager @@ -106,13 +107,16 @@ def get_media_file_paths(media_files, urlgen): filepath` pairs. :param urlgen: An urlgen object, usually found on request.urlgen. ''' - if isinstance(mg_globals.public_store, BasicFileStorage): - pass # TODO - media_urls = {} for key, val in media_files.items(): - media_urls[key] = mg_globals.public_store.file_url(val) + if isinstance(mg_globals.public_store, BasicFileStorage): + # BasicFileStorage does not provide a qualified URI + media_urls[key] = urljoin( + urlgen('index', qualified=True), + mg_globals.public_store.file_url(val)) + else: + media_urls[key] = mg_globals.public_store.file_url(val) return media_urls diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index ff177e29..2eb9e414 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -129,8 +129,15 @@ def api_test(request): def get_entries(request): entries = request.db.MediaEntry.query + # TODO: Make it possible to fetch unprocessed media, or media in-processing entries = entries.filter_by(state=u'processed') + # TODO: Add sort order customization + entries = entries.order_by(request.db.MediaEntry.created.desc()) + + # TODO: Fetch default and upper limit from config + entries = entries.limit(int(request.GET.get('limit') or 10)) + entries_serializable = [] for entry in entries: -- cgit v1.2.3 From 09e528acbb4d1321fce5cec8b22fd7fd153bf68a Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Mon, 17 Sep 2012 23:54:27 +0200 Subject: Fixed validation in API post_entry. Added state to API get_entry_serializable --- mediagoblin/plugins/api/tools.py | 1 + mediagoblin/plugins/api/views.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index e5aca29b..c4630ba7 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -95,6 +95,7 @@ def get_entry_serializable(entry, urlgen): 'description': entry.description, 'description_html': entry.description_html, 'media_type': entry.media_type, + 'state': entry.state, 'permalink': entry.url_for_self(urlgen, qualified=True), 'media_files': get_media_file_paths(entry.media_files, urlgen)} diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 2eb9e414..d537ec6e 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -20,6 +20,7 @@ import uuid from os.path import splitext from webob import exc, Response +from cgi import FieldStorage from werkzeug.utils import secure_filename from celery import registry @@ -43,10 +44,18 @@ _log = logging.getLogger(__name__) @require_active_login def post_entry(request): _log.debug('Posting entry') + + if request.method == 'OPTIONS': + return json_response({'status': 200}) + if request.method != 'POST': + _log.debug('Must POST against post_entry') return exc.HTTPBadRequest() - if not 'file' in request.POST or not hasattr(request.POST['file'], 'file'): + if not 'file' in request.POST \ + or not isinstance(request.POST['file'], FieldStorage) \ + or not request.POST['file'].file: + _log.debug('File field not found') return exc.HTTPBadRequest() media_file = request.POST['file'] -- cgit v1.2.3 From df3147b986b183198d6419c50ca743e4f0c1bdea Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Tue, 18 Sep 2012 21:42:10 +0200 Subject: Added documentation for the OAuth plugin --- mediagoblin/plugins/oauth/README.rst | 130 +++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 mediagoblin/plugins/oauth/README.rst (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/README.rst b/mediagoblin/plugins/oauth/README.rst new file mode 100644 index 00000000..cd2edc0e --- /dev/null +++ b/mediagoblin/plugins/oauth/README.rst @@ -0,0 +1,130 @@ +============== + OAuth plugin +============== + +The OAuth plugin enables third party web applications to authenticate as one or +more GNU MediaGoblin users in a safe way in order retrieve, create and update +content stored on the GNU MediaGoblin instance. + +The OAuth plugin is based on the `oauth v2.25 draft`_ and is pointing by using +the ``oauthlib.oauth2.draft25.WebApplicationClient`` from oauthlib_ to a +mediagoblin instance and building the OAuth 2 provider logic around the client. + +There are surely some aspects of the OAuth v2.25 draft that haven't made it +into this plugin due to the technique used to develop it. + +.. _`oauth v2.25 draft`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25 +.. _oauthlib: http://pypi.python.org/pypi/oauthlib + + +Set up the OAuth plugin +======================= + +1. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section:: + + [[mediagoblin.plugins.oauth]] + +2. Run:: + + gmg dbupdate + + in order to create and apply migrations to any database tables that the + plugin requires. + +.. note:: + This only enables the OAuth plugin. To be able to let clients fetch data + from the MediaGoblin instance you should also enable the API plugin or some + other plugin that supports authenticating with OAuth credentials. + + +Authenticate against GNU MediaGoblin +==================================== + +.. note:: + As mentioned in `capabilities`_ GNU MediaGoblin currently only supports the + `Authorization Code Grant`_ procedure for obtaining an OAuth access token. + +Authorization Code Grant +------------------------ + +.. note:: + As mentioned in `incapabilities`_ GNU MediaGoblin currently does not + support `client registration`_ + +The `authorization code grant`_ works in the following way: + +`Definitions` + + Authorization server + The GNU MediaGoblin instance + Resource server + Also the GNU MediaGoblin instance ;) + Client + The web application intended to use the data + Redirect uri + An URI pointing to a page controlled by the *client* + Resource owner + The GNU MediaGoblin user who's resources the client requests access to + User agent + Commonly the GNU MediaGoblin user's web browser + Authorization code + An intermediate token that is exchanged for an *access token* + Access token + A secret token that the *client* uses to authenticate itself agains the + *resource server* as a specific *resource owner*. + + +Brief description of the procedure +++++++++++++++++++++++++++++++++++ + +1. The *client* requests an *authorization code* from the *authorization + server* by redirecting the *user agent* to the `Authorization Endpoint`_. + Which parameters should be included in the redirect are covered later in + this document. +2. The *authorization server* authenticates the *resource owner* and redirects + the *user agent* back to the *redirect uri* (covered later in this + document). +3. The *client* recieves the request from the *user agent*, attached is the + *authorization code*. +4. The *client* requests an *access token* from the *authorization server* +5. \?\?\?\?\? +6. Profit! + + +Detailed description of the procedure ++++++++++++++++++++++++++++++++++++++ + +TBD, in the meantime here is a proof-of-concept GNU MediaGoblin client: + +https://github.com/jwandborg/omgmg/ + +and here are some detailed descriptions from other OAuth 2 +providers: + +- https://developers.google.com/accounts/docs/OAuth2WebServer +- https://developers.facebook.com/docs/authentication/server-side/ ( + + +Capabilities +============ + +- `Authorization endpoint`_ - Located at ``/oauth/authorize`` +- `Token endpoint`_ - Located at ``/oauth/access_token`` +- `Authorization Code Grant`_ + +.. _`Authorization endpoint`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.1 +.. _`Token endpoint`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.2 +.. _`Authorization Code Grant`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-4.1 + +Incapabilities +============== + +- `Client Registration`_ - `planned feature + `_ +- `Access Token Scope`_ +- `Implicit Grant`_ +- ... + +.. _`Client Registration`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-2 +.. _`Access Token Scope`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.3 +.. _`Implicit Grant`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-4.2 -- cgit v1.2.3 From 4427b1bea4a47aed36c8c7fe8b90bd014a4a12a5 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Tue, 18 Sep 2012 21:44:02 +0200 Subject: Fixed typo in OAuth docs, recieve => receive --- mediagoblin/plugins/oauth/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/README.rst b/mediagoblin/plugins/oauth/README.rst index cd2edc0e..2f49916b 100644 --- a/mediagoblin/plugins/oauth/README.rst +++ b/mediagoblin/plugins/oauth/README.rst @@ -84,7 +84,7 @@ Brief description of the procedure 2. The *authorization server* authenticates the *resource owner* and redirects the *user agent* back to the *redirect uri* (covered later in this document). -3. The *client* recieves the request from the *user agent*, attached is the +3. The *client* receives the request from the *user agent*, attached is the *authorization code*. 4. The *client* requests an *access token* from the *authorization server* 5. \?\?\?\?\? -- cgit v1.2.3 From a7b8c214e929b2d1ea5237e594c7cd88432ad891 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Tue, 18 Sep 2012 21:51:22 +0200 Subject: Added some more helpful links to the OAuth docs - OAuth v2.25 draft - IRC channel - OAuth plugin source code --- mediagoblin/plugins/oauth/README.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/README.rst b/mediagoblin/plugins/oauth/README.rst index 2f49916b..790b2cdf 100644 --- a/mediagoblin/plugins/oauth/README.rst +++ b/mediagoblin/plugins/oauth/README.rst @@ -102,7 +102,13 @@ and here are some detailed descriptions from other OAuth 2 providers: - https://developers.google.com/accounts/docs/OAuth2WebServer -- https://developers.facebook.com/docs/authentication/server-side/ ( +- https://developers.facebook.com/docs/authentication/server-side/ + +and if you're unsure about anything, there's the `OAuth v2.25 draft +`_, the `OAuth plugin +source code +`_ +and the `#mediagoblin IRC channel `_. Capabilities -- cgit v1.2.3 From f26224d43359041f45adb28bdc3a9ac48570a0a3 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Wed, 19 Sep 2012 21:57:59 +0200 Subject: Fixed a horrible security issue in the OAuth plugin. Also added some real triggering logic to the OAuthAuth Auth object. --- mediagoblin/plugins/oauth/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py index 95919728..33dcaf16 100644 --- a/mediagoblin/plugins/oauth/__init__.py +++ b/mediagoblin/plugins/oauth/__init__.py @@ -48,7 +48,10 @@ def setup_plugin(): class OAuthAuth(Auth): def trigger(self, request): - return True + if 'access_token' in request.GET: + return True + + return False def __call__(self, request, *args, **kw): access_token = request.GET.get('access_token') @@ -60,9 +63,9 @@ class OAuthAuth(Auth): return False request.user = token.user + return True - return True - + return False hooks = { 'setup': setup_plugin, -- cgit v1.2.3 From 6ed236d5b5d727028597d4f9a8b68a729d438d2e Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Wed, 19 Sep 2012 22:12:13 +0200 Subject: Added security warning to OAuth README --- mediagoblin/plugins/oauth/README.rst | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/README.rst b/mediagoblin/plugins/oauth/README.rst index 790b2cdf..8ca5ed9f 100644 --- a/mediagoblin/plugins/oauth/README.rst +++ b/mediagoblin/plugins/oauth/README.rst @@ -2,6 +2,11 @@ OAuth plugin ============== +.. warning:: + In it's current state. This plugin has received no security audit. + Development has been entirely focused on Making It Work(TM). Use this + plugin with caution. + The OAuth plugin enables third party web applications to authenticate as one or more GNU MediaGoblin users in a safe way in order retrieve, create and update content stored on the GNU MediaGoblin instance. -- cgit v1.2.3 From d4c066abf017bc7af8fa30a25248dbae9e40355d Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Wed, 19 Sep 2012 22:13:16 +0200 Subject: Fixed typo in OAuth README --- mediagoblin/plugins/oauth/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/README.rst b/mediagoblin/plugins/oauth/README.rst index 8ca5ed9f..e5a1dc3a 100644 --- a/mediagoblin/plugins/oauth/README.rst +++ b/mediagoblin/plugins/oauth/README.rst @@ -3,7 +3,7 @@ ============== .. warning:: - In it's current state. This plugin has received no security audit. + In its current state. This plugin has received no security audit. Development has been entirely focused on Making It Work(TM). Use this plugin with caution. -- cgit v1.2.3 From 88a9662be4f97da5b04a3842c8d0caa2652be355 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Fri, 21 Sep 2012 13:02:35 +0200 Subject: Added client registration caps to OAuth plugin THE MIGRATIONS SUPPLIED WITH THIS COMMIT WILL DROP AND RE-CREATE YOUR oauth__tokens AND oauth__codes TABLES. ALL YOUR OAUTH CODES AND TOKENS WILL BE LOST. - Fixed pylint issues in db/sql/migrations. - Added __repr__ to the User model. - Added _disable_cors option to json_response. - Added crude error handling to the api.tools.api_auth decorator - Updated the OAuth README. - Added client registration, client overview, connection overview, client authorization views and templates. - Added error handling to the OAuthAuth Auth object. - Added AuthorizationForm, ClientRegistrationForm in oauth/forms. - Added migrations for OAuth, added client registration migration. - Added OAuthClient, OAuthUserClient models. - Added oauth/tools with require_client_auth decorator method. --- mediagoblin/plugins/api/tools.py | 20 ++- mediagoblin/plugins/oauth/README.rst | 7 +- mediagoblin/plugins/oauth/__init__.py | 43 +++++- mediagoblin/plugins/oauth/forms.py | 70 ++++++++++ mediagoblin/plugins/oauth/migrations.py | 46 ++++++ mediagoblin/plugins/oauth/models.py | 99 ++++++++++++- .../plugins/oauth/templates/oauth/authorize.html | 31 +++++ .../oauth/templates/oauth/client/connections.html | 34 +++++ .../plugins/oauth/templates/oauth/client/list.html | 45 ++++++ .../oauth/templates/oauth/client/register.html | 34 +++++ mediagoblin/plugins/oauth/tools.py | 43 ++++++ mediagoblin/plugins/oauth/views.py | 154 ++++++++++++++++++--- 12 files changed, 589 insertions(+), 37 deletions(-) create mode 100644 mediagoblin/plugins/oauth/forms.py create mode 100644 mediagoblin/plugins/oauth/migrations.py create mode 100644 mediagoblin/plugins/oauth/templates/oauth/authorize.html create mode 100644 mediagoblin/plugins/oauth/templates/oauth/client/connections.html create mode 100644 mediagoblin/plugins/oauth/templates/oauth/client/list.html create mode 100644 mediagoblin/plugins/oauth/templates/oauth/client/register.html create mode 100644 mediagoblin/plugins/oauth/tools.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index c4630ba7..ecc50364 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -52,7 +52,7 @@ class Auth(object): raise NotImplemented() -def json_response(serializable, *args, **kw): +def json_response(serializable, _disable_cors=False, *args, **kw): ''' Serializes a json objects and returns a webob.Response object with the serialized value as the response body and Content-Type: application/json. @@ -64,11 +64,14 @@ def json_response(serializable, *args, **kw): ''' response = Response(json.dumps(serializable), *args, **kw) response.headers['Content-Type'] = 'application/json' - cors_headers = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} - response.headers.update(cors_headers) + + if not _disable_cors: + cors_headers = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} + response.headers.update(cors_headers) + return response @@ -149,6 +152,11 @@ def api_auth(controller): auth, request.url)) if not auth(request, *args, **kw): + if getattr(auth, 'errors', []): + return json_response({ + 'status': 403, + 'errors': auth.errors}) + return exc.HTTPForbidden() return controller(request, *args, **kw) diff --git a/mediagoblin/plugins/oauth/README.rst b/mediagoblin/plugins/oauth/README.rst index e5a1dc3a..0c278e3e 100644 --- a/mediagoblin/plugins/oauth/README.rst +++ b/mediagoblin/plugins/oauth/README.rst @@ -122,20 +122,21 @@ Capabilities - `Authorization endpoint`_ - Located at ``/oauth/authorize`` - `Token endpoint`_ - Located at ``/oauth/access_token`` - `Authorization Code Grant`_ +- `Client Registration`_ .. _`Authorization endpoint`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.1 .. _`Token endpoint`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.2 .. _`Authorization Code Grant`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-4.1 +.. _`Client Registration`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-2 Incapabilities ============== -- `Client Registration`_ - `planned feature - `_ +- Only `bearer tokens`_ are issued. - `Access Token Scope`_ - `Implicit Grant`_ - ... -.. _`Client Registration`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-2 +.. _`bearer tokens`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08 .. _`Access Token Scope`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.3 .. _`Implicit Grant`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-4.2 diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py index 33dcaf16..63bf49a8 100644 --- a/mediagoblin/plugins/oauth/__init__.py +++ b/mediagoblin/plugins/oauth/__init__.py @@ -18,11 +18,10 @@ import os import logging from routes.route import Route -from webob import exc from mediagoblin.tools import pluginapi -from mediagoblin.tools.response import render_to_response -from mediagoblin.plugins.oauth.models import OAuthToken +from mediagoblin.plugins.oauth.models import OAuthToken, OAuthClient, \ + OAuthUserClient from mediagoblin.plugins.api.tools import Auth _log = logging.getLogger(__name__) @@ -39,8 +38,19 @@ def setup_plugin(): routes = [ Route('mediagoblin.plugins.oauth.authorize', '/oauth/authorize', controller='mediagoblin.plugins.oauth.views:authorize'), + Route('mediagoblin.plugins.oauth.authorize_client', '/oauth/client/authorize', + controller='mediagoblin.plugins.oauth.views:authorize_client'), Route('mediagoblin.plugins.oauth.access_token', '/oauth/access_token', - controller='mediagoblin.plugins.oauth.views:access_token')] + controller='mediagoblin.plugins.oauth.views:access_token'), + Route('mediagoblin.plugins.oauth.access_token', + '/oauth/client/connections', + controller='mediagoblin.plugins.oauth.views:list_connections'), + Route('mediagoblin.plugins.oauth.register_client', + '/oauth/client/register', + controller='mediagoblin.plugins.oauth.views:register_client'), + Route('mediagoblin.plugins.oauth.list_clients', + '/oauth/client/list', + controller='mediagoblin.plugins.oauth.views:list_clients')] pluginapi.register_routes(routes) pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) @@ -54,17 +64,42 @@ class OAuthAuth(Auth): return False def __call__(self, request, *args, **kw): + self.errors = [] + # TODO: Add suport for client credentials authorization + client_id = request.GET.get('client_id') # TODO: Not used + client_secret = request.GET.get('client_secret') # TODO: Not used access_token = request.GET.get('access_token') + + _log.debug('Authorizing request {0}'.format(request.url)) + if access_token: token = OAuthToken.query.filter(OAuthToken.token == access_token)\ .first() if not token: + self.errors.append('Invalid access token') + return False + + _log.debug('Access token: {0}'.format(token)) + _log.debug('Client: {0}'.format(token.client)) + + relation = OAuthUserClient.query.filter( + (OAuthUserClient.user == token.user) + & (OAuthUserClient.client == token.client) + & (OAuthUserClient.state == u'approved')).first() + + _log.debug('Relation: {0}'.format(relation)) + + if not relation: + self.errors.append( + u'Client has not been approved by the resource owner') return False request.user = token.user return True + self.errors.append(u'No access_token specified') + return False hooks = { diff --git a/mediagoblin/plugins/oauth/forms.py b/mediagoblin/plugins/oauth/forms.py new file mode 100644 index 00000000..35995373 --- /dev/null +++ b/mediagoblin/plugins/oauth/forms.py @@ -0,0 +1,70 @@ +# 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 . + +import wtforms + +from urlparse import urlparse + +from mediagoblin.tools.extlib.wtf_html5 import URLField +from mediagoblin.tools.translate import fake_ugettext_passthrough as _ + + +class AuthorizationForm(wtforms.Form): + client_id = wtforms.HiddenField(_(u'Client ID'), + [wtforms.validators.Required()]) + next = wtforms.HiddenField(_(u'Next URL'), + [wtforms.validators.Required()]) + allow = wtforms.SubmitField(_(u'Allow')) + deny = wtforms.SubmitField(_(u'Deny')) + + +class ClientRegistrationForm(wtforms.Form): + name = wtforms.TextField(_('Name'), [wtforms.validators.Required()], + description=_('The name of the OAuth client')) + description = wtforms.TextAreaField(_('Description'), + [wtforms.validators.Length(min=0, max=500)], + description=_('''This will be visisble to users allowing your + appplication to authenticate as them.''')) + type = wtforms.SelectField(_('Type'), + [wtforms.validators.Required()], + choices=[ + ('confidential', 'Confidential'), + ('public', 'Public')], + description=_('''Confidential - The client can + make requests to the GNU MediaGoblin instance that can not be + intercepted by the user agent (e.g. server-side client).
+ Public - The client can't make confidential + requests to the GNU MediaGoblin instance (e.g. client-side + JavaScript client).''')) + + redirect_uri = URLField(_('Redirect URI'), + [wtforms.validators.Optional(), wtforms.validators.URL()], + description=_('''The redirect URI for the applications, this field + is required for public clients.''')) + + def __init__(self, *args, **kw): + wtforms.Form.__init__(self, *args, **kw) + + def validate(self): + if not wtforms.Form.validate(self): + return False + + if self.type.data == 'public' and not self.redirect_uri.data: + self.redirect_uri.errors.append( + _('This field is required for public clients')) + return False + + return True diff --git a/mediagoblin/plugins/oauth/migrations.py b/mediagoblin/plugins/oauth/migrations.py new file mode 100644 index 00000000..f2af3907 --- /dev/null +++ b/mediagoblin/plugins/oauth/migrations.py @@ -0,0 +1,46 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from sqlalchemy import MetaData, Table + +from mediagoblin.db.sql.util import RegisterMigration + +from mediagoblin.plugins.oauth.models import OAuthClient, OAuthToken, \ + OAuthUserClient, OAuthCode + +MIGRATIONS = {} + + +@RegisterMigration(1, MIGRATIONS) +def remove_and_replace_token_and_code(db): + metadata = MetaData(bind=db.bind) + + token_table = Table('oauth__tokens', metadata, autoload=True, + autoload_with=db.bind) + + token_table.drop() + + code_table = Table('oauth__codes', metadata, autoload=True, + autoload_with=db.bind) + + code_table.drop() + + OAuthClient.__table__.create(db.bind) + OAuthUserClient.__table__.create(db.bind) + OAuthToken.__table__.create(db.bind) + OAuthCode.__table__.create(db.bind) + + db.commit() diff --git a/mediagoblin/plugins/oauth/models.py b/mediagoblin/plugins/oauth/models.py index 295987c8..91bd0fc6 100644 --- a/mediagoblin/plugins/oauth/models.py +++ b/mediagoblin/plugins/oauth/models.py @@ -14,15 +14,84 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import uuid +import bcrypt + from datetime import datetime, timedelta from mediagoblin.db.sql.base import Base from mediagoblin.db.sql.models import User from sqlalchemy import ( - Column, Unicode, Integer, DateTime, ForeignKey) + Column, Unicode, Integer, DateTime, ForeignKey, Enum) from sqlalchemy.orm import relationship +# Don't remove this, I *think* it applies sqlalchemy-migrate functionality onto +# the models. +from migrate import changeset + + +class OAuthClient(Base): + __tablename__ = 'oauth__client' + + id = Column(Integer, primary_key=True) + created = Column(DateTime, nullable=False, + default=datetime.now) + + name = Column(Unicode) + description = Column(Unicode) + + identifier = Column(Unicode, unique=True, index=True) + secret = Column(Unicode, index=True) + + owner_id = Column(Integer, ForeignKey(User.id)) + owner = relationship(User, backref='registered_clients') + + redirect_uri = Column(Unicode) + + type = Column(Enum( + u'confidential', + u'public')) + + def generate_identifier(self): + self.identifier = unicode(uuid.uuid4()) + + def generate_secret(self): + self.secret = unicode( + bcrypt.hashpw( + unicode(uuid.uuid4()), + bcrypt.gensalt())) + + def __repr__(self): + return '<{0} {1}:{2} ({3})>'.format( + self.__class__.__name__, + self.id, + self.name.encode('ascii', 'replace'), + self.owner.username.encode('ascii', 'replace')) + + +class OAuthUserClient(Base): + __tablename__ = 'oauth__user_client' + id = Column(Integer, primary_key=True) + + user_id = Column(Integer, ForeignKey(User.id)) + user = relationship(User, backref='oauth_clients') + + client_id = Column(Integer, ForeignKey(OAuthClient.id)) + client = relationship(OAuthClient, backref='users') + + state = Column(Enum( + u'approved', + u'rejected')) + + def __repr__(self): + return '<{0} #{1} {2} [{3}, {4}]>'.format( + self.__class__.__name__, + self.id, + self.state.encode('ascii', 'replace'), + self.user, + self.client) + class OAuthToken(Base): __tablename__ = 'oauth__tokens' @@ -39,6 +108,17 @@ class OAuthToken(Base): index=True) user = relationship(User) + client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False) + client = relationship(OAuthClient) + + def __repr__(self): + return '<{0} #{1} expires {2} [{3}, {4}]>'.format( + self.__class__.__name__, + self.id, + self.expires.isoformat(), + self.user, + self.client) + class OAuthCode(Base): __tablename__ = 'oauth__codes' @@ -54,5 +134,20 @@ class OAuthCode(Base): index=True) user = relationship(User) + client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False) + client = relationship(OAuthClient) + + def __repr__(self): + return '<{0} #{1} expires {2} [{3}, {4}]>'.format( + self.__class__.__name__, + self.id, + self.expires.isoformat(), + self.user, + self.client) + -MODELS = [OAuthToken, OAuthCode] +MODELS = [ + OAuthToken, + OAuthCode, + OAuthClient, + OAuthUserClient] diff --git a/mediagoblin/plugins/oauth/templates/oauth/authorize.html b/mediagoblin/plugins/oauth/templates/oauth/authorize.html new file mode 100644 index 00000000..647fa41f --- /dev/null +++ b/mediagoblin/plugins/oauth/templates/oauth/authorize.html @@ -0,0 +1,31 @@ +{# +# 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. +#, se, seee +# 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 . +-#} +{% extends "mediagoblin/base.html" %} +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_content %} +
+
+ {{ csrf_token }} +

Authorize {{ client.name }}?

+

{{ client.description }}

+ {{ wtforms_util.render_divs(form) }} +
+
+{% endblock %} diff --git a/mediagoblin/plugins/oauth/templates/oauth/client/connections.html b/mediagoblin/plugins/oauth/templates/oauth/client/connections.html new file mode 100644 index 00000000..63b0230a --- /dev/null +++ b/mediagoblin/plugins/oauth/templates/oauth/client/connections.html @@ -0,0 +1,34 @@ +{# +# 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 . +-#} +{% extends "mediagoblin/base.html" %} +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_content %} +

{% trans %}OAuth client connections{% endtrans %}

+{% if connections %} +
    + {% for connection in connections %} +
  • {{ + connection.client.name }} - {{ connection.state }} +
  • + {% endfor %} +
+{% else %} +

You haven't connected using an OAuth client before.

+{% endif %} +{% endblock %} diff --git a/mediagoblin/plugins/oauth/templates/oauth/client/list.html b/mediagoblin/plugins/oauth/templates/oauth/client/list.html new file mode 100644 index 00000000..21024bb7 --- /dev/null +++ b/mediagoblin/plugins/oauth/templates/oauth/client/list.html @@ -0,0 +1,45 @@ +{# +# 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 . +-#} +{% extends "mediagoblin/base.html" %} +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_content %} +

{% trans %}Your OAuth clients{% endtrans %}

+{% if clients %} +
    + {% for client in clients %} +
  • {{ client.name }} +
    +
    Type
    +
    {{ client.type }}
    +
    Description
    +
    {{ client.description }}
    +
    Identifier
    +
    {{ client.identifier }}
    +
    Secret
    +
    {{ client.secret }}
    +
    Redirect URI
    +
    {{ client.redirect_uri }}
    +
    +
  • + {% endfor %} +
+{% else %} +

You don't have any clients yet. Add one.

+{% endif %} +{% endblock %} diff --git a/mediagoblin/plugins/oauth/templates/oauth/client/register.html b/mediagoblin/plugins/oauth/templates/oauth/client/register.html new file mode 100644 index 00000000..6fd700d3 --- /dev/null +++ b/mediagoblin/plugins/oauth/templates/oauth/client/register.html @@ -0,0 +1,34 @@ +{# +# 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 . +-#} +{% extends "mediagoblin/base.html" %} +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_content %} +
+
+

Register OAuth client

+ {{ wtforms_util.render_divs(form) }} +
+ {{ csrf_token }} + +
+
+
+{% endblock %} diff --git a/mediagoblin/plugins/oauth/tools.py b/mediagoblin/plugins/oauth/tools.py new file mode 100644 index 00000000..d21c8a5b --- /dev/null +++ b/mediagoblin/plugins/oauth/tools.py @@ -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 . + +from functools import wraps + +from mediagoblin.plugins.oauth.models import OAuthClient +from mediagoblin.plugins.api.tools import json_response + + +def require_client_auth(controller): + @wraps(controller) + def wrapper(request, *args, **kw): + if not request.GET.get('client_id'): + return json_response({ + 'status': 400, + 'errors': [u'No client identifier in URL']}, + _disable_cors=True) + + client = OAuthClient.query.filter( + OAuthClient.identifier == request.GET.get('client_id')).first() + + if not client: + return json_response({ + 'status': 400, + 'errors': [u'No such client identifier']}, + _disable_cors=True) + + return controller(request, client) + + return wrapper diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py index 70be3039..1c0d7f86 100644 --- a/mediagoblin/plugins/oauth/views.py +++ b/mediagoblin/plugins/oauth/views.py @@ -21,38 +21,142 @@ from webob import exc, Response from urllib import urlencode from uuid import uuid4 from datetime import datetime -from functools import wraps -from mediagoblin.tools import pluginapi -from mediagoblin.tools.response import render_to_response +from mediagoblin.tools.response import render_to_response, redirect from mediagoblin.decorators import require_active_login from mediagoblin.messages import add_message, SUCCESS, ERROR from mediagoblin.tools.translate import pass_to_ugettext as _ -from mediagoblin.plugins.oauth.models import OAuthCode, OAuthToken +from mediagoblin.plugins.oauth.models import OAuthCode, OAuthToken, \ + OAuthClient, OAuthUserClient +from mediagoblin.plugins.oauth.forms import ClientRegistrationForm, \ + AuthorizationForm +from mediagoblin.plugins.oauth.tools import require_client_auth +from mediagoblin.plugins.api.tools import json_response _log = logging.getLogger(__name__) @require_active_login -def authorize(request): - # TODO: Check if allowed +def register_client(request): + ''' + Register an OAuth client + ''' + form = ClientRegistrationForm(request.POST) - # Client is allowed by the user - if True or already_authorized: - # Generate a code - # Save the code, the client will later use it to obtain an access token - # Redirect the user agent to the redirect_uri with the code + if request.method == 'POST' and form.validate(): + client = OAuthClient() + client.name = unicode(request.POST['name']) + client.description = unicode(request.POST['description']) + client.type = unicode(request.POST['type']) + client.owner_id = request.user.id + client.redirect_uri = unicode(request.POST['redirect_uri']) - if not 'redirect_uri' in request.GET: - add_message(request, ERROR, _('No redirect_uri found')) + client.generate_identifier() + client.generate_secret() + + client.save() + + add_message(request, SUCCESS, _('The client {0} has been registered!')\ + .format( + client.name)) + + return redirect(request, 'mediagoblin.plugins.oauth.list_clients') + + return render_to_response( + request, + 'oauth/client/register.html', + {'form': form}) + + +@require_active_login +def list_clients(request): + clients = request.db.OAuthClient.query.filter( + OAuthClient.owner_id == request.user.id).all() + return render_to_response(request, 'oauth/client/list.html', + {'clients': clients}) + + +@require_active_login +def list_connections(request): + connections = OAuthUserClient.query.filter( + OAuthUserClient.user == request.user).all() + return render_to_response(request, 'oauth/client/connections.html', + {'connections': connections}) + + +@require_active_login +def authorize_client(request): + form = AuthorizationForm(request.POST) + + client = OAuthClient.query.filter(OAuthClient.id == + form.client_id.data).first() + + if not client: + _log.error('''No such client id as received from client authorization + form.''') + return exc.HTTPBadRequest() + + if form.validate(): + relation = OAuthUserClient() + relation.user_id = request.user.id + relation.client_id = form.client_id.data + if form.allow.data: + relation.state = u'approved' + elif form.deny.data: + relation.state = u'rejected' + else: + return exc.HTTPBadRequest + + relation.save() + + return exc.HTTPFound(location=form.next.data) + + return render_to_response( + request, + 'oauth/authorize.html', + {'form': form, + 'client': client}) + + +@require_client_auth +@require_active_login +def authorize(request, client): + # TODO: Get rid of the JSON responses in this view, it's called by the + # user-agent, not the client. + user_client_relation = OAuthUserClient.query.filter( + (OAuthUserClient.user == request.user) + & (OAuthUserClient.client == client)) + + if user_client_relation.filter(OAuthUserClient.state == + u'approved').count(): + redirect_uri = None + + if client.type == u'public': + if not client.redirect_uri: + return json_response({ + 'status': 400, + 'errors': + [u'Public clients MUST have a redirect_uri pre-set']}, + _disable_cors=True) + + redirect_uri = client.redirect_uri + + if client.type == u'confidential': + redirect_uri = request.GET.get('redirect_uri', client.redirect_uri) + if not redirect_uri: + return json_response({ + 'status': 400, + 'errors': [u'Can not find a redirect_uri for client: {0}'\ + .format(client.name)]}, _disable_cors=True) code = OAuthCode() code.code = unicode(uuid4()) code.user = request.user + code.client = client code.save() redirect_uri = ''.join([ - request.GET.get('redirect_uri'), + redirect_uri, '?', urlencode({'code': code.code})]) @@ -65,28 +169,34 @@ def authorize(request): # code parameter # - on deny: send the user agent back to the redirect uri with error # information - pass - return render_to_response(request, 'oauth/base.html', {}) + form = AuthorizationForm(request.POST) + form.client_id.data = client.id + form.next.data = request.url + return render_to_response( + request, + 'oauth/authorize.html', + {'form': form, + 'client': client}) def access_token(request): if request.GET.get('code'): - code = OAuthCode.query.filter(OAuthCode.code == request.GET.get('code'))\ - .first() + code = OAuthCode.query.filter(OAuthCode.code == + request.GET.get('code')).first() if code: token = OAuthToken() token.token = unicode(uuid4()) token.user = code.user + token.client = code.client token.save() access_token_data = { 'access_token': token.token, - 'token_type': 'what_do_i_use_this_for', # TODO + 'token_type': 'bearer', 'expires_in': - (token.expires - datetime.now()).total_seconds(), - 'refresh_token': 'This should probably be safe'} - return Response(json.dumps(access_token_data)) + (token.expires - datetime.now()).total_seconds()} + return json_response(access_token_data, _disable_cors=True) error_data = { 'error': 'Incorrect code'} -- cgit v1.2.3 From 6b8c66d44ccab4f73935caa420aaafd5ef5bb9b1 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Fri, 21 Sep 2012 13:20:56 +0200 Subject: Added name kwarg to Column(Enum(...)) in OAuth models --- mediagoblin/plugins/oauth/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/models.py b/mediagoblin/plugins/oauth/models.py index 91bd0fc6..7e247c1a 100644 --- a/mediagoblin/plugins/oauth/models.py +++ b/mediagoblin/plugins/oauth/models.py @@ -51,7 +51,8 @@ class OAuthClient(Base): type = Column(Enum( u'confidential', - u'public')) + u'public', + name=u'oauth__client_type')) def generate_identifier(self): self.identifier = unicode(uuid.uuid4()) @@ -82,7 +83,8 @@ class OAuthUserClient(Base): state = Column(Enum( u'approved', - u'rejected')) + u'rejected', + name=u'oauth__relation_state')) def __repr__(self): return '<{0} #{1} {2} [{3}, {4}]>'.format( -- cgit v1.2.3 From 5ee1ab2abb9d4ab0945930a6d79750bdd02ba8a3 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 22 Sep 2012 12:03:32 +0200 Subject: Fixed typos visisble, appplication --- mediagoblin/plugins/oauth/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/forms.py b/mediagoblin/plugins/oauth/forms.py index 35995373..7a2b9fa0 100644 --- a/mediagoblin/plugins/oauth/forms.py +++ b/mediagoblin/plugins/oauth/forms.py @@ -36,8 +36,8 @@ class ClientRegistrationForm(wtforms.Form): description=_('The name of the OAuth client')) description = wtforms.TextAreaField(_('Description'), [wtforms.validators.Length(min=0, max=500)], - description=_('''This will be visisble to users allowing your - appplication to authenticate as them.''')) + description=_('''This will be visble to users allowing your + application to authenticate as them.''')) type = wtforms.SelectField(_('Type'), [wtforms.validators.Required()], choices=[ -- cgit v1.2.3 From 315ac0a2d3fe570cb23a01ce7f094389e1e207d2 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Mon, 24 Sep 2012 11:38:55 -0500 Subject: Correcting a couple of spelling errors. Thanks elesa, for finding them! --- mediagoblin/plugins/oauth/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/forms.py b/mediagoblin/plugins/oauth/forms.py index 7a2b9fa0..2a956dad 100644 --- a/mediagoblin/plugins/oauth/forms.py +++ b/mediagoblin/plugins/oauth/forms.py @@ -36,7 +36,7 @@ class ClientRegistrationForm(wtforms.Form): description=_('The name of the OAuth client')) description = wtforms.TextAreaField(_('Description'), [wtforms.validators.Length(min=0, max=500)], - description=_('''This will be visble to users allowing your + description=_('''This will be visible to users allowing your application to authenticate as them.''')) type = wtforms.SelectField(_('Type'), [wtforms.validators.Required()], -- cgit v1.2.3 From 5354f954dc94aafd35bc037faad2412f73320d8c Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Mon, 24 Sep 2012 23:47:32 +0200 Subject: Added support for http callbacks on processing Sends an HTTP POST request back to an URL given on submission to the API submit view. --- mediagoblin/plugins/api/views.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index d537ec6e..5f38f8d2 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -98,6 +98,12 @@ def post_entry(request): # Save now so we have this data before kicking off processing entry.save(validate=True) + if request.POST.get('callback_url'): + metadata = request.db.ProcessingMetaData() + metadata.media_entry = entry + metadata.callback_url = unicode(request.POST['callback_url']) + metadata.save() + # Pass off to processing # # (... don't change entry after this point to avoid race -- cgit v1.2.3 From 111a609df526bd3690fc03e623eaf5826f33f4d2 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 29 Sep 2012 21:07:15 +0200 Subject: Replaced all request.POST with request.form, ... - Fixed error handling in OAuth plugin - Changed request.POST file fields to request.files --- mediagoblin/plugins/api/views.py | 27 ++++++++--------- mediagoblin/plugins/oauth/README.rst | 6 ++-- mediagoblin/plugins/oauth/views.py | 58 ++++++++++++++++++++++++++++-------- 3 files changed, 61 insertions(+), 30 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 5f38f8d2..a1b1bcac 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -20,8 +20,8 @@ import uuid from os.path import splitext from webob import exc, Response -from cgi import FieldStorage from werkzeug.utils import secure_filename +from werkzeug.datastructures import FileStorage from celery import registry from mediagoblin.db.util import ObjectId @@ -29,13 +29,10 @@ from mediagoblin.decorators import require_active_login from mediagoblin.processing import mark_entry_failed from mediagoblin.processing.task import ProcessMedia from mediagoblin.meddleware.csrf import csrf_exempt -from mediagoblin.media_types import sniff_media, InvalidFileType, \ - FileTypeNotSupported +from mediagoblin.media_types import sniff_media from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \ json_response -from mediagoblin.plugins.api import config - _log = logging.getLogger(__name__) @@ -52,24 +49,24 @@ def post_entry(request): _log.debug('Must POST against post_entry') return exc.HTTPBadRequest() - if not 'file' in request.POST \ - or not isinstance(request.POST['file'], FieldStorage) \ - or not request.POST['file'].file: + if not 'file' in request.files \ + or not isinstance(request.files['file'], FileStorage) \ + or not request.files['file'].stream: _log.debug('File field not found') return exc.HTTPBadRequest() - media_file = request.POST['file'] + media_file = request.files['file'] media_type, media_manager = sniff_media(media_file) entry = request.db.MediaEntry() entry.id = ObjectId() entry.media_type = unicode(media_type) - entry.title = unicode(request.POST.get('title') + entry.title = unicode(request.form.get('title') or splitext(media_file.filename)[0]) - entry.description = unicode(request.POST.get('description')) - entry.license = unicode(request.POST.get('license', '')) + entry.description = unicode(request.form.get('description')) + entry.license = unicode(request.form.get('license', '')) entry.uploader = request.user.id @@ -88,7 +85,7 @@ def post_entry(request): queue_filepath, 'wb') with queue_file: - queue_file.write(request.POST['file'].file.read()) + queue_file.write(request.files['file'].stream.read()) # Add queued filename to the entry entry.queued_media_file = queue_filepath @@ -98,10 +95,10 @@ def post_entry(request): # Save now so we have this data before kicking off processing entry.save(validate=True) - if request.POST.get('callback_url'): + if request.form.get('callback_url'): metadata = request.db.ProcessingMetaData() metadata.media_entry = entry - metadata.callback_url = unicode(request.POST['callback_url']) + metadata.callback_url = unicode(request.form['callback_url']) metadata.save() # Pass off to processing diff --git a/mediagoblin/plugins/oauth/README.rst b/mediagoblin/plugins/oauth/README.rst index 0c278e3e..405a67e2 100644 --- a/mediagoblin/plugins/oauth/README.rst +++ b/mediagoblin/plugins/oauth/README.rst @@ -133,10 +133,12 @@ Incapabilities ============== - Only `bearer tokens`_ are issued. -- `Access Token Scope`_ - `Implicit Grant`_ +- `Force TLS for token endpoint`_ - This one is up the the siteadmin +- Authorization `scope`_ and `state` - ... .. _`bearer tokens`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08 -.. _`Access Token Scope`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.3 +.. _`scope`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.3 .. _`Implicit Grant`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-4.2 +.. _`Force TLS for token endpoint`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.2 diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py index 1c0d7f86..cf605fd2 100644 --- a/mediagoblin/plugins/oauth/views.py +++ b/mediagoblin/plugins/oauth/views.py @@ -41,15 +41,15 @@ def register_client(request): ''' Register an OAuth client ''' - form = ClientRegistrationForm(request.POST) + form = ClientRegistrationForm(request.form) if request.method == 'POST' and form.validate(): client = OAuthClient() - client.name = unicode(request.POST['name']) - client.description = unicode(request.POST['description']) - client.type = unicode(request.POST['type']) + client.name = unicode(request.form['name']) + client.description = unicode(request.form['description']) + client.type = unicode(request.form['type']) client.owner_id = request.user.id - client.redirect_uri = unicode(request.POST['redirect_uri']) + client.redirect_uri = unicode(request.form['redirect_uri']) client.generate_identifier() client.generate_secret() @@ -86,7 +86,7 @@ def list_connections(request): @require_active_login def authorize_client(request): - form = AuthorizationForm(request.POST) + form = AuthorizationForm(request.form) client = OAuthClient.query.filter(OAuthClient.id == form.client_id.data).first() @@ -169,7 +169,7 @@ def authorize(request, client): # code parameter # - on deny: send the user agent back to the redirect uri with error # information - form = AuthorizationForm(request.POST) + form = AuthorizationForm(request.form) form.client_id.data = client.id form.next.data = request.url return render_to_response( @@ -185,6 +185,31 @@ def access_token(request): request.GET.get('code')).first() if code: + if code.client.type == u'confidential': + client_identifier = request.GET.get('client_id') + + if not client_identifier: + return json_response({ + 'error': 'invalid_request', + 'error_description': + 'Missing client_id in request'}) + + client_secret = request.GET.get('client_secret') + + if not client_secret: + return json_response({ + 'error': 'invalid_request', + 'error_description': + 'Missing client_secret in request'}) + + if not client_secret == code.client.secret or \ + not client_identifier == code.client.identifier: + return json_response({ + 'error': 'invalid_client', + 'error_description': + 'The client_id or client_secret does not match the' + ' code'}) + token = OAuthToken() token.token = unicode(uuid4()) token.user = code.user @@ -194,10 +219,17 @@ def access_token(request): access_token_data = { 'access_token': token.token, 'token_type': 'bearer', - 'expires_in': - (token.expires - datetime.now()).total_seconds()} + 'expires_in': int( + round( + (token.expires - datetime.now()).total_seconds()))} return json_response(access_token_data, _disable_cors=True) - - error_data = { - 'error': 'Incorrect code'} - return Response(json.dumps(error_data)) + else: + return json_response({ + 'error': 'invalid_request', + 'error_description': + 'Invalid code'}) + else: + return json_response({ + 'error': 'invalid_request', + 'error_descriptin': + 'Missing `code` parameter in request'}) -- cgit v1.2.3 From 7742dcc1fbda04c3a1c76a057a1a93a8f504502e Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sun, 14 Oct 2012 13:46:31 +0200 Subject: Switched most stuff over from Routes Removed the Routes routing functionality and replaced it with werkzeug.routes. Most views are functional. Known issues: - Translation integration with the request object is not yet figured out. This breaks 404 pages. --- mediagoblin/plugins/api/__init__.py | 12 ++++++------ mediagoblin/plugins/oauth/__init__.py | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/__init__.py b/mediagoblin/plugins/api/__init__.py index 40722088..f370cca6 100644 --- a/mediagoblin/plugins/api/__init__.py +++ b/mediagoblin/plugins/api/__init__.py @@ -33,12 +33,12 @@ def setup_plugin(): _log.debug('API config: {0}'.format(config)) routes = [ - Route('mediagoblin.plugins.api.test', '/api/test', - controller='mediagoblin.plugins.api.views:api_test'), - Route('mediagoblin.plugins.api.entries', '/api/entries', - controller='mediagoblin.plugins.api.views:get_entries'), - Route('mediagoblin.plugins.api.post_entry', '/api/submit', - controller='mediagoblin.plugins.api.views:post_entry')] + ('mediagoblin.plugins.api.test', '/api/test', + 'mediagoblin.plugins.api.views:api_test'), + ('mediagoblin.plugins.api.entries', '/api/entries', + 'mediagoblin.plugins.api.views:get_entries'), + ('mediagoblin.plugins.api.post_entry', '/api/submit', + 'mediagoblin.plugins.api.views:post_entry')] pluginapi.register_routes(routes) diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py index 63bf49a8..64acf0e7 100644 --- a/mediagoblin/plugins/oauth/__init__.py +++ b/mediagoblin/plugins/oauth/__init__.py @@ -36,21 +36,21 @@ def setup_plugin(): _log.debug('OAuth config: {0}'.format(config)) routes = [ - Route('mediagoblin.plugins.oauth.authorize', '/oauth/authorize', - controller='mediagoblin.plugins.oauth.views:authorize'), - Route('mediagoblin.plugins.oauth.authorize_client', '/oauth/client/authorize', - controller='mediagoblin.plugins.oauth.views:authorize_client'), - Route('mediagoblin.plugins.oauth.access_token', '/oauth/access_token', - controller='mediagoblin.plugins.oauth.views:access_token'), - Route('mediagoblin.plugins.oauth.access_token', + ('mediagoblin.plugins.oauth.authorize', '/oauth/authorize', + 'mediagoblin.plugins.oauth.views:authorize'), + ('mediagoblin.plugins.oauth.authorize_client', '/oauth/client/authorize', + 'mediagoblin.plugins.oauth.views:authorize_client'), + ('mediagoblin.plugins.oauth.access_token', '/oauth/access_token', + 'mediagoblin.plugins.oauth.views:access_token'), + ('mediagoblin.plugins.oauth.access_token', '/oauth/client/connections', - controller='mediagoblin.plugins.oauth.views:list_connections'), - Route('mediagoblin.plugins.oauth.register_client', + 'mediagoblin.plugins.oauth.views:list_connections'), + ('mediagoblin.plugins.oauth.register_client', '/oauth/client/register', - controller='mediagoblin.plugins.oauth.views:register_client'), - Route('mediagoblin.plugins.oauth.list_clients', + 'mediagoblin.plugins.oauth.views:register_client'), + ('mediagoblin.plugins.oauth.list_clients', '/oauth/client/list', - controller='mediagoblin.plugins.oauth.views:list_clients')] + 'mediagoblin.plugins.oauth.views:list_clients')] pluginapi.register_routes(routes) pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) -- cgit v1.2.3 From d56e82635f85f7a8a7d184a3eae539c09a7b001d Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Mon, 15 Oct 2012 00:12:58 +0200 Subject: Fixed OAuth access_token duplicate route Changed route name to "[...]list_connections" --- mediagoblin/plugins/api/__init__.py | 9 ++++++--- mediagoblin/plugins/oauth/__init__.py | 11 +++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/__init__.py b/mediagoblin/plugins/api/__init__.py index f370cca6..3b7ced0c 100644 --- a/mediagoblin/plugins/api/__init__.py +++ b/mediagoblin/plugins/api/__init__.py @@ -33,11 +33,14 @@ def setup_plugin(): _log.debug('API config: {0}'.format(config)) routes = [ - ('mediagoblin.plugins.api.test', '/api/test', + ('mediagoblin.plugins.api.test', + '/api/test', 'mediagoblin.plugins.api.views:api_test'), - ('mediagoblin.plugins.api.entries', '/api/entries', + ('mediagoblin.plugins.api.entries', + '/api/entries', 'mediagoblin.plugins.api.views:get_entries'), - ('mediagoblin.plugins.api.post_entry', '/api/submit', + ('mediagoblin.plugins.api.post_entry', + '/api/submit', 'mediagoblin.plugins.api.views:post_entry')] pluginapi.register_routes(routes) diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py index 64acf0e7..3ed695de 100644 --- a/mediagoblin/plugins/oauth/__init__.py +++ b/mediagoblin/plugins/oauth/__init__.py @@ -36,13 +36,16 @@ def setup_plugin(): _log.debug('OAuth config: {0}'.format(config)) routes = [ - ('mediagoblin.plugins.oauth.authorize', '/oauth/authorize', + ('mediagoblin.plugins.oauth.authorize', + '/oauth/authorize', 'mediagoblin.plugins.oauth.views:authorize'), - ('mediagoblin.plugins.oauth.authorize_client', '/oauth/client/authorize', + ('mediagoblin.plugins.oauth.authorize_client', + '/oauth/client/authorize', 'mediagoblin.plugins.oauth.views:authorize_client'), - ('mediagoblin.plugins.oauth.access_token', '/oauth/access_token', - 'mediagoblin.plugins.oauth.views:access_token'), ('mediagoblin.plugins.oauth.access_token', + '/oauth/access_token', + 'mediagoblin.plugins.oauth.views:access_token'), + ('mediagoblin.plugins.oauth.list_connections', '/oauth/client/connections', 'mediagoblin.plugins.oauth.views:list_connections'), ('mediagoblin.plugins.oauth.register_client', -- cgit v1.2.3 From 5b60ec41ee5d0f25d66190b2a0114a8e1b216f86 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 20 Oct 2012 12:09:23 +0200 Subject: Removed Routes dependency, added admin routes --- mediagoblin/plugins/api/__init__.py | 2 -- mediagoblin/plugins/flatpagesfile/__init__.py | 3 +-- mediagoblin/plugins/oauth/__init__.py | 2 -- 3 files changed, 1 insertion(+), 6 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/__init__.py b/mediagoblin/plugins/api/__init__.py index 3b7ced0c..d3fdf2ef 100644 --- a/mediagoblin/plugins/api/__init__.py +++ b/mediagoblin/plugins/api/__init__.py @@ -17,8 +17,6 @@ import os import logging -from routes.route import Route - from mediagoblin.tools import pluginapi _log = logging.getLogger(__name__) diff --git a/mediagoblin/plugins/flatpagesfile/__init__.py b/mediagoblin/plugins/flatpagesfile/__init__.py index b9b52012..3d797809 100644 --- a/mediagoblin/plugins/flatpagesfile/__init__.py +++ b/mediagoblin/plugins/flatpagesfile/__init__.py @@ -19,7 +19,6 @@ import logging import os import jinja2 -from routes.route import Route from mediagoblin.tools import pluginapi from mediagoblin.tools.response import render_to_response @@ -68,7 +67,7 @@ def setup_plugin(): name = 'flatpagesfile.%s' % name.strip() controller = flatpage_handler_builder(template) routes.append( - Route(name, url, controller=controller)) + (name, url, controller)) pluginapi.register_routes(routes) _log.info('Done setting up flatpagesfile!') diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py index 3ed695de..4714d95d 100644 --- a/mediagoblin/plugins/oauth/__init__.py +++ b/mediagoblin/plugins/oauth/__init__.py @@ -17,8 +17,6 @@ import os import logging -from routes.route import Route - from mediagoblin.tools import pluginapi from mediagoblin.plugins.oauth.models import OAuthToken, OAuthClient, \ OAuthUserClient -- cgit v1.2.3 From ac4c5aef5775d5c4a3d8ebd5528e6d2db8062174 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 20 Oct 2012 23:56:00 +0200 Subject: Added HTTP API auth plugin --- mediagoblin/plugins/httpapiauth/__init__.py | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 mediagoblin/plugins/httpapiauth/__init__.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/httpapiauth/__init__.py b/mediagoblin/plugins/httpapiauth/__init__.py new file mode 100644 index 00000000..d3d2065e --- /dev/null +++ b/mediagoblin/plugins/httpapiauth/__init__.py @@ -0,0 +1,58 @@ +# 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 . + +import logging +import base64 + +from werkzeug.exceptions import BadRequest, Unauthorized + +from mediagoblin.plugins.api.tools import Auth + +_log = logging.getLogger(__name__) + + +def setup_http_api_auth(): + _log.info('Setting up HTTP API Auth...') + + +class HTTPAuth(Auth): + def trigger(self, request): + if request.authorization: + return True + + return False + + def __call__(self, request, *args, **kw): + _log.debug('Trying to authorize the user agent via HTTP Auth') + if not request.authorization: + return False + + user = request.db.User.query.filter_by( + username=request.authorization['username']).first() + + if user.check_login(request.authorization['password']): + request.user = user + return True + else: + raise Unauthorized() + + return False + + + +hooks = { + 'setup': setup_http_api_auth, + 'auth': HTTPAuth()} -- cgit v1.2.3 From 316e1dfddeb7955c3bb8a5183c53024c68184a22 Mon Sep 17 00:00:00 2001 From: Elrond Date: Sat, 24 Nov 2012 19:19:18 +0100 Subject: SQL Migrations: Rewrite table creation completely. We have migrations creating new tables. Those currently use "raw" table definitions. This easily gives errors (we already had this problem). So instead rewrite those to use declarative tables and use those to create new tables. Just copy the new table over to the migration, strip it down to the bare minimum, rename to _v0, base it on declarative_base() and be done! Do this for the current migrations. --- mediagoblin/plugins/oauth/migrations.py | 92 ++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 7 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/migrations.py b/mediagoblin/plugins/oauth/migrations.py index f2af3907..797e7585 100644 --- a/mediagoblin/plugins/oauth/migrations.py +++ b/mediagoblin/plugins/oauth/migrations.py @@ -14,16 +14,94 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sqlalchemy import MetaData, Table +from datetime import datetime, timedelta +from sqlalchemy import (MetaData, Table, Column, + Integer, Unicode, Enum, DateTime, ForeignKey) +from sqlalchemy.ext.declarative import declarative_base from mediagoblin.db.sql.util import RegisterMigration +from mediagoblin.db.sql.models import User -from mediagoblin.plugins.oauth.models import OAuthClient, OAuthToken, \ - OAuthUserClient, OAuthCode MIGRATIONS = {} +class OAuthClient_v0(declarative_base()): + __tablename__ = 'oauth__client' + + id = Column(Integer, primary_key=True) + created = Column(DateTime, nullable=False, + default=datetime.now) + + name = Column(Unicode) + description = Column(Unicode) + + identifier = Column(Unicode, unique=True, index=True) + secret = Column(Unicode, index=True) + + owner_id = Column(Integer, ForeignKey(User.id)) + redirect_uri = Column(Unicode) + + type = Column(Enum( + u'confidential', + u'public', + name=u'oauth__client_type')) + + +class OAuthUserClient_v0(declarative_base()): + __tablename__ = 'oauth__user_client' + id = Column(Integer, primary_key=True) + + user_id = Column(Integer, ForeignKey(User.id)) + client_id = Column(Integer, ForeignKey(OAuthClient_v0.id)) + + state = Column(Enum( + u'approved', + u'rejected', + name=u'oauth__relation_state')) + + +class OAuthToken_v0(declarative_base()): + __tablename__ = 'oauth__tokens' + + id = Column(Integer, primary_key=True) + created = Column(DateTime, nullable=False, + default=datetime.now) + expires = Column(DateTime, nullable=False, + default=lambda: datetime.now() + timedelta(days=30)) + token = Column(Unicode, index=True) + refresh_token = Column(Unicode, index=True) + + user_id = Column(Integer, ForeignKey(User.id), nullable=False, + index=True) + + client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False) + + def __repr__(self): + return '<{0} #{1} expires {2} [{3}, {4}]>'.format( + self.__class__.__name__, + self.id, + self.expires.isoformat(), + self.user, + self.client) + + +class OAuthCode_v0(declarative_base()): + __tablename__ = 'oauth__codes' + + id = Column(Integer, primary_key=True) + created = Column(DateTime, nullable=False, + default=datetime.now) + expires = Column(DateTime, nullable=False, + default=lambda: datetime.now() + timedelta(minutes=5)) + code = Column(Unicode, index=True) + + user_id = Column(Integer, ForeignKey(User.id), nullable=False, + index=True) + + client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False) + + @RegisterMigration(1, MIGRATIONS) def remove_and_replace_token_and_code(db): metadata = MetaData(bind=db.bind) @@ -38,9 +116,9 @@ def remove_and_replace_token_and_code(db): code_table.drop() - OAuthClient.__table__.create(db.bind) - OAuthUserClient.__table__.create(db.bind) - OAuthToken.__table__.create(db.bind) - OAuthCode.__table__.create(db.bind) + OAuthClient_v0.__table__.create(db.bind) + OAuthUserClient_v0.__table__.create(db.bind) + OAuthToken_v0.__table__.create(db.bind) + OAuthCode_v0.__table__.create(db.bind) db.commit() -- cgit v1.2.3 From 9945468603b8476f735d735dbc7505ee47ff9f42 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Thu, 29 Nov 2012 11:28:25 +0100 Subject: trim_whitespace meddleware plugin Our HTML output is very verbose (=whitespacy) as our templates are written with an 80 char limit and lots of newlines between blocks, variables etc.... This is a plugin that naively strips of all but the first whitespace from the HTML response. We might want to have an all-fancy html tidy interface here at some point, but it nicely decreases the HTML size about a third on some simple pages. Signed-off-by: Sebastian Spaeth --- mediagoblin/plugins/trim_whitespace/README.rst | 25 +++++++++ mediagoblin/plugins/trim_whitespace/__init__.py | 73 +++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 mediagoblin/plugins/trim_whitespace/README.rst create mode 100644 mediagoblin/plugins/trim_whitespace/__init__.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/trim_whitespace/README.rst b/mediagoblin/plugins/trim_whitespace/README.rst new file mode 100644 index 00000000..b55ce35e --- /dev/null +++ b/mediagoblin/plugins/trim_whitespace/README.rst @@ -0,0 +1,25 @@ +======================= + Trim whitespace plugin +======================= + +Mediagoblin templates are written with 80 char limit for better +readability. However that means that the html output is very verbose +containing LOTS of whitespace. This plugin inserts a Middleware that +filters out whitespace from the returned HTML in the Response() objects. + +Simply enable this plugin by putting it somewhere where python can reach it and put it's path into the [plugins] section of your mediagoblin.ini or mediagoblin_local.ini like for example this: + + [plugins] + [[mediagoblin.plugins.trim_whitespace]] + +There is no further configuration required. If this plugin is enabled, +all text/html documents should not have lots of whitespace in between +elements, although it does a very naive filtering right now (just keep +the first whitespace and delete all subsequent ones). + +Nonetheless, it is a useful plugin that might serve as inspiration for +other plugin writers. + +It was originally conceived by Sebastian Spaeth. It is licensed under +the GNU AGPL v3 (or any later version) license. + diff --git a/mediagoblin/plugins/trim_whitespace/__init__.py b/mediagoblin/plugins/trim_whitespace/__init__.py new file mode 100644 index 00000000..3da1e8b4 --- /dev/null +++ b/mediagoblin/plugins/trim_whitespace/__init__.py @@ -0,0 +1,73 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import unicode_literals +import logging +import re + +from mediagoblin import meddleware + +_log = logging.getLogger(__name__) + +class TrimWhiteSpaceMeddleware(meddleware.BaseMeddleware): + _setup_plugin_called = 0 + RE_MULTI_WHITESPACE = re.compile(b'(\s)\s+', re.M) + + def process_response(self, request, response): + """Perform very naive html tidying by removing multiple whitespaces""" + # werkzeug.BaseResponse has no content_type attr, this comes via + # werkzeug.wrappers.CommonRequestDescriptorsMixin (part of + # wrappers.Response) + if getattr(response ,'content_type', None) != 'text/html': + return + + # This is a tad more complex than needed to be able to handle + # response.data and response.body, depending on whether we have + # a werkzeug Resonse or a webob one. Let's kill webob soon! + if hasattr(response, 'body') and not hasattr(response, 'data'): + # Old-style webob Response object. + # TODO: Remove this once we transition away from webob + resp_attr = 'body' + else: + resp_attr = 'data' + # Don't flatten iterator to list when we fudge the response body + # (see werkzeug.Response documentation) + response.implicit_sequence_conversion = False + + # Set the tidied text. Very naive tidying for now, just strip all + # subsequent whitespaces (this preserves most newlines) + setattr(response, resp_attr, re.sub( + TrimWhiteSpaceMeddleware.RE_MULTI_WHITESPACE, br'\1', + getattr(response, resp_attr))) + + @classmethod + def setup_plugin(cls): + """Set up this meddleware as a plugin during 'setup' hook""" + global _log + if cls._setup_plugin_called: + _log.info('Trim whitespace plugin was already set up.') + return + + _log.debug('Trim whitespace plugin set up.') + cls._setup_plugin_called += 1 + + # Append ourselves to the list of enabled Meddlewares + meddleware.ENABLED_MEDDLEWARE.append( + '{0}:{1}'.format(cls.__module__, cls.__name__)) + + +hooks = { + 'setup': TrimWhiteSpaceMeddleware.setup_plugin + } -- cgit v1.2.3 From 2f5926a65d4e56d4ab9c7bfd6b3de25a032b8be5 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 14 Dec 2012 10:54:53 +0100 Subject: Fiy python2.7'ism (#566) The oauth plugin used timedelta.total_seconds which was introduced in python 2.7 only. To preserve backwards compatability, we simply calculate the time difference in seconds manually. I considered monkeypatching total_seconds to the timedelta object, but it is a built-in type written in C (I believe) and modifying attributes failed horribly. Switch this to use total_seconds once we require python 2.7 as minimum version. Signed-off-by: Sebastian Spaeth --- mediagoblin/plugins/oauth/views.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py index cf605fd2..643c2783 100644 --- a/mediagoblin/plugins/oauth/views.py +++ b/mediagoblin/plugins/oauth/views.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # GNU MediaGoblin -- federated, autonomous media hosting # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. # @@ -216,12 +217,15 @@ def access_token(request): token.client = code.client token.save() + # expire time of token in full seconds + # timedelta.total_seconds is python >= 2.7 or we would use that + td = token.expires - datetime.now() + exp_in = 86400*td.days + td.seconds # just ignore µsec + access_token_data = { 'access_token': token.token, 'token_type': 'bearer', - 'expires_in': int( - round( - (token.expires - datetime.now()).total_seconds()))} + 'expires_in': exp_in} return json_response(access_token_data, _disable_cors=True) else: return json_response({ -- cgit v1.2.3 From 5c2b84869fe3f4bfe41a31ff3968bb13c6d7f868 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 30 Nov 2012 10:49:06 +0100 Subject: Move DBModel._id -> DBModel.id We were refering to model._id in most of the code base as this is what Mongo uses. However, each use of _id required a) fixup of queries: e.g. what we did in our find() and find_one() functions moving all '_id' to 'id'. It also required using AliasFields to make the ._id attribute available. This all means lots of superfluous fixing and transitioning in a SQL world. It will also not work in the long run. Much newer code already refers to the objects by model.id (e.g. in the oauth plugin), which will break with Mongo. So let's be honest, rip out the _id mongoism and live with .id as the one canonical way to address objects. This commit modifies all users and providers of model._id to use model.id instead. This patch works with or without Mongo removed first, but will break Mongo usage (even more than before) I have not bothered to fixup db.mongo.* and db.sql.convert (which converts from Mongo to SQL) Signed-off-by: Sebastian Spaeth --- mediagoblin/plugins/api/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index a1b1bcac..7f93108e 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -108,7 +108,7 @@ def post_entry(request): process_media = registry.tasks[ProcessMedia.name] try: process_media.apply_async( - [unicode(entry._id)], {}, + [unicode(entry.id)], {}, task_id=task_id) except BaseException as e: # The purpose of this section is because when running in "lazy" @@ -119,7 +119,7 @@ def post_entry(request): # # ... not completely the diaper pattern because the # exception is re-raised :) - mark_entry_failed(entry._id, e) + mark_entry_failed(entry.id, e) # re-raise the exception raise -- cgit v1.2.3 From 62d14bf50baf45ac15fe5276be74b073de880f77 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Thu, 15 Nov 2012 16:55:15 +0100 Subject: Transition webob.HttpForbidden to webob's exceptions Forbidden Also the BadRequest exception. --- mediagoblin/plugins/api/tools.py | 7 ++++--- mediagoblin/plugins/api/views.py | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index ecc50364..c4073d23 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -18,8 +18,9 @@ import logging import json from functools import wraps -from webob import exc, Response +from webob import Response from urlparse import urljoin +from werkzeug.exceptions import Forbidden from mediagoblin import mg_globals from mediagoblin.tools.pluginapi import PluginManager @@ -143,7 +144,7 @@ def api_auth(controller): # If we can't find any authentication methods, we should not let them # pass. if not auth_candidates: - return exc.HTTPForbidden() + return Forbidden() # For now, just select the first one in the list auth = auth_candidates[0] @@ -157,7 +158,7 @@ def api_auth(controller): 'status': 403, 'errors': auth.errors}) - return exc.HTTPForbidden() + return Forbidden() return controller(request, *args, **kw) diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 7f93108e..39f864c4 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -19,7 +19,8 @@ import logging import uuid from os.path import splitext -from webob import exc, Response +from webob import Response +from werkzeug.exceptions import BadRequest, Forbidden from werkzeug.utils import secure_filename from werkzeug.datastructures import FileStorage from celery import registry @@ -47,13 +48,13 @@ def post_entry(request): if request.method != 'POST': _log.debug('Must POST against post_entry') - return exc.HTTPBadRequest() + return BadRequest() if not 'file' in request.files \ or not isinstance(request.files['file'], FileStorage) \ or not request.files['file'].stream: _log.debug('File field not found') - return exc.HTTPBadRequest() + return BadRequest() media_file = request.files['file'] @@ -129,7 +130,7 @@ def post_entry(request): @api_auth def api_test(request): if not request.user: - return exc.HTTPForbidden() + return Forbidden() user_data = { 'username': request.user.username, -- cgit v1.2.3 From bf3b9e783dc86b38864e66c2aab149472e636461 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 16 Nov 2012 11:13:01 +0100 Subject: Transition webob BadRequest|HTTPFound to webob/redirect More transitioning away from webob Response import from webob was unused --- mediagoblin/plugins/oauth/views.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py index 643c2783..c7b2a332 100644 --- a/mediagoblin/plugins/oauth/views.py +++ b/mediagoblin/plugins/oauth/views.py @@ -18,7 +18,6 @@ import logging import json -from webob import exc, Response from urllib import urlencode from uuid import uuid4 from datetime import datetime @@ -95,7 +94,7 @@ def authorize_client(request): if not client: _log.error('''No such client id as received from client authorization form.''') - return exc.HTTPBadRequest() + return BadRequest() if form.validate(): relation = OAuthUserClient() @@ -106,11 +105,11 @@ def authorize_client(request): elif form.deny.data: relation.state = u'rejected' else: - return exc.HTTPBadRequest + return BadRequest relation.save() - return exc.HTTPFound(location=form.next.data) + return redirect(request, location=form.next.data) return render_to_response( request, @@ -163,7 +162,7 @@ def authorize(request, client): _log.debug('Redirecting to {0}'.format(redirect_uri)) - return exc.HTTPFound(location=redirect_uri) + return redirect(request, location=redirect_uri) else: # Show prompt to allow client to access data # - on accept: send the user agent back to the redirect_uri with the -- cgit v1.2.3 From 74af60bb32f9009b838f7acee73c9a6cca2be265 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 16 Nov 2012 11:21:15 +0100 Subject: replace webob.Response with werkzeug Response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace webob usage in one more file. Document a TODO that should be clarified, we should probably be using json_response rather than Response() here. Modify the TestMeddleware to not rely on the content_type attribute being present, while werkzeug.wrappers Response() has it the BaseResponse() object which is often returned in tests does not have it. Signed-off-by: Sebastian Spaeth --- mediagoblin/plugins/api/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 39f864c4..8e02d7bd 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -19,10 +19,10 @@ import logging import uuid from os.path import splitext -from webob import Response +from werkzeug.datastructures import FileStorage from werkzeug.exceptions import BadRequest, Forbidden from werkzeug.utils import secure_filename -from werkzeug.datastructures import FileStorage +from werkzeug.wrappers import Response from celery import registry from mediagoblin.db.util import ObjectId @@ -136,6 +136,8 @@ def api_test(request): 'username': request.user.username, 'email': request.user.email} + # TODO: This is the *only* thing using Response() here, should that + # not simply use json_response()? return Response(json.dumps(user_data)) -- cgit v1.2.3 From 7c552c0bd76a4bb0292bbdf694d0ce4ba65de0a7 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Wed, 12 Dec 2012 11:38:51 +0100 Subject: plugins/api: use headers.set(), not headers.update() The werkzeug.Response().headers do not offer an update() method as the same key can be twice in the header 'dict'. Thus, iterate over the header keys and use header.set(key, value) which replaces an existing header key. Signed-off-by: Sebastian Spaeth --- mediagoblin/plugins/api/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index c4073d23..333a5682 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -71,7 +71,7 @@ def json_response(serializable, _disable_cors=False, *args, **kw): 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} - response.headers.update(cors_headers) + (response.headers.set(key, value) for key, value in cors_headers) return response -- cgit v1.2.3 From cc195d5b821319732038fbd5d13bcc8b5a951ffc Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 16 Nov 2012 11:31:16 +0100 Subject: plugins/api: webob.Response -> werkzeug.Response --- mediagoblin/plugins/api/tools.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index 333a5682..0ef91127 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -18,10 +18,9 @@ import logging import json from functools import wraps -from webob import Response from urlparse import urljoin from werkzeug.exceptions import Forbidden - +from werkzeug.wrappers import Response from mediagoblin import mg_globals from mediagoblin.tools.pluginapi import PluginManager from mediagoblin.storage.filestorage import BasicFileStorage @@ -55,16 +54,15 @@ class Auth(object): def json_response(serializable, _disable_cors=False, *args, **kw): ''' - Serializes a json objects and returns a webob.Response object with the + Serializes a json objects and returns a werkzeug Response object with the serialized value as the response body and Content-Type: application/json. :param serializable: A json-serializable object Any extra arguments and keyword arguments are passed to the - webob.Response.__init__ method. + Response.__init__ method. ''' - response = Response(json.dumps(serializable), *args, **kw) - response.headers['Content-Type'] = 'application/json' + response = Response(json.dumps(serializable), *args, content_type='application/json', **kw) if not _disable_cors: cors_headers = { -- cgit v1.2.3 From cfa922295e5ddfaab336a3c2c0403422f77758b6 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Sun, 23 Dec 2012 11:58:51 +0100 Subject: Convert return HttpException to raise HttpException controllers (view function) raise HttpException's and do not return them. Signed-off-by: Sebastian Spaeth --- mediagoblin/plugins/api/tools.py | 4 ++-- mediagoblin/plugins/api/views.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index 0ef91127..03f528ce 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -142,7 +142,7 @@ def api_auth(controller): # If we can't find any authentication methods, we should not let them # pass. if not auth_candidates: - return Forbidden() + raise Forbidden() # For now, just select the first one in the list auth = auth_candidates[0] @@ -156,7 +156,7 @@ def api_auth(controller): 'status': 403, 'errors': auth.errors}) - return Forbidden() + raise Forbidden() return controller(request, *args, **kw) diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 8e02d7bd..3d9437e0 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -48,13 +48,13 @@ def post_entry(request): if request.method != 'POST': _log.debug('Must POST against post_entry') - return BadRequest() + raise BadRequest() if not 'file' in request.files \ or not isinstance(request.files['file'], FileStorage) \ or not request.files['file'].stream: _log.debug('File field not found') - return BadRequest() + raise BadRequest() media_file = request.files['file'] @@ -130,7 +130,7 @@ def post_entry(request): @api_auth def api_test(request): if not request.user: - return Forbidden() + raise Forbidden() user_data = { 'username': request.user.username, -- cgit v1.2.3 From 57c6473aa2146f3337a42cb4b9c54dc164b7b76a Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sun, 23 Dec 2012 00:34:27 +0100 Subject: Added API tests --- mediagoblin/plugins/api/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index 03f528ce..e5878258 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -135,8 +135,8 @@ def api_auth(controller): auth_candidates = [] for auth in PluginManager().get_hook_callables('auth'): - _log.debug('Plugin auth: {0}'.format(auth)) if auth.trigger(request): + _log.debug('{0} believes it is capable of authenticating this request.'.format(auth)) auth_candidates.append(auth) # If we can't find any authentication methods, we should not let them -- cgit v1.2.3 From 1eac751bd2b3ecf4f9bfffe101c3f16f1e49eba6 Mon Sep 17 00:00:00 2001 From: Elrond Date: Sun, 23 Dec 2012 23:55:44 +0100 Subject: Fix some unicode related issues in oauth and the api. Found using the previous commit. --- mediagoblin/plugins/httpapiauth/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/httpapiauth/__init__.py b/mediagoblin/plugins/httpapiauth/__init__.py index d3d2065e..081b590e 100644 --- a/mediagoblin/plugins/httpapiauth/__init__.py +++ b/mediagoblin/plugins/httpapiauth/__init__.py @@ -41,7 +41,7 @@ class HTTPAuth(Auth): return False user = request.db.User.query.filter_by( - username=request.authorization['username']).first() + username=unicode(request.authorization['username'])).first() if user.check_login(request.authorization['password']): request.user = user -- cgit v1.2.3 From b39d1f2351352fca21666137fa137eb64926a036 Mon Sep 17 00:00:00 2001 From: Elrond Date: Sun, 23 Dec 2012 21:01:13 +0100 Subject: Mongo removal: Remove the validate=True arg to obj.save() all callers were forced to use validate=True anyway. So remove this useless stuff. --- mediagoblin/plugins/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 3d9437e0..4c36d110 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -94,7 +94,7 @@ def post_entry(request): entry.queued_task_id = task_id # Save now so we have this data before kicking off processing - entry.save(validate=True) + entry.save() if request.form.get('callback_url'): metadata = request.db.ProcessingMetaData() -- cgit v1.2.3 From ac8212fe657802eaf9dfd9d6c0f28f25b750399e Mon Sep 17 00:00:00 2001 From: Elrond Date: Sun, 23 Dec 2012 21:44:05 +0100 Subject: Remove mongo style .id = ObjectId() On SQL we can't generate the primary key on our own. So just remove this stuff. --- mediagoblin/plugins/api/views.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 4c36d110..d3cef432 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -25,7 +25,6 @@ from werkzeug.utils import secure_filename from werkzeug.wrappers import Response from celery import registry -from mediagoblin.db.util import ObjectId from mediagoblin.decorators import require_active_login from mediagoblin.processing import mark_entry_failed from mediagoblin.processing.task import ProcessMedia @@ -61,7 +60,6 @@ def post_entry(request): media_type, media_manager = sniff_media(media_file) entry = request.db.MediaEntry() - entry.id = ObjectId() entry.media_type = unicode(media_type) entry.title = unicode(request.form.get('title') or splitext(media_file.filename)[0]) -- cgit v1.2.3 From 37f90b435d7b1d2d8f74d1378f92e1a8743b77a8 Mon Sep 17 00:00:00 2001 From: Elrond Date: Tue, 18 Dec 2012 12:24:10 +0100 Subject: Use run_process_media in the api. Now refactor in the api. Start with run_process_media. --- mediagoblin/plugins/api/views.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index d3cef432..15c9a082 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -23,15 +23,13 @@ from werkzeug.datastructures import FileStorage from werkzeug.exceptions import BadRequest, Forbidden from werkzeug.utils import secure_filename from werkzeug.wrappers import Response -from celery import registry from mediagoblin.decorators import require_active_login -from mediagoblin.processing import mark_entry_failed -from mediagoblin.processing.task import ProcessMedia from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.media_types import sniff_media from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \ json_response +from mediagoblin.submit.lib import run_process_media _log = logging.getLogger(__name__) @@ -104,23 +102,7 @@ def post_entry(request): # # (... don't change entry after this point to avoid race # conditions with changes to the document via processing code) - process_media = registry.tasks[ProcessMedia.name] - try: - process_media.apply_async( - [unicode(entry.id)], {}, - task_id=task_id) - except BaseException as e: - # The purpose of this section is because when running in "lazy" - # or always-eager-with-exceptions-propagated celery mode that - # the failure handling won't happen on Celery end. Since we - # expect a lot of users to run things in this way we have to - # capture stuff here. - # - # ... not completely the diaper pattern because the - # exception is re-raised :) - mark_entry_failed(entry.id, e) - # re-raise the exception - raise + run_process_media(entry) return json_response(get_entry_serializable(entry, request.urlgen)) -- cgit v1.2.3 From 01986008f66a1899d415a186c688f3056f6d8e6b Mon Sep 17 00:00:00 2001 From: Elrond Date: Tue, 18 Dec 2012 12:32:42 +0100 Subject: upload refactor: Use prepare_entry in api. --- mediagoblin/plugins/api/views.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 15c9a082..e8f64574 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -16,12 +16,10 @@ import json import logging -import uuid from os.path import splitext from werkzeug.datastructures import FileStorage from werkzeug.exceptions import BadRequest, Forbidden -from werkzeug.utils import secure_filename from werkzeug.wrappers import Response from mediagoblin.decorators import require_active_login @@ -29,7 +27,7 @@ from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.media_types import sniff_media from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \ json_response -from mediagoblin.submit.lib import run_process_media +from mediagoblin.submit.lib import prepare_entry, run_process_media _log = logging.getLogger(__name__) @@ -69,26 +67,12 @@ def post_entry(request): entry.generate_slug() - task_id = unicode(uuid.uuid4()) - - # Now store generate the queueing related filename - queue_filepath = request.app.queue_store.get_unique_filepath( - ['media_entries', - task_id, - secure_filename(media_file.filename)]) - # queue appropriately - queue_file = request.app.queue_store.get_file( - queue_filepath, 'wb') + queue_file = prepare_entry(request, entry, media_file.filename) with queue_file: queue_file.write(request.files['file'].stream.read()) - # Add queued filename to the entry - entry.queued_media_file = queue_filepath - - entry.queued_task_id = task_id - # Save now so we have this data before kicking off processing entry.save() -- cgit v1.2.3 From b228d89715558281d9573543dd0d6c74836d42ca Mon Sep 17 00:00:00 2001 From: Elrond Date: Wed, 26 Dec 2012 23:40:42 +0100 Subject: prepare_queue_task: Take app not request. First rename prepare_entry to prepare_queue_task, because this is really more like what this thing does. Thanks to Velmont for noting that we do not need a request in here, but an "app" is good enough. Which means, that this stuff can be called from tool scripts too. --- mediagoblin/plugins/api/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index e8f64574..0991297d 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -27,7 +27,7 @@ from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.media_types import sniff_media from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \ json_response -from mediagoblin.submit.lib import prepare_entry, run_process_media +from mediagoblin.submit.lib import prepare_queue_task, run_process_media _log = logging.getLogger(__name__) @@ -68,7 +68,7 @@ def post_entry(request): entry.generate_slug() # queue appropriately - queue_file = prepare_entry(request, entry, media_file.filename) + queue_file = prepare_queue_task(request.app, entry, media_file.filename) with queue_file: queue_file.write(request.files['file'].stream.read()) -- cgit v1.2.3 From 78fa73bcd57f46b3711eba22a3537f3965069d4f Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Wed, 2 Jan 2013 20:05:07 +0100 Subject: Made api_test use @require_active_login --- mediagoblin/plugins/api/views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 0991297d..6aa4ef9f 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -92,10 +92,8 @@ def post_entry(request): @api_auth +@require_active_login def api_test(request): - if not request.user: - raise Forbidden() - user_data = { 'username': request.user.username, 'email': request.user.email} -- cgit v1.2.3 From 1e46dc2537c5ee706a55876c4dbf86129baafbbf Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 30 Nov 2012 09:49:45 +0100 Subject: Move db.sql.util to db.util Now that sqlalchemy is providing the database abstractions, there is no need to hide everything in db.sql. sub-modules. It complicates the code and provides a futher layer of indirection. Move the db.sql.util.py to db.util.py and adapt the importers. --- mediagoblin/plugins/oauth/migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/migrations.py b/mediagoblin/plugins/oauth/migrations.py index 797e7585..3beef087 100644 --- a/mediagoblin/plugins/oauth/migrations.py +++ b/mediagoblin/plugins/oauth/migrations.py @@ -19,7 +19,7 @@ from sqlalchemy import (MetaData, Table, Column, Integer, Unicode, Enum, DateTime, ForeignKey) from sqlalchemy.ext.declarative import declarative_base -from mediagoblin.db.sql.util import RegisterMigration +from mediagoblin.db.util import RegisterMigration from mediagoblin.db.sql.models import User -- cgit v1.2.3 From b0c8328e547288028e7e43f0ceb1fa9f7c8dac4a Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 30 Nov 2012 10:10:35 +0100 Subject: Move db.sql.models* to db.models* --- mediagoblin/plugins/oauth/migrations.py | 2 +- mediagoblin/plugins/oauth/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/migrations.py b/mediagoblin/plugins/oauth/migrations.py index 3beef087..7b028faf 100644 --- a/mediagoblin/plugins/oauth/migrations.py +++ b/mediagoblin/plugins/oauth/migrations.py @@ -20,7 +20,7 @@ from sqlalchemy import (MetaData, Table, Column, from sqlalchemy.ext.declarative import declarative_base from mediagoblin.db.util import RegisterMigration -from mediagoblin.db.sql.models import User +from mediagoblin.db.models import User MIGRATIONS = {} diff --git a/mediagoblin/plugins/oauth/models.py b/mediagoblin/plugins/oauth/models.py index 7e247c1a..f52a8ce9 100644 --- a/mediagoblin/plugins/oauth/models.py +++ b/mediagoblin/plugins/oauth/models.py @@ -20,7 +20,7 @@ import bcrypt from datetime import datetime, timedelta from mediagoblin.db.sql.base import Base -from mediagoblin.db.sql.models import User +from mediagoblin.db.models import User from sqlalchemy import ( Column, Unicode, Integer, DateTime, ForeignKey, Enum) -- cgit v1.2.3 From 39dc3bf8db4a0a21220560a259574da4f2c1e12a Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 7 Jan 2013 13:03:51 +0100 Subject: Mv db.sql.base to db.base This concludes the db.sql.* -> db.* move. Our db abstraction layer is sqlalchemy, so there is no need to a separate db.sql.* hierarchy. All tests have been run for each of the commit series to make sure everything works at every step. --- mediagoblin/plugins/oauth/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/models.py b/mediagoblin/plugins/oauth/models.py index f52a8ce9..695dad31 100644 --- a/mediagoblin/plugins/oauth/models.py +++ b/mediagoblin/plugins/oauth/models.py @@ -19,7 +19,7 @@ import bcrypt from datetime import datetime, timedelta -from mediagoblin.db.sql.base import Base +from mediagoblin.db.base import Base from mediagoblin.db.models import User from sqlalchemy import ( -- cgit v1.2.3 From a050e776c6d9c4970dd241e92b2f24a8c9deb36d Mon Sep 17 00:00:00 2001 From: Elrond Date: Thu, 13 Dec 2012 12:31:47 +0100 Subject: Move all the migration tools into new migration_tools.py Factor all the migration related stuff out into a new .db.sql.migration_tools. First we don't have to load this module for our normal server. Second it makes all the import dependencies a little more cleaner. --- mediagoblin/plugins/oauth/migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/migrations.py b/mediagoblin/plugins/oauth/migrations.py index 7b028faf..45256dcc 100644 --- a/mediagoblin/plugins/oauth/migrations.py +++ b/mediagoblin/plugins/oauth/migrations.py @@ -19,7 +19,7 @@ from sqlalchemy import (MetaData, Table, Column, Integer, Unicode, Enum, DateTime, ForeignKey) from sqlalchemy.ext.declarative import declarative_base -from mediagoblin.db.util import RegisterMigration +from mediagoblin.db.sql.migration_tools import RegisterMigration from mediagoblin.db.models import User -- cgit v1.2.3 From c130e3ee79affaf9e2e52a98506ffb1a7f5c9db6 Mon Sep 17 00:00:00 2001 From: Elrond Date: Tue, 8 Jan 2013 22:12:16 +0100 Subject: Move db.sql.migration_tools to db.migration_tools. Follow the new trend. --- mediagoblin/plugins/oauth/migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/migrations.py b/mediagoblin/plugins/oauth/migrations.py index 45256dcc..6aa0d7cb 100644 --- a/mediagoblin/plugins/oauth/migrations.py +++ b/mediagoblin/plugins/oauth/migrations.py @@ -19,7 +19,7 @@ from sqlalchemy import (MetaData, Table, Column, Integer, Unicode, Enum, DateTime, ForeignKey) from sqlalchemy.ext.declarative import declarative_base -from mediagoblin.db.sql.migration_tools import RegisterMigration +from mediagoblin.db.migration_tools import RegisterMigration from mediagoblin.db.models import User -- cgit v1.2.3 From 2cfffd5ed8c054bb60c27ede4e69667f97d12b09 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Tue, 15 Jan 2013 14:41:30 +0100 Subject: Make PuSHing the Pubhubsubbub server an async task (#436, #585) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notifying the PuSH servers had 3 problems.  1) it was done immediately after sending of the processing task to celery. So if celery was run in a separate process we would notify the PuSH servers before the new media was processed/ visible. (#436) 2) Notification code was called in submit/views.py, so submitting via the API never resulted in notifications. (#585) 3) If Notifying the PuSH server failed, we would never retry. The solution was to make the PuSH notification an asynchronous subtask. This way: 1) it will only be called once async processing has finished, 2) it is in the main processing code path, so even API calls will result in notifications, and 3) We retry 3 times in case of failure before giving up. If the server is in a separate process, we will wait 3x 2 minutes before retrying the notification. The only downside is that the celery server needs to have access to the internet to ping the PuSH server. If that is a problem, we need to make the task belong to a special group of celery servers that has access to the internet. As a side effect, I believe I removed the limitation that prevented us from upgrading celery. Signed-off-by: Sebastian Spaeth --- mediagoblin/plugins/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 6aa4ef9f..7383e20d 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -86,7 +86,7 @@ def post_entry(request): # # (... don't change entry after this point to avoid race # conditions with changes to the document via processing code) - run_process_media(entry) + run_process_media(entry, request) return json_response(get_entry_serializable(entry, request.urlgen)) -- cgit v1.2.3 From c7b3d070b65a84e3bfa9d8e3e6f52aac6552910f Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Tue, 15 Jan 2013 15:03:00 +0100 Subject: Don't pass request into run_process_media People(tm) want to start run_process_media from the CLI and might not have a request object handy. So pass in the feed_url into run_process_media rather than the request object and allow the feed url to be empty (resulting in no PuSH notification at all then). Signed-off-by: Sebastian Spaeth --- mediagoblin/plugins/api/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 7383e20d..2055a663 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -86,7 +86,10 @@ def post_entry(request): # # (... don't change entry after this point to avoid race # conditions with changes to the document via processing code) - run_process_media(entry, request) + feed_url = request.urlgen( + 'mediagoblin.user_pages.atom_feed', + qualified=True, user=request.user.username) + run_process_media(entry, feed_url) return json_response(get_entry_serializable(entry, request.urlgen)) -- cgit v1.2.3 From 1c2d01ae3ba421536d5775e5992393019714b856 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Fri, 25 Jan 2013 10:39:20 -0600 Subject: Very start of plugin hooks and openstreetmap pluginification - Added start of template hook code to pluginapi.py - Started to break openstreetmap into plugin; moved templates - Added plugin hooks in media and image media templates ... almost certainly, none of this works yet. :) --- .../mediagoblin/plugins/geolocation/map.html | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html new file mode 100644 index 00000000..b48678bb --- /dev/null +++ b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html @@ -0,0 +1,60 @@ +{# +# 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 . +#} + +{% block geolocation_map %} + {% if app_config['geolocation_map_visible'] + and media.media_data.gps_latitude is defined + and media.media_data.gps_latitude + and media.media_data.gps_longitude is defined + and media.media_data.gps_longitude %} +

{% trans %}Location{% endtrans %}

+
+ {%- set lon = media.media_data.gps_longitude %} + {%- set lat = media.media_data.gps_latitude %} + {%- set osm_url = "http://openstreetmap.org/?mlat={lat}&mlon={lon}".format(lat=lat, lon=lon) %} +
+ + +
+ + +

+ + {% trans -%} + View on OpenStreetMap + {%- endtrans %} + +

+
+ {% endif %} +{% endblock %} -- cgit v1.2.3 From a3f811a6e8589fc4b47c9f3036ac1cf0c8b0e200 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Mon, 28 Jan 2013 11:58:38 -0600 Subject: Geolocation stuff, including including templates seems to be working-ish - I'm having trouble seeing if the geolocation stuff actually works, but plugins are included - including a list of template hooks works, however the macro to include them does not, so it's kinda verbose --- mediagoblin/plugins/geolocation/__init__.py | 35 ++++++++++++++++++++++ .../plugins/geolocation/map_js_head.html | 25 ++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 mediagoblin/plugins/geolocation/__init__.py create mode 100644 mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map_js_head.html (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/geolocation/__init__.py b/mediagoblin/plugins/geolocation/__init__.py new file mode 100644 index 00000000..c55e1e6a --- /dev/null +++ b/mediagoblin/plugins/geolocation/__init__.py @@ -0,0 +1,35 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from mediagoblin.tools import pluginapi +import os + +PLUGIN_DIR = os.path.dirname(__file__) + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.geolocation') + + # Register the template path. + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + pluginapi.register_template_hooks( + {"image_sideinfo": "mediagoblin/plugins/geolocation/map.html", + "image_extrahead": "mediagoblin/plugins/geolocation/map_js_head.html"}) + + +hooks = { + 'setup': setup_plugin + } diff --git a/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map_js_head.html b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map_js_head.html new file mode 100644 index 00000000..aca0f730 --- /dev/null +++ b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map_js_head.html @@ -0,0 +1,25 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +#} + + + + + -- cgit v1.2.3 From c9abf931cb34a26642049ad34593ad1a16ebfb6e Mon Sep 17 00:00:00 2001 From: Elrond Date: Sat, 2 Feb 2013 20:40:19 +0100 Subject: issue 615: config geolocation_map_visible gone. The template in the geolocation plugin still used the old config option. Just remove that. To enable it, you enable the plugin. No need for extra config. Tested by manwesulimo2004 (via IRC). --- .../geolocation/templates/mediagoblin/plugins/geolocation/map.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html index b48678bb..70f837ff 100644 --- a/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html +++ b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html @@ -17,8 +17,7 @@ #} {% block geolocation_map %} - {% if app_config['geolocation_map_visible'] - and media.media_data.gps_latitude is defined + {% if media.media_data.gps_latitude is defined and media.media_data.gps_latitude and media.media_data.gps_longitude is defined and media.media_data.gps_longitude %} -- cgit v1.2.3 From 9b2cd962af78ce8fbe2bce88d7b9d3a9d01e4aa9 Mon Sep 17 00:00:00 2001 From: Runar Petursson Date: Fri, 15 Feb 2013 10:17:24 +0800 Subject: plugins/api: fix for cross origin requests The response headers were never getting set because of a bug in the 7c552c0 commit. This expands the loop into a more readable form and results in the headers getting set. --- mediagoblin/plugins/api/tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index e5878258..92411f4b 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -69,7 +69,8 @@ def json_response(serializable, _disable_cors=False, *args, **kw): 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} - (response.headers.set(key, value) for key, value in cors_headers) + for key, value in cors_headers.iteritems(): + response.headers.set(key, value) return response -- cgit v1.2.3 From cac17c156b85b275b76384550261a13b697eb5d6 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Sun, 24 Feb 2013 12:42:17 -0600 Subject: Renaming "extrahead" template hooks to "head". As Elrond points out, the "extra" is implied by it being a hook! This commit sponsored by Andrew Fustini. Thanks, Drew! --- mediagoblin/plugins/geolocation/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/geolocation/__init__.py b/mediagoblin/plugins/geolocation/__init__.py index c55e1e6a..5d14590e 100644 --- a/mediagoblin/plugins/geolocation/__init__.py +++ b/mediagoblin/plugins/geolocation/__init__.py @@ -27,7 +27,7 @@ def setup_plugin(): pluginapi.register_template_hooks( {"image_sideinfo": "mediagoblin/plugins/geolocation/map.html", - "image_extrahead": "mediagoblin/plugins/geolocation/map_js_head.html"}) + "image_head": "mediagoblin/plugins/geolocation/map_js_head.html"}) hooks = { -- cgit v1.2.3 From 77c85224b139a123e7895588af51b6cd4ecaa2b9 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 2 Mar 2013 23:40:24 +0100 Subject: Fixed hidden fields in oauth client authorization Removed the translation marking and passed in empty strings to avoid WTForms automagically creating the labels from the field names (i.e. client_id => 'Client Id'). --- mediagoblin/plugins/oauth/forms.py | 7 +++---- mediagoblin/plugins/oauth/templates/oauth/authorize.html | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/forms.py b/mediagoblin/plugins/oauth/forms.py index 2a956dad..d0a4e9b8 100644 --- a/mediagoblin/plugins/oauth/forms.py +++ b/mediagoblin/plugins/oauth/forms.py @@ -23,10 +23,9 @@ from mediagoblin.tools.translate import fake_ugettext_passthrough as _ class AuthorizationForm(wtforms.Form): - client_id = wtforms.HiddenField(_(u'Client ID'), - [wtforms.validators.Required()]) - next = wtforms.HiddenField(_(u'Next URL'), - [wtforms.validators.Required()]) + client_id = wtforms.HiddenField(u'', + validators=[wtforms.validators.Required()]) + next = wtforms.HiddenField(u'', validators=[wtforms.validators.Required()]) allow = wtforms.SubmitField(_(u'Allow')) deny = wtforms.SubmitField(_(u'Deny')) diff --git a/mediagoblin/plugins/oauth/templates/oauth/authorize.html b/mediagoblin/plugins/oauth/templates/oauth/authorize.html index 647fa41f..8a00c925 100644 --- a/mediagoblin/plugins/oauth/templates/oauth/authorize.html +++ b/mediagoblin/plugins/oauth/templates/oauth/authorize.html @@ -16,7 +16,7 @@ # along with this program. If not, see . -#} {% extends "mediagoblin/base.html" %} -{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} +{% import "mediagoblin/utils/wtforms.html" as wtforms_util %} {% block mediagoblin_content %}
Date: Fri, 22 Feb 2013 23:42:29 +0100 Subject: Added raven plugin --- mediagoblin/plugins/raven/README.rst | 16 +++++++++++ mediagoblin/plugins/raven/__init__.py | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 mediagoblin/plugins/raven/README.rst create mode 100644 mediagoblin/plugins/raven/__init__.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/raven/README.rst b/mediagoblin/plugins/raven/README.rst new file mode 100644 index 00000000..06510a96 --- /dev/null +++ b/mediagoblin/plugins/raven/README.rst @@ -0,0 +1,16 @@ +============== + raven plugin +============== + +.. warning:: + The raven plugin only sets up raven for celery. To enable raven for paster, + see the deployment docs section on setting up exception monitoring. + + +Set up the raven plugin +======================= + +1. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section:: + + [[mediagoblin.plugins.raven]] + sentry_dsn = diff --git a/mediagoblin/plugins/raven/__init__.py b/mediagoblin/plugins/raven/__init__.py new file mode 100644 index 00000000..d6308d3b --- /dev/null +++ b/mediagoblin/plugins/raven/__init__.py @@ -0,0 +1,52 @@ +# 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 . + +import os +import logging + +from mediagoblin.tools import pluginapi +from raven import Client +from raven.contrib.celery import register_signal + +_log = logging.getLogger(__name__) + +PLUGIN_DIR = os.path.dirname(__file__) + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.oauth') + + _log.info('Setting up raven for celery...') + + sentry_dsn = config.get('sentry_dsn') + + if sentry_dsn: + _log.info('Setting up raven from plugin config: {0}'.format( + sentry_dsn)) + client = Client(sentry_dsn)) + elif os.environ.get('SENTRY_DSN'): + _log.info('Setting up raven from SENTRY_DSN environment variable: {0}'\ + .format(os.environ.get('SENTRY_DSN'))) + client = Client() # Implicitly looks for SENTRY_DSN + + if client: + register_signal(client) + else: + _log.error('Could not set up client, missing sentry DSN') + +hooks = { + 'setup': setup_plugin, + } -- cgit v1.2.3 From b9c5c97238e59791828d1689ac4f942861883eba Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Fri, 22 Feb 2013 23:44:17 +0100 Subject: Refactored raven plugin --- mediagoblin/plugins/raven/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/raven/__init__.py b/mediagoblin/plugins/raven/__init__.py index d6308d3b..624a615e 100644 --- a/mediagoblin/plugins/raven/__init__.py +++ b/mediagoblin/plugins/raven/__init__.py @@ -41,11 +41,12 @@ def setup_plugin(): _log.info('Setting up raven from SENTRY_DSN environment variable: {0}'\ .format(os.environ.get('SENTRY_DSN'))) client = Client() # Implicitly looks for SENTRY_DSN - - if client: - register_signal(client) else: _log.error('Could not set up client, missing sentry DSN') + return + + register_signal(client) + hooks = { 'setup': setup_plugin, -- cgit v1.2.3 From 0c1ae3518e4d9442069ee8128e61ccda972c1401 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Fri, 22 Feb 2013 23:46:36 +0100 Subject: Don't look for the oauth config in raven plugin --- mediagoblin/plugins/raven/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/raven/__init__.py b/mediagoblin/plugins/raven/__init__.py index 624a615e..f4546f85 100644 --- a/mediagoblin/plugins/raven/__init__.py +++ b/mediagoblin/plugins/raven/__init__.py @@ -27,7 +27,7 @@ PLUGIN_DIR = os.path.dirname(__file__) def setup_plugin(): - config = pluginapi.get_config('mediagoblin.plugins.oauth') + config = pluginapi.get_config('mediagoblin.plugins.raven') _log.info('Setting up raven for celery...') -- cgit v1.2.3 From dee408291a245dc64cdaf73dcb8c4a37833e7ba1 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Fri, 22 Feb 2013 23:47:55 +0100 Subject: Removed PLUGIN_DIR from raven --- mediagoblin/plugins/raven/__init__.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/raven/__init__.py b/mediagoblin/plugins/raven/__init__.py index f4546f85..39eb41d3 100644 --- a/mediagoblin/plugins/raven/__init__.py +++ b/mediagoblin/plugins/raven/__init__.py @@ -23,8 +23,6 @@ from raven.contrib.celery import register_signal _log = logging.getLogger(__name__) -PLUGIN_DIR = os.path.dirname(__file__) - def setup_plugin(): config = pluginapi.get_config('mediagoblin.plugins.raven') -- cgit v1.2.3 From 32174bedc1e6d9a9842330b986c20b8e9dc3fd67 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 23 Feb 2013 00:02:39 +0100 Subject: Removed stray ) --- mediagoblin/plugins/raven/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/raven/__init__.py b/mediagoblin/plugins/raven/__init__.py index 39eb41d3..a0e83c0a 100644 --- a/mediagoblin/plugins/raven/__init__.py +++ b/mediagoblin/plugins/raven/__init__.py @@ -34,7 +34,7 @@ def setup_plugin(): if sentry_dsn: _log.info('Setting up raven from plugin config: {0}'.format( sentry_dsn)) - client = Client(sentry_dsn)) + client = Client(sentry_dsn) elif os.environ.get('SENTRY_DSN'): _log.info('Setting up raven from SENTRY_DSN environment variable: {0}'\ .format(os.environ.get('SENTRY_DSN'))) -- cgit v1.2.3 From 40ef3f5e05f5db071566a5caf55a7b1264ae083d Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sun, 24 Feb 2013 00:23:05 +0100 Subject: plugin/raven: Fix paster's celery config issue Check for CELERY_CONFIG_MODULE before we import raven.contrib.celery. It seems that the import otherwise sets up the celery client before we get to pass it our mediagoblin-specific settings. --- mediagoblin/plugins/raven/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/raven/__init__.py b/mediagoblin/plugins/raven/__init__.py index a0e83c0a..16b3b94b 100644 --- a/mediagoblin/plugins/raven/__init__.py +++ b/mediagoblin/plugins/raven/__init__.py @@ -18,13 +18,17 @@ import os import logging from mediagoblin.tools import pluginapi -from raven import Client -from raven.contrib.celery import register_signal _log = logging.getLogger(__name__) def setup_plugin(): + if not os.environ.get('CELERY_CONFIG_MODULE'): + # Exit early if we're (seemingly) not called from the celery process + return + + from raven import Client + from raven.contrib.celery import register_signal config = pluginapi.get_config('mediagoblin.plugins.raven') _log.info('Setting up raven for celery...') -- cgit v1.2.3 From f3f530286ff576a3120e29f734aff0b7b7fe882c Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sun, 3 Mar 2013 02:32:03 +0100 Subject: Updated raven plugin - Added wrap_wsgi, celery_setup, celery_logging_setup hooks - Updated raven plugin docs - Updated production considerations docs - Added raven logging setup --- mediagoblin/plugins/raven/README.rst | 7 ++--- mediagoblin/plugins/raven/__init__.py | 59 ++++++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 15 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/raven/README.rst b/mediagoblin/plugins/raven/README.rst index 06510a96..de5fd20d 100644 --- a/mediagoblin/plugins/raven/README.rst +++ b/mediagoblin/plugins/raven/README.rst @@ -2,10 +2,7 @@ raven plugin ============== -.. warning:: - The raven plugin only sets up raven for celery. To enable raven for paster, - see the deployment docs section on setting up exception monitoring. - +.. _raven-setup: Set up the raven plugin ======================= @@ -14,3 +11,5 @@ Set up the raven plugin [[mediagoblin.plugins.raven]] sentry_dsn = + # Logging is very high-volume, set to 0 if you want to turn off logging + setup_logging = 1 diff --git a/mediagoblin/plugins/raven/__init__.py b/mediagoblin/plugins/raven/__init__.py index 16b3b94b..8cfaed0a 100644 --- a/mediagoblin/plugins/raven/__init__.py +++ b/mediagoblin/plugins/raven/__init__.py @@ -22,19 +22,14 @@ from mediagoblin.tools import pluginapi _log = logging.getLogger(__name__) -def setup_plugin(): - if not os.environ.get('CELERY_CONFIG_MODULE'): - # Exit early if we're (seemingly) not called from the celery process - return - +def get_client(): from raven import Client - from raven.contrib.celery import register_signal config = pluginapi.get_config('mediagoblin.plugins.raven') - _log.info('Setting up raven for celery...') - sentry_dsn = config.get('sentry_dsn') + client = None + if sentry_dsn: _log.info('Setting up raven from plugin config: {0}'.format( sentry_dsn)) @@ -43,13 +38,55 @@ def setup_plugin(): _log.info('Setting up raven from SENTRY_DSN environment variable: {0}'\ .format(os.environ.get('SENTRY_DSN'))) client = Client() # Implicitly looks for SENTRY_DSN - else: + + if not client: _log.error('Could not set up client, missing sentry DSN') - return + return None + + return client + + +def setup_celery(): + from raven.contrib.celery import register_signal + + client = get_client() register_signal(client) +def setup_logging(): + config = pluginapi.get_config('mediagoblin.plugins.raven') + + conf_setup_logging = False + if config.get('setup_logging'): + conf_setup_logging = bool(int(config.get('setup_logging'))) + + if not conf_setup_logging: + return + + from raven.handlers.logging import SentryHandler + from raven.conf import setup_logging + + client = get_client() + + _log.info('Setting up raven logging handler') + + setup_logging(SentryHandler(client)) + + +def wrap_wsgi(app): + from raven.middleware import Sentry + + client = get_client() + + _log.info('Attaching raven middleware...') + + return Sentry(app, client) + + hooks = { - 'setup': setup_plugin, + 'setup': setup_logging, + 'wrap_wsgi': wrap_wsgi, + 'celery_logging_setup': setup_logging, + 'celery_setup': setup_celery, } -- cgit v1.2.3 From 81f73707a63fec1fc46f2cdd49c983e5afc053c8 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Fri, 8 Mar 2013 14:38:55 -0600 Subject: Providing warning to users about instability of OAuth/API --- mediagoblin/plugins/oauth/README.rst | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/README.rst b/mediagoblin/plugins/oauth/README.rst index 405a67e2..753b180f 100644 --- a/mediagoblin/plugins/oauth/README.rst +++ b/mediagoblin/plugins/oauth/README.rst @@ -7,6 +7,10 @@ Development has been entirely focused on Making It Work(TM). Use this plugin with caution. + Additionally, this and the API may break... consider it pre-alpha. + There's also a known issue that the OAuth client doesn't do + refresh tokens so this might result in issues for users. + The OAuth plugin enables third party web applications to authenticate as one or more GNU MediaGoblin users in a safe way in order retrieve, create and update content stored on the GNU MediaGoblin instance. -- cgit v1.2.3 From 427beb08afa5fee6a7537f3418e95cf93f440571 Mon Sep 17 00:00:00 2001 From: Elrond Date: Wed, 13 Mar 2013 14:51:00 +0100 Subject: Starting a piwigo api plugin. This one just puts up the basic endpoint, some infrastructure and a fake login method. Lots more needed. --- mediagoblin/plugins/piwigo/__init__.py | 37 ++++++++++++++++ mediagoblin/plugins/piwigo/views.py | 79 ++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 mediagoblin/plugins/piwigo/__init__.py create mode 100644 mediagoblin/plugins/piwigo/views.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/__init__.py b/mediagoblin/plugins/piwigo/__init__.py new file mode 100644 index 00000000..73326e9e --- /dev/null +++ b/mediagoblin/plugins/piwigo/__init__.py @@ -0,0 +1,37 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import logging + +from mediagoblin.tools import pluginapi + +_log = logging.getLogger(__name__) + + +def setup_plugin(): + _log.info('Setting up piwigo...') + + routes = [ + ('mediagoblin.plugins.piwigo.wsphp', + '/api/piwigo/ws.php', + 'mediagoblin.plugins.piwigo.views:ws_php'), + ] + + pluginapi.register_routes(routes) + +hooks = { + 'setup': setup_plugin +} diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py new file mode 100644 index 00000000..39c332c7 --- /dev/null +++ b/mediagoblin/plugins/piwigo/views.py @@ -0,0 +1,79 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import logging + +from werkzeug.exceptions import MethodNotAllowed + +from mediagoblin.meddleware.csrf import csrf_exempt +from mediagoblin.tools.response import render_404 + + +_log = logging.getLogger(__name__) + + +class CmdTable(object): + _cmd_table = {} + + def __init__(self, cmd_name, only_post=False): + assert not cmd_name in self._cmd_table + self.cmd_name = cmd_name + self.only_post = only_post + + def __call__(self, to_be_wrapped): + assert not self.cmd_name in self._cmd_table + self._cmd_table[self.cmd_name] = (to_be_wrapped, self.only_post) + return to_be_wrapped + + @classmethod + def find_func(cls, request): + if request.method == "GET": + cmd_name = request.args.get("method") + else: + cmd_name = request.form.get("method") + entry = cls._cmd_table.get(cmd_name) + if not entry: + return entry + func, only_post = entry + if only_post and request.method != "POST": + _log.warn("Method %s only allowed for POST", cmd_name) + raise MethodNotAllowed() + return func + + +@CmdTable("pwg.session.login", True) +def pwg_login(request): + username = request.form.get("username") + password = request.form.get("password") + _log.info("Login for %r/%r...", username, password) + return render_404(request) + + +@csrf_exempt +def ws_php(request): + if request.method not in ("GET", "POST"): + _log.error("Method %r not supported", request.method) + raise MethodNotAllowed() + + func = CmdTable.find_func(request) + if not func: + _log.warn("wsphp: Unhandled %s %r %r", request.method, + request.args, request.form) + return render_404(request) + + result = func(request) + + return result -- cgit v1.2.3 From bd3bc0446ce58db7a9050c4036c11ff624023968 Mon Sep 17 00:00:00 2001 From: Elrond Date: Tue, 19 Mar 2013 10:18:29 +0100 Subject: piwigo: start xml response encoding, more (fake) methods. --- mediagoblin/plugins/piwigo/views.py | 57 +++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 39c332c7..bb9ad87c 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -16,15 +16,47 @@ import logging +import lxml.etree as ET from werkzeug.exceptions import MethodNotAllowed from mediagoblin.meddleware.csrf import csrf_exempt -from mediagoblin.tools.response import render_404 +from mediagoblin.tools.response import Response, render_404 _log = logging.getLogger(__name__) +def _fill_element_dict(el, data, as_attr=()): + for k,v in data.iteritems(): + if k in as_attr: + el.set(k, v) + else: + n = ET.SubElement(el, k) + _fill_element(n, v) + + +def _fill_element(el, data): + if isinstance(data, bool): + if data: + el.text = "1" + else: + el.text = "0" + elif isinstance(data, basestring): + el.text = data + elif isinstance(data, dict): + _fill_element_dict(el, data) + else: + _log.warn("Can't convert to xml: %r", data) + + +def as_xml(result): + r = ET.Element("rsp") + r.set("stat", "ok") + _fill_element(r, result) + return Response(ET.tostring(r, encoding="utf-8", xml_declaration=True), + content_type='text/xml') + + class CmdTable(object): _cmd_table = {} @@ -54,12 +86,33 @@ class CmdTable(object): return func +@CmdTable("gmg.test") +def gmg_test(request): + _log.info("Test...") + r = {"abc": "def", "subdict": {"name": "Foo", "True": True}} + return as_xml(r) + + @CmdTable("pwg.session.login", True) def pwg_login(request): username = request.form.get("username") password = request.form.get("password") _log.info("Login for %r/%r...", username, password) - return render_404(request) + _log.warn("login: %s %r %r", request.method, + request.args, request.form) + return as_xml(True) + + +@CmdTable("pwg.session.logout") +def pwg_logout(request): + _log.info("Logout") + return as_xml(True) + + +@CmdTable("pwg.getVersion") +def pwg_getversion(request): + _log.info("getversion") + return as_xml("piwigo 2.5.0") @csrf_exempt -- cgit v1.2.3 From e4e5948c5837f744823b6a44e8f4eee2dd08220b Mon Sep 17 00:00:00 2001 From: Elrond Date: Tue, 19 Mar 2013 11:45:22 +0100 Subject: Start at pwg.categories.getList and improve xml output. - The xml formatting is now in the main function. - Add PwgNamedArray to have named lists in xml output. - Remove gmg.test method --- mediagoblin/plugins/piwigo/views.py | 64 +++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 13 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index bb9ad87c..7c5bff3b 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -18,6 +18,7 @@ import logging import lxml.etree as ET from werkzeug.exceptions import MethodNotAllowed +from werkzeug.wrappers import BaseResponse from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.tools.response import Response, render_404 @@ -26,9 +27,26 @@ from mediagoblin.tools.response import Response, render_404 _log = logging.getLogger(__name__) +class PwgNamedArray(list): + def __init__(self, l, item_name, as_attrib=()): + self.item_name = item_name + self.as_attrib = as_attrib + list.__init__(self, l) + + def fill_element_xml(self, el): + for it in self: + n = ET.SubElement(el, self.item_name) + if isinstance(it, dict): + _fill_element_dict(n, it, self.as_attrib) + else: + _fill_element(n, it) + + def _fill_element_dict(el, data, as_attr=()): for k,v in data.iteritems(): if k in as_attr: + if not isinstance(v, basestring): + v = str(v) el.set(k, v) else: n = ET.SubElement(el, k) @@ -43,18 +61,22 @@ def _fill_element(el, data): el.text = "0" elif isinstance(data, basestring): el.text = data + elif isinstance(data, int): + el.text = str(data) elif isinstance(data, dict): _fill_element_dict(el, data) + elif isinstance(data, PwgNamedArray): + data.fill_element_xml(el) else: _log.warn("Can't convert to xml: %r", data) -def as_xml(result): +def response_xml(result): r = ET.Element("rsp") r.set("stat", "ok") _fill_element(r, result) return Response(ET.tostring(r, encoding="utf-8", xml_declaration=True), - content_type='text/xml') + mimetype='text/xml') class CmdTable(object): @@ -86,13 +108,6 @@ class CmdTable(object): return func -@CmdTable("gmg.test") -def gmg_test(request): - _log.info("Test...") - r = {"abc": "def", "subdict": {"name": "Foo", "True": True}} - return as_xml(r) - - @CmdTable("pwg.session.login", True) def pwg_login(request): username = request.form.get("username") @@ -100,19 +115,39 @@ def pwg_login(request): _log.info("Login for %r/%r...", username, password) _log.warn("login: %s %r %r", request.method, request.args, request.form) - return as_xml(True) + return True @CmdTable("pwg.session.logout") def pwg_logout(request): _log.info("Logout") - return as_xml(True) + return True @CmdTable("pwg.getVersion") def pwg_getversion(request): _log.info("getversion") - return as_xml("piwigo 2.5.0") + return "piwigo 2.5.0" + + +@CmdTable("pwg.categories.getList") +def pwg_categories_getList(request): + catlist = ({'id': -29711},) + return { + 'categories': PwgNamedArray( + catlist, + 'category', + ( + 'id', + 'url', + 'nb_images', + 'total_nb_images', + 'nb_categories', + 'date_last', + 'max_date_last', + ) + ) + } @csrf_exempt @@ -129,4 +164,7 @@ def ws_php(request): result = func(request) - return result + if isinstance(result, BaseResponse): + return result + + return response_xml(result) -- cgit v1.2.3 From 4234fffafacd0313559103d82fe5abf6dc42844c Mon Sep 17 00:00:00 2001 From: Elrond Date: Tue, 19 Mar 2013 12:07:23 +0100 Subject: piwigo: Move tool functions into tools.py --- mediagoblin/plugins/piwigo/tools.py | 106 ++++++++++++++++++++++++++++++++++++ mediagoblin/plugins/piwigo/views.py | 85 +---------------------------- 2 files changed, 108 insertions(+), 83 deletions(-) create mode 100644 mediagoblin/plugins/piwigo/tools.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py new file mode 100644 index 00000000..d2f7da1e --- /dev/null +++ b/mediagoblin/plugins/piwigo/tools.py @@ -0,0 +1,106 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import logging + +import lxml.etree as ET +from werkzeug.exceptions import MethodNotAllowed + +from mediagoblin.tools.response import Response + + +_log = logging.getLogger(__name__) + + +class PwgNamedArray(list): + def __init__(self, l, item_name, as_attrib=()): + self.item_name = item_name + self.as_attrib = as_attrib + list.__init__(self, l) + + def fill_element_xml(self, el): + for it in self: + n = ET.SubElement(el, self.item_name) + if isinstance(it, dict): + _fill_element_dict(n, it, self.as_attrib) + else: + _fill_element(n, it) + + +def _fill_element_dict(el, data, as_attr=()): + for k,v in data.iteritems(): + if k in as_attr: + if not isinstance(v, basestring): + v = str(v) + el.set(k, v) + else: + n = ET.SubElement(el, k) + _fill_element(n, v) + + +def _fill_element(el, data): + if isinstance(data, bool): + if data: + el.text = "1" + else: + el.text = "0" + elif isinstance(data, basestring): + el.text = data + elif isinstance(data, int): + el.text = str(data) + elif isinstance(data, dict): + _fill_element_dict(el, data) + elif isinstance(data, PwgNamedArray): + data.fill_element_xml(el) + else: + _log.warn("Can't convert to xml: %r", data) + + +def response_xml(result): + r = ET.Element("rsp") + r.set("stat", "ok") + _fill_element(r, result) + return Response(ET.tostring(r, encoding="utf-8", xml_declaration=True), + mimetype='text/xml') + + +class CmdTable(object): + _cmd_table = {} + + def __init__(self, cmd_name, only_post=False): + assert not cmd_name in self._cmd_table + self.cmd_name = cmd_name + self.only_post = only_post + + def __call__(self, to_be_wrapped): + assert not self.cmd_name in self._cmd_table + self._cmd_table[self.cmd_name] = (to_be_wrapped, self.only_post) + return to_be_wrapped + + @classmethod + def find_func(cls, request): + if request.method == "GET": + cmd_name = request.args.get("method") + else: + cmd_name = request.form.get("method") + entry = cls._cmd_table.get(cmd_name) + if not entry: + return entry + func, only_post = entry + if only_post and request.method != "POST": + _log.warn("Method %s only allowed for POST", cmd_name) + raise MethodNotAllowed() + return func diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 7c5bff3b..2b6b7b0c 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -16,98 +16,17 @@ import logging -import lxml.etree as ET from werkzeug.exceptions import MethodNotAllowed from werkzeug.wrappers import BaseResponse from mediagoblin.meddleware.csrf import csrf_exempt -from mediagoblin.tools.response import Response, render_404 +from mediagoblin.tools.response import render_404 +from .tools import CmdTable, PwgNamedArray, response_xml _log = logging.getLogger(__name__) -class PwgNamedArray(list): - def __init__(self, l, item_name, as_attrib=()): - self.item_name = item_name - self.as_attrib = as_attrib - list.__init__(self, l) - - def fill_element_xml(self, el): - for it in self: - n = ET.SubElement(el, self.item_name) - if isinstance(it, dict): - _fill_element_dict(n, it, self.as_attrib) - else: - _fill_element(n, it) - - -def _fill_element_dict(el, data, as_attr=()): - for k,v in data.iteritems(): - if k in as_attr: - if not isinstance(v, basestring): - v = str(v) - el.set(k, v) - else: - n = ET.SubElement(el, k) - _fill_element(n, v) - - -def _fill_element(el, data): - if isinstance(data, bool): - if data: - el.text = "1" - else: - el.text = "0" - elif isinstance(data, basestring): - el.text = data - elif isinstance(data, int): - el.text = str(data) - elif isinstance(data, dict): - _fill_element_dict(el, data) - elif isinstance(data, PwgNamedArray): - data.fill_element_xml(el) - else: - _log.warn("Can't convert to xml: %r", data) - - -def response_xml(result): - r = ET.Element("rsp") - r.set("stat", "ok") - _fill_element(r, result) - return Response(ET.tostring(r, encoding="utf-8", xml_declaration=True), - mimetype='text/xml') - - -class CmdTable(object): - _cmd_table = {} - - def __init__(self, cmd_name, only_post=False): - assert not cmd_name in self._cmd_table - self.cmd_name = cmd_name - self.only_post = only_post - - def __call__(self, to_be_wrapped): - assert not self.cmd_name in self._cmd_table - self._cmd_table[self.cmd_name] = (to_be_wrapped, self.only_post) - return to_be_wrapped - - @classmethod - def find_func(cls, request): - if request.method == "GET": - cmd_name = request.args.get("method") - else: - cmd_name = request.form.get("method") - entry = cls._cmd_table.get(cmd_name) - if not entry: - return entry - func, only_post = entry - if only_post and request.method != "POST": - _log.warn("Method %s only allowed for POST", cmd_name) - raise MethodNotAllowed() - return func - - @CmdTable("pwg.session.login", True) def pwg_login(request): username = request.form.get("username") -- cgit v1.2.3 From 1330abf722ff894e1a7f492718519a8cb5290a0b Mon Sep 17 00:00:00 2001 From: Elrond Date: Tue, 19 Mar 2013 15:29:01 +0100 Subject: Add warning README.rst and fix pep8. --- mediagoblin/plugins/piwigo/README.rst | 23 +++++++++++++++++++++++ mediagoblin/plugins/piwigo/tools.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 mediagoblin/plugins/piwigo/README.rst (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/README.rst b/mediagoblin/plugins/piwigo/README.rst new file mode 100644 index 00000000..0c71ffbc --- /dev/null +++ b/mediagoblin/plugins/piwigo/README.rst @@ -0,0 +1,23 @@ +=================== + piwigo api plugin +=================== + +.. danger:: + This plugin does not work. + It might make your instance unstable or even insecure. + So do not use it, unless you want to help to develop it. + +.. warning:: + You should not depend on this plugin in any way for now. + It might even go away without any notice. + +Okay, so if you still want to test this plugin, +add the following to your mediagoblin_local.ini: + +.. code-block:: ini + + [plugins] + [[mediagoblin.plugins.piwigo]] + +Then try to connect using some piwigo client. +There should be some logging, that might help. diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py index d2f7da1e..e53d94b9 100644 --- a/mediagoblin/plugins/piwigo/tools.py +++ b/mediagoblin/plugins/piwigo/tools.py @@ -41,7 +41,7 @@ class PwgNamedArray(list): def _fill_element_dict(el, data, as_attr=()): - for k,v in data.iteritems(): + for k, v in data.iteritems(): if k in as_attr: if not isinstance(v, basestring): v = str(v) -- cgit v1.2.3 From dc7c26f3559be00e272c5c880f87c7550d80baae Mon Sep 17 00:00:00 2001 From: Elrond Date: Thu, 21 Mar 2013 09:01:48 +0100 Subject: piwigo: Sent a fake cookie. shotwell needs a pwg_id cookie to continue. And really, it's the only cookie it supports, so in the long run, we need to send a proper session cookie as pwg_id. --- mediagoblin/plugins/piwigo/views.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 2b6b7b0c..d0629b73 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -69,6 +69,19 @@ def pwg_categories_getList(request): } +def possibly_add_cookie(request, response): + # TODO: We should only add a *real* cookie, if + # authenticated. And if there is no cookie already. + if True: + response.set_cookie( + 'pwg_id', + "some_fake_for_now", + path=request.environ['SCRIPT_NAME'], + domain=mg_globals.app_config.get('csrf_cookie_domain'), + secure=(request.scheme.lower() == 'https'), + httponly=True) + + @csrf_exempt def ws_php(request): if request.method not in ("GET", "POST"): @@ -86,4 +99,8 @@ def ws_php(request): if isinstance(result, BaseResponse): return result - return response_xml(result) + response = response_xml(result) + + possibly_add_cookie(request, response) + + return response -- cgit v1.2.3 From cf0816c182c2acab30b1ee9e191aff091ac41d13 Mon Sep 17 00:00:00 2001 From: Elrond Date: Thu, 21 Mar 2013 09:07:59 +0100 Subject: piwigo: Add session.getStatus, improve categories.getList - pwg.session.getStatus returns the current user as "fake_user". When we have a session, we'll return something better. - pwg.categories.getList add a name and the parent id for its one and only "collection". - Improve logging a bit. --- mediagoblin/plugins/piwigo/tools.py | 1 + mediagoblin/plugins/piwigo/views.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py index e53d94b9..85d77310 100644 --- a/mediagoblin/plugins/piwigo/tools.py +++ b/mediagoblin/plugins/piwigo/tools.py @@ -99,6 +99,7 @@ class CmdTable(object): entry = cls._cmd_table.get(cmd_name) if not entry: return entry + _log.debug("Found method %s", cmd_name) func, only_post = entry if only_post and request.method != "POST": _log.warn("Method %s only allowed for POST", cmd_name) diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index d0629b73..6198ec6e 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -32,8 +32,6 @@ def pwg_login(request): username = request.form.get("username") password = request.form.get("password") _log.info("Login for %r/%r...", username, password) - _log.warn("login: %s %r %r", request.method, - request.args, request.form) return True @@ -45,13 +43,19 @@ def pwg_logout(request): @CmdTable("pwg.getVersion") def pwg_getversion(request): - _log.info("getversion") - return "piwigo 2.5.0" + return "piwigo 2.5.0 (MediaGoblin)" + + +@CmdTable("pwg.session.getStatus") +def pwg_session_getStatus(request): + return {'username': "fake_user"} @CmdTable("pwg.categories.getList") def pwg_categories_getList(request): - catlist = ({'id': -29711},) + catlist = ({'id': -29711, + 'uppercats': "-29711", + 'name': "All my images"},) return { 'categories': PwgNamedArray( catlist, -- cgit v1.2.3 From 398d384137bce928592dd63c210126ab989ee69c Mon Sep 17 00:00:00 2001 From: Elrond Date: Thu, 21 Mar 2013 09:16:05 +0100 Subject: piwigo start at pwg.images.addChunk. This function receives part of an upload. Does most parameter validation, but does not safe the data anywhere for now. Also fake pwg.images.exist --- mediagoblin/plugins/piwigo/views.py | 44 ++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 6198ec6e..e9ce6206 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -15,10 +15,12 @@ # along with this program. If not, see . import logging +import re -from werkzeug.exceptions import MethodNotAllowed +from werkzeug.exceptions import MethodNotAllowed, BadRequest from werkzeug.wrappers import BaseResponse +from mediagoblin import mg_globals from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.tools.response import render_404 from .tools import CmdTable, PwgNamedArray, response_xml @@ -73,6 +75,46 @@ def pwg_categories_getList(request): } +@CmdTable("pwg.images.exist") +def pwg_images_exist(request): + return {} + + +md5sum_matcher = re.compile(r"^[0-9a-fA-F]{32}$") + +def fetch_md5(request, parm_name, optional_parm=False): + val = request.form.get(parm_name) + if (val is None) and (not optional_parm): + _log.error("Parameter %s missing", parm_name) + raise BadRequest("Parameter %s missing" % parm_name) + if not md5sum_matcher.match(val): + _log.error("Parameter %s=%r has no valid md5 value", parm_name, val) + raise BadRequest("Parameter %s is not md5" % parm_name) + return val + + +@CmdTable("pwg.images.addChunk", True) +def pwg_images_addChunk(request): + o_sum = fetch_md5(request, 'original_sum') + typ = request.form.get('type') + pos = request.form.get('position') + data = request.form.get('data') + + # Validate params: + pos = int(pos) + if not typ in ("file", "thumb"): + _log.error("type %r not allowed for now", typ) + return False + + _log.info("addChunk for %r, type %r, position %d, len: %d", + o_sum, typ, pos, len(data)) + if typ == "thumb": + _log.info("addChunk: Ignoring thumb, because we create our own") + return True + + return True + + def possibly_add_cookie(request, response): # TODO: We should only add a *real* cookie, if # authenticated. And if there is no cookie already. -- cgit v1.2.3 From 79f87b975e47c27e24496c94a88656565f4f1e79 Mon Sep 17 00:00:00 2001 From: Elrond Date: Tue, 26 Mar 2013 19:19:32 +0100 Subject: piwigo: Start at pwg.images.addSimple. Without a session and a logged in user, this can't go much further. Misses check for the file upload field. Need refactored test tool for this. --- mediagoblin/plugins/piwigo/forms.py | 28 ++++++++++++++++++++++++++++ mediagoblin/plugins/piwigo/views.py | 15 +++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 mediagoblin/plugins/piwigo/forms.py (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/forms.py b/mediagoblin/plugins/piwigo/forms.py new file mode 100644 index 00000000..5bb12e62 --- /dev/null +++ b/mediagoblin/plugins/piwigo/forms.py @@ -0,0 +1,28 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import wtforms + + +class AddSimpleForm(wtforms.Form): + image = wtforms.FileField() + name = wtforms.TextField( + validators=[wtforms.validators.Length(min=0, max=500)]) + comment = wtforms.TextField() + # tags = wtforms.FieldList(wtforms.TextField()) + category = wtforms.IntegerField() + level = wtforms.IntegerField() diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index e9ce6206..8de7673c 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -24,6 +24,7 @@ from mediagoblin import mg_globals from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.tools.response import render_404 from .tools import CmdTable, PwgNamedArray, response_xml +from .forms import AddSimpleForm _log = logging.getLogger(__name__) @@ -80,6 +81,20 @@ def pwg_images_exist(request): return {} +@CmdTable("pwg.images.addSimple", True) +def pwg_images_addSimple(request): + form = AddSimpleForm(request.form) + if not form.validate(): + _log.error("addSimple: form failed") + raise BadRequest() + dump = [] + for f in form: + dump.append("%s=%r" % (f.name, f.data)) + _log.info("addimple: %r %s %r", request.form, " ".join(dump), request.files) + + return {'image_id': 123456, 'url': ''} + + md5sum_matcher = re.compile(r"^[0-9a-fA-F]{32}$") def fetch_md5(request, parm_name, optional_parm=False): -- cgit v1.2.3 From 9924cd0fb6f2a9d53a083ce68d1bbf872ee57d9b Mon Sep 17 00:00:00 2001 From: Elrond Date: Sun, 24 Mar 2013 16:09:40 +0100 Subject: piwigo: Fix pwg_getversion This one needs to return just "2.5.0 (Mediagoblin)" instead of "Piwigo 2...". --- mediagoblin/plugins/piwigo/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 8de7673c..3dee09cd 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -46,7 +46,7 @@ def pwg_logout(request): @CmdTable("pwg.getVersion") def pwg_getversion(request): - return "piwigo 2.5.0 (MediaGoblin)" + return "2.5.0 (MediaGoblin)" @CmdTable("pwg.session.getStatus") -- cgit v1.2.3 From 29f523e1db243e0b2d4726d93f2d64bc67f84e4d Mon Sep 17 00:00:00 2001 From: Hans Lo Date: Thu, 28 Mar 2013 00:08:18 -0400 Subject: Use WTForms data field in plugins/oauth/views.py --- mediagoblin/plugins/oauth/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py index c7b2a332..ea45c209 100644 --- a/mediagoblin/plugins/oauth/views.py +++ b/mediagoblin/plugins/oauth/views.py @@ -45,11 +45,11 @@ def register_client(request): if request.method == 'POST' and form.validate(): client = OAuthClient() - client.name = unicode(request.form['name']) - client.description = unicode(request.form['description']) - client.type = unicode(request.form['type']) + client.name = unicode(form.name.data) + client.description = unicode(form.description.data) + client.type = unicode(form.type.data) client.owner_id = request.user.id - client.redirect_uri = unicode(request.form['redirect_uri']) + client.redirect_uri = unicode(form.redirect_uri.data) client.generate_identifier() client.generate_secret() -- cgit v1.2.3 From 2ef2f46e73845dcd55666cad49c5a17908bf5b46 Mon Sep 17 00:00:00 2001 From: Elrond Date: Fri, 22 Mar 2013 15:45:21 +0100 Subject: Refactor file field checking. When uploading, the file field needs some checks, it seems. So refactor them into check_file_field and use around. --- mediagoblin/plugins/api/views.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 2055a663..fde76fe4 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -18,7 +18,6 @@ import json import logging from os.path import splitext -from werkzeug.datastructures import FileStorage from werkzeug.exceptions import BadRequest, Forbidden from werkzeug.wrappers import Response @@ -27,7 +26,8 @@ from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.media_types import sniff_media from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \ json_response -from mediagoblin.submit.lib import prepare_queue_task, run_process_media +from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ + run_process_media _log = logging.getLogger(__name__) @@ -45,9 +45,7 @@ def post_entry(request): _log.debug('Must POST against post_entry') raise BadRequest() - if not 'file' in request.files \ - or not isinstance(request.files['file'], FileStorage) \ - or not request.files['file'].stream: + if not check_file_field(request, 'file'): _log.debug('File field not found') raise BadRequest() -- cgit v1.2.3 From 6c6e9911f5556bcd1f287afa39d4cc6d14d9c6c1 Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Fri, 29 Mar 2013 08:09:26 -0500 Subject: Warning that raven plugin is somewhat experimental! --- mediagoblin/plugins/raven/README.rst | 2 ++ 1 file changed, 2 insertions(+) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/raven/README.rst b/mediagoblin/plugins/raven/README.rst index de5fd20d..4006060d 100644 --- a/mediagoblin/plugins/raven/README.rst +++ b/mediagoblin/plugins/raven/README.rst @@ -4,6 +4,8 @@ .. _raven-setup: +Warning: this plugin is somewhat experimental. + Set up the raven plugin ======================= -- cgit v1.2.3 From c121a7d3d0c48d4e3abf43c06047dde3e25616c3 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sun, 10 Mar 2013 22:52:07 +0100 Subject: OAuth: Support refresh tokens, etc Initially I was going to write a failing test for refresh tokens. Thus this fix includes an orphaned 'expect_failure' method in test utils. I ended up writing support for OAuth refresh tokens, as well as a lot of cleanup (hopefully) in the OAuth plugin code. **Rebase**: While waiting for this stuff to be merged, the testing framework changed, it comes with batteries included regarding fails. Removed legacy nosetest helper. Also added a lot of backref=backref([...], cascade='all, delete-orphan') --- mediagoblin/plugins/oauth/__init__.py | 2 +- mediagoblin/plugins/oauth/migrations.py | 34 ++++++++ mediagoblin/plugins/oauth/models.py | 87 ++++++++++++------ mediagoblin/plugins/oauth/tools.py | 73 +++++++++++++++- mediagoblin/plugins/oauth/views.py | 150 ++++++++++++++++++-------------- 5 files changed, 252 insertions(+), 94 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py index 4714d95d..5762379d 100644 --- a/mediagoblin/plugins/oauth/__init__.py +++ b/mediagoblin/plugins/oauth/__init__.py @@ -34,7 +34,7 @@ def setup_plugin(): _log.debug('OAuth config: {0}'.format(config)) routes = [ - ('mediagoblin.plugins.oauth.authorize', + ('mediagoblin.plugins.oauth.authorize', '/oauth/authorize', 'mediagoblin.plugins.oauth.views:authorize'), ('mediagoblin.plugins.oauth.authorize_client', diff --git a/mediagoblin/plugins/oauth/migrations.py b/mediagoblin/plugins/oauth/migrations.py index 6aa0d7cb..d7b89da3 100644 --- a/mediagoblin/plugins/oauth/migrations.py +++ b/mediagoblin/plugins/oauth/migrations.py @@ -102,6 +102,21 @@ class OAuthCode_v0(declarative_base()): client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False) +class OAuthRefreshToken_v0(declarative_base()): + __tablename__ = 'oauth__refresh_tokens' + + id = Column(Integer, primary_key=True) + created = Column(DateTime, nullable=False, + default=datetime.now) + + token = Column(Unicode, index=True) + + user_id = Column(Integer, ForeignKey(User.id), nullable=False) + + # XXX: Is it OK to use OAuthClient_v0.id in this way? + client_id = Column(Integer, ForeignKey(OAuthClient_v0.id), nullable=False) + + @RegisterMigration(1, MIGRATIONS) def remove_and_replace_token_and_code(db): metadata = MetaData(bind=db.bind) @@ -122,3 +137,22 @@ def remove_and_replace_token_and_code(db): OAuthCode_v0.__table__.create(db.bind) db.commit() + + +@RegisterMigration(2, MIGRATIONS) +def remove_refresh_token_field(db): + metadata = MetaData(bind=db.bind) + + token_table = Table('oauth__tokens', metadata, autoload=True, + autoload_with=db.bind) + + refresh_token = token_table.columns['refresh_token'] + + refresh_token.drop() + db.commit() + +@RegisterMigration(3, MIGRATIONS) +def create_refresh_token_table(db): + OAuthRefreshToken_v0.__table__.create(db.bind) + + db.commit() diff --git a/mediagoblin/plugins/oauth/models.py b/mediagoblin/plugins/oauth/models.py index 695dad31..439424d3 100644 --- a/mediagoblin/plugins/oauth/models.py +++ b/mediagoblin/plugins/oauth/models.py @@ -14,17 +14,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import uuid -import bcrypt from datetime import datetime, timedelta -from mediagoblin.db.base import Base -from mediagoblin.db.models import User from sqlalchemy import ( Column, Unicode, Integer, DateTime, ForeignKey, Enum) -from sqlalchemy.orm import relationship +from sqlalchemy.orm import relationship, backref +from mediagoblin.db.base import Base +from mediagoblin.db.models import User +from mediagoblin.plugins.oauth.tools import generate_identifier, \ + generate_secret, generate_token, generate_code, generate_refresh_token # Don't remove this, I *think* it applies sqlalchemy-migrate functionality onto # the models. @@ -41,11 +41,14 @@ class OAuthClient(Base): name = Column(Unicode) description = Column(Unicode) - identifier = Column(Unicode, unique=True, index=True) - secret = Column(Unicode, index=True) + identifier = Column(Unicode, unique=True, index=True, + default=generate_identifier) + secret = Column(Unicode, index=True, default=generate_secret) owner_id = Column(Integer, ForeignKey(User.id)) - owner = relationship(User, backref='registered_clients') + owner = relationship( + User, + backref=backref('registered_clients', cascade='all, delete-orphan')) redirect_uri = Column(Unicode) @@ -54,14 +57,8 @@ class OAuthClient(Base): u'public', name=u'oauth__client_type')) - def generate_identifier(self): - self.identifier = unicode(uuid.uuid4()) - - def generate_secret(self): - self.secret = unicode( - bcrypt.hashpw( - unicode(uuid.uuid4()), - bcrypt.gensalt())) + def update_secret(self): + self.secret = generate_secret() def __repr__(self): return '<{0} {1}:{2} ({3})>'.format( @@ -76,10 +73,15 @@ class OAuthUserClient(Base): id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey(User.id)) - user = relationship(User, backref='oauth_clients') + user = relationship( + User, + backref=backref('oauth_client_relations', + cascade='all, delete-orphan')) client_id = Column(Integer, ForeignKey(OAuthClient.id)) - client = relationship(OAuthClient, backref='users') + client = relationship( + OAuthClient, + backref=backref('oauth_user_relations', cascade='all, delete-orphan')) state = Column(Enum( u'approved', @@ -103,15 +105,18 @@ class OAuthToken(Base): default=datetime.now) expires = Column(DateTime, nullable=False, default=lambda: datetime.now() + timedelta(days=30)) - token = Column(Unicode, index=True) - refresh_token = Column(Unicode, index=True) + token = Column(Unicode, index=True, default=generate_token) user_id = Column(Integer, ForeignKey(User.id), nullable=False, index=True) - user = relationship(User) + user = relationship( + User, + backref=backref('oauth_tokens', cascade='all, delete-orphan')) client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False) - client = relationship(OAuthClient) + client = relationship( + OAuthClient, + backref=backref('oauth_tokens', cascade='all, delete-orphan')) def __repr__(self): return '<{0} #{1} expires {2} [{3}, {4}]>'.format( @@ -121,6 +126,34 @@ class OAuthToken(Base): self.user, self.client) +class OAuthRefreshToken(Base): + __tablename__ = 'oauth__refresh_tokens' + + id = Column(Integer, primary_key=True) + created = Column(DateTime, nullable=False, + default=datetime.now) + + token = Column(Unicode, index=True, + default=generate_refresh_token) + + user_id = Column(Integer, ForeignKey(User.id), nullable=False) + + user = relationship(User, backref=backref('oauth_refresh_tokens', + cascade='all, delete-orphan')) + + client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False) + client = relationship(OAuthClient, + backref=backref( + 'oauth_refresh_tokens', + cascade='all, delete-orphan')) + + def __repr__(self): + return '<{0} #{1} [{3}, {4}]>'.format( + self.__class__.__name__, + self.id, + self.user, + self.client) + class OAuthCode(Base): __tablename__ = 'oauth__codes' @@ -130,14 +163,17 @@ class OAuthCode(Base): default=datetime.now) expires = Column(DateTime, nullable=False, default=lambda: datetime.now() + timedelta(minutes=5)) - code = Column(Unicode, index=True) + code = Column(Unicode, index=True, default=generate_code) user_id = Column(Integer, ForeignKey(User.id), nullable=False, index=True) - user = relationship(User) + user = relationship(User, backref=backref('oauth_codes', + cascade='all, delete-orphan')) client_id = Column(Integer, ForeignKey(OAuthClient.id), nullable=False) - client = relationship(OAuthClient) + client = relationship(OAuthClient, backref=backref( + 'oauth_codes', + cascade='all, delete-orphan')) def __repr__(self): return '<{0} #{1} expires {2} [{3}, {4}]>'.format( @@ -150,6 +186,7 @@ class OAuthCode(Base): MODELS = [ OAuthToken, + OAuthRefreshToken, OAuthCode, OAuthClient, OAuthUserClient] diff --git a/mediagoblin/plugins/oauth/tools.py b/mediagoblin/plugins/oauth/tools.py index d21c8a5b..27ff32b4 100644 --- a/mediagoblin/plugins/oauth/tools.py +++ b/mediagoblin/plugins/oauth/tools.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # GNU MediaGoblin -- federated, autonomous media hosting # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. # @@ -14,13 +15,26 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import uuid + +from random import getrandbits + +from datetime import datetime + from functools import wraps -from mediagoblin.plugins.oauth.models import OAuthClient from mediagoblin.plugins.api.tools import json_response def require_client_auth(controller): + ''' + View decorator + + - Requires the presence of ``?client_id`` + ''' + # Avoid circular import + from mediagoblin.plugins.oauth.models import OAuthClient + @wraps(controller) def wrapper(request, *args, **kw): if not request.GET.get('client_id'): @@ -41,3 +55,60 @@ def require_client_auth(controller): return controller(request, client) return wrapper + + +def create_token(client, user): + ''' + Create an OAuthToken and an OAuthRefreshToken entry in the database + + Returns the data structure expected by the OAuth clients. + ''' + from mediagoblin.plugins.oauth.models import OAuthToken, OAuthRefreshToken + + token = OAuthToken() + token.user = user + token.client = client + token.save() + + refresh_token = OAuthRefreshToken() + refresh_token.user = user + refresh_token.client = client + refresh_token.save() + + # expire time of token in full seconds + # timedelta.total_seconds is python >= 2.7 or we would use that + td = token.expires - datetime.now() + exp_in = 86400*td.days + td.seconds # just ignore µsec + + return {'access_token': token.token, 'token_type': 'bearer', + 'refresh_token': refresh_token.token, 'expires_in': exp_in} + + +def generate_identifier(): + ''' Generates a ``uuid.uuid4()`` ''' + return unicode(uuid.uuid4()) + + +def generate_token(): + ''' Uses generate_identifier ''' + return generate_identifier() + + +def generate_refresh_token(): + ''' Uses generate_identifier ''' + return generate_identifier() + + +def generate_code(): + ''' Uses generate_identifier ''' + return generate_identifier() + + +def generate_secret(): + ''' + Generate a long string of pseudo-random characters + ''' + # XXX: We might not want it to use bcrypt, since bcrypt takes its time to + # generate the result. + return unicode(getrandbits(192)) + diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py index ea45c209..d6fd314f 100644 --- a/mediagoblin/plugins/oauth/views.py +++ b/mediagoblin/plugins/oauth/views.py @@ -16,21 +16,21 @@ # along with this program. If not, see . import logging -import json from urllib import urlencode -from uuid import uuid4 -from datetime import datetime + +from werkzeug.exceptions import BadRequest from mediagoblin.tools.response import render_to_response, redirect from mediagoblin.decorators import require_active_login -from mediagoblin.messages import add_message, SUCCESS, ERROR +from mediagoblin.messages import add_message, SUCCESS from mediagoblin.tools.translate import pass_to_ugettext as _ -from mediagoblin.plugins.oauth.models import OAuthCode, OAuthToken, \ - OAuthClient, OAuthUserClient +from mediagoblin.plugins.oauth.models import OAuthCode, OAuthClient, \ + OAuthUserClient, OAuthRefreshToken from mediagoblin.plugins.oauth.forms import ClientRegistrationForm, \ AuthorizationForm -from mediagoblin.plugins.oauth.tools import require_client_auth +from mediagoblin.plugins.oauth.tools import require_client_auth, \ + create_token from mediagoblin.plugins.api.tools import json_response _log = logging.getLogger(__name__) @@ -51,9 +51,6 @@ def register_client(request): client.owner_id = request.user.id client.redirect_uri = unicode(form.redirect_uri.data) - client.generate_identifier() - client.generate_secret() - client.save() add_message(request, SUCCESS, _('The client {0} has been registered!')\ @@ -92,9 +89,9 @@ def authorize_client(request): form.client_id.data).first() if not client: - _log.error('''No such client id as received from client authorization - form.''') - return BadRequest() + _log.error('No such client id as received from client authorization \ +form.') + raise BadRequest() if form.validate(): relation = OAuthUserClient() @@ -105,7 +102,7 @@ def authorize_client(request): elif form.deny.data: relation.state = u'rejected' else: - return BadRequest + raise BadRequest() relation.save() @@ -136,7 +133,7 @@ def authorize(request, client): return json_response({ 'status': 400, 'errors': - [u'Public clients MUST have a redirect_uri pre-set']}, + [u'Public clients should have a redirect_uri pre-set.']}, _disable_cors=True) redirect_uri = client.redirect_uri @@ -146,11 +143,10 @@ def authorize(request, client): if not redirect_uri: return json_response({ 'status': 400, - 'errors': [u'Can not find a redirect_uri for client: {0}'\ - .format(client.name)]}, _disable_cors=True) + 'errors': [u'No redirect_uri supplied!']}, + _disable_cors=True) code = OAuthCode() - code.code = unicode(uuid4()) code.user = request.user code.client = client code.save() @@ -180,59 +176,79 @@ def authorize(request, client): def access_token(request): + ''' + Access token endpoint provides access tokens to any clients that have the + right grants/credentials + ''' + + client = None + user = None + if request.GET.get('code'): + # Validate the code arg, then get the client object from the db. code = OAuthCode.query.filter(OAuthCode.code == request.GET.get('code')).first() - if code: - if code.client.type == u'confidential': - client_identifier = request.GET.get('client_id') - - if not client_identifier: - return json_response({ - 'error': 'invalid_request', - 'error_description': - 'Missing client_id in request'}) - - client_secret = request.GET.get('client_secret') - - if not client_secret: - return json_response({ - 'error': 'invalid_request', - 'error_description': - 'Missing client_secret in request'}) - - if not client_secret == code.client.secret or \ - not client_identifier == code.client.identifier: - return json_response({ - 'error': 'invalid_client', - 'error_description': - 'The client_id or client_secret does not match the' - ' code'}) - - token = OAuthToken() - token.token = unicode(uuid4()) - token.user = code.user - token.client = code.client - token.save() - - # expire time of token in full seconds - # timedelta.total_seconds is python >= 2.7 or we would use that - td = token.expires - datetime.now() - exp_in = 86400*td.days + td.seconds # just ignore µsec - - access_token_data = { - 'access_token': token.token, - 'token_type': 'bearer', - 'expires_in': exp_in} - return json_response(access_token_data, _disable_cors=True) - else: + if not code: return json_response({ 'error': 'invalid_request', 'error_description': - 'Invalid code'}) - else: - return json_response({ - 'error': 'invalid_request', - 'error_descriptin': - 'Missing `code` parameter in request'}) + 'Invalid code.'}) + + client = code.client + user = code.user + + elif request.args.get('refresh_token'): + # Validate a refresh token, then get the client object from the db. + refresh_token = OAuthRefreshToken.query.filter( + OAuthRefreshToken.token == + request.args.get('refresh_token')).first() + + if not refresh_token: + return json_response({ + 'error': 'invalid_request', + 'error_description': + 'Invalid refresh token.'}) + + client = refresh_token.client + user = refresh_token.user + + if client: + client_identifier = request.GET.get('client_id') + + if not client_identifier: + return json_response({ + 'error': 'invalid_request', + 'error_description': + 'Missing client_id in request.'}) + + if not client_identifier == client.identifier: + return json_response({ + 'error': 'invalid_client', + 'error_description': + 'Mismatching client credentials.'}) + + if client.type == u'confidential': + client_secret = request.GET.get('client_secret') + + if not client_secret: + return json_response({ + 'error': 'invalid_request', + 'error_description': + 'Missing client_secret in request.'}) + + if not client_secret == client.secret: + return json_response({ + 'error': 'invalid_client', + 'error_description': + 'Mismatching client credentials.'}) + + + access_token_data = create_token(client, user) + + return json_response(access_token_data, _disable_cors=True) + + return json_response({ + 'error': 'invalid_request', + 'error_description': + 'Missing `code` or `refresh_token` parameter in request.'}) -- cgit v1.2.3 From bc92ff9d3cc1e4b4aadbd7728f52f2829178e699 Mon Sep 17 00:00:00 2001 From: Elrond Date: Thu, 18 Apr 2013 15:53:09 +0200 Subject: Start to use six for basestring. six allows us to smoothly get more forward compatible with py3. The idea is to change things over to use six, when/if we feel a need for it. --- mediagoblin/plugins/piwigo/tools.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py index 85d77310..4d2e985a 100644 --- a/mediagoblin/plugins/piwigo/tools.py +++ b/mediagoblin/plugins/piwigo/tools.py @@ -16,6 +16,7 @@ import logging +import six import lxml.etree as ET from werkzeug.exceptions import MethodNotAllowed @@ -43,7 +44,7 @@ class PwgNamedArray(list): def _fill_element_dict(el, data, as_attr=()): for k, v in data.iteritems(): if k in as_attr: - if not isinstance(v, basestring): + if not isinstance(v, six.string_types): v = str(v) el.set(k, v) else: @@ -57,7 +58,7 @@ def _fill_element(el, data): el.text = "1" else: el.text = "0" - elif isinstance(data, basestring): + elif isinstance(data, six.string_types): el.text = data elif isinstance(data, int): el.text = str(data) -- cgit v1.2.3 From f6f557696d1b9b5587513c517d1846f0b91e46cf Mon Sep 17 00:00:00 2001 From: Elrond Date: Fri, 22 Mar 2013 16:07:07 +0100 Subject: Use check_file_field in pwg_images_addSimple. --- mediagoblin/plugins/piwigo/views.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 3dee09cd..26e5019a 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -23,6 +23,7 @@ from werkzeug.wrappers import BaseResponse from mediagoblin import mg_globals from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.tools.response import render_404 +from mediagoblin.submit.lib import check_file_field from .tools import CmdTable, PwgNamedArray, response_xml from .forms import AddSimpleForm @@ -92,6 +93,9 @@ def pwg_images_addSimple(request): dump.append("%s=%r" % (f.name, f.data)) _log.info("addimple: %r %s %r", request.form, " ".join(dump), request.files) + if not check_file_field(request, 'image'): + raise BadRequest() + return {'image_id': 123456, 'url': ''} -- cgit v1.2.3 From 90d7de255ae04dd6db386db00db394e596f99881 Mon Sep 17 00:00:00 2001 From: Elrond Date: Fri, 29 Mar 2013 14:36:37 +0100 Subject: piwigo: Send NotImplemented for unknown methods. That's somewhat, what piwigo does. --- mediagoblin/plugins/piwigo/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 26e5019a..bd3f9320 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -17,12 +17,11 @@ import logging import re -from werkzeug.exceptions import MethodNotAllowed, BadRequest +from werkzeug.exceptions import MethodNotAllowed, BadRequest, NotImplemented from werkzeug.wrappers import BaseResponse from mediagoblin import mg_globals from mediagoblin.meddleware.csrf import csrf_exempt -from mediagoblin.tools.response import render_404 from mediagoblin.submit.lib import check_file_field from .tools import CmdTable, PwgNamedArray, response_xml from .forms import AddSimpleForm @@ -157,7 +156,7 @@ def ws_php(request): if not func: _log.warn("wsphp: Unhandled %s %r %r", request.method, request.args, request.form) - return render_404(request) + raise NotImplemented() result = func(request) -- cgit v1.2.3 From 665b9c420aa1a7c768e44a8639b6fc185823e202 Mon Sep 17 00:00:00 2001 From: Aditi Mittal Date: Mon, 22 Apr 2013 19:18:45 +0530 Subject: Fix-bug-667-Use-lazy_pass_to_ugettext-for-forms. --- mediagoblin/plugins/oauth/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/oauth/forms.py b/mediagoblin/plugins/oauth/forms.py index d0a4e9b8..5edd992a 100644 --- a/mediagoblin/plugins/oauth/forms.py +++ b/mediagoblin/plugins/oauth/forms.py @@ -19,7 +19,7 @@ import wtforms from urlparse import urlparse from mediagoblin.tools.extlib.wtf_html5 import URLField -from mediagoblin.tools.translate import fake_ugettext_passthrough as _ +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ class AuthorizationForm(wtforms.Form): -- cgit v1.2.3 From 1422cab669d5238e027109b7495bcfd6219a0064 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sun, 5 May 2013 22:24:34 +0200 Subject: Removed unused imports in httpapiauth --- mediagoblin/plugins/httpapiauth/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/httpapiauth/__init__.py b/mediagoblin/plugins/httpapiauth/__init__.py index 081b590e..99b6a4b0 100644 --- a/mediagoblin/plugins/httpapiauth/__init__.py +++ b/mediagoblin/plugins/httpapiauth/__init__.py @@ -15,9 +15,8 @@ # along with this program. If not, see . import logging -import base64 -from werkzeug.exceptions import BadRequest, Unauthorized +from werkzeug.exceptions import Unauthorized from mediagoblin.plugins.api.tools import Auth -- cgit v1.2.3 From 230b5eb2eb9ae9c2b0930bbd2e32dc963b90b9fc Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Wed, 8 May 2013 15:20:27 -0500 Subject: Fixing API setup with new plugin "config spec" world It shouldn't reference the config until in the setup_plugin() method, else there's a race condition. --- mediagoblin/plugins/api/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/__init__.py b/mediagoblin/plugins/api/__init__.py index d3fdf2ef..1eddd9e0 100644 --- a/mediagoblin/plugins/api/__init__.py +++ b/mediagoblin/plugins/api/__init__.py @@ -23,11 +23,11 @@ _log = logging.getLogger(__name__) PLUGIN_DIR = os.path.dirname(__file__) -config = pluginapi.get_config(__name__) - def setup_plugin(): _log.info('Setting up API...') + config = pluginapi.get_config(__name__) + _log.debug('API config: {0}'.format(config)) routes = [ -- cgit v1.2.3 From 180a0081007532ed7f6adad8fe3f2cde97031a2f Mon Sep 17 00:00:00 2001 From: Elrond Date: Sun, 5 May 2013 16:45:37 +0200 Subject: piwigo: Remove possibly_add_cookie. This one was a fake thing to make clients happy. Real sessions coming sonn. --- mediagoblin/plugins/piwigo/views.py | 15 --------------- 1 file changed, 15 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index bd3f9320..e064b418 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -133,19 +133,6 @@ def pwg_images_addChunk(request): return True -def possibly_add_cookie(request, response): - # TODO: We should only add a *real* cookie, if - # authenticated. And if there is no cookie already. - if True: - response.set_cookie( - 'pwg_id', - "some_fake_for_now", - path=request.environ['SCRIPT_NAME'], - domain=mg_globals.app_config.get('csrf_cookie_domain'), - secure=(request.scheme.lower() == 'https'), - httponly=True) - - @csrf_exempt def ws_php(request): if request.method not in ("GET", "POST"): @@ -165,6 +152,4 @@ def ws_php(request): response = response_xml(result) - possibly_add_cookie(request, response) - return response -- cgit v1.2.3 From c1df8d19630b1e60598db1bd93171926234b633b Mon Sep 17 00:00:00 2001 From: Elrond Date: Mon, 25 Mar 2013 15:34:21 +0100 Subject: piwigo: Add .images.add including form handling. To make things a bit easier, switch to WTForms for validating the received data. --- mediagoblin/plugins/piwigo/forms.py | 16 ++++++++++++++++ mediagoblin/plugins/piwigo/tools.py | 15 ++++++++++++++- mediagoblin/plugins/piwigo/views.py | 13 +++++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/forms.py b/mediagoblin/plugins/piwigo/forms.py index 5bb12e62..18cbd5c5 100644 --- a/mediagoblin/plugins/piwigo/forms.py +++ b/mediagoblin/plugins/piwigo/forms.py @@ -26,3 +26,19 @@ class AddSimpleForm(wtforms.Form): # tags = wtforms.FieldList(wtforms.TextField()) category = wtforms.IntegerField() level = wtforms.IntegerField() + + +_md5_validator = wtforms.validators.Regexp(r"^[0-9a-fA-F]{32}$") + + +class AddForm(wtforms.Form): + original_sum = wtforms.TextField(None, + [_md5_validator, + wtforms.validators.Required()]) + thumbnail_sum = wtforms.TextField(None, + [wtforms.validators.Optional(False), + _md5_validator]) + file_sum = wtforms.TextField(None, [_md5_validator]) + name = wtforms.TextField() + date_creation = wtforms.TextField() + categories = wtforms.TextField() diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py index 4d2e985a..cd466367 100644 --- a/mediagoblin/plugins/piwigo/tools.py +++ b/mediagoblin/plugins/piwigo/tools.py @@ -18,7 +18,7 @@ import logging import six import lxml.etree as ET -from werkzeug.exceptions import MethodNotAllowed +from werkzeug.exceptions import MethodNotAllowed, BadRequest from mediagoblin.tools.response import Response @@ -106,3 +106,16 @@ class CmdTable(object): _log.warn("Method %s only allowed for POST", cmd_name) raise MethodNotAllowed() return func + + +def check_form(form): + if not form.validate(): + _log.error("form validation failed for form %r", form) + for f in form: + if len(f.error): + _log.error("Errors for %s: %r", f.name, f.errors) + raise BadRequest() + dump = [] + for f in form: + dump.append("%s=%r" % (f.name, f.data)) + _log.debug("form: %s", " ".join(dump)) diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index e064b418..837d8eca 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -23,8 +23,8 @@ from werkzeug.wrappers import BaseResponse from mediagoblin import mg_globals from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.submit.lib import check_file_field -from .tools import CmdTable, PwgNamedArray, response_xml -from .forms import AddSimpleForm +from .tools import CmdTable, PwgNamedArray, response_xml, check_form +from .forms import AddSimpleForm, AddForm _log = logging.getLogger(__name__) @@ -133,6 +133,15 @@ def pwg_images_addChunk(request): return True +@CmdTable("pwg.images.add", True) +def pwg_images_add(request): + _log.info("add: %r", request.form) + form = AddForm(request.form) + check_form(form) + + return {'image_id': 123456, 'url': ''} + + @csrf_exempt def ws_php(request): if request.method not in ("GET", "POST"): -- cgit v1.2.3 From 7fb419ddd2bd1770d62fffadc674c53b670cba81 Mon Sep 17 00:00:00 2001 From: Elrond Date: Fri, 29 Mar 2013 14:49:13 +0100 Subject: Create new session system for piwigo plugin. Using the brand new itsdangerous sessions to power the sessions for piwigo. The real point is: Clients want to have the session in a "pwg_id" cookie and don't accept any other cookie name. --- mediagoblin/plugins/piwigo/__init__.py | 5 +++++ mediagoblin/plugins/piwigo/tools.py | 31 +++++++++++++++++++++++++++++++ mediagoblin/plugins/piwigo/views.py | 26 +++++++++++++++++++------- 3 files changed, 55 insertions(+), 7 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/__init__.py b/mediagoblin/plugins/piwigo/__init__.py index 73326e9e..c4da708a 100644 --- a/mediagoblin/plugins/piwigo/__init__.py +++ b/mediagoblin/plugins/piwigo/__init__.py @@ -17,6 +17,8 @@ import logging from mediagoblin.tools import pluginapi +from mediagoblin.tools.session import SessionManager +from .tools import PWGSession _log = logging.getLogger(__name__) @@ -32,6 +34,9 @@ def setup_plugin(): pluginapi.register_routes(routes) + PWGSession.session_manager = SessionManager("pwg_id", "plugins.piwigo") + + hooks = { 'setup': setup_plugin } diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py index cd466367..400be615 100644 --- a/mediagoblin/plugins/piwigo/tools.py +++ b/mediagoblin/plugins/piwigo/tools.py @@ -20,6 +20,7 @@ import six import lxml.etree as ET from werkzeug.exceptions import MethodNotAllowed, BadRequest +from mediagoblin.tools.request import setup_user_in_request from mediagoblin.tools.response import Response @@ -119,3 +120,33 @@ def check_form(form): for f in form: dump.append("%s=%r" % (f.name, f.data)) _log.debug("form: %s", " ".join(dump)) + + +class PWGSession(object): + session_manager = None + + def __init__(self, request): + self.request = request + self.in_pwg_session = False + + def __enter__(self): + # Backup old state + self.old_session = self.request.session + self.old_user = self.request.user + # Load piwigo session into state + self.request.session = self.session_manager.load_session_from_cookie( + self.request) + setup_user_in_request(self.request) + self.in_pwg_session = True + return self + + def __exit__(self, *args): + # Restore state + self.request.session = self.old_session + self.request.user = self.old_user + self.in_pwg_session = False + + def save_to_cookie(self, response): + assert self.in_pwg_session + self.session_manager.save_session_to_cookie(self.request.session, + self.request, response) diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 837d8eca..6a246f18 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -20,10 +20,11 @@ import re from werkzeug.exceptions import MethodNotAllowed, BadRequest, NotImplemented from werkzeug.wrappers import BaseResponse -from mediagoblin import mg_globals from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.submit.lib import check_file_field -from .tools import CmdTable, PwgNamedArray, response_xml, check_form +from mediagoblin.auth.lib import fake_login_attempt +from .tools import CmdTable, PwgNamedArray, response_xml, check_form, \ + PWGSession from .forms import AddSimpleForm, AddForm @@ -35,12 +36,21 @@ def pwg_login(request): username = request.form.get("username") password = request.form.get("password") _log.info("Login for %r/%r...", username, password) + user = request.db.User.query.filter_by(username=username).first() + if not user: + fake_login_attempt() + return False + if not user.check_login(password): + return False + request.session["user_id"] = user.id + request.session.save() return True @CmdTable("pwg.session.logout") def pwg_logout(request): _log.info("Logout") + request.session.delete() return True @@ -154,11 +164,13 @@ def ws_php(request): request.args, request.form) raise NotImplemented() - result = func(request) + with PWGSession(request) as session: + result = func(request) - if isinstance(result, BaseResponse): - return result + if isinstance(result, BaseResponse): + return result - response = response_xml(result) + response = response_xml(result) + session.save_to_cookie(response) - return response + return response -- cgit v1.2.3 From 665946033e1c9eef775a84a538ceebe9f52c657e Mon Sep 17 00:00:00 2001 From: Elrond Date: Fri, 29 Mar 2013 14:48:08 +0100 Subject: piwigo: Let getStatus return the current user. If there is a user logged in, show his name. --- mediagoblin/plugins/piwigo/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 6a246f18..5d7d371d 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -61,7 +61,11 @@ def pwg_getversion(request): @CmdTable("pwg.session.getStatus") def pwg_session_getStatus(request): - return {'username': "fake_user"} + if request.user: + username = request.user.username + else: + username = "guest" + return {'username': username} @CmdTable("pwg.categories.getList") -- cgit v1.2.3 From f035ec3db4c67e4a4d03d78dccd57ea82b9a6a5d Mon Sep 17 00:00:00 2001 From: Elrond Date: Sun, 5 May 2013 17:02:00 +0200 Subject: piwigo: Better logging for login. --- mediagoblin/plugins/piwigo/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 5d7d371d..b59247ad 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -35,13 +35,16 @@ _log = logging.getLogger(__name__) def pwg_login(request): username = request.form.get("username") password = request.form.get("password") - _log.info("Login for %r/%r...", username, password) + _log.debug("Login for %r/%r...", username, password) user = request.db.User.query.filter_by(username=username).first() if not user: + _log.info("User %r not found", username) fake_login_attempt() return False if not user.check_login(password): + _log.warn("Wrong password for %r", username) return False + _log.info("Logging %r in", username) request.session["user_id"] = user.id request.session.save() return True -- cgit v1.2.3 From 388daab77cd86e8b93f620716e626336056dcae1 Mon Sep 17 00:00:00 2001 From: Elrond Date: Thu, 9 May 2013 12:28:54 +0200 Subject: piwigo: Fix validator usage. wtforms.validators.Optional doesn't take an argument. I don't know, why I gave it one. --- mediagoblin/plugins/piwigo/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/forms.py b/mediagoblin/plugins/piwigo/forms.py index 18cbd5c5..fb04aa6a 100644 --- a/mediagoblin/plugins/piwigo/forms.py +++ b/mediagoblin/plugins/piwigo/forms.py @@ -36,7 +36,7 @@ class AddForm(wtforms.Form): [_md5_validator, wtforms.validators.Required()]) thumbnail_sum = wtforms.TextField(None, - [wtforms.validators.Optional(False), + [wtforms.validators.Optional(), _md5_validator]) file_sum = wtforms.TextField(None, [_md5_validator]) name = wtforms.TextField() -- cgit v1.2.3 From c732f422ae9fd27de060468e8d9aa4f8c7dfe47e Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sun, 19 May 2013 22:54:39 +0200 Subject: Added upload processing to the piwigo/addSimple --- mediagoblin/plugins/piwigo/views.py | 61 +++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index b59247ad..5633f136 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -16,13 +16,18 @@ import logging import re +from os.path import splitext +import shutil from werkzeug.exceptions import MethodNotAllowed, BadRequest, NotImplemented from werkzeug.wrappers import BaseResponse from mediagoblin.meddleware.csrf import csrf_exempt -from mediagoblin.submit.lib import check_file_field from mediagoblin.auth.lib import fake_login_attempt +from mediagoblin.media_types import sniff_media +from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ + run_process_media + from .tools import CmdTable, PwgNamedArray, response_xml, check_form, \ PWGSession from .forms import AddSimpleForm, AddForm @@ -112,11 +117,61 @@ def pwg_images_addSimple(request): if not check_file_field(request, 'image'): raise BadRequest() - return {'image_id': 123456, 'url': ''} + filename = request.files['image'].filename + + # Sniff the submitted media to determine which + # media plugin should handle processing + media_type, media_manager = sniff_media( + request.files['image']) + + # create entry and save in database + entry = request.db.MediaEntry() + entry.media_type = unicode(media_type) + entry.title = ( + unicode(form.name.data) + or unicode(splitext(filename)[0])) + + entry.description = unicode(form.comment.data) + + # entry.license = unicode(form.license.data) or None + + entry.uploader = request.user.id + + ''' + # Process the user's folksonomy "tags" + entry.tags = convert_to_tag_list_of_dicts( + form.tags.data) + ''' + + # Generate a slug from the title + entry.generate_slug() + + queue_file = prepare_queue_task(request.app, entry, filename) + + with queue_file: + shutil.copyfileobj(request.files['image'].stream, + queue_file, + length=4 * 1048576) + + # Save now so we have this data before kicking off processing + entry.save() + + # Pass off to processing + # + # (... don't change entry after this point to avoid race + # conditions with changes to the document via processing code) + feed_url = request.urlgen( + 'mediagoblin.user_pages.atom_feed', + qualified=True, user=request.user.username) + run_process_media(entry, feed_url) + + return {'image_id': entry.id, 'url': entry.url_for_self(request.urlgen, + qualified=True)} + - md5sum_matcher = re.compile(r"^[0-9a-fA-F]{32}$") + def fetch_md5(request, parm_name, optional_parm=False): val = request.form.get(parm_name) if (val is None) and (not optional_parm): -- cgit v1.2.3 From d6c3375a781efd13a185b67e0f084c460c1e3cc6 Mon Sep 17 00:00:00 2001 From: Elrond Date: Sun, 19 May 2013 01:14:46 +0200 Subject: A bit of pep8 and small typo fix. --- mediagoblin/plugins/piwigo/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py index 400be615..f060e9f8 100644 --- a/mediagoblin/plugins/piwigo/tools.py +++ b/mediagoblin/plugins/piwigo/tools.py @@ -113,7 +113,7 @@ def check_form(form): if not form.validate(): _log.error("form validation failed for form %r", form) for f in form: - if len(f.error): + if len(f.errors): _log.error("Errors for %s: %r", f.name, f.errors) raise BadRequest() dump = [] @@ -140,7 +140,7 @@ class PWGSession(object): self.in_pwg_session = True return self - def __exit__(self, *args): + def __exit__(self, *args): # Restore state self.request.session = self.old_session self.request.user = self.old_user -- cgit v1.2.3 From 68910f6797e7e5a2e0d8e5b966861f9df0523e42 Mon Sep 17 00:00:00 2001 From: Elrond Date: Mon, 20 May 2013 19:28:35 +0200 Subject: piwigo: Add PwgError class. This allows to return piwigo xml errors. Those can also be matched into html error codes. --- mediagoblin/plugins/piwigo/tools.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py index f060e9f8..30bb4b97 100644 --- a/mediagoblin/plugins/piwigo/tools.py +++ b/mediagoblin/plugins/piwigo/tools.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from collections import namedtuple import logging import six @@ -27,6 +28,9 @@ from mediagoblin.tools.response import Response _log = logging.getLogger(__name__) +PwgError = namedtuple("PwgError", ["code", "msg"]) + + class PwgNamedArray(list): def __init__(self, l, item_name, as_attrib=()): self.item_name = item_name @@ -74,9 +78,17 @@ def _fill_element(el, data): def response_xml(result): r = ET.Element("rsp") r.set("stat", "ok") - _fill_element(r, result) + status = None + if isinstance(result, PwgError): + r.set("stat", "fail") + err = ET.SubElement(r, "err") + err.set("code", str(result.code)) + err.set("msg", result.msg) + status = result.code + else: + _fill_element(r, result) return Response(ET.tostring(r, encoding="utf-8", xml_declaration=True), - mimetype='text/xml') + mimetype='text/xml', status=status) class CmdTable(object): -- cgit v1.2.3 From 4adc3a85dda878b40f44e07b2283d4c55c6c5d02 Mon Sep 17 00:00:00 2001 From: Elrond Date: Mon, 20 May 2013 17:50:04 +0200 Subject: piwigo: Return proper error for wrong user/password. And fix tests. --- mediagoblin/plugins/piwigo/tools.py | 3 ++- mediagoblin/plugins/piwigo/views.py | 9 ++++----- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/tools.py b/mediagoblin/plugins/piwigo/tools.py index 30bb4b97..484ea531 100644 --- a/mediagoblin/plugins/piwigo/tools.py +++ b/mediagoblin/plugins/piwigo/tools.py @@ -84,7 +84,8 @@ def response_xml(result): err = ET.SubElement(r, "err") err.set("code", str(result.code)) err.set("msg", result.msg) - status = result.code + if result.code >= 100 and result.code < 600: + status = result.code else: _fill_element(r, result) return Response(ET.tostring(r, encoding="utf-8", xml_declaration=True), diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 5633f136..1c655bf5 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -28,8 +28,8 @@ from mediagoblin.media_types import sniff_media from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ run_process_media -from .tools import CmdTable, PwgNamedArray, response_xml, check_form, \ - PWGSession +from .tools import CmdTable, response_xml, check_form, \ + PWGSession, PwgNamedArray, PwgError from .forms import AddSimpleForm, AddForm @@ -40,15 +40,14 @@ _log = logging.getLogger(__name__) def pwg_login(request): username = request.form.get("username") password = request.form.get("password") - _log.debug("Login for %r/%r...", username, password) user = request.db.User.query.filter_by(username=username).first() if not user: _log.info("User %r not found", username) fake_login_attempt() - return False + return PwgError(999, 'Invalid username/password') if not user.check_login(password): _log.warn("Wrong password for %r", username) - return False + return PwgError(999, 'Invalid username/password') _log.info("Logging %r in", username) request.session["user_id"] = user.id request.session.save() -- cgit v1.2.3 From 6c1467d570a4da68ef8b4edac9aecdb9c87a61de Mon Sep 17 00:00:00 2001 From: Elrond Date: Tue, 21 May 2013 00:28:37 +0200 Subject: Refactor submit util new_upload_entry This tool creates an initial media entry for a given user. No magic. It just prefills the license with the user's default license and adds the user as uploader. --- mediagoblin/plugins/api/views.py | 6 ++---- mediagoblin/plugins/piwigo/views.py | 8 ++------ 2 files changed, 4 insertions(+), 10 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index fde76fe4..9159fe65 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -27,7 +27,7 @@ from mediagoblin.media_types import sniff_media from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \ json_response from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ - run_process_media + run_process_media, new_upload_entry _log = logging.getLogger(__name__) @@ -53,7 +53,7 @@ def post_entry(request): media_type, media_manager = sniff_media(media_file) - entry = request.db.MediaEntry() + entry = new_upload_entry(request.user) entry.media_type = unicode(media_type) entry.title = unicode(request.form.get('title') or splitext(media_file.filename)[0]) @@ -61,8 +61,6 @@ def post_entry(request): entry.description = unicode(request.form.get('description')) entry.license = unicode(request.form.get('license', '')) - entry.uploader = request.user.id - entry.generate_slug() # queue appropriately diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 1c655bf5..bd6b1012 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -26,7 +26,7 @@ from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.auth.lib import fake_login_attempt from mediagoblin.media_types import sniff_media from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ - run_process_media + run_process_media, new_upload_entry from .tools import CmdTable, response_xml, check_form, \ PWGSession, PwgNamedArray, PwgError @@ -124,7 +124,7 @@ def pwg_images_addSimple(request): request.files['image']) # create entry and save in database - entry = request.db.MediaEntry() + entry = new_upload_entry(request.user) entry.media_type = unicode(media_type) entry.title = ( unicode(form.name.data) @@ -132,10 +132,6 @@ def pwg_images_addSimple(request): entry.description = unicode(form.comment.data) - # entry.license = unicode(form.license.data) or None - - entry.uploader = request.user.id - ''' # Process the user's folksonomy "tags" entry.tags = convert_to_tag_list_of_dicts( -- cgit v1.2.3 From 18e64476e9c3dd28655793da092d195fd9e569c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20Sj=C3=B6berg?= Date: Tue, 21 May 2013 22:39:37 +0300 Subject: Fixed minor typo in piwigo logging. --- mediagoblin/plugins/piwigo/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index bd6b1012..b8a75493 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -111,7 +111,8 @@ def pwg_images_addSimple(request): dump = [] for f in form: dump.append("%s=%r" % (f.name, f.data)) - _log.info("addimple: %r %s %r", request.form, " ".join(dump), request.files) + _log.info("addSimple: %r %s %r", request.form, " ".join(dump), + request.files) if not check_file_field(request, 'image'): raise BadRequest() -- cgit v1.2.3 From 415011060751906aa779fe1849d9d5e93048a5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20Sj=C3=B6berg?= Date: Tue, 21 May 2013 22:40:02 +0300 Subject: Rudimentary collections support for piwigo plugin. --- mediagoblin/plugins/piwigo/views.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index b8a75493..c348fd54 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -28,6 +28,9 @@ from mediagoblin.media_types import sniff_media from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ run_process_media, new_upload_entry +from mediagoblin.user_pages.lib import add_media_to_collection +from mediagoblin.db.models import Collection + from .tools import CmdTable, response_xml, check_form, \ PWGSession, PwgNamedArray, PwgError from .forms import AddSimpleForm, AddForm @@ -77,9 +80,20 @@ def pwg_session_getStatus(request): @CmdTable("pwg.categories.getList") def pwg_categories_getList(request): - catlist = ({'id': -29711, + collections = Collection.query.filter_by( + get_creator=request.user).order_by(Collection.title) + + catlist = [{'id': -29711, 'uppercats': "-29711", - 'name': "All my images"},) + 'name': "All my images"}] + + for c in collections: + catlist.append({'id': c.id, + 'uppercats': str(c.id), + 'name': c.title, + 'comment': c.description + }) + return { 'categories': PwgNamedArray( catlist, @@ -161,6 +175,11 @@ def pwg_images_addSimple(request): qualified=True, user=request.user.username) run_process_media(entry, feed_url) + collection_id = form.category.data + if collection_id > 0: + collection = Collection.query.get(collection_id) + add_media_to_collection(collection, entry, "") + return {'image_id': entry.id, 'url': entry.url_for_self(request.urlgen, qualified=True)} -- cgit v1.2.3 From 7da90d56d14d47e9d5b124a42a5d9e1101a603e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20Sj=C3=B6berg?= Date: Tue, 21 May 2013 22:42:41 +0300 Subject: Piwigo: return collections list only to logged in users. --- mediagoblin/plugins/piwigo/views.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index c348fd54..250bf758 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -87,12 +87,13 @@ def pwg_categories_getList(request): 'uppercats': "-29711", 'name': "All my images"}] - for c in collections: - catlist.append({'id': c.id, - 'uppercats': str(c.id), - 'name': c.title, - 'comment': c.description - }) + if request.user: + for c in collections: + catlist.append({'id': c.id, + 'uppercats': str(c.id), + 'name': c.title, + 'comment': c.description + }) return { 'categories': PwgNamedArray( -- cgit v1.2.3 From 94d31920412ecaa77fbf0168f687c542df1c9b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20Sj=C3=B6berg?= Date: Tue, 21 May 2013 22:46:57 +0300 Subject: Piwigo: some sanity checks before adding to collection as per Elrond's suggestions. --- mediagoblin/plugins/piwigo/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 250bf758..5285ce65 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -179,7 +179,8 @@ def pwg_images_addSimple(request): collection_id = form.category.data if collection_id > 0: collection = Collection.query.get(collection_id) - add_media_to_collection(collection, entry, "") + if collection is not None and collection.creator == request.user.id: + add_media_to_collection(collection, entry, "") return {'image_id': entry.id, 'url': entry.url_for_self(request.urlgen, qualified=True)} -- cgit v1.2.3 From cac478e5aac621fc80adf0ef4534c646ab3c4fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20Sj=C3=B6berg?= Date: Tue, 21 May 2013 22:55:55 +0300 Subject: Piwigo minor fix in categories_getList. --- mediagoblin/plugins/piwigo/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 5285ce65..78668ed4 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -80,14 +80,14 @@ def pwg_session_getStatus(request): @CmdTable("pwg.categories.getList") def pwg_categories_getList(request): - collections = Collection.query.filter_by( - get_creator=request.user).order_by(Collection.title) - catlist = [{'id': -29711, 'uppercats': "-29711", 'name': "All my images"}] if request.user: + collections = Collection.query.filter_by( + get_creator=request.user).order_by(Collection.title) + for c in collections: catlist.append({'id': c.id, 'uppercats': str(c.id), -- cgit v1.2.3 From 75fc93686d0763ced6a5769e99e570a4c8fd3273 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Sat, 25 May 2013 07:59:03 -0700 Subject: created a check_login_simple function cherry-picked from rodney757, fixed few conflicts due to out of order cherry-picking. Thanks to rodney757 for making my idea even better. --- mediagoblin/plugins/httpapiauth/__init__.py | 7 ++++--- mediagoblin/plugins/piwigo/views.py | 12 +++--------- 2 files changed, 7 insertions(+), 12 deletions(-) (limited to 'mediagoblin/plugins') diff --git a/mediagoblin/plugins/httpapiauth/__init__.py b/mediagoblin/plugins/httpapiauth/__init__.py index 99b6a4b0..2b2d593c 100644 --- a/mediagoblin/plugins/httpapiauth/__init__.py +++ b/mediagoblin/plugins/httpapiauth/__init__.py @@ -18,6 +18,7 @@ import logging from werkzeug.exceptions import Unauthorized +from mediagoblin.auth.tools import check_login_simple from mediagoblin.plugins.api.tools import Auth _log = logging.getLogger(__name__) @@ -39,10 +40,10 @@ class HTTPAuth(Auth): if not request.authorization: return False - user = request.db.User.query.filter_by( - username=unicode(request.authorization['username'])).first() + user = check_login_simple(unicode(request.authorization['username']), + request.authorization['password']) - if user.check_login(request.authorization['password']): + if user: request.user = user return True else: diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 78668ed4..ca723189 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -23,7 +23,7 @@ from werkzeug.exceptions import MethodNotAllowed, BadRequest, NotImplemented from werkzeug.wrappers import BaseResponse from mediagoblin.meddleware.csrf import csrf_exempt -from mediagoblin.auth.lib import fake_login_attempt +from mediagoblin.auth.tools import check_login_simple from mediagoblin.media_types import sniff_media from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ run_process_media, new_upload_entry @@ -43,15 +43,9 @@ _log = logging.getLogger(__name__) def pwg_login(request): username = request.form.get("username") password = request.form.get("password") - user = request.db.User.query.filter_by(username=username).first() + user = check_login_simple(username, password) if not user: - _log.info("User %r not found", username) - fake_login_attempt() return PwgError(999, 'Invalid username/password') - if not user.check_login(password): - _log.warn("Wrong password for %r", username) - return PwgError(999, 'Invalid username/password') - _log.info("Logging %r in", username) request.session["user_id"] = user.id request.session.save() return True @@ -126,7 +120,7 @@ def pwg_images_addSimple(request): dump = [] for f in form: dump.append("%s=%r" % (f.name, f.data)) - _log.info("addSimple: %r %s %r", request.form, " ".join(dump), + _log.info("addSimple: %r %s %r", request.form, " ".join(dump), request.files) if not check_file_field(request, 'image'): -- cgit v1.2.3