%%%---------------------------------------------------------------------- %%% File : mod_configure.erl %%% Author : Alexey Shchepin %%% Purpose : Support for online configuration of ejabberd %%% Created : 19 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2020 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. %%% %%%---------------------------------------------------------------------- -module(mod_configure). -author('alexey@process-one.net'). -protocol({xep, 133, '1.1'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, get_local_identity/5, get_local_features/5, get_local_items/5, 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, depends/2, mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_sm.hrl"). -include("translate.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). start(Host, _Opts) -> ejabberd_hooks:add(disco_local_items, Host, ?MODULE, get_local_items, 50), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_local_features, 50), ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, get_local_identity, 50), ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, get_sm_items, 50), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, get_sm_identity, 50), ejabberd_hooks:add(adhoc_local_items, Host, ?MODULE, adhoc_local_items, 50), ejabberd_hooks:add(adhoc_local_commands, Host, ?MODULE, adhoc_local_commands, 50), ejabberd_hooks:add(adhoc_sm_items, Host, ?MODULE, adhoc_sm_items, 50), ejabberd_hooks:add(adhoc_sm_commands, Host, ?MODULE, adhoc_sm_commands, 50), ok. stop(Host) -> ejabberd_hooks:delete(adhoc_sm_commands, Host, ?MODULE, adhoc_sm_commands, 50), ejabberd_hooks:delete(adhoc_sm_items, Host, ?MODULE, adhoc_sm_items, 50), ejabberd_hooks:delete(adhoc_local_commands, Host, ?MODULE, adhoc_local_commands, 50), ejabberd_hooks:delete(adhoc_local_items, Host, ?MODULE, adhoc_local_items, 50), ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, get_sm_identity, 50), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, get_sm_items, 50), ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, get_local_identity, 50), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_local_features, 50), ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, get_local_items, 50). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> [{mod_adhoc, hard}, {mod_last, soft}]. %%%----------------------------------------------------------------------- -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(NODEJID(To, Name, Node), #disco_item{jid = To, name = tr(Lang, Name), node = Node}). -define(NODE(Name, Node), #disco_item{jid = jid:make(Server), node = Node, name = tr(Lang, Name)}). -define(NS_ADMINX(Sub), <<(?NS_ADMIN)/binary, "#", Sub/binary>>). -define(NS_ADMINL(Sub), [<<"http:">>, <<"jabber.org">>, <<"protocol">>, <<"admin">>, Sub]). -spec tokenize(binary()) -> [binary()]. tokenize(Node) -> str:tokens(Node, <<"/#">>). -spec get_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. get_sm_identity(Acc, _From, _To, Node, Lang) -> case Node of <<"config">> -> ?INFO_COMMAND(?T("Configuration"), Lang); _ -> Acc end. -spec get_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. get_local_identity(Acc, _From, _To, Node, Lang) -> LNode = tokenize(Node), case LNode of [<<"running nodes">>, ENode] -> ?INFO_IDENTITY(<<"ejabberd">>, <<"node">>, ENode, Lang); [<<"running nodes">>, _ENode, <<"DB">>] -> ?INFO_COMMAND(?T("Database"), Lang); [<<"running nodes">>, _ENode, <<"backup">>, <<"backup">>] -> ?INFO_COMMAND(?T("Backup"), Lang); [<<"running nodes">>, _ENode, <<"backup">>, <<"restore">>] -> ?INFO_COMMAND(?T("Restore"), Lang); [<<"running nodes">>, _ENode, <<"backup">>, <<"textfile">>] -> ?INFO_COMMAND(?T("Dump to Text File"), Lang); [<<"running nodes">>, _ENode, <<"import">>, <<"file">>] -> ?INFO_COMMAND(?T("Import File"), Lang); [<<"running nodes">>, _ENode, <<"import">>, <<"dir">>] -> ?INFO_COMMAND(?T("Import Directory"), Lang); [<<"running nodes">>, _ENode, <<"restart">>] -> ?INFO_COMMAND(?T("Restart Service"), Lang); [<<"running nodes">>, _ENode, <<"shutdown">>] -> ?INFO_COMMAND(?T("Shut Down Service"), Lang); ?NS_ADMINL(<<"add-user">>) -> ?INFO_COMMAND(?T("Add User"), Lang); ?NS_ADMINL(<<"delete-user">>) -> ?INFO_COMMAND(?T("Delete User"), Lang); ?NS_ADMINL(<<"end-user-session">>) -> ?INFO_COMMAND(?T("End User Session"), Lang); ?NS_ADMINL(<<"get-user-password">>) -> ?INFO_COMMAND(?T("Get User Password"), Lang); ?NS_ADMINL(<<"change-user-password">>) -> ?INFO_COMMAND(?T("Change User Password"), Lang); ?NS_ADMINL(<<"get-user-lastlogin">>) -> ?INFO_COMMAND(?T("Get User Last Login Time"), Lang); ?NS_ADMINL(<<"user-stats">>) -> ?INFO_COMMAND(?T("Get User Statistics"), Lang); ?NS_ADMINL(<<"get-registered-users-num">>) -> ?INFO_COMMAND(?T("Get Number of Registered Users"), Lang); ?NS_ADMINL(<<"get-online-users-num">>) -> ?INFO_COMMAND(?T("Get Number of Online Users"), Lang); _ -> Acc end. %%%----------------------------------------------------------------------- -define(INFO_RESULT(Allow, Feats, Lang), case Allow of deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> {result, Feats} end). -spec get_sm_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). get_sm_features(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> Allow = acl:match_rule(LServer, configure, From), case Node of <<"config">> -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); _ -> Acc end end. -spec get_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). get_local_features(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> LNode = tokenize(Node), Allow = acl:match_rule(LServer, configure, From), case LNode of [<<"config">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"user">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"online users">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"all users">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"all users">>, <<$@, _/binary>>] -> ?INFO_RESULT(Allow, [], Lang); [<<"outgoing s2s">> | _] -> ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"stopped nodes">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode] -> ?INFO_RESULT(Allow, [?NS_STATS], Lang); [<<"running nodes">>, _ENode, <<"DB">>] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"backup">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode, <<"backup">>, _] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"import">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode, <<"import">>, _] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"restart">>] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"shutdown">>] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"config">>, _] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"add-user">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"delete-user">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"end-user-session">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-user-password">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"change-user-password">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-user-lastlogin">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"user-stats">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-registered-users-num">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-online-users-num">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); _ -> Acc end end. %%%----------------------------------------------------------------------- -spec adhoc_sm_items(mod_disco:items_acc(), 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 allow -> Items = case Acc of {result, Its} -> Its; empty -> [] end, Nodes = [#disco_item{jid = To, node = <<"config">>, name = tr(Lang, ?T("Configuration"))}], {result, Items ++ Nodes}; _ -> Acc end. %%%----------------------------------------------------------------------- -spec get_sm_items(mod_disco:items_acc(), jid(), jid(), binary(), binary()) -> mod_disco:items_acc(). get_sm_items(Acc, From, #jid{user = User, server = Server, lserver = LServer} = To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> Items = case Acc of {result, Its} -> Its; empty -> [] end, case {acl:match_rule(LServer, configure, From), Node} of {allow, <<"">>} -> Nodes = [?NODEJID(To, ?T("Configuration"), <<"config">>), ?NODEJID(To, ?T("User Management"), <<"user">>)], {result, Items ++ Nodes ++ get_user_resources(User, Server)}; {allow, <<"config">>} -> {result, []}; {_, <<"config">>} -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; _ -> Acc end end. -spec get_user_resources(binary(), binary()) -> [disco_item()]. get_user_resources(User, Server) -> Rs = ejabberd_sm:get_user_resources(User, Server), lists:map(fun (R) -> #disco_item{jid = jid:make(User, Server, R), name = User} end, lists:sort(Rs)). %%%----------------------------------------------------------------------- -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) -> case acl:match_rule(LServer, configure, From) of allow -> Items = case Acc of {result, Its} -> Its; empty -> [] end, PermLev = get_permission_level(From), Nodes = recursively_get_local_items(PermLev, LServer, <<"">>, Server, Lang), Nodes1 = lists:filter( fun (#disco_item{node = Nd}) -> F = get_local_features(empty, From, To, Nd, Lang), case F of {result, [?NS_COMMANDS]} -> true; _ -> false end end, Nodes), {result, Items ++ Nodes1}; _ -> Acc end. -spec recursively_get_local_items(global | vhost, binary(), binary(), binary(), binary()) -> [disco_item()]. recursively_get_local_items(_PermLev, _LServer, <<"online users">>, _Server, _Lang) -> []; recursively_get_local_items(_PermLev, _LServer, <<"all users">>, _Server, _Lang) -> []; recursively_get_local_items(PermLev, LServer, Node, Server, Lang) -> LNode = tokenize(Node), Items = case get_local_items({PermLev, LServer}, LNode, 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( PermLev, LServer, Nd, Server, Lang)] end end, Items)). -spec get_permission_level(jid()) -> global | vhost. get_permission_level(JID) -> case acl:match_rule(global, configure, JID) of allow -> global; deny -> vhost end. %%%----------------------------------------------------------------------- -define(ITEMS_RESULT(Allow, LNode, Fallback), case Allow of deny -> Fallback; allow -> PermLev = get_permission_level(From), case get_local_items({PermLev, LServer}, LNode, jid:encode(To), Lang) of {result, Res} -> {result, Res}; {error, Error} -> {error, Error} end end). -spec get_local_items(mod_disco:items_acc(), jid(), jid(), binary(), binary()) -> mod_disco:items_acc(). get_local_items(Acc, From, #jid{lserver = LServer} = To, <<"">>, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> Items = case Acc of {result, Its} -> Its; empty -> [] end, Allow = acl:match_rule(LServer, configure, From), case Allow of deny -> {result, Items}; allow -> PermLev = get_permission_level(From), case get_local_items({PermLev, LServer}, [], jid:encode(To), Lang) of {result, Res} -> {result, Items ++ Res}; {error, _Error} -> {result, Items} end end end; get_local_items(Acc, From, #jid{lserver = LServer} = To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> LNode = tokenize(Node), Allow = acl:match_rule(LServer, configure, From), Err = xmpp:err_forbidden(?T("Access denied by service policy"), Lang), case LNode of [<<"config">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"user">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"online users">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"all users">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"all users">>, <<$@, _/binary>>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"outgoing s2s">> | _] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"stopped nodes">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"DB">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"backup">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"backup">>, _] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"import">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"import">>, _] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"restart">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"shutdown">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"config">>, _] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"add-user">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"delete-user">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"end-user-session">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-user-password">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"change-user-password">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-user-lastlogin">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"user-stats">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-registered-users-num">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-online-users-num">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); _ -> Acc end end. %%%----------------------------------------------------------------------- -spec get_local_items({global | vhost, binary()}, [binary()], binary(), binary()) -> {result, [disco_item()]} | {error, stanza_error()}. get_local_items(_Host, [], Server, Lang) -> {result, [?NODE(?T("Configuration"), <<"config">>), ?NODE(?T("User Management"), <<"user">>), ?NODE(?T("Online Users"), <<"online users">>), ?NODE(?T("All Users"), <<"all users">>), ?NODE(?T("Outgoing s2s Connections"), <<"outgoing s2s">>), ?NODE(?T("Running Nodes"), <<"running nodes">>), ?NODE(?T("Stopped Nodes"), <<"stopped nodes">>)]}; get_local_items(_Host, [<<"config">>, _], _Server, _Lang) -> {result, []}; get_local_items(_Host, [<<"user">>], Server, Lang) -> {result, [?NODE(?T("Add User"), (?NS_ADMINX(<<"add-user">>))), ?NODE(?T("Delete User"), (?NS_ADMINX(<<"delete-user">>))), ?NODE(?T("End User Session"), (?NS_ADMINX(<<"end-user-session">>))), ?NODE(?T("Get User Password"), (?NS_ADMINX(<<"get-user-password">>))), ?NODE(?T("Change User Password"), (?NS_ADMINX(<<"change-user-password">>))), ?NODE(?T("Get User Last Login Time"), (?NS_ADMINX(<<"get-user-lastlogin">>))), ?NODE(?T("Get User Statistics"), (?NS_ADMINX(<<"user-stats">>))), ?NODE(?T("Get Number of Registered Users"), (?NS_ADMINX(<<"get-registered-users-num">>))), ?NODE(?T("Get Number of Online Users"), (?NS_ADMINX(<<"get-online-users-num">>)))]}; get_local_items(_Host, [<<"http:">> | _], _Server, _Lang) -> {result, []}; get_local_items({_, Host}, [<<"online users">>], _Server, _Lang) -> {result, get_online_vh_users(Host)}; get_local_items({_, Host}, [<<"all users">>], _Server, _Lang) -> {result, get_all_vh_users(Host)}; get_local_items({_, Host}, [<<"all users">>, <<$@, Diap/binary>>], _Server, _Lang) -> Users = ejabberd_auth:get_users(Host), SUsers = lists:sort([{S, U} || {U, S} <- Users]), try [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>), N1 = binary_to_integer(S1), N2 = binary_to_integer(S2), Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), {result, lists:map( fun({S, U}) -> #disco_item{jid = jid:make(U, S), name = <>} end, Sub)} catch _:_ -> {error, xmpp:err_not_acceptable()} end; get_local_items({_, Host}, [<<"outgoing s2s">>], _Server, Lang) -> {result, get_outgoing_s2s(Host, Lang)}; get_local_items({_, Host}, [<<"outgoing s2s">>, To], _Server, Lang) -> {result, get_outgoing_s2s(Host, Lang, To)}; get_local_items(_Host, [<<"running nodes">>], Server, Lang) -> {result, get_running_nodes(Server, Lang)}; get_local_items(_Host, [<<"stopped nodes">>], _Server, Lang) -> {result, get_stopped_nodes(Lang)}; get_local_items({global, _Host}, [<<"running nodes">>, ENode], Server, Lang) -> {result, [?NODE(?T("Database"), <<"running nodes/", ENode/binary, "/DB">>), ?NODE(?T("Backup Management"), <<"running nodes/", ENode/binary, "/backup">>), ?NODE(?T("Import Users From jabberd14 Spool Files"), <<"running nodes/", ENode/binary, "/import">>), ?NODE(?T("Restart Service"), <<"running nodes/", ENode/binary, "/restart">>), ?NODE(?T("Shut Down Service"), <<"running nodes/", ENode/binary, "/shutdown">>)]}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"DB">>], _Server, _Lang) -> {result, []}; get_local_items(_Host, [<<"running nodes">>, ENode, <<"backup">>], Server, Lang) -> {result, [?NODE(?T("Backup"), <<"running nodes/", ENode/binary, "/backup/backup">>), ?NODE(?T("Restore"), <<"running nodes/", ENode/binary, "/backup/restore">>), ?NODE(?T("Dump to Text File"), <<"running nodes/", ENode/binary, "/backup/textfile">>)]}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"backup">>, _], _Server, _Lang) -> {result, []}; get_local_items(_Host, [<<"running nodes">>, ENode, <<"import">>], Server, Lang) -> {result, [?NODE(?T("Import File"), <<"running nodes/", ENode/binary, "/import/file">>), ?NODE(?T("Import Directory"), <<"running nodes/", ENode/binary, "/import/dir">>)]}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"import">>, _], _Server, _Lang) -> {result, []}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"restart">>], _Server, _Lang) -> {result, []}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"shutdown">>], _Server, _Lang) -> {result, []}; get_local_items(_Host, _, _Server, _Lang) -> {error, xmpp:err_item_not_found()}. -spec get_online_vh_users(binary()) -> [disco_item()]. get_online_vh_users(Host) -> USRs = ejabberd_sm:get_vh_session_list(Host), SURs = lists:sort([{S, U, R} || {U, S, R} <- USRs]), lists:map( fun({S, U, R}) -> #disco_item{jid = jid:make(U, S, R), name = <>} end, SURs). -spec get_all_vh_users(binary()) -> [disco_item()]. get_all_vh_users(Host) -> Users = ejabberd_auth:get_users(Host), SUsers = lists:sort([{S, U} || {U, S} <- Users]), case length(SUsers) of N when N =< 100 -> lists:map(fun({S, U}) -> #disco_item{jid = jid:make(U, S), name = <>} end, SUsers); N -> NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1, M = trunc(N / NParts) + 1, lists:map( fun (K) -> L = K + M - 1, Node = <<"@", (integer_to_binary(K))/binary, "-", (integer_to_binary(L))/binary>>, {FS, FU} = lists:nth(K, SUsers), {LS, LU} = if L < N -> lists:nth(L, SUsers); true -> lists:last(SUsers) end, Name = <>, #disco_item{jid = jid:make(Host), node = <<"all users/", Node/binary>>, name = Name} end, lists:seq(1, N, M)) end. -spec get_outgoing_s2s(binary(), binary()) -> [disco_item()]. get_outgoing_s2s(Host, Lang) -> Connections = ejabberd_s2s:dirty_get_connections(), DotHost = <<".", Host/binary>>, TConns = [TH || {FH, TH} <- Connections, Host == FH orelse str:suffix(DotHost, FH)], lists:map( fun (T) -> Name = str:translate_and_format(Lang, ?T("To ~ts"),[T]), #disco_item{jid = jid:make(Host), node = <<"outgoing s2s/", T/binary>>, name = Name} end, lists:usort(TConns)). -spec get_outgoing_s2s(binary(), binary(), binary()) -> [disco_item()]. get_outgoing_s2s(Host, Lang, To) -> Connections = ejabberd_s2s:dirty_get_connections(), lists:map( fun ({F, _T}) -> Node = <<"outgoing s2s/", To/binary, "/", F/binary>>, Name = str:translate_and_format(Lang, ?T("From ~ts"), [F]), #disco_item{jid = jid:make(Host), node = Node, name = Name} end, lists:keysort( 1, lists:filter(fun (E) -> element(2, E) == To end, Connections))). -spec get_running_nodes(binary(), binary()) -> [disco_item()]. get_running_nodes(Server, _Lang) -> DBNodes = mnesia:system_info(running_db_nodes), lists:map( fun (N) -> S = iolist_to_binary(atom_to_list(N)), #disco_item{jid = jid:make(Server), node = <<"running nodes/", S/binary>>, name = S} end, lists:sort(DBNodes)). -spec get_stopped_nodes(binary()) -> [disco_item()]. get_stopped_nodes(_Lang) -> DBNodes = lists:usort(mnesia:system_info(db_nodes) ++ mnesia:system_info(extra_db_nodes)) -- mnesia:system_info(running_db_nodes), lists:map( fun (N) -> S = iolist_to_binary(atom_to_list(N)), #disco_item{jid = jid:make(ejabberd_config:get_myname()), node = <<"stopped nodes/", S/binary>>, name = S} end, lists:sort(DBNodes)). %%------------------------------------------------------------------------- -define(COMMANDS_RESULT(LServerOrGlobal, From, To, Request, Lang), case acl:match_rule(LServerOrGlobal, configure, From) of deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> adhoc_local_commands(From, To, Request) end). -spec adhoc_local_commands(adhoc_command(), jid(), jid(), adhoc_command()) -> adhoc_command() | {error, stanza_error()}. adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To, #adhoc_command{node = Node, lang = Lang} = Request) -> LNode = tokenize(Node), case LNode of [<<"running nodes">>, _ENode, <<"DB">>] -> ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"backup">>, _] -> ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"import">>, _] -> ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"restart">>] -> ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"shutdown">>] -> ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"config">>, _] -> ?COMMANDS_RESULT(LServer, From, To, Request, Lang); ?NS_ADMINL(_) -> ?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}); {result, Status, Form} -> xmpp_util:make_adhoc_response( Request, #adhoc_command{status = Status, 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. -define(TVFIELD(Type, Var, Val), #xdata_field{type = Type, var = Var, values = [Val]}). -define(HFIELD(), ?TVFIELD(hidden, <<"FORM_TYPE">>, (?NS_ADMIN))). -define(TLFIELD(Type, Label, Var), #xdata_field{type = Type, label = tr(Lang, Label), var = Var}). -define(XFIELD(Type, Label, Var, Val), #xdata_field{type = Type, label = tr(Lang, Label), var = Var, values = [Val]}). -define(XMFIELD(Type, Label, Var, Vals), #xdata_field{type = Type, label = tr(Lang, Label), var = Var, values = Vals}). -define(TABLEFIELD(Table, Val), #xdata_field{ type = 'list-single', label = iolist_to_binary(atom_to_list(Table)), var = iolist_to_binary(atom_to_list(Table)), values = [iolist_to_binary(atom_to_list(Val))], options = [#xdata_option{label = tr(Lang, ?T("RAM copy")), value = <<"ram_copies">>}, #xdata_option{label = tr(Lang, ?T("RAM and disc copy")), value = <<"disc_copies">>}, #xdata_option{label = tr(Lang, ?T("Disc only copy")), value = <<"disc_only_copies">>}, #xdata_option{label = tr(Lang, ?T("Remote copy")), value = <<"unknown">>}]}). -spec get_form(binary(), [binary()], binary()) -> {result, xdata()} | {result, completed, xdata()} | {error, stanza_error()}. get_form(_Host, [<<"running nodes">>, ENode, <<"DB">>], Lang) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case ejabberd_cluster:call(Node, mnesia, system_info, [tables]) of {badrpc, Reason} -> ?ERROR_MSG("RPC call mnesia:system_info(tables) on node " "~ts failed: ~p", [Node, Reason]), {error, xmpp:err_internal_server_error()}; Tables -> STables = lists:sort(Tables), Title = <<(tr(Lang, ?T("Database Tables Configuration at ")))/binary, ENode/binary>>, Instr = tr(Lang, ?T("Choose storage type of tables")), try Fs = lists:map( fun(Table) -> case ejabberd_cluster:call( Node, mnesia, table_info, [Table, storage_type]) of Type when is_atom(Type) -> ?TABLEFIELD(Table, Type) end end, STables), {result, #xdata{title = Title, type = form, instructions = [Instr], fields = [?HFIELD()|Fs]}} catch _:{case_clause, {badrpc, Reason}} -> ?ERROR_MSG("RPC call mnesia:table_info/2 " "on node ~ts failed: ~p", [Node, Reason]), {error, xmpp:err_internal_server_error()} end end end; get_form(_Host, [<<"running nodes">>, ENode, <<"backup">>, <<"backup">>], Lang) -> {result, #xdata{title = <<(tr(Lang, ?T("Backup to File at ")))/binary, ENode/binary>>, type = form, instructions = [tr(Lang, ?T("Enter path to backup file"))], fields = [?HFIELD(), ?XFIELD('text-single', ?T("Path to File"), <<"path">>, <<"">>)]}}; get_form(_Host, [<<"running nodes">>, ENode, <<"backup">>, <<"restore">>], Lang) -> {result, #xdata{title = <<(tr(Lang, ?T("Restore Backup from File at ")))/binary, ENode/binary>>, type = form, instructions = [tr(Lang, ?T("Enter path to backup file"))], fields = [?HFIELD(), ?XFIELD('text-single', ?T("Path to File"), <<"path">>, <<"">>)]}}; get_form(_Host, [<<"running nodes">>, ENode, <<"backup">>, <<"textfile">>], Lang) -> {result, #xdata{title = <<(tr(Lang, ?T("Dump Backup to Text File at ")))/binary, ENode/binary>>, type = form, instructions = [tr(Lang, ?T("Enter path to text file"))], fields = [?HFIELD(), ?XFIELD('text-single', ?T("Path to File"), <<"path">>, <<"">>)]}}; get_form(_Host, [<<"running nodes">>, ENode, <<"import">>, <<"file">>], Lang) -> {result, #xdata{title = <<(tr(Lang, ?T("Import User from File at ")))/binary, ENode/binary>>, type = form, instructions = [tr(Lang, ?T("Enter path to jabberd14 spool file"))], fields = [?HFIELD(), ?XFIELD('text-single', ?T("Path to File"), <<"path">>, <<"">>)]}}; get_form(_Host, [<<"running nodes">>, ENode, <<"import">>, <<"dir">>], Lang) -> {result, #xdata{title = <<(tr(Lang, ?T("Import Users from Dir at ")))/binary, ENode/binary>>, type = form, instructions = [tr(Lang, ?T("Enter path to jabberd14 spool dir"))], fields = [?HFIELD(), ?XFIELD('text-single', ?T("Path to Dir"), <<"path">>, <<"">>)]}}; get_form(_Host, [<<"running nodes">>, _ENode, <<"restart">>], Lang) -> Make_option = fun (LabelNum, LabelUnit, Value) -> #xdata_option{ label = <>, value = Value} end, {result, #xdata{title = tr(Lang, ?T("Restart Service")), type = form, fields = [?HFIELD(), #xdata_field{ type = 'list-single', label = tr(Lang, ?T("Time delay")), var = <<"delay">>, required = true, options = [Make_option(<<"">>, <<"immediately">>, <<"1">>), Make_option(<<"15 ">>, <<"seconds">>, <<"15">>), Make_option(<<"30 ">>, <<"seconds">>, <<"30">>), Make_option(<<"60 ">>, <<"seconds">>, <<"60">>), Make_option(<<"90 ">>, <<"seconds">>, <<"90">>), Make_option(<<"2 ">>, <<"minutes">>, <<"120">>), Make_option(<<"3 ">>, <<"minutes">>, <<"180">>), Make_option(<<"4 ">>, <<"minutes">>, <<"240">>), Make_option(<<"5 ">>, <<"minutes">>, <<"300">>), Make_option(<<"10 ">>, <<"minutes">>, <<"600">>), Make_option(<<"15 ">>, <<"minutes">>, <<"900">>), Make_option(<<"30 ">>, <<"minutes">>, <<"1800">>)]}, #xdata_field{type = fixed, label = tr(Lang, ?T("Send announcement to all online users " "on all hosts"))}, #xdata_field{var = <<"subject">>, type = 'text-single', label = tr(Lang, ?T("Subject"))}, #xdata_field{var = <<"announcement">>, type = 'text-multi', label = tr(Lang, ?T("Message body"))}]}}; get_form(_Host, [<<"running nodes">>, _ENode, <<"shutdown">>], Lang) -> Make_option = fun (LabelNum, LabelUnit, Value) -> #xdata_option{ label = <>, value = Value} end, {result, #xdata{title = tr(Lang, ?T("Shut Down Service")), type = form, fields = [?HFIELD(), #xdata_field{ type = 'list-single', label = tr(Lang, ?T("Time delay")), var = <<"delay">>, required = true, options = [Make_option(<<"">>, <<"immediately">>, <<"1">>), Make_option(<<"15 ">>, <<"seconds">>, <<"15">>), Make_option(<<"30 ">>, <<"seconds">>, <<"30">>), Make_option(<<"60 ">>, <<"seconds">>, <<"60">>), Make_option(<<"90 ">>, <<"seconds">>, <<"90">>), Make_option(<<"2 ">>, <<"minutes">>, <<"120">>), Make_option(<<"3 ">>, <<"minutes">>, <<"180">>), Make_option(<<"4 ">>, <<"minutes">>, <<"240">>), Make_option(<<"5 ">>, <<"minutes">>, <<"300">>), Make_option(<<"10 ">>, <<"minutes">>, <<"600">>), Make_option(<<"15 ">>, <<"minutes">>, <<"900">>), Make_option(<<"30 ">>, <<"minutes">>, <<"1800">>)]}, #xdata_field{type = fixed, label = tr(Lang, ?T("Send announcement to all online users " "on all hosts"))}, #xdata_field{var = <<"subject">>, type = 'text-single', label = tr(Lang, ?T("Subject"))}, #xdata_field{var = <<"announcement">>, type = 'text-multi', label = tr(Lang, ?T("Message body"))}]}}; get_form(_Host, ?NS_ADMINL(<<"add-user">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Add User")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), required = true, var = <<"accountjid">>}, #xdata_field{type = 'text-private', label = tr(Lang, ?T("Password")), required = true, var = <<"password">>}, #xdata_field{type = 'text-private', label = tr(Lang, ?T("Password Verification")), required = true, var = <<"password-verify">>}]}}; get_form(_Host, ?NS_ADMINL(<<"delete-user">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Delete User")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-multi', label = tr(Lang, ?T("Jabber ID")), required = true, var = <<"accountjids">>}]}}; get_form(_Host, ?NS_ADMINL(<<"end-user-session">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("End User Session")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), required = true, var = <<"accountjid">>}]}}; get_form(_Host, ?NS_ADMINL(<<"get-user-password">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Get User Password")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), var = <<"accountjid">>, required = true}]}}; get_form(_Host, ?NS_ADMINL(<<"change-user-password">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Change User Password")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), required = true, var = <<"accountjid">>}, #xdata_field{type = 'text-private', label = tr(Lang, ?T("Password")), required = true, var = <<"password">>}]}}; get_form(_Host, ?NS_ADMINL(<<"get-user-lastlogin">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Get User Last Login Time")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), var = <<"accountjid">>, required = true}]}}; get_form(_Host, ?NS_ADMINL(<<"user-stats">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Get User Statistics")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), var = <<"accountjid">>, required = true}]}}; get_form(Host, ?NS_ADMINL(<<"get-registered-users-num">>), Lang) -> Num = integer_to_binary(ejabberd_auth:count_users(Host)), {result, completed, #xdata{type = form, fields = [?HFIELD(), #xdata_field{type = 'text-single', label = tr(Lang, ?T("Number of registered users")), var = <<"registeredusersnum">>, values = [Num]}]}}; get_form(Host, ?NS_ADMINL(<<"get-online-users-num">>), Lang) -> Num = integer_to_binary(ejabberd_sm:get_vh_session_number(Host)), {result, completed, #xdata{type = form, fields = [?HFIELD(), #xdata_field{type = 'text-single', label = tr(Lang, ?T("Number of online users")), var = <<"onlineusersnum">>, values = [Num]}]}}; 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, [<<"running nodes">>, ENode, <<"DB">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> lists:foreach( fun(#xdata_field{var = SVar, values = SVals}) -> Table = misc:binary_to_atom(SVar), Type = case SVals of [<<"unknown">>] -> unknown; [<<"ram_copies">>] -> ram_copies; [<<"disc_copies">>] -> disc_copies; [<<"disc_only_copies">>] -> disc_only_copies; _ -> false end, if Type == false -> ok; Type == unknown -> mnesia:del_table_copy(Table, Node); true -> case mnesia:add_table_copy(Table, Node, Type) of {aborted, _} -> mnesia:change_table_copy_type( Table, Node, Type); _ -> ok end end end, XData#xdata.fields), {result, undefined} end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"backup">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case xmpp_util:get_xdata_values(<<"path">>, XData) of [] -> Txt = ?T("No 'path' found in data form"), {error, xmpp:err_bad_request(Txt, Lang)}; [String] -> case ejabberd_cluster:call( Node, mnesia, backup, [binary_to_list(String)], timer:minutes(10)) of {badrpc, Reason} -> ?ERROR_MSG("RPC call mnesia:backup(~ts) to node ~ts " "failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; {error, Reason} -> ?ERROR_MSG("RPC call mnesia:backup(~ts) to node ~ts " "failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; _ -> {result, undefined} end; _ -> Txt = ?T("Incorrect value of 'path' in data form"), {error, xmpp:err_bad_request(Txt, Lang)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"restore">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case xmpp_util:get_xdata_values(<<"path">>, XData) of [] -> Txt = ?T("No 'path' found in data form"), {error, xmpp:err_bad_request(Txt, Lang)}; [String] -> case ejabberd_cluster:call( Node, ejabberd_admin, restore, [String], timer:minutes(10)) of {badrpc, Reason} -> ?ERROR_MSG("RPC call ejabberd_admin:restore(~ts) to node " "~ts failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; {error, Reason} -> ?ERROR_MSG("RPC call ejabberd_admin:restore(~ts) to node " "~ts failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; _ -> {result, undefined} end; _ -> Txt = ?T("Incorrect value of 'path' in data form"), {error, xmpp:err_bad_request(Txt, Lang)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"textfile">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case xmpp_util:get_xdata_values(<<"path">>, XData) of [] -> Txt = ?T("No 'path' found in data form"), {error, xmpp:err_bad_request(Txt, Lang)}; [String] -> case ejabberd_cluster:call( Node, ejabberd_admin, dump_to_textfile, [String], timer:minutes(10)) of {badrpc, Reason} -> ?ERROR_MSG("RPC call ejabberd_admin:dump_to_textfile(~ts) " "to node ~ts failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; {error, Reason} -> ?ERROR_MSG("RPC call ejabberd_admin:dump_to_textfile(~ts) " "to node ~ts failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; _ -> {result, undefined} end; _ -> Txt = ?T("Incorrect value of 'path' in data form"), {error, xmpp:err_bad_request(Txt, Lang)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"import">>, <<"file">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case xmpp_util:get_xdata_values(<<"path">>, XData) of [] -> Txt = ?T("No 'path' found in data form"), {error, xmpp:err_bad_request(Txt, Lang)}; [String] -> ejabberd_cluster:call(Node, jd2ejd, import_file, [String]), {result, undefined}; _ -> Txt = ?T("Incorrect value of 'path' in data form"), {error, xmpp:err_bad_request(Txt, Lang)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"import">>, <<"dir">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case xmpp_util:get_xdata_values(<<"path">>, XData) of [] -> Txt = ?T("No 'path' found in data form"), {error, xmpp:err_bad_request(Txt, Lang)}; [String] -> ejabberd_cluster:call(Node, jd2ejd, import_dir, [String]), {result, undefined}; _ -> Txt = ?T("Incorrect value of 'path' in data form"), {error, xmpp:err_bad_request(Txt, Lang)} end end; set_form(From, Host, [<<"running nodes">>, ENode, <<"restart">>], _Lang, XData) -> stop_node(From, Host, ENode, restart, XData); set_form(From, Host, [<<"running nodes">>, ENode, <<"shutdown">>], _Lang, XData) -> stop_node(From, Host, ENode, stop, XData); set_form(From, Host, ?NS_ADMINL(<<"add-user">>), _Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), Password = get_value(<<"password">>, XData), Password = get_value(<<"password-verify">>, XData), AccountJID = jid:decode(AccountString), User = AccountJID#jid.luser, Server = AccountJID#jid.lserver, true = lists:member(Server, ejabberd_option:hosts()), true = Server == Host orelse get_permission_level(From) == global, case ejabberd_auth:try_register(User, Server, Password) of ok -> {result, undefined}; {error, exists} -> {error, xmpp:err_conflict()}; {error, not_allowed} -> {error, xmpp:err_not_allowed()} end; set_form(From, Host, ?NS_ADMINL(<<"delete-user">>), _Lang, XData) -> AccountStringList = get_values(<<"accountjids">>, XData), [_ | _] = AccountStringList, ASL2 = lists:map(fun (AccountString) -> JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse get_permission_level(From) == global, true = ejabberd_auth:user_exists(User, Server), {User, Server} end, AccountStringList), [ejabberd_auth:remove_user(User, Server) || {User, Server} <- ASL2], {result, undefined}; set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>), _Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), JID = jid:decode(AccountString), LServer = JID#jid.lserver, true = LServer == Host orelse get_permission_level(From) == global, case JID#jid.lresource of <<>> -> ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver); R -> ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver, R) end, {result, undefined}; set_form(From, Host, ?NS_ADMINL(<<"get-user-password">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse get_permission_level(From) == global, Password = ejabberd_auth:get_password(User, Server), true = is_binary(Password), {result, #xdata{type = form, fields = [?HFIELD(), ?XFIELD('jid-single', ?T("Jabber ID"), <<"accountjid">>, AccountString), ?XFIELD('text-single', ?T("Password"), <<"password">>, Password)]}}; set_form(From, Host, ?NS_ADMINL(<<"change-user-password">>), _Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), Password = get_value(<<"password">>, XData), JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse get_permission_level(From) == global, true = ejabberd_auth:user_exists(User, Server), ejabberd_auth:set_password(User, Server, Password), {result, undefined}; set_form(From, Host, ?NS_ADMINL(<<"get-user-lastlogin">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse get_permission_level(From) == global, FLast = case ejabberd_sm:get_user_resources(User, Server) of [] -> case get_last_info(User, Server) of not_found -> tr(Lang, ?T("Never")); {ok, Timestamp, _Status} -> Shift = Timestamp, TimeStamp = {Shift div 1000000, Shift rem 1000000, 0}, {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(TimeStamp), (str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second])) end; _ -> tr(Lang, ?T("Online")) end, {result, #xdata{type = form, fields = [?HFIELD(), ?XFIELD('jid-single', ?T("Jabber ID"), <<"accountjid">>, AccountString), ?XFIELD('text-single', ?T("Last login"), <<"lastlogin">>, FLast)]}}; set_form(From, Host, ?NS_ADMINL(<<"user-stats">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse get_permission_level(From) == global, Resources = ejabberd_sm:get_user_resources(User, Server), IPs1 = [ejabberd_sm:get_user_ip(User, Server, Resource) || Resource <- Resources], IPs = [<<(misc:ip_to_list(IP))/binary, ":", (integer_to_binary(Port))/binary>> || {IP, Port} <- IPs1], Items = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]), Rostersize = integer_to_binary(erlang:length(Items)), {result, #xdata{type = form, fields = [?HFIELD(), ?XFIELD('jid-single', ?T("Jabber ID"), <<"accountjid">>, AccountString), ?XFIELD('text-single', ?T("Roster size"), <<"rostersize">>, Rostersize), ?XMFIELD('text-multi', ?T("IP addresses"), <<"ipaddresses">>, IPs), ?XMFIELD('text-multi', ?T("Resources"), <<"onlineresources">>, Resources)]}}; set_form(_From, _Host, _, _Lang, _XData) -> {error, xmpp:err_service_unavailable()}. -spec get_value(binary(), xdata()) -> binary(). get_value(Field, XData) -> hd(get_values(Field, XData)). -spec get_values(binary(), xdata()) -> [binary()]. get_values(Field, XData) -> xmpp_util:get_xdata_values(Field, XData). -spec search_running_node(binary()) -> false | node(). search_running_node(SNode) -> search_running_node(SNode, mnesia:system_info(running_db_nodes)). -spec search_running_node(binary(), [node()]) -> false | node(). search_running_node(_, []) -> false; search_running_node(SNode, [Node | Nodes]) -> case atom_to_binary(Node, utf8) of SNode -> Node; _ -> search_running_node(SNode, Nodes) end. -spec stop_node(jid(), binary(), binary(), restart | stop, xdata()) -> {result, undefined}. stop_node(From, Host, ENode, Action, XData) -> Delay = binary_to_integer(get_value(<<"delay">>, XData)), Subject = case get_values(<<"subject">>, XData) of [] -> []; [S|_] -> [#xdata_field{var = <<"subject">>, values = [S]}] end, Announcement = case get_values(<<"announcement">>, XData) of [] -> []; As -> [#xdata_field{var = <<"body">>, values = As}] end, case Subject ++ Announcement of [] -> ok; Fields -> Request = #adhoc_command{node = ?NS_ADMINX(<<"announce-allhosts">>), action = complete, xdata = #xdata{type = submit, fields = Fields}}, To = jid:make(Host), mod_announce:announce_commands(empty, From, To, Request) end, Time = timer:seconds(Delay), Node = misc:binary_to_atom(ENode), {ok, _} = timer:apply_after(Time, ejabberd_cluster, call, [Node, init, Action, []]), {result, undefined}. -spec get_last_info(binary(), binary()) -> {ok, non_neg_integer(), binary()} | not_found. get_last_info(User, Server) -> case gen_mod:is_loaded(Server, mod_last) of true -> mod_last:get_last_info(User, Server); false -> not_found end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec adhoc_sm_commands(adhoc_command(), jid(), jid(), adhoc_command()) -> adhoc_command() | {error, stanza_error()}. 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 deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> ActionIsExecute = Action == execute orelse Action == complete, if Action == cancel -> xmpp_util:make_adhoc_response( Request, #adhoc_command{status = canceled}); XData == undefined, ActionIsExecute -> case get_sm_form(User, Server, <<"config">>, Lang) of {result, Form} -> xmpp_util:make_adhoc_response( Request, #adhoc_command{status = executing, xdata = Form}); {error, Error} -> {error, Error} end; XData /= undefined, ActionIsExecute -> set_sm_form(User, Server, <<"config">>, Request); true -> Txt = ?T("Unexpected action"), {error, xmpp:err_bad_request(Txt, Lang)} end end; adhoc_sm_commands(Acc, _From, _To, _Request) -> Acc. -spec get_sm_form(binary(), binary(), binary(), binary()) -> {result, xdata()} | {error, stanza_error()}. get_sm_form(User, Server, <<"config">>, Lang) -> {result, #xdata{type = form, title = <<(tr(Lang, ?T("Administration of ")))/binary, User/binary>>, fields = [?HFIELD(), #xdata_field{ type = 'list-single', label = tr(Lang, ?T("Action on user")), var = <<"action">>, values = [<<"edit">>], options = [#xdata_option{ label = tr(Lang, ?T("Edit Properties")), value = <<"edit">>}, #xdata_option{ label = tr(Lang, ?T("Remove User")), value = <<"remove">>}]}, ?XFIELD('text-private', ?T("Password"), <<"password">>, ejabberd_auth:get_password_s(User, Server))]}}; get_sm_form(_User, _Server, _Node, _Lang) -> {error, xmpp:err_service_unavailable()}. -spec set_sm_form(binary(), binary(), binary(), adhoc_command()) -> adhoc_command() | {error, stanza_error()}. set_sm_form(User, Server, <<"config">>, #adhoc_command{lang = Lang, node = Node, sid = SessionID, xdata = XData}) -> Response = #adhoc_command{lang = Lang, node = Node, sid = SessionID, status = completed}, case xmpp_util:get_xdata_values(<<"action">>, XData) of [<<"edit">>] -> case xmpp_util:get_xdata_values(<<"password">>, XData) of [Password] -> ejabberd_auth:set_password(User, Server, Password), xmpp_util:make_adhoc_response(Response); _ -> Txt = ?T("No 'password' found in data form"), {error, xmpp:err_not_acceptable(Txt, Lang)} end; [<<"remove">>] -> ejabberd_auth:remove_user(User, Server), xmpp_util:make_adhoc_response(Response); _ -> Txt = ?T("Incorrect value of 'action' in data form"), {error, xmpp:err_not_acceptable(Txt, Lang)} end; set_sm_form(_User, _Server, _Node, _Request) -> {error, xmpp:err_service_unavailable()}. -spec tr(binary(), binary()) -> binary(). tr(Lang, Text) -> translate:translate(Lang, Text). mod_options(_) -> []. mod_doc() -> #{desc => ?T("The module provides server configuration functionality via " "https://xmpp.org/extensions/xep-0050.html" "[XEP-0050: Ad-Hoc Commands]. This module requires " "'mod_adhoc' to be loaded.")}.