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