diff options
Diffstat (limited to 'sleekxmpp/xmlstream')
-rw-r--r-- | sleekxmpp/xmlstream/filesocket.py | 36 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/handler/base.py | 93 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/handler/callback.py | 104 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/handler/waiter.py | 116 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/handler/xmlcallback.py | 32 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/handler/xmlwaiter.py | 28 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/matcher/base.py | 30 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/matcher/id.py | 29 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/matcher/many.py | 39 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/matcher/stanzapath.py | 34 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/matcher/xmlmask.py | 204 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/matcher/xpath.py | 99 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/stanzabase.py | 187 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/xmlstream.py | 13 |
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." |