comparison sat/memory/params.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/params.py@0046283a285d
children 56f94936df1e
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 _, D_
21
22 from sat.core import exceptions
23 from sat.core.constants import Const as C
24 from sat.memory.crypto import BlockCipher, PasswordHasher
25 from xml.dom import minidom, NotFoundErr
26 from sat.core.log import getLogger
27 log = getLogger(__name__)
28 from twisted.internet import defer
29 from twisted.python.failure import Failure
30 from twisted.words.xish import domish
31 from twisted.words.protocols.jabber import jid
32 from sat.tools.xml_tools import paramsXML2XMLUI, getText
33
34 # TODO: params should be rewritten using Twisted directly instead of minidom
35 # general params should be linked to sat.conf and kept synchronised
36 # this need an overall simplification to make maintenance easier
37
38
39 def createJidElts(jids):
40 """Generator which return <jid/> elements from jids
41
42 @param jids(iterable[id.jID]): jids to use
43 @return (generator[domish.Element]): <jid/> elements
44 """
45 for jid_ in jids:
46 jid_elt = domish.Element((None, 'jid'))
47 jid_elt.addContent(jid_.full())
48 yield jid_elt
49
50
51 class Params(object):
52 """This class manage parameters with xml"""
53 ### TODO: add desciption in params
54
55 #TODO: when priority is changed, a new presence stanza must be emitted
56 #TODO: int type (Priority should be int instead of string)
57 default_xml = u"""
58 <params>
59 <general>
60 </general>
61 <individual>
62 <category name="General" label="%(category_general)s">
63 <param name="Password" value="" type="password" />
64 <param name="%(history_param)s" label="%(history_label)s" value="20" constraint="0;100" type="int" security="0" />
65 <param name="%(show_offline_contacts)s" label="%(show_offline_contacts_label)s" value="false" type="bool" security="0" />
66 <param name="%(show_empty_groups)s" label="%(show_empty_groups_label)s" value="true" type="bool" security="0" />
67 </category>
68 <category name="Connection" label="%(category_connection)s">
69 <param name="JabberID" value="name@example.org" type="string" security="10" />
70 <param name="Password" value="" type="password" security="10" />
71 <param name="Priority" value="50" type="int" constraint="-128;127" security="10" />
72 <param name="%(force_server_param)s" value="" type="string" security="50" />
73 <param name="%(force_port_param)s" value="" type="int" constraint="1;65535" security="50" />
74 <param name="autoconnect" label="%(autoconnect_label)s" value="true" type="bool" security="50" />
75 <param name="autodisconnect" label="%(autodisconnect_label)s" value="false" type="bool" security="50" />
76 </category>
77 </individual>
78 </params>
79 """ % {
80 'category_general': D_("General"),
81 'category_connection': D_("Connection"),
82 'history_param': C.HISTORY_LIMIT,
83 'history_label': D_('Chat history limit'),
84 'show_offline_contacts': C.SHOW_OFFLINE_CONTACTS,
85 'show_offline_contacts_label': D_('Show offline contacts'),
86 'show_empty_groups': C.SHOW_EMPTY_GROUPS,
87 'show_empty_groups_label': D_('Show empty groups'),
88 'force_server_param': C.FORCE_SERVER_PARAM,
89 'force_port_param': C.FORCE_PORT_PARAM,
90 'new_account_label': D_("Register new account"),
91 'autoconnect_label': D_('Connect on frontend startup'),
92 'autodisconnect_label': D_('Disconnect on frontend closure'),
93 }
94
95 def load_default_params(self):
96 self.dom = minidom.parseString(Params.default_xml.encode('utf-8'))
97
98 def _mergeParams(self, source_node, dest_node):
99 """Look for every node in source_node and recursively copy them to dest if they don't exists"""
100
101 def getNodesMap(children):
102 ret = {}
103 for child in children:
104 if child.nodeType == child.ELEMENT_NODE:
105 ret[(child.tagName, child.getAttribute('name'))] = child
106 return ret
107 source_map = getNodesMap(source_node.childNodes)
108 dest_map = getNodesMap(dest_node.childNodes)
109 source_set = set(source_map.keys())
110 dest_set = set(dest_map.keys())
111 to_add = source_set.difference(dest_set)
112
113 for node_key in to_add:
114 dest_node.appendChild(source_map[node_key].cloneNode(True))
115
116 to_recurse = source_set - to_add
117 for node_key in to_recurse:
118 self._mergeParams(source_map[node_key], dest_map[node_key])
119
120 def load_xml(self, xml_file):
121 """Load parameters template from xml file"""
122 self.dom = minidom.parse(xml_file)
123 default_dom = minidom.parseString(Params.default_xml.encode('utf-8'))
124 self._mergeParams(default_dom.documentElement, self.dom.documentElement)
125
126 def loadGenParams(self):
127 """Load general parameters data from storage
128
129 @return: deferred triggered once params are loaded
130 """
131 return self.storage.loadGenParams(self.params_gen)
132
133 def loadIndParams(self, profile, cache=None):
134 """Load individual parameters
135
136 set self.params cache or a temporary cache
137 @param profile: profile to load (*must exist*)
138 @param cache: if not None, will be used to store the value, as a short time cache
139 @return: deferred triggered once params are loaded
140 """
141 if cache is None:
142 self.params[profile] = {}
143 return self.storage.loadIndParams(self.params[profile] if cache is None else cache, profile)
144
145 def purgeProfile(self, profile):
146 """Remove cache data of a profile
147
148 @param profile: %(doc_profile)s
149 """
150 try:
151 del self.params[profile]
152 except KeyError:
153 log.error(_(u"Trying to purge cache of a profile not in memory: [%s]") % profile)
154
155 def save_xml(self, filename):
156 """Save parameters template to xml file"""
157 with open(filename, 'wb') as xml_file:
158 xml_file.write(self.dom.toxml('utf-8'))
159
160 def __init__(self, host, storage):
161 log.debug("Parameters init")
162 self.host = host
163 self.storage = storage
164 self.default_profile = None
165 self.params = {}
166 self.params_gen = {}
167
168 def createProfile(self, profile, component):
169 """Create a new profile
170
171 @param profile(unicode): name of the profile
172 @param component(unicode): entry point if profile is a component
173 @param callback: called when the profile actually exists in database and memory
174 @return: a Deferred instance
175 """
176 if self.storage.hasProfile(profile):
177 log.info(_('The profile name already exists'))
178 return defer.fail(Failure(exceptions.ConflictError))
179 if not self.host.trigger.point("ProfileCreation", profile):
180 return defer.fail(Failure(exceptions.CancelError))
181 return self.storage.createProfile(profile, component or None)
182
183 def asyncDeleteProfile(self, profile, force=False):
184 """Delete an existing profile
185
186 @param profile: name of the profile
187 @param force: force the deletion even if the profile is connected.
188 To be used for direct calls only (not through the bridge).
189 @return: a Deferred instance
190 """
191 if not self.storage.hasProfile(profile):
192 log.info(_('Trying to delete an unknown profile'))
193 return defer.fail(Failure(exceptions.ProfileUnknownError(profile)))
194 if self.host.isConnected(profile):
195 if force:
196 self.host.disconnect(profile)
197 else:
198 log.info(_("Trying to delete a connected profile"))
199 return defer.fail(Failure(exceptions.ProfileConnected))
200 return self.storage.deleteProfile(profile)
201
202 def getProfileName(self, profile_key, return_profile_keys=False):
203 """return profile according to profile_key
204
205 @param profile_key: profile name or key which can be
206 C.PROF_KEY_ALL for all profiles
207 C.PROF_KEY_DEFAULT for default profile
208 @param return_profile_keys: if True, return unmanaged profile keys (like C.PROF_KEY_ALL). This keys must be managed by the caller
209 @return: requested profile name
210 @raise exceptions.ProfileUnknownError: profile doesn't exists
211 @raise exceptions.ProfileNotSetError: if C.PROF_KEY_NONE is used
212 """
213 if profile_key == '@DEFAULT@':
214 default = self.host.memory.memory_data.get('Profile_default')
215 if not default:
216 log.info(_('No default profile, returning first one'))
217 try:
218 default = self.host.memory.memory_data['Profile_default'] = self.storage.getProfilesList()[0]
219 except IndexError:
220 log.info(_('No profile exist yet'))
221 raise exceptions.ProfileUnknownError(profile_key)
222 return default # FIXME: temporary, must use real default value, and fallback to first one if it doesn't exists
223 elif profile_key == C.PROF_KEY_NONE:
224 raise exceptions.ProfileNotSetError
225 elif return_profile_keys and profile_key in [C.PROF_KEY_ALL]:
226 return profile_key # this value must be managed by the caller
227 if not self.storage.hasProfile(profile_key):
228 log.error(_(u'Trying to access an unknown profile (%s)') % profile_key)
229 raise exceptions.ProfileUnknownError(profile_key)
230 return profile_key
231
232 def __get_unique_node(self, parent, tag, name):
233 """return node with given tag
234
235 @param parent: parent of nodes to check (e.g. documentElement)
236 @param tag: tag to check (e.g. "category")
237 @param name: name to check (e.g. "JID")
238 @return: node if it exist or None
239 """
240 for node in parent.childNodes:
241 if node.nodeName == tag and node.getAttribute("name") == name:
242 #the node already exists
243 return node
244 #the node is new
245 return None
246
247 def updateParams(self, xml, security_limit=C.NO_SECURITY_LIMIT, app=''):
248 """import xml in parameters, update if the param already exists
249
250 If security_limit is specified and greater than -1, the parameters
251 that have a security level greater than security_limit are skipped.
252 @param xml: parameters in xml form
253 @param security_limit: -1 means no security, 0 is the maximum security then the higher the less secure
254 @param app: name of the frontend registering the parameters or empty value
255 """
256 # TODO: should word with domish.Element
257 src_parent = minidom.parseString(xml.encode('utf-8')).documentElement
258
259 def pre_process_app_node(src_parent, security_limit, app):
260 """Parameters that are registered from a frontend must be checked"""
261 to_remove = []
262 for type_node in src_parent.childNodes:
263 if type_node.nodeName != C.INDIVIDUAL:
264 to_remove.append(type_node) # accept individual parameters only
265 continue
266 for cat_node in type_node.childNodes:
267 if cat_node.nodeName != 'category':
268 to_remove.append(cat_node)
269 continue
270 to_remove_count = 0 # count the params to be removed from current category
271 for node in cat_node.childNodes:
272 if node.nodeName != "param" or not self.checkSecurityLimit(node, security_limit):
273 to_remove.append(node)
274 to_remove_count += 1
275 continue
276 node.setAttribute('app', app)
277 if len(cat_node.childNodes) == to_remove_count: # remove empty category
278 for dummy in xrange(0, to_remove_count):
279 to_remove.pop()
280 to_remove.append(cat_node)
281 for node in to_remove:
282 node.parentNode.removeChild(node)
283
284 def import_node(tgt_parent, src_parent):
285 for child in src_parent.childNodes:
286 if child.nodeName == '#text':
287 continue
288 node = self.__get_unique_node(tgt_parent, child.nodeName, child.getAttribute("name"))
289 if not node: # The node is new
290 tgt_parent.appendChild(child.cloneNode(True))
291 else:
292 if child.nodeName == "param":
293 # The child updates an existing parameter, we replace the node
294 tgt_parent.replaceChild(child, node)
295 else:
296 # the node already exists, we recurse 1 more level
297 import_node(node, child)
298
299 if app:
300 pre_process_app_node(src_parent, security_limit, app)
301 import_node(self.dom.documentElement, src_parent)
302
303 def paramsRegisterApp(self, xml, security_limit, app):
304 """Register frontend's specific parameters
305
306 If security_limit is specified and greater than -1, the parameters
307 that have a security level greater than security_limit are skipped.
308 @param xml: XML definition of the parameters to be added
309 @param security_limit: -1 means no security, 0 is the maximum security then the higher the less secure
310 @param app: name of the frontend registering the parameters
311 """
312 if not app:
313 log.warning(_(u"Trying to register frontends parameters with no specified app: aborted"))
314 return
315 if not hasattr(self, "frontends_cache"):
316 self.frontends_cache = []
317 if app in self.frontends_cache:
318 log.debug(_(u"Trying to register twice frontends parameters for %(app)s: aborted" % {"app": app}))
319 return
320 self.frontends_cache.append(app)
321 self.updateParams(xml, security_limit, app)
322 log.debug(u"Frontends parameters registered for %(app)s" % {'app': app})
323
324 def __default_ok(self, value, name, category):
325 #FIXME: will not work with individual parameters
326 self.setParam(name, value, category)
327
328 def __default_ko(self, failure, name, category):
329 log.error(_(u"Can't determine default value for [%(category)s/%(name)s]: %(reason)s") % {'category': category, 'name': name, 'reason': str(failure.value)})
330
331 def setDefault(self, name, category, callback, errback=None):
332 """Set default value of parameter
333
334 'default_cb' attibute of parameter must be set to 'yes'
335 @param name: name of the parameter
336 @param category: category of the parameter
337 @param callback: must return a string with the value (use deferred if needed)
338 @param errback: must manage the error with args failure, name, category
339 """
340 #TODO: send signal param update if value changed
341 #TODO: manage individual paramaters
342 log.debug ("setDefault called for %(category)s/%(name)s" % {"category": category, "name": name})
343 node = self._getParamNode(name, category, '@ALL@')
344 if not node:
345 log.error(_(u"Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category})
346 return
347 if node[1].getAttribute('default_cb') == 'yes':
348 # del node[1].attributes['default_cb'] # default_cb is not used anymore as a flag to know if we have to set the default value,
349 # and we can still use it later e.g. to call a generic setDefault method
350 value = self._getParam(category, name, C.GENERAL)
351 if value is None: # no value set by the user: we have the default value
352 log.debug ("Default value to set, using callback")
353 d = defer.maybeDeferred(callback)
354 d.addCallback(self.__default_ok, name, category)
355 d.addErrback(errback or self.__default_ko, name, category)
356
357 def _getAttr_internal(self, node, attr, value):
358 """Get attribute value.
359
360 /!\ This method would return encrypted password values.
361
362 @param node: XML param node
363 @param attr: name of the attribute to get (e.g.: 'value' or 'type')
364 @param value: user defined value
365 @return: value (can be str, bool, int, list, None)
366 """
367 if attr == 'value':
368 value_to_use = value if value is not None else node.getAttribute(attr) # we use value (user defined) if it exist, else we use node's default value
369 if node.getAttribute('type') == 'bool':
370 return C.bool(value_to_use)
371 if node.getAttribute('type') == 'int':
372 return int(value_to_use)
373 elif node.getAttribute('type') == 'list':
374 if not value_to_use: # no user defined value, take default value from the XML
375 options = [option for option in node.childNodes if option.nodeName == 'option']
376 selected = [option for option in options if option.getAttribute('selected') == 'true']
377 cat, param = node.parentNode.getAttribute('name'), node.getAttribute('name')
378 if len(selected) == 1:
379 value_to_use = selected[0].getAttribute('value')
380 log.info(_("Unset parameter (%(cat)s, %(param)s) of type list will use the default option '%(value)s'") %
381 {'cat': cat, 'param': param, 'value': value_to_use})
382 return value_to_use
383 if len(selected) == 0:
384 log.error(_(u'Parameter (%(cat)s, %(param)s) of type list has no default option!') % {'cat': cat, 'param': param})
385 else:
386 log.error(_(u'Parameter (%(cat)s, %(param)s) of type list has more than one default option!') % {'cat': cat, 'param': param})
387 raise exceptions.DataError
388 elif node.getAttribute('type') == 'jids_list':
389 if value_to_use:
390 jids = value_to_use.split('\t') # FIXME: it's not good to use tabs as separator !
391 else: # no user defined value, take default value from the XML
392 jids = [getText(jid_) for jid_ in node.getElementsByTagName("jid")]
393 to_delete = []
394 for idx, value in enumerate(jids):
395 try:
396 jids[idx] = jid.JID(value)
397 except (RuntimeError, jid.InvalidFormat, AttributeError):
398 log.warning(u"Incorrect jid value found in jids list: [{}]".format(value))
399 to_delete.append(value)
400 for value in to_delete:
401 jids.remove(value)
402 return jids
403 return value_to_use
404 return node.getAttribute(attr)
405
406 def _getAttr(self, node, attr, value):
407 """Get attribute value (synchronous).
408
409 /!\ This method can not be used to retrieve password values.
410 @param node: XML param node
411 @param attr: name of the attribute to get (e.g.: 'value' or 'type')
412 @param value: user defined value
413 @return (unicode, bool, int, list): value to retrieve
414 """
415 if attr == 'value' and node.getAttribute('type') == 'password':
416 raise exceptions.InternalError('To retrieve password values, use _asyncGetAttr instead of _getAttr')
417 return self._getAttr_internal(node, attr, value)
418
419 def _asyncGetAttr(self, node, attr, value, profile=None):
420 """Get attribute value.
421
422 Profile passwords are returned hashed (if not empty),
423 other passwords are returned decrypted (if not empty).
424 @param node: XML param node
425 @param attr: name of the attribute to get (e.g.: 'value' or 'type')
426 @param value: user defined value
427 @param profile: %(doc_profile)s
428 @return (unicode, bool, int, list): Deferred value to retrieve
429 """
430 value = self._getAttr_internal(node, attr, value)
431 if attr != 'value' or node.getAttribute('type') != 'password':
432 return defer.succeed(value)
433 param_cat = node.parentNode.getAttribute('name')
434 param_name = node.getAttribute('name')
435 if ((param_cat, param_name) == C.PROFILE_PASS_PATH) or not value:
436 return defer.succeed(value) # profile password and empty passwords are returned "as is"
437 if not profile:
438 raise exceptions.ProfileNotSetError('The profile is needed to decrypt a password')
439 d = self.host.memory.decryptValue(value, profile)
440
441 def gotPlainPassword(password):
442 if password is None: # empty value means empty password, None means decryption failure
443 raise exceptions.InternalError(_('The stored password could not be decrypted!'))
444 return password
445
446 return d.addCallback(gotPlainPassword)
447
448 def __type_to_string(self, result):
449 """ convert result to string, according to its type """
450 if isinstance(result, bool):
451 return "true" if result else "false"
452 elif isinstance(result, int):
453 return str(result)
454 return result
455
456 def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
457 """ Same as getParamA but for bridge: convert non string value to string """
458 return self.__type_to_string(self.getParamA(name, category, attr, profile_key=profile_key))
459
460 def getParamA(self, name, category, attr="value", use_default=True, profile_key=C.PROF_KEY_NONE):
461 """Helper method to get a specific attribute.
462
463 /!\ This method would return encrypted password values,
464 to get the plain values you have to use _asyncGetParamA.
465 @param name: name of the parameter
466 @param category: category of the parameter
467 @param attr: name of the attribute (default: "value")
468 @parm use_default(bool): if True and attr=='value', return default value if not set
469 else return None if not set
470 @param profile: owner of the param (@ALL@ for everyone)
471 @return: attribute
472 """
473 # FIXME: looks really dirty and buggy, need to be reviewed/refactored
474 # FIXME: security_limit is not managed here !
475 node = self._getParamNode(name, category)
476 if not node:
477 log.error(_(u"Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category})
478 raise exceptions.NotFound
479
480 if attr == 'value' and node[1].getAttribute('type') == 'password':
481 raise exceptions.InternalError('To retrieve password values, use asyncGetParamA instead of getParamA')
482
483 if node[0] == C.GENERAL:
484 value = self._getParam(category, name, C.GENERAL)
485 if value is None and attr=='value' and not use_default:
486 return value
487 return self._getAttr(node[1], attr, value)
488
489 assert node[0] == C.INDIVIDUAL
490
491 profile = self.getProfileName(profile_key)
492 if not profile:
493 log.error(_('Requesting a param for an non-existant profile'))
494 raise exceptions.ProfileUnknownError(profile_key)
495
496 if profile not in self.params:
497 log.error(_('Requesting synchronous param for not connected profile'))
498 raise exceptions.ProfileNotConnected(profile)
499
500 if attr == "value":
501 value = self._getParam(category, name, profile=profile)
502 if value is None and attr=='value' and not use_default:
503 return value
504 return self._getAttr(node[1], attr, value)
505
506 def asyncGetStringParamA(self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
507 d = self.asyncGetParamA(name, category, attr, security_limit, profile_key)
508 d.addCallback(self.__type_to_string)
509 return d
510
511 def asyncGetParamA(self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
512 """Helper method to get a specific attribute.
513
514 @param name: name of the parameter
515 @param category: category of the parameter
516 @param attr: name of the attribute (default: "value")
517 @param profile: owner of the param (@ALL@ for everyone)
518 @return (defer.Deferred): parameter value, with corresponding type (bool, int, list, etc)
519 """
520 node = self._getParamNode(name, category)
521 if not node:
522 log.error(_(u"Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category})
523 raise ValueError("Requested param doesn't exist")
524
525 if not self.checkSecurityLimit(node[1], security_limit):
526 log.warning(_(u"Trying to get parameter '%(param)s' in category '%(cat)s' without authorization!!!"
527 % {'param': name, 'cat': category}))
528 raise exceptions.PermissionError
529
530 if node[0] == C.GENERAL:
531 value = self._getParam(category, name, C.GENERAL)
532 return self._asyncGetAttr(node[1], attr, value)
533
534 assert node[0] == C.INDIVIDUAL
535
536 profile = self.getProfileName(profile_key)
537 if not profile:
538 raise exceptions.InternalError(_('Requesting a param for a non-existant profile'))
539
540 if attr != "value":
541 return defer.succeed(node[1].getAttribute(attr))
542 try:
543 value = self._getParam(category, name, profile=profile)
544 return self._asyncGetAttr(node[1], attr, value, profile)
545 except exceptions.ProfileNotInCacheError:
546 #We have to ask data to the storage manager
547 d = self.storage.getIndParam(category, name, profile)
548 return d.addCallback(lambda value: self._asyncGetAttr(node[1], attr, value, profile))
549
550 def asyncGetParamsValuesFromCategory(self, category, security_limit, profile_key):
551 """Get all parameters "attribute" for a category
552
553 @param category(unicode): the desired category
554 @param security_limit(int): NO_SECURITY_LIMIT (-1) to return all the params.
555 Otherwise sole the params which have a security level defined *and*
556 lower or equal to the specified value are returned.
557 @param profile_key: %(doc_profile_key)s
558 @return (dict): key: param name, value: param value (converted to string if needed)
559 """
560 #TODO: manage category of general type (without existant profile)
561 profile = self.getProfileName(profile_key)
562 if not profile:
563 log.error(_("Asking params for inexistant profile"))
564 return ""
565
566 def setValue(value, ret, name):
567 ret[name] = value
568
569 def returnCategoryXml(prof_xml):
570 ret = {}
571 names_d_list = []
572 for category_node in prof_xml.getElementsByTagName("category"):
573 if category_node.getAttribute("name") == category:
574 for param_node in category_node.getElementsByTagName("param"):
575 name = param_node.getAttribute('name')
576 if not name:
577 log.warning(u"ignoring attribute without name: {}".format(param_node.toxml()))
578 continue
579 d = self.asyncGetStringParamA(name, category, security_limit=security_limit, profile_key=profile)
580 d.addCallback(setValue, ret, name)
581 names_d_list.append(d)
582 break
583
584 prof_xml.unlink()
585 dlist = defer.gatherResults(names_d_list)
586 dlist.addCallback(lambda dummy: ret)
587 return ret
588
589 d = self._constructProfileXml(security_limit, '', profile)
590 return d.addCallback(returnCategoryXml)
591
592 def _getParam(self, category, name, type_=C.INDIVIDUAL, cache=None, profile=C.PROF_KEY_NONE):
593 """Return the param, or None if it doesn't exist
594
595 @param category: param category
596 @param name: param name
597 @param type_: GENERAL or INDIVIDUAL
598 @param cache: temporary cache, to use when profile is not logged
599 @param profile: the profile name (not profile key, i.e. name and not something like @DEFAULT@)
600 @return: param value or None if it doesn't exist
601 """
602 if type_ == C.GENERAL:
603 if (category, name) in self.params_gen:
604 return self.params_gen[(category, name)]
605 return None # This general param has the default value
606 assert type_ == C.INDIVIDUAL
607 if profile == C.PROF_KEY_NONE:
608 raise exceptions.ProfileNotSetError
609 if profile in self.params:
610 cache = self.params[profile] # if profile is in main cache, we use it,
611 # ignoring the temporary cache
612 elif cache is None: # else we use the temporary cache if it exists, or raise an exception
613 raise exceptions.ProfileNotInCacheError
614 if (category, name) not in cache:
615 return None
616 return cache[(category, name)]
617
618 def _constructProfileXml(self, security_limit, app, profile):
619 """Construct xml for asked profile, filling values when needed
620
621 /!\ as noticed in doc, don't forget to unlink the minidom.Document
622 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params.
623 Otherwise sole the params which have a security level defined *and*
624 lower or equal to the specified value are returned.
625 @param app: name of the frontend requesting the parameters, or '' to get all parameters
626 @param profile: profile name (not key !)
627 @return: a deferred that fire a minidom.Document of the profile xml (cf warning above)
628 """
629
630 def checkNode(node):
631 """Check the node against security_limit and app"""
632 return self.checkSecurityLimit(node, security_limit) and self.checkApp(node, app)
633
634 def constructProfile(ignore, profile_cache):
635 # init the result document
636 prof_xml = minidom.parseString('<params/>')
637 cache = {}
638
639 for type_node in self.dom.documentElement.childNodes:
640 if type_node.nodeName != C.GENERAL and type_node.nodeName != C.INDIVIDUAL:
641 continue
642 # we use all params, general and individual
643 for cat_node in type_node.childNodes:
644 if cat_node.nodeName != 'category':
645 continue
646 category = cat_node.getAttribute('name')
647 dest_params = {} # result (merged) params for category
648 if category not in cache:
649 # we make a copy for the new xml
650 cache[category] = dest_cat = cat_node.cloneNode(True)
651 to_remove = []
652 for node in dest_cat.childNodes:
653 if node.nodeName != "param":
654 continue
655 if not checkNode(node):
656 to_remove.append(node)
657 continue
658 dest_params[node.getAttribute('name')] = node
659 for node in to_remove:
660 dest_cat.removeChild(node)
661 new_node = True
662 else:
663 # It's not a new node, we use the previously cloned one
664 dest_cat = cache[category]
665 new_node = False
666 params = cat_node.getElementsByTagName("param")
667
668 for param_node in params:
669 # we have to merge new params (we are parsing individual parameters, we have to add them
670 # to the previously parsed general ones)
671 name = param_node.getAttribute('name')
672 if not checkNode(param_node):
673 continue
674 if name not in dest_params:
675 # this is reached when a previous category exists
676 dest_params[name] = param_node.cloneNode(True)
677 dest_cat.appendChild(dest_params[name])
678
679 profile_value = self._getParam(category,
680 name, type_node.nodeName,
681 cache=profile_cache, profile=profile)
682 if profile_value is not None:
683 # there is a value for this profile, we must change the default
684 if dest_params[name].getAttribute('type') == 'list':
685 for option in dest_params[name].getElementsByTagName("option"):
686 if option.getAttribute('value') == profile_value:
687 option.setAttribute('selected', 'true')
688 else:
689 try:
690 option.removeAttribute('selected')
691 except NotFoundErr:
692 pass
693 elif dest_params[name].getAttribute('type') == 'jids_list':
694 jids = profile_value.split('\t')
695 for jid_elt in dest_params[name].getElementsByTagName("jid"):
696 dest_params[name].removeChild(jid_elt) # remove all default
697 for jid_ in jids: # rebuilt the children with use values
698 try:
699 jid.JID(jid_)
700 except (RuntimeError, jid.InvalidFormat, AttributeError):
701 log.warning(u"Incorrect jid value found in jids list: [{}]".format(jid_))
702 else:
703 jid_elt = prof_xml.createElement('jid')
704 jid_elt.appendChild(prof_xml.createTextNode(jid_))
705 dest_params[name].appendChild(jid_elt)
706 else:
707 dest_params[name].setAttribute('value', profile_value)
708 if new_node:
709 prof_xml.documentElement.appendChild(dest_cat)
710
711 to_remove = []
712 for cat_node in prof_xml.documentElement.childNodes:
713 # we remove empty categories
714 if cat_node.getElementsByTagName("param").length == 0:
715 to_remove.append(cat_node)
716 for node in to_remove:
717 prof_xml.documentElement.removeChild(node)
718 return prof_xml
719
720 if profile in self.params:
721 d = defer.succeed(None)
722 profile_cache = self.params[profile]
723 else:
724 #profile is not in cache, we load values in a short time cache
725 profile_cache = {}
726 d = self.loadIndParams(profile, profile_cache)
727
728 return d.addCallback(constructProfile, profile_cache)
729
730 def getParamsUI(self, security_limit, app, profile_key):
731 """
732 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params.
733 Otherwise sole the params which have a security level defined *and*
734 lower or equal to the specified value are returned.
735 @param app: name of the frontend requesting the parameters, or '' to get all parameters
736 @param profile_key: Profile key which can be either a magic (eg: @DEFAULT@) or the name of an existing profile.
737 @return: a SàT XMLUI for parameters
738 """
739 profile = self.getProfileName(profile_key)
740 if not profile:
741 log.error(_("Asking params for inexistant profile"))
742 return ""
743 d = self.getParams(security_limit, app, profile)
744 return d.addCallback(lambda param_xml: paramsXML2XMLUI(param_xml))
745
746 def getParams(self, security_limit, app, profile_key):
747 """Construct xml for asked profile, take params xml as skeleton
748
749 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params.
750 Otherwise sole the params which have a security level defined *and*
751 lower or equal to the specified value are returned.
752 @param app: name of the frontend requesting the parameters, or '' to get all parameters
753 @param profile_key: Profile key which can be either a magic (eg: @DEFAULT@) or the name of an existing profile.
754 @return: XML of parameters
755 """
756 profile = self.getProfileName(profile_key)
757 if not profile:
758 log.error(_("Asking params for inexistant profile"))
759 return defer.succeed("")
760
761 def returnXML(prof_xml):
762 return_xml = prof_xml.toxml()
763 prof_xml.unlink()
764 return '\n'.join((line for line in return_xml.split('\n') if line))
765
766 return self._constructProfileXml(security_limit, app, profile).addCallback(returnXML)
767
768 def _getParamNode(self, name, category, type_="@ALL@"): # FIXME: is type_ useful ?
769 """Return a node from the param_xml
770 @param name: name of the node
771 @param category: category of the node
772 @param type_: keyword for search:
773 @ALL@ search everywhere
774 @GENERAL@ only search in general type
775 @INDIVIDUAL@ only search in individual type
776 @return: a tuple (node type, node) or None if not found"""
777
778 for type_node in self.dom.documentElement.childNodes:
779 if (((type_ == "@ALL@" or type_ == "@GENERAL@") and type_node.nodeName == C.GENERAL)
780 or ((type_ == "@ALL@" or type_ == "@INDIVIDUAL@") and type_node.nodeName == C.INDIVIDUAL)):
781 for node in type_node.getElementsByTagName('category'):
782 if node.getAttribute("name") == category:
783 params = node.getElementsByTagName("param")
784 for param in params:
785 if param.getAttribute("name") == name:
786 return (type_node.nodeName, param)
787 return None
788
789 def getParamsCategories(self):
790 """return the categories availables"""
791 categories = []
792 for cat in self.dom.getElementsByTagName("category"):
793 name = cat.getAttribute("name")
794 if name not in categories:
795 categories.append(cat.getAttribute("name"))
796 return categories
797
798 def setParam(self, name, value, category, security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
799 """Set a parameter, return None if the parameter is not in param xml.
800
801 Parameter of type 'password' that are not the SàT profile password are
802 stored encrypted (if not empty). The profile password is stored hashed
803 (if not empty).
804
805 @param name (str): the parameter name
806 @param value (str): the new value
807 @param category (str): the parameter category
808 @param security_limit (int)
809 @param profile_key (str): %(doc_profile_key)s
810 @return: a deferred None value when everything is done
811 """
812 # FIXME: setParam should accept the right type for value, not only str !
813 if profile_key != C.PROF_KEY_NONE:
814 profile = self.getProfileName(profile_key)
815 if not profile:
816 log.error(_(u'Trying to set parameter for an unknown profile'))
817 raise exceptions.ProfileUnknownError(profile_key)
818
819 node = self._getParamNode(name, category, '@ALL@')
820 if not node:
821 log.error(_(u'Requesting an unknown parameter (%(category)s/%(name)s)')
822 % {'category': category, 'name': name})
823 return defer.succeed(None)
824
825 if not self.checkSecurityLimit(node[1], security_limit):
826 log.warning(_(u"Trying to set parameter '%(param)s' in category '%(cat)s' without authorization!!!"
827 % {'param': name, 'cat': category}))
828 return defer.succeed(None)
829
830 type_ = node[1].getAttribute("type")
831 if type_ == 'int':
832 if not value: # replace with the default value (which might also be '')
833 value = node[1].getAttribute("value")
834 else:
835 try:
836 int(value)
837 except ValueError:
838 log.debug(_(u"Trying to set parameter '%(param)s' in category '%(cat)s' with an non-integer value"
839 % {'param': name, 'cat': category}))
840 return defer.succeed(None)
841 if node[1].hasAttribute("constraint"):
842 constraint = node[1].getAttribute("constraint")
843 try:
844 min_, max_ = [int(limit) for limit in constraint.split(";")]
845 except ValueError:
846 raise exceptions.InternalError("Invalid integer parameter constraint: %s" % constraint)
847 value = str(min(max(int(value), min_), max_))
848
849
850 log.info(_("Setting parameter (%(category)s, %(name)s) = %(value)s") %
851 {'category': category, 'name': name, 'value': value if type_ != 'password' else '********'})
852
853 if node[0] == C.GENERAL:
854 self.params_gen[(category, name)] = value
855 self.storage.setGenParam(category, name, value)
856 for profile in self.storage.getProfilesList():
857 if self.host.memory.isSessionStarted(profile):
858 self.host.bridge.paramUpdate(name, value, category, profile)
859 self.host.trigger.point("paramUpdateTrigger", name, value, category, node[0], profile)
860 return defer.succeed(None)
861
862 assert node[0] == C.INDIVIDUAL
863 assert profile_key != C.PROF_KEY_NONE
864
865 if type_ == "button":
866 log.debug(u"Clicked param button %s" % node.toxml())
867 return defer.succeed(None)
868 elif type_ == "password":
869 try:
870 personal_key = self.host.memory.auth_sessions.profileGetUnique(profile)[C.MEMORY_CRYPTO_KEY]
871 except TypeError:
872 raise exceptions.InternalError(_('Trying to encrypt a password while the personal key is undefined!'))
873 if (category, name) == C.PROFILE_PASS_PATH:
874 # using 'value' as the encryption key to encrypt another encryption key... could be confusing!
875 d = self.host.memory.encryptPersonalData(data_key=C.MEMORY_CRYPTO_KEY,
876 data_value=personal_key,
877 crypto_key=value,
878 profile=profile)
879 d.addCallback(lambda dummy: PasswordHasher.hash(value)) # profile password is hashed (empty value stays empty)
880 elif value: # other non empty passwords are encrypted with the personal key
881 d = BlockCipher.encrypt(personal_key, value)
882 else:
883 d = defer.succeed(value)
884 else:
885 d = defer.succeed(value)
886
887 def gotFinalValue(value):
888 if self.host.memory.isSessionStarted(profile):
889 self.params[profile][(category, name)] = value
890 self.host.bridge.paramUpdate(name, value, category, profile)
891 self.host.trigger.point("paramUpdateTrigger", name, value, category, node[0], profile)
892 return self.storage.setIndParam(category, name, value, profile)
893 else:
894 raise exceptions.ProfileNotConnected
895
896 d.addCallback(gotFinalValue)
897 return d
898
899 def _getNodesOfTypes(self, attr_type, node_type="@ALL@"):
900 """Return all the nodes matching the given types.
901
902 TODO: using during the dev but not anymore... remove if not needed
903
904 @param attr_type (str): the attribute type (string, text, password, bool, int, button, list)
905 @param node_type (str): keyword for filtering:
906 @ALL@ search everywhere
907 @GENERAL@ only search in general type
908 @INDIVIDUAL@ only search in individual type
909 @return: dict{tuple: node}: a dict {key, value} where:
910 - key is a couple (attribute category, attribute name)
911 - value is a node
912 """
913 ret = {}
914 for type_node in self.dom.documentElement.childNodes:
915 if (((node_type == "@ALL@" or node_type == "@GENERAL@") and type_node.nodeName == C.GENERAL) or
916 ((node_type == "@ALL@" or node_type == "@INDIVIDUAL@") and type_node.nodeName == C.INDIVIDUAL)):
917 for cat_node in type_node.getElementsByTagName('category'):
918 cat = cat_node.getAttribute('name')
919 params = cat_node.getElementsByTagName("param")
920 for param in params:
921 if param.getAttribute("type") == attr_type:
922 ret[(cat, param.getAttribute("name"))] = param
923 return ret
924
925 def checkSecurityLimit(self, node, security_limit):
926 """Check the given node against the given security limit.
927 The value NO_SECURITY_LIMIT (-1) means that everything is allowed.
928 @return: True if this node can be accessed with the given security limit.
929 """
930 if security_limit < 0:
931 return True
932 if node.hasAttribute("security"):
933 if int(node.getAttribute("security")) <= security_limit:
934 return True
935 return False
936
937 def checkApp(self, node, app):
938 """Check the given node against the given app.
939 @param node: parameter node
940 @param app: name of the frontend requesting the parameters, or '' to get all parameters
941 @return: True if this node concerns the given app.
942 """
943 if not app or not node.hasAttribute("app"):
944 return True
945 return node.getAttribute("app") == app