diff options
Diffstat (limited to 'sleekxmpp/xmlstream')
24 files changed, 1069 insertions, 603 deletions
diff --git a/sleekxmpp/xmlstream/__init__.py b/sleekxmpp/xmlstream/__init__.py index e69de29b..c82ab346 100644 --- a/sleekxmpp/xmlstream/__init__.py +++ b/sleekxmpp/xmlstream/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream.jid import JID +from sleekxmpp.xmlstream.stanzabase import StanzaBase, ElementBase +from sleekxmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py index f60c5b8e..07b395dc 100644 --- a/sleekxmpp/xmlstream/filesocket.py +++ b/sleekxmpp/xmlstream/filesocket.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from socket import _fileobject import socket diff --git a/sleekxmpp/xmlstream/handler/__init__.py b/sleekxmpp/xmlstream/handler/__init__.py index e69de29b..50e286a3 100644 --- a/sleekxmpp/xmlstream/handler/__init__.py +++ b/sleekxmpp/xmlstream/handler/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.handler.waiter import Waiter +from sleekxmpp.xmlstream.handler.xmlcallback import XMLCallback +from sleekxmpp.xmlstream.handler.xmlwaiter import XMLWaiter diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py index 5d55f4ee..720846d6 100644 --- a/sleekxmpp/xmlstream/handler/base.py +++ b/sleekxmpp/xmlstream/handler/base.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ class BaseHandler(object): diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py index 49cfa14d..889b0aa7 100644 --- a/sleekxmpp/xmlstream/handler/callback.py +++ b/sleekxmpp/xmlstream/handler/callback.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base import logging diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py index c85a0c46..7c4330a4 100644 --- a/sleekxmpp/xmlstream/handler/waiter.py +++ b/sleekxmpp/xmlstream/handler/waiter.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base try: diff --git a/sleekxmpp/xmlstream/handler/xmlcallback.py b/sleekxmpp/xmlstream/handler/xmlcallback.py index 632c142b..67879dfe 100644 --- a/sleekxmpp/xmlstream/handler/xmlcallback.py +++ b/sleekxmpp/xmlstream/handler/xmlcallback.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ import threading from . callback import Callback diff --git a/sleekxmpp/xmlstream/handler/xmlwaiter.py b/sleekxmpp/xmlstream/handler/xmlwaiter.py index 2344403b..cf90751d 100644 --- a/sleekxmpp/xmlstream/handler/xmlwaiter.py +++ b/sleekxmpp/xmlstream/handler/xmlwaiter.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . waiter import Waiter diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py new file mode 100644 index 00000000..292abd92 --- /dev/null +++ b/sleekxmpp/xmlstream/jid.py @@ -0,0 +1,121 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +class JID(object): + """ + A representation of a Jabber ID, or JID. + + Each JID may have three components: a user, a domain, and an optional + resource. For example: user@domain/resource + + When a resource is not used, the JID is called a bare JID. + The JID is a full JID otherwise. + + Attributes: + jid -- Alias for 'full'. + full -- The value of the full JID. + bare -- The value of the bare JID. + user -- The username portion of the JID. + domain -- The domain name portion of the JID. + server -- Alias for 'domain'. + resource -- The resource portion of the JID. + + Methods: + reset -- Use a new JID value. + regenerate -- Recreate the JID from its components. + """ + + def __init__(self, jid): + """Initialize a new JID""" + self.reset(jid) + + def reset(self, jid): + """ + Start fresh from a new JID string. + + Arguments: + jid - The new JID value. + """ + self._full = self._jid = str(jid) + self._domain = None + self._resource = None + self._user = None + self._bare = None + + def __getattr__(self, name): + """ + Handle getting the JID values, using cache if available. + + Arguments: + name -- One of: user, server, domain, resource, + full, or bare. + """ + if name == 'resource': + if self._resource is None: + self._resource = self._jid.split('/', 1)[-1] + return self._resource + elif name == 'user': + if self._user is None: + if '@' in self._jid: + self._user = self._jid.split('@', 1)[0] + else: + self._user = self._user + return self._user + elif name in ('server', 'domain'): + if self._domain is None: + self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0] + return self._domain + elif name == 'full': + return self._jid + elif name == 'bare': + if self._bare is None: + self._bare = self._jid.split('/', 1)[0] + return self._bare + + def __setattr__(self, name, value): + """ + Edit a JID by updating it's individual values, resetting the + generated JID in the end. + + Arguments: + name -- The name of the JID part. One of: user, domain, + server, resource, full, jid, or bare. + value -- The new value for the JID part. + """ + if name in ('resource', 'user', 'domain'): + object.__setattr__(self, "_%s" % name, value) + self.regenerate() + elif name == 'server': + self.domain = value + elif name in ('full', 'jid'): + self.reset(value) + elif name == 'bare': + if '@' in value: + u, d = value.split('@', 1) + object.__setattr__(self, "_user", u) + object.__setattr__(self, "_domain", d) + else: + object.__setattr__(self, "_domain", value) + self.regenerate() + else: + object.__setattr__(self, name, value) + + def regenerate(self): + """Generate a new JID based on current values, useful after editing.""" + jid = "" + if self.user: + jid = "%s@" % self.user + jid += self.domain + if self.resource: + jid += "/%s" % self.resource + self.reset(jid) + + def __str__(self): + """Use the full JID as the string value.""" + return self.full diff --git a/sleekxmpp/xmlstream/matcher/__init__.py b/sleekxmpp/xmlstream/matcher/__init__.py index e69de29b..91cb8d6e 100644 --- a/sleekxmpp/xmlstream/matcher/__init__.py +++ b/sleekxmpp/xmlstream/matcher/__init__.py @@ -0,0 +1,13 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream.matcher.id import MatcherId +from sleekxmpp.xmlstream.matcher.many import MatchMany +from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath +from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask +from sleekxmpp.xmlstream.matcher.xpath import MatchXPath diff --git a/sleekxmpp/xmlstream/matcher/base.py b/sleekxmpp/xmlstream/matcher/base.py index 8185bdc5..51da0942 100644 --- a/sleekxmpp/xmlstream/matcher/base.py +++ b/sleekxmpp/xmlstream/matcher/base.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ class MatcherBase(object): diff --git a/sleekxmpp/xmlstream/matcher/id.py b/sleekxmpp/xmlstream/matcher/id.py index bb858fc4..43972c23 100644 --- a/sleekxmpp/xmlstream/matcher/id.py +++ b/sleekxmpp/xmlstream/matcher/id.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base diff --git a/sleekxmpp/xmlstream/matcher/many.py b/sleekxmpp/xmlstream/matcher/many.py index cf860e62..ff0c4e4d 100644 --- a/sleekxmpp/xmlstream/matcher/many.py +++ b/sleekxmpp/xmlstream/matcher/many.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base from xml.etree import cElementTree diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py index bd091c0e..e315445d 100644 --- a/sleekxmpp/xmlstream/matcher/stanzapath.py +++ b/sleekxmpp/xmlstream/matcher/stanzapath.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base from xml.etree import cElementTree diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index eba3e954..89fd6422 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base from xml.etree import cElementTree diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py index f6d04243..7f3d20be 100644 --- a/sleekxmpp/xmlstream/matcher/xpath.py +++ b/sleekxmpp/xmlstream/matcher/xpath.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base from xml.etree import cElementTree diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 64020c8f..83a8ddfc 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -3,386 +3,516 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ -from xml.etree import cElementTree as ET + +import copy import logging -import traceback import sys import weakref +from xml.etree import cElementTree as ET + +from sleekxmpp.xmlstream import JID +from sleekxmpp.xmlstream.tostring import tostring + + +# Used to check if an argument is an XML object. +XML_TYPE = type(ET.Element('xml')) + + +def registerStanzaPlugin(stanza, plugin): + """ + Associate a stanza object as a plugin for another stanza. + + Arguments: + stanza -- The class of the parent stanza. + plugin -- The class of the plugin stanza. + """ + tag = "{%s}%s" % (plugin.namespace, plugin.name) + stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin + stanza.plugin_tag_map[tag] = plugin + + +class ElementBase(object): + name = 'stanza' + plugin_attrib = 'plugin' + namespace = 'jabber:client' + interfaces = set(('type', 'to', 'from', 'id', 'payload')) + types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) + sub_interfaces = tuple() + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = None + + def __init__(self, xml=None, parent=None): + """ + Create a new stanza object. + + Arguments: + xml -- Initialize the stanza with optional existing XML. + parent -- Optional stanza object that contains this stanza. + """ + self.xml = xml + self.plugins = {} + self.iterables = [] + self.idx = 0 + if parent is None: + self.parent = None + else: + self.parent = weakref.ref(parent) + + if self.setup(xml): + # If we generated our own XML, then everything is ready. + return + + # Initialize values using provided XML + for child in self.xml.getchildren(): + if child.tag in self.plugin_tag_map: + plugin = self.plugin_tag_map[child.tag] + self.plugins[plugin.plugin_attrib] = plugin(child, self) + if self.subitem is not None: + for sub in self.subitem: + if child.tag == "{%s}%s" % (sub.namespace, sub.name): + self.iterables.append(sub(child, self)) + break + + def setup(self, xml=None): + """ + Initialize the stanza's XML contents. + + Will return True if XML was generated according to the stanza's + definition. + + Arguments: + xml -- Optional XML object to use for the stanza's content + instead of generating XML. + """ + if self.xml is None: + self.xml = xml + + if self.xml is None: + # Generate XML from the stanza definition + for ename in self.name.split('/'): + new = ET.Element("{%s}%s" % (self.namespace, ename)) + if self.xml is None: + self.xml = new + else: + last_xml.append(new) + last_xml = new + if self.parent is not None: + self.parent().xml.append(self.xml) + + # We had to generate XML + return True + else: + # We did not generate XML + return False + + def enable(self, attrib): + """ + Enable and initialize a stanza plugin. + + Alias for initPlugin. + + Arguments: + attrib -- The stanza interface for the plugin. + """ + return self.initPlugin(attrib) + + def initPlugin(self, attrib): + """ + Enable and initialize a stanza plugin. + + Arguments: + attrib -- The stanza interface for the plugin. + """ + if attrib not in self.plugins: + plugin_class = self.plugin_attrib_map[attrib] + self.plugins[attrib] = plugin_class(parent=self) + return self + + def getStanzaValues(self): + """ + Return a dictionary of the stanza's interface values. + + Stanza plugin values are included as nested dictionaries. + """ + values = {} + for interface in self.interfaces: + values[interface] = self[interface] + for plugin, stanza in self.plugins.items(): + values[plugin] = stanza.getStanzaValues() + if self.iterables: + iterables = [] + for stanza in self.iterables: + iterables.append(stanza.getStanzaValues()) + iterables[-1].update({ + '__childtag__': "{%s}%s" % (stanza.namespace, stanza.name) + }) + values['substanzas'] = iterables + return values + + def setStanzaValues(self, values): + """ + Set multiple stanza interface values using a dictionary. + + Stanza plugin values may be set using nested dictionaries. + + Arguments: + values -- A dictionary mapping stanza interface with values. + Plugin interfaces may accept a nested dictionary that + will be used recursively. + """ + for interface, value in values.items(): + if interface == 'substanzas': + for subdict in value: + if '__childtag__' in subdict: + for subclass in self.subitem: + child_tag = "{%s}%s" % (subclass.namespace, + subclass.name) + if subdict['__childtag__'] == child_tag: + sub = subclass(parent=self) + sub.setStanzaValues(subdict) + self.iterables.append(sub) + break + elif interface in self.interfaces: + self[interface] = value + elif interface in self.plugin_attrib_map: + if interface not in self.plugins: + self.initPlugin(interface) + self.plugins[interface].setStanzaValues(value) + return self + + def __getitem__(self, attrib): + """ + Return the value of a stanza interface using dictionary-like syntax. + + Example: + >>> msg['body'] + 'Message contents' + + Stanza interfaces are typically mapped directly to the underlying XML + object, but can be overridden by the presence of a getAttrib method + (or getFoo where the interface is named foo, etc). + + The search order for interface value retrieval for an interface + named 'foo' is: + 1. The list of substanzas. + 2. The result of calling getFoo. + 3. The contents of the foo subelement, if foo is a sub interface. + 4. The value of the foo attribute of the XML object. + 5. The plugin named 'foo' + 6. An empty string. + + Arguments: + attrib -- The name of the requested stanza interface. + """ + if attrib == 'substanzas': + return self.iterables + elif attrib in self.interfaces: + get_method = "get%s" % attrib.title() + if hasattr(self, get_method): + return getattr(self, get_method)() + else: + if attrib in self.sub_interfaces: + return self._getSubText(attrib) + else: + return self._getAttr(attrib) + elif attrib in self.plugin_attrib_map: + if attrib not in self.plugins: + self.initPlugin(attrib) + return self.plugins[attrib] + else: + return '' + + def __setitem__(self, attrib, value): + """ + Set the value of a stanza interface using dictionary-like syntax. + + Example: + >>> msg['body'] = "Hi!" + >>> msg['body'] + 'Hi!' + + Stanza interfaces are typically mapped directly to the underlying XML + object, but can be overridden by the presence of a setAttrib method + (or setFoo where the interface is named foo, etc). + + The effect of interface value assignment for an interface + named 'foo' will be one of: + 1. Delete the interface's contents if the value is None. + 2. Call setFoo, if it exists. + 3. Set the text of a foo element, if foo is in sub_interfaces. + 4. Set the value of a top level XML attribute name foo. + 5. Attempt to pass value to a plugin named foo using the plugin's + foo interface. + 6. Do nothing. + + Arguments: + attrib -- The name of the stanza interface to modify. + value -- The new value of the stanza interface. + """ + if attrib in self.interfaces: + if value is not None: + if hasattr(self, "set%s" % attrib.title()): + getattr(self, "set%s" % attrib.title())(value,) + else: + if attrib in self.sub_interfaces: + return self._setSubText(attrib, text=value) + else: + self._setAttr(attrib, value) + else: + self.__delitem__(attrib) + elif attrib in self.plugin_attrib_map: + if attrib not in self.plugins: + self.initPlugin(attrib) + self.plugins[attrib][attrib] = value + return self + + @property + def attrib(self): #backwards compatibility + return self -if sys.version_info < (3,0): - from . import tostring26 as tostring -else: - from . import tostring - -xmltester = type(ET.Element('xml')) - -class JID(object): - def __init__(self, jid): - self.jid = jid - - def __getattr__(self, name): - if name == 'resource': - return self.jid.split('/', 1)[-1] - elif name == 'user': - if '@' in self.jid: - return self.jid.split('@', 1)[0] - else: - return '' - elif name == 'server': - return self.jid.split('@', 1)[-1].split('/', 1)[0] - elif name == 'full': - return self.jid - elif name == 'bare': - return self.jid.split('/', 1)[0] - - def __str__(self): - return self.jid - -class ElementBase(tostring.ToString): - name = 'stanza' - plugin_attrib = 'plugin' - namespace = 'jabber:client' - interfaces = set(('type', 'to', 'from', 'id', 'payload')) - types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) - sub_interfaces = tuple() - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = None - - def __init__(self, xml=None, parent=None): - if parent is None: - self.parent = None - else: - self.parent = weakref.ref(parent) - self.xml = xml - self.plugins = {} - self.iterables = [] - self.idx = 0 - if not self.setup(xml): - for child in self.xml.getchildren(): - if child.tag in self.plugin_tag_map: - self.plugins[self.plugin_tag_map[child.tag].plugin_attrib] = self.plugin_tag_map[child.tag](xml=child, parent=self) - if self.subitem is not None: - for sub in self.subitem: - if child.tag == "{%s}%s" % (sub.namespace, sub.name): - self.iterables.append(sub(xml=child, parent=self)) - break - - - @property - def attrib(self): #backwards compatibility - return self - - def __iter__(self): - self.idx = 0 - return self - - def __bool__(self): - return True - - def __next__(self): - self.idx += 1 - if self.idx > len(self.iterables): - self.idx = 0 - raise StopIteration - return self.iterables[self.idx - 1] - - def next(self): - return self.__next__() - - def __len__(self): - return len(self.iterables) - - def append(self, item): - if not isinstance(item, ElementBase): - if type(item) == xmltester: - return self.appendxml(item) - else: - raise TypeError - self.xml.append(item.xml) - self.iterables.append(item) - return self - - def pop(self, idx=0): - aff = self.iterables.pop(idx) - self.xml.remove(aff.xml) - return aff - - def get(self, key, defaultvalue=None): - value = self[key] - if value is None or value == '': - return defaultvalue - return value - - def keys(self): - out = [] - out += [x for x in self.interfaces] - out += [x for x in self.plugins] - if self.iterables: - out.append('substanzas') - return tuple(out) - - def match(self, matchstring): - if isinstance(matchstring, str): - nodes = matchstring.split('/') - else: - nodes = matchstring - tagargs = nodes[0].split('@') - if tagargs[0] not in (self.plugins, self.plugin_attrib): return False - founditerable = False - for iterable in self.iterables: - if nodes[1:] == []: - break - founditerable = iterable.match(nodes[1:]) - if founditerable: break; - for evals in tagargs[1:]: - x,y = evals.split('=') - if self[x] != y: return False - if not founditerable and len(nodes) > 1: - next = nodes[1].split('@')[0] - if next in self.plugins: - return self.plugins[next].match(nodes[1:]) - else: - return False - return True - - def find(self, xpath): # for backwards compatiblity, expose elementtree interface - return self.xml.find(xpath) - - def findall(self, xpath): - return self.xml.findall(xpath) - - def setup(self, xml=None): - if self.xml is None: - self.xml = xml - if self.xml is None: - for ename in self.name.split('/'): - new = ET.Element("{%(namespace)s}%(name)s" % {'name': self.name, 'namespace': self.namespace}) - if self.xml is None: - self.xml = new - else: - self.xml.append(new) - if self.parent is not None: - self.parent().xml.append(self.xml) - return True #had to generate XML - else: - return False - - def enable(self, attrib): - self.initPlugin(attrib) - return self - - def initPlugin(self, attrib): - if attrib not in self.plugins: - self.plugins[attrib] = self.plugin_attrib_map[attrib](parent=self) - - def __getitem__(self, attrib): - if attrib == 'substanzas': - return self.iterables - elif attrib in self.interfaces: - if hasattr(self, "get%s" % attrib.title()): - return getattr(self, "get%s" % attrib.title())() - else: - if attrib in self.sub_interfaces: - return self._getSubText(attrib) - else: - return self._getAttr(attrib) - elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: self.initPlugin(attrib) - return self.plugins[attrib] - else: - return '' - - def __setitem__(self, attrib, value): - if attrib in self.interfaces: - if value is not None: - if hasattr(self, "set%s" % attrib.title()): - getattr(self, "set%s" % attrib.title())(value,) - else: - if attrib in self.sub_interfaces: - return self._setSubText(attrib, text=value) - else: - self._setAttr(attrib, value) - else: - self.__delitem__(attrib) - elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: self.initPlugin(attrib) - self.initPlugin(attrib) - self.plugins[attrib][attrib] = value - return self - - def __delitem__(self, attrib): - if attrib.lower() in self.interfaces: - if hasattr(self, "del%s" % attrib.title()): - getattr(self, "del%s" % attrib.title())() - else: - if attrib in self.sub_interfaces: - return self._delSub(attrib) - else: - self._delAttr(attrib) - elif attrib in self.plugin_attrib_map: - if attrib in self.plugins: - del self.plugins[attrib] - return self - - def __eq__(self, other): - if not isinstance(other, ElementBase): - return False - values = self.getValues() - for key in other: - if key not in values or values[key] != other[key]: - return False - return True - - def _setAttr(self, name, value): - if value is None or value == '': - self.__delitem__(name) - else: - self.xml.attrib[name] = value - - def _delAttr(self, name): - if name in self.xml.attrib: - del self.xml.attrib[name] - - def _getAttr(self, name): - return self.xml.attrib.get(name, '') - - def _getSubText(self, name): - stanza = self.xml.find("{%s}%s" % (self.namespace, name)) - if stanza is None or stanza.text is None: - return '' - else: - return stanza.text - - def _setSubText(self, name, attrib={}, text=None): - if text is None or text == '': - return self.__delitem__(name) - stanza = self.xml.find("{%s}%s" % (self.namespace, name)) - if stanza is None: - #self.xml.append(ET.Element("{%s}%s" % (self.namespace, name), attrib)) - stanza = ET.Element("{%s}%s" % (self.namespace, name)) - self.xml.append(stanza) - stanza.text = text - return stanza - - def _delSub(self, name): - for child in self.xml.getchildren(): - if child.tag == "{%s}%s" % (self.namespace, name): - self.xml.remove(child) - - def getValues(self): - out = {} - for interface in self.interfaces: - out[interface] = self[interface] - for pluginkey in self.plugins: - out[pluginkey] = self.plugins[pluginkey].getValues() - if self.iterables: - iterables = [] - for stanza in self.iterables: - iterables.append(stanza.getValues()) - iterables[-1].update({'__childtag__': "{%s}%s" % (stanza.namespace, stanza.name)}) - out['substanzas'] = iterables - return out - - def setValues(self, attrib): - for interface in attrib: - if interface == 'substanzas': - for subdict in attrib['substanzas']: - if '__childtag__' in subdict: - for subclass in self.subitem: - if subdict['__childtag__'] == "{%s}%s" % (subclass.namespace, subclass.name): - sub = subclass(parent=self) - sub.setValues(subdict) - self.iterables.append(sub) - break - elif interface in self.interfaces: - self[interface] = attrib[interface] - elif interface in self.plugin_attrib_map and interface not in self.plugins: - self.initPlugin(interface) - if interface in self.plugins: - self.plugins[interface].setValues(attrib[interface]) - return self - - def appendxml(self, xml): - self.xml.append(xml) - return self - - #def __del__(self): #prevents garbage collection of reference cycle - # if self.parent is not None: - # self.parent.xml.remove(self.xml) + def __iter__(self): + self.idx = 0 + return self + + def __bool__(self): + return True + + def __next__(self): + self.idx += 1 + if self.idx > len(self.iterables): + self.idx = 0 + raise StopIteration + return self.iterables[self.idx - 1] + + def next(self): + return self.__next__() + + def __len__(self): + return len(self.iterables) + + def append(self, item): + if not isinstance(item, ElementBase): + if type(item) == XML_TYPE: + return self.appendxml(item) + else: + raise TypeError + self.xml.append(item.xml) + self.iterables.append(item) + return self + + def pop(self, idx=0): + aff = self.iterables.pop(idx) + self.xml.remove(aff.xml) + return aff + + def get(self, key, defaultvalue=None): + value = self[key] + if value is None or value == '': + return defaultvalue + return value + + def keys(self): + out = [] + out += [x for x in self.interfaces] + out += [x for x in self.plugins] + if self.iterables: + out.append('substanzas') + return tuple(out) + + def match(self, matchstring): + if isinstance(matchstring, str): + nodes = matchstring.split('/') + else: + nodes = matchstring + tagargs = nodes[0].split('@') + if tagargs[0] not in (self.plugins, self.plugin_attrib): return False + founditerable = False + for iterable in self.iterables: + if nodes[1:] == []: + break + founditerable = iterable.match(nodes[1:]) + if founditerable: break; + for evals in tagargs[1:]: + x,y = evals.split('=') + if self[x] != y: return False + if not founditerable and len(nodes) > 1: + next = nodes[1].split('@')[0] + if next in self.plugins: + return self.plugins[next].match(nodes[1:]) + else: + return False + return True + + def find(self, xpath): # for backwards compatiblity, expose elementtree interface + return self.xml.find(xpath) + + def findall(self, xpath): + return self.xml.findall(xpath) + + def __delitem__(self, attrib): + if attrib.lower() in self.interfaces: + if hasattr(self, "del%s" % attrib.title()): + getattr(self, "del%s" % attrib.title())() + else: + if attrib in self.sub_interfaces: + return self._delSub(attrib) + else: + self._delAttr(attrib) + elif attrib in self.plugin_attrib_map: + if attrib in self.plugins: + del self.plugins[attrib] + return self + + def __eq__(self, other): + if not isinstance(other, ElementBase): + return False + values = self.getStanzaValues() + for key in other: + if key not in values or values[key] != other[key]: + return False + return True + + def _setAttr(self, name, value): + if value is None or value == '': + self.__delitem__(name) + else: + self.xml.attrib[name] = value + + def _delAttr(self, name): + if name in self.xml.attrib: + del self.xml.attrib[name] + + def _getAttr(self, name, default=''): + return self.xml.attrib.get(name, default) + + def _getSubText(self, name): + if '}' not in name: + name = "{%s}%s" % (self.namespace, name) + stanza = self.xml.find(name) + if stanza is None or stanza.text is None: + return '' + else: + return stanza.text + + def _setSubText(self, name, attrib={}, text=None): + if '}' not in name: + name = "{%s}%s" % (self.namespace, name) + if text is None or text == '': + return self.__delitem__(name) + stanza = self.xml.find(name) + if stanza is None: + stanza = ET.Element(name) + self.xml.append(stanza) + stanza.text = text + return stanza + + def _delSub(self, name): + if '}' not in name: + name = "{%s}%s" % (self.namespace, name) + for child in self.xml.getchildren(): + if child.tag == name: + self.xml.remove(child) + + def appendxml(self, xml): + self.xml.append(xml) + return self + + def __copy__(self): + return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent) + + def __str__(self): + return tostring(self.xml, xmlns='', stanza_ns=self.namespace) + + def __repr__(self): + return self.__str__() + +#def __del__(self): #prevents garbage collection of reference cycle +# if self.parent is not None: +# self.parent.xml.remove(self.xml) class StanzaBase(ElementBase): - name = 'stanza' - namespace = 'jabber:client' - interfaces = set(('type', 'to', 'from', 'id', 'payload')) - types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) - sub_interfaces = tuple() - - def __init__(self, stream=None, xml=None, stype=None, sto=None, sfrom=None, sid=None): - self.stream = stream - if stream is not None: - self.namespace = stream.default_ns - ElementBase.__init__(self, xml) - if stype is not None: - self['type'] = stype - if sto is not None: - self['to'] = sto - if sfrom is not None: - self['from'] = sfrom - self.tag = "{%s}%s" % (self.namespace, self.name) - - def setType(self, value): - if value in self.types: - self.xml.attrib['type'] = value - return self - - def getPayload(self): - return self.xml.getchildren() - - def setPayload(self, value): - self.xml.append(value) - return self - - def delPayload(self): - self.clear() - return self - - def clear(self): - for child in self.xml.getchildren(): - self.xml.remove(child) - for plugin in list(self.plugins.keys()): - del self.plugins[plugin] - return self - - def reply(self): - self['from'], self['to'] = self['to'], self['from'] - self.clear() - return self - - def error(self): - self['type'] = 'error' - return self - - def getTo(self): - return JID(self._getAttr('to')) - - def setTo(self, value): - return self._setAttr('to', str(value)) - - def getFrom(self): - return JID(self._getAttr('from')) - - def setFrom(self, value): - return self._setAttr('from', str(value)) - - def unhandled(self): - pass - - def exception(self, e): - logging.error(traceback.format_tb(e)) - - def send(self): - self.stream.sendRaw(self.__str__()) + name = 'stanza' + namespace = 'jabber:client' + interfaces = set(('type', 'to', 'from', 'id', 'payload')) + types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) + sub_interfaces = tuple() + + def __init__(self, stream=None, xml=None, stype=None, sto=None, sfrom=None, sid=None): + self.stream = stream + if stream is not None: + self.namespace = stream.default_ns + ElementBase.__init__(self, xml) + if stype is not None: + self['type'] = stype + if sto is not None: + self['to'] = sto + if sfrom is not None: + self['from'] = sfrom + self.tag = "{%s}%s" % (self.namespace, self.name) + + def setType(self, value): + if value in self.types: + self.xml.attrib['type'] = value + return self + + def getPayload(self): + return self.xml.getchildren() + + def setPayload(self, value): + self.xml.append(value) + return self + + def delPayload(self): + self.clear() + return self + + def clear(self): + for child in self.xml.getchildren(): + self.xml.remove(child) + for plugin in list(self.plugins.keys()): + del self.plugins[plugin] + return self + + def reply(self): + # if it's a component, use from + if self.stream and hasattr(self.stream, "is_component") and self.stream.is_component: + self['from'], self['to'] = self['to'], self['from'] + else: + self['to'] = self['from'] + del self['from'] + self.clear() + return self + + def error(self): + self['type'] = 'error' + return self + + def getTo(self): + return JID(self._getAttr('to')) + + def setTo(self, value): + return self._setAttr('to', str(value)) + + def getFrom(self): + return JID(self._getAttr('from')) + + def setFrom(self, value): + return self._setAttr('from', str(value)) + + def unhandled(self): + pass + + def exception(self, e): + logging.exception('Error handling {%s}%s stanza' % (self.namespace, self.name)) + + def send(self): + self.stream.sendRaw(self.__str__()) + + def __copy__(self): + return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream) + def __str__(self): + return tostring(self.xml, xmlns='', stanza_ns=self.namespace, stream=self.stream) diff --git a/sleekxmpp/xmlstream/statemachine.py b/sleekxmpp/xmlstream/statemachine.py index fb7d1508..8a1aa22d 100644 --- a/sleekxmpp/xmlstream/statemachine.py +++ b/sleekxmpp/xmlstream/statemachine.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from __future__ import with_statement import threading diff --git a/sleekxmpp/xmlstream/statemanager.py b/sleekxmpp/xmlstream/statemanager.py new file mode 100644 index 00000000..c7f76e75 --- /dev/null +++ b/sleekxmpp/xmlstream/statemanager.py @@ -0,0 +1,139 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from __future__ import with_statement +import threading + + +class StateError(Exception): + """Raised whenever a state transition was attempted but failed.""" + + +class StateManager(object): + """ + At the very core of SleekXMPP there is a need to track various + library configuration settings, XML stream features, and the + network connection status. The state manager is responsible for + tracking this information in a thread-safe manner. + + State 'variables' store the current state of these items as simple + string values or booleans. Changing those values must be done + according to transitions defined when creating the state variable. + + If a state variable is given a value that is not allowed according + to the transition definitions, a StateError is raised. When a + valid value is assigned an event is raised named: + + _state_changed_nameofthestatevariable + + The event carries a dictionary containing the previous and the new + state values. + """ + + def __init__(self, event_func=None): + """ + Initialize the state manager. The parameter event_func should be + the event() method of a SleekXMPP object in order to enable + _state_changed_* events. + """ + self.main_lock = threading.Lock() + self.locks = {} + self.state_variables = {} + + if event_func is not None: + self.event = event_func + else: + self.event = lambda name, data: None + + def add(self, name, default=False, values=None, transitions=None): + """ + Create a new state variable. + + When transitions is specified, only those defined state change + transitions will be allowed. + + When values is specified (and not transitions), any state changes + between those values are allowed. + + If neither values nor transitions are defined, then the state variable + will be a binary switch between True and False. + """ + if name in self.state_variables: + raise IndexError("State variable %s already exists" % name) + + self.locks[name] = threading.Lock() + with self.locks[name]: + var = {'value': default, + 'default': default, + 'transitions': {}} + + if transitions is not None: + for start in transitions: + var['transitions'][start] = set(transitions[start]) + elif values is not None: + values = set(values) + for value in values: + var['transitions'][value] = values + elif values is None: + var['transitions'] = {True: [False], + False: [True]} + + self.state_variables[name] = var + + def addStates(self, var_defs): + """ + Create multiple state variables at once. + """ + for var, data in var_defs: + self.add(var, + default=data.get('default', False), + values=data.get('values', None), + transitions=data.get('transitions', None)) + + def force_set(self, name, val): + """ + Force setting a state variable's value by overriding transition checks. + """ + with self.locks[name]: + self.state_variables[name]['value'] = val + + def reset(self, name): + """ + Reset a state variable to its default value. + """ + with self.locks[name]: + default = self.state_variables[name]['default'] + self.state_variables[name]['value'] = default + + def __getitem__(self, name): + """ + Get the value of a state variable if it exists. + """ + with self.locks[name]: + if name not in self.state_variables: + raise IndexError("State variable %s does not exist" % name) + return self.state_variables[name]['value'] + + def __setitem__(self, name, val): + """ + Attempt to set the value of a state variable, but raise StateError + if the transition is undefined. + + A _state_changed_* event is triggered after a successful transition. + """ + with self.locks[name]: + if name not in self.state_variables: + raise IndexError("State variable %s does not exist" % name) + current = self.state_variables[name]['value'] + if current == val: + return + if val in self.state_variables[name]['transitions'][current]: + self.state_variables[name]['value'] = val + self.event('_state_changed_%s' % name, {'from': current, 'to': val}) + else: + raise StateError("Can not transition from '%s' to '%s'" % (str(current), str(val))) diff --git a/sleekxmpp/xmlstream/tostring/__init__.py b/sleekxmpp/xmlstream/tostring/__init__.py index 6603cbb8..5852cba2 100644 --- a/sleekxmpp/xmlstream/tostring/__init__.py +++ b/sleekxmpp/xmlstream/tostring/__init__.py @@ -1,60 +1,19 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. -class ToString(object): - def __str__(self, xml=None, xmlns='', stringbuffer=''): - if xml is None: - xml = self.xml - newoutput = [stringbuffer] - #TODO respect ET mapped namespaces - itag = xml.tag.split('}', 1)[-1] - if '}' in xml.tag: - ixmlns = xml.tag.split('}', 1)[0][1:] - else: - ixmlns = '' - nsbuffer = '' - if xmlns != ixmlns and ixmlns != '' and ixmlns != self.namespace: - if self.stream is not None and ixmlns in self.stream.namespace_map: - if self.stream.namespace_map[ixmlns] != '': - itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag) - else: - nsbuffer = """ xmlns="%s\"""" % ixmlns - if ixmlns not in ('', xmlns, self.namespace): - nsbuffer = """ xmlns="%s\"""" % ixmlns - newoutput.append("<%s" % itag) - newoutput.append(nsbuffer) - for attrib in xml.attrib: - if '{' not in attrib: - newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib]))) - if len(xml) or xml.text or xml.tail: - newoutput.append(">") - if xml.text: - newoutput.append(self.xmlesc(xml.text)) - if len(xml): - for child in xml.getchildren(): - newoutput.append(self.__str__(child, ixmlns)) - newoutput.append("</%s>" % (itag, )) - if xml.tail: - newoutput.append(self.xmlesc(xml.tail)) - elif xml.text: - newoutput.append(">%s</%s>" % (self.xmlesc(xml.text), itag)) - else: - newoutput.append(" />") - return ''.join(newoutput) + See the file LICENSE for copying permission. +""" - def xmlesc(self, text): - text = list(text) - cc = 0 - matches = ('&', '<', '"', '>', "'") - for c in text: - if c in matches: - if c == '&': - text[cc] = '&' - elif c == '<': - text[cc] = '<' - elif c == '>': - text[cc] = '>' - elif c == "'": - text[cc] = ''' - else: - text[cc] = '"' - cc += 1 - return ''.join(text) +import sys + +# Import the correct tostring and xml_escape functions based on the Python +# version in order to properly handle Unicode. + +if sys.version_info < (3, 0): + from sleekxmpp.xmlstream.tostring.tostring26 import tostring, xml_escape +else: + from sleekxmpp.xmlstream.tostring.tostring import tostring, xml_escape + +__all__ = ['tostring', 'xml_escape'] diff --git a/sleekxmpp/xmlstream/tostring/tostring.py b/sleekxmpp/xmlstream/tostring/tostring.py new file mode 100644 index 00000000..c2696321 --- /dev/null +++ b/sleekxmpp/xmlstream/tostring/tostring.py @@ -0,0 +1,95 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): + """ + Serialize an XML object to a Unicode string. + + If namespaces are provided using xmlns or stanza_ns, then elements + that use those namespaces will not include the xmlns attribute in + the output. + + Arguments: + xml -- The XML object to serialize. If the value is None, + then the XML object contained in this stanza + object will be used. + xmlns -- Optional namespace of an element wrapping the XML + object. + stanza_ns -- The namespace of the stanza object that contains + the XML object. + stream -- The XML stream that generated the XML object. + outbuffer -- Optional buffer for storing serializations during + recursive calls. + """ + # Add previous results to the start of the output. + output = [outbuffer] + + # Extract the element's tag name. + tag_name = xml.tag.split('}', 1)[-1] + + # Extract the element's namespace if it is defined. + if '}' in xml.tag: + tag_xmlns = xml.tag.split('}', 1)[0][1:] + else: + tag_xmlns = '' + + # Output the tag name and derived namespace of the element. + namespace = '' + if tag_xmlns not in ['', xmlns, stanza_ns]: + namespace = ' xmlns="%s"' % tag_xmlns + if stream and tag_xmlns in stream.namespace_map: + mapped_namespace = stream.namespace_map[tag_xmlns] + if mapped_namespace: + tag = "%s:%s" % (mapped_namespace, tag_name) + output.append("<%s" % tag_name) + output.append(namespace) + + # Output escaped attribute values. + for attrib, value in xml.attrib.items(): + if '{' not in attrib: + value = xml_escape(value) + output.append(' %s="%s"' % (attrib, value)) + + if len(xml) or xml.text: + # If there are additional child elements to serialize. + output.append(">") + if xml.text: + output.append(xml_escape(xml.text)) + if len(xml): + for child in xml.getchildren(): + output.append(tostring(child, tag_xmlns, stanza_ns, stream)) + output.append("</%s>" % tag_name) + elif xml.text: + # If we only have text content. + output.append(">%s</%s>" % (xml_escape(xml.text), tag_name)) + else: + # Empty element. + output.append(" />") + if xml.tail: + # If there is additional text after the element. + output.append(xml_escape(xml.tail)) + return ''.join(output) + + +def xml_escape(text): + """ + Convert special characters in XML to escape sequences. + + Arguments: + text -- The XML text to convert. + """ + text = list(text) + escapes = {'&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"'} + for i, c in enumerate(text): + text[i] = escapes.get(c, c) + return ''.join(text) diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py new file mode 100644 index 00000000..7a376374 --- /dev/null +++ b/sleekxmpp/xmlstream/tostring/tostring26.py @@ -0,0 +1,104 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from __future__ import unicode_literals +import types + + +def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): + """ + Serialize an XML object to a Unicode string. + + If namespaces are provided using xmlns or stanza_ns, then elements + that use those namespaces will not include the xmlns attribute in + the output. + + Arguments: + xml -- The XML object to serialize. If the value is None, + then the XML object contained in this stanza + object will be used. + xmlns -- Optional namespace of an element wrapping the XML + object. + stanza_ns -- The namespace of the stanza object that contains + the XML object. + stream -- The XML stream that generated the XML object. + outbuffer -- Optional buffer for storing serializations during + recursive calls. + """ + # Add previous results to the start of the output. + output = [outbuffer] + + # Extract the element's tag name. + tag_name = xml.tag.split('}', 1)[-1] + + # Extract the element's namespace if it is defined. + if '}' in xml.tag: + tag_xmlns = xml.tag.split('}', 1)[0][1:] + else: + tag_xmlns = u'' + + # Output the tag name and derived namespace of the element. + namespace = u'' + if tag_xmlns not in ['', xmlns, stanza_ns]: + namespace = u' xmlns="%s"' % tag_xmlns + if stream and tag_xmlns in stream.namespace_map: + mapped_namespace = stream.namespace_map[tag_xmlns] + if mapped_namespace: + tag = u"%s:%s" % (mapped_namespace, tag_name) + output.append(u"<%s" % tag_name) + output.append(namespace) + + # Output escaped attribute values. + for attrib, value in xml.attrib.items(): + if '{' not in attrib: + value = xml_escape(value) + output.append(u' %s="%s"' % (attrib, value)) + + if len(xml) or xml.text: + # If there are additional child elements to serialize. + output.append(u">") + if xml.text: + output.append(xml_escape(xml.text)) + if len(xml): + for child in xml.getchildren(): + output.append(tostring(child, tag_xmlns, stanza_ns, stream)) + output.append(u"</%s>" % tag_name) + if xml.tail: + # If there is additional text after the element. + output.append(xml_escape(xml.tail)) + elif xml.text: + # If we only have text content. + output.append(u">%s</%s>" % (xml_escape(xml.text), tag_name)) + else: + # Empty element. + output.append(u" />") + if xml.tail: + # If there is additional text after the element. + output.append(xml_escape(xml.tail)) + return u''.join(output) + + +def xml_escape(text): + """ + Convert special characters in XML to escape sequences. + + Arguments: + text -- The XML text to convert. + """ + if type(text) != types.UnicodeType: + text = list(unicode(text, 'utf-8', 'ignore')) + else: + text = list(text) + escapes = {u'&': u'&', + u'<': u'<', + u'>': u'>', + u"'": u''', + u'"': u'"'} + for i, c in enumerate(text): + text[i] = escapes.get(c, c) + return u''.join(text) diff --git a/sleekxmpp/xmlstream/tostring26/__init__.py b/sleekxmpp/xmlstream/tostring26/__init__.py deleted file mode 100644 index 9711c300..00000000 --- a/sleekxmpp/xmlstream/tostring26/__init__.py +++ /dev/null @@ -1,65 +0,0 @@ -import types - -class ToString(object): - def __str__(self, xml=None, xmlns='', stringbuffer=''): - if xml is None: - xml = self.xml - newoutput = [stringbuffer] - #TODO respect ET mapped namespaces - itag = xml.tag.split('}', 1)[-1] - if '}' in xml.tag: - ixmlns = xml.tag.split('}', 1)[0][1:] - else: - ixmlns = '' - nsbuffer = '' - if xmlns != ixmlns and ixmlns != u'' and ixmlns != self.namespace: - if self.stream is not None and ixmlns in self.stream.namespace_map: - if self.stream.namespace_map[ixmlns] != u'': - itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag) - else: - nsbuffer = """ xmlns="%s\"""" % ixmlns - if ixmlns not in ('', xmlns, self.namespace): - nsbuffer = """ xmlns="%s\"""" % ixmlns - newoutput.append("<%s" % itag) - newoutput.append(nsbuffer) - for attrib in xml.attrib: - if '{' not in attrib: - newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib]))) - if len(xml) or xml.text or xml.tail: - newoutput.append(u">") - if xml.text: - newoutput.append(self.xmlesc(xml.text)) - if len(xml): - for child in xml.getchildren(): - newoutput.append(self.__str__(child, ixmlns)) - newoutput.append(u"</%s>" % (itag, )) - if xml.tail: - newoutput.append(self.xmlesc(xml.tail)) - elif xml.text: - newoutput.append(">%s</%s>" % (self.xmlesc(xml.text), itag)) - else: - newoutput.append(" />") - return u''.join(newoutput) - - def xmlesc(self, text): - if type(text) != types.UnicodeType: - text = list(unicode(text, 'utf-8', 'ignore')) - else: - text = list(text) - - cc = 0 - matches = (u'&', u'<', u'"', u'>', u"'") - for c in text: - if c in matches: - if c == u'&': - text[cc] = u'&' - elif c == u'<': - text[cc] = u'<' - elif c == u'>': - text[cc] = u'>' - elif c == u"'": - text[cc] = u''' - else: - text[cc] = u'"' - cc += 1 - return ''.join(text) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 6b92abca..bf39bb33 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from __future__ import with_statement, unicode_literals @@ -19,11 +19,13 @@ import logging import socket import threading import time -import traceback import types +import copy import xml.sax.saxutils from . import scheduler +from sleekxmpp.xmlstream.tostring import tostring +RESPONSE_TIMEOUT = 10 HANDLER_THREADS = 1 ssl_support = True @@ -36,7 +38,7 @@ if sys.version_info < (3, 0): #monkey patch broken filesocket object from . import filesocket #socket._fileobject = filesocket.filesocket - + class RestartStream(Exception): pass @@ -71,6 +73,7 @@ class XMLStream(object): self.use_ssl = False self.use_tls = False + self.default_ns = '' self.stream_header = "<stream>" self.stream_footer = "</stream>" @@ -81,7 +84,7 @@ class XMLStream(object): self.namespace_map = {} self.run = True - + def setSocket(self, socket): "Set the socket" self.socket = socket @@ -89,10 +92,10 @@ class XMLStream(object): self.filesocket = socket.makefile('rb', 0) # ElementTree.iterparse requires a file. 0 buffer files have to be binary self.state.set('connected', True) - + def setFileSocket(self, filesocket): self.filesocket = filesocket - + def connect(self, host='', port=0, use_ssl=False, use_tls=True): "Link to connectTCP" return self.connectTCP(host, port, use_ssl, use_tls) @@ -124,7 +127,7 @@ class XMLStream(object): except socket.error as serr: logging.error("Could not connect. Socket Error #%s: %s" % (serr.errno, serr.strerror)) time.sleep(1) - + def connectUnix(self, filepath): "Connect to Unix file and create socket" @@ -145,7 +148,7 @@ class XMLStream(object): logging.warning("Tried to enable TLS, but ssl module not found.") return False raise RestartStream() - + def process(self, threaded=True): self.scheduler.process(threaded=True) for t in range(0, HANDLER_THREADS): @@ -159,10 +162,10 @@ class XMLStream(object): self.__thread['process'].start() else: self._process() - + def schedule(self, name, seconds, callback, args=None, kwargs=None, repeat=False): self.scheduler.add(name, seconds, callback, args, kwargs, repeat, qpointer=self.eventqueue) - + def _process(self): "Start processing the socket." firstrun = True @@ -194,14 +197,14 @@ class XMLStream(object): return else: self.state.set('processing', False) - traceback.print_exc() + logging.exception('Socket Error') self.disconnect(reconnect=True) except: if not self.state.reconnect: return else: self.state.set('processing', False) - traceback.print_exc() + logging.exception('Connection error. Reconnecting.') self.disconnect(reconnect=True) if self.state['reconnect']: self.reconnect() @@ -211,7 +214,7 @@ class XMLStream(object): #self.__thread['readXML'].start() #self.__thread['spawnEvents'] = threading.Thread(name='spawnEvents', target=self.__spawnEvents) #self.__thread['spawnEvents'].start() - + def __readXML(self): "Parses the incoming stream, adding to xmlin queue as it goes" #build cElementTree object from expat was we go @@ -244,7 +247,7 @@ class XMLStream(object): if event == b'start': edepth += 1 logging.debug("Ending readXML loop") - + def _sendThread(self): while self.run: data = self.sendqueue.get(True) @@ -257,14 +260,13 @@ class XMLStream(object): logging.warning("Failed to send %s" % data) self.state.set('connected', False) if self.state.reconnect: - logging.error("Disconnected. Socket Error.") - traceback.print_exc() + logging.exception("Disconnected. Socket Error.") self.disconnect(reconnect=True) - + def sendRaw(self, data): self.sendqueue.put(data) return True - + def disconnect(self, reconnect=False): self.state.set('reconnect', reconnect) if self.state['disconnecting']: @@ -290,41 +292,40 @@ class XMLStream(object): if self.state['processing']: #raise CloseStream pass - + def reconnect(self): self.state.set('tls',False) self.state.set('ssl',False) time.sleep(1) self.connect() - + def incoming_filter(self, xmlobj): return xmlobj - + def __spawnEvent(self, xmlobj): "watching xmlOut and processes handlers" #convert XML into Stanza - logging.debug("RECV: %s" % cElementTree.tostring(xmlobj)) + logging.debug("RECV: %s" % tostring(xmlobj, xmlns=self.default_ns, stream=self)) xmlobj = self.incoming_filter(xmlobj) - stanza = None + stanza_type = StanzaBase for stanza_class in self.__root_stanza: if xmlobj.tag == "{%s}%s" % (self.default_ns, stanza_class.name): - #if self.__root_stanza[stanza_class].match(xmlobj): - stanza = stanza_class(self, xmlobj) + stanza_type = stanza_class break - if stanza is None: - stanza = StanzaBase(self, xmlobj) unhandled = True + stanza = stanza_type(self, xmlobj) for handler in self.__handlers: if handler.match(stanza): - handler.prerun(stanza) - self.eventqueue.put(('stanza', handler, stanza)) + stanza_copy = stanza_type(self, copy.deepcopy(xmlobj)) + handler.prerun(stanza_copy) + self.eventqueue.put(('stanza', handler, stanza_copy)) if handler.checkDelete(): self.__handlers.pop(self.__handlers.index(handler)) unhandled = False if unhandled: stanza.unhandled() #loop through handlers and test match #spawn threads as necessary, call handlers, sending Stanza - + def _eventRunner(self): logging.debug("Loading event runner") while self.run: @@ -344,22 +345,22 @@ class XMLStream(object): try: handler.run(args[0]) except Exception as e: - traceback.print_exc() + logging.exception('Error processing event handler: %s' % handler.name) args[0].exception(e) elif etype == 'schedule': try: logging.debug(args) handler(*args[0]) except: - logging.error(traceback.format_exc()) + logging.exception('Error processing scheduled task') elif etype == 'quit': logging.debug("Quitting eventRunner thread") return False - + def registerHandler(self, handler, before=None, after=None): "Add handler with matcher class and parameters." self.__handlers.append(handler) - + def removeHandler(self, name): "Removes the handler." idx = 0 @@ -368,81 +369,27 @@ class XMLStream(object): self.__handlers.pop(idx) return idx += 1 - + def registerStanza(self, stanza_class): "Adds stanza. If root stanzas build stanzas sent in events while non-root stanzas build substanza objects." self.__root_stanza.append(stanza_class) - + def registerStanzaExtension(self, stanza_class, stanza_extension): if stanza_class not in stanza_extensions: stanza_extensions[stanza_class] = [stanza_extension] else: stanza_extensions[stanza_class].append(stanza_extension) - + def removeStanza(self, stanza_class, root=False): "Removes the stanza's registration." if root: del self.__root_stanza[stanza_class] else: del self.__stanza[stanza_class] - + def removeStanzaExtension(self, stanza_class, stanza_extension): stanza_extension[stanza_class].pop(stanza_extension) - def tostring(self, xml, xmlns='', stringbuffer=''): - newoutput = [stringbuffer] - #TODO respect ET mapped namespaces - itag = xml.tag.split('}', 1)[-1] - if '}' in xml.tag: - ixmlns = xml.tag.split('}', 1)[0][1:] - else: - ixmlns = '' - nsbuffer = '' - if xmlns != ixmlns and ixmlns != '': - if ixmlns in self.namespace_map: - if self.namespace_map[ixmlns] != '': - itag = "%s:%s" % (self.namespace_map[ixmlns], itag) - else: - nsbuffer = """ xmlns="%s\"""" % ixmlns - newoutput.append("<%s" % itag) - newoutput.append(nsbuffer) - for attrib in xml.attrib: - newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib]))) - if len(xml) or xml.text or xml.tail: - newoutput.append(">") - if xml.text: - newoutput.append(self.xmlesc(xml.text)) - if len(xml): - for child in xml.getchildren(): - newoutput.append(self.tostring(child, ixmlns)) - newoutput.append("</%s>" % (itag, )) - if xml.tail: - newoutput.append(self.xmlesc(xml.tail)) - elif xml.text: - newoutput.append(">%s</%s>" % (self.xmlesc(xml.text), itag)) - else: - newoutput.append(" />") - return ''.join(newoutput) - - def xmlesc(self, text): - text = list(text) - cc = 0 - matches = ('&', '<', '"', '>', "'") - for c in text: - if c in matches: - if c == '&': - text[cc] = '&' - elif c == '<': - text[cc] = '<' - elif c == '>': - text[cc] = '>' - elif c == "'": - text[cc] = ''' - elif self.escape_quotes: - text[cc] = '"' - cc += 1 - return ''.join(text) - def start_stream_handler(self, xml): """Meant to be overridden""" pass |