diff options
Diffstat (limited to 'sleekxmpp/xmlstream')
-rw-r--r-- | sleekxmpp/xmlstream/cert.py | 7 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/handler/base.py | 4 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/handler/callback.py | 2 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/matcher/xmlmask.py | 10 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/matcher/xpath.py | 4 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/scheduler.py | 4 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/stanzabase.py | 435 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/tostring.py | 16 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/xmlstream.py | 128 |
9 files changed, 424 insertions, 186 deletions
diff --git a/sleekxmpp/xmlstream/cert.py b/sleekxmpp/xmlstream/cert.py index b2711e8e..339f872d 100644 --- a/sleekxmpp/xmlstream/cert.py +++ b/sleekxmpp/xmlstream/cert.py @@ -7,11 +7,12 @@ try: from pyasn1.type.univ import Any, ObjectIdentifier, OctetString from pyasn1.type.char import BMPString, IA5String, UTF8String from pyasn1.type.useful import GeneralizedTime - from pyasn1_modules.rfc2459 import Certificate, DirectoryString, SubjectAltName, GeneralNames, GeneralName + from pyasn1_modules.rfc2459 import (Certificate, DirectoryString, + SubjectAltName, GeneralNames, + GeneralName) from pyasn1_modules.rfc2459 import id_ce_subjectAltName as SUBJECT_ALT_NAME from pyasn1_modules.rfc2459 import id_at_commonName as COMMON_NAME - XMPP_ADDR = ObjectIdentifier('1.3.6.1.5.5.7.8.5') SRV_NAME = ObjectIdentifier('1.3.6.1.5.5.7.8.7') @@ -149,7 +150,7 @@ def verify(expected, raw_cert): expected_wild = expected[expected.index('.'):] expected_srv = '_xmpp-client.%s' % expected - for name in cert_names['XMPPAddr']: + for name in cert_names['XMPPAddr']: if name == expected: return True for name in cert_names['SRV']: diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py index 59dcb306..01c1991a 100644 --- a/sleekxmpp/xmlstream/handler/base.py +++ b/sleekxmpp/xmlstream/handler/base.py @@ -49,7 +49,7 @@ class BaseHandler(object): def match(self, xml): """Compare a stanza or XML object with the handler's matcher. - :param xml: An XML or + :param xml: An XML or :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object """ return self._matcher.match(xml) @@ -73,7 +73,7 @@ class BaseHandler(object): self._payload = payload def check_delete(self): - """Check if the handler should be removed from the list + """Check if the handler should be removed from the list of stream handlers. """ return self._destroy diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py index 37f53335..7e3388f1 100644 --- a/sleekxmpp/xmlstream/handler/callback.py +++ b/sleekxmpp/xmlstream/handler/callback.py @@ -33,7 +33,7 @@ class Callback(BaseHandler): :param matcher: A :class:`~sleekxmpp.xmlstream.matcher.base.MatcherBase` derived object for matching stanza objects. :param pointer: The function to execute during callback. - :param bool thread: **DEPRECATED.** Remains only for + :param bool thread: **DEPRECATED.** Remains only for backwards compatibility. :param bool once: Indicates if the handler should be used only once. Defaults to False. diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index 7977e767..a0568f08 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -34,9 +34,9 @@ class MatchXMLMask(MatcherBase): <message xmlns="jabber:client"><body /></message> - Use of XMLMask is discouraged, and - :class:`~sleekxmpp.xmlstream.matcher.xpath.MatchXPath` or - :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` + Use of XMLMask is discouraged, and + :class:`~sleekxmpp.xmlstream.matcher.xpath.MatchXPath` or + :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` should be used instead. The use of namespaces in the mask comparison is controlled by @@ -151,8 +151,8 @@ class MatchXMLMask(MatcherBase): """ tag = tag.split('}')[-1] try: - children = [c.tag.split('}')[-1] for c in xml.getchildren()] + children = [c.tag.split('}')[-1] for c in xml] index = children.index(tag) except ValueError: return None - return xml.getchildren()[index] + return list(xml)[index] diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py index b6af0609..3f03e68e 100644 --- a/sleekxmpp/xmlstream/matcher/xpath.py +++ b/sleekxmpp/xmlstream/matcher/xpath.py @@ -77,10 +77,10 @@ class MatchXPath(MatcherBase): # Skip empty tag name artifacts from the cleanup phase. continue - children = [c.tag.split('}')[-1] for c in xml.getchildren()] + children = [c.tag.split('}')[-1] for c in xml] try: index = children.index(tag) except ValueError: return False - xml = xml.getchildren()[index] + xml = list(xml)[index] return True diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py index cf47c164..70e36f24 100644 --- a/sleekxmpp/xmlstream/scheduler.py +++ b/sleekxmpp/xmlstream/scheduler.py @@ -57,7 +57,7 @@ class Task(object): #: The keyword arguments to pass to :attr:`callback`. self.kwargs = kwargs or {} - + #: Indicates if the task should repeat after executing, #: using the same :attr:`seconds` delay. self.repeat = repeat @@ -103,7 +103,7 @@ class Scheduler(object): def __init__(self, parentstop=None): #: A queue for storing tasks self.addq = queue.Queue() - + #: A list of tasks in order of execution time. self.schedule = [] diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 5d572437..64e00626 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -12,6 +12,8 @@ :license: MIT, see LICENSE for more details """ +from __future__ import with_statement, unicode_literals + import copy import logging import weakref @@ -29,6 +31,9 @@ log = logging.getLogger(__name__) XML_TYPE = type(ET.Element('xml')) +XML_NS = 'http://www.w3.org/XML/1998/namespace' + + def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False): """ Associate a stanza object as a plugin for another stanza. @@ -40,7 +45,7 @@ def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False): substanzas for the parent, using ``parent['substanzas']``. If the attribute ``plugin_multi_attrib`` was defined for the plugin, then the substanza set can be filtered to only instances of the plugin - class. For example, given a plugin class ``Foo`` with + class. For example, given a plugin class ``Foo`` with ``plugin_multi_attrib = 'foos'`` then:: parent['foos'] @@ -94,6 +99,14 @@ def multifactory(stanza, plugin_attrib): """ Returns a ElementBase class for handling reoccuring child stanzas """ + + def plugin_filter(self): + return lambda x: isinstance(x, self._multistanza) + + def plugin_lang_filter(self, lang): + return lambda x: isinstance(x, self._multistanza) and \ + x['lang'] == lang + class Multi(ElementBase): """ Template class for multifactory @@ -101,28 +114,45 @@ def multifactory(stanza, plugin_attrib): def setup(self, xml=None): self.xml = ET.Element('') - def get_multi(self): + def get_multi(self, lang=None): parent = self.parent() - res = filter(lambda sub: isinstance(sub, self._multistanza), parent) + if not lang or lang == '*': + res = filter(plugin_filter(self), parent) + else: + res = filter(plugin_filter(self, lang), parent) return list(res) - def set_multi(self, val): + def set_multi(self, val, lang=None): parent = self.parent() - del parent[self.plugin_attrib] + del_multi = getattr(self, 'del_%s' % plugin_attrib) + del_multi(lang) for sub in val: parent.append(sub) - def del_multi(self): + def del_multi(self, lang=None): parent = self.parent() - res = filter(lambda sub: isinstance(sub, self._multistanza), parent) - for stanza in list(res): - parent.iterables.remove(stanza) - parent.xml.remove(stanza.xml) + if not lang or lang == '*': + res = filter(plugin_filter(self), parent) + else: + res = filter(plugin_filter(self, lang), parent) + res = list(res) + if not res: + del parent.plugins[(plugin_attrib, None)] + parent.loaded_plugins.remove(plugin_attrib) + try: + parent.xml.remove(self.xml) + except: + pass + else: + for stanza in list(res): + parent.iterables.remove(stanza) + parent.xml.remove(stanza.xml) Multi.is_extension = True Multi.plugin_attrib = plugin_attrib Multi._multistanza = stanza - Multi.interfaces = (plugin_attrib,) + Multi.interfaces = set([plugin_attrib]) + Multi.lang_interfaces = set([plugin_attrib]) setattr(Multi, "get_%s" % plugin_attrib, get_multi) setattr(Multi, "set_%s" % plugin_attrib, set_multi) setattr(Multi, "del_%s" % plugin_attrib, del_multi) @@ -231,8 +261,10 @@ class ElementBase(object): directly from the parent stanza, as shown below, but retrieving information will require all interfaces to be used, as so:: - >>> message['custom'] = 'bar' # Same as using message['custom']['custom'] - >>> message['custom']['custom'] # Must use all interfaces + >>> # Same as using message['custom']['custom'] + >>> message['custom'] = 'bar' + >>> # Must use all interfaces + >>> message['custom']['custom'] 'bar' If the plugin sets :attr:`is_extension` to ``True``, then both setting @@ -245,13 +277,13 @@ class ElementBase(object): :param xml: Initialize the stanza object with an existing XML object. - :param parent: Optionally specify a parent stanza object will will + :param parent: Optionally specify a parent stanza object will contain this substanza. """ #: The XML tag name of the element, not including any namespace - #: prefixes. For example, an :class:`ElementBase` object for ``<message />`` - #: would use ``name = 'message'``. + #: prefixes. For example, an :class:`ElementBase` object for + #: ``<message />`` would use ``name = 'message'``. name = 'stanza' #: The XML namespace for the element. Given ``<foo xmlns="bar" />``, @@ -289,14 +321,17 @@ class ElementBase(object): #: subelements of the underlying XML object. Using this set, the text #: of these subelements may be set, retrieved, or removed without #: needing to define custom methods. - sub_interfaces = tuple() + sub_interfaces = set() #: A subset of :attr:`interfaces` which maps the presence of #: subelements to boolean values. Using this set allows for quickly #: checking for the existence of empty subelements like ``<required />``. #: #: .. versionadded:: 1.1 - bool_interfaces = tuple() + bool_interfaces = set() + + #: .. versionadded:: 1.1.2 + lang_interfaces = set() #: In some cases you may wish to override the behaviour of one of the #: parent stanza's interfaces. The ``overrides`` list specifies the @@ -363,7 +398,7 @@ class ElementBase(object): subitem = set() #: The default XML namespace: ``http://www.w3.org/XML/1998/namespace``. - xml_ns = 'http://www.w3.org/XML/1998/namespace' + xml_ns = XML_NS def __init__(self, xml=None, parent=None): self._index = 0 @@ -375,6 +410,7 @@ class ElementBase(object): #: An ordered dictionary of plugin stanzas, mapped by their #: :attr:`plugin_attrib` value. self.plugins = OrderedDict() + self.loaded_plugins = set() #: A list of child stanzas whose class is included in #: :attr:`plugin_iterables`. @@ -385,6 +421,12 @@ class ElementBase(object): #: ``'{namespace}elementname'``. self.tag = self.tag_name() + if 'lang' not in self.interfaces: + if isinstance(self.interfaces, tuple): + self.interfaces += ('lang',) + else: + self.interfaces.add('lang') + #: A :class:`weakref.weakref` to the parent stanza, if there is one. #: If not, then :attr:`parent` is ``None``. self.parent = None @@ -403,13 +445,12 @@ class ElementBase(object): return # Initialize values using provided XML - for child in self.xml.getchildren(): + for child in self.xml: if child.tag in self.plugin_tag_map: plugin_class = self.plugin_tag_map[child.tag] - plugin = plugin_class(child, self) - self.plugins[plugin.plugin_attrib] = plugin - if plugin_class in self.plugin_iterables: - self.iterables.append(plugin) + self.init_plugin(plugin_class.plugin_attrib, + existing_xml=child, + reuse=False) def setup(self, xml=None): """Initialize the stanza's XML contents. @@ -443,7 +484,7 @@ class ElementBase(object): # We did not generate XML return False - def enable(self, attrib): + def enable(self, attrib, lang=None): """Enable and initialize a stanza plugin. Alias for :meth:`init_plugin`. @@ -451,24 +492,67 @@ class ElementBase(object): :param string attrib: The :attr:`plugin_attrib` value of the plugin to enable. """ - return self.init_plugin(attrib) + return self.init_plugin(attrib, lang) - def init_plugin(self, attrib): + def _get_plugin(self, name, lang=None): + if lang is None: + lang = self.get_lang() + + if name not in self.plugin_attrib_map: + return None + + plugin_class = self.plugin_attrib_map[name] + + if plugin_class.is_extension: + if (name, None) in self.plugins: + return self.plugins[(name, None)] + else: + return self.init_plugin(name, lang) + else: + if (name, lang) in self.plugins: + return self.plugins[(name, lang)] + else: + return self.init_plugin(name, lang) + + def init_plugin(self, attrib, lang=None, existing_xml=None, reuse=True): """Enable and initialize a stanza plugin. :param string attrib: The :attr:`plugin_attrib` value of the plugin to enable. """ - if attrib not in self.plugins: - plugin_class = self.plugin_attrib_map[attrib] + if lang is None: + lang = self.get_lang() + + plugin_class = self.plugin_attrib_map[attrib] + + if plugin_class.is_extension and (attrib, None) in self.plugins: + return self.plugins[(attrib, None)] + if reuse and (attrib, lang) in self.plugins: + return self.plugins[(attrib, lang)] + + if existing_xml is None: existing_xml = self.xml.find(plugin_class.tag_name()) - plugin = plugin_class(parent=self, xml=existing_xml) - self.plugins[attrib] = plugin - if plugin_class in self.plugin_iterables: - self.iterables.append(plugin) - if plugin_class.plugin_multi_attrib: - self.init_plugin(plugin_class.plugin_multi_attrib) - return self + + if existing_xml is not None: + if existing_xml.attrib.get('{%s}lang' % XML_NS, '') != lang: + existing_xml = None + + plugin = plugin_class(parent=self, xml=existing_xml) + + if plugin.is_extension: + self.plugins[(attrib, None)] = plugin + else: + plugin['lang'] = lang + self.plugins[(attrib, lang)] = plugin + + if plugin_class in self.plugin_iterables: + self.iterables.append(plugin) + if plugin_class.plugin_multi_attrib: + self.init_plugin(plugin_class.plugin_multi_attrib) + + self.loaded_plugins.add(attrib) + + return plugin def _get_stanza_values(self): """Return A JSON/dictionary version of the XML content @@ -492,8 +576,14 @@ class ElementBase(object): values = {} for interface in self.interfaces: values[interface] = self[interface] + if interface in self.lang_interfaces: + values['%s|*' % interface] = self['%s|*' % interface] for plugin, stanza in self.plugins.items(): - values[plugin] = stanza.values + lang = stanza['lang'] + if lang: + values['%s|%s' % (plugin, lang)] = stanza.values + else: + values[plugin[0]] = stanza.values if self.iterables: iterables = [] for stanza in self.iterables: @@ -517,6 +607,11 @@ class ElementBase(object): p in self.plugin_iterables] for interface, value in values.items(): + full_interface = interface + interface_lang = ('%s|' % interface).split('|') + interface = interface_lang[0] + lang = interface_lang[1] or self.get_lang() + if interface == 'substanzas': # Remove existing substanzas for stanza in self.iterables: @@ -535,12 +630,12 @@ class ElementBase(object): self.iterables.append(sub) break elif interface in self.interfaces: - self[interface] = value + self[full_interface] = value elif interface in self.plugin_attrib_map: if interface not in iterable_interfaces: - if interface not in self.plugins: - self.init_plugin(interface) - self.plugins[interface].values = value + plugin = self._get_plugin(interface, lang) + if plugin: + plugin.values = value return self def __getitem__(self, attrib): @@ -572,6 +667,15 @@ class ElementBase(object): :param string attrib: The name of the requested stanza interface. """ + full_attrib = attrib + attrib_lang = ('%s|' % attrib).split('|') + attrib = attrib_lang[0] + lang = attrib_lang[1] or '' + + kwargs = {} + if lang and attrib in self.lang_interfaces: + kwargs['lang'] = lang + if attrib == 'substanzas': return self.iterables elif attrib in self.interfaces: @@ -579,32 +683,31 @@ class ElementBase(object): get_method2 = "get%s" % attrib.title() if self.plugin_overrides: - plugin = self.plugin_overrides.get(get_method, None) - if plugin: - if plugin not in self.plugins: - self.init_plugin(plugin) - handler = getattr(self.plugins[plugin], get_method, None) - if handler: - return handler() + name = self.plugin_overrides.get(get_method, None) + if name: + plugin = self._get_plugin(name, lang) + if plugin: + handler = getattr(plugin, get_method, None) + if handler: + return handler(**kwargs) if hasattr(self, get_method): - return getattr(self, get_method)() + return getattr(self, get_method)(**kwargs) elif hasattr(self, get_method2): - return getattr(self, get_method2)() + return getattr(self, get_method2)(**kwargs) else: if attrib in self.sub_interfaces: - return self._get_sub_text(attrib) + return self._get_sub_text(attrib, lang=lang) elif attrib in self.bool_interfaces: elem = self.xml.find('{%s}%s' % (self.namespace, attrib)) return elem is not None else: return self._get_attr(attrib) elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: - self.init_plugin(attrib) - if self.plugins[attrib].is_extension: - return self.plugins[attrib][attrib] - return self.plugins[attrib] + plugin = self._get_plugin(attrib, lang) + if plugin and plugin.is_extension: + return plugin[full_attrib] + return plugin else: return '' @@ -640,41 +743,58 @@ class ElementBase(object): :param string attrib: The name of the stanza interface to modify. :param value: The new value of the stanza interface. """ + full_attrib = attrib + attrib_lang = ('%s|' % attrib).split('|') + attrib = attrib_lang[0] + lang = attrib_lang[1] or '' + + kwargs = {} + if lang and attrib in self.lang_interfaces: + kwargs['lang'] = lang + if attrib in self.interfaces: if value is not None: set_method = "set_%s" % attrib.lower() set_method2 = "set%s" % attrib.title() if self.plugin_overrides: - plugin = self.plugin_overrides.get(set_method, None) - if plugin: - if plugin not in self.plugins: - self.init_plugin(plugin) - handler = getattr(self.plugins[plugin], - set_method, None) - if handler: - return handler(value) + name = self.plugin_overrides.get(set_method, None) + if name: + plugin = self._get_plugin(name, lang) + if plugin: + handler = getattr(plugin, set_method, None) + if handler: + return handler(value, **kwargs) if hasattr(self, set_method): - getattr(self, set_method)(value,) + getattr(self, set_method)(value, **kwargs) elif hasattr(self, set_method2): - getattr(self, set_method2)(value,) + getattr(self, set_method2)(value, **kwargs) else: if attrib in self.sub_interfaces: - return self._set_sub_text(attrib, text=value) + if lang == '*': + return self._set_all_sub_text(attrib, + value, + lang='*') + return self._set_sub_text(attrib, text=value, + lang=lang) elif attrib in self.bool_interfaces: if value: - return self._set_sub_text(attrib, '', keep=True) + return self._set_sub_text(attrib, '', + keep=True, + lang=lang) else: - return self._set_sub_text(attrib, '', keep=False) + return self._set_sub_text(attrib, '', + keep=False, + lang=lang) else: self._set_attr(attrib, value) else: self.__delitem__(attrib) elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: - self.init_plugin(attrib) - self.plugins[attrib][attrib] = value + plugin = self._get_plugin(attrib, lang) + if plugin: + plugin[full_attrib] = value return self def __delitem__(self, attrib): @@ -709,40 +829,53 @@ class ElementBase(object): :param attrib: The name of the affected stanza interface. """ + full_attrib = attrib + attrib_lang = ('%s|' % attrib).split('|') + attrib = attrib_lang[0] + lang = attrib_lang[1] or '' + + kwargs = {} + if lang and attrib in self.lang_interfaces: + kwargs['lang'] = lang + if attrib in self.interfaces: del_method = "del_%s" % attrib.lower() del_method2 = "del%s" % attrib.title() if self.plugin_overrides: - plugin = self.plugin_overrides.get(del_method, None) - if plugin: - if plugin not in self.plugins: - self.init_plugin(plugin) - handler = getattr(self.plugins[plugin], del_method, None) - if handler: - return handler() + name = self.plugin_overrides.get(del_method, None) + if name: + plugin = self._get_plugin(attrib, lang) + if plugin: + handler = getattr(plugin, del_method, None) + if handler: + return handler(**kwargs) if hasattr(self, del_method): - getattr(self, del_method)() + getattr(self, del_method)(**kwargs) elif hasattr(self, del_method2): - getattr(self, del_method2)() + getattr(self, del_method2)(**kwargs) else: if attrib in self.sub_interfaces: - return self._del_sub(attrib) + return self._del_sub(attrib, lang=lang) elif attrib in self.bool_interfaces: - return self._del_sub(attrib) + return self._del_sub(attrib, lang=lang) else: self._del_attr(attrib) elif attrib in self.plugin_attrib_map: - if attrib in self.plugins: - xml = self.plugins[attrib].xml - if self.plugins[attrib].is_extension: - del self.plugins[attrib][attrib] - del self.plugins[attrib] - try: - self.xml.remove(xml) - except: - pass + plugin = self._get_plugin(attrib, lang) + if not plugin: + return self + if plugin.is_extension: + del plugin[full_attrib] + del self.plugins[(attrib, None)] + else: + del self.plugins[(attrib, lang)] + self.loaded_plugins.remove(attrib) + try: + self.xml.remove(plugin.xml) + except: + pass return self def _set_attr(self, name, value): @@ -781,7 +914,7 @@ class ElementBase(object): """ return self.xml.attrib.get(name, default) - def _get_sub_text(self, name, default=''): + def _get_sub_text(self, name, default='', lang=None): """Return the text contents of a sub element. In case the element does not exist, or it has no textual content, @@ -793,13 +926,38 @@ class ElementBase(object): not exists. An empty string is returned otherwise. """ name = self._fix_ns(name) - stanza = self.xml.find(name) - if stanza is None or stanza.text is None: + if lang == '*': + return self._get_all_sub_text(name, default, None) + + default_lang = self.get_lang() + if not lang: + lang = default_lang + + stanzas = self.xml.findall(name) + if not stanzas: return default - else: - return stanza.text + for stanza in stanzas: + if stanza.attrib.get('{%s}lang' % XML_NS, default_lang) == lang: + if stanza.text is None: + return default + return stanza.text + return default + + def _get_all_sub_text(self, name, default='', lang=None): + name = self._fix_ns(name) - def _set_sub_text(self, name, text=None, keep=False): + default_lang = self.get_lang() + results = OrderedDict() + stanzas = self.xml.findall(name) + if stanzas: + for stanza in stanzas: + stanza_lang = stanza.attrib.get('{%s}lang' % XML_NS, + default_lang) + if not lang or lang == '*' or stanza_lang == lang: + results[stanza_lang] = stanza.text + return results + + def _set_sub_text(self, name, text=None, keep=False, lang=None): """Set the text contents of a sub element. In case the element does not exist, a element will be created, @@ -817,9 +975,14 @@ class ElementBase(object): """ path = self._fix_ns(name, split=True) element = self.xml.find(name) + parent = self.xml + + default_lang = self.get_lang() + if lang is None: + lang = default_lang if not text and not keep: - return self._del_sub(name) + return self._del_sub(name, lang=lang) if element is None: # We need to add the element. If the provided name was @@ -833,14 +996,31 @@ class ElementBase(object): element = self.xml.find("/".join(walked)) if element is None: element = ET.Element(ename) + if lang: + element.attrib['{%s}lang' % XML_NS] = lang last_xml.append(element) + parent = last_xml last_xml = element element = last_xml + if element.attrib.get('{%s}lang' % XML_NS, default_lang) != lang: + element = ET.Element(ename) + if lang: + element.attrib['{%s}lang' % XML_NS] = lang + parent.append(element) + element.text = text return element - def _del_sub(self, name, all=False): + def _set_all_sub_text(self, name, values, keep=False, lang=None): + self._del_sub(name, lang) + for value_lang, value in values.items(): + if not lang or lang == '*' or value_lang == lang: + self._set_sub_text(name, text=value, + keep=keep, + lang=value_lang) + + def _del_sub(self, name, all=False, lang=None): """Remove sub elements that match the given name or XPath. If the element is in a path, then any parent elements that become @@ -854,6 +1034,10 @@ class ElementBase(object): path = self._fix_ns(name, split=True) original_target = path[-1] + default_lang = self.get_lang() + if not lang: + lang = default_lang + for level, _ in enumerate(path): # Generate the paths to the target elements and their parent. element_path = "/".join(path[:len(path) - level]) @@ -866,11 +1050,13 @@ class ElementBase(object): if parent is None: parent = self.xml for element in elements: - if element.tag == original_target or \ - not element.getchildren(): + if element.tag == original_target or not list(element): # Only delete the originally requested elements, and # any parent elements that have become empty. - parent.remove(element) + elem_lang = element.attrib.get('{%s}lang' % XML_NS, + default_lang) + if lang == '*' or elem_lang == lang: + parent.remove(element) if not all: # If we don't want to delete elements up the tree, stop # after deleting the first level of elements. @@ -903,7 +1089,7 @@ class ElementBase(object): attributes = components[1:] if tag not in (self.name, "{%s}%s" % (self.namespace, self.name)) and \ - tag not in self.plugins and tag not in self.plugin_attrib: + tag not in self.loaded_plugins and tag not in self.plugin_attrib: # The requested tag is not in this stanza, so no match. return False @@ -932,10 +1118,12 @@ class ElementBase(object): if not matched_substanzas and len(xpath) > 1: # Convert {namespace}tag@attribs to just tag next_tag = xpath[1].split('@')[0].split('}')[-1] - if next_tag in self.plugins: - return self.plugins[next_tag].match(xpath[1:]) - else: - return False + langs = [name[1] for name in self.plugins if name[0] == next_tag] + for lang in langs: + plugin = self._get_plugin(next_tag, lang) + if plugin and plugin.match(xpath[1:]): + return True + return False # Everything matched. return True @@ -995,7 +1183,7 @@ class ElementBase(object): """ out = [] out += [x for x in self.interfaces] - out += [x for x in self.plugins] + out += [x for x in self.loaded_plugins] if self.iterables: out.append('substanzas') return out @@ -1075,6 +1263,23 @@ class ElementBase(object): """ return "{%s}%s" % (cls.namespace, cls.name) + def get_lang(self): + result = self.xml.attrib.get('{%s}lang' % XML_NS, '') + if not result and self.parent and self.parent(): + return self.parent()['lang'] + return result + + def set_lang(self, lang): + self.del_lang() + attr = '{%s}lang' % XML_NS + if lang: + self.xml.attrib[attr] = lang + + def del_lang(self): + attr = '{%s}lang' % XML_NS + if attr in self.xml.attrib: + del self.xml.attrib[attr] + @property def attrib(self): """Return the stanza object itself. @@ -1090,8 +1295,8 @@ class ElementBase(object): return self def _fix_ns(self, xpath, split=False, propagate_ns=True): - return fix_ns(xpath, split=split, - propagate_ns=propagate_ns, + return fix_ns(xpath, split=split, + propagate_ns=propagate_ns, default_ns=self.namespace) def __eq__(self, other): @@ -1219,6 +1424,8 @@ class StanzaBase(ElementBase): :param sfrom: Optional string or :class:`sleekxmpp.xmlstream.JID` object of the sender's JID. :param string sid: Optional ID value for the stanza. + :param parent: Optionally specify a parent stanza object will + contain this substanza. """ #: The default XMPP client namespace @@ -1233,11 +1440,11 @@ class StanzaBase(ElementBase): types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) def __init__(self, stream=None, xml=None, stype=None, - sto=None, sfrom=None, sid=None): + sto=None, sfrom=None, sid=None, parent=None): self.stream = stream if stream is not None: self.namespace = stream.default_ns - ElementBase.__init__(self, xml) + ElementBase.__init__(self, xml, parent) if stype is not None: self['type'] = stype if sto is not None: @@ -1285,7 +1492,7 @@ class StanzaBase(ElementBase): def get_payload(self): """Return a list of XML objects contained in the stanza.""" - return self.xml.getchildren() + return list(self.xml) def set_payload(self, value): """Add XML content to the stanza. diff --git a/sleekxmpp/xmlstream/tostring.py b/sleekxmpp/xmlstream/tostring.py index 8e729f79..2480f9b2 100644 --- a/sleekxmpp/xmlstream/tostring.py +++ b/sleekxmpp/xmlstream/tostring.py @@ -13,14 +13,19 @@ :license: MIT, see LICENSE for more details """ +from __future__ import unicode_literals + import sys if sys.version_info < (3, 0): import types +XML_NS = 'http://www.w3.org/XML/1998/namespace' + + def tostring(xml=None, xmlns='', stanza_ns='', stream=None, - outbuffer='', top_level=False): + outbuffer='', top_level=False, open_only=False): """Serialize an XML object to a Unicode string. If namespaces are provided using ``xmlns`` or ``stanza_ns``, then @@ -88,6 +93,13 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, output.append(' %s:%s="%s"' % (mapped_ns, attrib, value)) + elif attrib_ns == XML_NS: + output.append(' xml:%s="%s"' % (attrib, value)) + + if open_only: + # Only output the opening tag, regardless of content. + output.append(">") + return ''.join(output) if len(xml) or xml.text: # If there are additional child elements to serialize. @@ -95,7 +107,7 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, if xml.text: output.append(xml_escape(xml.text)) if len(xml): - for child in xml.getchildren(): + for child in xml: output.append(tostring(child, tag_xmlns, stanza_ns, stream)) output.append("</%s>" % tag_name) elif xml.text: diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 40ae38c9..4e7b4050 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -55,7 +55,7 @@ RESPONSE_TIMEOUT = 30 WAIT_TIMEOUT = 0.1 #: The number of threads to use to handle XML stream events. This is not the -#: same as the number of custom event handling threads. +#: same as the number of custom event handling threads. #: :data:`HANDLER_THREADS` must be at least 1. For Python implementations #: with a GIL, this should be left at 1, but for implemetnations without #: a GIL increasing this value can provide better performance. @@ -124,7 +124,7 @@ class XMLStream(object): self.ssl_support = SSL_SUPPORT #: Most XMPP servers support TLSv1, but OpenFire in particular - #: does not work well with it. For OpenFire, set + #: does not work well with it. For OpenFire, set #: :attr:`ssl_version` to use ``SSLv23``:: #: #: import ssl @@ -134,30 +134,30 @@ class XMLStream(object): #: Path to a file containing certificates for verifying the #: server SSL certificate. A non-``None`` value will trigger #: certificate checking. - #: + #: #: .. note:: #: #: On Mac OS X, certificates in the system keyring will #: be consulted, even if they are not in the provided file. self.ca_certs = None - #: The time in seconds to wait for events from the event queue, + #: The time in seconds to wait for events from the event queue, #: and also the time between checks for the process stop signal. self.wait_timeout = WAIT_TIMEOUT - #: The time in seconds to wait before timing out waiting + #: The time in seconds to wait before timing out waiting #: for response stanzas. self.response_timeout = RESPONSE_TIMEOUT - #: The current amount to time to delay attempting to reconnect. + #: The current amount to time to delay attempting to reconnect. #: This value doubles (with some jitter) with each failed #: connection attempt up to :attr:`reconnect_max_delay` seconds. self.reconnect_delay = None - + #: Maximum time to delay between connection attempts is one hour. self.reconnect_max_delay = RECONNECT_MAX_DELAY - #: Maximum number of attempts to connect to the server before + #: Maximum number of attempts to connect to the server before #: quitting and raising a 'connect_failed' event. Setting to #: ``None`` allows infinite reattempts, while setting it to ``0`` #: will disable reconnection attempts. Defaults to ``None``. @@ -178,16 +178,16 @@ class XMLStream(object): #: The default port to return when querying DNS records. self.default_port = int(port) - - #: The domain to try when querying DNS records. + + #: The domain to try when querying DNS records. self.default_domain = '' #: The expected name of the server, for validation. self._expected_server_name = '' - + #: The desired, or actual, address of the connected server. self.address = (host, int(port)) - + #: A file-like wrapper for the socket for use with the #: :mod:`~xml.etree.ElementTree` module. self.filesocket = None @@ -223,6 +223,9 @@ class XMLStream(object): #: stream wrapper itself. self.default_ns = '' + self.default_lang = None + self.peer_default_lang = None + #: The namespace of the enveloping stream element. self.stream_ns = '' @@ -255,7 +258,7 @@ class XMLStream(object): #: and data is sent immediately over the wire. self.session_started_event = threading.Event() - #: The default time in seconds to wait for a session to start + #: The default time in seconds to wait for a session to start #: after connecting before reconnecting and trying again. self.session_timeout = 45 @@ -414,12 +417,12 @@ class XMLStream(object): if use_tls is not None: self.use_tls = use_tls - # Repeatedly attempt to connect until a successful connection # is established. attempts = self.reconnect_max_attempts connected = self.state.transition('disconnected', 'connected', - func=self._connect, args=(reattempt,)) + func=self._connect, + args=(reattempt,)) while reattempt and not connected and not self.stop.is_set(): connected = self.state.transition('disconnected', 'connected', func=self._connect) @@ -434,7 +437,7 @@ class XMLStream(object): def _connect(self, reattempt=True): self.scheduler.remove('Session timeout check') self.stop.clear() - + if self.reconnect_delay is None or not reattempt: delay = 1.0 else: @@ -480,7 +483,7 @@ class XMLStream(object): if self.use_proxy: connected = self._connect_proxy() if not connected: - if reattempt: + if reattempt: self.reconnect_delay = delay return False @@ -517,7 +520,8 @@ class XMLStream(object): except (Socket.error, ssl.SSLError): log.error('CERT: Invalid certificate trust chain.') if not self.event_handled('ssl_invalid_chain'): - self.disconnect(self.auto_reconnect, send_close=False) + self.disconnect(self.auto_reconnect, + send_close=False) else: self.event('ssl_invalid_chain', direct=True) return False @@ -525,7 +529,7 @@ class XMLStream(object): self._der_cert = self.socket.getpeercert(binary_form=True) pem_cert = ssl.DER_cert_to_PEM_cert(self._der_cert) log.debug('CERT: %s', pem_cert) - + self.event('ssl_cert', pem_cert, direct=True) try: cert.verify(self._expected_server_name, self._der_cert) @@ -534,7 +538,9 @@ class XMLStream(object): if not self.event_handled('ssl_invalid_cert'): self.disconnect(send_close=False) else: - self.event('ssl_invalid_cert', pem_cert, direct=True) + self.event('ssl_invalid_cert', + pem_cert, + direct=True) self.set_socket(self.socket, ignore=True) #this event is where you should set your application state @@ -627,7 +633,7 @@ class XMLStream(object): If the disconnect should take place after all items in the send queue have been sent, use ``wait=True``. - + .. warning:: If you are constantly adding items to the queue @@ -648,7 +654,7 @@ class XMLStream(object): """ self.state.transition('connected', 'disconnected', wait=2.0, - func=self._disconnect, + func=self._disconnect, args=(reconnect, wait, send_close)) def _disconnect(self, reconnect=False, wait=None, send_close=True): @@ -702,16 +708,18 @@ class XMLStream(object): """Reset the stream's state and reconnect to the server.""" log.debug("reconnecting...") if self.state.ensure('connected'): - self.state.transition('connected', 'disconnected', + self.state.transition('connected', 'disconnected', wait=2.0, - func=self._disconnect, + func=self._disconnect, args=(True, wait, send_close)) attempts = self.reconnect_max_attempts log.debug("connecting...") connected = self.state.transition('disconnected', 'connected', - wait=2.0, func=self._connect, args=(reattempt,)) + wait=2.0, + func=self._connect, + args=(reattempt,)) while reattempt and not connected and not self.stop.is_set(): connected = self.state.transition('disconnected', 'connected', wait=2.0, func=self._connect) @@ -759,8 +767,8 @@ class XMLStream(object): """ Configure and set options for a :class:`~dns.resolver.Resolver` instance, and other DNS related tasks. For example, you - can also check :meth:`~socket.socket.getaddrinfo` to see - if you need to call out to ``libresolv.so.2`` to + can also check :meth:`~socket.socket.getaddrinfo` to see + if you need to call out to ``libresolv.so.2`` to run ``res_init()``. Meant to be overridden. @@ -814,7 +822,7 @@ class XMLStream(object): log.debug('CERT: %s', pem_cert) self.event('ssl_cert', pem_cert, direct=True) - try: + try: cert.verify(self._expected_server_name, self._der_cert) except cert.CertificateError as err: log.error(err.message) @@ -874,8 +882,8 @@ class XMLStream(object): self.schedule('Whitespace Keepalive', self.whitespace_keepalive_interval, self.send_raw, - args = (' ',), - kwargs = {'now': True}, + args=(' ',), + kwargs={'now': True}, repeat=True) def _remove_schedules(self, event): @@ -884,7 +892,7 @@ class XMLStream(object): self.scheduler.remove('Certificate Expiration') def start_stream_handler(self, xml): - """Perform any initialization actions, such as handshakes, + """Perform any initialization actions, such as handshakes, once the stream header has been sent. Meant to be overridden. @@ -892,8 +900,8 @@ class XMLStream(object): pass def register_stanza(self, stanza_class): - """Add a stanza object class as a known root stanza. - + """Add a stanza object class as a known root stanza. + A root stanza is one that appears as a direct child of the stream's root element. @@ -910,8 +918,8 @@ class XMLStream(object): self.__root_stanza.append(stanza_class) def remove_stanza(self, stanza_class): - """Remove a stanza from being a known root stanza. - + """Remove a stanza from being a known root stanza. + A root stanza is one that appears as a direct child of the stream's root element. @@ -976,8 +984,9 @@ class XMLStream(object): """Add a stream event handler that will be executed when a matching stanza is received. - :param handler: The :class:`~sleekxmpp.xmlstream.handler.base.BaseHandler` - derived object to execute. + :param handler: + The :class:`~sleekxmpp.xmlstream.handler.base.BaseHandler` + derived object to execute. """ if handler.stream is None: self.__handlers.append(handler) @@ -1004,11 +1013,12 @@ class XMLStream(object): """ if port is None: port = self.default_port - + resolver = default_resolver() self.configure_dns(resolver, domain=domain, port=port) - return resolve(domain, port, service=self.dns_service, resolver=resolver) + return resolve(domain, port, service=self.dns_service, + resolver=resolver) def pick_dns_answer(self, domain, port=None): """Pick a server and port from DNS answers. @@ -1026,7 +1036,7 @@ class XMLStream(object): return self.dns_answers.next() else: return next(self.dns_answers) - + def add_event_handler(self, name, pointer, threaded=False, disposable=False): """Add a custom event handler that will be executed whenever @@ -1141,9 +1151,9 @@ class XMLStream(object): May optionally block until an expected response is received. - :param data: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + :param data: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` stanza to send on the stream. - :param mask: **DEPRECATED** + :param mask: **DEPRECATED** An XML string snippet matching the structure of the expected response. Execution will block in this thread until the response is received @@ -1195,9 +1205,9 @@ class XMLStream(object): """Send an XML object on the stream, and optionally wait for a response. - :param data: The :class:`~xml.etree.ElementTree.Element` XML object + :param data: The :class:`~xml.etree.ElementTree.Element` XML object to send on the stream. - :param mask: **DEPRECATED** + :param mask: **DEPRECATED** An XML string snippet matching the structure of the expected response. Execution will block in this thread until the response is received @@ -1237,14 +1247,15 @@ class XMLStream(object): count += 1 except ssl.SSLError as serr: if tries >= self.ssl_retry_max: - log.debug('SSL error - max retries reached') + log.debug('SSL error: max retries reached') self.exception(serr) log.warning("Failed to send %s", data) if reconnect is None: reconnect = self.auto_reconnect if not self.stop.is_set(): - self.disconnect(reconnect, send_close=False) - log.warning('SSL write error - reattempting') + self.disconnect(reconnect, + send_close=False) + log.warning('SSL write error: retrying') if not self.stop.is_set(): time.sleep(self.ssl_retry_delay) tries += 1 @@ -1299,7 +1310,7 @@ class XMLStream(object): def _wait_for_threads(self): with self.__thread_cond: if self.__thread_count != 0: - log.debug("Waiting for %s threads to exit." % + log.debug("Waiting for %s threads to exit." % self.__thread_count) name = threading.current_thread().name if name in self.__thread: @@ -1331,7 +1342,7 @@ class XMLStream(object): Defaults to ``True``. This does **not** mean that no threads are used at all if ``threaded=False``. - Regardless of these threading options, these threads will + Regardless of these threading options, these threads will always exist: - The event queue processor @@ -1421,7 +1432,7 @@ class XMLStream(object): def __read_xml(self): """Parse the incoming XML stream - + Stream events are raised for each received stanza. """ depth = 0 @@ -1431,6 +1442,10 @@ class XMLStream(object): if depth == 0: # We have received the start of the root element. root = xml + log.debug('RECV: %s', tostring(root, xmlns=self.default_ns, + stream=self, + top_level=True, + open_only=True)) # Perform any stream initialization actions, such # as handshakes. self.stream_end_event.clear() @@ -1461,10 +1476,10 @@ class XMLStream(object): """Create a stanza object from a given XML object. If a specialized stanza type is not found for the XML, then - a generic :class:`~sleekxmpp.xmlstream.stanzabase.StanzaBase` + a generic :class:`~sleekxmpp.xmlstream.stanzabase.StanzaBase` stanza will be returned. - :param xml: The :class:`~xml.etree.ElementTree.Element` XML object + :param xml: The :class:`~xml.etree.ElementTree.Element` XML object to convert into a stanza object. :param default_ns: Optional default namespace to use instead of the stream's current default namespace. @@ -1478,6 +1493,8 @@ class XMLStream(object): stanza_type = stanza_class break stanza = stanza_type(self, xml) + if stanza['lang'] is None and self.peer_default_lang: + stanza['lang'] = self.peer_default_lang return stanza def __spawn_event(self, xml): @@ -1647,12 +1664,13 @@ class XMLStream(object): count += 1 except ssl.SSLError as serr: if tries >= self.ssl_retry_max: - log.debug('SSL error - max retries reached') + log.debug('SSL error: max retries reached') self.exception(serr) log.warning("Failed to send %s", data) if not self.stop.is_set(): - self.disconnect(self.auto_reconnect, send_close=False) - log.warning('SSL write error - reattempting') + self.disconnect(self.auto_reconnect, + send_close=False) + log.warning('SSL write error: retrying') if not self.stop.is_set(): time.sleep(self.ssl_retry_delay) tries += 1 |