From 7a4f561e851fdc7246d804c3abb6748b8a4199a6 Mon Sep 17 00:00:00 2001 From: gnowgi Date: Thu, 15 Mar 2012 16:19:20 +0530 Subject: master trunk of gnowsys-studio --- gstudio/xmlrpc/__init__.py | 81 ++++++++++ gstudio/xmlrpc/metaweblog.py | 344 +++++++++++++++++++++++++++++++++++++++++++ gstudio/xmlrpc/pingback.py | 140 ++++++++++++++++++ 3 files changed, 565 insertions(+) create mode 100644 gstudio/xmlrpc/__init__.py create mode 100644 gstudio/xmlrpc/metaweblog.py create mode 100644 gstudio/xmlrpc/pingback.py (limited to 'gstudio/xmlrpc') diff --git a/gstudio/xmlrpc/__init__.py b/gstudio/xmlrpc/__init__.py new file mode 100644 index 00000000..88ced6b4 --- /dev/null +++ b/gstudio/xmlrpc/__init__.py @@ -0,0 +1,81 @@ +# Copyright (c) 2011, 2012 Free Software Foundation + +# 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 project incorporates work covered by the following copyright and permission notice: + +# Copyright (c) 2009, Julien Fache +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: + +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# * Neither the name of the author nor the names of other +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. +"""XML-RPC methods for Gstudio""" + + +GSTUDIO_XMLRPC_PINGBACK = [ + ('gstudio.xmlrpc.pingback.pingback_ping', + 'pingback.ping'), + ('gstudio.xmlrpc.pingback.pingback_extensions_get_pingbacks', + 'pingback.extensions.getPingbacks')] + +GSTUDIO_XMLRPC_METAWEBLOG = [ + ('gstudio.xmlrpc.metaweblog.get_users_blogs', + 'blogger.getUsersBlogs'), + ('gstudio.xmlrpc.metaweblog.get_user_info', + 'blogger.getUserInfo'), + ('gstudio.xmlrpc.metaweblog.delete_post', + 'blogger.deletePost'), + ('gstudio.xmlrpc.metaweblog.get_authors', + 'wp.getAuthors'), + ('gstudio.xmlrpc.metaweblog.get_metatypes', + 'metaWeblog.getMetatypes'), + ('gstudio.xmlrpc.metaweblog.new_metatype', + 'wp.newMetatype'), + ('gstudio.xmlrpc.metaweblog.get_recent_posts', + 'metaWeblog.getRecentPosts'), + ('gstudio.xmlrpc.metaweblog.get_post', + 'metaWeblog.getPost'), + ('gstudio.xmlrpc.metaweblog.new_post', + 'metaWeblog.newPost'), + ('gstudio.xmlrpc.metaweblog.edit_post', + 'metaWeblog.editPost'), + ('gstudio.xmlrpc.metaweblog.new_media_object', + 'metaWeblog.newMediaObject')] + +GSTUDIO_XMLRPC_METHODS = GSTUDIO_XMLRPC_PINGBACK + GSTUDIO_XMLRPC_METAWEBLOG diff --git a/gstudio/xmlrpc/metaweblog.py b/gstudio/xmlrpc/metaweblog.py new file mode 100644 index 00000000..56ee26cb --- /dev/null +++ b/gstudio/xmlrpc/metaweblog.py @@ -0,0 +1,344 @@ +# Copyright (c) 2011, 2012 Free Software Foundation + +# 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 project incorporates work covered by the following copyright and permission notice: + +# Copyright (c) 2009, Julien Fache +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: + +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# * Neither the name of the author nor the names of other +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. +"""XML-RPC methods of Gstudio metaWeblog API""" +import os +from datetime import datetime +from xmlrpclib import Fault +from xmlrpclib import DateTime + +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.core.urlresolvers import reverse +from django.utils.translation import gettext as _ +from django.utils.html import strip_tags +from django.utils.text import truncate_words +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage +from django.template.defaultfilters import slugify + +from gstudio.models import Nodetype +from gstudio.models import Metatype +from gstudio.settings import PROTOCOL +from gstudio.settings import UPLOAD_TO +from gstudio.managers import DRAFT, PUBLISHED +from django_xmlrpc.decorators import xmlrpc_func + +# http://docs.nucleuscms.org/blog/12#errorcodes +LOGIN_ERROR = 801 +PERMISSION_DENIED = 803 + + +def authenticate(username, password, permission=None): + """Authenticate staff_user with permission""" + try: + user = User.objects.get(username__exact=username) + except User.DoesNotExist: + raise Fault(LOGIN_ERROR, _('Username is incorrect.')) + if not user.check_password(password): + raise Fault(LOGIN_ERROR, _('Password is invalid.')) + if not user.is_staff or not user.is_active: + raise Fault(PERMISSION_DENIED, _('User account unavailable.')) + if permission: + if not user.has_perm(permission): + raise Fault(PERMISSION_DENIED, _('User cannot %s.') % permission) + return user + + +def blog_structure(site): + """A blog structure""" + return {'url': '%s://%s%s' % ( + PROTOCOL, site.domain, reverse('gstudio_nodetype_archive_index')), + 'blogid': settings.SITE_ID, + 'blogName': site.name} + + +def user_structure(user, site): + """An user structure""" + return {'userid': user.pk, + 'email': user.email, + 'nickname': user.username, + 'lastname': user.last_name, + 'firstname': user.first_name, + 'url': '%s://%s%s' % ( + PROTOCOL, site.domain, + reverse('gstudio_author_detail', args=[user.username]))} + + +def author_structure(user): + """An author structure""" + return {'user_id': user.pk, + 'user_login': user.username, + 'display_name': user.username, + 'user_email': user.email} + + +def metatype_structure(metatype, site): + """A metatype structure""" + return {'description': metatype.title, + 'htmlUrl': '%s://%s%s' % ( + PROTOCOL, site.domain, + metatype.get_absolute_url()), + 'rssUrl': '%s://%s%s' % ( + PROTOCOL, site.domain, + reverse('gstudio_metatype_feed', args=[metatype.tree_path])), + # Useful Wordpress Extensions + 'metatypeId': metatype.pk, + 'parentId': metatype.parent and metatype.parent.pk or 0, + 'metatypeDescription': metatype.description, + 'metatypeName': metatype.title} + + +def post_structure(nodetype, site): + """A post structure with extensions""" + author = nodetype.authors.all()[0] + return {'title': nodetype.title, + 'description': unicode(nodetype.html_content), + 'link': '%s://%s%s' % (PROTOCOL, site.domain, + nodetype.get_absolute_url()), + # Basic Extensions + 'permaLink': '%s://%s%s' % (PROTOCOL, site.domain, + nodetype.get_absolute_url()), + 'metatypes': [cat.title for cat in nodetype.metatypes.all()], + 'dateCreated': DateTime(nodetype.creation_date.isoformat()), + 'postid': nodetype.pk, + 'userid': author.username, + # Useful Movable Type Extensions + 'mt_excerpt': nodetype.excerpt, + 'mt_allow_comments': int(nodetype.comment_enabled), + 'mt_allow_pings': int(nodetype.pingback_enabled), + 'mt_keywords': nodetype.tags, + # Useful Wordpress Extensions + 'wp_author': author.username, + 'wp_author_id': author.pk, + 'wp_author_display_name': author.username, + 'wp_password': nodetype.password, + 'wp_slug': nodetype.slug, + 'sticky': nodetype.featured} + + +@xmlrpc_func(returns='struct[]', args=['string', 'string', 'string']) +def get_users_blogs(apikey, username, password): + """blogger.getUsersBlogs(api_key, username, password) + => blog structure[]""" + authenticate(username, password) + site = Site.objects.get_current() + return [blog_structure(site)] + + +@xmlrpc_func(returns='struct', args=['string', 'string', 'string']) +def get_user_info(apikey, username, password): + """blogger.getUserInfo(api_key, username, password) + => user structure""" + user = authenticate(username, password) + site = Site.objects.get_current() + return user_structure(user, site) + + +@xmlrpc_func(returns='struct[]', args=['string', 'string', 'string']) +def get_authors(apikey, username, password): + """wp.getAuthors(api_key, username, password) + => author structure[]""" + authenticate(username, password) + return [author_structure(author) + for author in User.objects.filter(is_staff=True)] + + +@xmlrpc_func(returns='boolean', args=['string', 'string', + 'string', 'string', 'string']) +def delete_post(apikey, post_id, username, password, publish): + """blogger.deletePost(api_key, post_id, username, password, 'publish') + => boolean""" + user = authenticate(username, password, 'gstudio.delete_nodetype') + nodetype = Nodetype.objects.get(id=post_id, authors=user) + nodetype.delete() + return True + + +@xmlrpc_func(returns='struct', args=['string', 'string', 'string']) +def get_post(post_id, username, password): + """metaWeblog.getPost(post_id, username, password) + => post structure""" + user = authenticate(username, password) + site = Site.objects.get_current() + return post_structure(Nodetype.objects.get(id=post_id, authors=user), site) + + +@xmlrpc_func(returns='struct[]', + args=['string', 'string', 'string', 'integer']) +def get_recent_posts(blog_id, username, password, number): + """metaWeblog.getRecentPosts(blog_id, username, password, number) + => post structure[]""" + user = authenticate(username, password) + site = Site.objects.get_current() + return [post_structure(nodetype, site) \ + for nodetype in Nodetype.objects.filter(authors=user)[:number]] + + +@xmlrpc_func(returns='struct[]', args=['string', 'string', 'string']) +def get_metatypes(blog_id, username, password): + """metaWeblog.getMetatypes(blog_id, username, password) + => metatype structure[]""" + authenticate(username, password) + site = Site.objects.get_current() + return [metatype_structure(metatype, site) \ + for metatype in Metatype.objects.all()] + + +@xmlrpc_func(returns='string', args=['string', 'string', 'string', 'struct']) +def new_metatype(blog_id, username, password, metatype_struct): + """wp.newMetatype(blog_id, username, password, metatype) + => metatype_id""" + authenticate(username, password, 'gstudio.add_metatype') + metatype_dict = {'title': metatype_struct['name'], + 'description': metatype_struct['description'], + 'slug': metatype_struct['slug']} + if int(metatype_struct['parent_id']): + metatype_dict['parent'] = Metatype.objects.get( + pk=metatype_struct['parent_id']) + metatype = Metatype.objects.create(**metatype_dict) + + return metatype.pk + + +@xmlrpc_func(returns='string', args=['string', 'string', 'string', + 'struct', 'boolean']) +def new_post(blog_id, username, password, post, publish): + """metaWeblog.newPost(blog_id, username, password, post, publish) + => post_id""" + user = authenticate(username, password, 'gstudio.add_nodetype') + if post.get('dateCreated'): + creation_date = datetime.strptime( + post['dateCreated'].value.replace('Z', '').replace('-', ''), + '%Y%m%dT%H:%M:%S') + else: + creation_date = datetime.now() + + nodetype_dict = {'title': post['title'], + 'content': post['description'], + 'excerpt': post.get('mt_excerpt', truncate_words( + strip_tags(post['description']), 50)), + 'creation_date': creation_date, + 'last_update': creation_date, + 'comment_enabled': post.get('mt_allow_comments', 1) == 1, + 'pingback_enabled': post.get('mt_allow_pings', 1) == 1, + 'featured': post.get('sticky', 0) == 1, + 'tags': 'mt_keywords' in post and post['mt_keywords'] or '', + 'slug': 'wp_slug' in post and post['wp_slug'] or slugify( + post['title']), + 'password': post.get('wp_password', ''), + 'status': publish and PUBLISHED or DRAFT} + nodetype = Nodetype.objects.create(**nodetype_dict) + + author = user + if 'wp_author_id' in post and user.has_perm('gstudio.can_change_author'): + if int(post['wp_author_id']) != user.pk: + author = User.objects.get(pk=post['wp_author_id']) + nodetype.authors.add(author) + + nodetype.sites.add(Site.objects.get_current()) + if 'metatypes' in post: + nodetype.metatypes.add(*[Metatype.objects.get_or_create( + title=cat, slug=slugify(cat))[0] + for cat in post['metatypes']]) + + return nodetype.pk + + +@xmlrpc_func(returns='boolean', args=['string', 'string', 'string', + 'struct', 'boolean']) +def edit_post(post_id, username, password, post, publish): + """metaWeblog.editPost(post_id, username, password, post, publish) + => boolean""" + user = authenticate(username, password, 'gstudio.change_nodetype') + nodetype = Nodetype.objects.get(id=post_id, authors=user) + if post.get('dateCreated'): + creation_date = datetime.strptime( + post['dateCreated'].value.replace('Z', '').replace('-', ''), + '%Y%m%dT%H:%M:%S') + else: + creation_date = nodetype.creation_date + + nodetype.title = post['title'] + nodetype.content = post['description'] + nodetype.excerpt = post.get('mt_excerpt', truncate_words( + strip_tags(post['description']), 50)) + nodetype.creation_date = creation_date + nodetype.last_update = datetime.now() + nodetype.comment_enabled = post.get('mt_allow_comments', 1) == 1 + nodetype.pingback_enabled = post.get('mt_allow_pings', 1) == 1 + nodetype.featured = post.get('sticky', 0) == 1 + nodetype.tags = 'mt_keywords' in post and post['mt_keywords'] or '' + nodetype.slug = 'wp_slug' in post and post['wp_slug'] or slugify( + post['title']) + nodetype.status = publish and PUBLISHED or DRAFT + nodetype.password = post.get('wp_password', '') + nodetype.save() + + if 'wp_author_id' in post and user.has_perm('gstudio.can_change_author'): + if int(post['wp_author_id']) != user.pk: + author = User.objects.get(pk=post['wp_author_id']) + nodetype.authors.clear() + nodetype.authors.add(author) + + if 'metatypes' in post: + nodetype.metatypes.clear() + nodetype.metatypes.add(*[Metatype.objects.get_or_create( + title=cat, slug=slugify(cat))[0] + for cat in post['metatypes']]) + return True + + +@xmlrpc_func(returns='struct', args=['string', 'string', 'string', 'struct']) +def new_media_object(blog_id, username, password, media): + """metaWeblog.newMediaObject(blog_id, username, password, media) + => media structure""" + authenticate(username, password) + path = default_storage.save(os.path.join(UPLOAD_TO, media['name']), + ContentFile(media['bits'].data)) + return {'url': default_storage.url(path)} diff --git a/gstudio/xmlrpc/pingback.py b/gstudio/xmlrpc/pingback.py new file mode 100644 index 00000000..44aaafd7 --- /dev/null +++ b/gstudio/xmlrpc/pingback.py @@ -0,0 +1,140 @@ +"""XML-RPC methods of Gstudio Pingback""" +from urllib2 import urlopen +from urllib2 import URLError +from urllib2 import HTTPError +from urlparse import urlsplit + +from django.contrib import comments +from django.utils.html import strip_tags +from django.contrib.sites.models import Site +from django.core.urlresolvers import resolve +from django.core.urlresolvers import Resolver404 +from django.utils.translation import ugettext as _ +from django.contrib.contenttypes.models import ContentType + +from gstudio.models import Nodetype +from gstudio.settings import PINGBACK_CONTENT_LENGTH +from BeautifulSoup import BeautifulSoup +from django_xmlrpc.decorators import xmlrpc_func + +UNDEFINED_ERROR = 0 +SOURCE_DOES_NOT_EXIST = 16 +SOURCE_DOES_NOT_LINK = 17 +TARGET_DOES_NOT_EXIST = 32 +TARGET_IS_NOT_PINGABLE = 33 +PINGBACK_ALREADY_REGISTERED = 48 + + +def generate_pingback_content(soup, target, max_length, trunc_char='...'): + """Generate a description text for the pingback""" + link = soup.find('a', href=target) + + content = strip_tags(unicode(link.findParent())) + index = content.index(link.string) + + if len(content) > max_length: + middle = max_length / 2 + start = index - middle + end = index + middle + + if start <= 0: + end -= start + extract = content[0:end] + else: + extract = '%s%s' % (trunc_char, content[start:end]) + + if end < len(content): + extract += trunc_char + return extract + + return content + + +@xmlrpc_func(returns='string', args=['string', 'string']) +def pingback_ping(source, target): + """pingback.ping(sourceURI, targetURI) => 'Pingback message' + + Notifies the server that a link has been added to sourceURI, + pointing to targetURI. + + See: http://hixie.ch/specs/pingback/pingback-1.0""" + try: + if source == target: + return UNDEFINED_ERROR + + site = Site.objects.get_current() + try: + document = ''.join(urlopen(source).readlines()) + except (HTTPError, URLError): + return SOURCE_DOES_NOT_EXIST + + if not target in document: + return SOURCE_DOES_NOT_LINK + + scheme, netloc, path, query, fragment = urlsplit(target) + if netloc != site.domain: + return TARGET_DOES_NOT_EXIST + + try: + view, args, kwargs = resolve(path) + except Resolver404: + return TARGET_DOES_NOT_EXIST + + try: + nodetype = Nodetype.published.get( + slug=kwargs['slug'], + creation_date__year=kwargs['year'], + creation_date__month=kwargs['month'], + creation_date__day=kwargs['day']) + if not nodetype.pingback_enabled: + return TARGET_IS_NOT_PINGABLE + except (KeyError, Nodetype.DoesNotExist): + return TARGET_IS_NOT_PINGABLE + + soup = BeautifulSoup(document) + title = soup.find('title') + title = title and strip_tags(title) or _('No title') + description = generate_pingback_content(soup, target, + PINGBACK_CONTENT_LENGTH) + + comment, created = comments.get_model().objects.get_or_create( + content_type=ContentType.objects.get_for_model(Nodetype), + object_pk=nodetype.pk, user_url=source, site=site, + defaults={'comment': description, 'user_name': title}) + if created: + user = nodetype.authors.all()[0] + comment.flags.create(user=user, flag='pingback') + return 'Pingback from %s to %s registered.' % (source, target) + return PINGBACK_ALREADY_REGISTERED + except: + return UNDEFINED_ERROR + + +@xmlrpc_func(returns='string[]', args=['string']) +def pingback_extensions_get_pingbacks(target): + """pingback.extensions.getPingbacks(url) => '[url, url, ...]' + + Returns an array of URLs that link to the specified url. + + See: http://www.aquarionics.com/misc/archives/blogite/0198.html""" + site = Site.objects.get_current() + + scheme, netloc, path, query, fragment = urlsplit(target) + if netloc != site.domain: + return TARGET_DOES_NOT_EXIST + + try: + view, args, kwargs = resolve(path) + except Resolver404: + return TARGET_DOES_NOT_EXIST + + try: + nodetype = Nodetype.published.get( + slug=kwargs['slug'], + creation_date__year=kwargs['year'], + creation_date__month=kwargs['month'], + creation_date__day=kwargs['day']) + except (KeyError, Nodetype.DoesNotExist): + return TARGET_IS_NOT_PINGABLE + + return [pingback.user_url for pingback in nodetype.pingbacks] -- cgit v1.2.3-70-g09d2