summaryrefslogtreecommitdiff
path: root/sleekxmpp/xmlstream
diff options
context:
space:
mode:
Diffstat (limited to 'sleekxmpp/xmlstream')
-rw-r--r--sleekxmpp/xmlstream/cert.py7
-rw-r--r--sleekxmpp/xmlstream/handler/base.py4
-rw-r--r--sleekxmpp/xmlstream/handler/callback.py2
-rw-r--r--sleekxmpp/xmlstream/matcher/xmlmask.py10
-rw-r--r--sleekxmpp/xmlstream/matcher/xpath.py4
-rw-r--r--sleekxmpp/xmlstream/scheduler.py4
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py435
-rw-r--r--sleekxmpp/xmlstream/tostring.py16
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py128
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