comparison libervia/backend/memory/params.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/memory/params.py@524856bd7b19
children 0d7bb4df2343
comparison
equal deleted inserted replaced
4070:d10748475025 4071:4b842c1fb686
1 #!/usr/bin/env python3
2
3 # Libervia: an XMPP client
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
15
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 from libervia.backend.core.i18n import _, D_
20
21 from libervia.backend.core import exceptions
22 from libervia.backend.core.constants import Const as C
23 from libervia.backend.memory.crypto import BlockCipher, PasswordHasher
24 from xml.dom import minidom, NotFoundErr
25 from libervia.backend.core.log import getLogger
26
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 libervia.backend.tools.xml_tools import params_xml_2_xmlui, get_text
33 from libervia.backend.tools.common import data_format
34 from xml.sax.saxutils import quoteattr
35
36 # TODO: params should be rewritten using Twisted directly instead of minidom
37 # general params should be linked to sat.conf and kept synchronised
38 # this need an overall simplification to make maintenance easier
39
40
41 def create_jid_elts(jids):
42 """Generator which return <jid/> elements from jids
43
44 @param jids(iterable[id.jID]): jids to use
45 @return (generator[domish.Element]): <jid/> elements
46 """
47 for jid_ in jids:
48 jid_elt = domish.Element((None, "jid"))
49 jid_elt.addContent(jid_.full())
50 yield jid_elt
51
52
53 class Params(object):
54 """This class manage parameters with xml"""
55
56 ### TODO: add desciption in params
57
58 # TODO: when priority is changed, a new presence stanza must be emitted
59 # TODO: int type (Priority should be int instead of string)
60 default_xml = """
61 <params>
62 <general>
63 </general>
64 <individual>
65 <category name="General" label="%(category_general)s">
66 <param name="Password" value="" type="password" />
67 <param name="%(history_param)s" label="%(history_label)s" value="20" constraint="0;100" type="int" security="0" />
68 <param name="%(show_offline_contacts)s" label="%(show_offline_contacts_label)s" value="false" type="bool" security="0" />
69 <param name="%(show_empty_groups)s" label="%(show_empty_groups_label)s" value="true" type="bool" security="0" />
70 </category>
71 <category name="Connection" label="%(category_connection)s">
72 <param name="JabberID" value="name@example.org" type="string" security="10" />
73 <param name="Password" value="" type="password" security="10" />
74 <param name="Priority" value="50" type="int" constraint="-128;127" security="10" />
75 <param name="%(force_server_param)s" value="" type="string" security="50" />
76 <param name="%(force_port_param)s" value="" type="int" constraint="1;65535" security="50" />
77 <param name="autoconnect_backend" label="%(autoconnect_backend_label)s" value="false" type="bool" security="50" />
78 <param name="autoconnect" label="%(autoconnect_label)s" value="true" type="bool" security="50" />
79 <param name="autodisconnect" label="%(autodisconnect_label)s" value="false" type="bool" security="50" />
80 <param name="check_certificate" label="%(check_certificate_label)s" value="true" type="bool" security="4" />
81 </category>
82 </individual>
83 </params>
84 """ % {
85 "category_general": D_("General"),
86 "category_connection": D_("Connection"),
87 "history_param": C.HISTORY_LIMIT,
88 "history_label": D_("Chat history limit"),
89 "show_offline_contacts": C.SHOW_OFFLINE_CONTACTS,
90 "show_offline_contacts_label": D_("Show offline contacts"),
91 "show_empty_groups": C.SHOW_EMPTY_GROUPS,
92 "show_empty_groups_label": D_("Show empty groups"),
93 "force_server_param": C.FORCE_SERVER_PARAM,
94 "force_port_param": C.FORCE_PORT_PARAM,
95 "autoconnect_backend_label": D_("Connect on backend startup"),
96 "autoconnect_label": D_("Connect on frontend startup"),
97 "autodisconnect_label": D_("Disconnect on frontend closure"),
98 "check_certificate_label": D_("Check certificate (don't uncheck if unsure)"),
99 }
100
101 def load_default_params(self):
102 self.dom = minidom.parseString(Params.default_xml.encode("utf-8"))
103
104 def _merge_params(self, source_node, dest_node):
105 """Look for every node in source_node and recursively copy them to dest if they don't exists"""
106
107 def get_nodes_map(children):
108 ret = {}
109 for child in children:
110 if child.nodeType == child.ELEMENT_NODE:
111 ret[(child.tagName, child.getAttribute("name"))] = child
112 return ret
113
114 source_map = get_nodes_map(source_node.childNodes)
115 dest_map = get_nodes_map(dest_node.childNodes)
116 source_set = set(source_map.keys())
117 dest_set = set(dest_map.keys())
118 to_add = source_set.difference(dest_set)
119
120 for node_key in to_add:
121 dest_node.appendChild(source_map[node_key].cloneNode(True))
122
123 to_recurse = source_set - to_add
124 for node_key in to_recurse:
125 self._merge_params(source_map[node_key], dest_map[node_key])
126
127 def load_xml(self, xml_file):
128 """Load parameters template from xml file"""
129 self.dom = minidom.parse(xml_file)
130 default_dom = minidom.parseString(Params.default_xml.encode("utf-8"))
131 self._merge_params(default_dom.documentElement, self.dom.documentElement)
132
133 def load_gen_params(self):
134 """Load general parameters data from storage
135
136 @return: deferred triggered once params are loaded
137 """
138 return self.storage.load_gen_params(self.params_gen)
139
140 def load_ind_params(self, profile, cache=None):
141 """Load individual parameters
142
143 set self.params cache or a temporary cache
144 @param profile: profile to load (*must exist*)
145 @param cache: if not None, will be used to store the value, as a short time cache
146 @return: deferred triggered once params are loaded
147 """
148 if cache is None:
149 self.params[profile] = {}
150 return self.storage.load_ind_params(
151 self.params[profile] if cache is None else cache, profile
152 )
153
154 def purge_profile(self, profile):
155 """Remove cache data of a profile
156
157 @param profile: %(doc_profile)s
158 """
159 try:
160 del self.params[profile]
161 except KeyError:
162 log.error(
163 _("Trying to purge cache of a profile not in memory: [%s]") % profile
164 )
165
166 def save_xml(self, filename):
167 """Save parameters template to xml file"""
168 with open(filename, "wb") as xml_file:
169 xml_file.write(self.dom.toxml("utf-8"))
170
171 def __init__(self, host, storage):
172 log.debug("Parameters init")
173 self.host = host
174 self.storage = storage
175 self.default_profile = None
176 self.params = {}
177 self.params_gen = {}
178
179 def create_profile(self, profile, component):
180 """Create a new profile
181
182 @param profile(unicode): name of the profile
183 @param component(unicode): entry point if profile is a component
184 @param callback: called when the profile actually exists in database and memory
185 @return: a Deferred instance
186 """
187 if self.storage.has_profile(profile):
188 log.info(_("The profile name already exists"))
189 return defer.fail(exceptions.ConflictError())
190 if not self.host.trigger.point("ProfileCreation", profile):
191 return defer.fail(exceptions.CancelError())
192 return self.storage.create_profile(profile, component or None)
193
194 def profile_delete_async(self, profile, force=False):
195 """Delete an existing profile
196
197 @param profile: name of the profile
198 @param force: force the deletion even if the profile is connected.
199 To be used for direct calls only (not through the bridge).
200 @return: a Deferred instance
201 """
202 if not self.storage.has_profile(profile):
203 log.info(_("Trying to delete an unknown profile"))
204 return defer.fail(Failure(exceptions.ProfileUnknownError(profile)))
205 if self.host.is_connected(profile):
206 if force:
207 self.host.disconnect(profile)
208 else:
209 log.info(_("Trying to delete a connected profile"))
210 return defer.fail(Failure(exceptions.ProfileConnected))
211 return self.storage.delete_profile(profile)
212
213 def get_profile_name(self, profile_key, return_profile_keys=False):
214 """return profile according to profile_key
215
216 @param profile_key: profile name or key which can be
217 C.PROF_KEY_ALL for all profiles
218 C.PROF_KEY_DEFAULT for default profile
219 @param return_profile_keys: if True, return unmanaged profile keys (like
220 C.PROF_KEY_ALL). This keys must be managed by the caller
221 @return: requested profile name
222 @raise exceptions.ProfileUnknownError: profile doesn't exists
223 @raise exceptions.ProfileNotSetError: if C.PROF_KEY_NONE is used
224 """
225 if profile_key == "@DEFAULT@":
226 default = self.host.memory.memory_data.get("Profile_default")
227 if not default:
228 log.info(_("No default profile, returning first one"))
229 try:
230 default = self.host.memory.memory_data[
231 "Profile_default"
232 ] = self.storage.get_profiles_list()[0]
233 except IndexError:
234 log.info(_("No profile exist yet"))
235 raise exceptions.ProfileUnknownError(profile_key)
236 return (
237 default
238 ) # FIXME: temporary, must use real default value, and fallback to first one if it doesn't exists
239 elif profile_key == C.PROF_KEY_NONE:
240 raise exceptions.ProfileNotSetError
241 elif return_profile_keys and profile_key in [C.PROF_KEY_ALL]:
242 return profile_key # this value must be managed by the caller
243 if not self.storage.has_profile(profile_key):
244 log.error(_("Trying to access an unknown profile (%s)") % profile_key)
245 raise exceptions.ProfileUnknownError(profile_key)
246 return profile_key
247
248 def __get_unique_node(self, parent, tag, name):
249 """return node with given tag
250
251 @param parent: parent of nodes to check (e.g. documentElement)
252 @param tag: tag to check (e.g. "category")
253 @param name: name to check (e.g. "JID")
254 @return: node if it exist or None
255 """
256 for node in parent.childNodes:
257 if node.nodeName == tag and node.getAttribute("name") == name:
258 # the node already exists
259 return node
260 # the node is new
261 return None
262
263 def update_params(self, xml, security_limit=C.NO_SECURITY_LIMIT, app=""):
264 """import xml in parameters, update if the param already exists
265
266 If security_limit is specified and greater than -1, the parameters
267 that have a security level greater than security_limit are skipped.
268 @param xml: parameters in xml form
269 @param security_limit: -1 means no security, 0 is the maximum security then the higher the less secure
270 @param app: name of the frontend registering the parameters or empty value
271 """
272 # TODO: should word with domish.Element
273 src_parent = minidom.parseString(xml.encode("utf-8")).documentElement
274
275 def pre_process_app_node(src_parent, security_limit, app):
276 """Parameters that are registered from a frontend must be checked"""
277 to_remove = []
278 for type_node in src_parent.childNodes:
279 if type_node.nodeName != C.INDIVIDUAL:
280 to_remove.append(type_node) # accept individual parameters only
281 continue
282 for cat_node in type_node.childNodes:
283 if cat_node.nodeName != "category":
284 to_remove.append(cat_node)
285 continue
286 to_remove_count = (
287 0
288 ) # count the params to be removed from current category
289 for node in cat_node.childNodes:
290 if node.nodeName != "param" or not self.check_security_limit(
291 node, security_limit
292 ):
293 to_remove.append(node)
294 to_remove_count += 1
295 continue
296 node.setAttribute("app", app)
297 if (
298 len(cat_node.childNodes) == to_remove_count
299 ): # remove empty category
300 for __ in range(0, to_remove_count):
301 to_remove.pop()
302 to_remove.append(cat_node)
303 for node in to_remove:
304 node.parentNode.removeChild(node)
305
306 def import_node(tgt_parent, src_parent):
307 for child in src_parent.childNodes:
308 if child.nodeName == "#text":
309 continue
310 node = self.__get_unique_node(
311 tgt_parent, child.nodeName, child.getAttribute("name")
312 )
313 if not node: # The node is new
314 tgt_parent.appendChild(child.cloneNode(True))
315 else:
316 if child.nodeName == "param":
317 # The child updates an existing parameter, we replace the node
318 tgt_parent.replaceChild(child, node)
319 else:
320 # the node already exists, we recurse 1 more level
321 import_node(node, child)
322
323 if app:
324 pre_process_app_node(src_parent, security_limit, app)
325 import_node(self.dom.documentElement, src_parent)
326
327 def params_register_app(self, xml, security_limit, app):
328 """Register frontend's specific parameters
329
330 If security_limit is specified and greater than -1, the parameters
331 that have a security level greater than security_limit are skipped.
332 @param xml: XML definition of the parameters to be added
333 @param security_limit: -1 means no security, 0 is the maximum security then the higher the less secure
334 @param app: name of the frontend registering the parameters
335 """
336 if not app:
337 log.warning(
338 _(
339 "Trying to register frontends parameters with no specified app: aborted"
340 )
341 )
342 return
343 if not hasattr(self, "frontends_cache"):
344 self.frontends_cache = []
345 if app in self.frontends_cache:
346 log.debug(
347 _(
348 "Trying to register twice frontends parameters for %(app)s: aborted"
349 % {"app": app}
350 )
351 )
352 return
353 self.frontends_cache.append(app)
354 self.update_params(xml, security_limit, app)
355 log.debug("Frontends parameters registered for %(app)s" % {"app": app})
356
357 def __default_ok(self, value, name, category):
358 # FIXME: will not work with individual parameters
359 self.param_set(name, value, category)
360
361 def __default_ko(self, failure, name, category):
362 log.error(
363 _("Can't determine default value for [%(category)s/%(name)s]: %(reason)s")
364 % {"category": category, "name": name, "reason": str(failure.value)}
365 )
366
367 def set_default(self, name, category, callback, errback=None):
368 """Set default value of parameter
369
370 'default_cb' attibute of parameter must be set to 'yes'
371 @param name: name of the parameter
372 @param category: category of the parameter
373 @param callback: must return a string with the value (use deferred if needed)
374 @param errback: must manage the error with args failure, name, category
375 """
376 # TODO: send signal param update if value changed
377 # TODO: manage individual paramaters
378 log.debug(
379 "set_default called for %(category)s/%(name)s"
380 % {"category": category, "name": name}
381 )
382 node = self._get_param_node(name, category, "@ALL@")
383 if not node:
384 log.error(
385 _(
386 "Requested param [%(name)s] in category [%(category)s] doesn't exist !"
387 )
388 % {"name": name, "category": category}
389 )
390 return
391 if node[1].getAttribute("default_cb") == "yes":
392 # 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,
393 # and we can still use it later e.g. to call a generic set_default method
394 value = self._get_param(category, name, C.GENERAL)
395 if value is None: # no value set by the user: we have the default value
396 log.debug("Default value to set, using callback")
397 d = defer.maybeDeferred(callback)
398 d.addCallback(self.__default_ok, name, category)
399 d.addErrback(errback or self.__default_ko, name, category)
400
401 def _get_attr_internal(self, node, attr, value):
402 """Get attribute value.
403
404 /!\ This method would return encrypted password values.
405
406 @param node: XML param node
407 @param attr: name of the attribute to get (e.g.: 'value' or 'type')
408 @param value: user defined value
409 @return: value (can be str, bool, int, list, None)
410 """
411 if attr == "value":
412 value_to_use = (
413 value if value is not None else node.getAttribute(attr)
414 ) # we use value (user defined) if it exist, else we use node's default value
415 if node.getAttribute("type") == "bool":
416 return C.bool(value_to_use)
417 if node.getAttribute("type") == "int":
418 return int(value_to_use) if value_to_use else value_to_use
419 elif node.getAttribute("type") == "list":
420 if (
421 not value_to_use
422 ): # no user defined value, take default value from the XML
423 options = [
424 option
425 for option in node.childNodes
426 if option.nodeName == "option"
427 ]
428 selected = [
429 option
430 for option in options
431 if option.getAttribute("selected") == "true"
432 ]
433 cat, param = (
434 node.parentNode.getAttribute("name"),
435 node.getAttribute("name"),
436 )
437 if len(selected) == 1:
438 value_to_use = selected[0].getAttribute("value")
439 log.info(
440 _(
441 "Unset parameter (%(cat)s, %(param)s) of type list will use the default option '%(value)s'"
442 )
443 % {"cat": cat, "param": param, "value": value_to_use}
444 )
445 return value_to_use
446 if len(selected) == 0:
447 log.error(
448 _(
449 "Parameter (%(cat)s, %(param)s) of type list has no default option!"
450 )
451 % {"cat": cat, "param": param}
452 )
453 else:
454 log.error(
455 _(
456 "Parameter (%(cat)s, %(param)s) of type list has more than one default option!"
457 )
458 % {"cat": cat, "param": param}
459 )
460 raise exceptions.DataError
461 elif node.getAttribute("type") == "jids_list":
462 if value_to_use:
463 jids = value_to_use.split(
464 "\t"
465 ) # FIXME: it's not good to use tabs as separator !
466 else: # no user defined value, take default value from the XML
467 jids = [get_text(jid_) for jid_ in node.getElementsByTagName("jid")]
468 to_delete = []
469 for idx, value in enumerate(jids):
470 try:
471 jids[idx] = jid.JID(value)
472 except (RuntimeError, jid.InvalidFormat, AttributeError):
473 log.warning(
474 "Incorrect jid value found in jids list: [{}]".format(value)
475 )
476 to_delete.append(value)
477 for value in to_delete:
478 jids.remove(value)
479 return jids
480 return value_to_use
481 return node.getAttribute(attr)
482
483 def _get_attr(self, node, attr, value):
484 """Get attribute value (synchronous).
485
486 /!\ This method can not be used to retrieve password values.
487 @param node: XML param node
488 @param attr: name of the attribute to get (e.g.: 'value' or 'type')
489 @param value: user defined value
490 @return (unicode, bool, int, list): value to retrieve
491 """
492 if attr == "value" and node.getAttribute("type") == "password":
493 raise exceptions.InternalError(
494 "To retrieve password values, use _async_get_attr instead of _get_attr"
495 )
496 return self._get_attr_internal(node, attr, value)
497
498 def _async_get_attr(self, node, attr, value, profile=None):
499 """Get attribute value.
500
501 Profile passwords are returned hashed (if not empty),
502 other passwords are returned decrypted (if not empty).
503 @param node: XML param node
504 @param attr: name of the attribute to get (e.g.: 'value' or 'type')
505 @param value: user defined value
506 @param profile: %(doc_profile)s
507 @return (unicode, bool, int, list): Deferred value to retrieve
508 """
509 value = self._get_attr_internal(node, attr, value)
510 if attr != "value" or node.getAttribute("type") != "password":
511 return defer.succeed(value)
512 param_cat = node.parentNode.getAttribute("name")
513 param_name = node.getAttribute("name")
514 if ((param_cat, param_name) == C.PROFILE_PASS_PATH) or not value:
515 return defer.succeed(
516 value
517 ) # profile password and empty passwords are returned "as is"
518 if not profile:
519 raise exceptions.ProfileNotSetError(
520 "The profile is needed to decrypt a password"
521 )
522 password = self.host.memory.decrypt_value(value, profile)
523
524 if password is None:
525 raise exceptions.InternalError("password should never be None")
526 return defer.succeed(password)
527
528 def _type_to_str(self, result):
529 """Convert result to string, according to its type """
530 if isinstance(result, bool):
531 return C.bool_const(result)
532 elif isinstance(result, (list, set, tuple)):
533 return ', '.join(self._type_to_str(r) for r in result)
534 else:
535 return str(result)
536
537 def get_string_param_a(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
538 """ Same as param_get_a but for bridge: convert non string value to string """
539 return self._type_to_str(
540 self.param_get_a(name, category, attr, profile_key=profile_key)
541 )
542
543 def param_get_a(
544 self, name, category, attr="value", use_default=True, profile_key=C.PROF_KEY_NONE
545 ):
546 """Helper method to get a specific attribute.
547
548 /!\ This method would return encrypted password values,
549 to get the plain values you have to use param_get_a_async.
550 @param name: name of the parameter
551 @param category: category of the parameter
552 @param attr: name of the attribute (default: "value")
553 @parm use_default(bool): if True and attr=='value', return default value if not set
554 else return None if not set
555 @param profile: owner of the param (@ALL@ for everyone)
556 @return: attribute
557 """
558 # FIXME: looks really dirty and buggy, need to be reviewed/refactored
559 # FIXME: security_limit is not managed here !
560 node = self._get_param_node(name, category)
561 if not node:
562 log.error(
563 _(
564 "Requested param [%(name)s] in category [%(category)s] doesn't exist !"
565 )
566 % {"name": name, "category": category}
567 )
568 raise exceptions.NotFound
569
570 if attr == "value" and node[1].getAttribute("type") == "password":
571 raise exceptions.InternalError(
572 "To retrieve password values, use param_get_a_async instead of param_get_a"
573 )
574
575 if node[0] == C.GENERAL:
576 value = self._get_param(category, name, C.GENERAL)
577 if value is None and attr == "value" and not use_default:
578 return value
579 return self._get_attr(node[1], attr, value)
580
581 assert node[0] == C.INDIVIDUAL
582
583 profile = self.get_profile_name(profile_key)
584 if not profile:
585 log.error(_("Requesting a param for an non-existant profile"))
586 raise exceptions.ProfileUnknownError(profile_key)
587
588 if profile not in self.params:
589 log.error(_("Requesting synchronous param for not connected profile"))
590 raise exceptions.ProfileNotConnected(profile)
591
592 if attr == "value":
593 value = self._get_param(category, name, profile=profile)
594 if value is None and attr == "value" and not use_default:
595 return value
596 return self._get_attr(node[1], attr, value)
597
598 async def async_get_string_param_a(
599 self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT,
600 profile=C.PROF_KEY_NONE):
601 value = await self.param_get_a_async(
602 name, category, attr, security_limit, profile_key=profile)
603 return self._type_to_str(value)
604
605 def param_get_a_async(
606 self,
607 name,
608 category,
609 attr="value",
610 security_limit=C.NO_SECURITY_LIMIT,
611 profile_key=C.PROF_KEY_NONE,
612 ):
613 """Helper method to get a specific attribute.
614
615 @param name: name of the parameter
616 @param category: category of the parameter
617 @param attr: name of the attribute (default: "value")
618 @param profile: owner of the param (@ALL@ for everyone)
619 @return (defer.Deferred): parameter value, with corresponding type (bool, int, list, etc)
620 """
621 node = self._get_param_node(name, category)
622 if not node:
623 log.error(
624 _(
625 "Requested param [%(name)s] in category [%(category)s] doesn't exist !"
626 )
627 % {"name": name, "category": category}
628 )
629 raise ValueError("Requested param doesn't exist")
630
631 if not self.check_security_limit(node[1], security_limit):
632 log.warning(
633 _(
634 "Trying to get parameter '%(param)s' in category '%(cat)s' without authorization!!!"
635 % {"param": name, "cat": category}
636 )
637 )
638 raise exceptions.PermissionError
639
640 if node[0] == C.GENERAL:
641 value = self._get_param(category, name, C.GENERAL)
642 return self._async_get_attr(node[1], attr, value)
643
644 assert node[0] == C.INDIVIDUAL
645
646 profile = self.get_profile_name(profile_key)
647 if not profile:
648 raise exceptions.InternalError(
649 _("Requesting a param for a non-existant profile")
650 )
651
652 if attr != "value":
653 return defer.succeed(node[1].getAttribute(attr))
654 try:
655 value = self._get_param(category, name, profile=profile)
656 return self._async_get_attr(node[1], attr, value, profile)
657 except exceptions.ProfileNotInCacheError:
658 # We have to ask data to the storage manager
659 d = self.storage.get_ind_param(category, name, profile)
660 return d.addCallback(
661 lambda value: self._async_get_attr(node[1], attr, value, profile)
662 )
663
664 def _get_params_values_from_category(
665 self, category, security_limit, app, extra_s, profile_key):
666 client = self.host.get_client(profile_key)
667 extra = data_format.deserialise(extra_s)
668 return defer.ensureDeferred(self.get_params_values_from_category(
669 client, category, security_limit, app, extra))
670
671 async def get_params_values_from_category(
672 self, client, category, security_limit, app='', extra=None):
673 """Get all parameters "attribute" for a category
674
675 @param category(unicode): the desired category
676 @param security_limit(int): NO_SECURITY_LIMIT (-1) to return all the params.
677 Otherwise sole the params which have a security level defined *and*
678 lower or equal to the specified value are returned.
679 @param app(str): see [get_params]
680 @param extra(dict): see [get_params]
681 @return (dict): key: param name, value: param value (converted to string if needed)
682 """
683 # TODO: manage category of general type (without existant profile)
684 if extra is None:
685 extra = {}
686 prof_xml = await self._construct_profile_xml(client, security_limit, app, extra)
687 ret = {}
688 for category_node in prof_xml.getElementsByTagName("category"):
689 if category_node.getAttribute("name") == category:
690 for param_node in category_node.getElementsByTagName("param"):
691 name = param_node.getAttribute("name")
692 if not name:
693 log.warning(
694 "ignoring attribute without name: {}".format(
695 param_node.toxml()
696 )
697 )
698 continue
699 value = await self.async_get_string_param_a(
700 name, category, security_limit=security_limit,
701 profile=client.profile)
702
703 ret[name] = value
704 break
705
706 prof_xml.unlink()
707 return ret
708
709 def _get_param(
710 self, category, name, type_=C.INDIVIDUAL, cache=None, profile=C.PROF_KEY_NONE
711 ):
712 """Return the param, or None if it doesn't exist
713
714 @param category: param category
715 @param name: param name
716 @param type_: GENERAL or INDIVIDUAL
717 @param cache: temporary cache, to use when profile is not logged
718 @param profile: the profile name (not profile key, i.e. name and not something like @DEFAULT@)
719 @return: param value or None if it doesn't exist
720 """
721 if type_ == C.GENERAL:
722 if (category, name) in self.params_gen:
723 return self.params_gen[(category, name)]
724 return None # This general param has the default value
725 assert type_ == C.INDIVIDUAL
726 if profile == C.PROF_KEY_NONE:
727 raise exceptions.ProfileNotSetError
728 if profile in self.params:
729 cache = self.params[profile] # if profile is in main cache, we use it,
730 # ignoring the temporary cache
731 elif (
732 cache is None
733 ): # else we use the temporary cache if it exists, or raise an exception
734 raise exceptions.ProfileNotInCacheError
735 if (category, name) not in cache:
736 return None
737 return cache[(category, name)]
738
739 async def _construct_profile_xml(self, client, security_limit, app, extra):
740 """Construct xml for asked profile, filling values when needed
741
742 /!\ as noticed in doc, don't forget to unlink the minidom.Document
743 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params.
744 Otherwise sole the params which have a security level defined *and*
745 lower or equal to the specified value are returned.
746 @param app: name of the frontend requesting the parameters, or '' to get all parameters
747 @param profile: profile name (not key !)
748 @return: a deferred that fire a minidom.Document of the profile xml (cf warning above)
749 """
750 profile = client.profile
751
752 def check_node(node):
753 """Check the node against security_limit, app and extra"""
754 return (self.check_security_limit(node, security_limit)
755 and self.check_app(node, app)
756 and self.check_extra(node, extra))
757
758 if profile in self.params:
759 profile_cache = self.params[profile]
760 else:
761 # profile is not in cache, we load values in a short time cache
762 profile_cache = {}
763 await self.load_ind_params(profile, profile_cache)
764
765 # init the result document
766 prof_xml = minidom.parseString("<params/>")
767 cache = {}
768
769 for type_node in self.dom.documentElement.childNodes:
770 if type_node.nodeName != C.GENERAL and type_node.nodeName != C.INDIVIDUAL:
771 continue
772 # we use all params, general and individual
773 for cat_node in type_node.childNodes:
774 if cat_node.nodeName != "category":
775 continue
776 category = cat_node.getAttribute("name")
777 dest_params = {} # result (merged) params for category
778 if category not in cache:
779 # we make a copy for the new xml
780 cache[category] = dest_cat = cat_node.cloneNode(True)
781 to_remove = []
782 for node in dest_cat.childNodes:
783 if node.nodeName != "param":
784 continue
785 if not check_node(node):
786 to_remove.append(node)
787 continue
788 dest_params[node.getAttribute("name")] = node
789 for node in to_remove:
790 dest_cat.removeChild(node)
791 new_node = True
792 else:
793 # It's not a new node, we use the previously cloned one
794 dest_cat = cache[category]
795 new_node = False
796 params = cat_node.getElementsByTagName("param")
797
798 for param_node in params:
799 # we have to merge new params (we are parsing individual parameters, we have to add them
800 # to the previously parsed general ones)
801 name = param_node.getAttribute("name")
802 if not check_node(param_node):
803 continue
804 if name not in dest_params:
805 # this is reached when a previous category exists
806 dest_params[name] = param_node.cloneNode(True)
807 dest_cat.appendChild(dest_params[name])
808
809 profile_value = self._get_param(
810 category,
811 name,
812 type_node.nodeName,
813 cache=profile_cache,
814 profile=profile,
815 )
816 if profile_value is not None:
817 # there is a value for this profile, we must change the default
818 if dest_params[name].getAttribute("type") == "list":
819 for option in dest_params[name].getElementsByTagName(
820 "option"
821 ):
822 if option.getAttribute("value") == profile_value:
823 option.setAttribute("selected", "true")
824 else:
825 try:
826 option.removeAttribute("selected")
827 except NotFoundErr:
828 pass
829 elif dest_params[name].getAttribute("type") == "jids_list":
830 jids = profile_value.split("\t")
831 for jid_elt in dest_params[name].getElementsByTagName(
832 "jid"
833 ):
834 dest_params[name].removeChild(
835 jid_elt
836 ) # remove all default
837 for jid_ in jids: # rebuilt the children with use values
838 try:
839 jid.JID(jid_)
840 except (
841 RuntimeError,
842 jid.InvalidFormat,
843 AttributeError,
844 ):
845 log.warning(
846 "Incorrect jid value found in jids list: [{}]".format(
847 jid_
848 )
849 )
850 else:
851 jid_elt = prof_xml.createElement("jid")
852 jid_elt.appendChild(prof_xml.createTextNode(jid_))
853 dest_params[name].appendChild(jid_elt)
854 else:
855 dest_params[name].setAttribute("value", profile_value)
856 if new_node:
857 prof_xml.documentElement.appendChild(dest_cat)
858
859 to_remove = []
860 for cat_node in prof_xml.documentElement.childNodes:
861 # we remove empty categories
862 if cat_node.getElementsByTagName("param").length == 0:
863 to_remove.append(cat_node)
864 for node in to_remove:
865 prof_xml.documentElement.removeChild(node)
866
867 return prof_xml
868
869
870 def _get_params_ui(self, security_limit, app, extra_s, profile_key):
871 client = self.host.get_client(profile_key)
872 extra = data_format.deserialise(extra_s)
873 return defer.ensureDeferred(self.param_ui_get(client, security_limit, app, extra))
874
875 async def param_ui_get(self, client, security_limit, app, extra=None):
876 """Get XMLUI to handle parameters
877
878 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params.
879 Otherwise sole the params which have a security level defined *and*
880 lower or equal to the specified value are returned.
881 @param app: name of the frontend requesting the parameters, or '' to get all parameters
882 @param extra (dict, None): extra options. Key can be:
883 - ignore: list of (category/name) values to remove from parameters
884 @return(str): a SàT XMLUI for parameters
885 """
886 param_xml = await self.get_params(client, security_limit, app, extra)
887 return params_xml_2_xmlui(param_xml)
888
889 async def get_params(self, client, security_limit, app, extra=None):
890 """Construct xml for asked profile, take params xml as skeleton
891
892 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params.
893 Otherwise sole the params which have a security level defined *and*
894 lower or equal to the specified value are returned.
895 @param app: name of the frontend requesting the parameters, or '' to get all parameters
896 @param extra (dict, None): extra options. Key can be:
897 - ignore: list of (category/name) values to remove from parameters
898 @param profile_key: Profile key which can be either a magic (eg: @DEFAULT@) or the name of an existing profile.
899 @return: XML of parameters
900 """
901 if extra is None:
902 extra = {}
903 prof_xml = await self._construct_profile_xml(client, security_limit, app, extra)
904 return_xml = prof_xml.toxml()
905 prof_xml.unlink()
906 return "\n".join((line for line in return_xml.split("\n") if line))
907
908 def _get_param_node(self, name, category, type_="@ALL@"): # FIXME: is type_ useful ?
909 """Return a node from the param_xml
910 @param name: name of the node
911 @param category: category of the node
912 @param type_: keyword for search:
913 @ALL@ search everywhere
914 @GENERAL@ only search in general type
915 @INDIVIDUAL@ only search in individual type
916 @return: a tuple (node type, node) or None if not found"""
917
918 for type_node in self.dom.documentElement.childNodes:
919 if (
920 (type_ == "@ALL@" or type_ == "@GENERAL@")
921 and type_node.nodeName == C.GENERAL
922 ) or (
923 (type_ == "@ALL@" or type_ == "@INDIVIDUAL@")
924 and type_node.nodeName == C.INDIVIDUAL
925 ):
926 for node in type_node.getElementsByTagName("category"):
927 if node.getAttribute("name") == category:
928 params = node.getElementsByTagName("param")
929 for param in params:
930 if param.getAttribute("name") == name:
931 return (type_node.nodeName, param)
932 return None
933
934 def params_categories_get(self):
935 """return the categories availables"""
936 categories = []
937 for cat in self.dom.getElementsByTagName("category"):
938 name = cat.getAttribute("name")
939 if name not in categories:
940 categories.append(cat.getAttribute("name"))
941 return categories
942
943 def param_set(self, name, value, category, security_limit=C.NO_SECURITY_LIMIT,
944 profile_key=C.PROF_KEY_NONE):
945 """Set a parameter, return None if the parameter is not in param xml.
946
947 Parameter of type 'password' that are not the SàT profile password are
948 stored encrypted (if not empty). The profile password is stored hashed
949 (if not empty).
950
951 @param name (str): the parameter name
952 @param value (str): the new value
953 @param category (str): the parameter category
954 @param security_limit (int)
955 @param profile_key (str): %(doc_profile_key)s
956 @return: a deferred None value when everything is done
957 """
958 # FIXME: param_set should accept the right type for value, not only str !
959 if profile_key != C.PROF_KEY_NONE:
960 profile = self.get_profile_name(profile_key)
961 if not profile:
962 log.error(_("Trying to set parameter for an unknown profile"))
963 raise exceptions.ProfileUnknownError(profile_key)
964
965 node = self._get_param_node(name, category, "@ALL@")
966 if not node:
967 log.error(
968 _("Requesting an unknown parameter (%(category)s/%(name)s)")
969 % {"category": category, "name": name}
970 )
971 return defer.succeed(None)
972
973 if not self.check_security_limit(node[1], security_limit):
974 msg = _(
975 "{profile!r} is trying to set parameter {name!r} in category "
976 "{category!r} without authorization!!!").format(
977 profile=repr(profile),
978 name=repr(name),
979 category=repr(category)
980 )
981 log.warning(msg)
982 raise exceptions.PermissionError(msg)
983
984 type_ = node[1].getAttribute("type")
985 if type_ == "int":
986 if not value: # replace with the default value (which might also be '')
987 value = node[1].getAttribute("value")
988 else:
989 try:
990 int(value)
991 except ValueError:
992 log.warning(_(
993 "Trying to set parameter {name} in category {category} with"
994 "an non-integer value"
995 ).format(
996 name=repr(name),
997 category=repr(category)
998 ))
999 return defer.succeed(None)
1000 if node[1].hasAttribute("constraint"):
1001 constraint = node[1].getAttribute("constraint")
1002 try:
1003 min_, max_ = [int(limit) for limit in constraint.split(";")]
1004 except ValueError:
1005 raise exceptions.InternalError(
1006 "Invalid integer parameter constraint: %s" % constraint
1007 )
1008 value = str(min(max(int(value), min_), max_))
1009
1010 log.info(
1011 _("Setting parameter (%(category)s, %(name)s) = %(value)s")
1012 % {
1013 "category": category,
1014 "name": name,
1015 "value": value if type_ != "password" else "********",
1016 }
1017 )
1018
1019 if node[0] == C.GENERAL:
1020 self.params_gen[(category, name)] = value
1021 self.storage.set_gen_param(category, name, value)
1022 for profile in self.storage.get_profiles_list():
1023 if self.host.memory.is_session_started(profile):
1024 self.host.bridge.param_update(name, value, category, profile)
1025 self.host.trigger.point(
1026 "param_update_trigger", name, value, category, node[0], profile
1027 )
1028 return defer.succeed(None)
1029
1030 assert node[0] == C.INDIVIDUAL
1031 assert profile_key != C.PROF_KEY_NONE
1032
1033 if type_ == "button":
1034 log.debug("Clicked param button %s" % node.toxml())
1035 return defer.succeed(None)
1036 elif type_ == "password":
1037 try:
1038 personal_key = self.host.memory.auth_sessions.profile_get_unique(profile)[
1039 C.MEMORY_CRYPTO_KEY
1040 ]
1041 except TypeError:
1042 raise exceptions.InternalError(
1043 _("Trying to encrypt a password while the personal key is undefined!")
1044 )
1045 if (category, name) == C.PROFILE_PASS_PATH:
1046 # using 'value' as the encryption key to encrypt another encryption key... could be confusing!
1047 d = self.host.memory.encrypt_personal_data(
1048 data_key=C.MEMORY_CRYPTO_KEY,
1049 data_value=personal_key,
1050 crypto_key=value,
1051 profile=profile,
1052 )
1053 d.addCallback(
1054 lambda __: PasswordHasher.hash(value)
1055 ) # profile password is hashed (empty value stays empty)
1056 elif value: # other non empty passwords are encrypted with the personal key
1057 d = defer.succeed(BlockCipher.encrypt(personal_key, value))
1058 else:
1059 d = defer.succeed(value)
1060 else:
1061 d = defer.succeed(value)
1062
1063 def got_final_value(value):
1064 if self.host.memory.is_session_started(profile):
1065 self.params[profile][(category, name)] = value
1066 self.host.bridge.param_update(name, value, category, profile)
1067 self.host.trigger.point(
1068 "param_update_trigger", name, value, category, node[0], profile
1069 )
1070 return self.storage.set_ind_param(category, name, value, profile)
1071 else:
1072 raise exceptions.ProfileNotConnected
1073
1074 d.addCallback(got_final_value)
1075 return d
1076
1077 def _get_nodes_of_types(self, attr_type, node_type="@ALL@"):
1078 """Return all the nodes matching the given types.
1079
1080 TODO: using during the dev but not anymore... remove if not needed
1081
1082 @param attr_type (str): the attribute type (string, text, password, bool, int, button, list)
1083 @param node_type (str): keyword for filtering:
1084 @ALL@ search everywhere
1085 @GENERAL@ only search in general type
1086 @INDIVIDUAL@ only search in individual type
1087 @return: dict{tuple: node}: a dict {key, value} where:
1088 - key is a couple (attribute category, attribute name)
1089 - value is a node
1090 """
1091 ret = {}
1092 for type_node in self.dom.documentElement.childNodes:
1093 if (
1094 (node_type == "@ALL@" or node_type == "@GENERAL@")
1095 and type_node.nodeName == C.GENERAL
1096 ) or (
1097 (node_type == "@ALL@" or node_type == "@INDIVIDUAL@")
1098 and type_node.nodeName == C.INDIVIDUAL
1099 ):
1100 for cat_node in type_node.getElementsByTagName("category"):
1101 cat = cat_node.getAttribute("name")
1102 params = cat_node.getElementsByTagName("param")
1103 for param in params:
1104 if param.getAttribute("type") == attr_type:
1105 ret[(cat, param.getAttribute("name"))] = param
1106 return ret
1107
1108 def check_security_limit(self, node, security_limit):
1109 """Check the given node against the given security limit.
1110 The value NO_SECURITY_LIMIT (-1) means that everything is allowed.
1111 @return: True if this node can be accessed with the given security limit.
1112 """
1113 if security_limit < 0:
1114 return True
1115 if node.hasAttribute("security"):
1116 if int(node.getAttribute("security")) <= security_limit:
1117 return True
1118 return False
1119
1120 def check_app(self, node, app):
1121 """Check the given node against the given app.
1122
1123 @param node: parameter node
1124 @param app: name of the frontend requesting the parameters, or '' to get all parameters
1125 @return: True if this node concerns the given app.
1126 """
1127 if not app or not node.hasAttribute("app"):
1128 return True
1129 return node.getAttribute("app") == app
1130
1131 def check_extra(self, node, extra):
1132 """Check the given node against the extra filters.
1133
1134 @param node: parameter node
1135 @param app: name of the frontend requesting the parameters, or '' to get all parameters
1136 @return: True if node doesn't match category/name of extra['ignore'] list
1137 """
1138 ignore_list = extra.get('ignore')
1139 if not ignore_list:
1140 return True
1141 category = node.parentNode.getAttribute('name')
1142 name = node.getAttribute('name')
1143 ignore = [category, name] in ignore_list
1144 if ignore:
1145 log.debug(f"Ignoring parameter {category}/{name} as requested")
1146 return False
1147 return True
1148
1149
1150 def make_options(options, selected=None):
1151 """Create option XML form dictionary
1152
1153 @param options(dict): option's name => option's label map
1154 @param selected(None, str): value of selected option
1155 None to use first value
1156 @return (str): XML to use in parameters
1157 """
1158 str_list = []
1159 if selected is None:
1160 selected = next(iter(options.keys()))
1161 selected_found = False
1162 for value, label in options.items():
1163 if value == selected:
1164 selected = 'selected="true"'
1165 selected_found = True
1166 else:
1167 selected = ''
1168 str_list.append(
1169 f'<option value={quoteattr(value)} label={quoteattr(label)} {selected}/>'
1170 )
1171 if not selected_found:
1172 raise ValueError(f"selected value ({selected}) not found in options")
1173 return '\n'.join(str_list)