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