comparison src/core/sat_main.py @ 2126:2f264f3df280

core (menus): improvments: - use the new convention for bridge names (getMenus ==> menusGet, etc.) - menu now use canonical path, which is the untranslated path with each element stripped and lowercase, it must be unique by menu type - added menuLaunch method to manually launch a menu like an action, canonical path is used instead of id - added SECURITY_LIMIT_MAX constant
author Goffi <goffi@goffi.org>
date Thu, 26 Jan 2017 20:29:48 +0100
parents 9c861d07b5b6
children aa94f33fd2ad
comparison
equal deleted inserted replaced
2125:ca82c97db195 2126:2f264f3df280
52 class SAT(service.Service): 52 class SAT(service.Service):
53 53
54 def __init__(self): 54 def __init__(self):
55 self._cb_map = {} # map from callback_id to callbacks 55 self._cb_map = {} # map from callback_id to callbacks
56 self._menus = OrderedDict() # dynamic menus. key: callback_id, value: menu data (dictionnary) 56 self._menus = OrderedDict() # dynamic menus. key: callback_id, value: menu data (dictionnary)
57 self._menus_paths = {} # path to id. key: (menu_type, lower case tuple of path), value: menu id
57 self.initialised = defer.Deferred() 58 self.initialised = defer.Deferred()
58 self.profiles = {} 59 self.profiles = {}
59 self.plugins = {} 60 self.plugins = {}
60 61
61 self.memory = Memory(self) 62 self.memory = Memory(self)
110 self.bridge.register_method("isConnected", self.isConnected) 111 self.bridge.register_method("isConnected", self.isConnected)
111 self.bridge.register_method("launchAction", self.launchCallback) 112 self.bridge.register_method("launchAction", self.launchCallback)
112 self.bridge.register_method("actionsGet", self.actionsGet) 113 self.bridge.register_method("actionsGet", self.actionsGet)
113 self.bridge.register_method("progressGet", self._progressGet) 114 self.bridge.register_method("progressGet", self._progressGet)
114 self.bridge.register_method("progressGetAll", self._progressGetAll) 115 self.bridge.register_method("progressGetAll", self._progressGetAll)
115 self.bridge.register_method("getMenus", self.getMenus) 116 self.bridge.register_method("menusGet", self.getMenus)
116 self.bridge.register_method("getMenuHelp", self.getMenuHelp) 117 self.bridge.register_method("menuHelpGet", self.getMenuHelp)
118 self.bridge.register_method("menuLaunch", self._launchMenu)
117 self.bridge.register_method("discoInfos", self.memory.disco._discoInfos) 119 self.bridge.register_method("discoInfos", self.memory.disco._discoInfos)
118 self.bridge.register_method("discoItems", self.memory.disco._discoItems) 120 self.bridge.register_method("discoItems", self.memory.disco._discoItems)
119 self.bridge.register_method("saveParamsTemplate", self.memory.save_xml) 121 self.bridge.register_method("saveParamsTemplate", self.memory.save_xml)
120 self.bridge.register_method("loadParamsTemplate", self.memory.load_xml) 122 self.bridge.register_method("loadParamsTemplate", self.memory.load_xml)
121 self.bridge.register_method("sessionInfosGet", self.getSessionInfos) 123 self.bridge.register_method("sessionInfosGet", self.getSessionInfos)
915 return progress_all 917 return progress_all
916 918
917 def registerCallback(self, callback, *args, **kwargs): 919 def registerCallback(self, callback, *args, **kwargs):
918 """Register a callback. 920 """Register a callback.
919 921
920 Use with_data=True in kwargs if the callback use the optional data dict 922 @param callback(callable): method to call
921 use force_id=id to avoid generated id. Can lead to name conflict, avoid if possible 923 @param kwargs: can contain:
922 use one_shot=True to delete callback once it have been called 924 with_data(bool): True if the callback use the optional data dict
923 @param callback: any callable 925 force_id(unicode): id to avoid generated id. Can lead to name conflict, avoid if possible
926 one_shot(bool): True to delete callback once it have been called
924 @return: id of the registered callback 927 @return: id of the registered callback
925 """ 928 """
926 callback_id = kwargs.pop('force_id', None) 929 callback_id = kwargs.pop('force_id', None)
927 if callback_id is None: 930 if callback_id is None:
928 callback_id = str(uuid4()) 931 callback_id = str(uuid4())
947 log.debug("Removing callback [%s]" % callback_id) 950 log.debug("Removing callback [%s]" % callback_id)
948 del self._cb_map[callback_id] 951 del self._cb_map[callback_id]
949 952
950 def launchCallback(self, callback_id, data=None, profile_key=C.PROF_KEY_NONE): 953 def launchCallback(self, callback_id, data=None, profile_key=C.PROF_KEY_NONE):
951 """Launch a specific callback 954 """Launch a specific callback
955
952 @param callback_id: id of the action (callback) to launch 956 @param callback_id: id of the action (callback) to launch
953 @param data: optional data 957 @param data: optional data
954 @profile_key: %(doc_profile_key)s 958 @profile_key: %(doc_profile_key)s
955 @return: a deferred which fire a dict where key can be: 959 @return: a deferred which fire a dict where key can be:
956 - xmlui: a XMLUI need to be displayed 960 - xmlui: a XMLUI need to be displayed
957 - validated: if present, can be used to launch a callback, it can have the values 961 - validated: if present, can be used to launch a callback, it can have the values
958 - C.BOOL_TRUE 962 - C.BOOL_TRUE
959 - C.BOOL_FALSE 963 - C.BOOL_FALSE
960 """ 964 """
965 # FIXME: security limit need to be checked here
961 try: 966 try:
962 client = self.getClient(profile_key) 967 client = self.getClient(profile_key)
963 except exceptions.NotFound: 968 except exceptions.NotFound:
964 # client is not available yet 969 # client is not available yet
965 profile = self.memory.getProfileName(profile_key) 970 profile = self.memory.getProfileName(profile_key)
966 if not profile: 971 if not profile:
967 raise exceptions.ProfileUnknownError(_('trying to launch action with a non-existant profile')) 972 raise exceptions.ProfileUnknownError(_(u'trying to launch action with a non-existant profile'))
968 else: 973 else:
969 profile = client.profile 974 profile = client.profile
970 # we check if the action is kept, and remove it 975 # we check if the action is kept, and remove it
971 try: 976 try:
972 action_tuple = client.actions[callback_id] 977 action_tuple = client.actions[callback_id]
994 999
995 return defer.maybeDeferred(callback, *args, **kwargs) 1000 return defer.maybeDeferred(callback, *args, **kwargs)
996 1001
997 #Menus management 1002 #Menus management
998 1003
1004 def _getMenuCanonicalPath(self, path):
1005 """give canonical form of path
1006
1007 canonical form is a tuple of the path were every element is stripped and lowercase
1008 @param path(iterable[unicode]): untranslated path to menu
1009 @return (tuple[unicode]): canonical form of path
1010 """
1011 return tuple((p.lower().strip() for p in path))
1012
999 def importMenu(self, path, callback, security_limit=C.NO_SECURITY_LIMIT, help_string="", type_=C.MENU_GLOBAL): 1013 def importMenu(self, path, callback, security_limit=C.NO_SECURITY_LIMIT, help_string="", type_=C.MENU_GLOBAL):
1000 """register a new menu for frontends 1014 """register a new menu for frontends
1001 1015
1002 @param path: path to go to the menu (category/subcategory/.../item), must be an iterable (e.g.: ("File", "Open")) 1016 @param path(iterable[unicode]): path to go to the menu (category/subcategory/.../item) (e.g.: ("File", "Open"))
1003 /!\ use D_() instead of _() for translations (e.g. (D_("File"), D_("Open"))) 1017 /!\ use D_() instead of _() for translations (e.g. (D_("File"), D_("Open")))
1004 @param callback: method to be called when menuitem is selected, callable or a callback id (string) as returned by [registerCallback] 1018 untranslated/lower case path can be used to identity a menu, for this reason it must be unique independently of case.
1005 @param security_limit: %(doc_security_limit)s 1019 @param callback(callable): method to be called when menuitem is selected, callable or a callback id (string) as returned by [registerCallback]
1020 @param security_limit(int): %(doc_security_limit)s
1006 /!\ security_limit MUST be added to data in launchCallback if used #TODO 1021 /!\ security_limit MUST be added to data in launchCallback if used #TODO
1007 @param help_string: string used to indicate what the menu do (can be show as a tooltip). 1022 @param help_string(unicode): string used to indicate what the menu do (can be show as a tooltip).
1008 /!\ use D_() instead of _() for translations 1023 /!\ use D_() instead of _() for translations
1009 @param type: one of: 1024 @param type(unicode): one of:
1010 - C.MENU_GLOBAL: classical menu, can be shown in a menubar on top (e.g. something like File/Open) 1025 - C.MENU_GLOBAL: classical menu, can be shown in a menubar on top (e.g. something like File/Open)
1011 - C.MENU_ROOM: like a global menu, but only shown in multi-user chat 1026 - C.MENU_ROOM: like a global menu, but only shown in multi-user chat
1012 menu_data must contain a "room_jid" data 1027 menu_data must contain a "room_jid" data
1013 - C.MENU_SINGLE: like a global menu, but only shown in one2one chat 1028 - C.MENU_SINGLE: like a global menu, but only shown in one2one chat
1014 menu_data must contain a "jid" data 1029 menu_data must contain a "jid" data
1016 menu_data must contain a "jid" data 1031 menu_data must contain a "jid" data
1017 - C.MENU_ROSTER_JID_CONTEXT: like JID_CONTEXT, but restricted to jids in roster. 1032 - C.MENU_ROSTER_JID_CONTEXT: like JID_CONTEXT, but restricted to jids in roster.
1018 menu_data must contain a "room_jid" data 1033 menu_data must contain a "room_jid" data
1019 - C.MENU_ROSTER_GROUP_CONTEXT: contextual menu, used with group (e.g.: publish microblog, group is already filled) 1034 - C.MENU_ROSTER_GROUP_CONTEXT: contextual menu, used with group (e.g.: publish microblog, group is already filled)
1020 menu_data must contain a "group" data 1035 menu_data must contain a "group" data
1021 @return: menu_id (same as callback_id) 1036 @return (unicode): menu_id (same as callback_id)
1022 """ 1037 """
1023 1038
1024 if callable(callback): 1039 if callable(callback):
1025 callback_id = self.registerCallback(callback, with_data=True) 1040 callback_id = self.registerCallback(callback, with_data=True)
1026 elif isinstance(callback, basestring): 1041 elif isinstance(callback, basestring):
1036 1051
1037 for menu_data in self._menus.itervalues(): 1052 for menu_data in self._menus.itervalues():
1038 if menu_data['path'] == path and menu_data['type'] == type_: 1053 if menu_data['path'] == path and menu_data['type'] == type_:
1039 raise exceptions.ConflictError(_("A menu with the same path and type already exists")) 1054 raise exceptions.ConflictError(_("A menu with the same path and type already exists"))
1040 1055
1041 menu_data = {'path': path, 1056 path_canonical = self._getMenuCanonicalPath(path)
1057 menu_key = (type_, path_canonical)
1058
1059 if menu_key in self._menus_paths:
1060 raise exceptions.ConflictError(u"this menu path is already used: {path} ({menu_key})".format(
1061 path=path_canonical, menu_key=menu_key))
1062
1063 menu_data = {'path': tuple(path),
1064 'path_canonical': path_canonical,
1042 'security_limit': security_limit, 1065 'security_limit': security_limit,
1043 'help_string': help_string, 1066 'help_string': help_string,
1044 'type': type_ 1067 'type': type_
1045 } 1068 }
1046 1069
1047 self._menus[callback_id] = menu_data 1070 self._menus[callback_id] = menu_data
1071 self._menus_paths[menu_key] = callback_id
1048 1072
1049 return callback_id 1073 return callback_id
1050 1074
1051 def getMenus(self, language='', security_limit=C.NO_SECURITY_LIMIT): 1075 def getMenus(self, language='', security_limit=C.NO_SECURITY_LIMIT):
1052 """Return all menus registered 1076 """Return all menus registered
1075 extra = {} # TODO: manage extra data like icon 1099 extra = {} # TODO: manage extra data like icon
1076 ret.append((menu_id, type_, path, path_i18n, extra)) 1100 ret.append((menu_id, type_, path, path_i18n, extra))
1077 1101
1078 return ret 1102 return ret
1079 1103
1104 def _launchMenu(self, menu_type, path, data=None, security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
1105 client = self.getClient(profile_key)
1106 return self.launchMenu(client, menu_type, path, data, security_limit)
1107
1108 def launchMenu(self, client, menu_type, path, data=None, security_limit=C.NO_SECURITY_LIMIT):
1109 """launch action a menu action
1110
1111 @param menu_type(unicode): type of menu to launch
1112 @param path(iterable[unicode]): canonical path of the menu
1113 @params data(dict): menu data
1114 @raise NotFound: this path is not known
1115 """
1116 # FIXME: manage security_limit here
1117 # defaut security limit should be high instead of C.NO_SECURITY_LIMIT
1118 canonical_path = self._getMenuCanonicalPath(path)
1119 menu_key = (menu_type, canonical_path)
1120 try:
1121 callback_id = self._menus_paths[menu_key]
1122 except KeyError:
1123 raise exceptions.NotFound(u"Can't find menu {path} ({menu_type})".format(
1124 path=canonical_path, menu_type=menu_type))
1125 return self.launchCallback(callback_id, data, client.profile)
1126
1080 def getMenuHelp(self, menu_id, language=''): 1127 def getMenuHelp(self, menu_id, language=''):
1081 """return the help string of the menu 1128 """return the help string of the menu
1082 1129
1083 @param menu_id: id of the menu (same as callback_id) 1130 @param menu_id: id of the menu (same as callback_id)
1084 @param language: language used for translation, or empty string for default 1131 @param language: language used for translation, or empty string for default