]>
Commit | Line | Data |
---|---|---|
511b6ecb MF |
1 | import datetime |
2 | import logging | |
3 | ||
4 | RFC6415_TYPE = 'application/xrd+xml' | |
5 | RFC7033_TYPE = 'application/jrd+json' | |
6 | ||
7 | JRD_TYPES = ('application/jrd+json', 'application/xrd+json', 'application/json', 'text/json') | |
8 | XRD_TYPES = ('application/xrd+xml', 'text/xml') | |
9 | ||
10 | KNOWN_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 | ||
35 | logger = logging.getLogger("rd") | |
36 | ||
37 | ||
38 | def _is_str(s): | |
39 | try: | |
40 | return isinstance(s, str) | |
41 | except NameError: | |
42 | return isinstance(s, str) | |
43 | ||
44 | ||
45 | def 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 | ||
65 | def 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 | ||
115 | class 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 | ||
131 | class 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 | ||
139 | class 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 | ||
157 | class 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 | ||
179 | class 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 | ||
194 | class 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 | ||
209 | class 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 | ||
217 | class 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 | ||
229 | class 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 | ||
242 | class 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 | ||
263 | class 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 | ||
306 | class 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) |