From 181aea737d5bce9479795b58c29b5a92da3bd48b Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 5 Jun 2012 16:54:26 -0700 Subject: Add initial support for xml:lang for streams and stanza plugins. Remaining items are suitable default actions for language supporting interfaces. --- sleekxmpp/basexmpp.py | 11 +- sleekxmpp/clientxmpp.py | 11 +- sleekxmpp/plugins/xep_0033.py | 2 + sleekxmpp/plugins/xep_0092/version.py | 4 +- sleekxmpp/stanza/roster.py | 1 + sleekxmpp/test/sleektest.py | 8 ++ sleekxmpp/xmlstream/stanzabase.py | 260 ++++++++++++++++++++++++---------- sleekxmpp/xmlstream/tostring.py | 14 +- sleekxmpp/xmlstream/xmlstream.py | 9 ++ 9 files changed, 235 insertions(+), 85 deletions(-) (limited to 'sleekxmpp') diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 43ea6063..4f48f809 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -31,6 +31,7 @@ from sleekxmpp.xmlstream import XMLStream, JID from sleekxmpp.xmlstream import ET, register_stanza_plugin from sleekxmpp.xmlstream.matcher import MatchXPath from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.stanzabase import XML_NS from sleekxmpp.features import * from sleekxmpp.plugins import PluginManager, register_plugin, load_plugin @@ -180,6 +181,8 @@ class BaseXMPP(XMLStream): :param xml: The incoming stream's root element. """ self.stream_id = xml.get('id', '') + self.stream_version = xml.get('version', '') + self.peer_default_lang = xml.get('{%s}lang' % XML_NS, None) def process(self, *args, **kwargs): """Initialize plugins and begin processing the XML stream. @@ -272,7 +275,9 @@ class BaseXMPP(XMLStream): def Message(self, *args, **kwargs): """Create a Message stanza associated with this stream.""" - return Message(self, *args, **kwargs) + msg = Message(self, *args, **kwargs) + msg['lang'] = self.default_lang + return msg def Iq(self, *args, **kwargs): """Create an Iq stanza associated with this stream.""" @@ -280,7 +285,9 @@ class BaseXMPP(XMLStream): def Presence(self, *args, **kwargs): """Create a Presence stanza associated with this stream.""" - return Presence(self, *args, **kwargs) + pres = Presence(self, *args, **kwargs) + pres['lang'] = self.default_lang + return pres def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None): """Create a new Iq stanza with a given Id and from JID. diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 94ced031..8a941867 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -60,8 +60,8 @@ class ClientXMPP(BaseXMPP): :param escape_quotes: **Deprecated.** """ - def __init__(self, jid, password, ssl=False, plugin_config={}, - plugin_whitelist=[], escape_quotes=True, sasl_mech=None): + def __init__(self, jid, password, plugin_config={}, plugin_whitelist=[], + escape_quotes=True, sasl_mech=None, lang='en'): BaseXMPP.__init__(self, jid, 'jabber:client') self.set_jid(jid) @@ -69,15 +69,18 @@ class ClientXMPP(BaseXMPP): self.plugin_config = plugin_config self.plugin_whitelist = plugin_whitelist self.default_port = 5222 + self.default_lang = lang self.credentials = {} self.password = password - self.stream_header = "" % ( + self.stream_header = "" % ( self.boundjid.host, "xmlns:stream='%s'" % self.stream_ns, - "xmlns='%s'" % self.default_ns) + "xmlns='%s'" % self.default_ns, + "xml:lang='%s'" % self.default_lang, + "version='1.0'") self.stream_footer = "" self.features = set() diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py index feef5a13..9276b807 100644 --- a/sleekxmpp/plugins/xep_0033.py +++ b/sleekxmpp/plugins/xep_0033.py @@ -42,6 +42,8 @@ class Addresses(ElementBase): self.delAddresses(set_type) for addr in addresses: addr = dict(addr) + if 'lang' in addr: + del addr['lang'] # Remap 'type' to 'atype' to match the add method if set_type is not None: addr['type'] = set_type diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py index c6223c10..5e84b2ff 100644 --- a/sleekxmpp/plugins/xep_0092/version.py +++ b/sleekxmpp/plugins/xep_0092/version.py @@ -79,5 +79,7 @@ class XEP_0092(BasePlugin): result = iq.send() if result and result['type'] != 'error': - return result['software_version'].values + values = result['software_version'].values + del values['lang'] + return values return False diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py index 253c2aea..3fae42cc 100644 --- a/sleekxmpp/stanza/roster.py +++ b/sleekxmpp/stanza/roster.py @@ -102,6 +102,7 @@ class Roster(ElementBase): # Remove extra JID reference to keep everything # backward compatible del items[item['jid']]['jid'] + del items[item['jid']]['lang'] return items def del_items(self): diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py index ba79dce8..92a7688a 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -333,6 +333,9 @@ class SleekTest(unittest.TestCase): # Remove unique ID prefix to make it easier to test self.xmpp._id_prefix = '' self.xmpp._disconnect_wait_for_threads = False + self.xmpp.default_lang = None + self.xmpp.peer_default_lang = None + # We will use this to wait for the session_start event # for live connections. @@ -386,6 +389,7 @@ class SleekTest(unittest.TestCase): sid='', stream_ns="http://etherx.jabber.org/streams", default_ns="jabber:client", + default_lang="en", version="1.0", xml_header=True): """ @@ -413,6 +417,8 @@ class SleekTest(unittest.TestCase): parts.append('from="%s"' % sfrom) if sid: parts.append('id="%s"' % sid) + if default_lang: + parts.append('xml:lang="%s"' % default_lang) parts.append('version="%s"' % version) parts.append('xmlns:stream="%s"' % stream_ns) parts.append('xmlns="%s"' % default_ns) @@ -564,6 +570,7 @@ class SleekTest(unittest.TestCase): sid='', stream_ns="http://etherx.jabber.org/streams", default_ns="jabber:client", + default_lang="en", version="1.0", xml_header=False, timeout=1): @@ -585,6 +592,7 @@ class SleekTest(unittest.TestCase): header = self.make_header(sto, sfrom, sid, stream_ns=stream_ns, default_ns=default_ns, + default_lang=default_lang, version=version, xml_header=xml_header) sent_header = self.xmpp.socket.next_sent(timeout) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 5d572437..f06573bf 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. @@ -101,20 +106,26 @@ 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 lang is None: + res = filter(lambda sub: isinstance(sub, self._multistanza), parent) + else: + res = filter(lambda sub: isinstance(sub, self._multistanza) and sub['lang'] == 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] 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) + if lang is None: + res = filter(lambda sub: isinstance(sub, self._multistanza), parent) + else: + res = filter(lambda sub: isinstance(sub, self._multistanza) and sub['lang'] == lang, parent) for stanza in list(res): parent.iterables.remove(stanza) parent.xml.remove(stanza.xml) @@ -122,7 +133,8 @@ def multifactory(stanza, plugin_attrib): 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) @@ -289,14 +301,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 ````. #: #: .. 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 +378,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 +390,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 +401,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 @@ -406,10 +428,9 @@ class ElementBase(object): for child in self.xml.getchildren(): 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 +464,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 +472,60 @@ 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 _get_plugin(self, name, lang=None): + if lang is None: + lang = self.get_lang() + 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): + 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 and 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 @@ -493,7 +550,11 @@ class ElementBase(object): for interface in self.interfaces: values[interface] = self[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 +578,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: @@ -538,9 +604,8 @@ class ElementBase(object): self[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) + plugin.values = value return self def __getitem__(self, attrib): @@ -572,6 +637,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,18 +653,17 @@ 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) + name = self.plugin_overrides.get(get_method, None) + if name: + plugin = self._get_plugin(name, lang) + handler = getattr(plugin, get_method, None) if handler: - return 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) @@ -600,11 +673,10 @@ class ElementBase(object): 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.is_extension: + return plugin[full_attrib] + return plugin else: return '' @@ -640,25 +712,32 @@ 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) + name = self.plugin_overrides.get(set_method, None) + if name: + plugin = self._get_plugin(name, lang) + handler = getattr(plugin, set_method, None) if handler: - return handler(value) + 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) @@ -672,9 +751,8 @@ class ElementBase(object): 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) + plugin[full_attrib] = value return self def __delitem__(self, attrib): @@ -709,23 +787,31 @@ 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) + name = self.plugin_overrides.get(del_method, None) + if name: + plugin = self._get_plugin(attrib, lang) + handler = getattr(plugin, del_method, None) if handler: - return 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) @@ -734,15 +820,17 @@ class ElementBase(object): 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 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): @@ -903,7 +991,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 +1020,11 @@ 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: + if self._get_plugin(next_tag, lang).match(xpath[1:]): + return True + return False # Everything matched. return True @@ -995,7 +1084,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 +1164,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. diff --git a/sleekxmpp/xmlstream/tostring.py b/sleekxmpp/xmlstream/tostring.py index 8e729f79..379ea09a 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. diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 40ae38c9..14c13e72 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -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 = '' @@ -1431,6 +1434,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() @@ -1478,6 +1485,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): -- cgit v1.2.3