Mercurial > libervia-backend
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 |