diff sat/memory/params.py @ 3123:130f9cb6e0ab

core (memory/params): added `extra` argument to filter out params notably in `getParamsUI`: In some case, it may be desirable for a frontend to not expose some parameters to user (e.g. it is the case on Android with the `autoconnect_backend` parameter). An new `extra` parameter has been added to a couple of parameters method for that: it can contain the `ignore` key with a list of [category, name] of parameters to skip.
author Goffi <goffi@goffi.org>
date Sat, 25 Jan 2020 21:08:40 +0100
parents 0c29155ac68b
children 2b0f739f8a46
line wrap: on
line diff
--- a/sat/memory/params.py	Sat Jan 25 21:08:39 2020 +0100
+++ b/sat/memory/params.py	Sat Jan 25 21:08:40 2020 +0100
@@ -31,6 +31,7 @@
 from twisted.words.xish import domish
 from twisted.words.protocols.jabber import jid
 from sat.tools.xml_tools import paramsXML2XMLUI, getText
+from sat.tools.common import data_format
 from xml.sax.saxutils import quoteattr
 
 # TODO: params should be rewritten using Twisted directly instead of minidom
@@ -553,7 +554,7 @@
         """Helper method to get a specific attribute.
 
         /!\ This method would return encrypted password values,
-            to get the plain values you have to use _asyncGetParamA.
+            to get the plain values you have to use asyncGetParamA.
         @param name: name of the parameter
         @param category: category of the parameter
         @param attr: name of the attribute (default: "value")
@@ -602,12 +603,12 @@
                 return value
             return self._getAttr(node[1], attr, value)
 
-    def asyncGetStringParamA(
+    async def asyncGetStringParamA(
         self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT,
-        profile_key=C.PROF_KEY_NONE):
-        d = self.asyncGetParamA(name, category, attr, security_limit, profile_key)
-        d.addCallback(self._type_to_str)
-        return d
+        profile=C.PROF_KEY_NONE):
+        value = await self.asyncGetParamA(
+            name, category, attr, security_limit, profile_key=profile)
+        return self._type_to_str(value)
 
     def asyncGetParamA(
         self,
@@ -668,56 +669,50 @@
                 lambda value: self._asyncGetAttr(node[1], attr, value, profile)
             )
 
-    def asyncGetParamsValuesFromCategory(self, category, security_limit, profile_key):
+    def _getParamsValuesFromCategory(
+        self, category, security_limit, app, extra_s, profile_key):
+        client = self.host.getClient(profile_key)
+        extra = data_format.deserialise(extra_s)
+        return defer.ensureDeferred(self.getParamsValuesFromCategory(
+            client, category, security_limit, app, extra))
+
+    async def getParamsValuesFromCategory(
+        self, client, category, security_limit, app='', extra=None):
         """Get all parameters "attribute" for a category
 
         @param category(unicode): the desired category
         @param security_limit(int): NO_SECURITY_LIMIT (-1) to return all the params.
             Otherwise sole the params which have a security level defined *and*
             lower or equal to the specified value are returned.
-        @param profile_key: %(doc_profile_key)s
+        @param app(str): see [getParams]
+        @param extra(dict): see [getParams]
         @return (dict): key: param name, value: param value (converted to string if needed)
         """
         # TODO: manage category of general type (without existant profile)
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            log.error(_("Asking params for inexistant profile"))
-            return ""
-
-        def setValue(value, ret, name):
-            ret[name] = value
+        if extra is None:
+            extra = {}
+        prof_xml = await self._constructProfileXml(client, security_limit, app, extra)
+        ret = {}
+        for category_node in prof_xml.getElementsByTagName("category"):
+            if category_node.getAttribute("name") == category:
+                for param_node in category_node.getElementsByTagName("param"):
+                    name = param_node.getAttribute("name")
+                    if not name:
+                        log.warning(
+                            "ignoring attribute without name: {}".format(
+                                param_node.toxml()
+                            )
+                        )
+                        continue
+                    value = await self.asyncGetStringParamA(
+                        name, category, security_limit=security_limit,
+                        profile=client.profile)
 
-        def returnCategoryXml(prof_xml):
-            ret = {}
-            names_d_list = []
-            for category_node in prof_xml.getElementsByTagName("category"):
-                if category_node.getAttribute("name") == category:
-                    for param_node in category_node.getElementsByTagName("param"):
-                        name = param_node.getAttribute("name")
-                        if not name:
-                            log.warning(
-                                "ignoring attribute without name: {}".format(
-                                    param_node.toxml()
-                                )
-                            )
-                            continue
-                        d = self.asyncGetStringParamA(
-                            name,
-                            category,
-                            security_limit=security_limit,
-                            profile_key=profile,
-                        )
-                        d.addCallback(setValue, ret, name)
-                        names_d_list.append(d)
-                    break
+                    ret[name] = value
+                break
 
-            prof_xml.unlink()
-            dlist = defer.gatherResults(names_d_list)
-            dlist.addCallback(lambda __: ret)
-            return ret
-
-        d = self._constructProfileXml(security_limit, "", profile)
-        return d.addCallback(returnCategoryXml)
+        prof_xml.unlink()
+        return ret
 
     def _getParam(
         self, category, name, type_=C.INDIVIDUAL, cache=None, profile=C.PROF_KEY_NONE
@@ -749,7 +744,7 @@
             return None
         return cache[(category, name)]
 
-    def _constructProfileXml(self, security_limit, app, profile):
+    async def _constructProfileXml(self, client, security_limit, app, extra):
         """Construct xml for asked profile, filling values when needed
 
         /!\ as noticed in doc, don't forget to unlink the minidom.Document
@@ -760,166 +755,163 @@
         @param profile: profile name (not key !)
         @return: a deferred that fire a minidom.Document of the profile xml (cf warning above)
         """
+        profile = client.profile
 
         def checkNode(node):
-            """Check the node against security_limit and app"""
-            return self.checkSecurityLimit(node, security_limit) and self.checkApp(
-                node, app
-            )
-
-        def constructProfile(ignore, profile_cache):
-            # init the result document
-            prof_xml = minidom.parseString("<params/>")
-            cache = {}
-
-            for type_node in self.dom.documentElement.childNodes:
-                if type_node.nodeName != C.GENERAL and type_node.nodeName != C.INDIVIDUAL:
-                    continue
-                # we use all params, general and individual
-                for cat_node in type_node.childNodes:
-                    if cat_node.nodeName != "category":
-                        continue
-                    category = cat_node.getAttribute("name")
-                    dest_params = {}  # result (merged) params for category
-                    if category not in cache:
-                        # we make a copy for the new xml
-                        cache[category] = dest_cat = cat_node.cloneNode(True)
-                        to_remove = []
-                        for node in dest_cat.childNodes:
-                            if node.nodeName != "param":
-                                continue
-                            if not checkNode(node):
-                                to_remove.append(node)
-                                continue
-                            dest_params[node.getAttribute("name")] = node
-                        for node in to_remove:
-                            dest_cat.removeChild(node)
-                        new_node = True
-                    else:
-                        # It's not a new node, we use the previously cloned one
-                        dest_cat = cache[category]
-                        new_node = False
-                    params = cat_node.getElementsByTagName("param")
-
-                    for param_node in params:
-                        # we have to merge new params (we are parsing individual parameters, we have to add them
-                        # to the previously parsed general ones)
-                        name = param_node.getAttribute("name")
-                        if not checkNode(param_node):
-                            continue
-                        if name not in dest_params:
-                            # this is reached when a previous category exists
-                            dest_params[name] = param_node.cloneNode(True)
-                            dest_cat.appendChild(dest_params[name])
-
-                        profile_value = self._getParam(
-                            category,
-                            name,
-                            type_node.nodeName,
-                            cache=profile_cache,
-                            profile=profile,
-                        )
-                        if profile_value is not None:
-                            # there is a value for this profile, we must change the default
-                            if dest_params[name].getAttribute("type") == "list":
-                                for option in dest_params[name].getElementsByTagName(
-                                    "option"
-                                ):
-                                    if option.getAttribute("value") == profile_value:
-                                        option.setAttribute("selected", "true")
-                                    else:
-                                        try:
-                                            option.removeAttribute("selected")
-                                        except NotFoundErr:
-                                            pass
-                            elif dest_params[name].getAttribute("type") == "jids_list":
-                                jids = profile_value.split("\t")
-                                for jid_elt in dest_params[name].getElementsByTagName(
-                                    "jid"
-                                ):
-                                    dest_params[name].removeChild(
-                                        jid_elt
-                                    )  # remove all default
-                                for jid_ in jids:  # rebuilt the children with use values
-                                    try:
-                                        jid.JID(jid_)
-                                    except (
-                                        RuntimeError,
-                                        jid.InvalidFormat,
-                                        AttributeError,
-                                    ):
-                                        log.warning(
-                                            "Incorrect jid value found in jids list: [{}]".format(
-                                                jid_
-                                            )
-                                        )
-                                    else:
-                                        jid_elt = prof_xml.createElement("jid")
-                                        jid_elt.appendChild(prof_xml.createTextNode(jid_))
-                                        dest_params[name].appendChild(jid_elt)
-                            else:
-                                dest_params[name].setAttribute("value", profile_value)
-                    if new_node:
-                        prof_xml.documentElement.appendChild(dest_cat)
-
-            to_remove = []
-            for cat_node in prof_xml.documentElement.childNodes:
-                # we remove empty categories
-                if cat_node.getElementsByTagName("param").length == 0:
-                    to_remove.append(cat_node)
-            for node in to_remove:
-                prof_xml.documentElement.removeChild(node)
-            return prof_xml
+            """Check the node against security_limit, app and extra"""
+            return (self.checkSecurityLimit(node, security_limit)
+                    and self.checkApp(node, app)
+                    and self.checkExtra(node, extra))
 
         if profile in self.params:
-            d = defer.succeed(None)
             profile_cache = self.params[profile]
         else:
             # profile is not in cache, we load values in a short time cache
             profile_cache = {}
-            d = self.loadIndParams(profile, profile_cache)
+            await self.loadIndParams(profile, profile_cache)
+
+        # init the result document
+        prof_xml = minidom.parseString("<params/>")
+        cache = {}
 
-        return d.addCallback(constructProfile, profile_cache)
+        for type_node in self.dom.documentElement.childNodes:
+            if type_node.nodeName != C.GENERAL and type_node.nodeName != C.INDIVIDUAL:
+                continue
+            # we use all params, general and individual
+            for cat_node in type_node.childNodes:
+                if cat_node.nodeName != "category":
+                    continue
+                category = cat_node.getAttribute("name")
+                dest_params = {}  # result (merged) params for category
+                if category not in cache:
+                    # we make a copy for the new xml
+                    cache[category] = dest_cat = cat_node.cloneNode(True)
+                    to_remove = []
+                    for node in dest_cat.childNodes:
+                        if node.nodeName != "param":
+                            continue
+                        if not checkNode(node):
+                            to_remove.append(node)
+                            continue
+                        dest_params[node.getAttribute("name")] = node
+                    for node in to_remove:
+                        dest_cat.removeChild(node)
+                    new_node = True
+                else:
+                    # It's not a new node, we use the previously cloned one
+                    dest_cat = cache[category]
+                    new_node = False
+                params = cat_node.getElementsByTagName("param")
+
+                for param_node in params:
+                    # we have to merge new params (we are parsing individual parameters, we have to add them
+                    # to the previously parsed general ones)
+                    name = param_node.getAttribute("name")
+                    if not checkNode(param_node):
+                        continue
+                    if name not in dest_params:
+                        # this is reached when a previous category exists
+                        dest_params[name] = param_node.cloneNode(True)
+                        dest_cat.appendChild(dest_params[name])
 
-    def getParamsUI(self, security_limit, app, profile_key):
-        """
+                    profile_value = self._getParam(
+                        category,
+                        name,
+                        type_node.nodeName,
+                        cache=profile_cache,
+                        profile=profile,
+                    )
+                    if profile_value is not None:
+                        # there is a value for this profile, we must change the default
+                        if dest_params[name].getAttribute("type") == "list":
+                            for option in dest_params[name].getElementsByTagName(
+                                "option"
+                            ):
+                                if option.getAttribute("value") == profile_value:
+                                    option.setAttribute("selected", "true")
+                                else:
+                                    try:
+                                        option.removeAttribute("selected")
+                                    except NotFoundErr:
+                                        pass
+                        elif dest_params[name].getAttribute("type") == "jids_list":
+                            jids = profile_value.split("\t")
+                            for jid_elt in dest_params[name].getElementsByTagName(
+                                "jid"
+                            ):
+                                dest_params[name].removeChild(
+                                    jid_elt
+                                )  # remove all default
+                            for jid_ in jids:  # rebuilt the children with use values
+                                try:
+                                    jid.JID(jid_)
+                                except (
+                                    RuntimeError,
+                                    jid.InvalidFormat,
+                                    AttributeError,
+                                ):
+                                    log.warning(
+                                        "Incorrect jid value found in jids list: [{}]".format(
+                                            jid_
+                                        )
+                                    )
+                                else:
+                                    jid_elt = prof_xml.createElement("jid")
+                                    jid_elt.appendChild(prof_xml.createTextNode(jid_))
+                                    dest_params[name].appendChild(jid_elt)
+                        else:
+                            dest_params[name].setAttribute("value", profile_value)
+                if new_node:
+                    prof_xml.documentElement.appendChild(dest_cat)
+
+        to_remove = []
+        for cat_node in prof_xml.documentElement.childNodes:
+            # we remove empty categories
+            if cat_node.getElementsByTagName("param").length == 0:
+                to_remove.append(cat_node)
+        for node in to_remove:
+            prof_xml.documentElement.removeChild(node)
+
+        return prof_xml
+
+
+    def _getParamsUI(self, security_limit, app, extra_s, profile_key):
+        client = self.host.getClient(profile_key)
+        extra = data_format.deserialise(extra_s)
+        return defer.ensureDeferred(self.getParamsUI(client, security_limit, app, extra))
+
+    async def getParamsUI(self, client, security_limit, app, extra=None):
+        """Get XMLUI to handle parameters
+
         @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params.
-        Otherwise sole the params which have a security level defined *and*
-        lower or equal to the specified value are returned.
+            Otherwise sole the params which have a security level defined *and*
+            lower or equal to the specified value are returned.
         @param app: name of the frontend requesting the parameters, or '' to get all parameters
-        @param profile_key: Profile key which can be either a magic (eg: @DEFAULT@) or the name of an existing profile.
-        @return: a SàT XMLUI for parameters
+        @param extra (dict, None): extra options. Key can be:
+            - ignore: list of (category/name) values to remove from parameters
+        @return(str): a SàT XMLUI for parameters
         """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            log.error(_("Asking params for inexistant profile"))
-            return ""
-        d = self.getParams(security_limit, app, profile)
-        return d.addCallback(lambda param_xml: paramsXML2XMLUI(param_xml))
+        param_xml = await self.getParams(client, security_limit, app, extra)
+        return paramsXML2XMLUI(param_xml)
 
-    def getParams(self, security_limit, app, profile_key):
+    async def getParams(self, client, security_limit, app, extra=None):
         """Construct xml for asked profile, take params xml as skeleton
 
         @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params.
-        Otherwise sole the params which have a security level defined *and*
-        lower or equal to the specified value are returned.
+            Otherwise sole the params which have a security level defined *and*
+            lower or equal to the specified value are returned.
         @param app: name of the frontend requesting the parameters, or '' to get all parameters
+        @param extra (dict, None): extra options. Key can be:
+            - ignore: list of (category/name) values to remove from parameters
         @param profile_key: Profile key which can be either a magic (eg: @DEFAULT@) or the name of an existing profile.
         @return: XML of parameters
         """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            log.error(_("Asking params for inexistant profile"))
-            return defer.succeed("")
-
-        def returnXML(prof_xml):
-            return_xml = prof_xml.toxml()
-            prof_xml.unlink()
-            return "\n".join((line for line in return_xml.split("\n") if line))
-
-        return self._constructProfileXml(security_limit, app, profile).addCallback(
-            returnXML
-        )
+        if extra is None:
+            extra = {}
+        prof_xml = await self._constructProfileXml(client, security_limit, app, extra)
+        return_xml = prof_xml.toxml()
+        prof_xml.unlink()
+        return "\n".join((line for line in return_xml.split("\n") if line))
 
     def _getParamNode(self, name, category, type_="@ALL@"):  # FIXME: is type_ useful ?
         """Return a node from the param_xml
@@ -1128,6 +1120,7 @@
 
     def checkApp(self, node, app):
         """Check the given node against the given app.
+
         @param node: parameter node
         @param app: name of the frontend requesting the parameters, or '' to get all parameters
         @return: True if this node concerns the given app.
@@ -1136,6 +1129,24 @@
             return True
         return node.getAttribute("app") == app
 
+    def checkExtra(self, node, extra):
+        """Check the given node against the extra filters.
+
+        @param node: parameter node
+        @param app: name of the frontend requesting the parameters, or '' to get all parameters
+        @return: True if node doesn't match category/name of extra['ignore'] list
+        """
+        ignore_list = extra.get('ignore')
+        if not ignore_list:
+            return True
+        category = node.parentNode.getAttribute('name')
+        name = node.getAttribute('name')
+        ignore = [category, name] in ignore_list
+        if ignore:
+            log.debug(f"Ignoring parameter {category}/{name} as requested")
+            return False
+        return True
+
 
 def makeOptions(options, selected=None):
     """Create option XML form dictionary