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