From 1ea0dde417d1b96782f8f13e2b9c6616cb5d084b Mon Sep 17 00:00:00 2001 From: Badlop Date: Wed, 5 Mar 2025 18:10:01 +0100 Subject: [PATCH 1/7] ejabberd_admin: Allow using mnesia_list_tables and mnesia_table_change_storage --- src/ejabberd_admin.erl | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 259831d7f..9f2cc84b8 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -598,8 +598,9 @@ get_commands_spec() -> args = [{node, atom}, {table, binary}, {page, integer}], result = {res, any}}, - #ejabberd_commands{name = mnesia_list_tables, tags = [internal, mnesia], + #ejabberd_commands{name = mnesia_list_tables, tags = [mnesia], desc = "List of Mnesia tables", + note = "added in 25.xx", module = ?MODULE, function = mnesia_list_tables, result = {tables, {list, {table, {tuple, [{name, atom}, {storage_type, binary}, @@ -615,8 +616,10 @@ get_commands_spec() -> {value, binary} ]}}}}}, - #ejabberd_commands{name = mnesia_table_change_storage, tags = [internal, mnesia], - desc = "Change storage type of a Mnesia table to: ram_copies, disc_copies, or disc_only_copies.", + #ejabberd_commands{name = mnesia_table_change_storage, tags = [mnesia], + desc = "Change storage type of a Mnesia table", + note = "added in 25.xx", + longdesc = "Storage type can be: `ram_copies`, `disc_copies`, `disc_only_copies`, `remote_copy`.", module = ?MODULE, function = mnesia_table_change_storage, args = [{table, binary}, {storage_type, binary}], result = {res, restuple}}, @@ -1281,13 +1284,12 @@ is_my_host(Host) -> %% @format-begin -%% mnesia:del_table_copy(Table, Node); -%% mnesia:change_table_copy_type(Table, Node, Type); - mnesia_table_change_storage(STable, SType) -> Table = binary_to_existing_atom(STable, latin1), Type = case SType of + <<"remote_copy">> -> + remote_copy; <<"ram_copies">> -> ram_copies; <<"disc_copies">> -> @@ -1297,7 +1299,24 @@ mnesia_table_change_storage(STable, SType) -> _ -> false end, - mnesia:add_table_copy(Table, node(), Type). + Node = node(), + Result = + case Type of + false -> + "Nothing to do"; + remote_copy -> + mnesia:del_table_copy(Table, Node), + "Deleted table copy"; + _ -> + case mnesia:add_table_copy(Table, Node, Type) of + {aborted, _} -> + mnesia:change_table_copy_type(Table, Node, Type), + "Changed table copy type"; + _ -> + "Added table copy" + end + end, + {ok, Result}. mnesia_table_clear(STable) -> Table = binary_to_existing_atom(STable, latin1), From 6151674e64a595b66d98bcd83582b9ae1f547a75 Mon Sep 17 00:00:00 2001 From: Badlop Date: Wed, 5 Mar 2025 21:10:19 +0100 Subject: [PATCH 2/7] ejabberd_commands: Show warning when registering command with an existing name In ejabberd modules, only register/unregister commands if module is not running for any other vhost. --- src/ejabberd_commands.erl | 35 +++++++++++++++++++++++++++++++---- src/mod_admin_extra.erl | 11 +++-------- src/mod_admin_update_sql.erl | 8 ++++---- src/mod_fail2ban.erl | 9 ++------- src/mod_mam.erl | 9 ++------- src/mod_muc_admin.erl | 11 +++-------- src/mod_private.erl | 9 ++------- src/mod_pubsub.erl | 9 ++------- src/mod_push.erl | 9 ++------- 9 files changed, 51 insertions(+), 59 deletions(-) diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 5028f05fc..2318564ba 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -42,7 +42,9 @@ get_tags_commands/1, register_commands/1, register_commands/2, + register_commands/3, unregister_commands/1, + unregister_commands/3, get_commands_spec/0, get_commands_definition/0, get_commands_definition/1, @@ -142,19 +144,34 @@ code_change(_OldVsn, State, _Extra) -> register_commands(Commands) -> register_commands(unknown, Commands). +-spec register_commands(atom(), [ejabberd_commands()]) -> ok. + register_commands(Definer, Commands) -> + ExistingCommands = list_commands(), lists:foreach( fun(Command) -> - %% XXX check if command exists - mnesia:dirty_write(register_command_prepare(Command, Definer)) - %% ?DEBUG("This command is already defined:~n~p", [Command]) + Name = Command#ejabberd_commands.name, + case lists:keyfind(Name, 1, ExistingCommands) of + false -> + mnesia:dirty_write(register_command_prepare(Command, Definer)); + _ -> + OtherCommandDef = get_command_definition(Name), + ?CRITICAL_MSG("Error trying to define a command: another one already exists with the same name:~n Existing: ~p~n New: ~p", [OtherCommandDef, Command]) + end end, Commands), ejabberd_access_permissions:invalidate(), ok. +-spec register_commands(binary(), atom(), [ejabberd_commands()]) -> ok. - +register_commands(Host, Definer, Commands) -> + case gen_mod:is_loaded_elsewhere(Host, Definer) of + false -> + register_commands(Definer, Commands); + true -> + ok + end. register_command_prepare(Command, Definer) -> Tags1 = Command#ejabberd_commands.tags, @@ -175,6 +192,16 @@ unregister_commands(Commands) -> Commands), ejabberd_access_permissions:invalidate(). +-spec unregister_commands(binary(), atom(), [ejabberd_commands()]) -> ok. + +unregister_commands(Host, Definer, Commands) -> + case gen_mod:is_loaded_elsewhere(Host, Definer) of + false -> + unregister_commands(Commands); + true -> + ok + end. + -spec list_commands() -> [{atom(), [aterm()], string()}]. list_commands() -> diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index 11b0fbb95..d461edc89 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -104,8 +104,8 @@ %%% gen_mod %%% -start(_Host, _Opts) -> - ejabberd_commands:register_commands(?MODULE, get_commands_spec()), +start(Host, _Opts) -> + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), {ok, [{hook, webadmin_menu_main, web_menu_main, 50, global}, {hook, webadmin_page_main, web_page_main, 50, global}, {hook, webadmin_menu_host, web_menu_host, 50}, @@ -118,12 +118,7 @@ start(_Host, _Opts) -> {hook, webadmin_page_node, web_page_node, 50, global}]}. stop(Host) -> - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end. + ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()). reload(_Host, _NewOpts, _OldOpts) -> ok. diff --git a/src/mod_admin_update_sql.erl b/src/mod_admin_update_sql.erl index 0c3f9095f..c5b4ab865 100644 --- a/src/mod_admin_update_sql.erl +++ b/src/mod_admin_update_sql.erl @@ -47,11 +47,11 @@ %%% gen_mod %%% -start(_Host, _Opts) -> - ejabberd_commands:register_commands(?MODULE, get_commands_spec()). +start(Host, _Opts) -> + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()). -stop(_Host) -> - ejabberd_commands:unregister_commands(get_commands_spec()). +stop(Host) -> + ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()). reload(_Host, _NewOpts, _OldOpts) -> ok. diff --git a/src/mod_fail2ban.erl b/src/mod_fail2ban.erl index 2680248e5..2dbd8575c 100644 --- a/src/mod_fail2ban.erl +++ b/src/mod_fail2ban.erl @@ -107,16 +107,11 @@ c2s_stream_started(#{ip := {Addr, _}} = State, _) -> start(Host, Opts) -> catch ets:new(failed_auth, [named_table, public, {heir, erlang:group_leader(), none}]), - ejabberd_commands:register_commands(?MODULE, get_commands_spec()), + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end, + ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()), gen_mod:stop_child(?MODULE, Host). reload(_Host, _NewOpts, _OldOpts) -> diff --git a/src/mod_mam.erl b/src/mod_mam.erl index aa4bb3b84..a7b47fe91 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -182,7 +182,7 @@ start(Host, Opts) -> ejabberd_hooks:add(check_create_room, Host, ?MODULE, check_create_room, 50) end, - ejabberd_commands:register_commands(?MODULE, get_commands_spec()), + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), ok; Err -> Err @@ -263,12 +263,7 @@ stop(Host) -> ejabberd_hooks:delete(check_create_room, Host, ?MODULE, check_create_room, 50) end, - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end. + ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()). reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 35a0f2f94..c1c6ccf54 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -68,8 +68,8 @@ %% gen_mod %%---------------------------- -start(_Host, _Opts) -> - ejabberd_commands:register_commands(?MODULE, get_commands_spec()), +start(Host, _Opts) -> + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), {ok, [{hook, webadmin_menu_main, web_menu_main, 50, global}, {hook, webadmin_page_main, web_page_main, 50, global}, {hook, webadmin_menu_host, web_menu_host, 50}, @@ -79,12 +79,7 @@ start(_Host, _Opts) -> ]}. stop(Host) -> - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end. + ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()). reload(_Host, _NewOpts, _OldOpts) -> ok. diff --git a/src/mod_private.erl b/src/mod_private.erl index 145edefcf..3bb93ed77 100644 --- a/src/mod_private.erl +++ b/src/mod_private.erl @@ -71,7 +71,7 @@ start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), - ejabberd_commands:register_commands(?MODULE, get_commands_spec()), + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), {ok, [{hook, remove_user, remove_user, 50}, {hook, disco_sm_features, get_sm_features, 50}, {hook, pubsub_publish_item, pubsub_publish_item, 50}, @@ -82,12 +82,7 @@ start(Host, Opts) -> {iq_handler, ejabberd_sm, ?NS_PRIVATE, process_sm_iq}]}. stop(Host) -> - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end. + ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()). reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 3ebc69fa4..059fa9518 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -338,7 +338,7 @@ init([ServerHost|_]) -> false -> ok end, - ejabberd_commands:register_commands(?MODULE, get_commands_spec()), + ejabberd_commands:register_commands(ServerHost, ?MODULE, get_commands_spec()), NodeTree = config(ServerHost, nodetree), Plugins = config(ServerHost, plugins), PepMapping = config(ServerHost, pep_mapping), @@ -809,12 +809,7 @@ terminate(_Reason, terminate_plugins(Host, ServerHost, Plugins, TreePlugin), ejabberd_router:unregister_route(Host) end, Hosts), - case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end. + ejabberd_commands:unregister_commands(ServerHost, ?MODULE, get_commands_spec()). %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} diff --git a/src/mod_push.erl b/src/mod_push.erl index 20cb6394c..7d2792eb4 100644 --- a/src/mod_push.erl +++ b/src/mod_push.erl @@ -96,7 +96,7 @@ start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), - ejabberd_commands:register_commands(?MODULE, get_commands_spec()), + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), {ok, [{iq_handler, ejabberd_sm, ?NS_PUSH_0, process_iq}, {hook, disco_sm_features, disco_sm_features, 50}, {hook, c2s_session_pending, c2s_session_pending, 50}, @@ -111,12 +111,7 @@ start(Host, Opts) -> -spec stop(binary()) -> ok. stop(Host) -> - case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_commands:unregister_commands(get_commands_spec()); - true -> - ok - end. + ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()). -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(Host, NewOpts, OldOpts) -> From aa78362c7f5bfabc56183837233e24cf2a45ad09 Mon Sep 17 00:00:00 2001 From: Badlop Date: Fri, 21 Feb 2025 13:45:20 +0100 Subject: [PATCH 3/7] mod_configure: Add option 'access' to let configure the access name --- src/mod_configure.erl | 85 +++++++++++++++++++++++++++------------ src/mod_configure_opt.erl | 13 ++++++ 2 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 src/mod_configure_opt.erl diff --git a/src/mod_configure.erl b/src/mod_configure.erl index d213ae9d9..08380a16a 100644 --- a/src/mod_configure.erl +++ b/src/mod_configure.erl @@ -1,7 +1,7 @@ %%%---------------------------------------------------------------------- %%% File : mod_configure.erl %%% Author : Alexey Shchepin -%%% Purpose : Support for online configuration of ejabberd +%%% Purpose : Support for online configuration of ejabberd using XEP-0050 %%% Created : 19 Jan 2003 by Alexey Shchepin %%% %%% @@ -36,6 +36,7 @@ adhoc_local_items/4, adhoc_local_commands/4, get_sm_identity/5, get_sm_features/5, get_sm_items/5, adhoc_sm_items/4, adhoc_sm_commands/4, mod_options/1, + mod_opt_type/1, depends/2, mod_doc/0]). -include("logger.hrl"). @@ -92,6 +93,10 @@ depends(_Host, _Opts) -> -spec tokenize(binary()) -> [binary()]. tokenize(Node) -> str:tokens(Node, <<"/#">>). +acl_match_rule(Host, From) -> + Access = mod_configure_opt:access(Host), + acl:match_rule(Host, Access, From). + -spec get_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. get_sm_identity(Acc, _From, _To, Node, Lang) -> case Node of @@ -167,7 +172,7 @@ get_sm_features(Acc, From, case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> - Allow = acl:match_rule(LServer, configure, From), + Allow = acl_match_rule(LServer, From), case Node of <<"config">> -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); _ -> Acc @@ -182,7 +187,7 @@ get_local_features(Acc, From, false -> Acc; _ -> LNode = tokenize(Node), - Allow = acl:match_rule(LServer, configure, From), + Allow = acl_match_rule(LServer, From), case LNode of [<<"config">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"user">>] -> ?INFO_RESULT(Allow, [], Lang); @@ -240,7 +245,7 @@ get_local_features(Acc, From, jid(), jid(), binary()) -> mod_disco:items_acc(). adhoc_sm_items(Acc, From, #jid{lserver = LServer} = To, Lang) -> - case acl:match_rule(LServer, configure, From) of + case acl_match_rule(LServer, From) of allow -> Items = case Acc of {result, Its} -> Its; @@ -266,7 +271,7 @@ get_sm_items(Acc, From, {result, Its} -> Its; empty -> [] end, - case {acl:match_rule(LServer, configure, From), Node} of + case {acl_match_rule(LServer, From), Node} of {allow, <<"">>} -> Nodes = [?NODEJID(To, ?T("Configuration"), <<"config">>), @@ -295,13 +300,13 @@ get_user_resources(User, Server) -> jid(), jid(), binary()) -> mod_disco:items_acc(). adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To, Lang) -> - case acl:match_rule(LServer, configure, From) of + case acl_match_rule(LServer, From) of allow -> Items = case Acc of {result, Its} -> Its; empty -> [] end, - PermLev = get_permission_level(From), + PermLev = get_permission_level(From, LServer), Nodes = recursively_get_local_items(PermLev, LServer, <<"">>, Server, Lang), Nodes1 = lists:filter( @@ -348,9 +353,10 @@ recursively_get_local_items(PermLev, LServer, Node, end, Items)). --spec get_permission_level(jid()) -> global | vhost. -get_permission_level(JID) -> - case acl:match_rule(global, configure, JID) of +-spec get_permission_level(jid(), binary()) -> global | vhost. +get_permission_level(JID, Host) -> + Access = mod_configure_opt:access(Host), + case acl:match_rule(global, Access, JID) of allow -> global; deny -> vhost end. @@ -361,7 +367,7 @@ get_permission_level(JID) -> case Allow of deny -> Fallback; allow -> - PermLev = get_permission_level(From), + PermLev = get_permission_level(From, LServer), case get_local_items({PermLev, LServer}, LNode, jid:encode(To), Lang) of @@ -381,11 +387,11 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To, {result, Its} -> Its; empty -> [] end, - Allow = acl:match_rule(LServer, configure, From), + Allow = acl_match_rule(LServer, From), case Allow of deny -> {result, Items}; allow -> - PermLev = get_permission_level(From), + PermLev = get_permission_level(From, LServer), case get_local_items({PermLev, LServer}, [], jid:encode(To), Lang) of @@ -400,7 +406,7 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To, false -> Acc; _ -> LNode = tokenize(Node), - Allow = acl:match_rule(LServer, configure, From), + Allow = acl_match_rule(LServer, From), Err = xmpp:err_forbidden(?T("Access denied by service policy"), Lang), case LNode of [<<"config">>] -> @@ -690,7 +696,7 @@ get_stopped_nodes(_Lang) -> -define(COMMANDS_RESULT(LServerOrGlobal, From, To, Request, Lang), - case acl:match_rule(LServerOrGlobal, configure, From) of + case acl_match_rule(LServerOrGlobal, From) of deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> adhoc_local_commands(From, To, Request) end). @@ -1268,7 +1274,7 @@ set_form(From, Host, ?NS_ADMINL(<<"add-user">>), _Lang, Server = AccountJID#jid.lserver, true = lists:member(Server, ejabberd_option:hosts()), true = Server == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, case ejabberd_auth:try_register(User, Server, Password) of ok -> {result, undefined}; {error, exists} -> {error, xmpp:err_conflict()}; @@ -1284,7 +1290,7 @@ set_form(From, Host, ?NS_ADMINL(<<"delete-user">>), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, true = ejabberd_auth:user_exists(User, Server), {User, Server} end, @@ -1298,7 +1304,7 @@ set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>), JID = jid:decode(AccountString), LServer = JID#jid.lserver, true = LServer == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, case JID#jid.lresource of <<>> -> ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver); @@ -1314,7 +1320,7 @@ set_form(From, Host, User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, true = ejabberd_auth:user_exists(User, Server), ejabberd_auth:set_password(User, Server, Password), {result, undefined}; @@ -1325,7 +1331,7 @@ set_form(From, Host, User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, FLast = case ejabberd_sm:get_user_resources(User, Server) of @@ -1357,7 +1363,7 @@ set_form(From, Host, ?NS_ADMINL(<<"user-stats">>), Lang, User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse - get_permission_level(From) == global, + get_permission_level(From, Host) == global, Resources = ejabberd_sm:get_user_resources(User, Server), IPs1 = [ejabberd_sm:get_user_ip(User, Server, Resource) @@ -1448,7 +1454,7 @@ adhoc_sm_commands(_Acc, From, #jid{user = User, server = Server, lserver = LServer}, #adhoc_command{lang = Lang, node = <<"config">>, action = Action, xdata = XData} = Request) -> - case acl:match_rule(LServer, configure, From) of + case acl_match_rule(LServer, From) of deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> @@ -1530,12 +1536,41 @@ set_sm_form(_User, _Server, _Node, _Request) -> tr(Lang, Text) -> translate:translate(Lang, Text). -mod_options(_) -> []. +-spec mod_opt_type(atom()) -> econf:validator(). +mod_opt_type(access) -> + econf:acl(). + +-spec mod_options(binary()) -> [{services, [tuple()]} | {atom(), any()}]. +mod_options(_Host) -> + [{access, configure}]. + +%% @format-begin mod_doc() -> #{desc => ?T("The module provides server configuration functionality via " "https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]. " - "Implements many commands as defined in " + "It also implements many commands as defined in " "https://xmpp.org/extensions/xep-0133.html[XEP-0133: Service Administration]. " - "This module requires _`mod_adhoc`_ to be loaded.")}. + "This module requires _`mod_adhoc`_ (to execute the commands), " + "and recommends _`mod_disco`_ (to discover the commands). "), + opts => + [{access, + #{value => ?T("AccessName"), + note => "added in 25.xx", + desc => + ?T("This option defines which access rule will be used to " + "control who is allowed to access the features provided by this module. " + "The default value is 'configure'.")}}], + example => + ["acl:", + " admin:", + " user: sun@localhost", + "", + "access_rules:", + " configure:", + " allow: admin", + "", + "modules:", + " mod_configure:", + " access: configure"]}. diff --git a/src/mod_configure_opt.erl b/src/mod_configure_opt.erl new file mode 100644 index 000000000..0a8c190fd --- /dev/null +++ b/src/mod_configure_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_configure_opt). + +-export([access/1]). + +-spec access(gen_mod:opts() | global | binary()) -> 'configure' | acl:acl(). +access(Opts) when is_map(Opts) -> + gen_mod:get_opt(access, Opts); +access(Host) -> + gen_mod:get_module_opt(Host, mod_configure, access). + From 6d77ace5c969e531f8793dbc878e98583ad777ea Mon Sep 17 00:00:00 2001 From: Badlop Date: Wed, 5 Mar 2025 19:20:41 +0100 Subject: [PATCH 4/7] mod_http_api: Sort list elements in a command result --- src/mod_http_api.erl | 9 ++++++--- test/commands_tests.erl | 14 +++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index b2fe7b7c0..f5c9b3c2a 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -473,14 +473,17 @@ format_result(Code, {Name, restuple}) -> {[{<<"res">>, Code == true orelse Code == ok}, {<<"text">>, <<"">>}]}}; -format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) -> +format_result(Els1, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) -> + Els = lists:keysort(1, Els1), {misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}}; -format_result(Els, {Name, {list, {_, {tuple, [{name, string}, {value, _}]}} = Fmt}}) -> +format_result(Els1, {Name, {list, {_, {tuple, [{name, string}, {value, _}]}} = Fmt}}) -> + Els = lists:keysort(1, Els1), {misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}}; %% Covered by command_test_list and command_test_list_tuple -format_result(Els, {Name, {list, Def}}) -> +format_result(Els1, {Name, {list, Def}}) -> + Els = lists:sort(Els1), {misc:atom_to_binary(Name), [element(2, format_result(El, Def)) || El <- Els]}; format_result(Tuple, {_Name, {tuple, [{_, atom}, ValFmt]}}) -> diff --git a/test/commands_tests.erl b/test/commands_tests.erl index 52933e409..a1c60c5e4 100644 --- a/test/commands_tests.erl +++ b/test/commands_tests.erl @@ -130,7 +130,7 @@ http_restuple(Config) -> http_list(Config) -> ListS = ["one", "first", "primary"], - ListB = [<<"one">>, <<"first">>, <<"primary">>], + ListB = lists:sort([<<"one">>, <<"first">>, <<"primary">>]), ?match(ListB, query(Config, "command_test_list", #{arg_list => ListS})), ?match(ListB, query(Config, "command_test_list", #{arg_list => ListB})). @@ -148,20 +148,20 @@ http_tuple(Config) -> http_list_tuple(Config) -> LTA = [#{element1 => "one", element2 => "uno"}, - #{element1 => "dos", element2 => "two"}, + #{element1 => "two", element2 => "dos"}, #{element1 => "three", element2 => "tres"}], - LTB = [#{<<"element1">> => <<"one">>, <<"element2">> => <<"uno">>}, - #{<<"element1">> => <<"dos">>, <<"element2">> => <<"two">>}, - #{<<"element1">> => <<"three">>, <<"element2">> => <<"tres">>}], + LTB = lists:sort([#{<<"element1">> => <<"one">>, <<"element2">> => <<"uno">>}, + #{<<"element1">> => <<"two">>, <<"element2">> => <<"dos">>}, + #{<<"element1">> => <<"three">>, <<"element2">> => <<"tres">>}]), ?match(LTB, query(Config, "command_test_list_tuple", #{arg_list => LTA})), ?match(LTB, query(Config, "command_test_list_tuple", #{arg_list => LTB})). http_list_tuple_map(Config) -> LTA = #{<<"one">> => <<"uno">>, - <<"dos">> => <<"two">>, + <<"two">> => <<"dos">>, <<"three">> => <<"tres">>}, LTB = lists:sort([#{<<"element1">> => <<"one">>, <<"element2">> => <<"uno">>}, - #{<<"element1">> => <<"dos">>, <<"element2">> => <<"two">>}, + #{<<"element1">> => <<"two">>, <<"element2">> => <<"dos">>}, #{<<"element1">> => <<"three">>, <<"element2">> => <<"tres">>}]), ?match(LTB, lists:sort(query(Config, "command_test_list_tuple", #{arg_list => LTA}))). From 573e06cc0cb2432b2eb69a446f41b37ce53d5a22 Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 3 Mar 2025 13:07:46 +0100 Subject: [PATCH 5/7] mod_muc_admin: create_room_with_opts command recommends using ; and = separators --- src/mod_muc_admin.erl | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index c1c6ccf54..06995daed 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -189,14 +189,20 @@ get_commands_spec() -> #ejabberd_commands{name = create_room_with_opts, tags = [muc_room, muc_sub], desc = "Create a MUC room name@service in host with given options", longdesc = - "The syntax of `affiliations` is: `Type:JID,Type:JID`. " - "The syntax of `subscribers` is: `JID:Nick:Node:Node2:Node3,JID:Nick:Node`.", + "Options `affiliations` and `subscribers` are lists of tuples. " + "The tuples in the list are separated with `;` and " + "the elements in each tuple are separated with `=` " + "(until ejabberd 24.12 the separators were `,` and `:` respectively). " + "Each subscriber can have one or more nodes. " + "In summary, `affiliations` is like `Type1=JID1;Type2=JID2` " + "and `subscribers` is like `JID1=Nick1=Node1A=Node1B=Node1C;JID2=Nick2=Node2`.", + note = "modified in 25.xx", module = ?MODULE, function = create_room_with_opts, args_desc = ["Room name", "MUC service", "Server host", "List of options"], args_example = ["room1", "conference.example.com", "localhost", [{"members_only","true"}, - {"affiliations", "owner:bob@example.com,member:peter@example.com"}, - {"subscribers", "bob@example.com:Bob:messages:subject,anne@example.com:Anne:messages"}]], + {"affiliations", "owner=user1@localhost;member=user2@localhost"}, + {"subscribers", "user3@localhost=User3=messages=subject;user4@localhost=User4=messages"}]], args = [{room, binary}, {service, binary}, {host, binary}, {options, {list, @@ -1726,9 +1732,9 @@ format_room_option(OptionString, ValueString) -> lang -> ValueString; pubsub -> ValueString; affiliations -> - [parse_affiliation_string(Opt) || Opt <- str:tokens(ValueString, <<",">>)]; + [parse_affiliation_string(Opt) || Opt <- str:tokens(ValueString, <<";,">>)]; subscribers -> - [parse_subscription_string(Opt) || Opt <- str:tokens(ValueString, <<",">>)]; + [parse_subscription_string(Opt) || Opt <- str:tokens(ValueString, <<";,">>)]; allow_private_messages_from_visitors when (ValueString == <<"anyone">>) or (ValueString == <<"moderators">>) or @@ -1760,10 +1766,16 @@ throw_error(O, V) -> parse_affiliation_string(String) -> {Type, JidS} = case String of + %% Old syntax <<"owner:", Jid/binary>> -> {owner, Jid}; <<"admin:", Jid/binary>> -> {admin, Jid}; <<"member:", Jid/binary>> -> {member, Jid}; <<"outcast:", Jid/binary>> -> {outcast, Jid}; + %% New syntax + <<"owner=", Jid/binary>> -> {owner, Jid}; + <<"admin=", Jid/binary>> -> {admin, Jid}; + <<"member=", Jid/binary>> -> {member, Jid}; + <<"outcast=", Jid/binary>> -> {outcast, Jid}; _ -> throw({error, "Invalid 'affiliation'"}) end, try jid:decode(JidS) of @@ -1774,7 +1786,7 @@ parse_affiliation_string(String) -> end. parse_subscription_string(String) -> - case str:tokens(String, <<":">>) of + case str:tokens(String, <<"=:">>) of [_] -> throw({error, "Invalid 'subscribers' - missing nick"}); [_, _] -> From 496daf9220e0916d77f1d607de7b208d5a0884bd Mon Sep 17 00:00:00 2001 From: Badlop Date: Wed, 19 Mar 2025 10:39:53 +0100 Subject: [PATCH 6/7] mod_adhoc_api: New module to execute API Commands using Ad-Hoc Commands --- ejabberd.yml.example | 8 +- src/ejabberd_access_permissions.erl | 14 +- src/ejabberd_commands.erl | 11 + src/mod_adhoc_api.erl | 729 ++++++++++++++++++++++++++ src/mod_adhoc_api_opt.erl | 13 + src/mod_http_api.erl | 9 +- test/commands_tests.erl | 280 +++++++++- test/ejabberd_SUITE_data/ejabberd.yml | 1 + 8 files changed, 1046 insertions(+), 19 deletions(-) create mode 100644 src/mod_adhoc_api.erl create mode 100644 src/mod_adhoc_api_opt.erl diff --git a/ejabberd.yml.example b/ejabberd.yml.example index 0964afa06..070194412 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -118,7 +118,12 @@ api_permissions: from: ejabberd_web_admin who: admin what: "*" - "admin access": + "adhoc commands": + from: mod_adhoc_api + who: admin + what: "*" + "http access": + from: mod_http_api who: access: allow: @@ -159,6 +164,7 @@ shaper_rules: modules: mod_adhoc: {} + mod_adhoc_api: {} mod_admin_extra: {} mod_announce: access: announce diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl index 1889ca8df..57b3637e3 100644 --- a/src/ejabberd_access_permissions.erl +++ b/src/ejabberd_access_permissions.erl @@ -344,10 +344,20 @@ validator(from) -> fun(L) when is_list(L) -> lists:map( fun({K, V}) -> {(econf:enum([tag]))(K), (econf:binary())(V)}; - (A) -> (econf:enum([ejabberd_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl, ejabberd_web_admin]))(A) + (A) -> (econf:enum([ejabberd_ctl, + ejabberd_web_admin, + ejabberd_xmlrpc, + mod_adhoc_api, + mod_cron, + mod_http_api]))(A) end, lists:flatten(L)); (A) -> - [(econf:enum([ejabberd_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl, ejabberd_web_admin]))(A)] + [(econf:enum([ejabberd_ctl, + ejabberd_web_admin, + ejabberd_xmlrpc, + mod_adhoc_api, + mod_cron, + mod_http_api]))(A)] end; validator(what) -> econf:and_then( diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 2318564ba..243a594ed 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -33,6 +33,7 @@ -export([start_link/0, list_commands/0, list_commands/1, + list_commands/2, get_command_format/1, get_command_format/2, get_command_format/3, @@ -217,6 +218,16 @@ list_commands(Version) -> desc = Desc} <- Commands, not lists:member(internal, Tags)]. +-spec list_commands(integer(), map()) -> [{atom(), [aterm()], string()}]. + +list_commands(Version, CallerInfo) -> + lists:filter( + fun({Name, _Args, _Desc}) -> + allow == ejabberd_access_permissions:can_access(Name, CallerInfo) + end, + list_commands(Version) + ). + -spec get_command_format(atom()) -> {[aterm()], [{atom(),atom()}], rterm()}. get_command_format(Name) -> diff --git a/src/mod_adhoc_api.erl b/src/mod_adhoc_api.erl new file mode 100644 index 000000000..397f9be6c --- /dev/null +++ b/src/mod_adhoc_api.erl @@ -0,0 +1,729 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_adhoc_api.erl +%%% Author : Badlop +%%% Purpose : Frontend for ejabberd API Commands via XEP-0050 Ad-Hoc Commands +%%% Created : 21 Feb 2025 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2025 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +%%%% definitions +%% @format-begin + +-module(mod_adhoc_api). + +-behaviour(gen_mod). + +-author('badlop@process-one.net'). + +%% gen_mod callbacks +-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). +%% hooks +-export([adhoc_local_commands/4, adhoc_local_items/4, disco_local_features/5, + disco_local_identity/5, disco_local_items/5]). + +-include("ejabberd_commands.hrl"). +-include("ejabberd_sm.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +-include_lib("stdlib/include/ms_transform.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +-define(DEFAULT_API_VERSION, 1000000). + +%%%================================== +%%%% gen_mod + +start(_Host, _Opts) -> + {ok, + [{hook, adhoc_local_commands, adhoc_local_commands, 40}, + {hook, adhoc_local_items, adhoc_local_items, 40}, + {hook, disco_local_features, disco_local_features, 40}, + {hook, disco_local_identity, disco_local_identity, 40}, + {hook, disco_local_items, disco_local_items, 40}]}. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +mod_opt_type(default_version) -> + econf:either( + econf:int(0, 3), + econf:and_then( + econf:binary(), + fun(Binary) -> + case binary_to_list(Binary) of + F when F >= "24.06" -> + 2; + F when (F > "23.10") and (F < "24.06") -> + 1; + F when F =< "23.10" -> + 0 + end + end)). + +-spec mod_options(binary()) -> [{default_version, integer()}]. +mod_options(_) -> + [{default_version, ?DEFAULT_API_VERSION}]. + +depends(_Host, _Opts) -> + [{mod_adhoc, hard}, {mod_last, soft}]. + +mod_doc() -> + #{desc => + ?T("Execute https://docs.ejabberd.im/developer/ejabberd-api/[API Commands] " + "in a XMPP client using " + "https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]. " + "This module requires _`mod_adhoc`_ (to execute the commands), " + "and recommends _`mod_disco`_ (to discover the commands)."), + note => "added in 25.xx", + opts => + [{default_version, + #{value => "integer() | string()", + desc => + ?T("What API version to use. " + "If setting an ejabberd version, it will use the latest API " + "version that was available in that ejabberd version. " + "For example, setting '\"24.06\"' in this option implies '2'. " + "The default value is the latest version.")}}], + example => + ["acl:", + " admin:", + " user: jan@localhost", + "", + "api_permissions:", + " \"adhoc commands\":", + " from: mod_adhoc_api", + " who: admin", + " what:", + " - \"[tag:roster]\"", + " - \"[tag:session]\"", + " - stats", + " - status", + "", + "modules:", + " mod_adhoc_api:", + " default_version: 2"]}. + +%%%================================== +%%%% Ad-Hoc Commands (copied from mod_configure) + +-define(INFO_IDENTITY(Category, Type, Name, Lang), + [#identity{category = Category, + type = Type, + name = tr(Lang, Name)}]). +-define(INFO_COMMAND(Name, Lang), + ?INFO_IDENTITY(<<"automation">>, <<"command-node">>, Name, Lang)). +-define(NODE(Name, Node), + #disco_item{jid = jid:make(Server), + node = Node, + name = tr(Lang, Name)}). + +-spec tokenize(binary()) -> [binary()]. +tokenize(Node) -> + str:tokens(Node, <<"/#">>). + +-spec tr(binary(), binary()) -> binary(). +tr(Lang, Text) -> + translate:translate(Lang, Text). + +%%%================================== +%%%% - disco identity + +-spec disco_local_identity([identity()], jid(), jid(), binary(), binary()) -> + [identity()]. +disco_local_identity(Acc, _From, #jid{lserver = LServer} = _To, Node, Lang) -> + case tokenize(Node) of + [<<"api-commands">>] -> + ?INFO_COMMAND(?T("API Commands"), Lang); + [<<"api-commands">>, CommandName] -> + ?INFO_COMMAND(get_api_command_desc(CommandName, LServer), Lang); + _ -> + Acc + end. + +get_api_command_desc(NameAtom, Host) -> + iolist_to_binary((get_api_command(NameAtom, Host))#ejabberd_commands.desc). + +%%%================================== +%%%% - disco features + +-spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> + mod_disco:features_acc(). +disco_local_features(Acc, _From, #jid{lserver = LServer} = _To, Node, _Lang) -> + case gen_mod:is_loaded(LServer, mod_adhoc) of + false -> + Acc; + _ -> + case tokenize(Node) of + [<<"api-commands">>] -> + {result, []}; + [<<"api-commands">>, _] -> + {result, [?NS_COMMANDS]}; + _ -> + Acc + end + end. + +%%%================================== +%%%% - adhoc items + +-spec adhoc_local_items(mod_disco:items_acc(), jid(), jid(), binary()) -> + mod_disco:items_acc(). +adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To, Lang) -> + Items = + case Acc of + {result, Its} -> + Its; + empty -> + [] + end, + Nodes = recursively_get_local_items(From, global, LServer, <<"">>, Server, Lang), + Nodes1 = + lists:filter(fun(#disco_item{node = Nd}) -> + F = disco_local_features(empty, From, To, Nd, Lang), + case F of + {result, [?NS_COMMANDS]} -> + true; + _ -> + false + end + end, + Nodes), + {result, Items ++ Nodes1}. + +-spec recursively_get_local_items(jid(), + global | vhost, + binary(), + binary(), + binary(), + binary()) -> + [disco_item()]. +recursively_get_local_items(From, PermLev, LServer, Node, Server, Lang) -> + Items = + case get_local_items2(From, {PermLev, LServer}, tokenize(Node), Server, Lang) of + {result, Res} -> + Res; + {error, _Error} -> + [] + end, + lists:flatten( + lists:map(fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) -> + if (S /= Server) or (Nd == <<"">>) -> + []; + true -> + [Item, + recursively_get_local_items(From, PermLev, LServer, Nd, Server, Lang)] + end + end, + Items)). + +%%%================================== +%%%% - disco items + +-spec disco_local_items(mod_disco:items_acc(), jid(), jid(), binary(), binary()) -> + mod_disco:items_acc(). +disco_local_items(Acc, From, #jid{lserver = LServer} = To, Node, Lang) -> + case gen_mod:is_loaded(LServer, mod_adhoc) of + false -> + Acc; + _ -> + Items = + case Acc of + {result, Its} -> + Its; + empty -> + []; + Other -> + Other + end, + case tokenize(Node) of + LNode when (LNode == [<<"api-commands">>]) or (LNode == []) -> + case get_local_items2(From, {global, LServer}, LNode, jid:encode(To), Lang) of + {result, Res} -> + {result, Res}; + {error, Error} -> + {error, Error} + end; + _ -> + {result, Items} + end + end. + +%%%================================== +%%%% - get_local_items2 + +-spec get_local_items2(jid(), + {global | vhost, binary()}, + [binary()], + binary(), + binary()) -> + {result, [disco_item()]} | {error, stanza_error()}. +get_local_items2(_From, _Host, [], Server, Lang) -> + {result, [?NODE(?T("API Commands"), <<"api-commands">>)]}; +get_local_items2(From, {_, Host}, [<<"api-commands">>], _Server, Lang) -> + {result, get_api_commands(From, Host, Lang)}; +get_local_items2(_From, {_, _Host}, [<<"api-commands">>, _], _Server, _Lang) -> + {result, []}; +get_local_items2(_From, _Host, _, _Server, _Lang) -> + {error, xmpp:err_item_not_found()}. + +-spec get_api_commands(jid(), binary(), binary()) -> [disco_item()]. +get_api_commands(From, Server, Lang) -> + ApiVersion = mod_adhoc_api_opt:default_version(Server), + lists:map(fun({Name, _Args, _Desc}) -> + NameBin = list_to_binary(atom_to_list(Name)), + ?NODE(NameBin, <<"api-commands/", NameBin/binary>>) + end, + ejabberd_commands:list_commands(ApiVersion, get_caller_info(From))). + +%%%================================== +%%%% - adhoc commands + +-define(COMMANDS_RESULT(LServerOrGlobal, From, To, Request, Lang), + adhoc_local_commands(From, To, Request)). + +-spec adhoc_local_commands(adhoc_command(), jid(), jid(), adhoc_command()) -> + adhoc_command() | {error, stanza_error()}. +adhoc_local_commands(Acc, From, To, #adhoc_command{node = Node} = Request) -> + case tokenize(Node) of + [<<"api-commands">>, _CommandName] -> + ?COMMANDS_RESULT(LServer, From, To, Request, Lang); + _ -> + Acc + end. + +-spec adhoc_local_commands(jid(), jid(), adhoc_command()) -> + adhoc_command() | {error, stanza_error()}. +adhoc_local_commands(From, + #jid{lserver = LServer} = _To, + #adhoc_command{lang = Lang, + node = Node, + sid = SessionID, + action = Action, + xdata = XData} = + Request) -> + LNode = tokenize(Node), + ActionIsExecute = Action == execute orelse Action == complete, + if Action == cancel -> + #adhoc_command{status = canceled, + lang = Lang, + node = Node, + sid = SessionID}; + XData == undefined, ActionIsExecute -> + case get_form(LServer, LNode, Lang) of + {result, Form} -> + xmpp_util:make_adhoc_response(Request, + #adhoc_command{status = executing, xdata = Form}); + {error, Error} -> + {error, Error} + end; + XData /= undefined, ActionIsExecute -> + case set_form(From, LServer, LNode, Lang, XData) of + {result, Res} -> + xmpp_util:make_adhoc_response(Request, + #adhoc_command{xdata = Res, status = completed}); + %%{'EXIT', _} -> {error, xmpp:err_bad_request()}; + {error, Error} -> + {error, Error} + end; + true -> + {error, xmpp:err_bad_request(?T("Unexpected action"), Lang)} + end. + +-spec get_form(binary(), [binary()], binary()) -> + {result, xdata()} | {error, stanza_error()}. +get_form(Host, [<<"api-commands">>, CommandName], Lang) -> + get_form_api_command(CommandName, Host, Lang); +get_form(_Host, _, _Lang) -> + {error, xmpp:err_service_unavailable()}. + +-spec set_form(jid(), binary(), [binary()], binary(), xdata()) -> + {result, xdata() | undefined} | {error, stanza_error()}. +set_form(From, Host, [<<"api-commands">>, Command], Lang, XData) -> + set_form_api_command(From, Host, Command, XData, Lang); +set_form(_From, _Host, _, _Lang, _XData) -> + {error, xmpp:err_service_unavailable()}. + +%%%================================== +%%%% API Commands + +get_api_command(Name, Host) when is_binary(Name) -> + get_api_command(binary_to_existing_atom(Name, latin1), Host); +get_api_command(Name, Host) when is_atom(Name) -> + ApiVersion = mod_adhoc_api_opt:default_version(Host), + ejabberd_commands:get_command_definition(Name, ApiVersion). + +get_caller_info(#jid{user = User, server = Server} = From) -> + #{tag => <<>>, + usr => {User, Server, <<"">>}, + caller_server => Server, + ip => get_ip_address(From), + caller_module => ?MODULE}. + +get_ip_address(#jid{user = User, + server = Server, + resource = Resource}) -> + case ejabberd_sm:get_user_ip(User, Server, Resource) of + {IP, _Port} when is_tuple(IP) -> + IP; + _ -> + error_ip_address + end. + +%%%================================== +%%%% - get form + +get_form_api_command(NameBin, Host, _Lang) -> + Def = get_api_command(NameBin, Host), + Title = list_to_binary(atom_to_list(Def#ejabberd_commands.name)), + Instructions = get_instructions(Def), + FieldsArgs = + build_fields(Def#ejabberd_commands.args, + Def#ejabberd_commands.args_desc, + Def#ejabberd_commands.args_example, + Def#ejabberd_commands.policy, + get_replacements(Host), + true), + FieldsArgsWithHeads = + case FieldsArgs of + [] -> + []; + _ -> + [#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs] + end, + NodeFields = build_node_fields(), + {result, + #xdata{title = Title, + type = form, + instructions = Instructions, + fields = FieldsArgsWithHeads ++ NodeFields}}. + +get_replacements(Host) -> + [{user, <<"">>}, + {localuser, <<"">>}, + {host, Host}, + {localhost, Host}, + {password, <<"">>}, + {newpass, <<"">>}, + {service, mod_muc_admin:find_hosts(Host)}]. + +build_node_fields() -> + build_node_fields([node() | nodes()]). + +build_node_fields([_ThisNode]) -> + []; +build_node_fields(AtomNodes) -> + [ThisNode | _] = Nodes = [atom_to_binary(Atom, latin1) || Atom <- AtomNodes], + Options = [#xdata_option{label = N, value = N} || N <- Nodes], + [#xdata_field{type = fixed, label = ?T("Clustering")}, + #xdata_field{type = 'list-single', + label = <<"ejabberd node">>, + var = <<"mod_adhoc_api_target_node">>, + values = [ThisNode], + options = Options}]. + +%%%================================== +%%%% - set form + +set_form_api_command(From, Host, CommandNameBin, XData, _Lang) -> + %% Description + Def = get_api_command(CommandNameBin, Host), + Title = list_to_binary(atom_to_list(Def#ejabberd_commands.name)), + Instructions = get_instructions(Def), + + %% Arguments + FieldsArgs1 = [Field || Field <- XData#xdata.fields, Field#xdata_field.type /= fixed], + + {Node, FieldsArgs} = + case lists:keytake(<<"mod_adhoc_api_target_node">>, #xdata_field.var, FieldsArgs1) of + {value, #xdata_field{values = [TargetNode]}, FAs} -> + {binary_to_existing_atom(TargetNode, latin1), FAs}; + false -> + {node(), FieldsArgs1} + end, + + FieldsArgsWithHeads = + case FieldsArgs of + [] -> + []; + _ -> + [#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs] + end, + + %% Execute + Arguments = api_extract_fields(FieldsArgs, Def#ejabberd_commands.args), + ApiVersion = mod_adhoc_api_opt:default_version(Host), + CallResult = + ejabberd_cluster:call(Node, + mod_http_api, + handle, + [binary_to_existing_atom(CommandNameBin, latin1), + get_caller_info(From), + Arguments, + ApiVersion]), + + %% Command result + FieldsResult2 = + case CallResult of + {200, RR} -> + build_fields([Def#ejabberd_commands.result], + [Def#ejabberd_commands.result_desc], + [RR], + restricted, + [{host, Host}], + false); + {Code, _ApiErrorCode, MessageBin} -> + [#xdata_field{type = 'text-single', + label = <<"Error ", (integer_to_binary(Code))/binary>>, + values = encode(MessageBin, irrelevat_type), + var = <<"error">>}]; + {Code, MessageBin} -> + [#xdata_field{type = 'text-single', + label = <<"Error ", (integer_to_binary(Code))/binary>>, + values = encode(MessageBin, irrelevat_type), + var = <<"error">>}] + end, + FieldsResultWithHeads = + [#xdata_field{type = fixed, label = ?T("")}, + #xdata_field{type = fixed, label = ?T("Result")} + | FieldsResult2], + + %% Result stanza + {result, + #xdata{title = Title, + type = result, + instructions = Instructions, + fields = FieldsArgsWithHeads ++ FieldsResultWithHeads}}. + +api_extract_fields(Fields, ArgsDef) -> + lists:map(fun(#xdata_field{values = Values, var = ANameBin}) -> + ArgDef = proplists:get_value(binary_to_existing_atom(ANameBin, latin1), ArgsDef), + V = case {Values, ArgDef} of + {Values, {list, {_ElementName, {tuple, ElementsDef}}}} -> + [parse_tuple(ElementsDef, Value) || Value <- Values]; + {[Value], {tuple, ElementsDef}} -> + parse_tuple(ElementsDef, Value); + {[Value], _} -> + Value; + _ -> + Values + end, + {ANameBin, V} + end, + Fields). + +parse_tuple(ElementsDef, Value) -> + Values = str:tokens(Value, <<":">>), + List1 = + [{atom_to_binary(Name, latin1), Val} + || {{Name, _Type}, Val} <- lists:zip(ElementsDef, Values)], + maps:from_list(List1). + +%%%================================== +%%%% - get instructions + +get_instructions(Def) -> + Note2 = + case Def#ejabberd_commands.note of + [] -> + []; + Note -> + N = iolist_to_binary(Note), + [<<"Note: ", N/binary>>] + end, + Tags2 = + case Def#ejabberd_commands.tags of + [] -> + []; + Tags -> + T = str:join([atom_to_binary(Tag, latin1) || Tag <- Tags], <<", ">>), + [<<"Tags: ", T/binary>>] + end, + Module2 = + case Def#ejabberd_commands.definer of + unknown -> + []; + DefinerAtom -> + D = atom_to_binary(DefinerAtom, latin1), + [<<"Module: ", D/binary>>] + end, + Version2 = + case Def#ejabberd_commands.version of + 0 -> + []; + Version -> + V = integer_to_binary(Version), + [<<"API version: ", V/binary>>] + end, + get_instructions2([Def#ejabberd_commands.desc, Def#ejabberd_commands.longdesc] + ++ Note2 + ++ Tags2 + ++ Module2 + ++ Version2). + +get_instructions2(ListStrings) -> + [re:replace(String, "[\t]*[ ]+", " ", [{return, binary}, global]) + || String <- ListStrings, String /= ""]. + +%%%================================== +%%%% - build fields + +build_fields(NameTypes, none, Examples, Policy, Replacements, Required) -> + build_fields(NameTypes, [], Examples, Policy, Replacements, Required); +build_fields(NameTypes, Descs, none, Policy, Replacements, Required) -> + build_fields(NameTypes, Descs, [], Policy, Replacements, Required); +build_fields(NameTypes, [none], Examples, Policy, Replacements, Required) -> + build_fields(NameTypes, [], Examples, Policy, Replacements, Required); +build_fields(NameTypes, Descs, [none], Policy, Replacements, Required) -> + build_fields(NameTypes, Descs, [], Policy, Replacements, Required); +build_fields(NameTypes, Descs, Examples, Policy, Replacements, Required) -> + {NameTypes2, Descs2, Examples2} = + case Policy of + user -> + {[{user, binary}, {host, binary} | NameTypes], + ["Username", "Server host" | Descs], + ["tom", "example.com" | Examples]}; + _ -> + {NameTypes, Descs, Examples} + end, + build_fields2(NameTypes2, Descs2, Examples2, Replacements, Required). + +build_fields2([{_ArgName, {list, _ArgNameType}}] = NameTypes, + Descs, + Examples, + _Replacements, + Required) -> + Args = lists_zip3_pad(NameTypes, Descs, Examples), + lists:map(fun({{AName, AType}, ADesc, AExample}) -> + ANameBin = list_to_binary(atom_to_list(AName)), + #xdata_field{type = 'text-multi', + label = ANameBin, + desc = list_to_binary(ADesc), + values = encode(AExample, AType), + required = Required, + var = ANameBin} + end, + Args); +build_fields2(NameTypes, Descs, Examples, Replacements, Required) -> + Args = lists_zip3_pad(NameTypes, Descs, Examples), + lists:map(fun({{AName, AType}, ADesc, AExample}) -> + ANameBin = list_to_binary(atom_to_list(AName)), + AValue = proplists:get_value(AName, Replacements, AExample), + Values = encode(AValue, AType), + Type = + case {AType, Values} of + {{list, _}, _} -> + 'text-multi'; + {string, [_, _ | _]} -> + 'text-multi'; + _ -> + 'text-single' + end, + #xdata_field{type = Type, + label = ANameBin, + desc = make_desc(ADesc, AValue), + values = Values, + required = Required, + var = ANameBin} + end, + Args). + +-ifdef(OTP_BELOW_26). + +lists_zip3_pad(As, Bs, Cs) -> + lists_zip3_pad(As, Bs, Cs, []). + +lists_zip3_pad([A | As], [B | Bs], [C | Cs], Xs) -> + lists_zip3_pad(As, Bs, Cs, [{A, B, C} | Xs]); +lists_zip3_pad([A | As], [B | Bs], Nil, Xs) when (Nil == none) or (Nil == []) -> + lists_zip3_pad(As, Bs, [], [{A, B, ""} | Xs]); +lists_zip3_pad([A | As], Nil, [C | Cs], Xs) when (Nil == none) or (Nil == []) -> + lists_zip3_pad(As, [], Cs, [{A, "", C} | Xs]); +lists_zip3_pad([A | As], Nil, Nil, Xs) when (Nil == none) or (Nil == []) -> + lists_zip3_pad(As, [], [], [{A, "", ""} | Xs]); +lists_zip3_pad([], Nil, Nil, Xs) when (Nil == none) or (Nil == []) -> + lists:reverse(Xs). + +-else. + +lists_zip3_pad(As, Bs, Cs) -> + lists:zip3(As, Bs, Cs, {pad, {error_missing_args_def, "", ""}}). + +-endif. + +make_desc(ADesc, T) when is_tuple(T) -> + T3 = string:join(tuple_to_list(T), " : "), + iolist_to_binary([ADesc, " {", T3, "}"]); +make_desc(ADesc, M) when is_map(M) -> + M2 = [binary_to_list(V) || V <- maps:keys(M)], + M3 = string:join(M2, " : "), + iolist_to_binary([ADesc, " {", M3, "}"]); +make_desc(ADesc, _M) -> + iolist_to_binary(ADesc). + +%%%================================== +%%%% - encode + +encode({[T | _] = List}, Type) when is_tuple(T) -> + encode(List, Type); +encode([T | _] = List, Type) when is_tuple(T) -> + [encode(Element, Type) || Element <- List]; +encode(T, _Type) when is_tuple(T) -> + T2 = [x_to_binary(E) || E <- tuple_to_list(T)], + T3 = str:join(T2, <<":">>), + [T3]; +encode(M, {tuple, Types}) when is_map(M) -> + M2 = [x_to_list(maps:get(atom_to_binary(Key, latin1), M)) + || {Key, _ElementType} <- Types], + M3 = string:join(M2, " : "), + [iolist_to_binary(M3)]; +encode([S | _] = SList, _Type) when is_list(S) -> + [iolist_to_binary(A) || A <- SList]; +encode([B | _] = BList, _Type) when is_binary(B) -> + BList; +encode(I, _Type) when is_integer(I) -> + [integer_to_binary(I)]; +encode([M | _] = List, {list, {_Name, TupleType}}) when is_map(M) -> + [encode(M1, TupleType) || M1 <- List]; +encode(S, _Type) when is_list(S) -> + [iolist_to_binary(S)]; +encode(B, _Type) when is_binary(B) -> + str:tokens(B, <<"\n">>). + +x_to_list(B) when is_binary(B) -> + binary_to_list(B); +x_to_list(I) when is_integer(I) -> + integer_to_list(I); +x_to_list(L) when is_list(L) -> + L. + +x_to_binary(B) when is_binary(B) -> + B; +x_to_binary(I) when is_integer(I) -> + integer_to_binary(I); +x_to_binary(L) when is_list(L) -> + iolist_to_binary(L). + +%%%================================== + +%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: diff --git a/src/mod_adhoc_api_opt.erl b/src/mod_adhoc_api_opt.erl new file mode 100644 index 000000000..bd7cdce42 --- /dev/null +++ b/src/mod_adhoc_api_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_adhoc_api_opt). + +-export([default_version/1]). + +-spec default_version(gen_mod:opts() | global | binary()) -> integer(). +default_version(Opts) when is_map(Opts) -> + gen_mod:get_opt(default_version, Opts); +default_version(Host) -> + gen_mod:get_module_opt(Host, mod_adhoc_api, default_version). + diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index f5c9b3c2a..68d5c4181 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -30,7 +30,7 @@ -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process/2, depends/2, - format_arg/2, + format_arg/2, handle/4, mod_opt_type/1, mod_options/1, mod_doc/0]). -include_lib("xmpp/include/xmpp.hrl"). @@ -353,6 +353,9 @@ format_arg(Elements, || Element <- Elements]; %% Covered by command_test_list and command_test_list_tuple +format_arg(Element, {list, Def}) + when not is_list(Element) -> + format_arg([Element], {list, Def}); format_arg(Elements, {list, {_ElementDefName, ElementDefFormat}}) when is_list(Elements) -> @@ -395,6 +398,7 @@ format_arg(Elements, {list, ElementsDef}) || Element <- Elements]; format_arg(Arg, integer) when is_integer(Arg) -> Arg; +format_arg(Arg, integer) when is_binary(Arg) -> binary_to_integer(Arg); format_arg(Arg, binary) when is_list(Arg) -> process_unicode_codepoints(Arg); format_arg(Arg, binary) when is_binary(Arg) -> Arg; format_arg(Arg, string) when is_list(Arg) -> Arg; @@ -460,6 +464,9 @@ format_result([String | _] = StringList, {Name, string}) when is_list(String) -> format_result(String, {Name, string}) -> {misc:atom_to_binary(Name), iolist_to_binary(String)}; +format_result(Binary, {Name, binary}) -> + {misc:atom_to_binary(Name), Binary}; + format_result(Code, {Name, rescode}) -> {misc:atom_to_binary(Name), Code == true orelse Code == ok}; diff --git a/test/commands_tests.erl b/test/commands_tests.erl index a1c60c5e4..7b0675c3f 100644 --- a/test/commands_tests.erl +++ b/test/commands_tests.erl @@ -23,8 +23,6 @@ %%%% definitions -%% @format-begin - -module(commands_tests). -compile(export_all). @@ -56,10 +54,29 @@ single_cases() -> single_test(http_tuple), single_test(http_list_tuple), single_test(http_list_tuple_map), + single_test(adhoc_list_commands), + single_test(adhoc_apiversion), + single_test(adhoc_apizero), + single_test(adhoc_apione), + single_test(adhoc_integer), + single_test(adhoc_string), + single_test(adhoc_binary), + single_test(adhoc_tuple), + single_test(adhoc_list), + single_test(adhoc_list_tuple), + single_test(adhoc_atom), + single_test(adhoc_rescode), + single_test(adhoc_restuple), + %%single_test(adhoc_all), single_test(clean)]}. -endif. +%% @format-begin + +single_test(T) -> + list_to_atom("commands_" ++ atom_to_list(T)). + setup(_Config) -> M = <<"mod_example">>, clean(_Config), @@ -90,6 +107,9 @@ ejabberdctl(_Config) -> Installed = execute(modules_installed, []), ?match(true, lists:keymember(mod_example, 1, Installed)). +execute(Name, Args) -> + ejabberd_commands:execute_command2(Name, Args, #{caller_module => ejabberd_ctl}, 1000000). + %%%================================== %%%% mod_http_api @@ -165,19 +185,7 @@ http_list_tuple_map(Config) -> #{<<"element1">> => <<"three">>, <<"element2">> => <<"tres">>}]), ?match(LTB, lists:sort(query(Config, "command_test_list_tuple", #{arg_list => LTA}))). -%%%================================== -%%%% internal functions - -single_test(T) -> - list_to_atom("commands_" ++ atom_to_list(T)). - -execute(Name, Args) -> - ejabberd_commands:execute_command2(Name, Args, #{caller_module => ejabberd_ctl}, 1000000). - -page(Config, Tail) -> - Server = ?config(server_host, Config), - Port = ct:get_config(web_port, 5280), - "http://" ++ Server ++ ":" ++ integer_to_list(Port) ++ "/api/" ++ Tail. +%%% internal functions query(Config, Tail, Map) -> BodyQ = misc:json_encode(Map), @@ -192,6 +200,248 @@ make_query(Config, Tail, BodyQ) -> [{body_format, binary}]), Body). +page(Config, Tail) -> + Server = ?config(server_host, Config), + Port = ct:get_config(web_port, 5280), + "http://" ++ Server ++ ":" ++ integer_to_list(Port) ++ "/api/" ++ Tail. + +%%%================================== +%%%% ad-hoc + +%%% list commands + +adhoc_list_commands(Config) -> + {ok, Result} = get_items(Config, <<"api-commands">>), + {value, #disco_item{name = <<"command_test_binary">>}} = + lists:keysearch(<<"command_test_binary">>, #disco_item.name, Result), + suite:disconnect(Config). + +get_items(Config, Node) -> + case suite:send_recv(Config, + #iq{type = get, + to = server_jid(Config), + sub_els = [#disco_items{node = Node}]}) + of + #iq{type = result, sub_els = [#disco_items{node = Node, items = Items}]} -> + {ok, Items}; + #iq{type = result, sub_els = []} -> + {empty, []}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +%%% apiversion + +adhoc_apiversion(Config) -> + Node = <<"api-commands/command_test_apiversion">>, + ArgFields = make_fields_args([]), + ResFields = make_fields_res([{<<"apiversion">>, <<"2">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% apizero + +adhoc_apizero(Config) -> + Node = <<"api-commands/command_test_apizero">>, + ArgFields = make_fields_args([]), + ResFields = make_fields_res([{<<"apiversion">>, <<"0">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% apione + +adhoc_apione(Config) -> + Node = <<"api-commands/command_test_apione">>, + ArgFields = make_fields_args([]), + ResFields = make_fields_res([{<<"apiversion">>, <<"1">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% integer + +adhoc_integer(Config) -> + Node = <<"api-commands/command_test_integer">>, + ArgFields = make_fields_args([{<<"arg_integer">>, <<"12345">>}]), + ResFields = make_fields_res([{<<"res_integer">>, <<"12345">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% string + +adhoc_string(Config) -> + Node = <<"api-commands/command_test_string">>, + ArgFields = make_fields_args([{<<"arg_string">>, <<"Some string.">>}]), + ResFields = make_fields_res([{<<"res_string">>, <<"Some string.">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% binary + +adhoc_binary(Config) -> + Node = <<"api-commands/command_test_binary">>, + ArgFields = make_fields_args([{<<"arg_binary">>, <<"Some binary.">>}]), + ResFields = make_fields_res([{<<"res_string">>, <<"Some binary.">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% tuple + +adhoc_tuple(Config) -> + Node = <<"api-commands/command_test_tuple">>, + ArgFields = make_fields_args([{<<"arg_tuple">>, <<"one:two:three">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, + [{xdata_field, + <<"res_tuple">>, + 'text-single', + <<"res_tuple">>, + false, + <<" {element1 : element2 : element3}">>, + [<<"one : two : three">>], + [], + []}]}, + set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% list + +adhoc_list(Config) -> + Node = <<"api-commands/command_test_list">>, + ArgFields = make_fields_args([{<<"arg_list">>, [<<"one">>, <<"first">>, <<"primary">>]}]), + ResFields = + make_fields_res([{<<"res_list">>, lists:sort([<<"one">>, <<"first">>, <<"primary">>])}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% list_tuple + +adhoc_list_tuple(Config) -> + Node = <<"api-commands/command_test_list_tuple">>, + ArgFields = + make_fields_args([{<<"arg_list">>, [<<"one:uno">>, <<"two:dos">>, <<"three:tres">>]}]), + ResFields = + make_fields_res([{<<"res_list">>, + lists:sort([<<"one : uno">>, <<"two : dos">>, <<"three : tres">>])}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% atom + +adhoc_atom(Config) -> + Node = <<"api-commands/command_test_atom">>, + ArgFields = make_fields_args([{<<"arg_string">>, <<"a_test_atom">>}]), + ResFields = make_fields_res([{<<"res_atom">>, <<"a_test_atom">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% rescode + +adhoc_rescode(Config) -> + Node = <<"api-commands/command_test_rescode">>, + ArgFields = make_fields_args([{<<"code">>, <<"ok">>}]), + ResFields = make_fields_res([{<<"res_atom">>, <<"0">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% restuple + +adhoc_restuple(Config) -> + Node = <<"api-commands/command_test_restuple">>, + ArgFields = + make_fields_args([{<<"code">>, <<"ok">>}, {<<"text">>, <<"Just a result text">>}]), + ResFields = make_fields_res([{<<"res_atom">>, <<"Just a result text">>}]), + {ok, Sid, _FormFields} = get_form(Config, Node), + ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), + suite:disconnect(Config). + +%%% internal functions + +server_jid(Config) -> + jid:make(<<>>, ?config(server, Config), <<>>). + +make_fields_args(Fields) -> + lists:map(fun ({Var, Values}) when is_list(Values) -> + #xdata_field{label = Var, + var = Var, + required = true, + type = 'text-multi', + values = Values}; + ({Var, Value}) -> + #xdata_field{label = Var, + var = Var, + required = true, + type = 'text-single', + values = [Value]} + end, + Fields). + +make_fields_res(Fields) -> + lists:map(fun ({Var, Values}) when is_list(Values) -> + #xdata_field{label = Var, + var = Var, + type = 'text-multi', + values = Values}; + ({Var, Value}) -> + #xdata_field{label = Var, + var = Var, + type = 'text-single', + values = [Value]} + end, + Fields). + +get_form(Config, Node) -> + case suite:send_recv(Config, + #iq{type = set, + to = server_jid(Config), + sub_els = [#adhoc_command{node = Node}]}) + of + #iq{type = result, + sub_els = + [#adhoc_command{node = Node, + action = execute, + status = executing, + sid = Sid, + actions = #adhoc_actions{execute = complete, complete = true}, + xdata = #xdata{fields = Fields}}]} -> + {ok, Sid, [F || F <- Fields, F#xdata_field.type /= fixed]}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +set_form(Config, Node, Sid, ArgFields) -> + Xdata = #xdata{type = submit, fields = ArgFields}, + case suite:send_recv(Config, + #iq{type = set, + to = server_jid(Config), + sub_els = + [#adhoc_command{node = Node, + action = complete, + sid = Sid, + xdata = Xdata}]}) + of + #iq{type = result, + sub_els = + [#adhoc_command{node = Node, + action = execute, + status = completed, + sid = Sid, + xdata = #xdata{fields = ResFields}}]} -> + ResFields2 = [F || F <- ResFields, F#xdata_field.type /= fixed], + {ok, ResFields2 -- ArgFields}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + %%%================================== %%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: diff --git a/test/ejabberd_SUITE_data/ejabberd.yml b/test/ejabberd_SUITE_data/ejabberd.yml index e3aa43c3d..0abc35c2a 100644 --- a/test/ejabberd_SUITE_data/ejabberd.yml +++ b/test/ejabberd_SUITE_data/ejabberd.yml @@ -111,6 +111,7 @@ max_fsm_queue: 1000 queue_type: file modules: mod_adhoc: [] + mod_adhoc_api: [] mod_admin_extra: [] mod_admin_update_sql: [] mod_announce: [] From 9bf2d6ce5bea39734599e00df308aa83ae6af3e6 Mon Sep 17 00:00:00 2001 From: Badlop Date: Thu, 6 Mar 2025 20:11:36 +0100 Subject: [PATCH 7/7] mod_configure: Document available alternative API commands --- src/mod_configure.erl | 47 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/mod_configure.erl b/src/mod_configure.erl index 08380a16a..8e4c88405 100644 --- a/src/mod_configure.erl +++ b/src/mod_configure.erl @@ -1546,14 +1546,49 @@ mod_options(_Host) -> %% @format-begin +%% All ad-hoc commands implemented by mod_configure are available as API Commands: +%% - add-user -> register +%% - delete-user -> unregister +%% - end-user-session -> kick_session / kick_user +%% - change-user-password -> change_password +%% - get-user-lastlogin -> get_last +%% - user-stats -> user_sessions_info +%% - get-registered-users-list -> registered_users +%% - get-registered-users-num -> stats +%% - get-online-users-list -> connected_users +%% - get-online-users-num -> stats +%% - stopped nodes -> list_cluster_detailed +%% - DB -> mnesia_list_tables and mnesia_table_change_storage +%% - restart -> stop_kindly / restart +%% - shutdown -> stop_kindly +%% - backup -> backup +%% - restore -> restore +%% - textfile -> dump +%% - import/file -> import_file +%% - import/dir -> import_dir +%% +%% An exclusive feature available only in this module is to list items and discover them: +%% - outgoing s2s +%% - online users +%% - all users + mod_doc() -> #{desc => - ?T("The module provides server configuration functionality via " - "https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]. " - "It also implements many commands as defined in " - "https://xmpp.org/extensions/xep-0133.html[XEP-0133: Service Administration]. " - "This module requires _`mod_adhoc`_ (to execute the commands), " - "and recommends _`mod_disco`_ (to discover the commands). "), + [?T("The module provides server configuration functionalities using " + "https://xmpp.org/extensions/xep-0030.html[XEP-0030: Service Discovery] and " + "https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]:"), + "", + "- List and discover outgoing s2s, online client sessions and all registered accounts", + "- Most of the ad-hoc commands defined in https://xmpp.org/extensions/xep-0133.html[XEP-0133: Service Administration]", + "- Additional custom ad-hoc commands specific to ejabberd", + "", + ?T("This module requires _`mod_adhoc`_ (to execute the commands), " + "and recommends _`mod_disco`_ (to discover the commands). "), + "", + ?T("Please notice that all the ad-hoc commands implemented by this module " + "have an equivalent " + "https://docs.ejabberd.im/developer/ejabberd-api/[API Command] " + "that you can execute using _`mod_adhoc_api`_ or any other API frontend.")], opts => [{access, #{value => ?T("AccessName"),