Package common :: Module caps_cache
[hide private]
[frames] | no frames]

Source Code for Module common.caps_cache

  1  # -*- coding:utf-8 -*- 
  2  ## src/common/caps_cache.py 
  3  ## 
  4  ## Copyright (C) 2007 Tomasz Melcer <liori AT exroot.org> 
  5  ##                    Travis Shirk <travis AT pobox.com> 
  6  ## Copyright (C) 2007-2010 Yann Leboulanger <asterix AT lagaule.org> 
  7  ## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com> 
  8  ##                    Jonathan Schleifer <js-gajim AT webkeks.org> 
  9  ## Copyright (C) 2008-2009 Stephan Erb <steve-e AT h3c.de> 
 10  ## 
 11  ## This file is part of Gajim. 
 12  ## 
 13  ## Gajim is free software; you can redistribute it and/or modify 
 14  ## it under the terms of the GNU General Public License as published 
 15  ## by the Free Software Foundation; version 3 only. 
 16  ## 
 17  ## Gajim is distributed in the hope that it will be useful, 
 18  ## but WITHOUT ANY WARRANTY; without even the implied warranty of 
 19  ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
 20  ## GNU General Public License for more details. 
 21  ## 
 22  ## You should have received a copy of the GNU General Public License 
 23  ## along with Gajim. If not, see <http://www.gnu.org/licenses/>. 
 24  ## 
 25   
 26  """ 
 27  Module containing all XEP-115 (Entity Capabilities) related classes 
 28   
 29  Basic Idea: 
 30  CapsCache caches features to hash relationships. The cache is queried 
 31  through ClientCaps objects which are hold by contact instances. 
 32  """ 
 33   
 34  import base64 
 35  import hashlib 
 36   
 37  import logging 
 38  log = logging.getLogger('gajim.c.caps_cache') 
 39   
 40  from common.xmpp import (NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, NS_CHATSTATES, 
 41          NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_CAPS) 
 42  # Features where we cannot safely assume that the other side supports them 
 43  FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, 
 44          NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO] 
 45   
 46  # Query entry status codes 
 47  NEW = 0 
 48  QUERIED = 1 
 49  CACHED = 2 # got the answer 
 50  FAKED = 3 # allow NullClientCaps to behave as it has a cached item 
 51   
 52  ################################################################################ 
 53  ### Public API of this module 
 54  ################################################################################ 
 55   
 56  capscache = None 
57 -def initialize(logger):
58 """ 59 Initialize this module 60 """ 61 global capscache 62 capscache = CapsCache(logger)
63
64 -def client_supports(client_caps, requested_feature):
65 lookup_item = client_caps.get_cache_lookup_strategy() 66 cache_item = lookup_item(capscache) 67 68 supported_features = cache_item.features 69 if requested_feature in supported_features: 70 return True 71 elif not supported_features and cache_item.status in (NEW, QUERIED, FAKED): 72 # assume feature is supported, if we don't know yet, what the client 73 # is capable of 74 return requested_feature not in FEATURE_BLACKLIST 75 else: 76 return False
77
78 -def create_suitable_client_caps(node, caps_hash, hash_method):
79 """ 80 Create and return a suitable ClientCaps object for the given node, 81 caps_hash, hash_method combination. 82 """ 83 if not node or not caps_hash: 84 # improper caps, ignore client capabilities. 85 client_caps = NullClientCaps() 86 elif not hash_method: 87 client_caps = OldClientCaps(caps_hash, node) 88 else: 89 client_caps = ClientCaps(caps_hash, node, hash_method) 90 return client_caps
91
92 -def compute_caps_hash(identities, features, dataforms=[], hash_method='sha-1'):
93 """ 94 Compute caps hash according to XEP-0115, V1.5 95 96 dataforms are xmpp.DataForms objects as common.dataforms don't allow several 97 values without a field type list-multi 98 """ 99 def sort_identities_func(i1, i2): 100 cat1 = i1['category'] 101 cat2 = i2['category'] 102 if cat1 < cat2: 103 return -1 104 if cat1 > cat2: 105 return 1 106 type1 = i1.get('type', '') 107 type2 = i2.get('type', '') 108 if type1 < type2: 109 return -1 110 if type1 > type2: 111 return 1 112 lang1 = i1.get('xml:lang', '') 113 lang2 = i2.get('xml:lang', '') 114 if lang1 < lang2: 115 return -1 116 if lang1 > lang2: 117 return 1 118 return 0
119 120 def sort_dataforms_func(d1, d2): 121 f1 = d1.getField('FORM_TYPE') 122 f2 = d2.getField('FORM_TYPE') 123 if f1 and f2 and (f1.getValue() < f2.getValue()): 124 return -1 125 return 1 126 127 S = '' 128 identities.sort(cmp=sort_identities_func) 129 for i in identities: 130 c = i['category'] 131 type_ = i.get('type', '') 132 lang = i.get('xml:lang', '') 133 name = i.get('name', '') 134 S += '%s/%s/%s/%s<' % (c, type_, lang, name) 135 features.sort() 136 for f in features: 137 S += '%s<' % f 138 dataforms.sort(cmp=sort_dataforms_func) 139 for dataform in dataforms: 140 # fields indexed by var 141 fields = {} 142 for f in dataform.getChildren(): 143 fields[f.getVar()] = f 144 form_type = fields.get('FORM_TYPE') 145 if form_type: 146 S += form_type.getValue() + '<' 147 del fields['FORM_TYPE'] 148 for var in sorted(fields.keys()): 149 S += '%s<' % var 150 values = sorted(fields[var].getValues()) 151 for value in values: 152 S += '%s<' % value 153 154 if hash_method == 'sha-1': 155 hash_ = hashlib.sha1(S) 156 elif hash_method == 'md5': 157 hash_ = hashlib.md5(S) 158 else: 159 return '' 160 return base64.b64encode(hash_.digest()) 161 162 163 ################################################################################ 164 ### Internal classes of this module 165 ################################################################################ 166
167 -class AbstractClientCaps(object):
168 """ 169 Base class representing a client and its capabilities as advertised by a 170 caps tag in a presence 171 """
172 - def __init__(self, caps_hash, node):
173 self._hash = caps_hash 174 self._node = node
175
176 - def get_discover_strategy(self):
177 return self._discover
178
179 - def _discover(self, connection, jid):
180 """ 181 To be implemented by subclassess 182 """ 183 raise NotImplementedError
184
186 return self._lookup_in_cache
187
188 - def _lookup_in_cache(self, caps_cache):
189 """ 190 To be implemented by subclassess 191 """ 192 raise NotImplementedError
193
195 return self._is_hash_valid
196
197 - def _is_hash_valid(self, identities, features, dataforms):
198 """ 199 To be implemented by subclassess 200 """ 201 raise NotImplementedError
202 203
204 -class ClientCaps(AbstractClientCaps):
205 """ 206 The current XEP-115 implementation 207 """
208 - def __init__(self, caps_hash, node, hash_method):
209 AbstractClientCaps.__init__(self, caps_hash, node) 210 assert hash_method != 'old' 211 self._hash_method = hash_method
212
213 - def _lookup_in_cache(self, caps_cache):
214 return caps_cache[(self._hash_method, self._hash)]
215
216 - def _discover(self, connection, jid):
217 connection.discoverInfo(jid, '%s#%s' % (self._node, self._hash))
218
219 - def _is_hash_valid(self, identities, features, dataforms):
220 computed_hash = compute_caps_hash(identities, features, 221 dataforms=dataforms, hash_method=self._hash_method) 222 return computed_hash == self._hash
223 224
225 -class OldClientCaps(AbstractClientCaps):
226 """ 227 Old XEP-115 implemtation. Kept around for background competability 228 """
229 - def __init__(self, caps_hash, node):
231
232 - def _lookup_in_cache(self, caps_cache):
233 return caps_cache[('old', self._node + '#' + self._hash)]
234
235 - def _discover(self, connection, jid):
237
238 - def _is_hash_valid(self, identities, features, dataforms):
239 return True
240 241
242 -class NullClientCaps(AbstractClientCaps):
243 """ 244 This is a NULL-Object to streamline caps handling if a client has not 245 advertised any caps or has advertised them in an improper way 246 247 Assumes (almost) everything is supported. 248 """ 249 _instance = None
250 - def __new__(cls, *args, **kwargs):
251 """ 252 Make it a singleton. 253 """ 254 if not cls._instance: 255 cls._instance = super(NullClientCaps, cls).__new__( 256 cls, *args, **kwargs) 257 return cls._instance
258
259 - def __init__(self):
260 AbstractClientCaps.__init__(self, None, None)
261
262 - def _lookup_in_cache(self, caps_cache):
263 # lookup something which does not exist to get a new CacheItem created 264 cache_item = caps_cache[('dummy', '')] 265 # Mark the item as cached so that protocol/caps.py does not update it 266 cache_item.status = FAKED 267 return cache_item
268
269 - def _discover(self, connection, jid):
270 pass
271
272 - def _is_hash_valid(self, identities, features, dataforms):
273 return False
274 275
276 -class CapsCache(object):
277 """ 278 This object keeps the mapping between caps data and real disco features they 279 represent, and provides simple way to query that info 280 """
281 - def __init__(self, logger=None):
282 # our containers: 283 # __cache is a dictionary mapping: pair of hash method and hash maps 284 # to CapsCacheItem object 285 # __CacheItem is a class that stores data about particular 286 # client (hash method/hash pair) 287 self.__cache = {} 288 289 class CacheItem(object): 290 # __names is a string cache; every string long enough is given 291 # another object, and we will have plenty of identical long 292 # strings. therefore we can cache them 293 __names = {} 294 295 def __init__(self, hash_method, hash_, logger): 296 # cached into db 297 self.hash_method = hash_method 298 self.hash = hash_ 299 self._features = [] 300 self._identities = [] 301 self._logger = logger 302 303 self.status = NEW 304 self._recently_seen = False
305 306 def _get_features(self): 307 return self._features
308 309 def _set_features(self, value): 310 self._features = [] 311 for feature in value: 312 self._features.append(self.__names.setdefault(feature, feature)) 313 314 features = property(_get_features, _set_features) 315 316 def _get_identities(self): 317 list_ = [] 318 for i in self._identities: 319 # transforms it back in a dict 320 d = dict() 321 d['category'] = i[0] 322 if i[1]: 323 d['type'] = i[1] 324 if i[2]: 325 d['xml:lang'] = i[2] 326 if i[3]: 327 d['name'] = i[3] 328 list_.append(d) 329 return list_ 330 331 def _set_identities(self, value): 332 self._identities = [] 333 for identity in value: 334 # dict are not hashable, so transform it into a tuple 335 t = (identity['category'], identity.get('type'), 336 identity.get('xml:lang'), identity.get('name')) 337 self._identities.append(self.__names.setdefault(t, t)) 338 339 identities = property(_get_identities, _set_identities) 340 341 def set_and_store(self, identities, features): 342 self.identities = identities 343 self.features = features 344 self._logger.add_caps_entry(self.hash_method, self.hash, 345 identities, features) 346 self.status = CACHED 347 348 def update_last_seen(self): 349 if not self._recently_seen: 350 self._recently_seen = True 351 self._logger.update_caps_time(self.hash_method, self.hash) 352 353 def is_valid(self): 354 """ 355 Returns True if identities and features for this cache item 356 are known. 357 """ 358 return self.status in (CACHED, FAKED) 359 360 self.__CacheItem = CacheItem 361 self.logger = logger 362
363 - def initialize_from_db(self):
364 self._remove_outdated_caps() 365 for hash_method, hash_, identities, features in \ 366 self.logger.iter_caps_data(): 367 x = self[(hash_method, hash_)] 368 x.identities = identities 369 x.features = features 370 x.status = CACHED
371
372 - def _remove_outdated_caps(self):
373 """ 374 Remove outdated values from the db 375 """ 376 self.logger.clean_caps_table()
377
378 - def __getitem__(self, caps):
379 if caps in self.__cache: 380 return self.__cache[caps] 381 382 hash_method, hash_ = caps 383 384 x = self.__CacheItem(hash_method, hash_, self.logger) 385 self.__cache[(hash_method, hash_)] = x 386 return x
387
388 - def query_client_of_jid_if_unknown(self, connection, jid, client_caps):
389 """ 390 Start a disco query to determine caps (node, ver, exts). Won't query if 391 the data is already in cache 392 """ 393 lookup_cache_item = client_caps.get_cache_lookup_strategy() 394 q = lookup_cache_item(self) 395 396 if q.status == NEW: 397 # do query for bare node+hash pair 398 # this will create proper object 399 q.status = QUERIED 400 discover = client_caps.get_discover_strategy() 401 discover(connection, jid) 402 else: 403 q.update_last_seen()
404