From d979b5f2b9cb38bdd145c95526358238449fa067 Mon Sep 17 00:00:00 2001
From: Lance Stout <lancestout@gmail.com>
Date: Wed, 28 Dec 2011 10:07:33 -0500
Subject: Add caching support to xep_0030.

New plugin configuration options:

    use_cache    - Enable caching disco info results. Defaults to True
    wrap_results - Always return disco results in an Iq stanza. Defaults
                   to False

Node handler changes:

    Handlers now take four arguments: jid, node, ifrom, data

    Most older style handlers will still work, depending on if they
    raise a TypeError for incorrect number of arguments. Handlers that
    used *args may not work.

New get_info options:

    cached - Passing cached=True to get_info() will attempt to load
             results from the cache. If nothing is found, a query
             will be sent as normal. If set to False, the cache
             will be skipped, even if it contains results.

New method:

    supports() - Given a JID/node pair and a feature, return True
                 if the feature is supported, False if not, and
                 None if there was a timeout. By default, the search
                 will use the cache.
---
 sleekxmpp/plugins/xep_0030/disco.py          | 173 ++++++++++++++----
 sleekxmpp/plugins/xep_0030/static.py         | 253 ++++++++++++++++++---------
 sleekxmpp/plugins/xep_0128/extended_disco.py |   6 +-
 sleekxmpp/plugins/xep_0128/static.py         |  37 ++--
 tests/test_stream_xep_0030.py                |  16 +-
 5 files changed, 339 insertions(+), 146 deletions(-)

diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py
index 53086d4e..503631ec 100644
--- a/sleekxmpp/plugins/xep_0030/disco.py
+++ b/sleekxmpp/plugins/xep_0030/disco.py
@@ -10,7 +10,7 @@ import logging
 
 import sleekxmpp
 from sleekxmpp import Iq
-from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout
 from sleekxmpp.plugins.base import base_plugin
 from sleekxmpp.xmlstream.handler import Callback
 from sleekxmpp.xmlstream.matcher import StanzaPath
@@ -108,11 +108,16 @@ class xep_0030(base_plugin):
 
         self.static = StaticDisco(self.xmpp)
 
+        self.use_cache = self.config.get('use_cache', True)
+        self.wrap_results = self.config.get('wrap_results', False)
+
         self._disco_ops = ['get_info', 'set_identities', 'set_features',
                            'get_items', 'set_items', 'del_items',
                            'add_identity', 'del_identity', 'add_feature',
                            'del_feature', 'add_item', 'del_item',
-                           'del_identities', 'del_features']
+                           'del_identities', 'del_features', 
+                           'cache_info', 'get_cached_info']
+        
         self.default_handlers = {}
         self._handlers = {}
         for op in self._disco_ops:
@@ -237,7 +242,47 @@ class xep_0030(base_plugin):
             self.del_node_handler(op, jid, node)
             self.set_node_handler(op, jid, node, self.default_handlers[op])
 
-    def get_info(self, jid=None, node=None, local=False, **kwargs):
+    def supports(self, jid=None, node=None, feature=None, local=False, 
+                       cached=True, ifrom=None):
+        """
+        Check if a JID supports a given feature.
+
+        Return values:
+            True  -- The feature is supported
+            False -- The feature is not listed as supported
+            None  -- Nothing could be found due to a timeout
+
+        Arguments:
+            jid      -- Request info from this JID.
+            node     -- The particular node to query.
+            feature  -- The name of the feature to check.
+            local    -- If true, then the query is for a JID/node
+                        combination handled by this Sleek instance and
+                        no stanzas need to be sent.
+                        Otherwise, a disco stanza must be sent to the
+                        remove JID to retrieve the info.
+            cached   -- If true, then look for the disco info data from
+                        the local cache system. If no results are found,
+                        send the query as usual. The self.use_cache
+                        setting must be set to true for this option to
+                        be useful. If set to false, then the cache will
+                        be skipped, even if a result has already been
+                        cached. Defaults to false.
+            ifrom    -- Specifiy the sender's JID.
+        """
+        try:
+            info = self.get_info(jid=jid, node=node, local=local,
+                                 cached=cached, ifrom=ifrom)
+            info = self._wrap(ifrom, jid, info, True)
+            features = info['disco_info']['features']
+            return feature in features
+        except IqError:
+            return False
+        except IqTimeout:
+            return None
+
+    def get_info(self, jid=None, node=None, local=False, 
+                       cached=None, **kwargs):
         """
         Retrieve the disco#info results from a given JID/node combination.
 
@@ -257,6 +302,13 @@ class xep_0030(base_plugin):
                         no stanzas need to be sent.
                         Otherwise, a disco stanza must be sent to the
                         remove JID to retrieve the info.
+            cached   -- If true, then look for the disco info data from
+                        the local cache system. If no results are found,
+                        send the query as usual. The self.use_cache
+                        setting must be set to true for this option to
+                        be useful. If set to false, then the cache will
+                        be skipped, even if a result has already been
+                        cached. Defaults to false.
             ifrom    -- Specifiy the sender's JID.
             block    -- If true, block and wait for the stanzas' reply.
             timeout  -- The time in seconds to block while waiting for
@@ -269,9 +321,19 @@ class xep_0030(base_plugin):
         if local or jid is None:
             log.debug("Looking up local disco#info data " + \
                       "for %s, node %s.", jid, node)
-            info = self._run_node_handler('get_info', jid, node, kwargs)
-            return self._fix_default_info(info)
+            info = self._run_node_handler('get_info', 
+                    jid, node, kwargs.get('ifrom', None), kwargs)
+            info = self._fix_default_info(info)
+            return self._wrap(kwargs.get('ifrom', None), jid, info)
 
+        if cached:
+            log.debug("Looking up cached disco#info data " + \
+                      "for %s, node %s.", jid, node)
+            info = self._run_node_handler('get_cached_info', 
+                    jid, node, kwargs.get('ifrom', None), kwargs)
+            if info is not None:
+                return self._wrap(kwargs.get('ifrom', None), jid, info)
+            
         iq = self.xmpp.Iq()
         # Check dfrom parameter for backwards compatibility
         iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
@@ -314,7 +376,9 @@ class xep_0030(base_plugin):
                         Otherwise the parameter is ignored.
         """
         if local or jid is None:
-            return self._run_node_handler('get_items', jid, node, kwargs)
+            items = self._run_node_handler('get_items', 
+                    jid, node, kwargs.get('ifrom', None), kwargs)
+            return self._wrap(kwargs.get('ifrom', None), jid, items)
 
         iq = self.xmpp.Iq()
         # Check dfrom parameter for backwards compatibility
@@ -341,7 +405,7 @@ class xep_0030(base_plugin):
             node  -- Optional node to modify.
             items -- A series of items in tuple format.
         """
-        self._run_node_handler('set_items', jid, node, kwargs)
+        self._run_node_handler('set_items', jid, node, None, kwargs)
 
     def del_items(self, jid=None, node=None, **kwargs):
         """
@@ -351,7 +415,7 @@ class xep_0030(base_plugin):
             jid  -- The JID to modify.
             node -- Optional node to modify.
         """
-        self._run_node_handler('del_items', jid, node, kwargs)
+        self._run_node_handler('del_items', jid, node, None, kwargs)
 
     def add_item(self, jid='', name='', node=None, subnode='', ijid=None):
         """
@@ -372,7 +436,7 @@ class xep_0030(base_plugin):
         kwargs = {'ijid': jid,
                   'name': name,
                   'inode': subnode}
-        self._run_node_handler('add_item', ijid, node, kwargs)
+        self._run_node_handler('add_item', ijid, node, None, kwargs)
 
     def del_item(self, jid=None, node=None, **kwargs):
         """
@@ -384,7 +448,7 @@ class xep_0030(base_plugin):
             ijid  -- The item's JID.
             inode -- The item's node.
         """
-        self._run_node_handler('del_item', jid, node, kwargs)
+        self._run_node_handler('del_item', jid, node, None, kwargs)
 
     def add_identity(self, category='', itype='', name='',
                      node=None, jid=None, lang=None):
@@ -411,7 +475,7 @@ class xep_0030(base_plugin):
                   'itype': itype,
                   'name': name,
                   'lang': lang}
-        self._run_node_handler('add_identity', jid, node, kwargs)
+        self._run_node_handler('add_identity', jid, node, None, kwargs)
 
     def add_feature(self, feature, node=None, jid=None):
         """
@@ -423,7 +487,7 @@ class xep_0030(base_plugin):
             jid     -- The JID to modify.
         """
         kwargs = {'feature': feature}
-        self._run_node_handler('add_feature', jid, node, kwargs)
+        self._run_node_handler('add_feature', jid, node, None, kwargs)
 
     def del_identity(self, jid=None, node=None, **kwargs):
         """
@@ -437,7 +501,7 @@ class xep_0030(base_plugin):
             name     -- Optional, human readable name for the identity.
             lang     -- Optional, the identity's xml:lang value.
         """
-        self._run_node_handler('del_identity', jid, node, kwargs)
+        self._run_node_handler('del_identity', jid, node, None, kwargs)
 
     def del_feature(self, jid=None, node=None, **kwargs):
         """
@@ -448,7 +512,7 @@ class xep_0030(base_plugin):
             node    -- The node to modify.
             feature -- The feature's namespace.
         """
-        self._run_node_handler('del_feature', jid, node, kwargs)
+        self._run_node_handler('del_feature', jid, node, None, kwargs)
 
     def set_identities(self, jid=None, node=None, **kwargs):
         """
@@ -463,7 +527,7 @@ class xep_0030(base_plugin):
             identities -- A set of identities in tuple form.
             lang       -- Optional, xml:lang value.
         """
-        self._run_node_handler('set_identities', jid, node, kwargs)
+        self._run_node_handler('set_identities', jid, node, None, kwargs)
 
     def del_identities(self, jid=None, node=None, **kwargs):
         """
@@ -478,7 +542,7 @@ class xep_0030(base_plugin):
             lang -- Optional. If given, only remove identities
                     using this xml:lang value.
         """
-        self._run_node_handler('del_identities', jid, node, kwargs)
+        self._run_node_handler('del_identities', jid, node, None, kwargs)
 
     def set_features(self, jid=None, node=None, **kwargs):
         """
@@ -490,7 +554,7 @@ class xep_0030(base_plugin):
             node     -- The node to modify.
             features -- The new set of supported features.
         """
-        self._run_node_handler('set_features', jid, node, kwargs)
+        self._run_node_handler('set_features', jid, node, None, kwargs)
 
     def del_features(self, jid=None, node=None, **kwargs):
         """
@@ -500,9 +564,9 @@ class xep_0030(base_plugin):
             jid  -- The JID to modify.
             node -- The node to modify.
         """
-        self._run_node_handler('del_features', jid, node, kwargs)
+        self._run_node_handler('del_features', jid, node, None, kwargs)
 
-    def _run_node_handler(self, htype, jid, node, data={}):
+    def _run_node_handler(self, htype, jid, node, ifrom, data={}):
         """
         Execute the most specific node handler for the given
         JID/node combination.
@@ -521,14 +585,28 @@ class xep_0030(base_plugin):
         if node is None:
             node = ''
 
-        if self._handlers[htype]['node'].get((jid, node), False):
-            return self._handlers[htype]['node'][(jid, node)](jid, node, data)
-        elif self._handlers[htype]['jid'].get(jid, False):
-            return self._handlers[htype]['jid'][jid](jid, node, data)
-        elif self._handlers[htype]['global']:
-            return self._handlers[htype]['global'](jid, node, data)
-        else:
-            return None
+        try:
+            args = (jid, node, ifrom, data)
+            if self._handlers[htype]['node'].get((jid, node), False):
+                return self._handlers[htype]['node'][(jid, node)](*args)
+            elif self._handlers[htype]['jid'].get(jid, False):
+                return self._handlers[htype]['jid'][jid](*args)
+            elif self._handlers[htype]['global']:
+                return self._handlers[htype]['global'](*args)
+            else:
+                return None
+        except TypeError:
+            # To preserve backward compatibility, drop the ifrom parameter
+            # for existing handlers that don't understand it.
+            args = (jid, node, data)
+            if self._handlers[htype]['node'].get((jid, node), False):
+                return self._handlers[htype]['node'][(jid, node)](*args)
+            elif self._handlers[htype]['jid'].get(jid, False):
+                return self._handlers[htype]['jid'][jid](*args)
+            elif self._handlers[htype]['global']:
+                return self._handlers[htype]['global'](*args)
+            else:
+                return None
 
     def _handle_disco_info(self, iq):
         """
@@ -550,6 +628,7 @@ class xep_0030(base_plugin):
             info = self._run_node_handler('get_info',
                                           jid,
                                           iq['disco_info']['node'],
+                                          iq['from'],
                                           iq)
             if isinstance(info, Iq):
                 info.send()
@@ -560,8 +639,16 @@ class xep_0030(base_plugin):
                     iq.set_payload(info.xml)
                 iq.send()
         elif iq['type'] == 'result':
-            log.debug("Received disco info result from" + \
-                      "%s to %s.", iq['from'], iq['to'])
+            log.debug("Received disco info result from " + \
+                      "<%s> to <%s>.", iq['from'], iq['to'])
+            if self.use_cache:
+                log.debug("Caching disco info result from " \
+                      "<%s> to <%s>.", iq['from'], iq['to'])
+                self._run_node_handler('cache_info',
+                                       iq['from'].full,
+                                       iq['disco_info']['node'],
+                                       iq['to'].full,
+                                       iq)
             self.xmpp.event('disco_info', iq)
 
     def _handle_disco_items(self, iq):
@@ -583,6 +670,7 @@ class xep_0030(base_plugin):
             items = self._run_node_handler('get_items',
                                           jid,
                                           iq['disco_items']['node'],
+                                          iq['from'].full,
                                           iq)
             if isinstance(items, Iq):
                 items.send()
@@ -607,6 +695,9 @@ class xep_0030(base_plugin):
         Arguments:
             info -- The disco#info quest (not the full Iq stanza) to modify.
         """
+        result = info
+        if isinstance(info, Iq):
+            info = iq['disco_info']
         if not info['node']:
             if not info['identities']:
                 if self.xmpp.is_component:
@@ -621,7 +712,29 @@ class xep_0030(base_plugin):
                 log.debug("No features found for this entity." + \
                           "Using default disco#info feature.")
                 info.add_feature(info.namespace)
-        return info
+        return result
+
+    def _wrap(self, ito, ifrom, payload, force=False):
+        """
+        Ensure that results are wrapped in an Iq stanza
+        if self.wrap_results has been set to True.
+
+        Arguments:
+            ito     -- The JID to use as the 'to' value
+            ifrom   -- The JID to use as the 'from' value
+            payload -- The disco data to wrap
+            force   -- Force wrapping, regardless of self.wrap_results
+        """
+        if (force or self.wrap_results) and not isinstance(payload, Iq):
+            iq = self.xmpp.Iq()
+            # Since we're simulating a result, we have to treat
+            # the 'from' and 'to' values opposite the normal way.
+            iq['to'] = self.xmpp.boundjid if ito is None else ito
+            iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom
+            iq['type'] = 'result'
+            iq.append(payload)
+            return iq
+        return payload
 
 
 # Retain some backwards compatibility
diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py
index 7e7f0353..09ce6850 100644
--- a/sleekxmpp/plugins/xep_0030/static.py
+++ b/sleekxmpp/plugins/xep_0030/static.py
@@ -7,6 +7,7 @@
 """
 
 import logging
+import threading
 
 import sleekxmpp
 from sleekxmpp import Iq
@@ -50,8 +51,9 @@ class StaticDisco(object):
         """
         self.nodes = {}
         self.xmpp = xmpp
+        self.lock = threading.RLock()
 
-    def add_node(self, jid=None, node=None):
+    def add_node(self, jid=None, node=None, ifrom=None):
         """
         Create a new set of stanzas for the provided
         JID and node combination.
@@ -60,38 +62,77 @@ class StaticDisco(object):
             jid  -- The JID that will own the new stanzas.
             node -- The node that will own the new stanzas.
         """
-        if jid is None:
-            jid = self.xmpp.boundjid.full
-        if node is None:
-            node = ''
-        if (jid, node) not in self.nodes:
-            self.nodes[(jid, node)] = {'info': DiscoInfo(),
-                                       'items': DiscoItems()}
-            self.nodes[(jid, node)]['info']['node'] = node
-            self.nodes[(jid, node)]['items']['node'] = node
+        with self.lock:
+            if jid is None:
+                jid = self.xmpp.boundjid.full
+            if node is None:
+                node = ''
+            if ifrom is None:
+                ifrom = ''
+            if isinstance(ifrom, JID):
+                ifrom = ifrom.full
+            if (jid, node, ifrom) not in self.nodes:
+                self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(),
+                                           'items': DiscoItems()}
+                self.nodes[(jid, node, ifrom)]['info']['node'] = node
+                self.nodes[(jid, node, ifrom)]['items']['node'] = node
+
+    def get_node(self, jid=None, node=None, ifrom=None):
+        with self.lock:
+            if jid is None:
+                jid = self.xmpp.boundjid.full
+            if node is None:
+                node = ''
+            if ifrom is None:
+                ifrom = ''
+            if isinstance(ifrom, JID):
+                ifrom = ifrom.full
+            if (jid, node, ifrom) not in self.nodes:
+                self.add_node(jid, node, ifrom)
+            return self.nodes[(jid, node, ifrom)]
+
+    def node_exists(self, jid=None, node=None, ifrom=None):
+        with self.lock:
+            if jid is None:
+                jid = self.xmpp.boundjid.full
+            if node is None:
+                node = ''
+            if ifrom is None:
+                ifrom = ''
+            if isinstance(ifrom, JID):
+                ifrom = ifrom.full
+            if (jid, node, ifrom) not in self.nodes:
+                return False
+            return True 
 
     # =================================================================
     # Node Handlers
     #
-    # Each handler accepts three arguments: jid, node, and data.
-    # The jid and node parameters together determine the set of
-    # info and items stanzas that will be retrieved or added.
-    # The data parameter is a dictionary with additional paramters
-    # that will be passed to other calls.
+    # Each handler accepts four arguments: jid, node, ifrom, and data.
+    # The jid and node parameters together determine the set of info
+    # and items stanzas that will be retrieved or added. Additionally,
+    # the ifrom value allows for cached results when results vary based
+    # on the requester's JID. The data parameter is a dictionary with
+    # additional parameters that will be passed to other calls.
+    #
+    # This implementation does not allow different responses based on
+    # the requester's JID, except for cached results. To do that, 
+    # register a custom node handler.
 
-    def get_info(self, jid, node, data):
+    def get_info(self, jid, node, ifrom, data):
         """
         Return the stored info data for the requested JID/node combination.
 
         The data parameter is not used.
         """
-        if (jid, node) not in self.nodes:
-            if not node:
-                return DiscoInfo()
+        with self.lock:
+            if not self.node_exists(jid, node):
+                if not node:
+                    return DiscoInfo()
+                else:
+                    raise XMPPError(condition='item-not-found')
             else:
-                raise XMPPError(condition='item-not-found')
-        else:
-            return self.nodes[(jid, node)]['info']
+                return self.get_node(jid, node)['info']
 
     def del_info(self, jid, node, data):
         """
@@ -99,44 +140,48 @@ class StaticDisco(object):
 
         The data parameter is not used.
         """
-        if (jid, node) in self.nodes:
-            self.nodes[(jid, node)]['info'] = DiscoInfo()
+        with self.lock:
+            if self.node_exists(jid, node):
+                self.get_node(jid, node)['info'] = DiscoInfo()
 
-    def get_items(self, jid, node, data):
+    def get_items(self, jid, node, ifrom, data):
         """
         Return the stored items data for the requested JID/node combination.
 
         The data parameter is not used.
         """
-        if (jid, node) not in self.nodes:
-            if not node:
-                return DiscoInfo()
+        with self.lock:
+            if not self.node_exists(jid, node):
+                if not node:
+                    return DiscoInfo()
+                else:
+                    raise XMPPError(condition='item-not-found')
             else:
-                raise XMPPError(condition='item-not-found')
-        else:
-            return self.nodes[(jid, node)]['items']
+                return self.get_node(jid, node)['items']
 
-    def set_items(self, jid, node, data):
+    def set_items(self, jid, node, ifrom, data):
         """
         Replace the stored items data for a JID/node combination.
 
         The data parameter may provided:
             items -- A set of items in tuple format.
         """
-        items = data.get('items', set())
-        self.add_node(jid, node)
-        self.nodes[(jid, node)]['items']['items'] = items
+        with self.lock:
+            items = data.get('items', set())
+            self.add_node(jid, node)
+            self.get_node(jid, node)['items']['items'] = items
 
-    def del_items(self, jid, node, data):
+    def del_items(self, jid, node, ifrom, data):
         """
         Reset the items stanza for a given JID/node combination.
 
         The data parameter is not used.
         """
-        if (jid, node) in self.nodes:
-            self.nodes[(jid, node)]['items'] = DiscoItems()
+        with self.lock:
+            if self.node_exists(jid, node):
+                self.get_node(jid, node)['items'] = DiscoItems()
 
-    def add_identity(self, jid, node, data):
+    def add_identity(self, jid, node, ifrom, data):
         """
         Add a new identity to te JID/node combination.
 
@@ -146,14 +191,15 @@ class StaticDisco(object):
             name     -- Optional human readable name for this identity.
             lang     -- Optional standard xml:lang value.
         """
-        self.add_node(jid, node)
-        self.nodes[(jid, node)]['info'].add_identity(
-                data.get('category', ''),
-                data.get('itype', ''),
-                data.get('name', None),
-                data.get('lang', None))
+        with self.lock:
+            self.add_node(jid, node)
+            self.get_node(jid, node)['info'].add_identity(
+                    data.get('category', ''),
+                    data.get('itype', ''),
+                    data.get('name', None),
+                    data.get('lang', None))
 
-    def set_identities(self, jid, node, data):
+    def set_identities(self, jid, node, ifrom, data):
         """
         Add or replace all identities for a JID/node combination.
 
@@ -161,11 +207,12 @@ class StaticDisco(object):
             identities -- A list of identities in tuple form:
                             (category, type, name, lang)
         """
-        identities = data.get('identities', set())
-        self.add_node(jid, node)
-        self.nodes[(jid, node)]['info']['identities'] = identities
+        with self.lock:
+            identities = data.get('identities', set())
+            self.add_node(jid, node)
+            self.get_node(jid, node)['info']['identities'] = identities
 
-    def del_identity(self, jid, node, data):
+    def del_identity(self, jid, node, ifrom, data):
         """
         Remove an identity from a JID/node combination.
 
@@ -175,67 +222,70 @@ class StaticDisco(object):
             name     -- Optional human readable name for this identity.
             lang     -- Optional, standard xml:lang value.
         """
-        if (jid, node) not in self.nodes:
-            return
-        self.nodes[(jid, node)]['info'].del_identity(
-                data.get('category', ''),
-                data.get('itype', ''),
-                data.get('name', None),
-                data.get('lang', None))
+        with self.lock:
+            if self.node_exists(jid, node):
+                self.get_node(jid, node)['info'].del_identity(
+                        data.get('category', ''),
+                        data.get('itype', ''),
+                        data.get('name', None),
+                        data.get('lang', None))
 
-    def del_identities(self, jid, node, data):
+    def del_identities(self, jid, node, ifrom, data):
         """
         Remove all identities from a JID/node combination.
 
         The data parameter is not used.
         """
-        if (jid, node) not in self.nodes:
-            return
-        del self.nodes[(jid, node)]['info']['identities']
+        with self.lock:
+            if self.node_exists(jid, node):
+                del self.get_node(jid, node)['info']['identities']
 
-    def add_feature(self, jid, node, data):
+    def add_feature(self, jid, node, ifrom, data):
         """
         Add a feature to a JID/node combination.
 
         The data parameter should include:
             feature -- The namespace of the supported feature.
         """
-        self.add_node(jid, node)
-        self.nodes[(jid, node)]['info'].add_feature(data.get('feature', ''))
+        with self.lock:
+            self.add_node(jid, node)
+            self.get_node(jid, node)['info'].add_feature(data.get('feature', ''))
 
-    def set_features(self, jid, node, data):
+    def set_features(self, jid, node, ifrom, data):
         """
         Add or replace all features for a JID/node combination.
 
         The data parameter should include:
             features -- The new set of supported features.
         """
-        features = data.get('features', set())
-        self.add_node(jid, node)
-        self.nodes[(jid, node)]['info']['features'] = features
+        with self.lock:
+            features = data.get('features', set())
+            self.add_node(jid, node)
+            self.get_node(jid, node)['info']['features'] = features
 
-    def del_feature(self, jid, node, data):
+    def del_feature(self, jid, node, ifrom, data):
         """
         Remove a feature from a JID/node combination.
 
         The data parameter should include:
             feature -- The namespace of the removed feature.
         """
-        if (jid, node) not in self.nodes:
-            return
-        self.nodes[(jid, node)]['info'].del_feature(data.get('feature', ''))
+        with self.lock:
+            if self.node_exists(jid, node):
+                self.get_node(jid, node)['info'].del_feature(data.get('feature', ''))
 
-    def del_features(self, jid, node, data):
+    def del_features(self, jid, node, ifrom, data):
         """
         Remove all features from a JID/node combination.
 
         The data parameter is not used.
         """
-        if (jid, node) not in self.nodes:
-            return
-        del self.nodes[(jid, node)]['info']['features']
+        with self.lock:
+            if not self.node_exists(jid, node):
+                return
+            del self.get_node(jid, node)['info']['features']
 
-    def add_item(self, jid, node, data):
+    def add_item(self, jid, node, ifrom, data):
         """
         Add an item to a JID/node combination.
 
@@ -245,13 +295,14 @@ class StaticDisco(object):
                      non-addressable items.
             name  -- Optional human readable name for the item.
         """
-        self.add_node(jid, node)
-        self.nodes[(jid, node)]['items'].add_item(
-                data.get('ijid', ''),
-                node=data.get('inode', ''),
-                name=data.get('name', ''))
+        with self.lock:
+            self.add_node(jid, node)
+            self.get_node(jid, node)['items'].add_item(
+                    data.get('ijid', ''),
+                    node=data.get('inode', ''),
+                    name=data.get('name', ''))
 
-    def del_item(self, jid, node, data):
+    def del_item(self, jid, node, ifrom, data):
         """
         Remove an item from a JID/node combination.
 
@@ -259,7 +310,35 @@ class StaticDisco(object):
             ijid  -- JID of the item to remove.
             inode -- Optional extra identifying information.
         """
-        if (jid, node) in self.nodes:
-            self.nodes[(jid, node)]['items'].del_item(
-                    data.get('ijid', ''),
-                    node=data.get('inode', None))
+        with self.lock:
+            if self.node_exists(jid, node):
+                self.get_node(jid, node)['items'].del_item(
+                        data.get('ijid', ''),
+                        node=data.get('inode', None))
+
+    def cache_info(self, jid, node, ifrom, data):
+        """
+        Cache disco information for an external JID.
+
+        The data parameter is the Iq result stanza
+        containing the disco info to cache, or
+        the disco#info substanza itself.
+        """
+        with self.lock:
+            if isinstance(data, Iq):
+                data = data['disco_info']
+
+            self.add_node(jid, node, ifrom)
+            self.get_node(jid, node, ifrom)['info'] = data
+
+    def get_cached_info(self, jid, node, ifrom, data):
+        """
+        Retrieve cached disco info data.
+
+        The data parameter is not used.
+        """
+        with self.lock:
+            if not self.node_exists(jid, node, ifrom):
+                return None
+            else:
+                return self.get_node(jid, node, ifrom)['info']
diff --git a/sleekxmpp/plugins/xep_0128/extended_disco.py b/sleekxmpp/plugins/xep_0128/extended_disco.py
index 63b3cfee..5bb78320 100644
--- a/sleekxmpp/plugins/xep_0128/extended_disco.py
+++ b/sleekxmpp/plugins/xep_0128/extended_disco.py
@@ -76,7 +76,7 @@ class xep_0128(base_plugin):
                     as extended information, replacing any
                     existing extensions.
         """
-        self.disco._run_node_handler('set_extended_info', jid, node, kwargs)
+        self.disco._run_node_handler('set_extended_info', jid, node, None, kwargs)
 
     def add_extended_info(self, jid=None, node=None, **kwargs):
         """
@@ -88,7 +88,7 @@ class xep_0128(base_plugin):
             data -- Either a form, or a list of forms to add
                     as extended information.
         """
-        self.disco._run_node_handler('add_extended_info', jid, node, kwargs)
+        self.disco._run_node_handler('add_extended_info', jid, node, None, kwargs)
 
     def del_extended_info(self, jid=None, node=None, **kwargs):
         """
@@ -98,4 +98,4 @@ class xep_0128(base_plugin):
             jid  -- The JID to modify.
             node -- The node to modify.
         """
-        self.disco._run_node_handler('del_extended_info', jid, node, kwargs)
+        self.disco._run_node_handler('del_extended_info', jid, node, None, kwargs)
diff --git a/sleekxmpp/plugins/xep_0128/static.py b/sleekxmpp/plugins/xep_0128/static.py
index 493d9370..427011c0 100644
--- a/sleekxmpp/plugins/xep_0128/static.py
+++ b/sleekxmpp/plugins/xep_0128/static.py
@@ -31,42 +31,43 @@ class StaticExtendedDisco(object):
         """
         self.static = static
 
-    def set_extended_info(self, jid, node, data):
+    def set_extended_info(self, jid, node, ifrom, data):
         """
         Replace the extended identity data for a JID/node combination.
 
         The data parameter may provide:
             data -- Either a single data form, or a list of data forms.
         """
-        self.del_extended_info(jid, node, data)
-        self.add_extended_info(jid, node, data)
+        with self.static.lock:
+            self.del_extended_info(jid, node, ifrom, data)
+            self.add_extended_info(jid, node, ifrom, data)
 
-    def add_extended_info(self, jid, node, data):
+    def add_extended_info(self, jid, node, ifrom, data):
         """
         Add additional extended identity data for a JID/node combination.
 
         The data parameter may provide:
             data -- Either a single data form, or a list of data forms.
         """
-        self.static.add_node(jid, node)
+        with self.static.lock:
+            self.static.add_node(jid, node)
 
-        forms = data.get('data', [])
-        if not isinstance(forms, list):
-            forms = [forms]
+            forms = data.get('data', [])
+            if not isinstance(forms, list):
+                forms = [forms]
 
-        for form in forms:
-            self.static.nodes[(jid, node)]['info'].append(form)
+            info = self.static.get_node(jid, node)['info']
+            for form in forms:
+                info.append(form)
 
-    def del_extended_info(self, jid, node, data):
+    def del_extended_info(self, jid, node, ifrom, data):
         """
         Replace the extended identity data for a JID/node combination.
 
         The data parameter is not used.
         """
-        if (jid, node) not in self.static.nodes:
-            return
-
-        info = self.static.nodes[(jid, node)]['info']
-
-        for form in info['substanza']:
-            info.xml.remove(form.xml)
+        with self.static.lock:
+            if self.static.node_exists(jid, node):
+                info = self.static.get_node(jid, node)['info']
+                for form in info['substanza']:
+                    info.xml.remove(form.xml)
diff --git a/tests/test_stream_xep_0030.py b/tests/test_stream_xep_0030.py
index 1666d3a1..dd43778a 100644
--- a/tests/test_stream_xep_0030.py
+++ b/tests/test_stream_xep_0030.py
@@ -122,7 +122,7 @@ class TestStreamDisco(SleekTest):
         self.stream_start(mode='client',
                           plugins=['xep_0030'])
 
-        def dynamic_jid(jid, node, iq):
+        def dynamic_jid(jid, node, ifrom, iq):
             result = self.xmpp['xep_0030'].stanza.DiscoInfo()
             result['node'] = node
             result.add_identity('client', 'console', name='Dynamic Info')
@@ -158,7 +158,7 @@ class TestStreamDisco(SleekTest):
                           jid='tester.localhost',
                           plugins=['xep_0030'])
 
-        def dynamic_global(jid, node, iq):
+        def dynamic_global(jid, node, ifrom, iq):
             result = self.xmpp['xep_0030'].stanza.DiscoInfo()
             result['node'] = node
             result.add_identity('component', 'generic', name='Dynamic Info')
@@ -194,7 +194,7 @@ class TestStreamDisco(SleekTest):
         self.stream_start(mode='client',
                           plugins=['xep_0030'])
 
-        def dynamic_jid(jid, node, iq):
+        def dynamic_jid(jid, node, ifrom, iq):
             result = self.xmpp['xep_0030'].stanza.DiscoInfo()
             result['node'] = node
             result.add_identity('client', 'console', name='Dynamic Info')
@@ -236,7 +236,7 @@ class TestStreamDisco(SleekTest):
                           jid='tester.localhost',
                           plugins=['xep_0030'])
 
-        def dynamic_global(jid, node, iq):
+        def dynamic_global(jid, node, ifrom, iq):
             result = self.xmpp['xep_0030'].stanza.DiscoInfo()
             result['node'] = node
             result.add_identity('component', 'generic', name='Dynamic Info')
@@ -325,7 +325,7 @@ class TestStreamDisco(SleekTest):
         self.stream_start(mode='client',
                           plugins=['xep_0030'])
 
-        def dynamic_jid(jid, node, iq):
+        def dynamic_jid(jid, node, ifrom, iq):
             result = self.xmpp['xep_0030'].stanza.DiscoItems()
             result['node'] = node
             result.add_item('tester@localhost', node='foo', name='JID')
@@ -359,7 +359,7 @@ class TestStreamDisco(SleekTest):
                           jid='tester.localhost',
                           plugins=['xep_0030'])
 
-        def dynamic_global(jid, node, iq):
+        def dynamic_global(jid, node, ifrom, iq):
             result = self.xmpp['xep_0030'].stanza.DiscoItems()
             result['node'] = node
             result.add_item('tester@localhost', node='foo', name='Global')
@@ -393,7 +393,7 @@ class TestStreamDisco(SleekTest):
         self.stream_start(mode='client',
                           plugins=['xep_0030'])
 
-        def dynamic_jid(jid, node, iq):
+        def dynamic_jid(jid, node, ifrom, iq):
             result = self.xmpp['xep_0030'].stanza.DiscoItems()
             result['node'] = node
             result.add_item('tester@localhost', node='foo', name='Global')
@@ -435,7 +435,7 @@ class TestStreamDisco(SleekTest):
                           jid='tester.localhost',
                           plugins=['xep_0030'])
 
-        def dynamic_global(jid, node, iq):
+        def dynamic_global(jid, node, ifrom, iq):
             result = self.xmpp['xep_0030'].stanza.DiscoItems()
             result['node'] = node
             result.add_item('tester.localhost', node='foo', name='Global')
-- 
cgit v1.2.3