]> git.frykholm.com Git - friends.git/blame - friends/rd/core.py
Some xml fixes, added rd library
[friends.git] / friends / rd / core.py
CommitLineData
511b6ecb
MF
1import datetime
2import logging
3
4RFC6415_TYPE = 'application/xrd+xml'
5RFC7033_TYPE = 'application/jrd+json'
6
7JRD_TYPES = ('application/jrd+json', 'application/xrd+json', 'application/json', 'text/json')
8XRD_TYPES = ('application/xrd+xml', 'text/xml')
9
10KNOWN_RELS = {
11 'activity_streams': 'http://activitystrea.ms/spec/1.0',
12 'app': ('http://apinamespace.org/atom', 'application/atomsvc+xml'),
13 'avatar': 'http://webfinger.net/rel/avatar',
14 'foaf': ('describedby', 'application/rdf+xml'),
15 'hcard': 'http://microformats.org/profile/hcard',
16 'oauth_access_token': 'http://apinamespace.org/oauth/access_token',
17 'oauth_authorize': 'http://apinamespace.org/oauth/authorize',
18 'oauth_request_token': 'http://apinamespace.org/oauth/request_token',
19 'openid': 'http://specs.openid.net/auth/2.0/provider',
20 'opensocial': 'http://ns.opensocial.org/2008/opensocial/activitystreams',
21 'portable_contacts': 'http://portablecontacts.net/spec/1.0',
22 'profile': 'http://webfinger.net/rel/profile-page',
23 'updates_from': 'http://schemas.google.com/g/2010#updates-from',
24 'ostatus_sub': 'http://ostatus.org/schema/1.0/subscribe',
25 'salmon_endpoint': 'salmon',
26 'salmon_key': 'magic-public-key',
27 'webfist': 'http://webfist.org/spec/rel',
28 'xfn': 'http://gmpg.org/xfn/11',
29
30 'jrd': ('lrdd', 'application/json'),
31 'webfinger': ('lrdd', 'application/jrd+json'),
32 'xrd': ('lrdd', 'application/xrd+xml'),
33}
34
35logger = logging.getLogger("rd")
36
37
38def _is_str(s):
39 try:
40 return isinstance(s, str)
41 except NameError:
42 return isinstance(s, str)
43
44
45def loads(content, content_type):
46
47 from rd import jrd, xrd
48
49 content_type = content_type.split(";")[0]
50
51 if content_type in JRD_TYPES:
52 logger.debug("loads() loading JRD")
53 return jrd.loads(content)
54
55 elif content_type in XRD_TYPES:
56 logger.debug("loads() loading XRD")
57 return xrd.loads(content)
58
59 raise TypeError('Unknown content type')
60
61#
62# helper functions for host parsing and discovery
63#
64
65def parse_uri_components(resource, default_scheme='https'):
66 hostname = None
67 scheme = default_scheme
68
69 from urllib.parse import urlparse
70
71 parts = urlparse(resource)
72 if parts.scheme and parts.netloc:
73 scheme = parts.scheme
74 ''' FIXME: if we have https://user@some.example/ we end up with parts.netloc='user@some.example' here. '''
75 hostname = parts.netloc
76 path = parts.path
77
78 elif parts.scheme == 'acct' or (not parts.scheme and '@' in parts.path):
79 ''' acct: means we expect WebFinger to work, and RFC7033 requires https, so host-meta should support it too. '''
80 scheme = 'https'
81
82 ''' We should just have user@site.example here, but if it instead
83 is user@site.example/whatever/else we have to split it later
84 on the first slash character, '/'.
85 '''
86 hostname = parts.path.split('@')[-1]
87 path = None
88
89 ''' In case we have hostname=='site.example/whatever/else' we do the split
90 on the first slash, giving us 'site.example' and 'whatever/else'.
91 '''
92 if '/' in hostname:
93 (hostname, path) = hostname.split('/', maxsplit=1)
94 ''' Normalize path so it always starts with /, which is the behaviour of urlparse too. '''
95 path = '/' + path
96
97 else:
98 if not parts.path:
99 raise ValueError('No hostname could be deduced from arguments.')
100
101 elif '/' in parts.path:
102 (hostname, path) = parts.path.split('/', maxsplit=1)
103 ''' Normalize path so it always starts with /, which is the behaviour of urlparse too. '''
104 path = '/' + path
105 else:
106 hostname = parts.path
107 path = None
108
109 return (scheme, hostname, path)
110
111#
112# special XRD types
113#
114
115class Attribute(object):
116
117 def __init__(self, name, value):
118 self.name = name
119 self.value = value
120
121 def __cmp__(self, other):
122 return cmp(str(self), str(other))
123
124 def __eq__(self, other):
125 return str(self) == other
126
127 def __str__(self):
128 return "%s=%s" % (self.name, self.value)
129
130
131class Element(object):
132
133 def __init__(self, name, value, attrs=None):
134 self.name = name
135 self.value = value
136 self.attrs = attrs or {}
137
138
139class Title(object):
140
141 def __init__(self, value, lang=None):
142 self.value = value
143 self.lang = lang
144
145 def __cmp__(self, other):
146 return cmp(str(self), str(other))
147
148 def __eq__(self, other):
149 return str(self) == str(other)
150
151 def __str__(self):
152 if self.lang:
153 return "%s:%s" % (self.lang, self.value)
154 return self.value
155
156
157class Property(object):
158
159 def __init__(self, type_, value=None):
160 self.type = type_
161 self.value = value
162
163 def __cmp__(self, other):
164 return cmp(str(self), str(other))
165
166 def __eq__(self, other):
167 return str(self) == other
168
169 def __str__(self):
170 if self.value:
171 return "%s:%s" % (self.type, self.value)
172 return self.type
173
174
175#
176# special list types
177#
178
179class ListLikeObject(list):
180
181 def __setitem__(self, key, value):
182 value = self.item(value)
183 super(ListLikeObject, self).__setitem__(key, value)
184
185 def append(self, value):
186 value = self.item(value)
187 super(ListLikeObject, self).append(value)
188
189 def extend(self, values):
190 values = (self.item(value) for value in values)
191 super(ListLikeObject, self).extend(values)
192
193
194class AttributeList(ListLikeObject):
195
196 def __call__(self, name):
197 for attr in self:
198 if attr.name == name:
199 yield attr
200
201 def item(self, value):
202 if isinstance(value, (list, tuple)):
203 return Attribute(*value)
204 elif not isinstance(value, Attribute):
205 raise ValueError('value must be an instance of Attribute')
206 return value
207
208
209class ElementList(ListLikeObject):
210
211 def item(self, value):
212 if not isinstance(value, Element):
213 raise ValueError('value must be an instance of Type')
214 return value
215
216
217class TitleList(ListLikeObject):
218
219 def item(self, value):
220 if _is_str(value):
221 return Title(value)
222 elif isinstance(value, (list, tuple)):
223 return Title(*value)
224 elif not isinstance(value, Title):
225 raise ValueError('value must be an instance of Title')
226 return value
227
228
229class LinkList(ListLikeObject):
230
231 def __call__(self, rel):
232 for link in self:
233 if link.rel == rel:
234 yield link
235
236 def item(self, value):
237 if not isinstance(value, Link):
238 raise ValueError('value must be an instance of Link')
239 return value
240
241
242class PropertyList(ListLikeObject):
243
244 def __call__(self, type_):
245 for prop in self:
246 if prop.type == type_:
247 yield prop
248
249 def item(self, value):
250 if _is_str(value):
251 return Property(value)
252 elif isinstance(value, (tuple, list)):
253 return Property(*value)
254 elif not isinstance(value, Property):
255 raise ValueError('value must be an instance of Property')
256 return value
257
258
259#
260# Link object
261#
262
263class Link(object):
264
265 def __init__(self, rel=None, type=None, href=None, template=None):
266 self.rel = rel
267 self.type = type
268 self.href = href
269 self.template = template
270 self._titles = TitleList()
271 self._properties = PropertyList()
272
273 def get_titles(self):
274 return self._titles
275 titles = property(get_titles)
276
277 def get_properties(self):
278 return self._properties
279 properties = property(get_properties)
280
281 def apply_template(self, uri):
282
283 from urllib.parse import quote
284
285 if not self.template:
286 raise TypeError('This is not a template Link')
287 return self.template.replace('{uri}', quote(uri, safe=''))
288
289 def __str__(self):
290
291 from cgi import escape
292
293 attrs = ''
294 for prop in ['rel', 'type', 'href', 'template']:
295 val = getattr(self, prop)
296 if val:
297 attrs += ' {!s}="{!s}"'.format(escape(prop), escape(val))
298
299 return '<Link{!s}/>'.format(attrs)
300
301
302#
303# main RD class
304#
305
306class RD(object):
307
308 def __init__(self, xml_id=None, subject=None):
309
310 self.xml_id = xml_id
311 self.subject = subject
312 self._expires = None
313 self._aliases = []
314 self._properties = PropertyList()
315 self._links = LinkList()
316 self._signatures = []
317
318 self._attributes = AttributeList()
319 self._elements = ElementList()
320
321 # ser/deser methods
322
323 def to_json(self):
324 from rd import jrd
325 return jrd.dumps(self)
326
327 def to_xml(self):
328 from rd import xrd
329 return xrd.dumps(self)
330
331 # helper methods
332
333 def find_link(self, rels, attr=None, mimetype=None):
334 if not isinstance(rels, (list, tuple)):
335 rels = (rels,)
336 for link in self.links:
337 if link.rel in rels:
338 if mimetype and link.type != mimetype:
339 continue
340 if attr:
341 return getattr(link, attr, None)
342 return link
343
344 def __getattr__(self, name, attr=None):
345 if name in KNOWN_RELS:
346 try:
347 ''' If we have a specific mimetype for this rel value '''
348 rel, mimetype = KNOWN_RELS[name]
349 except ValueError:
350 rel = KNOWN_RELS[name]
351 mimetype = None
352 return self.find_link(rel, attr=attr, mimetype=mimetype)
353 raise AttributeError(name)
354
355 # custom elements and attributes
356
357 def get_elements(self):
358 return self._elements
359 elements = property(get_elements)
360
361 @property
362 def attributes(self):
363 return self._attributes
364
365 # defined elements and attributes
366
367 def get_expires(self):
368 return self._expires
369
370 def set_expires(self, expires):
371 if not isinstance(expires, datetime.datetime):
372 raise ValueError('expires must be a datetime object')
373 self._expires = expires
374 expires = property(get_expires, set_expires)
375
376 def get_aliases(self):
377 return self._aliases
378 aliases = property(get_aliases)
379
380 def get_properties(self):
381 return self._properties
382 properties = property(get_properties)
383
384 def get_links(self):
385 return self._links
386 links = property(get_links)
387
388 def get_signatures(self):
389 return self._signatures
390 signatures = property(get_links)