From: Mikael Frykholm Date: Fri, 1 Apr 2016 03:52:41 +0000 (+0200) Subject: Some xml fixes, added rd library X-Git-Url: https://git.frykholm.com/friends.git/commitdiff_plain/511b6ecb108d9a00340f1e58b849c3fcdb4536c9 Some xml fixes, added rd library --- diff --git a/friends/rd/__init__.py b/friends/rd/__init__.py new file mode 100644 index 0000000..0386a2d --- /dev/null +++ b/friends/rd/__init__.py @@ -0,0 +1,6 @@ +from rd.core import * + +__author__ = "Jeremy Carbaugh (jcarbaugh@gmail.com)" +__version__ = "0.1" +__copyright__ = "Copyright (c) 2012 Jeremy Carbaugh" +__license__ = "BSD" diff --git a/friends/rd/__pycache__/__init__.cpython-35.pyc b/friends/rd/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000..0b27d3f Binary files /dev/null and b/friends/rd/__pycache__/__init__.cpython-35.pyc differ diff --git a/friends/rd/__pycache__/core.cpython-35.pyc b/friends/rd/__pycache__/core.cpython-35.pyc new file mode 100644 index 0000000..e417a55 Binary files /dev/null and b/friends/rd/__pycache__/core.cpython-35.pyc differ diff --git a/friends/rd/__pycache__/jrd.cpython-35.pyc b/friends/rd/__pycache__/jrd.cpython-35.pyc new file mode 100644 index 0000000..3421e21 Binary files /dev/null and b/friends/rd/__pycache__/jrd.cpython-35.pyc differ diff --git a/friends/rd/core.py b/friends/rd/core.py new file mode 100644 index 0000000..b017434 --- /dev/null +++ b/friends/rd/core.py @@ -0,0 +1,390 @@ +import datetime +import logging + +RFC6415_TYPE = 'application/xrd+xml' +RFC7033_TYPE = 'application/jrd+json' + +JRD_TYPES = ('application/jrd+json', 'application/xrd+json', 'application/json', 'text/json') +XRD_TYPES = ('application/xrd+xml', 'text/xml') + +KNOWN_RELS = { + 'activity_streams': 'http://activitystrea.ms/spec/1.0', + 'app': ('http://apinamespace.org/atom', 'application/atomsvc+xml'), + 'avatar': 'http://webfinger.net/rel/avatar', + 'foaf': ('describedby', 'application/rdf+xml'), + 'hcard': 'http://microformats.org/profile/hcard', + 'oauth_access_token': 'http://apinamespace.org/oauth/access_token', + 'oauth_authorize': 'http://apinamespace.org/oauth/authorize', + 'oauth_request_token': 'http://apinamespace.org/oauth/request_token', + 'openid': 'http://specs.openid.net/auth/2.0/provider', + 'opensocial': 'http://ns.opensocial.org/2008/opensocial/activitystreams', + 'portable_contacts': 'http://portablecontacts.net/spec/1.0', + 'profile': 'http://webfinger.net/rel/profile-page', + 'updates_from': 'http://schemas.google.com/g/2010#updates-from', + 'ostatus_sub': 'http://ostatus.org/schema/1.0/subscribe', + 'salmon_endpoint': 'salmon', + 'salmon_key': 'magic-public-key', + 'webfist': 'http://webfist.org/spec/rel', + 'xfn': 'http://gmpg.org/xfn/11', + + 'jrd': ('lrdd', 'application/json'), + 'webfinger': ('lrdd', 'application/jrd+json'), + 'xrd': ('lrdd', 'application/xrd+xml'), +} + +logger = logging.getLogger("rd") + + +def _is_str(s): + try: + return isinstance(s, str) + except NameError: + return isinstance(s, str) + + +def loads(content, content_type): + + from rd import jrd, xrd + + content_type = content_type.split(";")[0] + + if content_type in JRD_TYPES: + logger.debug("loads() loading JRD") + return jrd.loads(content) + + elif content_type in XRD_TYPES: + logger.debug("loads() loading XRD") + return xrd.loads(content) + + raise TypeError('Unknown content type') + +# +# helper functions for host parsing and discovery +# + +def parse_uri_components(resource, default_scheme='https'): + hostname = None + scheme = default_scheme + + from urllib.parse import urlparse + + parts = urlparse(resource) + if parts.scheme and parts.netloc: + scheme = parts.scheme + ''' FIXME: if we have https://user@some.example/ we end up with parts.netloc='user@some.example' here. ''' + hostname = parts.netloc + path = parts.path + + elif parts.scheme == 'acct' or (not parts.scheme and '@' in parts.path): + ''' acct: means we expect WebFinger to work, and RFC7033 requires https, so host-meta should support it too. ''' + scheme = 'https' + + ''' We should just have user@site.example here, but if it instead + is user@site.example/whatever/else we have to split it later + on the first slash character, '/'. + ''' + hostname = parts.path.split('@')[-1] + path = None + + ''' In case we have hostname=='site.example/whatever/else' we do the split + on the first slash, giving us 'site.example' and 'whatever/else'. + ''' + if '/' in hostname: + (hostname, path) = hostname.split('/', maxsplit=1) + ''' Normalize path so it always starts with /, which is the behaviour of urlparse too. ''' + path = '/' + path + + else: + if not parts.path: + raise ValueError('No hostname could be deduced from arguments.') + + elif '/' in parts.path: + (hostname, path) = parts.path.split('/', maxsplit=1) + ''' Normalize path so it always starts with /, which is the behaviour of urlparse too. ''' + path = '/' + path + else: + hostname = parts.path + path = None + + return (scheme, hostname, path) + +# +# special XRD types +# + +class Attribute(object): + + def __init__(self, name, value): + self.name = name + self.value = value + + def __cmp__(self, other): + return cmp(str(self), str(other)) + + def __eq__(self, other): + return str(self) == other + + def __str__(self): + return "%s=%s" % (self.name, self.value) + + +class Element(object): + + def __init__(self, name, value, attrs=None): + self.name = name + self.value = value + self.attrs = attrs or {} + + +class Title(object): + + def __init__(self, value, lang=None): + self.value = value + self.lang = lang + + def __cmp__(self, other): + return cmp(str(self), str(other)) + + def __eq__(self, other): + return str(self) == str(other) + + def __str__(self): + if self.lang: + return "%s:%s" % (self.lang, self.value) + return self.value + + +class Property(object): + + def __init__(self, type_, value=None): + self.type = type_ + self.value = value + + def __cmp__(self, other): + return cmp(str(self), str(other)) + + def __eq__(self, other): + return str(self) == other + + def __str__(self): + if self.value: + return "%s:%s" % (self.type, self.value) + return self.type + + +# +# special list types +# + +class ListLikeObject(list): + + def __setitem__(self, key, value): + value = self.item(value) + super(ListLikeObject, self).__setitem__(key, value) + + def append(self, value): + value = self.item(value) + super(ListLikeObject, self).append(value) + + def extend(self, values): + values = (self.item(value) for value in values) + super(ListLikeObject, self).extend(values) + + +class AttributeList(ListLikeObject): + + def __call__(self, name): + for attr in self: + if attr.name == name: + yield attr + + def item(self, value): + if isinstance(value, (list, tuple)): + return Attribute(*value) + elif not isinstance(value, Attribute): + raise ValueError('value must be an instance of Attribute') + return value + + +class ElementList(ListLikeObject): + + def item(self, value): + if not isinstance(value, Element): + raise ValueError('value must be an instance of Type') + return value + + +class TitleList(ListLikeObject): + + def item(self, value): + if _is_str(value): + return Title(value) + elif isinstance(value, (list, tuple)): + return Title(*value) + elif not isinstance(value, Title): + raise ValueError('value must be an instance of Title') + return value + + +class LinkList(ListLikeObject): + + def __call__(self, rel): + for link in self: + if link.rel == rel: + yield link + + def item(self, value): + if not isinstance(value, Link): + raise ValueError('value must be an instance of Link') + return value + + +class PropertyList(ListLikeObject): + + def __call__(self, type_): + for prop in self: + if prop.type == type_: + yield prop + + def item(self, value): + if _is_str(value): + return Property(value) + elif isinstance(value, (tuple, list)): + return Property(*value) + elif not isinstance(value, Property): + raise ValueError('value must be an instance of Property') + return value + + +# +# Link object +# + +class Link(object): + + def __init__(self, rel=None, type=None, href=None, template=None): + self.rel = rel + self.type = type + self.href = href + self.template = template + self._titles = TitleList() + self._properties = PropertyList() + + def get_titles(self): + return self._titles + titles = property(get_titles) + + def get_properties(self): + return self._properties + properties = property(get_properties) + + def apply_template(self, uri): + + from urllib.parse import quote + + if not self.template: + raise TypeError('This is not a template Link') + return self.template.replace('{uri}', quote(uri, safe='')) + + def __str__(self): + + from cgi import escape + + attrs = '' + for prop in ['rel', 'type', 'href', 'template']: + val = getattr(self, prop) + if val: + attrs += ' {!s}="{!s}"'.format(escape(prop), escape(val)) + + return ''.format(attrs) + + +# +# main RD class +# + +class RD(object): + + def __init__(self, xml_id=None, subject=None): + + self.xml_id = xml_id + self.subject = subject + self._expires = None + self._aliases = [] + self._properties = PropertyList() + self._links = LinkList() + self._signatures = [] + + self._attributes = AttributeList() + self._elements = ElementList() + + # ser/deser methods + + def to_json(self): + from rd import jrd + return jrd.dumps(self) + + def to_xml(self): + from rd import xrd + return xrd.dumps(self) + + # helper methods + + def find_link(self, rels, attr=None, mimetype=None): + if not isinstance(rels, (list, tuple)): + rels = (rels,) + for link in self.links: + if link.rel in rels: + if mimetype and link.type != mimetype: + continue + if attr: + return getattr(link, attr, None) + return link + + def __getattr__(self, name, attr=None): + if name in KNOWN_RELS: + try: + ''' If we have a specific mimetype for this rel value ''' + rel, mimetype = KNOWN_RELS[name] + except ValueError: + rel = KNOWN_RELS[name] + mimetype = None + return self.find_link(rel, attr=attr, mimetype=mimetype) + raise AttributeError(name) + + # custom elements and attributes + + def get_elements(self): + return self._elements + elements = property(get_elements) + + @property + def attributes(self): + return self._attributes + + # defined elements and attributes + + def get_expires(self): + return self._expires + + def set_expires(self, expires): + if not isinstance(expires, datetime.datetime): + raise ValueError('expires must be a datetime object') + self._expires = expires + expires = property(get_expires, set_expires) + + def get_aliases(self): + return self._aliases + aliases = property(get_aliases) + + def get_properties(self): + return self._properties + properties = property(get_properties) + + def get_links(self): + return self._links + links = property(get_links) + + def get_signatures(self): + return self._signatures + signatures = property(get_links) diff --git a/friends/rd/jrd.py b/friends/rd/jrd.py new file mode 100644 index 0000000..4dda5b5 --- /dev/null +++ b/friends/rd/jrd.py @@ -0,0 +1,146 @@ + +import json +import isodate + +from rd.core import RD, Attribute, Element, Link, Property, Title + + +def _clean_dict(d): + for key in list(d.keys()): + if not d[key]: + del d[key] + + +def loads(content): + + def expires_handler(key, val, obj): + obj.expires = isodate.parse_datetime(val) + + def subject_handler(key, val, obj): + obj.subject = val + + def aliases_handler(key, val, obj): + for alias in val: + obj.aliases.append(alias) + + def properties_handler(key, val, obj): + for ptype, pvalue in list(val.items()): + obj.properties.append(Property(ptype, pvalue)) + + def titles_handler(key, val, obj): + for tlang, tvalue in list(val.items()): + if tlang == 'default': + tlang = None + obj.titles.append(Title(tvalue, tlang)) + + def links_handler(key, val, obj): + for link in val: + l = Link() + l.rel = link.get('rel', None) + l.type = link.get('type', None) + l.href = link.get('href', None) + l.template = link.get('template', None) + if 'titles' in link: + titles_handler('title', link['titles'], l) + if 'properties' in link: + properties_handler('property', link['properties'], l) + obj.links.append(l) + + def namespace_handler(key, val, obj): + for namespace in val: + ns = list(namespace.keys())[0] + ns_uri = list(namespace.values())[0] + obj.attributes.append(Attribute("xmlns:%s" % ns, ns_uri)) + + handlers = { + 'expires': expires_handler, + 'subject': subject_handler, + 'aliases': aliases_handler, + 'properties': properties_handler, + 'links': links_handler, + 'titles': titles_handler, + 'namespace': namespace_handler, + } + + def unknown_handler(key, val, obj): + if ':' in key: + (ns, name) = key.split(':') + key = "%s:%s" % (ns, name.capitalize()) + obj.elements.append(Element(key, val)) + + doc = json.loads(content) + + rd = RD() + + for key, value in list(doc.items()): + handler = handlers.get(key, unknown_handler) + handler(key, value, rd) + + return rd + + +def dumps(xrd): + + doc = { + "aliases": [], + "links": [], + "namespace": [], + "properties": {}, + "titles": [], + } + + #list_keys = doc.keys() + + for attr in xrd.attributes: + if attr.name.startswith("xmlns:"): + ns = attr.name.split(":")[1] + doc['namespace'].append({ns: attr.value}) + + if xrd.expires: + doc['expires'] = xrd.expires.isoformat() + + if xrd.subject: + doc['subject'] = xrd.subject + + for alias in xrd.aliases: + doc['aliases'].append(alias) + + for prop in xrd.properties: + doc['properties'][prop.type] = prop.value + + for link in xrd.links: + + link_doc = { + 'titles': {}, + 'properties': {}, + } + + if link.rel: + link_doc['rel'] = link.rel + + if link.type: + link_doc['type'] = link.type + + if link.href: + link_doc['href'] = link.href + + if link.template: + link_doc['template'] = link.template + + for prop in link.properties: + link_doc['properties'][prop.type] = prop.value + + for title in link.titles: + lang = title.lang or "default" + link_doc['titles'][lang] = title.value + + _clean_dict(link_doc) + + doc['links'].append(link_doc) + + for elem in xrd.elements: + doc[elem.name.lower()] = elem.value + + _clean_dict(doc) + + return json.dumps(doc) diff --git a/friends/rd/xrd.py b/friends/rd/xrd.py new file mode 100755 index 0000000..afc1247 --- /dev/null +++ b/friends/rd/xrd.py @@ -0,0 +1,163 @@ + +from xml.dom.minidom import getDOMImplementation, parseString, Node + +from rd.core import RD, Element, Link, Property, Title + +XRD_NAMESPACE = "http://docs.oasis-open.org/ns/xri/xrd-1.0" + + +def _get_text(root): + text = '' + for node in root.childNodes: + if node.nodeType == Node.TEXT_NODE and node.nodeValue: + text += node.nodeValue + else: + text += _get_text(node) + return text.strip() or None + + +def loads(content): + + import isodate + + def expires_handler(node, obj): + obj.expires = isodate.parse_datetime(_get_text(node)) + + def subject_handler(node, obj): + obj.subject = _get_text(node) + + def alias_handler(node, obj): + obj.aliases.append(_get_text(node)) + + def property_handler(node, obj): + obj.properties.append(Property(node.getAttribute('type'), _get_text(node))) + + def title_handler(node, obj): + obj.titles.append(Title(_get_text(node), node.getAttribute('xml:lang'))) + + def link_handler(node, obj): + l = Link() + l.rel = node.getAttribute('rel') + l.type = node.getAttribute('type') + l.href = node.getAttribute('href') + l.template = node.getAttribute('template') + obj.links.append(l) + + handlers = { + 'Expires': expires_handler, + 'Subject': subject_handler, + 'Alias': alias_handler, + 'Property': property_handler, + 'Link': link_handler, + 'Title': title_handler, + } + + def unknown_handler(node, obj): + obj.elements.append(Element( + name=node.tagName, + value=_get_text(node), + )) + + def handle_node(node, obj): + handler = handlers.get(node.nodeName, unknown_handler) + if handler and node.nodeType == node.ELEMENT_NODE: + handler(node, obj) + + doc = parseString(content) + root = doc.documentElement + + rd = RD(root.getAttribute('xml:id')) + + for name, value in list(root.attributes.items()): + if name != 'xml:id': + rd.attributes.append((name, value)) + + for node in root.childNodes: + handle_node(node, rd) + if node.nodeName == 'Link': + link = rd.links[-1] + for child in node.childNodes: + handle_node(child, link) + + return rd + + +def dumps(xrd): + + doc = getDOMImplementation().createDocument(XRD_NAMESPACE, "XRD", None) + root = doc.documentElement + root.setAttribute('xmlns', XRD_NAMESPACE) + + if xrd.xml_id: + root.setAttribute('xml:id', xrd.xml_id) + + for attr in xrd.attributes: + root.setAttribute(attr.name, attr.value) + + if xrd.expires: + node = doc.createElement('Expires') + node.appendChild(doc.createTextNode(xrd.expires.isoformat())) + root.appendChild(node) + + if xrd.subject: + node = doc.createElement('Subject') + node.appendChild(doc.createTextNode(xrd.subject)) + root.appendChild(node) + + for alias in xrd.aliases: + node = doc.createElement('Alias') + node.appendChild(doc.createTextNode(alias)) + root.appendChild(node) + + for prop in xrd.properties: + node = doc.createElement('Property') + node.setAttribute('type', prop.type) + if prop.value: + node.appendChild(doc.createTextNode(str(prop.value))) + else: + node.setAttribute('xsi:nil', 'true') + root.appendChild(node) + + for element in xrd.elements: + node = doc.createElement(element.name) + node.appendChild(doc.createTextNode(element.value)) + root.appendChild(node) + + for link in xrd.links: + + if link.href and link.template: + raise ValueError('only one of href or template attributes may be specified') + + link_node = doc.createElement('Link') + + if link.rel: + link_node.setAttribute('rel', link.rel) + + if link.type: + link_node.setAttribute('type', link.type) + + if link.href: + link_node.setAttribute('href', link.href) + + if link.template: + link_node.setAttribute('template', link.template) + + for title in link.titles: + node = doc.createElement('Title') + node.appendChild(doc.createTextNode(str(title))) + if title.lang: + node.setAttribute('xml:lang', title.lang) + link_node.appendChild(node) + + for prop in link.properties: + node = doc.createElement('Property') + node.setAttribute('type', prop.type) + if prop.value: + node.appendChild(doc.createTextNode(str(prop.value))) + else: + node.setAttribute('xsi:nil', 'true') + link_node.appendChild(node) + + root.appendChild(link_node) + + return doc diff --git a/friends/server.py b/friends/server.py index ca64779..750ddc8 100644 --- a/friends/server.py +++ b/friends/server.py @@ -30,6 +30,7 @@ class PushHandler(tornado.web.RequestHandler): hub_lease_seconds = self.get_argument('hub.lease_seconds','') hub_secret = self.get_argument('hub.sercret','') hub_verify_token = self.get_argument('hub.verify_token','') + print(self.request.body) if hub_mode == 'unsubscribe': pass #FIXME path = hub_topic.split(self.settings['domain'])[1] @@ -39,6 +40,8 @@ class PushHandler(tornado.web.RequestHandler): db.execute("INSERT into subscriptions (userid, expires, callback, verified) values (?,?,?,?)",(row['id'],datetime.datetime.now(),hub_callback,False)) db.commit() self.set_status(202) + #TODO add GET callback with the same data we got + #TODO store secret, add it to outgoing feeds with hmac class XrdHandler(tornado.web.RequestHandler): def get(self): self.render("templates/xrd.xml", hostname="ronin.frykholm.com", url=self.settings['domain']) diff --git a/friends/static/mikael.jpg b/friends/static/mikael.jpg new file mode 100644 index 0000000..ace1db2 Binary files /dev/null and b/friends/static/mikael.jpg differ diff --git a/friends/templates/feed.xml b/friends/templates/feed.xml index 4308d1e..320eb65 100644 --- a/friends/templates/feed.xml +++ b/friends/templates/feed.xml @@ -1,9 +1,10 @@ - + {{feed_url}} {{user}} http://activitystrea.ms/schema/1.0/person + {{user}} {{user}}'s tidslinje {{feed_url}}