some project update
[worddb.git] / libs / openid / message.py
blob472e5175e8ebbb61a24ea0fd67596ce212be29fc
1 """Extension argument processing code
2 """
3 __all__ = ['Message', 'NamespaceMap', 'no_default', 'registerNamespaceAlias',
4 'OPENID_NS', 'BARE_NS', 'OPENID1_NS', 'OPENID2_NS', 'SREG_URI',
5 'IDENTIFIER_SELECT']
7 import copy
8 import warnings
9 import urllib
11 from openid import oidutil
12 from openid import kvform
13 try:
14 ElementTree = oidutil.importElementTree()
15 except ImportError:
16 # No elementtree found, so give up, but don't fail to import,
17 # since we have fallbacks.
18 ElementTree = None
20 # This doesn't REALLY belong here, but where is better?
21 IDENTIFIER_SELECT = 'http://specs.openid.net/auth/2.0/identifier_select'
23 # URI for Simple Registration extension, the only commonly deployed
24 # OpenID 1.x extension, and so a special case
25 SREG_URI = 'http://openid.net/sreg/1.0'
27 # The OpenID 1.X namespace URI
28 OPENID1_NS = 'http://openid.net/signon/1.0'
29 THE_OTHER_OPENID1_NS = 'http://openid.net/signon/1.1'
31 OPENID1_NAMESPACES = OPENID1_NS, THE_OTHER_OPENID1_NS
33 # The OpenID 2.0 namespace URI
34 OPENID2_NS = 'http://specs.openid.net/auth/2.0'
36 # The namespace consisting of pairs with keys that are prefixed with
37 # "openid." but not in another namespace.
38 NULL_NAMESPACE = oidutil.Symbol('Null namespace')
40 # The null namespace, when it is an allowed OpenID namespace
41 OPENID_NS = oidutil.Symbol('OpenID namespace')
43 # The top-level namespace, excluding all pairs with keys that start
44 # with "openid."
45 BARE_NS = oidutil.Symbol('Bare namespace')
47 # Limit, in bytes, of identity provider and return_to URLs, including
48 # response payload. See OpenID 1.1 specification, Appendix D.
49 OPENID1_URL_LIMIT = 2047
51 # All OpenID protocol fields. Used to check namespace aliases.
52 OPENID_PROTOCOL_FIELDS = [
53 'ns', 'mode', 'error', 'return_to', 'contact', 'reference',
54 'signed', 'assoc_type', 'session_type', 'dh_modulus', 'dh_gen',
55 'dh_consumer_public', 'claimed_id', 'identity', 'realm',
56 'invalidate_handle', 'op_endpoint', 'response_nonce', 'sig',
57 'assoc_handle', 'trust_root', 'openid',
60 class UndefinedOpenIDNamespace(ValueError):
61 """Raised if the generic OpenID namespace is accessed when there
62 is no OpenID namespace set for this message."""
64 class InvalidOpenIDNamespace(ValueError):
65 """Raised if openid.ns is not a recognized value.
67 For recognized values, see L{Message.allowed_openid_namespaces}
68 """
69 def __str__(self):
70 s = "Invalid OpenID Namespace"
71 if self.args:
72 s += " %r" % (self.args[0],)
73 return s
76 # Sentinel used for Message implementation to indicate that getArg
77 # should raise an exception instead of returning a default.
78 no_default = object()
80 # Global namespace / alias registration map. See
81 # registerNamespaceAlias.
82 registered_aliases = {}
84 class NamespaceAliasRegistrationError(Exception):
85 """
86 Raised when an alias or namespace URI has already been registered.
87 """
88 pass
90 def registerNamespaceAlias(namespace_uri, alias):
91 """
92 Registers a (namespace URI, alias) mapping in a global namespace
93 alias map. Raises NamespaceAliasRegistrationError if either the
94 namespace URI or alias has already been registered with a
95 different value. This function is required if you want to use a
96 namespace with an OpenID 1 message.
97 """
98 global registered_aliases
100 if registered_aliases.get(alias) == namespace_uri:
101 return
103 if namespace_uri in registered_aliases.values():
104 raise NamespaceAliasRegistrationError, \
105 'Namespace uri %r already registered' % (namespace_uri,)
107 if alias in registered_aliases:
108 raise NamespaceAliasRegistrationError, \
109 'Alias %r already registered' % (alias,)
111 registered_aliases[alias] = namespace_uri
113 class Message(object):
115 In the implementation of this object, None represents the global
116 namespace as well as a namespace with no key.
118 @cvar namespaces: A dictionary specifying specific
119 namespace-URI to alias mappings that should be used when
120 generating namespace aliases.
122 @ivar ns_args: two-level dictionary of the values in this message,
123 grouped by namespace URI. The first level is the namespace
124 URI.
127 allowed_openid_namespaces = [OPENID1_NS, THE_OTHER_OPENID1_NS, OPENID2_NS]
129 def __init__(self, openid_namespace=None):
130 """Create an empty Message.
132 @raises InvalidOpenIDNamespace: if openid_namespace is not in
133 L{Message.allowed_openid_namespaces}
135 self.args = {}
136 self.namespaces = NamespaceMap()
137 if openid_namespace is None:
138 self._openid_ns_uri = None
139 else:
140 implicit = openid_namespace in OPENID1_NAMESPACES
141 self.setOpenIDNamespace(openid_namespace, implicit)
143 def fromPostArgs(cls, args):
144 """Construct a Message containing a set of POST arguments.
147 self = cls()
149 # Partition into "openid." args and bare args
150 openid_args = {}
151 for key, value in args.items():
152 if isinstance(value, list):
153 raise TypeError("query dict must have one value for each key, "
154 "not lists of values. Query is %r" % (args,))
157 try:
158 prefix, rest = key.split('.', 1)
159 except ValueError:
160 prefix = None
162 if prefix != 'openid':
163 self.args[(BARE_NS, key)] = value
164 else:
165 openid_args[rest] = value
167 self._fromOpenIDArgs(openid_args)
169 return self
171 fromPostArgs = classmethod(fromPostArgs)
173 def fromOpenIDArgs(cls, openid_args):
174 """Construct a Message from a parsed KVForm message.
176 @raises InvalidOpenIDNamespace: if openid.ns is not in
177 L{Message.allowed_openid_namespaces}
179 self = cls()
180 self._fromOpenIDArgs(openid_args)
181 return self
183 fromOpenIDArgs = classmethod(fromOpenIDArgs)
185 def _fromOpenIDArgs(self, openid_args):
186 ns_args = []
188 # Resolve namespaces
189 for rest, value in openid_args.iteritems():
190 try:
191 ns_alias, ns_key = rest.split('.', 1)
192 except ValueError:
193 ns_alias = NULL_NAMESPACE
194 ns_key = rest
196 if ns_alias == 'ns':
197 self.namespaces.addAlias(value, ns_key)
198 elif ns_alias == NULL_NAMESPACE and ns_key == 'ns':
199 # null namespace
200 self.setOpenIDNamespace(value, False)
201 else:
202 ns_args.append((ns_alias, ns_key, value))
204 # Implicitly set an OpenID namespace definition (OpenID 1)
205 if not self.getOpenIDNamespace():
206 self.setOpenIDNamespace(OPENID1_NS, True)
208 # Actually put the pairs into the appropriate namespaces
209 for (ns_alias, ns_key, value) in ns_args:
210 ns_uri = self.namespaces.getNamespaceURI(ns_alias)
211 if ns_uri is None:
212 # we found a namespaced arg without a namespace URI defined
213 ns_uri = self._getDefaultNamespace(ns_alias)
214 if ns_uri is None:
215 ns_uri = self.getOpenIDNamespace()
216 ns_key = '%s.%s' % (ns_alias, ns_key)
217 else:
218 self.namespaces.addAlias(ns_uri, ns_alias, implicit=True)
220 self.setArg(ns_uri, ns_key, value)
222 def _getDefaultNamespace(self, mystery_alias):
223 """OpenID 1 compatibility: look for a default namespace URI to
224 use for this alias."""
225 global registered_aliases
226 # Only try to map an alias to a default if it's an
227 # OpenID 1.x message.
228 if self.isOpenID1():
229 return registered_aliases.get(mystery_alias)
230 else:
231 return None
233 def setOpenIDNamespace(self, openid_ns_uri, implicit):
234 """Set the OpenID namespace URI used in this message.
236 @raises InvalidOpenIDNamespace: if the namespace is not in
237 L{Message.allowed_openid_namespaces}
239 if openid_ns_uri not in self.allowed_openid_namespaces:
240 raise InvalidOpenIDNamespace(openid_ns_uri)
242 self.namespaces.addAlias(openid_ns_uri, NULL_NAMESPACE, implicit)
243 self._openid_ns_uri = openid_ns_uri
245 def getOpenIDNamespace(self):
246 return self._openid_ns_uri
248 def isOpenID1(self):
249 return self.getOpenIDNamespace() in OPENID1_NAMESPACES
251 def isOpenID2(self):
252 return self.getOpenIDNamespace() == OPENID2_NS
254 def fromKVForm(cls, kvform_string):
255 """Create a Message from a KVForm string"""
256 return cls.fromOpenIDArgs(kvform.kvToDict(kvform_string))
258 fromKVForm = classmethod(fromKVForm)
260 def copy(self):
261 return copy.deepcopy(self)
263 def toPostArgs(self):
264 """Return all arguments with openid. in front of namespaced arguments.
266 args = {}
268 # Add namespace definitions to the output
269 for ns_uri, alias in self.namespaces.iteritems():
270 if self.namespaces.isImplicit(ns_uri):
271 continue
272 if alias == NULL_NAMESPACE:
273 ns_key = 'openid.ns'
274 else:
275 ns_key = 'openid.ns.' + alias
276 args[ns_key] = ns_uri
278 for (ns_uri, ns_key), value in self.args.iteritems():
279 key = self.getKey(ns_uri, ns_key)
280 args[key] = value.encode('UTF-8')
282 return args
284 def toArgs(self):
285 """Return all namespaced arguments, failing if any
286 non-namespaced arguments exist."""
287 # FIXME - undocumented exception
288 post_args = self.toPostArgs()
289 kvargs = {}
290 for k, v in post_args.iteritems():
291 if not k.startswith('openid.'):
292 raise ValueError(
293 'This message can only be encoded as a POST, because it '
294 'contains arguments that are not prefixed with "openid."')
295 else:
296 kvargs[k[7:]] = v
298 return kvargs
300 def toFormMarkup(self, action_url, form_tag_attrs=None,
301 submit_text="Continue"):
302 """Generate HTML form markup that contains the values in this
303 message, to be HTTP POSTed as x-www-form-urlencoded UTF-8.
305 @param action_url: The URL to which the form will be POSTed
306 @type action_url: str
308 @param form_tag_attrs: Dictionary of attributes to be added to
309 the form tag. 'accept-charset' and 'enctype' have defaults
310 that can be overridden. If a value is supplied for
311 'action' or 'method', it will be replaced.
312 @type form_tag_attrs: {unicode: unicode}
314 @param submit_text: The text that will appear on the submit
315 button for this form.
316 @type submit_text: unicode
318 @returns: A string containing (X)HTML markup for a form that
319 encodes the values in this Message object.
320 @rtype: str or unicode
322 if ElementTree is None:
323 raise RuntimeError('This function requires ElementTree.')
325 assert action_url is not None
327 form = ElementTree.Element('form')
329 if form_tag_attrs:
330 for name, attr in form_tag_attrs.iteritems():
331 form.attrib[name] = attr
333 form.attrib['action'] = action_url
334 form.attrib['method'] = 'post'
335 form.attrib['accept-charset'] = 'UTF-8'
336 form.attrib['enctype'] = 'application/x-www-form-urlencoded'
338 for name, value in self.toPostArgs().iteritems():
339 attrs = {'type': 'hidden',
340 'name': name,
341 'value': value}
342 form.append(ElementTree.Element('input', attrs))
344 submit = ElementTree.Element(
345 'input', {'type':'submit', 'value':submit_text})
346 form.append(submit)
348 return ElementTree.tostring(form)
350 def toURL(self, base_url):
351 """Generate a GET URL with the parameters in this message
352 attached as query parameters."""
353 return oidutil.appendArgs(base_url, self.toPostArgs())
355 def toKVForm(self):
356 """Generate a KVForm string that contains the parameters in
357 this message. This will fail if the message contains arguments
358 outside of the 'openid.' prefix.
360 return kvform.dictToKV(self.toArgs())
362 def toURLEncoded(self):
363 """Generate an x-www-urlencoded string"""
364 args = self.toPostArgs().items()
365 args.sort()
366 return urllib.urlencode(args)
368 def _fixNS(self, namespace):
369 """Convert an input value into the internally used values of
370 this object
372 @param namespace: The string or constant to convert
373 @type namespace: str or unicode or BARE_NS or OPENID_NS
375 if namespace == OPENID_NS:
376 if self._openid_ns_uri is None:
377 raise UndefinedOpenIDNamespace('OpenID namespace not set')
378 else:
379 namespace = self._openid_ns_uri
381 if namespace != BARE_NS and type(namespace) not in [str, unicode]:
382 raise TypeError(
383 "Namespace must be BARE_NS, OPENID_NS or a string. got %r"
384 % (namespace,))
386 if namespace != BARE_NS and ':' not in namespace:
387 fmt = 'OpenID 2.0 namespace identifiers SHOULD be URIs. Got %r'
388 warnings.warn(fmt % (namespace,), DeprecationWarning)
390 if namespace == 'sreg':
391 fmt = 'Using %r instead of "sreg" as namespace'
392 warnings.warn(fmt % (SREG_URI,), DeprecationWarning,)
393 return SREG_URI
395 return namespace
397 def hasKey(self, namespace, ns_key):
398 namespace = self._fixNS(namespace)
399 return (namespace, ns_key) in self.args
401 def getKey(self, namespace, ns_key):
402 """Get the key for a particular namespaced argument"""
403 namespace = self._fixNS(namespace)
404 if namespace == BARE_NS:
405 return ns_key
407 ns_alias = self.namespaces.getAlias(namespace)
409 # No alias is defined, so no key can exist
410 if ns_alias is None:
411 return None
413 if ns_alias == NULL_NAMESPACE:
414 tail = ns_key
415 else:
416 tail = '%s.%s' % (ns_alias, ns_key)
418 return 'openid.' + tail
420 def getArg(self, namespace, key, default=None):
421 """Get a value for a namespaced key.
423 @param namespace: The namespace in the message for this key
424 @type namespace: str
426 @param key: The key to get within this namespace
427 @type key: str
429 @param default: The value to use if this key is absent from
430 this message. Using the special value
431 openid.message.no_default will result in this method
432 raising a KeyError instead of returning the default.
434 @rtype: str or the type of default
435 @raises KeyError: if default is no_default
436 @raises UndefinedOpenIDNamespace: if the message has not yet
437 had an OpenID namespace set
439 namespace = self._fixNS(namespace)
440 args_key = (namespace, key)
441 try:
442 return self.args[args_key]
443 except KeyError:
444 if default is no_default:
445 raise KeyError((namespace, key))
446 else:
447 return default
449 def getArgs(self, namespace):
450 """Get the arguments that are defined for this namespace URI
452 @returns: mapping from namespaced keys to values
453 @returntype: dict
455 namespace = self._fixNS(namespace)
456 return dict([
457 (ns_key, value)
458 for ((pair_ns, ns_key), value)
459 in self.args.iteritems()
460 if pair_ns == namespace
463 def updateArgs(self, namespace, updates):
464 """Set multiple key/value pairs in one call
466 @param updates: The values to set
467 @type updates: {unicode:unicode}
469 namespace = self._fixNS(namespace)
470 for k, v in updates.iteritems():
471 self.setArg(namespace, k, v)
473 def setArg(self, namespace, key, value):
474 """Set a single argument in this namespace"""
475 assert key is not None
476 assert value is not None
477 namespace = self._fixNS(namespace)
478 self.args[(namespace, key)] = value
479 if not (namespace is BARE_NS):
480 self.namespaces.add(namespace)
482 def delArg(self, namespace, key):
483 namespace = self._fixNS(namespace)
484 del self.args[(namespace, key)]
486 def __repr__(self):
487 return "<%s.%s %r>" % (self.__class__.__module__,
488 self.__class__.__name__,
489 self.args)
491 def __eq__(self, other):
492 return self.args == other.args
495 def __ne__(self, other):
496 return not (self == other)
499 def getAliasedArg(self, aliased_key, default=None):
500 if aliased_key == 'ns':
501 return self.getOpenIDNamespace()
503 if aliased_key.startswith('ns.'):
504 uri = self.namespaces.getNamespaceURI(aliased_key[3:])
505 if uri is None:
506 if default == no_default:
507 raise KeyError
508 else:
509 return default
510 else:
511 return uri
513 try:
514 alias, key = aliased_key.split('.', 1)
515 except ValueError:
516 # need more than x values to unpack
517 ns = None
518 else:
519 ns = self.namespaces.getNamespaceURI(alias)
521 if ns is None:
522 key = aliased_key
523 ns = self.getOpenIDNamespace()
525 return self.getArg(ns, key, default)
527 class NamespaceMap(object):
528 """Maintains a bijective map between namespace uris and aliases.
530 def __init__(self):
531 self.alias_to_namespace = {}
532 self.namespace_to_alias = {}
533 self.implicit_namespaces = []
535 def getAlias(self, namespace_uri):
536 return self.namespace_to_alias.get(namespace_uri)
538 def getNamespaceURI(self, alias):
539 return self.alias_to_namespace.get(alias)
541 def iterNamespaceURIs(self):
542 """Return an iterator over the namespace URIs"""
543 return iter(self.namespace_to_alias)
545 def iterAliases(self):
546 """Return an iterator over the aliases"""
547 return iter(self.alias_to_namespace)
549 def iteritems(self):
550 """Iterate over the mapping
552 @returns: iterator of (namespace_uri, alias)
554 return self.namespace_to_alias.iteritems()
556 def addAlias(self, namespace_uri, desired_alias, implicit=False):
557 """Add an alias from this namespace URI to the desired alias
559 # Check that desired_alias is not an openid protocol field as
560 # per the spec.
561 assert desired_alias not in OPENID_PROTOCOL_FIELDS, \
562 "%r is not an allowed namespace alias" % (desired_alias,)
564 # Check that desired_alias does not contain a period as per
565 # the spec.
566 if type(desired_alias) in [str, unicode]:
567 assert '.' not in desired_alias, \
568 "%r must not contain a dot" % (desired_alias,)
570 # Check that there is not a namespace already defined for
571 # the desired alias
572 current_namespace_uri = self.alias_to_namespace.get(desired_alias)
573 if (current_namespace_uri is not None
574 and current_namespace_uri != namespace_uri):
576 fmt = ('Cannot map %r to alias %r. '
577 '%r is already mapped to alias %r')
579 msg = fmt % (
580 namespace_uri,
581 desired_alias,
582 current_namespace_uri,
583 desired_alias)
584 raise KeyError(msg)
586 # Check that there is not already a (different) alias for
587 # this namespace URI
588 alias = self.namespace_to_alias.get(namespace_uri)
589 if alias is not None and alias != desired_alias:
590 fmt = ('Cannot map %r to alias %r. '
591 'It is already mapped to alias %r')
592 raise KeyError(fmt % (namespace_uri, desired_alias, alias))
594 assert (desired_alias == NULL_NAMESPACE or
595 type(desired_alias) in [str, unicode]), repr(desired_alias)
596 assert namespace_uri not in self.implicit_namespaces
597 self.alias_to_namespace[desired_alias] = namespace_uri
598 self.namespace_to_alias[namespace_uri] = desired_alias
599 if implicit:
600 self.implicit_namespaces.append(namespace_uri)
601 return desired_alias
603 def add(self, namespace_uri):
604 """Add this namespace URI to the mapping, without caring what
605 alias it ends up with"""
606 # See if this namespace is already mapped to an alias
607 alias = self.namespace_to_alias.get(namespace_uri)
608 if alias is not None:
609 return alias
611 # Fall back to generating a numerical alias
612 i = 0
613 while True:
614 alias = 'ext' + str(i)
615 try:
616 self.addAlias(namespace_uri, alias)
617 except KeyError:
618 i += 1
619 else:
620 return alias
622 assert False, "Not reached"
624 def isDefined(self, namespace_uri):
625 return namespace_uri in self.namespace_to_alias
627 def __contains__(self, namespace_uri):
628 return self.isDefined(namespace_uri)
630 def isImplicit(self, namespace_uri):
631 return namespace_uri in self.implicit_namespaces