comparison sat/memory/memory.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/memory/memory.py@8d82a62fa098
children 9446f1ea9eac
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT: a jabber client
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24
25 import os.path
26 import copy
27 from collections import namedtuple
28 from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError
29 from uuid import uuid4
30 from twisted.python import failure
31 from twisted.internet import defer, reactor, error
32 from twisted.words.protocols.jabber import jid
33 from sat.core import exceptions
34 from sat.core.constants import Const as C
35 from sat.memory.sqlite import SqliteStorage
36 from sat.memory.persistent import PersistentDict
37 from sat.memory.params import Params
38 from sat.memory.disco import Discovery
39 from sat.memory.crypto import BlockCipher
40 from sat.memory.crypto import PasswordHasher
41 from sat.tools import config as tools_config
42 import shortuuid
43 import mimetypes
44 import time
45
46
47 PresenceTuple = namedtuple("PresenceTuple", ('show', 'priority', 'statuses'))
48 MSG_NO_SESSION = "Session id doesn't exist or is finished"
49
50 class Sessions(object):
51 """Sessions are data associated to key used for a temporary moment, with optional profile checking."""
52 DEFAULT_TIMEOUT = 600
53
54 def __init__(self, timeout=None, resettable_timeout=True):
55 """
56 @param timeout (int): nb of seconds before session destruction
57 @param resettable_timeout (bool): if True, the timeout is reset on each access
58 """
59 self._sessions = dict()
60 self.timeout = timeout or Sessions.DEFAULT_TIMEOUT
61 self.resettable_timeout = resettable_timeout
62
63 def newSession(self, session_data=None, session_id=None, profile=None):
64 """Create a new session
65
66 @param session_data: mutable data to use, default to a dict
67 @param session_id (str): force the session_id to the given string
68 @param profile: if set, the session is owned by the profile,
69 and profileGet must be used instead of __getitem__
70 @return: session_id, session_data
71 """
72 if session_id is None:
73 session_id = str(uuid4())
74 elif session_id in self._sessions:
75 raise exceptions.ConflictError(u"Session id {} is already used".format(session_id))
76 timer = reactor.callLater(self.timeout, self._purgeSession, session_id)
77 if session_data is None:
78 session_data = {}
79 self._sessions[session_id] = (timer, session_data) if profile is None else (timer, session_data, profile)
80 return session_id, session_data
81
82 def _purgeSession(self, session_id):
83 try:
84 timer, session_data, profile = self._sessions[session_id]
85 except ValueError:
86 timer, session_data = self._sessions[session_id]
87 profile = None
88 try:
89 timer.cancel()
90 except error.AlreadyCalled:
91 # if the session is time-outed, the timer has been called
92 pass
93 del self._sessions[session_id]
94 log.debug(u"Session {} purged{}".format(session_id, u' (profile {})'.format(profile) if profile is not None else u''))
95
96 def __len__(self):
97 return len(self._sessions)
98
99 def __contains__(self, session_id):
100 return session_id in self._sessions
101
102 def profileGet(self, session_id, profile):
103 try:
104 timer, session_data, profile_set = self._sessions[session_id]
105 except ValueError:
106 raise exceptions.InternalError("You need to use __getitem__ when profile is not set")
107 except KeyError:
108 raise failure.Failure(KeyError(MSG_NO_SESSION))
109 if profile_set != profile:
110 raise exceptions.InternalError("current profile differ from set profile !")
111 if self.resettable_timeout:
112 timer.reset(self.timeout)
113 return session_data
114
115 def __getitem__(self, session_id):
116 try:
117 timer, session_data = self._sessions[session_id]
118 except ValueError:
119 raise exceptions.InternalError("You need to use profileGet instead of __getitem__ when profile is set")
120 except KeyError:
121 raise failure.Failure(KeyError(MSG_NO_SESSION))
122 if self.resettable_timeout:
123 timer.reset(self.timeout)
124 return session_data
125
126 def __setitem__(self, key, value):
127 raise NotImplementedError("You need do use newSession to create a session")
128
129 def __delitem__(self, session_id):
130 """ delete the session data """
131 self._purgeSession(session_id)
132
133 def keys(self):
134 return self._sessions.keys()
135
136 def iterkeys(self):
137 return self._sessions.iterkeys()
138
139
140 class ProfileSessions(Sessions):
141 """ProfileSessions extends the Sessions class, but here the profile can be
142 used as the key to retrieve data or delete a session (instead of session id).
143 """
144
145 def _profileGetAllIds(self, profile):
146 """Return a list of the sessions ids that are associated to the given profile.
147
148 @param profile: %(doc_profile)s
149 @return: a list containing the sessions ids
150 """
151 ret = []
152 for session_id in self._sessions.iterkeys():
153 try:
154 timer, session_data, profile_set = self._sessions[session_id]
155 except ValueError:
156 continue
157 if profile == profile_set:
158 ret.append(session_id)
159 return ret
160
161 def profileGetUnique(self, profile):
162 """Return the data of the unique session that is associated to the given profile.
163
164 @param profile: %(doc_profile)s
165 @return:
166 - mutable data (default: dict) of the unique session
167 - None if no session is associated to the profile
168 - raise an error if more than one session are found
169 """
170 ids = self._profileGetAllIds(profile)
171 if len(ids) > 1:
172 raise exceptions.InternalError('profileGetUnique has been used but more than one session has been found!')
173 return self.profileGet(ids[0], profile) if len(ids) == 1 else None # XXX: timeout might be reset
174
175 def profileDelUnique(self, profile):
176 """Delete the unique session that is associated to the given profile.
177
178 @param profile: %(doc_profile)s
179 @return: None, but raise an error if more than one session are found
180 """
181 ids = self._profileGetAllIds(profile)
182 if len(ids) > 1:
183 raise exceptions.InternalError('profileDelUnique has been used but more than one session has been found!')
184 if len(ids) == 1:
185 del self._sessions[ids[0]]
186
187
188 class PasswordSessions(ProfileSessions):
189
190 # FIXME: temporary hack for the user personal key not to be lost. The session
191 # must actually be purged and later, when the personal key is needed, the
192 # profile password should be asked again in order to decrypt it.
193 def __init__(self, timeout=None):
194 ProfileSessions.__init__(self, timeout, resettable_timeout=False)
195
196 def _purgeSession(self, session_id):
197 log.debug("FIXME: PasswordSessions should ask for the profile password after the session expired")
198
199
200 # XXX: tmp update code, will be removed in the future
201 # When you remove this, please add the default value for
202 # 'local_dir' in sat.core.constants.Const.DEFAULT_CONFIG
203 def fixLocalDir(silent=True):
204 """Retro-compatibility with the previous local_dir default value.
205
206 @param silent (boolean): toggle logging output (must be True when called from sat.sh)
207 """
208 user_config = SafeConfigParser()
209 try:
210 user_config.read(C.CONFIG_FILES)
211 except:
212 pass # file is readable but its structure if wrong
213 try:
214 current_value = user_config.get('DEFAULT', 'local_dir')
215 except (NoOptionError, NoSectionError):
216 current_value = ''
217 if current_value:
218 return # nothing to do
219 old_default = '~/.sat'
220 if os.path.isfile(os.path.expanduser(old_default) + '/' + C.SAVEFILE_DATABASE):
221 if not silent:
222 log.warning(_(u"A database has been found in the default local_dir for previous versions (< 0.5)"))
223 tools_config.fixConfigOption('', 'local_dir', old_default, silent)
224
225
226 class Memory(object):
227 """This class manage all the persistent information"""
228
229 def __init__(self, host):
230 log.info(_("Memory manager init"))
231 self.initialized = defer.Deferred()
232 self.host = host
233 self._entities_cache = {} # XXX: keep presence/last resource/other data in cache
234 # /!\ an entity is not necessarily in roster
235 # main key is bare jid, value is a dict
236 # where main key is resource, or None for bare jid
237 self._key_signals = set() # key which need a signal to frontends when updated
238 self.subscriptions = {}
239 self.auth_sessions = PasswordSessions() # remember the authenticated profiles
240 self.disco = Discovery(host)
241 fixLocalDir(False) # XXX: tmp update code, will be removed in the future
242 self.config = tools_config.parseMainConf()
243 database_file = os.path.expanduser(os.path.join(self.getConfig('', 'local_dir'), C.SAVEFILE_DATABASE))
244 self.storage = SqliteStorage(database_file, host.version)
245 PersistentDict.storage = self.storage
246 self.params = Params(host, self.storage)
247 log.info(_("Loading default params template"))
248 self.params.load_default_params()
249 d = self.storage.initialized.addCallback(lambda ignore: self.load())
250 self.memory_data = PersistentDict("memory")
251 d.addCallback(lambda ignore: self.memory_data.load())
252 d.addCallback(lambda ignore: self.disco.load())
253 d.chainDeferred(self.initialized)
254
255 ## Configuration ##
256
257 def getConfig(self, section, name, default=None):
258 """Get the main configuration option
259
260 @param section: section of the config file (None or '' for DEFAULT)
261 @param name: name of the option
262 @param default: value to use if not found
263 @return: str, list or dict
264 """
265 return tools_config.getConfig(self.config, section, name, default)
266
267 def load_xml(self, filename):
268 """Load parameters template from xml file
269
270 @param filename (str): input file
271 @return: bool: True in case of success
272 """
273 if not filename:
274 return False
275 filename = os.path.expanduser(filename)
276 if os.path.exists(filename):
277 try:
278 self.params.load_xml(filename)
279 log.debug(_(u"Parameters loaded from file: %s") % filename)
280 return True
281 except Exception as e:
282 log.error(_(u"Can't load parameters from file: %s") % e)
283 return False
284
285 def save_xml(self, filename):
286 """Save parameters template to xml file
287
288 @param filename (str): output file
289 @return: bool: True in case of success
290 """
291 if not filename:
292 return False
293 #TODO: need to encrypt files (at least passwords !) and set permissions
294 filename = os.path.expanduser(filename)
295 try:
296 self.params.save_xml(filename)
297 log.debug(_(u"Parameters saved to file: %s") % filename)
298 return True
299 except Exception as e:
300 log.error(_(u"Can't save parameters to file: %s") % e)
301 return False
302
303 def load(self):
304 """Load parameters and all memory things from db"""
305 #parameters data
306 return self.params.loadGenParams()
307
308 def loadIndividualParams(self, profile):
309 """Load individual parameters for a profile
310 @param profile: %(doc_profile)s"""
311 return self.params.loadIndParams(profile)
312
313 ## Profiles/Sessions management ##
314
315 def startSession(self, password, profile):
316 """"Iniatialise session for a profile
317
318 @param password(unicode): profile session password
319 or empty string is no password is set
320 @param profile: %(doc_profile)s
321 @raise exceptions.ProfileUnknownError if profile doesn't exists
322 @raise exceptions.PasswordError: the password does not match
323 """
324 profile = self.getProfileName(profile)
325
326 def createSession(dummy):
327 """Called once params are loaded."""
328 self._entities_cache[profile] = {}
329 log.info(u"[{}] Profile session started".format(profile))
330 return False
331
332 def backendInitialised(dummy):
333 def doStartSession(dummy=None):
334 if self.isSessionStarted(profile):
335 log.info("Session already started!")
336 return True
337 try:
338 # if there is a value at this point in self._entities_cache,
339 # it is the loadIndividualParams Deferred, the session is starting
340 session_d = self._entities_cache[profile]
341 except KeyError:
342 # else we do request the params
343 session_d = self._entities_cache[profile] = self.loadIndividualParams(profile)
344 session_d.addCallback(createSession)
345 finally:
346 return session_d
347
348 auth_d = self.profileAuthenticate(password, profile)
349 auth_d.addCallback(doStartSession)
350 return auth_d
351
352 if self.host.initialised.called:
353 return defer.succeed(None).addCallback(backendInitialised)
354 else:
355 return self.host.initialised.addCallback(backendInitialised)
356
357 def stopSession(self, profile):
358 """Delete a profile session
359
360 @param profile: %(doc_profile)s
361 """
362 if self.host.isConnected(profile):
363 log.debug(u"Disconnecting profile because of session stop")
364 self.host.disconnect(profile)
365 self.auth_sessions.profileDelUnique(profile)
366 try:
367 self._entities_cache[profile]
368 except KeyError:
369 log.warning(u"Profile was not in cache")
370
371 def _isSessionStarted(self, profile_key):
372 return self.isSessionStarted(self.getProfileName(profile_key))
373
374 def isSessionStarted(self, profile):
375 try:
376 # XXX: if the value in self._entities_cache is a Deferred,
377 # the session is starting but not started yet
378 return not isinstance(self._entities_cache[profile], defer.Deferred)
379 except KeyError:
380 return False
381
382 def profileAuthenticate(self, password, profile):
383 """Authenticate the profile.
384
385 @param password (unicode): the SàT profile password
386 @param profile: %(doc_profile)s
387 @return (D): a deferred None in case of success, a failure otherwise.
388 @raise exceptions.PasswordError: the password does not match
389 """
390 session_data = self.auth_sessions.profileGetUnique(profile)
391 if not password and session_data:
392 # XXX: this allows any frontend to connect with the empty password as soon as
393 # the profile has been authenticated at least once before. It is OK as long as
394 # submitting a form with empty passwords is restricted to local frontends.
395 return defer.succeed(None)
396
397 def check_result(result):
398 if not result:
399 log.warning(u'Authentication failure of profile {}'.format(profile))
400 raise failure.Failure(exceptions.PasswordError(u"The provided profile password doesn't match."))
401 if not session_data: # avoid to create two profile sessions when password if specified
402 return self.newAuthSession(password, profile)
403
404 d = self.asyncGetParamA(C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile)
405 d.addCallback(lambda sat_cipher: PasswordHasher.verify(password, sat_cipher))
406 return d.addCallback(check_result)
407
408 def newAuthSession(self, key, profile):
409 """Start a new session for the authenticated profile.
410
411 The personal key is loaded encrypted from a PersistentDict before being decrypted.
412
413 @param key: the key to decrypt the personal key
414 @param profile: %(doc_profile)s
415 @return: a deferred None value
416 """
417 def gotPersonalKey(personal_key):
418 """Create the session for this profile and store the personal key"""
419 self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, profile=profile)
420 log.debug(u'auth session created for profile %s' % profile)
421
422 d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
423 d.addCallback(lambda data: BlockCipher.decrypt(key, data[C.MEMORY_CRYPTO_KEY]))
424 return d.addCallback(gotPersonalKey)
425
426 def purgeProfileSession(self, profile):
427 """Delete cache of data of profile
428 @param profile: %(doc_profile)s"""
429 log.info(_("[%s] Profile session purge" % profile))
430 self.params.purgeProfile(profile)
431 try:
432 del self._entities_cache[profile]
433 except KeyError:
434 log.error(_(u"Trying to purge roster status cache for a profile not in memory: [%s]") % profile)
435
436 def getProfilesList(self, clients=True, components=False):
437 """retrieve profiles list
438
439 @param clients(bool): if True return clients profiles
440 @param components(bool): if True return components profiles
441 @return (list[unicode]): selected profiles
442 """
443 if not clients and not components:
444 log.warning(_(u"requesting no profiles at all"))
445 return []
446 profiles = self.storage.getProfilesList()
447 if clients and components:
448 return sorted(profiles)
449 isComponent = self.storage.profileIsComponent
450 if clients:
451 p_filter = lambda p: not isComponent(p)
452 else:
453 p_filter = lambda p: isComponent(p)
454
455 return sorted(p for p in profiles if p_filter(p))
456
457 def getProfileName(self, profile_key, return_profile_keys=False):
458 """Return name of profile from keyword
459
460 @param profile_key: can be the profile name or a keyword (like @DEFAULT@)
461 @param return_profile_keys: if True, return unmanaged profile keys (like "@ALL@"). This keys must be managed by the caller
462 @return: requested profile name
463 @raise exceptions.ProfileUnknownError if profile doesn't exists
464 """
465 return self.params.getProfileName(profile_key, return_profile_keys)
466
467 def profileSetDefault(self, profile):
468 """Set default profile
469
470 @param profile: %(doc_profile)s
471 """
472 # we want to be sure that the profile exists
473 profile = self.getProfileName(profile)
474
475 self.memory_data['Profile_default'] = profile
476
477 def createProfile(self, name, password, component=None):
478 """Create a new profile
479
480 @param name(unicode): profile name
481 @param password(unicode): profile password
482 Can be empty to disable password
483 @param component(None, unicode): set to entry point if this is a component
484 @return: Deferred
485 @raise exceptions.NotFound: component is not a known plugin import name
486 """
487 if not name:
488 raise ValueError(u"Empty profile name")
489 if name[0] == '@':
490 raise ValueError(u"A profile name can't start with a '@'")
491 if '\n' in name:
492 raise ValueError(u"A profile name can't contain line feed ('\\n')")
493
494 if name in self._entities_cache:
495 raise exceptions.ConflictError(u"A session for this profile exists")
496
497 if component:
498 if not component in self.host.plugins:
499 raise exceptions.NotFound(_(u"Can't find component {component} entry point".format(
500 component = component)))
501 # FIXME: PLUGIN_INFO is not currently accessible after import, but type shoul be tested here
502 # if self.host.plugins[component].PLUGIN_INFO[u"type"] != C.PLUG_TYPE_ENTRY_POINT:
503 #  raise ValueError(_(u"Plugin {component} is not an entry point !".format(
504 #  component = component)))
505
506 d = self.params.createProfile(name, component)
507
508 def initPersonalKey(dummy):
509 # be sure to call this after checking that the profile doesn't exist yet
510 personal_key = BlockCipher.getRandomKey(base64=True) # generated once for all and saved in a PersistentDict
511 self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, profile=name) # will be encrypted by setParam
512
513 def startFakeSession(dummy):
514 # avoid ProfileNotConnected exception in setParam
515 self._entities_cache[name] = None
516 self.params.loadIndParams(name)
517
518 def stopFakeSession(dummy):
519 del self._entities_cache[name]
520 self.params.purgeProfile(name)
521
522 d.addCallback(initPersonalKey)
523 d.addCallback(startFakeSession)
524 d.addCallback(lambda dummy: self.setParam(C.PROFILE_PASS_PATH[1], password, C.PROFILE_PASS_PATH[0], profile_key=name))
525 d.addCallback(stopFakeSession)
526 d.addCallback(lambda dummy: self.auth_sessions.profileDelUnique(name))
527 return d
528
529 def asyncDeleteProfile(self, name, force=False):
530 """Delete an existing profile
531
532 @param name: Name of the profile
533 @param force: force the deletion even if the profile is connected.
534 To be used for direct calls only (not through the bridge).
535 @return: a Deferred instance
536 """
537 def cleanMemory(dummy):
538 self.auth_sessions.profileDelUnique(name)
539 try:
540 del self._entities_cache[name]
541 except KeyError:
542 pass
543 d = self.params.asyncDeleteProfile(name, force)
544 d.addCallback(cleanMemory)
545 return d
546
547 def isComponent(self, profile_name):
548 """Tell if a profile is a component
549
550 @param profile_name(unicode): name of the profile
551 @return (bool): True if profile is a component
552 @raise exceptions.NotFound: profile doesn't exist
553 """
554 return self.storage.profileIsComponent(profile_name)
555
556 def getEntryPoint(self, profile_name):
557 """Get a component entry point
558
559 @param profile_name(unicode): name of the profile
560 @return (bool): True if profile is a component
561 @raise exceptions.NotFound: profile doesn't exist
562 """
563 return self.storage.getEntryPoint(profile_name)
564
565 ## History ##
566
567 def addToHistory(self, client, data):
568 return self.storage.addToHistory(data, client.profile)
569
570 def _historyGet(self, from_jid_s, to_jid_s, limit=C.HISTORY_LIMIT_NONE, between=True, filters=None, profile=C.PROF_KEY_NONE):
571 return self.historyGet(jid.JID(from_jid_s), jid.JID(to_jid_s), limit, between, filters, profile)
572
573 def historyGet(self, from_jid, to_jid, limit=C.HISTORY_LIMIT_NONE, between=True, filters=None, profile=C.PROF_KEY_NONE):
574 """Retrieve messages in history
575
576 @param from_jid (JID): source JID (full, or bare for catchall)
577 @param to_jid (JID): dest JID (full, or bare for catchall)
578 @param limit (int): maximum number of messages to get:
579 - 0 for no message (returns the empty list)
580 - C.HISTORY_LIMIT_NONE or None for unlimited
581 - C.HISTORY_LIMIT_DEFAULT to use the HISTORY_LIMIT parameter value
582 @param between (bool): confound source and dest (ignore the direction)
583 @param filters (str): pattern to filter the history results (see bridge API for details)
584 @param profile (str): %(doc_profile)s
585 @return (D(list)): list of message data as in [messageNew]
586 """
587 assert profile != C.PROF_KEY_NONE
588 if limit == C.HISTORY_LIMIT_DEFAULT:
589 limit = int(self.getParamA(C.HISTORY_LIMIT, 'General', profile_key=profile))
590 elif limit == C.HISTORY_LIMIT_NONE:
591 limit = None
592 if limit == 0:
593 return defer.succeed([])
594 return self.storage.historyGet(from_jid, to_jid, limit, between, filters, profile)
595
596 ## Statuses ##
597
598 def _getPresenceStatuses(self, profile_key):
599 ret = self.getPresenceStatuses(profile_key)
600 return {entity.full():data for entity, data in ret.iteritems()}
601
602 def getPresenceStatuses(self, profile_key):
603 """Get all the presence statuses of a profile
604
605 @param profile_key: %(doc_profile_key)s
606 @return: presence data: key=entity JID, value=presence data for this entity
607 """
608 client = self.host.getClient(profile_key)
609 profile_cache = self._getProfileCache(client)
610 entities_presence = {}
611
612 for entity_jid, entity_data in profile_cache.iteritems():
613 for resource, resource_data in entity_data.iteritems():
614 full_jid = copy.copy(entity_jid)
615 full_jid.resource = resource
616 try:
617 presence_data = self.getEntityDatum(full_jid, "presence", profile_key)
618 except KeyError:
619 continue
620 entities_presence.setdefault(entity_jid, {})[resource or ''] = presence_data
621
622 return entities_presence
623
624 def setPresenceStatus(self, entity_jid, show, priority, statuses, profile_key):
625 """Change the presence status of an entity
626
627 @param entity_jid: jid.JID of the entity
628 @param show: show status
629 @param priority: priority
630 @param statuses: dictionary of statuses
631 @param profile_key: %(doc_profile_key)s
632 """
633 presence_data = PresenceTuple(show, priority, statuses)
634 self.updateEntityData(entity_jid, "presence", presence_data, profile_key=profile_key)
635 if entity_jid.resource and show != C.PRESENCE_UNAVAILABLE:
636 # If a resource is available, bare jid should not have presence information
637 try:
638 self.delEntityDatum(entity_jid.userhostJID(), "presence", profile_key)
639 except (KeyError, exceptions.UnknownEntityError):
640 pass
641
642 ## Resources ##
643
644 def _getAllResource(self, jid_s, profile_key):
645 client = self.host.getClient(profile_key)
646 jid_ = jid.JID(jid_s)
647 return self.getAllResources(client, jid_)
648
649 def getAllResources(self, client, entity_jid):
650 """Return all resource from jid for which we have had data in this session
651
652 @param entity_jid: bare jid of the entity
653 return (list[unicode]): list of resources
654
655 @raise exceptions.UnknownEntityError: if entity is not in cache
656 @raise ValueError: entity_jid has a resource
657 """
658 if entity_jid.resource:
659 raise ValueError("getAllResources must be used with a bare jid (got {})".format(entity_jid))
660 profile_cache = self._getProfileCache(client)
661 try:
662 entity_data = profile_cache[entity_jid.userhostJID()]
663 except KeyError:
664 raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
665 resources= set(entity_data.keys())
666 resources.discard(None)
667 return resources
668
669 def getAvailableResources(self, client, entity_jid):
670 """Return available resource for entity_jid
671
672 This method differs from getAllResources by returning only available resources
673 @param entity_jid: bare jid of the entit
674 return (list[unicode]): list of available resources
675
676 @raise exceptions.UnknownEntityError: if entity is not in cache
677 """
678 available = []
679 for resource in self.getAllResources(client, entity_jid):
680 full_jid = copy.copy(entity_jid)
681 full_jid.resource = resource
682 try:
683 presence_data = self.getEntityDatum(full_jid, "presence", client.profile)
684 except KeyError:
685 log.debug(u"Can't get presence data for {}".format(full_jid))
686 else:
687 if presence_data.show != C.PRESENCE_UNAVAILABLE:
688 available.append(resource)
689 return available
690
691 def _getMainResource(self, jid_s, profile_key):
692 client = self.host.getClient(profile_key)
693 jid_ = jid.JID(jid_s)
694 return self.getMainResource(client, jid_) or ""
695
696 def getMainResource(self, client, entity_jid):
697 """Return the main resource used by an entity
698
699 @param entity_jid: bare entity jid
700 @return (unicode): main resource or None
701 """
702 if entity_jid.resource:
703 raise ValueError("getMainResource must be used with a bare jid (got {})".format(entity_jid))
704 try:
705 if self.host.plugins["XEP-0045"].isJoinedRoom(client, entity_jid):
706 return None # MUC rooms have no main resource
707 except KeyError: # plugin not found
708 pass
709 try:
710 resources = self.getAllResources(client, entity_jid)
711 except exceptions.UnknownEntityError:
712 log.warning(u"Entity is not in cache, we can't find any resource")
713 return None
714 priority_resources = []
715 for resource in resources:
716 full_jid = copy.copy(entity_jid)
717 full_jid.resource = resource
718 try:
719 presence_data = self.getEntityDatum(full_jid, "presence", client.profile)
720 except KeyError:
721 log.debug(u"No presence information for {}".format(full_jid))
722 continue
723 priority_resources.append((resource, presence_data.priority))
724 try:
725 return max(priority_resources, key=lambda res_tuple: res_tuple[1])[0]
726 except ValueError:
727 log.warning(u"No resource found at all for {}".format(entity_jid))
728 return None
729
730 ## Entities data ##
731
732 def _getProfileCache(self, client):
733 """Check profile validity and return its cache
734
735 @param client: SatXMPPClient
736 @return (dict): profile cache
737 """
738 return self._entities_cache[client.profile]
739
740 def setSignalOnUpdate(self, key, signal=True):
741 """Set a signal flag on the key
742
743 When the key will be updated, a signal will be sent to frontends
744 @param key: key to signal
745 @param signal(boolean): if True, do the signal
746 """
747 if signal:
748 self._key_signals.add(key)
749 else:
750 self._key_signals.discard(key)
751
752 def getAllEntitiesIter(self, client, with_bare=False):
753 """Return an iterator of full jids of all entities in cache
754
755 @param with_bare: if True, include bare jids
756 @return (list[unicode]): list of jids
757 """
758 profile_cache = self._getProfileCache(client)
759 # we construct a list of all known full jids (bare jid of entities x resources)
760 for bare_jid, entity_data in profile_cache.iteritems():
761 for resource in entity_data.iterkeys():
762 if resource is None:
763 continue
764 full_jid = copy.copy(bare_jid)
765 full_jid.resource = resource
766 yield full_jid
767
768 def updateEntityData(self, entity_jid, key, value, silent=False, profile_key=C.PROF_KEY_NONE):
769 """Set a misc data for an entity
770
771 If key was registered with setSignalOnUpdate, a signal will be sent to frontends
772 @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities,
773 C.ENTITY_ALL for all entities (all resources + bare jids)
774 @param key: key to set (eg: "type")
775 @param value: value for this key (eg: "chatroom")
776 @param silent(bool): if True, doesn't send signal to frontend, even if there is a signal flag (see setSignalOnUpdate)
777 @param profile_key: %(doc_profile_key)s
778 """
779 client = self.host.getClient(profile_key)
780 profile_cache = self._getProfileCache(client)
781 if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
782 entities = self.getAllEntitiesIter(client, entity_jid==C.ENTITY_ALL)
783 else:
784 entities = (entity_jid,)
785
786 for jid_ in entities:
787 entity_data = profile_cache.setdefault(jid_.userhostJID(),{}).setdefault(jid_.resource, {})
788
789 entity_data[key] = value
790 if key in self._key_signals and not silent:
791 if not isinstance(value, basestring):
792 log.error(u"Setting a non string value ({}) for a key ({}) which has a signal flag".format(value, key))
793 else:
794 self.host.bridge.entityDataUpdated(jid_.full(), key, value, self.getProfileName(profile_key))
795
796 def delEntityDatum(self, entity_jid, key, profile_key):
797 """Delete a data for an entity
798
799 @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities,
800 C.ENTITY_ALL for all entities (all resources + bare jids)
801 @param key: key to delete (eg: "type")
802 @param profile_key: %(doc_profile_key)s
803
804 @raise exceptions.UnknownEntityError: if entity is not in cache
805 @raise KeyError: key is not in cache
806 """
807 client = self.host.getClient(profile_key)
808 profile_cache = self._getProfileCache(client)
809 if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
810 entities = self.getAllEntitiesIter(client, entity_jid==C.ENTITY_ALL)
811 else:
812 entities = (entity_jid,)
813
814 for jid_ in entities:
815 try:
816 entity_data = profile_cache[jid_.userhostJID()][jid_.resource]
817 except KeyError:
818 raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(jid_))
819 try:
820 del entity_data[key]
821 except KeyError as e:
822 if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
823 continue # we ignore KeyError when deleting keys from several entities
824 else:
825 raise e
826
827 def _getEntitiesData(self, entities_jids, keys_list, profile_key):
828 ret = self.getEntitiesData([jid.JID(jid_) for jid_ in entities_jids], keys_list, profile_key)
829 return {jid_.full(): data for jid_, data in ret.iteritems()}
830
831 def getEntitiesData(self, entities_jids, keys_list=None, profile_key=C.PROF_KEY_NONE):
832 """Get a list of cached values for several entities at once
833
834 @param entities_jids: jids of the entities, or empty list for all entities in cache
835 @param keys_list (iterable,None): list of keys to get, None for everything
836 @param profile_key: %(doc_profile_key)s
837 @return: dict withs values for each key in keys_list.
838 if there is no value of a given key, resulting dict will
839 have nothing with that key nether
840 if an entity doesn't exist in cache, it will not appear
841 in resulting dict
842
843 @raise exceptions.UnknownEntityError: if entity is not in cache
844 """
845 def fillEntityData(entity_cache_data):
846 entity_data = {}
847 if keys_list is None:
848 entity_data = entity_cache_data
849 else:
850 for key in keys_list:
851 try:
852 entity_data[key] = entity_cache_data[key]
853 except KeyError:
854 continue
855 return entity_data
856
857 client = self.host.getClient(profile_key)
858 profile_cache = self._getProfileCache(client)
859 ret_data = {}
860 if entities_jids:
861 for entity in entities_jids:
862 try:
863 entity_cache_data = profile_cache[entity.userhostJID()][entity.resource]
864 except KeyError:
865 continue
866 ret_data[entity.full()] = fillEntityData(entity_cache_data, keys_list)
867 else:
868 for bare_jid, data in profile_cache.iteritems():
869 for resource, entity_cache_data in data.iteritems():
870 full_jid = copy.copy(bare_jid)
871 full_jid.resource = resource
872 ret_data[full_jid] = fillEntityData(entity_cache_data)
873
874 return ret_data
875
876 def getEntityData(self, entity_jid, keys_list=None, profile_key=C.PROF_KEY_NONE):
877 """Get a list of cached values for entity
878
879 @param entity_jid: JID of the entity
880 @param keys_list (iterable,None): list of keys to get, None for everything
881 @param profile_key: %(doc_profile_key)s
882 @return: dict withs values for each key in keys_list.
883 if there is no value of a given key, resulting dict will
884 have nothing with that key nether
885
886 @raise exceptions.UnknownEntityError: if entity is not in cache
887 """
888 client = self.host.getClient(profile_key)
889 profile_cache = self._getProfileCache(client)
890 try:
891 entity_data = profile_cache[entity_jid.userhostJID()][entity_jid.resource]
892 except KeyError:
893 raise exceptions.UnknownEntityError(u"Entity {} not in cache (was requesting {})".format(entity_jid, keys_list))
894 if keys_list is None:
895 return entity_data
896
897 return {key: entity_data[key] for key in keys_list if key in entity_data}
898
899 def getEntityDatum(self, entity_jid, key, profile_key):
900 """Get a datum from entity
901
902 @param entity_jid: JID of the entity
903 @param keys: key to get
904 @param profile_key: %(doc_profile_key)s
905 @return: requested value
906
907 @raise exceptions.UnknownEntityError: if entity is not in cache
908 @raise KeyError: if there is no value for this key and this entity
909 """
910 return self.getEntityData(entity_jid, (key,), profile_key)[key]
911
912 def delEntityCache(self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE):
913 """Remove all cached data for entity
914
915 @param entity_jid: JID of the entity to delete
916 @param delete_all_resources: if True also delete all known resources from cache (a bare jid must be given in this case)
917 @param profile_key: %(doc_profile_key)s
918
919 @raise exceptions.UnknownEntityError: if entity is not in cache
920 """
921 client = self.host.getClient(profile_key)
922 profile_cache = self._getProfileCache(client)
923
924 if delete_all_resources:
925 if entity_jid.resource:
926 raise ValueError(_("Need a bare jid to delete all resources"))
927 try:
928 del profile_cache[entity_jid]
929 except KeyError:
930 raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
931 else:
932 try:
933 del profile_cache[entity_jid.userhostJID()][entity_jid.resource]
934 except KeyError:
935 raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
936
937 ## Encryption ##
938
939 def encryptValue(self, value, profile):
940 """Encrypt a value for the given profile. The personal key must be loaded
941 already in the profile session, that should be the case if the profile is
942 already authenticated.
943
944 @param value (str): the value to encrypt
945 @param profile (str): %(doc_profile)s
946 @return: the deferred encrypted value
947 """
948 try:
949 personal_key = self.auth_sessions.profileGetUnique(profile)[C.MEMORY_CRYPTO_KEY]
950 except TypeError:
951 raise exceptions.InternalError(_('Trying to encrypt a value for %s while the personal key is undefined!') % profile)
952 return BlockCipher.encrypt(personal_key, value)
953
954 def decryptValue(self, value, profile):
955 """Decrypt a value for the given profile. The personal key must be loaded
956 already in the profile session, that should be the case if the profile is
957 already authenticated.
958
959 @param value (str): the value to decrypt
960 @param profile (str): %(doc_profile)s
961 @return: the deferred decrypted value
962 """
963 try:
964 personal_key = self.auth_sessions.profileGetUnique(profile)[C.MEMORY_CRYPTO_KEY]
965 except TypeError:
966 raise exceptions.InternalError(_('Trying to decrypt a value for %s while the personal key is undefined!') % profile)
967 return BlockCipher.decrypt(personal_key, value)
968
969 def encryptPersonalData(self, data_key, data_value, crypto_key, profile):
970 """Re-encrypt a personal data (saved to a PersistentDict).
971
972 @param data_key: key for the individual PersistentDict instance
973 @param data_value: the value to be encrypted
974 @param crypto_key: the key to encrypt the value
975 @param profile: %(profile_doc)s
976 @return: a deferred None value
977 """
978
979 def gotIndMemory(data):
980 d = BlockCipher.encrypt(crypto_key, data_value)
981
982 def cb(cipher):
983 data[data_key] = cipher
984 return data.force(data_key)
985
986 return d.addCallback(cb)
987
988 def done(dummy):
989 log.debug(_(u'Personal data (%(ns)s, %(key)s) has been successfuly encrypted') %
990 {'ns': C.MEMORY_CRYPTO_NAMESPACE, 'key': data_key})
991
992 d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
993 return d.addCallback(gotIndMemory).addCallback(done)
994
995 ## Subscription requests ##
996
997 def addWaitingSub(self, type_, entity_jid, profile_key):
998 """Called when a subcription request is received"""
999 profile = self.getProfileName(profile_key)
1000 assert profile
1001 if profile not in self.subscriptions:
1002 self.subscriptions[profile] = {}
1003 self.subscriptions[profile][entity_jid] = type_
1004
1005 def delWaitingSub(self, entity_jid, profile_key):
1006 """Called when a subcription request is finished"""
1007 profile = self.getProfileName(profile_key)
1008 assert profile
1009 if profile in self.subscriptions and entity_jid in self.subscriptions[profile]:
1010 del self.subscriptions[profile][entity_jid]
1011
1012 def getWaitingSub(self, profile_key):
1013 """Called to get a list of currently waiting subscription requests"""
1014 profile = self.getProfileName(profile_key)
1015 if not profile:
1016 log.error(_('Asking waiting subscriptions for a non-existant profile'))
1017 return {}
1018 if profile not in self.subscriptions:
1019 return {}
1020
1021 return self.subscriptions[profile]
1022
1023 ## Parameters ##
1024
1025 def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
1026 return self.params.getStringParamA(name, category, attr, profile_key)
1027
1028 def getParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
1029 return self.params.getParamA(name, category, attr, profile_key=profile_key)
1030
1031 def asyncGetParamA(self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
1032 return self.params.asyncGetParamA(name, category, attr, security_limit, profile_key)
1033
1034 def asyncGetParamsValuesFromCategory(self, category, security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
1035 return self.params.asyncGetParamsValuesFromCategory(category, security_limit, profile_key)
1036
1037 def asyncGetStringParamA(self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
1038 return self.params.asyncGetStringParamA(name, category, attr, security_limit, profile_key)
1039
1040 def getParamsUI(self, security_limit=C.NO_SECURITY_LIMIT, app='', profile_key=C.PROF_KEY_NONE):
1041 return self.params.getParamsUI(security_limit, app, profile_key)
1042
1043 def getParamsCategories(self):
1044 return self.params.getParamsCategories()
1045
1046 def setParam(self, name, value, category, security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
1047 return self.params.setParam(name, value, category, security_limit, profile_key)
1048
1049 def updateParams(self, xml):
1050 return self.params.updateParams(xml)
1051
1052 def paramsRegisterApp(self, xml, security_limit=C.NO_SECURITY_LIMIT, app=''):
1053 return self.params.paramsRegisterApp(xml, security_limit, app)
1054
1055 def setDefault(self, name, category, callback, errback=None):
1056 return self.params.setDefault(name, category, callback, errback)
1057
1058 ## Files ##
1059
1060 def checkFilePermission(self, file_data, peer_jid, perms_to_check):
1061 """check that an entity has the right permission on a file
1062
1063 @param file_data(dict): data of one file, as returned by getFiles
1064 @param peer_jid(jid.JID): entity trying to access the file
1065 @param perms_to_check(tuple[unicode]): permissions to check
1066 tuple of C.ACCESS_PERM_*
1067 @param check_parents(bool): if True, also check all parents until root node
1068 @raise exceptions.PermissionError: peer_jid doesn't have all permission
1069 in perms_to_check for file_data
1070 @raise exceptions.InternalError: perms_to_check is invalid
1071 """
1072 if peer_jid is None and perms_to_check is None:
1073 return
1074 peer_jid = peer_jid.userhostJID()
1075 if peer_jid == file_data['owner']:
1076 # the owner has all rights
1077 return
1078 if not C.ACCESS_PERMS.issuperset(perms_to_check):
1079 raise exceptions.InternalError(_(u'invalid permission'))
1080
1081 for perm in perms_to_check:
1082 # we check each perm and raise PermissionError as soon as one condition is not valid
1083 # we must never return here, we only return after the loop if nothing was blocking the access
1084 try:
1085 perm_data = file_data[u'access'][perm]
1086 perm_type = perm_data[u'type']
1087 except KeyError:
1088 raise failure.Failure(exceptions.PermissionError())
1089 if perm_type == C.ACCESS_TYPE_PUBLIC:
1090 continue
1091 elif perm_type == C.ACCESS_TYPE_WHITELIST:
1092 try:
1093 jids = perm_data[u'jids']
1094 except KeyError:
1095 raise failure.Failure(exceptions.PermissionError())
1096 if peer_jid.full() in jids:
1097 continue
1098 else:
1099 raise failure.Failure(exceptions.PermissionError())
1100 else:
1101 raise exceptions.InternalError(_(u'unknown access type: {type}').format(type=perm_type))
1102
1103 @defer.inlineCallbacks
1104 def checkPermissionToRoot(self, client, file_data, peer_jid, perms_to_check):
1105 """do checkFilePermission on file_data and all its parents until root"""
1106 current = file_data
1107 while True:
1108 self.checkFilePermission(current, peer_jid, perms_to_check)
1109 parent = current[u'parent']
1110 if not parent:
1111 break
1112 files_data = yield self.getFile(self, client, peer_jid=None, file_id=parent, perms_to_check=None)
1113 try:
1114 current = files_data[0]
1115 except IndexError:
1116 raise exceptions.DataError(u'Missing parent')
1117
1118 @defer.inlineCallbacks
1119 def _getParentDir(self, client, path, parent, namespace, owner, peer_jid, perms_to_check):
1120 """Retrieve parent node from a path, or last existing directory
1121
1122 each directory of the path will be retrieved, until the last existing one
1123 @return (tuple[unicode, list[unicode])): parent, remaining path elements:
1124 - parent is the id of the last retrieved directory (or u'' for root)
1125 - remaining path elements are the directories which have not been retrieved
1126 (i.e. which don't exist)
1127 """
1128 # if path is set, we have to retrieve parent directory of the file(s) from it
1129 if parent is not None:
1130 raise exceptions.ConflictError(_(u"You can't use path and parent at the same time"))
1131 path_elts = filter(None, path.split(u'/'))
1132 if {u'..', u'.'}.intersection(path_elts):
1133 raise ValueError(_(u'".." or "." can\'t be used in path'))
1134
1135 # we retrieve all directories from path until we get the parent container
1136 # non existing directories will be created
1137 parent = u''
1138 for idx, path_elt in enumerate(path_elts):
1139 directories = yield self.storage.getFiles(client, parent=parent, type_=C.FILE_TYPE_DIRECTORY,
1140 name=path_elt, namespace=namespace, owner=owner)
1141 if not directories:
1142 defer.returnValue((parent, path_elts[idx:]))
1143 # from this point, directories don't exist anymore, we have to create them
1144 elif len(directories) > 1:
1145 raise exceptions.InternalError(_(u"Several directories found, this should not happen"))
1146 else:
1147 directory = directories[0]
1148 self.checkFilePermission(directory, peer_jid, perms_to_check)
1149 parent = directory[u'id']
1150 defer.returnValue((parent, []))
1151
1152 @defer.inlineCallbacks
1153 def getFiles(self, client, peer_jid, file_id=None, version=None, parent=None, path=None, type_=None,
1154 file_hash=None, hash_algo=None, name=None, namespace=None, mime_type=None,
1155 owner=None, access=None, projection=None, unique=False, perms_to_check=(C.ACCESS_PERM_READ,)):
1156 """retrieve files with with given filters
1157
1158 @param peer_jid(jid.JID, None): jid trying to access the file
1159 needed to check permission.
1160 Use None to ignore permission (perms_to_check must be None too)
1161 @param file_id(unicode, None): id of the file
1162 None to ignore
1163 @param version(unicode, None): version of the file
1164 None to ignore
1165 empty string to look for current version
1166 @param parent(unicode, None): id of the directory containing the files
1167 None to ignore
1168 empty string to look for root files/directories
1169 @param projection(list[unicode], None): name of columns to retrieve
1170 None to retrieve all
1171 @param unique(bool): if True will remove duplicates
1172 @param perms_to_check(tuple[unicode],None): permission to check
1173 must be a tuple of C.ACCESS_PERM_* or None
1174 if None, permission will no be checked (peer_jid must be None too in this case)
1175 other params are the same as for [setFile]
1176 @return (list[dict]): files corresponding to filters
1177 @raise exceptions.NotFound: parent directory not found (when path is specified)
1178 @raise exceptions.PermissionError: peer_jid can't use perms_to_check for one of the file
1179 on the path
1180 """
1181 if peer_jid is None and perms_to_check or perms_to_check is None and peer_jid:
1182 raise exceptions.InternalError('if you want to disable permission check, both peer_jid and perms_to_check must be None')
1183 if owner is not None:
1184 owner = owner.userhostJID()
1185 if path is not None:
1186 # permission are checked by _getParentDir
1187 parent, remaining_path_elts = yield self._getParentDir(client, path, parent, namespace, owner, peer_jid, perms_to_check)
1188 if remaining_path_elts:
1189 # if we have remaining path elements,
1190 # the parent directory is not found
1191 raise failure.Failure(exceptions.NotFound())
1192 if parent and peer_jid:
1193 # if parent is given directly and permission check is need,
1194 # we need to check all the parents
1195 parent_data = yield self.storage.getFiles(client, file_id=parent)
1196 try:
1197 parent_data = parent_data[0]
1198 except IndexError:
1199 raise exceptions.DataError(u'mising parent')
1200 yield self.checkPermissionToRoot(client, parent_data, peer_jid, perms_to_check)
1201
1202 files = yield self.storage.getFiles(client, file_id=file_id, version=version, parent=parent, type_=type_,
1203 file_hash=file_hash, hash_algo=hash_algo, name=name, namespace=namespace,
1204 mime_type=mime_type, owner=owner, access=access,
1205 projection=projection, unique=unique)
1206
1207 if peer_jid:
1208 # if permission are checked, we must remove all file tha use can't access
1209 to_remove = []
1210 for file_data in files:
1211 try:
1212 self.checkFilePermission(file_data, peer_jid, perms_to_check)
1213 except exceptions.PermissionError:
1214 to_remove.append(file_data)
1215 for file_data in to_remove:
1216 files.remove(file_data)
1217 defer.returnValue(files)
1218
1219 @defer.inlineCallbacks
1220 def setFile(self, client, name, file_id=None, version=u'', parent=None, path=None,
1221 type_=C.FILE_TYPE_FILE, file_hash=None, hash_algo=None, size=None, namespace=None,
1222 mime_type=None, created=None, modified=None, owner=None, access=None, extra=None,
1223 peer_jid = None, perms_to_check=(C.ACCESS_PERM_WRITE,)):
1224 """set a file metadata
1225
1226 @param name(unicode): basename of the file
1227 @param file_id(unicode): unique id of the file
1228 @param version(unicode): version of this file
1229 empty string for current version or when there is no versioning
1230 @param parent(unicode, None): id of the directory containing the files
1231 @param path(unicode, None): virtual path of the file in the namespace
1232 if set, parent must be None. All intermediate directories will be created if needed,
1233 using current access.
1234 @param file_hash(unicode): unique hash of the payload
1235 @param hash_algo(unicode): algorithm used for hashing the file (usually sha-256)
1236 @param size(int): size in bytes
1237 @param namespace(unicode, None): identifier (human readable is better) to group files
1238 for instance, namespace could be used to group files in a specific photo album
1239 @param mime_type(unicode): MIME type of the file, or None if not known/guessed
1240 @param created(int): UNIX time of creation
1241 @param modified(int,None): UNIX time of last modification, or None to use created date
1242 @param owner(jid.JID, None): jid of the owner of the file (mainly useful for component)
1243 will be used to check permission (only bare jid is used, don't use with MUC).
1244 Use None to ignore permission (perms_to_check must be None too)
1245 @param access(dict, None): serialisable dictionary with access rules.
1246 None (or empty dict) to use private access, i.e. allow only profile's jid to access the file
1247 key can be on on C.ACCESS_PERM_*,
1248 then a sub dictionary with a type key is used (one of C.ACCESS_TYPE_*).
1249 According to type, extra keys can be used:
1250 - C.ACCESS_TYPE_PUBLIC: the permission is granted for everybody
1251 - C.ACCESS_TYPE_WHITELIST: the permission is granted for jids (as unicode) in the 'jids' key
1252 will be encoded to json in database
1253 @param extra(dict, None): serialisable dictionary of any extra data
1254 will be encoded to json in database
1255 @param perms_to_check(tuple[unicode],None): permission to check
1256 must be a tuple of C.ACCESS_PERM_* or None
1257 if None, permission will no be checked (peer_jid must be None too in this case)
1258 @param profile(unicode): profile owning the file
1259 """
1260 if '/' in name:
1261 raise ValueError('name must not contain a slash ("/")')
1262 if file_id is None:
1263 file_id = shortuuid.uuid()
1264 if file_hash is not None and hash_algo is None or hash_algo is not None and file_hash is None:
1265 raise ValueError('file_hash and hash_algo must be set at the same time')
1266 if mime_type is None:
1267 mime_type, file_encoding = mimetypes.guess_type(name)
1268 if created is None:
1269 created = time.time()
1270 if namespace is not None:
1271 namespace = namespace.strip() or None
1272 if type_ == C.FILE_TYPE_DIRECTORY:
1273 if any(version, file_hash, size, mime_type):
1274 raise ValueError(u"version, file_hash, size and mime_type can't be set for a directory")
1275 if owner is not None:
1276 owner = owner.userhostJID()
1277
1278 if path is not None:
1279 # _getParentDir will check permissions if peer_jid is set, so we use owner
1280 parent, remaining_path_elts = yield self._getParentDir(client, path, parent, namespace, owner, owner, perms_to_check)
1281 # if remaining directories don't exist, we have to create them
1282 for new_dir in remaining_path_elts:
1283 new_dir_id = shortuuid.uuid()
1284 yield self.storage.setFile(client, name=new_dir, file_id=new_dir_id, version=u'', parent=parent,
1285 type_=C.FILE_TYPE_DIRECTORY, namespace=namespace,
1286 created=time.time(),
1287 owner=owner,
1288 access=access, extra={})
1289 parent = new_dir_id
1290 elif parent is None:
1291 parent = u''
1292
1293 yield self.storage.setFile(client, file_id=file_id, version=version, parent=parent, type_=type_,
1294 file_hash=file_hash, hash_algo=hash_algo, name=name, size=size,
1295 namespace=namespace, mime_type=mime_type, created=created, modified=modified,
1296 owner=owner,
1297 access=access, extra=extra)
1298
1299 def fileUpdate(self, file_id, column, update_cb):
1300 """update a file column taking care of race condition
1301
1302 access is NOT checked in this method, it must be checked beforehand
1303 @param file_id(unicode): id of the file to update
1304 @param column(unicode): one of "access" or "extra"
1305 @param update_cb(callable): method to update the value of the colum
1306 the method will take older value as argument, and must update it in place
1307 Note that the callable must be thread-safe
1308 """
1309 return self.storage.fileUpdate(file_id, column, update_cb)
1310
1311 ## Misc ##
1312
1313 def isEntityAvailable(self, client, entity_jid):
1314 """Tell from the presence information if the given entity is available.
1315
1316 @param entity_jid (JID): the entity to check (if bare jid is used, all resources are tested)
1317 @return (bool): True if entity is available
1318 """
1319 if not entity_jid.resource:
1320 return bool(self.getAvailableResources(client, entity_jid)) # is any resource is available, entity is available
1321 try:
1322 presence_data = self.getEntityDatum(entity_jid, "presence", client.profile)
1323 except KeyError:
1324 log.debug(u"No presence information for {}".format(entity_jid))
1325 return False
1326 return presence_data.show != C.PRESENCE_UNAVAILABLE