comparison 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
comparison
equal deleted inserted replaced
3122:4486d72658b9 3123:130f9cb6e0ab
29 from twisted.internet import defer 29 from twisted.internet import defer
30 from twisted.python.failure import Failure 30 from twisted.python.failure import Failure
31 from twisted.words.xish import domish 31 from twisted.words.xish import domish
32 from twisted.words.protocols.jabber import jid 32 from twisted.words.protocols.jabber import jid
33 from sat.tools.xml_tools import paramsXML2XMLUI, getText 33 from sat.tools.xml_tools import paramsXML2XMLUI, getText
34 from sat.tools.common import data_format
34 from xml.sax.saxutils import quoteattr 35 from xml.sax.saxutils import quoteattr
35 36
36 # TODO: params should be rewritten using Twisted directly instead of minidom 37 # TODO: params should be rewritten using Twisted directly instead of minidom
37 # general params should be linked to sat.conf and kept synchronised 38 # general params should be linked to sat.conf and kept synchronised
38 # this need an overall simplification to make maintenance easier 39 # this need an overall simplification to make maintenance easier
551 self, name, category, attr="value", use_default=True, profile_key=C.PROF_KEY_NONE 552 self, name, category, attr="value", use_default=True, profile_key=C.PROF_KEY_NONE
552 ): 553 ):
553 """Helper method to get a specific attribute. 554 """Helper method to get a specific attribute.
554 555
555 /!\ This method would return encrypted password values, 556 /!\ This method would return encrypted password values,
556 to get the plain values you have to use _asyncGetParamA. 557 to get the plain values you have to use asyncGetParamA.
557 @param name: name of the parameter 558 @param name: name of the parameter
558 @param category: category of the parameter 559 @param category: category of the parameter
559 @param attr: name of the attribute (default: "value") 560 @param attr: name of the attribute (default: "value")
560 @parm use_default(bool): if True and attr=='value', return default value if not set 561 @parm use_default(bool): if True and attr=='value', return default value if not set
561 else return None if not set 562 else return None if not set
600 value = self._getParam(category, name, profile=profile) 601 value = self._getParam(category, name, profile=profile)
601 if value is None and attr == "value" and not use_default: 602 if value is None and attr == "value" and not use_default:
602 return value 603 return value
603 return self._getAttr(node[1], attr, value) 604 return self._getAttr(node[1], attr, value)
604 605
605 def asyncGetStringParamA( 606 async def asyncGetStringParamA(
606 self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT, 607 self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT,
607 profile_key=C.PROF_KEY_NONE): 608 profile=C.PROF_KEY_NONE):
608 d = self.asyncGetParamA(name, category, attr, security_limit, profile_key) 609 value = await self.asyncGetParamA(
609 d.addCallback(self._type_to_str) 610 name, category, attr, security_limit, profile_key=profile)
610 return d 611 return self._type_to_str(value)
611 612
612 def asyncGetParamA( 613 def asyncGetParamA(
613 self, 614 self,
614 name, 615 name,
615 category, 616 category,
666 d = self.storage.getIndParam(category, name, profile) 667 d = self.storage.getIndParam(category, name, profile)
667 return d.addCallback( 668 return d.addCallback(
668 lambda value: self._asyncGetAttr(node[1], attr, value, profile) 669 lambda value: self._asyncGetAttr(node[1], attr, value, profile)
669 ) 670 )
670 671
671 def asyncGetParamsValuesFromCategory(self, category, security_limit, profile_key): 672 def _getParamsValuesFromCategory(
673 self, category, security_limit, app, extra_s, profile_key):
674 client = self.host.getClient(profile_key)
675 extra = data_format.deserialise(extra_s)
676 return defer.ensureDeferred(self.getParamsValuesFromCategory(
677 client, category, security_limit, app, extra))
678
679 async def getParamsValuesFromCategory(
680 self, client, category, security_limit, app='', extra=None):
672 """Get all parameters "attribute" for a category 681 """Get all parameters "attribute" for a category
673 682
674 @param category(unicode): the desired category 683 @param category(unicode): the desired category
675 @param security_limit(int): NO_SECURITY_LIMIT (-1) to return all the params. 684 @param security_limit(int): NO_SECURITY_LIMIT (-1) to return all the params.
676 Otherwise sole the params which have a security level defined *and* 685 Otherwise sole the params which have a security level defined *and*
677 lower or equal to the specified value are returned. 686 lower or equal to the specified value are returned.
678 @param profile_key: %(doc_profile_key)s 687 @param app(str): see [getParams]
688 @param extra(dict): see [getParams]
679 @return (dict): key: param name, value: param value (converted to string if needed) 689 @return (dict): key: param name, value: param value (converted to string if needed)
680 """ 690 """
681 # TODO: manage category of general type (without existant profile) 691 # TODO: manage category of general type (without existant profile)
682 profile = self.getProfileName(profile_key) 692 if extra is None:
683 if not profile: 693 extra = {}
684 log.error(_("Asking params for inexistant profile")) 694 prof_xml = await self._constructProfileXml(client, security_limit, app, extra)
685 return "" 695 ret = {}
686 696 for category_node in prof_xml.getElementsByTagName("category"):
687 def setValue(value, ret, name): 697 if category_node.getAttribute("name") == category:
688 ret[name] = value 698 for param_node in category_node.getElementsByTagName("param"):
689 699 name = param_node.getAttribute("name")
690 def returnCategoryXml(prof_xml): 700 if not name:
691 ret = {} 701 log.warning(
692 names_d_list = [] 702 "ignoring attribute without name: {}".format(
693 for category_node in prof_xml.getElementsByTagName("category"): 703 param_node.toxml()
694 if category_node.getAttribute("name") == category:
695 for param_node in category_node.getElementsByTagName("param"):
696 name = param_node.getAttribute("name")
697 if not name:
698 log.warning(
699 "ignoring attribute without name: {}".format(
700 param_node.toxml()
701 )
702 ) 704 )
703 continue
704 d = self.asyncGetStringParamA(
705 name,
706 category,
707 security_limit=security_limit,
708 profile_key=profile,
709 ) 705 )
710 d.addCallback(setValue, ret, name) 706 continue
711 names_d_list.append(d) 707 value = await self.asyncGetStringParamA(
712 break 708 name, category, security_limit=security_limit,
713 709 profile=client.profile)
714 prof_xml.unlink() 710
715 dlist = defer.gatherResults(names_d_list) 711 ret[name] = value
716 dlist.addCallback(lambda __: ret) 712 break
717 return ret 713
718 714 prof_xml.unlink()
719 d = self._constructProfileXml(security_limit, "", profile) 715 return ret
720 return d.addCallback(returnCategoryXml)
721 716
722 def _getParam( 717 def _getParam(
723 self, category, name, type_=C.INDIVIDUAL, cache=None, profile=C.PROF_KEY_NONE 718 self, category, name, type_=C.INDIVIDUAL, cache=None, profile=C.PROF_KEY_NONE
724 ): 719 ):
725 """Return the param, or None if it doesn't exist 720 """Return the param, or None if it doesn't exist
747 raise exceptions.ProfileNotInCacheError 742 raise exceptions.ProfileNotInCacheError
748 if (category, name) not in cache: 743 if (category, name) not in cache:
749 return None 744 return None
750 return cache[(category, name)] 745 return cache[(category, name)]
751 746
752 def _constructProfileXml(self, security_limit, app, profile): 747 async def _constructProfileXml(self, client, security_limit, app, extra):
753 """Construct xml for asked profile, filling values when needed 748 """Construct xml for asked profile, filling values when needed
754 749
755 /!\ as noticed in doc, don't forget to unlink the minidom.Document 750 /!\ as noticed in doc, don't forget to unlink the minidom.Document
756 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params. 751 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params.
757 Otherwise sole the params which have a security level defined *and* 752 Otherwise sole the params which have a security level defined *and*
758 lower or equal to the specified value are returned. 753 lower or equal to the specified value are returned.
759 @param app: name of the frontend requesting the parameters, or '' to get all parameters 754 @param app: name of the frontend requesting the parameters, or '' to get all parameters
760 @param profile: profile name (not key !) 755 @param profile: profile name (not key !)
761 @return: a deferred that fire a minidom.Document of the profile xml (cf warning above) 756 @return: a deferred that fire a minidom.Document of the profile xml (cf warning above)
762 """ 757 """
758 profile = client.profile
763 759
764 def checkNode(node): 760 def checkNode(node):
765 """Check the node against security_limit and app""" 761 """Check the node against security_limit, app and extra"""
766 return self.checkSecurityLimit(node, security_limit) and self.checkApp( 762 return (self.checkSecurityLimit(node, security_limit)
767 node, app 763 and self.checkApp(node, app)
768 ) 764 and self.checkExtra(node, extra))
769
770 def constructProfile(ignore, profile_cache):
771 # init the result document
772 prof_xml = minidom.parseString("<params/>")
773 cache = {}
774
775 for type_node in self.dom.documentElement.childNodes:
776 if type_node.nodeName != C.GENERAL and type_node.nodeName != C.INDIVIDUAL:
777 continue
778 # we use all params, general and individual
779 for cat_node in type_node.childNodes:
780 if cat_node.nodeName != "category":
781 continue
782 category = cat_node.getAttribute("name")
783 dest_params = {} # result (merged) params for category
784 if category not in cache:
785 # we make a copy for the new xml
786 cache[category] = dest_cat = cat_node.cloneNode(True)
787 to_remove = []
788 for node in dest_cat.childNodes:
789 if node.nodeName != "param":
790 continue
791 if not checkNode(node):
792 to_remove.append(node)
793 continue
794 dest_params[node.getAttribute("name")] = node
795 for node in to_remove:
796 dest_cat.removeChild(node)
797 new_node = True
798 else:
799 # It's not a new node, we use the previously cloned one
800 dest_cat = cache[category]
801 new_node = False
802 params = cat_node.getElementsByTagName("param")
803
804 for param_node in params:
805 # we have to merge new params (we are parsing individual parameters, we have to add them
806 # to the previously parsed general ones)
807 name = param_node.getAttribute("name")
808 if not checkNode(param_node):
809 continue
810 if name not in dest_params:
811 # this is reached when a previous category exists
812 dest_params[name] = param_node.cloneNode(True)
813 dest_cat.appendChild(dest_params[name])
814
815 profile_value = self._getParam(
816 category,
817 name,
818 type_node.nodeName,
819 cache=profile_cache,
820 profile=profile,
821 )
822 if profile_value is not None:
823 # there is a value for this profile, we must change the default
824 if dest_params[name].getAttribute("type") == "list":
825 for option in dest_params[name].getElementsByTagName(
826 "option"
827 ):
828 if option.getAttribute("value") == profile_value:
829 option.setAttribute("selected", "true")
830 else:
831 try:
832 option.removeAttribute("selected")
833 except NotFoundErr:
834 pass
835 elif dest_params[name].getAttribute("type") == "jids_list":
836 jids = profile_value.split("\t")
837 for jid_elt in dest_params[name].getElementsByTagName(
838 "jid"
839 ):
840 dest_params[name].removeChild(
841 jid_elt
842 ) # remove all default
843 for jid_ in jids: # rebuilt the children with use values
844 try:
845 jid.JID(jid_)
846 except (
847 RuntimeError,
848 jid.InvalidFormat,
849 AttributeError,
850 ):
851 log.warning(
852 "Incorrect jid value found in jids list: [{}]".format(
853 jid_
854 )
855 )
856 else:
857 jid_elt = prof_xml.createElement("jid")
858 jid_elt.appendChild(prof_xml.createTextNode(jid_))
859 dest_params[name].appendChild(jid_elt)
860 else:
861 dest_params[name].setAttribute("value", profile_value)
862 if new_node:
863 prof_xml.documentElement.appendChild(dest_cat)
864
865 to_remove = []
866 for cat_node in prof_xml.documentElement.childNodes:
867 # we remove empty categories
868 if cat_node.getElementsByTagName("param").length == 0:
869 to_remove.append(cat_node)
870 for node in to_remove:
871 prof_xml.documentElement.removeChild(node)
872 return prof_xml
873 765
874 if profile in self.params: 766 if profile in self.params:
875 d = defer.succeed(None)
876 profile_cache = self.params[profile] 767 profile_cache = self.params[profile]
877 else: 768 else:
878 # profile is not in cache, we load values in a short time cache 769 # profile is not in cache, we load values in a short time cache
879 profile_cache = {} 770 profile_cache = {}
880 d = self.loadIndParams(profile, profile_cache) 771 await self.loadIndParams(profile, profile_cache)
881 772
882 return d.addCallback(constructProfile, profile_cache) 773 # init the result document
883 774 prof_xml = minidom.parseString("<params/>")
884 def getParamsUI(self, security_limit, app, profile_key): 775 cache = {}
885 """ 776
777 for type_node in self.dom.documentElement.childNodes:
778 if type_node.nodeName != C.GENERAL and type_node.nodeName != C.INDIVIDUAL:
779 continue
780 # we use all params, general and individual
781 for cat_node in type_node.childNodes:
782 if cat_node.nodeName != "category":
783 continue
784 category = cat_node.getAttribute("name")
785 dest_params = {} # result (merged) params for category
786 if category not in cache:
787 # we make a copy for the new xml
788 cache[category] = dest_cat = cat_node.cloneNode(True)
789 to_remove = []
790 for node in dest_cat.childNodes:
791 if node.nodeName != "param":
792 continue
793 if not checkNode(node):
794 to_remove.append(node)
795 continue
796 dest_params[node.getAttribute("name")] = node
797 for node in to_remove:
798 dest_cat.removeChild(node)
799 new_node = True
800 else:
801 # It's not a new node, we use the previously cloned one
802 dest_cat = cache[category]
803 new_node = False
804 params = cat_node.getElementsByTagName("param")
805
806 for param_node in params:
807 # we have to merge new params (we are parsing individual parameters, we have to add them
808 # to the previously parsed general ones)
809 name = param_node.getAttribute("name")
810 if not checkNode(param_node):
811 continue
812 if name not in dest_params:
813 # this is reached when a previous category exists
814 dest_params[name] = param_node.cloneNode(True)
815 dest_cat.appendChild(dest_params[name])
816
817 profile_value = self._getParam(
818 category,
819 name,
820 type_node.nodeName,
821 cache=profile_cache,
822 profile=profile,
823 )
824 if profile_value is not None:
825 # there is a value for this profile, we must change the default
826 if dest_params[name].getAttribute("type") == "list":
827 for option in dest_params[name].getElementsByTagName(
828 "option"
829 ):
830 if option.getAttribute("value") == profile_value:
831 option.setAttribute("selected", "true")
832 else:
833 try:
834 option.removeAttribute("selected")
835 except NotFoundErr:
836 pass
837 elif dest_params[name].getAttribute("type") == "jids_list":
838 jids = profile_value.split("\t")
839 for jid_elt in dest_params[name].getElementsByTagName(
840 "jid"
841 ):
842 dest_params[name].removeChild(
843 jid_elt
844 ) # remove all default
845 for jid_ in jids: # rebuilt the children with use values
846 try:
847 jid.JID(jid_)
848 except (
849 RuntimeError,
850 jid.InvalidFormat,
851 AttributeError,
852 ):
853 log.warning(
854 "Incorrect jid value found in jids list: [{}]".format(
855 jid_
856 )
857 )
858 else:
859 jid_elt = prof_xml.createElement("jid")
860 jid_elt.appendChild(prof_xml.createTextNode(jid_))
861 dest_params[name].appendChild(jid_elt)
862 else:
863 dest_params[name].setAttribute("value", profile_value)
864 if new_node:
865 prof_xml.documentElement.appendChild(dest_cat)
866
867 to_remove = []
868 for cat_node in prof_xml.documentElement.childNodes:
869 # we remove empty categories
870 if cat_node.getElementsByTagName("param").length == 0:
871 to_remove.append(cat_node)
872 for node in to_remove:
873 prof_xml.documentElement.removeChild(node)
874
875 return prof_xml
876
877
878 def _getParamsUI(self, security_limit, app, extra_s, profile_key):
879 client = self.host.getClient(profile_key)
880 extra = data_format.deserialise(extra_s)
881 return defer.ensureDeferred(self.getParamsUI(client, security_limit, app, extra))
882
883 async def getParamsUI(self, client, security_limit, app, extra=None):
884 """Get XMLUI to handle parameters
885
886 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params. 886 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params.
887 Otherwise sole the params which have a security level defined *and* 887 Otherwise sole the params which have a security level defined *and*
888 lower or equal to the specified value are returned. 888 lower or equal to the specified value are returned.
889 @param app: name of the frontend requesting the parameters, or '' to get all parameters 889 @param app: name of the frontend requesting the parameters, or '' to get all parameters
890 @param profile_key: Profile key which can be either a magic (eg: @DEFAULT@) or the name of an existing profile. 890 @param extra (dict, None): extra options. Key can be:
891 @return: a SàT XMLUI for parameters 891 - ignore: list of (category/name) values to remove from parameters
892 """ 892 @return(str): a SàT XMLUI for parameters
893 profile = self.getProfileName(profile_key) 893 """
894 if not profile: 894 param_xml = await self.getParams(client, security_limit, app, extra)
895 log.error(_("Asking params for inexistant profile")) 895 return paramsXML2XMLUI(param_xml)
896 return "" 896
897 d = self.getParams(security_limit, app, profile) 897 async def getParams(self, client, security_limit, app, extra=None):
898 return d.addCallback(lambda param_xml: paramsXML2XMLUI(param_xml))
899
900 def getParams(self, security_limit, app, profile_key):
901 """Construct xml for asked profile, take params xml as skeleton 898 """Construct xml for asked profile, take params xml as skeleton
902 899
903 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params. 900 @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params.
904 Otherwise sole the params which have a security level defined *and* 901 Otherwise sole the params which have a security level defined *and*
905 lower or equal to the specified value are returned. 902 lower or equal to the specified value are returned.
906 @param app: name of the frontend requesting the parameters, or '' to get all parameters 903 @param app: name of the frontend requesting the parameters, or '' to get all parameters
904 @param extra (dict, None): extra options. Key can be:
905 - ignore: list of (category/name) values to remove from parameters
907 @param profile_key: Profile key which can be either a magic (eg: @DEFAULT@) or the name of an existing profile. 906 @param profile_key: Profile key which can be either a magic (eg: @DEFAULT@) or the name of an existing profile.
908 @return: XML of parameters 907 @return: XML of parameters
909 """ 908 """
910 profile = self.getProfileName(profile_key) 909 if extra is None:
911 if not profile: 910 extra = {}
912 log.error(_("Asking params for inexistant profile")) 911 prof_xml = await self._constructProfileXml(client, security_limit, app, extra)
913 return defer.succeed("") 912 return_xml = prof_xml.toxml()
914 913 prof_xml.unlink()
915 def returnXML(prof_xml): 914 return "\n".join((line for line in return_xml.split("\n") if line))
916 return_xml = prof_xml.toxml()
917 prof_xml.unlink()
918 return "\n".join((line for line in return_xml.split("\n") if line))
919
920 return self._constructProfileXml(security_limit, app, profile).addCallback(
921 returnXML
922 )
923 915
924 def _getParamNode(self, name, category, type_="@ALL@"): # FIXME: is type_ useful ? 916 def _getParamNode(self, name, category, type_="@ALL@"): # FIXME: is type_ useful ?
925 """Return a node from the param_xml 917 """Return a node from the param_xml
926 @param name: name of the node 918 @param name: name of the node
927 @param category: category of the node 919 @param category: category of the node
1126 return True 1118 return True
1127 return False 1119 return False
1128 1120
1129 def checkApp(self, node, app): 1121 def checkApp(self, node, app):
1130 """Check the given node against the given app. 1122 """Check the given node against the given app.
1123
1131 @param node: parameter node 1124 @param node: parameter node
1132 @param app: name of the frontend requesting the parameters, or '' to get all parameters 1125 @param app: name of the frontend requesting the parameters, or '' to get all parameters
1133 @return: True if this node concerns the given app. 1126 @return: True if this node concerns the given app.
1134 """ 1127 """
1135 if not app or not node.hasAttribute("app"): 1128 if not app or not node.hasAttribute("app"):
1136 return True 1129 return True
1137 return node.getAttribute("app") == app 1130 return node.getAttribute("app") == app
1131
1132 def checkExtra(self, node, extra):
1133 """Check the given node against the extra filters.
1134
1135 @param node: parameter node
1136 @param app: name of the frontend requesting the parameters, or '' to get all parameters
1137 @return: True if node doesn't match category/name of extra['ignore'] list
1138 """
1139 ignore_list = extra.get('ignore')
1140 if not ignore_list:
1141 return True
1142 category = node.parentNode.getAttribute('name')
1143 name = node.getAttribute('name')
1144 ignore = [category, name] in ignore_list
1145 if ignore:
1146 log.debug(f"Ignoring parameter {category}/{name} as requested")
1147 return False
1148 return True
1138 1149
1139 1150
1140 def makeOptions(options, selected=None): 1151 def makeOptions(options, selected=None):
1141 """Create option XML form dictionary 1152 """Create option XML form dictionary
1142 1153