summaryrefslogtreecommitdiff
path: root/sleekxmpp/xmlstream
diff options
context:
space:
mode:
Diffstat (limited to 'sleekxmpp/xmlstream')
-rw-r--r--sleekxmpp/xmlstream/filesocket.py36
-rw-r--r--sleekxmpp/xmlstream/handler/base.py93
-rw-r--r--sleekxmpp/xmlstream/handler/callback.py104
-rw-r--r--sleekxmpp/xmlstream/handler/waiter.py116
-rw-r--r--sleekxmpp/xmlstream/handler/xmlcallback.py32
-rw-r--r--sleekxmpp/xmlstream/handler/xmlwaiter.py28
-rw-r--r--sleekxmpp/xmlstream/matcher/base.py30
-rw-r--r--sleekxmpp/xmlstream/matcher/id.py29
-rw-r--r--sleekxmpp/xmlstream/matcher/many.py39
-rw-r--r--sleekxmpp/xmlstream/matcher/stanzapath.py34
-rw-r--r--sleekxmpp/xmlstream/matcher/xmlmask.py204
-rw-r--r--sleekxmpp/xmlstream/matcher/xpath.py99
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py187
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py13
14 files changed, 817 insertions, 227 deletions
diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py
index 07b395dc..441ff875 100644
--- a/sleekxmpp/xmlstream/filesocket.py
+++ b/sleekxmpp/xmlstream/filesocket.py
@@ -5,21 +5,37 @@
See the file LICENSE for copying permission.
"""
+
from socket import _fileobject
import socket
-class filesocket(_fileobject):
- def read(self, size=4096):
- data = self._sock.recv(size)
- if data is not None:
- return data
+class FileSocket(_fileobject):
+
+ """
+ Create a file object wrapper for a socket to work around
+ issues present in Python 2.6 when using sockets as file objects.
+
+ The parser for xml.etree.cElementTree requires a file, but we will
+ be reading from the XMPP connection socket instead.
+ """
+
+ def read(self, size=4096):
+ """Read data from the socket as if it were a file."""
+ data = self._sock.recv(size)
+ if data is not None:
+ return data
+
class Socket26(socket._socketobject):
- def makefile(self, mode='r', bufsize=-1):
- """makefile([mode[, bufsize]]) -> file object
- Return a regular file object corresponding to the socket. The mode
- and bufsize arguments are as for the built-in open() function."""
- return filesocket(self._sock, mode, bufsize)
+ """
+ A custom socket implementation that uses our own FileSocket class
+ to work around issues in Python 2.6 when using sockets as files.
+ """
+ def makefile(self, mode='r', bufsize=-1):
+ """makefile([mode[, bufsize]]) -> file object
+ Return a regular file object corresponding to the socket. The mode
+ and bufsize arguments are as for the built-in open() function."""
+ return FileSocket(self._sock, mode, bufsize)
diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py
index 720846d6..3ae82a89 100644
--- a/sleekxmpp/xmlstream/handler/base.py
+++ b/sleekxmpp/xmlstream/handler/base.py
@@ -6,23 +6,82 @@
See the file LICENSE for copying permission.
"""
+
class BaseHandler(object):
+ """
+ Base class for stream handlers. Stream handlers are matched with
+ incoming stanzas so that the stanza may be processed in some way.
+ Stanzas may be matched with multiple handlers.
+
+ Handler execution may take place in two phases. The first is during
+ the stream processing itself. The second is after stream processing
+ and during SleekXMPP's main event loop. The prerun method is used
+ for execution during stream processing, and the run method is used
+ during the main event loop.
+
+ Attributes:
+ name -- The name of the handler.
+ stream -- The stream this handler is assigned to.
+
+ Methods:
+ match -- Compare a stanza with the handler's matcher.
+ prerun -- Handler execution during stream processing.
+ run -- Handler execution during the main event loop.
+ checkDelete -- Indicate if the handler may be removed from use.
+ """
+
+ def __init__(self, name, matcher, stream=None):
+ """
+ Create a new stream handler.
+
+ Arguments:
+ name -- The name of the handler.
+ matcher -- A matcher object from xmlstream.matcher that will be
+ used to determine if a stanza should be accepted by
+ this handler.
+ stream -- The XMLStream instance the handler should monitor.
+ """
+ self.name = name
+ self.stream = stream
+ self._destroy = False
+ self._payload = None
+ self._matcher = matcher
+ if stream is not None:
+ stream.registerHandler(self)
+
+ def match(self, xml):
+ """
+ Compare a stanza or XML object with the handler's matcher.
+
+ Arguments
+ xml -- An XML or stanza object.
+ """
+ return self._matcher.match(xml)
+
+ def prerun(self, payload):
+ """
+ Prepare the handler for execution while the XML stream is being
+ processed.
+
+ Arguments:
+ payload -- A stanza object.
+ """
+ self._payload = payload
+
+ def run(self, payload):
+ """
+ Execute the handler after XML stream processing and during the
+ main event loop.
+
+ Arguments:
+ payload -- A stanza object.
+ """
+ self._payload = payload
- def __init__(self, name, matcher):
- self.name = name
- self._destroy = False
- self._payload = None
- self._matcher = matcher
-
- def match(self, xml):
- return self._matcher.match(xml)
-
- def prerun(self, payload):
- self._payload = payload
-
- def run(self, payload):
- self._payload = payload
-
- def checkDelete(self):
- return self._destroy
+ def checkDelete(self):
+ """
+ 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 889b0aa7..f0a72853 100644
--- a/sleekxmpp/xmlstream/handler/callback.py
+++ b/sleekxmpp/xmlstream/handler/callback.py
@@ -5,30 +5,80 @@
See the file LICENSE for copying permission.
"""
-from . import base
-import logging
-
-class Callback(base.BaseHandler):
-
- def __init__(self, name, matcher, pointer, thread=False, once=False, instream=False):
- base.BaseHandler.__init__(self, name, matcher)
- self._pointer = pointer
- self._thread = thread
- self._once = once
- self._instream = instream
-
- def prerun(self, payload):
- base.BaseHandler.prerun(self, payload)
- if self._instream:
- self.run(payload, True)
-
- def run(self, payload, instream=False):
- if not self._instream or instream:
- base.BaseHandler.run(self, payload)
- #if self._thread:
- # x = threading.Thread(name="Callback_%s" % self.name, target=self._pointer, args=(payload,))
- # x.start()
- #else:
- self._pointer(payload)
- if self._once:
- self._destroy = True
+
+from sleekxmpp.xmlstream.handler.base import BaseHandler
+
+
+class Callback(BaseHandler):
+
+ """
+ The Callback handler will execute a callback function with
+ matched stanzas.
+
+ The handler may execute the callback either during stream
+ processing or during the main event loop.
+
+ Callback functions are all executed in the same thread, so be
+ aware if you are executing functions that will block for extended
+ periods of time. Typically, you should signal your own events using the
+ SleekXMPP object's event() method to pass the stanza off to a threaded
+ event handler for further processing.
+
+ Methods:
+ prerun -- Overrides BaseHandler.prerun
+ run -- Overrides BaseHandler.run
+ """
+
+ def __init__(self, name, matcher, pointer, thread=False,
+ once=False, instream=False, stream=None):
+ """
+ Create a new callback handler.
+
+ Arguments:
+ name -- The name of the handler.
+ matcher -- A matcher object for matching stanza objects.
+ pointer -- The function to execute during callback.
+ thread -- DEPRECATED. Remains only for backwards compatibility.
+ once -- Indicates if the handler should be used only
+ once. Defaults to False.
+ instream -- Indicates if the callback should be executed
+ during stream processing instead of in the
+ main event loop.
+ stream -- The XMLStream instance this handler should monitor.
+ """
+ BaseHandler.__init__(self, name, matcher, stream)
+ self._pointer = pointer
+ self._once = once
+ self._instream = instream
+
+ def prerun(self, payload):
+ """
+ Execute the callback during stream processing, if
+ the callback was created with instream=True.
+
+ Overrides BaseHandler.prerun
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ BaseHandler.prerun(self, payload)
+ if self._instream:
+ self.run(payload, True)
+
+ def run(self, payload, instream=False):
+ """
+ Execute the callback function with the matched stanza payload.
+
+ Overrides BaseHandler.run
+
+ Arguments:
+ payload -- The matched stanza object.
+ instream -- Force the handler to execute during
+ stream processing. Used only by prerun.
+ Defaults to False.
+ """
+ if not self._instream or instream:
+ BaseHandler.run(self, payload)
+ self._pointer(payload)
+ if self._once:
+ self._destroy = True
diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py
index 7c4330a4..1e101ed3 100644
--- a/sleekxmpp/xmlstream/handler/waiter.py
+++ b/sleekxmpp/xmlstream/handler/waiter.py
@@ -5,32 +5,94 @@
See the file LICENSE for copying permission.
"""
-from . import base
+
+import logging
try:
- import queue
+ import queue
except ImportError:
- import Queue as queue
-import logging
-from .. stanzabase import StanzaBase
-
-class Waiter(base.BaseHandler):
-
- def __init__(self, name, matcher):
- base.BaseHandler.__init__(self, name, matcher)
- self._payload = queue.Queue()
-
- def prerun(self, payload):
- self._payload.put(payload)
-
- def run(self, payload):
- pass
-
- def wait(self, timeout=60):
- try:
- return self._payload.get(True, timeout)
- except queue.Empty:
- logging.warning("Timed out waiting for %s" % self.name)
- return False
-
- def checkDelete(self):
- return True
+ import Queue as queue
+
+from sleekxmpp.xmlstream import StanzaBase, RESPONSE_TIMEOUT
+from sleekxmpp.xmlstream.handler.base import BaseHandler
+
+
+class Waiter(BaseHandler):
+
+ """
+ The Waiter handler allows an event handler to block
+ until a particular stanza has been received. The handler
+ will either be given the matched stanza, or False if the
+ waiter has timed out.
+
+ Methods:
+ checkDelete -- Overrides BaseHandler.checkDelete
+ prerun -- Overrides BaseHandler.prerun
+ run -- Overrides BaseHandler.run
+ wait -- Wait for a stanza to arrive and return it to
+ an event handler.
+ """
+
+ def __init__(self, name, matcher, stream=None):
+ """
+ Create a new Waiter.
+
+ Arguments:
+ name -- The name of the waiter.
+ matcher -- A matcher object to detect the desired stanza.
+ stream -- Optional XMLStream instance to monitor.
+ """
+ BaseHandler.__init__(self, name, matcher, stream=stream)
+ self._payload = queue.Queue()
+
+ def prerun(self, payload):
+ """
+ Store the matched stanza.
+
+ Overrides BaseHandler.prerun
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ self._payload.put(payload)
+
+ def run(self, payload):
+ """
+ Do not process this handler during the main event loop.
+
+ Overrides BaseHandler.run
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ pass
+
+ def wait(self, timeout=RESPONSE_TIMEOUT):
+ """
+ Block an event handler while waiting for a stanza to arrive.
+
+ Be aware that this will impact performance if called from a
+ non-threaded event handler.
+
+ Will return either the received stanza, or False if the waiter
+ timed out.
+
+ Arguments:
+ timeout -- The number of seconds to wait for the stanza to
+ arrive. Defaults to the global default timeout
+ value sleekxmpp.xmlstream.RESPONSE_TIMEOUT.
+ """
+ try:
+ stanza = self._payload.get(True, timeout)
+ except queue.Empty:
+ stanza = False
+ logging.warning("Timed out waiting for %s" % self.name)
+ self.stream.removeHandler(self.name)
+ return stanza
+
+ def checkDelete(self):
+ """
+ Always remove waiters after use.
+
+ Overrides BaseHandler.checkDelete
+ """
+ return True
diff --git a/sleekxmpp/xmlstream/handler/xmlcallback.py b/sleekxmpp/xmlstream/handler/xmlcallback.py
index 67879dfe..11607ffb 100644
--- a/sleekxmpp/xmlstream/handler/xmlcallback.py
+++ b/sleekxmpp/xmlstream/handler/xmlcallback.py
@@ -5,10 +5,32 @@
See the file LICENSE for copying permission.
"""
-import threading
-from . callback import Callback
+
+from sleekxmpp.xmlstream.handler import Callback
+
class XMLCallback(Callback):
-
- def run(self, payload, instream=False):
- Callback.run(self, payload.xml, instream)
+
+ """
+ The XMLCallback class is identical to the normal Callback class,
+ except that XML contents of matched stanzas will be processed instead
+ of the stanza objects themselves.
+
+ Methods:
+ run -- Overrides Callback.run
+ """
+
+ def run(self, payload, instream=False):
+ """
+ Execute the callback function with the matched stanza's
+ XML contents, instead of the stanza itself.
+
+ Overrides BaseHandler.run
+
+ Arguments:
+ payload -- The matched stanza object.
+ instream -- Force the handler to execute during
+ stream processing. Used only by prerun.
+ Defaults to False.
+ """
+ Callback.run(self, payload.xml, instream)
diff --git a/sleekxmpp/xmlstream/handler/xmlwaiter.py b/sleekxmpp/xmlstream/handler/xmlwaiter.py
index cf90751d..5201caf3 100644
--- a/sleekxmpp/xmlstream/handler/xmlwaiter.py
+++ b/sleekxmpp/xmlstream/handler/xmlwaiter.py
@@ -5,9 +5,29 @@
See the file LICENSE for copying permission.
"""
-from . waiter import Waiter
+
+from sleekxmpp.xmlstream.handler import Waiter
+
class XMLWaiter(Waiter):
-
- def prerun(self, payload):
- Waiter.prerun(self, payload.xml)
+
+ """
+ The XMLWaiter class is identical to the normal Waiter class
+ except that it returns the XML contents of the stanza instead
+ of the full stanza object itself.
+
+ Methods:
+ prerun -- Overrides Waiter.prerun
+ """
+
+ def prerun(self, payload):
+ """
+ Store the XML contents of the stanza to return to the
+ waiting event handler.
+
+ Overrides Waiter.prerun
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ Waiter.prerun(self, payload.xml)
diff --git a/sleekxmpp/xmlstream/matcher/base.py b/sleekxmpp/xmlstream/matcher/base.py
index 51da0942..701ab32f 100644
--- a/sleekxmpp/xmlstream/matcher/base.py
+++ b/sleekxmpp/xmlstream/matcher/base.py
@@ -5,10 +5,30 @@
See the file LICENSE for copying permission.
"""
+
+
class MatcherBase(object):
- def __init__(self, criteria):
- self._criteria = criteria
-
- def match(self, xml):
- return False
+ """
+ Base class for stanza matchers. Stanza matchers are used to pick
+ stanzas out of the XML stream and pass them to the appropriate
+ stream handlers.
+ """
+
+ def __init__(self, criteria):
+ """
+ Create a new stanza matcher.
+
+ Arguments:
+ criteria -- Object to compare some aspect of a stanza
+ against.
+ """
+ self._criteria = criteria
+
+ def match(self, xml):
+ """
+ Check if a stanza matches the stored criteria.
+
+ Meant to be overridden.
+ """
+ return False
diff --git a/sleekxmpp/xmlstream/matcher/id.py b/sleekxmpp/xmlstream/matcher/id.py
index 43972c23..0c8ce2d8 100644
--- a/sleekxmpp/xmlstream/matcher/id.py
+++ b/sleekxmpp/xmlstream/matcher/id.py
@@ -5,9 +5,28 @@
See the file LICENSE for copying permission.
"""
-from . import base
-class MatcherId(base.MatcherBase):
-
- def match(self, xml):
- return xml['id'] == self._criteria
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatcherId(MatcherBase):
+
+ """
+ The ID matcher selects stanzas that have the same stanza 'id'
+ interface value as the desired ID.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Compare the given stanza's 'id' attribute to the stored
+ id value.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza to compare against.
+ """
+ return xml['id'] == self._criteria
diff --git a/sleekxmpp/xmlstream/matcher/many.py b/sleekxmpp/xmlstream/matcher/many.py
index ff0c4e4d..f470ec9c 100644
--- a/sleekxmpp/xmlstream/matcher/many.py
+++ b/sleekxmpp/xmlstream/matcher/many.py
@@ -5,13 +5,36 @@
See the file LICENSE for copying permission.
"""
-from . import base
-from xml.etree import cElementTree
-class MatchMany(base.MatcherBase):
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
- def match(self, xml):
- for m in self._criteria:
- if m.match(xml):
- return True
- return False
+
+class MatchMany(MatcherBase):
+
+ """
+ The MatchMany matcher may compare a stanza against multiple
+ criteria. It is essentially an OR relation combining multiple
+ matchers.
+
+ Each of the criteria must implement a match() method.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Match a stanza against multiple criteria. The match is successful
+ if one of the criteria matches.
+
+ Each of the criteria must implement a match() method.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza object to compare against.
+ """
+ for m in self._criteria:
+ if m.match(xml):
+ return True
+ return False
diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py
index e315445d..f8ff283d 100644
--- a/sleekxmpp/xmlstream/matcher/stanzapath.py
+++ b/sleekxmpp/xmlstream/matcher/stanzapath.py
@@ -5,10 +5,34 @@
See the file LICENSE for copying permission.
"""
-from . import base
-from xml.etree import cElementTree
-class StanzaPath(base.MatcherBase):
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
- def match(self, stanza):
- return stanza.match(self._criteria)
+
+class StanzaPath(MatcherBase):
+
+ """
+ The StanzaPath matcher selects stanzas that match a given "stanza path",
+ which is similar to a normal XPath except that it uses the interfaces and
+ plugins of the stanza instead of the actual, underlying XML.
+
+ In most cases, the stanza path and XPath should be identical, but be
+ aware that differences may occur.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, stanza):
+ """
+ Compare a stanza against a "stanza path". A stanza path is similar to
+ an XPath expression, but uses the stanza's interfaces and plugins
+ instead of the underlying XML. For most cases, the stanza path and
+ XPath should be identical, but be aware that differences may occur.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ stanza -- The stanza object to compare against.
+ """
+ return stanza.match(self._criteria)
diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py
index 89fd6422..2967a2af 100644
--- a/sleekxmpp/xmlstream/matcher/xmlmask.py
+++ b/sleekxmpp/xmlstream/matcher/xmlmask.py
@@ -5,63 +5,151 @@
See the file LICENSE for copying permission.
"""
-from . import base
-from xml.etree import cElementTree
+
from xml.parsers.expat import ExpatError
-ignore_ns = False
-
-class MatchXMLMask(base.MatcherBase):
-
- def __init__(self, criteria):
- base.MatcherBase.__init__(self, criteria)
- if type(criteria) == type(''):
- self._criteria = cElementTree.fromstring(self._criteria)
- self.default_ns = 'jabber:client'
-
- def setDefaultNS(self, ns):
- self.default_ns = ns
-
- def match(self, xml):
- if hasattr(xml, 'xml'):
- xml = xml.xml
- return self.maskcmp(xml, self._criteria, True)
-
- def maskcmp(self, source, maskobj, use_ns=False, default_ns='__no_ns__'):
- """maskcmp(xmlobj, maskobj):
- Compare etree xml object to etree xml object mask"""
- use_ns = not ignore_ns
- #TODO require namespaces
- if source == None: #if element not found (happens during recursive check below)
- return False
- if not hasattr(maskobj, 'attrib'): #if the mask is a string, make it an xml obj
- try:
- maskobj = cElementTree.fromstring(maskobj)
- except ExpatError:
- logging.log(logging.WARNING, "Expat error: %s\nIn parsing: %s" % ('', maskobj))
- if not use_ns and source.tag.split('}', 1)[-1] != maskobj.tag.split('}', 1)[-1]: # strip off ns and compare
- return False
- if use_ns and (source.tag != maskobj.tag and "{%s}%s" % (self.default_ns, maskobj.tag) != source.tag ):
- return False
- if maskobj.text and source.text != maskobj.text:
- return False
- for attr_name in maskobj.attrib: #compare attributes
- if source.attrib.get(attr_name, "__None__") != maskobj.attrib[attr_name]:
- return False
- #for subelement in maskobj.getiterator()[1:]: #recursively compare subelements
- for subelement in maskobj: #recursively compare subelements
- if use_ns:
- if not self.maskcmp(source.find(subelement.tag), subelement, use_ns):
- return False
- else:
- if not self.maskcmp(self.getChildIgnoreNS(source, subelement.tag), subelement, use_ns):
- return False
- return True
-
- def getChildIgnoreNS(self, xml, tag):
- tag = tag.split('}')[-1]
- try:
- idx = [c.tag.split('}')[-1] for c in xml.getchildren()].index(tag)
- except ValueError:
- return None
- return xml.getchildren()[idx]
+from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+# Flag indicating if the builtin XPath matcher should be used, which
+# uses namespaces, or a custom matcher that ignores namespaces.
+# Changing this will affect ALL XMLMask matchers.
+IGNORE_NS = False
+
+
+class MatchXMLMask(MatcherBase):
+
+ """
+ The XMLMask matcher selects stanzas whose XML matches a given
+ XML pattern, or mask. For example, message stanzas with body elements
+ could be matched using the mask:
+
+ <message xmlns="jabber:client"><body /></message>
+
+ Use of XMLMask is discouraged, and XPath or StanzaPath should be used
+ instead.
+
+ The use of namespaces in the mask comparison is controlled by
+ IGNORE_NS. Setting IGNORE_NS to True will disable namespace based matching
+ for ALL XMLMask matchers.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ setDefaultNS -- Set the default namespace for the mask.
+ """
+
+ def __init__(self, criteria):
+ """
+ Create a new XMLMask matcher.
+
+ Arguments:
+ criteria -- Either an XML object or XML string to use as a mask.
+ """
+ MatcherBase.__init__(self, criteria)
+ if isinstance(criteria, str):
+ self._criteria = ET.fromstring(self._criteria)
+ self.default_ns = 'jabber:client'
+
+ def setDefaultNS(self, ns):
+ """
+ Set the default namespace to use during comparisons.
+
+ Arguments:
+ ns -- The new namespace to use as the default.
+ """
+ self.default_ns = ns
+
+ def match(self, xml):
+ """
+ Compare a stanza object or XML object against the stored XML mask.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza object or XML object to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ return self._mask_cmp(xml, self._criteria, True)
+
+ def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'):
+ """
+ Compare an XML object against an XML mask.
+
+ Arguments:
+ source -- The XML object to compare against the mask.
+ mask -- The XML object serving as the mask.
+ use_ns -- Indicates if namespaces should be respected during
+ the comparison.
+ default_ns -- The default namespace to apply to elements that
+ do not have a specified namespace.
+ Defaults to "__no_ns__".
+ """
+ use_ns = not IGNORE_NS
+
+ if source is None:
+ # If the element was not found. May happend during recursive calls.
+ return False
+
+ # Convert the mask to an XML object if it is a string.
+ if not hasattr(mask, 'attrib'):
+ try:
+ mask = ET.fromstring(mask)
+ except ExpatError:
+ logging.log(logging.WARNING,
+ "Expat error: %s\nIn parsing: %s" % ('', mask))
+
+ if not use_ns:
+ # Compare the element without using namespaces.
+ source_tag = source.tag.split('}', 1)[-1]
+ mask_tag = mask.tag.split('}', 1)[-1]
+ if source_tag != mask_tag:
+ return False
+ else:
+ # Compare the element using namespaces
+ mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag)
+ if source.tag not in [mask.tag, mask_ns_tag]:
+ return False
+
+ # If the mask includes text, compare it.
+ if mask.text and source.text != mask.text:
+ return False
+
+ # Compare attributes. The stanza must include the attributes
+ # defined by the mask, but may include others.
+ for name, value in mask.attrib.items():
+ if source.attrib.get(name, "__None__") != value:
+ return False
+
+ # Recursively check subelements.
+ for subelement in mask:
+ if use_ns:
+ if not self._mask_cmp(source.find(subelement.tag),
+ subelement, use_ns):
+ return False
+ else:
+ if not self._mask_cmp(self._get_child(source, subelement.tag),
+ subelement, use_ns):
+ return False
+
+ # Everything matches.
+ return True
+
+ def _get_child(self, xml, tag):
+ """
+ Return a child element given its tag, ignoring namespace values.
+
+ Returns None if the child was not found.
+
+ Arguments:
+ xml -- The XML object to search for the given child tag.
+ tag -- The name of the subelement to find.
+ """
+ tag = tag.split('}')[-1]
+ try:
+ children = [c.tag.split('}')[-1] for c in xml.getchildren()]
+ index = children.index(tag)
+ except ValueError:
+ return None
+ return xml.getchildren()[index]
diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py
index 7f3d20be..669c9f16 100644
--- a/sleekxmpp/xmlstream/matcher/xpath.py
+++ b/sleekxmpp/xmlstream/matcher/xpath.py
@@ -5,30 +5,75 @@
See the file LICENSE for copying permission.
"""
-from . import base
-from xml.etree import cElementTree
-
-ignore_ns = False
-
-class MatchXPath(base.MatcherBase):
-
- def match(self, xml):
- if hasattr(xml, 'xml'):
- xml = xml.xml
- x = cElementTree.Element('x')
- x.append(xml)
- if not ignore_ns:
- if x.find(self._criteria) is not None:
- return True
- return False
- else:
- criteria = [c.split('}')[-1] for c in self._criteria.split('/')]
- xml = x
- for tag in criteria:
- children = [c.tag.split('}')[-1] for c in xml.getchildren()]
- try:
- idx = children.index(tag)
- except ValueError:
- return False
- xml = xml.getchildren()[idx]
- return True
+
+from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+# Flag indicating if the builtin XPath matcher should be used, which
+# uses namespaces, or a custom matcher that ignores namespaces.
+# Changing this will affect ALL XPath matchers.
+IGNORE_NS = False
+
+
+class MatchXPath(MatcherBase):
+
+ """
+ The XPath matcher selects stanzas whose XML contents matches a given
+ XPath expression.
+
+ Note that using this matcher may not produce expected behavior when using
+ attribute selectors. For Python 2.6 and 3.1, the ElementTree find method
+ does not support the use of attribute selectors. If you need to support
+ Python 2.6 or 3.1, it might be more useful to use a StanzaPath matcher.
+
+ If the value of IGNORE_NS is set to true, then XPath expressions will
+ be matched without using namespaces.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Compare a stanza's XML contents to an XPath expression.
+
+ If the value of IGNORE_NS is set to true, then XPath expressions
+ will be matched without using namespaces.
+
+ Note that in Python 2.6 and 3.1 the ElementTree find method does
+ not support attribute selectors in the XPath expression.
+
+ Arguments:
+ xml -- The stanza object to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ x = ET.Element('x')
+ x.append(xml)
+
+ if not IGNORE_NS:
+ # Use builtin, namespace respecting, XPath matcher.
+ if x.find(self._criteria) is not None:
+ return True
+ return False
+ else:
+ # Remove namespaces from the XPath expression.
+ criteria = []
+ for ns_block in self._criteria.split('{'):
+ criteria.extend(ns_block.split('}')[-1].split('/'))
+
+ # Walk the XPath expression.
+ xml = x
+ for tag in criteria:
+ if not tag:
+ # Skip empty tag name artifacts from the cleanup phase.
+ continue
+
+ children = [c.tag.split('}')[-1] for c in xml.getchildren()]
+ try:
+ index = children.index(tag)
+ except ValueError:
+ return False
+ xml = xml.getchildren()[index]
+ return True
diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py
index 8814df78..f8242005 100644
--- a/sleekxmpp/xmlstream/stanzabase.py
+++ b/sleekxmpp/xmlstream/stanzabase.py
@@ -586,15 +586,16 @@ class ElementBase(object):
string or a list of element names with attribute checks.
"""
if isinstance(xpath, str):
- xpath = xpath.split('/')
+ xpath = self._fix_ns(xpath, split=True, propagate_ns=False)
+
# Extract the tag name and attribute checks for the first XPath node.
components = xpath[0].split('@')
tag = components[0]
attributes = components[1:]
- if tag not in (self.name, "{%s}%s" % (self.namespace, self.name),
- self.plugins, self.plugin_attrib):
+ 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:
# The requested tag is not in this stanza, so no match.
return False
@@ -613,6 +614,12 @@ class ElementBase(object):
if self[name] != value:
return False
+ # Check sub interfaces.
+ if len(xpath) > 1:
+ next_tag = xpath[1]
+ if next_tag in self.sub_interfaces and self[next_tag]:
+ return True
+
# Attempt to continue matching the XPath using the stanza's plugins.
if not matched_substanzas and len(xpath) > 1:
# Convert {namespace}tag@attribs to just tag
@@ -754,30 +761,45 @@ class ElementBase(object):
"""
return self
- def _fix_ns(self, xpath, split=False):
+ def _fix_ns(self, xpath, split=False, propagate_ns=True):
"""
Apply the stanza's namespace to elements in an XPath expression.
Arguments:
- xpath -- The XPath expression to fix with namespaces.
- split -- Indicates if the fixed XPath should be left as a
- list of element names with namespaces. Defaults to
- False, which returns a flat string path.
+ xpath -- The XPath expression to fix with namespaces.
+ split -- Indicates if the fixed XPath should be left as a
+ list of element names with namespaces. Defaults to
+ False, which returns a flat string path.
+ propagate_ns -- Overrides propagating parent element namespaces
+ to child elements. Useful if you wish to simply
+ split an XPath that has non-specified namespaces,
+ and child and parent namespaces are known not to
+ always match. Defaults to True.
"""
fixed = []
+ # Split the XPath into a series of blocks, where a block
+ # is started by an element with a namespace.
ns_blocks = xpath.split('{')
for ns_block in ns_blocks:
if '}' in ns_block:
+ # Apply the found namespace to following elements
+ # that do not have namespaces.
namespace = ns_block.split('}')[0]
elements = ns_block.split('}')[1].split('/')
else:
+ # Apply the stanza's namespace to the following
+ # elements since no namespace was provided.
namespace = self.namespace
elements = ns_block.split('/')
for element in elements:
if element:
- fixed.append('{%s}%s' % (namespace,
- element))
+ # Skip empty entry artifacts from splitting.
+ if propagate_ns:
+ tag = '{%s}%s' % (namespace, element)
+ else:
+ tag = element
+ fixed.append(tag)
if split:
return fixed
return '/'.join(fixed)
@@ -886,6 +908,48 @@ class ElementBase(object):
class StanzaBase(ElementBase):
+
+ """
+ StanzaBase provides the foundation for all other stanza objects used by
+ SleekXMPP, and defines a basic set of interfaces common to nearly
+ all stanzas. These interfaces are the 'id', 'type', 'to', and 'from'
+ attributes. An additional interface, 'payload', is available to access
+ the XML contents of the stanza. Most stanza objects will provided more
+ specific interfaces, however.
+
+ Stanza Interface:
+ from -- A JID object representing the sender's JID.
+ id -- An optional id value that can be used to associate stanzas
+ with their replies.
+ payload -- The XML contents of the stanza.
+ to -- A JID object representing the recipient's JID.
+ type -- The type of stanza, typically will be 'normal', 'error',
+ 'get', or 'set', etc.
+
+ Attributes:
+ stream -- The XMLStream instance that will handle sending this stanza.
+ tag -- The namespaced version of the stanza's name.
+
+ Methods:
+ setType -- Set the type of the stanza.
+ getTo -- Return the stanza recipients JID.
+ setTo -- Set the stanza recipient's JID.
+ getFrom -- Return the stanza sender's JID.
+ setFrom -- Set the stanza sender's JID.
+ getPayload -- Return the stanza's XML contents.
+ setPayload -- Append to the stanza's XML contents.
+ delPayload -- Remove the stanza's XML contents.
+ clear -- Reset the stanza's XML contents.
+ reply -- Reset the stanza and modify the 'to' and 'from'
+ attributes to prepare for sending a reply.
+ error -- Set the stanza's type to 'error'.
+ unhandled -- Callback for when the stanza is not handled by a
+ stream handler.
+ exception -- Callback for if an exception is raised while
+ handling the stanza.
+ send -- Send the stanza using the stanza's stream.
+ """
+
name = 'stanza'
namespace = 'jabber:client'
interfaces = set(('type', 'to', 'from', 'id', 'payload'))
@@ -894,6 +958,17 @@ class StanzaBase(ElementBase):
def __init__(self, stream=None, xml=None, stype=None,
sto=None, sfrom=None, sid=None):
+ """
+ Create a new stanza.
+
+ Arguments:
+ stream -- Optional XMLStream responsible for sending this stanza.
+ xml -- Optional XML contents to initialize stanza values.
+ stype -- Optional stanza type value.
+ sto -- Optional string or JID object of the recipient's JID.
+ sfrom -- Optional string or JID object of the sender's JID.
+ sid -- Optional ID value for the stanza.
+ """
self.stream = stream
if stream is not None:
self.namespace = stream.default_ns
@@ -907,22 +982,73 @@ class StanzaBase(ElementBase):
self.tag = "{%s}%s" % (self.namespace, self.name)
def setType(self, value):
+ """
+ Set the stanza's 'type' attribute.
+
+ Only type values contained in StanzaBase.types are accepted.
+
+ Arguments:
+ value -- One of the values contained in StanzaBase.types
+ """
if value in self.types:
self.xml.attrib['type'] = value
return self
+ def getTo(self):
+ """Return the value of the stanza's 'to' attribute."""
+ return JID(self._getAttr('to'))
+
+ def setTo(self, value):
+ """
+ Set the 'to' attribute of the stanza.
+
+ Arguments:
+ value -- A string or JID object representing the recipient's JID.
+ """
+ return self._setAttr('to', str(value))
+
+ def getFrom(self):
+ """Return the value of the stanza's 'from' attribute."""
+ return JID(self._getAttr('from'))
+
+ def setFrom(self, value):
+ """
+ Set the 'from' attribute of the stanza.
+
+ Arguments:
+ from -- A string or JID object representing the sender's JID.
+ """
+ return self._setAttr('from', str(value))
+
def getPayload(self):
+ """Return a list of XML objects contained in the stanza."""
return self.xml.getchildren()
def setPayload(self, value):
- self.xml.append(value)
+ """
+ Add XML content to the stanza.
+
+ Arguments:
+ value -- Either an XML or a stanza object, or a list
+ of XML or stanza objects.
+ """
+ if not isinstance(value, list):
+ value = [value]
+ for val in value:
+ self.append(val)
return self
def delPayload(self):
+ """Remove the XML contents of the stanza."""
self.clear()
return self
def clear(self):
+ """
+ Remove all XML element contents and plugins.
+
+ Any attribute values will be preserved.
+ """
for child in self.xml.getchildren():
self.xml.remove(child)
for plugin in list(self.plugins.keys()):
@@ -930,6 +1056,12 @@ class StanzaBase(ElementBase):
return self
def reply(self):
+ """
+ Reset the stanza and swap its 'from' and 'to' attributes to prepare
+ for sending a reply stanza.
+
+ For client streams, the 'from' attribute is removed.
+ """
# if it's a component, use from
if self.stream and hasattr(self.stream, "is_component") and \
self.stream.is_component:
@@ -941,35 +1073,42 @@ class StanzaBase(ElementBase):
return self
def error(self):
+ """Set the stanza's type to 'error'."""
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):
+ """
+ Called when no handlers have been registered to process this
+ stanza.
+
+ Meant to be overridden.
+ """
pass
def exception(self, e):
+ """
+ Handle exceptions raised during stanza processing.
+
+ Meant to be overridden.
+ """
logging.exception('Error handling {%s}%s stanza' % (self.namespace,
self.name))
def send(self):
+ """Queue the stanza to be sent on the XML stream."""
self.stream.sendRaw(self.__str__())
def __copy__(self):
- return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream)
+ """
+ Return a copy of the stanza object that does not share the
+ same underlying XML object, but does share the same XML stream.
+ """
+ return self.__class__(xml=copy.deepcopy(self.xml),
+ stream=self.stream)
def __str__(self):
+ """Serialize the stanza's XML to a string."""
return tostring(self.xml, xmlns='',
stanza_ns=self.namespace,
stream=self.stream)
diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py
index bf39bb33..28aee2b4 100644
--- a/sleekxmpp/xmlstream/xmlstream.py
+++ b/sleekxmpp/xmlstream/xmlstream.py
@@ -139,8 +139,7 @@ class XMLStream(object):
self.socket = ssl.wrap_socket(self.socket, ssl_version=ssl.PROTOCOL_TLSv1, do_handshake_on_connect=False)
self.socket.do_handshake()
if sys.version_info < (3,0):
- from . filesocket import filesocket
- self.filesocket = filesocket(self.socket)
+ self.filesocket = filesocket.FileSocket(self.socket)
else:
self.filesocket = self.socket.makefile('rb', 0)
return True
@@ -358,8 +357,10 @@ class XMLStream(object):
return False
def registerHandler(self, handler, before=None, after=None):
- "Add handler with matcher class and parameters."
- self.__handlers.append(handler)
+ "Add handler with matcher class and parameters."
+ if handler.stream is None:
+ self.__handlers.append(handler)
+ handler.stream = self
def removeHandler(self, name):
"Removes the handler."
@@ -367,8 +368,10 @@ class XMLStream(object):
for handler in self.__handlers:
if handler.name == name:
self.__handlers.pop(idx)
- return
+ return True
idx += 1
+ return False
+
def registerStanza(self, stanza_class):
"Adds stanza. If root stanzas build stanzas sent in events while non-root stanzas build substanza objects."