%%%---------------------------------------------------------------------- %%% File : ejabberd_web_admin.erl %%% Author : Alexey Shchepin %%% Purpose : Administration web interface %%% Created : 9 Apr 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2024 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 -module(ejabberd_web_admin). -author('alexey@process-one.net'). -export([process/2, pretty_print_xml/1, make_command/2, make_command/4, make_command_raw_value/3, make_table/2, make_table/4, term_to_id/1, id_to_term/1]). %% Internal commands -export([webadmin_host_last_activity/3, webadmin_node_db_table_page/3]). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_commands.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("logger.hrl"). -include("translate.hrl"). -define(INPUTATTRS(Type, Name, Value, Attrs), ?XA(<<"input">>, (Attrs ++ [{<<"type">>, Type}, {<<"name">>, Name}, {<<"value">>, Value}]))). %%%================================== %%%% get_acl_access -spec get_acl_rule(Path::[binary()], 'GET' | 'POST') -> {HostOfRule::binary(), [AccessRule::atom()]}. %% All accounts can access those URLs get_acl_rule([], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"style.css">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"logo.png">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"favicon.ico">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"additions.js">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"sortable.min.css">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"sortable.min.js">>], _) -> {<<"localhost">>, [all]}; %% This page only displays vhosts that the user is admin: get_acl_rule([<<"vhosts">>], _) -> {<<"localhost">>, [all]}; %% The pages of a vhost are only accessible if the user is admin of that vhost: get_acl_rule([<<"server">>, VHost | _RPath], Method) when Method =:= 'GET' orelse Method =:= 'HEAD' -> {VHost, [configure]}; get_acl_rule([<<"server">>, VHost | _RPath], 'POST') -> {VHost, [configure]}; %% Default rule: only global admins can access any other random page get_acl_rule(_RPath, Method) when Method =:= 'GET' orelse Method =:= 'HEAD' -> {global, [configure]}; get_acl_rule(_RPath, 'POST') -> {global, [configure]}. %%%================================== %%%% Menu Items Access get_jid(Auth, HostHTTP, Method) -> case get_auth_admin(Auth, HostHTTP, [], Method) of {ok, {User, Server}} -> jid:make(User, Server); {unauthorized, Error} -> ?ERROR_MSG("Unauthorized ~p: ~p", [Auth, Error]), throw({unauthorized, Auth}) end. get_menu_items(global, cluster, Lang, JID, Level) -> {_Base, _, Items} = make_server_menu([], [], Lang, JID, Level), lists:map(fun ({URI, Name}) -> {<>, Name}; ({URI, Name, _SubMenu}) -> {<>, Name} end, Items); get_menu_items(Host, cluster, Lang, JID, Level) -> {_Base, _, Items} = make_host_menu(Host, [], [], Lang, JID, Level), lists:map(fun ({URI, Name}) -> {<>, Name}; ({URI, Name, _SubMenu}) -> {<>, Name} end, Items). %% get_menu_items(Host, Node, Lang, JID) -> %% {Base, _, Items} = make_host_node_menu(Host, Node, Lang, JID), %% lists:map( %% fun({URI, Name}) -> %% {Base++URI++"/", Name}; %% ({URI, Name, _SubMenu}) -> %% {Base++URI++"/", Name} %% end, %% Items %% ). is_allowed_path(global, RPath, JID) -> is_allowed_path([], RPath, JID); is_allowed_path(Host, RPath, JID) when is_binary(Host) -> is_allowed_path([<<"server">>, Host], RPath, JID); is_allowed_path(BasePath, {Path, _}, JID) -> is_allowed_path(BasePath ++ [Path], JID); is_allowed_path(BasePath, {Path, _, _}, JID) -> is_allowed_path(BasePath ++ [Path], JID). is_allowed_path([<<"admin">> | Path], JID) -> is_allowed_path(Path, JID); is_allowed_path(Path, JID) -> {HostOfRule, AccessRule} = get_acl_rule(Path, 'GET'), any_rules_allowed(HostOfRule, AccessRule, JID). %%%================================== %%%% process/2 process(Path, #request{raw_path = RawPath} = Request) -> Continue = case Path of [E] -> binary:match(E, <<".">>) /= nomatch; _ -> false end, case Continue orelse binary:at(RawPath, size(RawPath) - 1) == $/ of true -> process2(Path, Request); _ -> {301, [{<<"Location">>, <>}], <<>>} end. process2([<<"logout">> | _], #request{lang = Lang}) -> Text = [?XCT(<<"h1">>, ?T("Logged Out")), ?XE(<<"p">>, [?C(<<"Your web browser has logout from WebAdmin. Close this window, or login again in: ">>), ?AC(<<"../">>, <<"ejabberd WebAdmin">>)])], {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"ejabberd\"">>}], ejabberd_web:make_xhtml(Text)}; process2([<<"server">>, SHost | RPath] = Path, #request{auth = Auth, lang = Lang, host = HostHTTP, method = Method} = Request) -> Host = jid:nameprep(SHost), case ejabberd_router:is_my_host(Host) of true -> case get_auth_admin(Auth, HostHTTP, Path, Method) of {ok, {User, Server}} -> AJID = get_jid(Auth, HostHTTP, Method), process_admin(Host, Request#request{path = RPath, us = {User, Server}}, AJID); {unauthorized, <<"no-auth-provided">>} -> {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"ejabberd\"">>}], ejabberd_web:make_xhtml(make_unauthorized(Lang))}; {unauthorized, Error} -> {BadUser, _BadPass} = Auth, {IPT, _Port} = Request#request.ip, IPS = ejabberd_config:may_hide_data(misc:ip_to_list(IPT)), ?WARNING_MSG("Access of ~p from ~p failed with error: ~p", [BadUser, IPS, Error]), {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"auth error, retry login " "to ejabberd\"">>}], ejabberd_web:make_xhtml(make_unauthorized(Lang))} end; false -> ejabberd_web:error(not_found) end; process2(RPath, #request{auth = Auth, lang = Lang, host = HostHTTP, method = Method} = Request) -> case get_auth_admin(Auth, HostHTTP, RPath, Method) of {ok, {User, Server}} -> AJID = get_jid(Auth, HostHTTP, Method), process_admin(global, Request#request{path = RPath, us = {User, Server}}, AJID); {unauthorized, <<"no-auth-provided">>} -> {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"ejabberd\"">>}], ejabberd_web:make_xhtml(make_unauthorized(Lang))}; {unauthorized, Error} -> {BadUser, _BadPass} = Auth, {IPT, _Port} = Request#request.ip, IPS = ejabberd_config:may_hide_data(misc:ip_to_list(IPT)), ?WARNING_MSG("Access of ~p from ~p failed with error: ~p", [BadUser, IPS, Error]), {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"auth error, retry login " "to ejabberd\"">>}], ejabberd_web:make_xhtml(make_unauthorized(Lang))} end. make_unauthorized(Lang) -> [?XCT(<<"h1">>, ?T("Unauthorized")), ?XE(<<"p">>, [?C(<<"There was some problem authenticating, or the account doesn't have privilege.">>)]), ?XE(<<"p">>, [?C(<<"Please check the log file for a more precise error message.">>)])]. get_auth_admin(Auth, HostHTTP, RPath, Method) -> case Auth of {SJID, Pass} -> {HostOfRule, AccessRule} = get_acl_rule(RPath, Method), try jid:decode(SJID) of #jid{user = <<"">>, server = User} -> case ejabberd_router:is_my_host(HostHTTP) of true -> get_auth_account(HostOfRule, AccessRule, User, HostHTTP, Pass); _ -> {unauthorized, <<"missing-server">>} end; #jid{user = User, server = Server} -> get_auth_account(HostOfRule, AccessRule, User, Server, Pass) catch _:{bad_jid, _} -> {unauthorized, <<"badformed-jid">>} end; invalid -> {unauthorized, <<"no-auth-provided">>}; undefined -> {unauthorized, <<"no-auth-provided">>} end. get_auth_account(HostOfRule, AccessRule, User, Server, Pass) -> case lists:member(Server, ejabberd_config:get_option(hosts)) of true -> get_auth_account2(HostOfRule, AccessRule, User, Server, Pass); false -> {unauthorized, <<"inexistent-host">>} end. get_auth_account2(HostOfRule, AccessRule, User, Server, Pass) -> case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of true -> case any_rules_allowed(HostOfRule, AccessRule, jid:make(User, Server)) of false -> {unauthorized, <<"unprivileged-account">>}; true -> {ok, {User, Server}} end; false -> case ejabberd_auth:user_exists(User, Server) of true -> {unauthorized, <<"bad-password">>}; false -> {unauthorized, <<"inexistent-account">>} end end. %%%================================== %%%% make_xhtml make_xhtml(Els, Host, Request, JID, Level) -> make_xhtml(Els, Host, cluster, unspecified, Request, JID, Level). make_xhtml(Els, Host, Username, Request, JID, Level) when (Username == unspecified) or (is_binary(Username)) -> make_xhtml(Els, Host, cluster, Username, Request, JID, Level); make_xhtml(Els, Host, Node, Request, JID, Level) -> make_xhtml(Els, Host, Node, unspecified, Request, JID, Level). -spec make_xhtml([xmlel()], Host::global | binary(), Node::cluster | atom(), Username::unspecified | binary(), Request::http_request(), jid(), Level::integer()) -> {200, [html], xmlel()}. make_xhtml(Els, Host, Node, Username, #request{lang = Lang} = R, JID, Level) -> Base = get_base_path_sum(0, 0, Level), MenuItems = make_navigation(Host, Node, Username, Lang, JID, Level) ++ make_login_items(R, Level), {200, [html], #xmlel{name = <<"html">>, attrs = [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}, {<<"xml:lang">>, Lang}, {<<"lang">>, Lang}]++direction(Lang), children = [#xmlel{name = <<"head">>, attrs = [], children = [?XCT(<<"title">>, ?T("ejabberd Web Admin")), #xmlel{name = <<"meta">>, attrs = [{<<"http-equiv">>, <<"Content-Type">>}, {<<"content">>, <<"text/html; charset=utf-8">>}], children = []}, #xmlel{name = <<"script">>, attrs = [{<<"src">>, <>}, {<<"type">>, <<"text/javascript">>}], children = [?C(<<" ">>)]}, #xmlel{name = <<"link">>, attrs = [{<<"href">>, <>}, {<<"type">>, <<"image/x-icon">>}, {<<"rel">>, <<"shortcut icon">>}], children = []}, #xmlel{name = <<"link">>, attrs = [{<<"href">>, <>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}], children = []}, #xmlel{name = <<"link">>, attrs = [{<<"href">>, <>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}], children = []}, #xmlel{name = <<"script">>, attrs = [{<<"src">>, <>}, {<<"type">>, <<"text/javascript">>}], children = [?C(<<" ">>)]}]}, ?XE(<<"body">>, [?XAE(<<"div">>, [{<<"id">>, <<"container">>}], [?XAE(<<"div">>, [{<<"id">>, <<"header">>}], [?XE(<<"h1">>, [?ACT(Base, <<"ejabberd Web Admin">>)])]), ?XAE(<<"div">>, [{<<"id">>, <<"navigation">>}], [?XE(<<"ul">>, MenuItems)]), ?XAE(<<"div">>, [{<<"id">>, <<"content">>}], Els), ?XAE(<<"div">>, [{<<"id">>, <<"clearcopyright">>}], [{xmlcdata, <<"">>}])]), ?XAE(<<"div">>, [{<<"id">>, <<"copyrightouter">>}], [?XAE(<<"div">>, [{<<"id">>, <<"copyright">>}], [?XE(<<"p">>, [?AC(<<"https://www.ejabberd.im/">>, <<"ejabberd">>), ?C(<<" ">>), ?C(ejabberd_option:version()), ?C(<<" (c) 2002-2024 ">>), ?AC(<<"https://www.process-one.net/">>, <<"ProcessOne, leader in messaging and push solutions">>)] )])])])]}}. direction(<<"he">>) -> [{<<"dir">>, <<"rtl">>}]; direction(_) -> []. get_base_path(Host, Node, Level) -> SumHost = case Host of global -> 0; _ -> -2 end, SumNode = case Node of cluster -> 0; _ -> -2 end, get_base_path_sum(SumHost, SumNode, Level). get_base_path_sum(SumHost, SumNode, Level) -> iolist_to_binary(lists:duplicate(Level + SumHost + SumNode, "../")). %%%================================== %%%% css & images additions_js() -> case misc:read_js("admin.js") of {ok, JS} -> JS; {error, _} -> <<>> end. css(Host) -> case misc:read_css("admin.css") of {ok, CSS} -> Base = get_base_path(Host, cluster, 0), re:replace(CSS, <<"@BASE@">>, Base, [{return, binary}]); {error, _} -> <<>> end. favicon() -> case misc:read_img("favicon.png") of {ok, ICO} -> ICO; {error, _} -> <<>> end. logo() -> case misc:read_img("admin-logo.png") of {ok, Img} -> Img; {error, _} -> <<>> end. sortable_css() -> case misc:read_css("sortable.min.css") of {ok, CSS} -> CSS; {error, _} -> <<>> end. sortable_js() -> case misc:read_js("sortable.min.js") of {ok, JS} -> JS; {error, _} -> <<>> end. %%%================================== %%%% process_admin process_admin(global, #request{path = [], lang = Lang} = Request, AJID) -> Title = ?H1GLraw(<<"">>, <<"">>, <<"home">>), MenuItems = get_menu_items(global, cluster, Lang, AJID, 0), Disclaimer = maybe_disclaimer_not_admin(MenuItems, AJID, Lang), WelcomeText = [?BR, ?XAE(<<"p">>, [{<<"align">>, <<"center">>}], [?XA(<<"img">>, [{<<"src">>, <<"logo.png">>}, {<<"style">>, <<"border-radius:10px; background:#49cbc1; padding: 1.1em;">>}]) ]) ] ++ Title ++ [ ?XAE(<<"blockquote">>, [{<<"id">>, <<"welcome">>}], [?XC(<<"p">>, <<"Welcome to ejabberd's WebAdmin!">>), ?XC(<<"p">>, <<"Browse the menu to navigate your XMPP virtual hosts, " "Erlang nodes, and other global server pages...">>), ?XC(<<"p">>, <<"Some pages have a link in the top right corner " "to relevant documentation in ejabberd Docs.">>), ?X(<<"hr">>), ?XE(<<"p">>, [?C(<<"Many pages use ejabberd's API commands to show information " "and to allow you perform administrative tasks. " "Click on a command name to view its details. " "You can also execute those same API commands " "using other interfaces, see: ">>), ?AC(<<"https://docs.ejabberd.im/developer/ejabberd-api/">>, <<"ejabberd Docs: API">>) ]), ?XC(<<"p">>, <<"For example, this is the 'stats' command, " "it accepts an argument and returns an integer:">>), make_command(stats, Request)]), ?BR], make_xhtml(Disclaimer ++ WelcomeText ++ [?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- MenuItems])], global, Request, AJID, 0); process_admin(Host, #request{path = [], lang = Lang} = R, AJID) -> make_xhtml([?XCT(<<"h1">>, ?T("Administration")), ?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- get_menu_items(Host, cluster, Lang, AJID, 2)])], Host, R, AJID, 2); process_admin(Host, #request{path = [<<"style.css">>]}, _) -> {200, [{<<"Content-Type">>, <<"text/css">>}, last_modified(), cache_control_public()], css(Host)}; process_admin(_Host, #request{path = [<<"favicon.ico">>]}, _) -> {200, [{<<"Content-Type">>, <<"image/x-icon">>}, last_modified(), cache_control_public()], favicon()}; process_admin(_Host, #request{path = [<<"logo.png">>]}, _) -> {200, [{<<"Content-Type">>, <<"image/png">>}, last_modified(), cache_control_public()], logo()}; process_admin(_Host, #request{path = [<<"additions.js">>]}, _) -> {200, [{<<"Content-Type">>, <<"text/javascript">>}, last_modified(), cache_control_public()], additions_js()}; process_admin(_Host, #request{path = [<<"sortable.min.css">>]}, _) -> {200, [{<<"Content-Type">>, <<"text/css">>}, last_modified(), cache_control_public()], sortable_css()}; process_admin(_Host, #request{path = [<<"sortable.min.js">>]}, _) -> {200, [{<<"Content-Type">>, <<"text/javascript">>}, last_modified(), cache_control_public()], sortable_js()}; %% @format-begin process_admin(global, #request{path = [<<"vhosts">> | RPath], lang = Lang} = R, AJID) -> Hosts = case make_command_raw_value(registered_vhosts, R, []) of Hs when is_list(Hs) -> Hs; _ -> {User, Server} = R#request.us, ?INFO_MSG("Access to WebAdmin page vhosts/ for account ~s@~s was denied", [User, Server]), [] end, Level = 1 + length(RPath), HostsAllowed = [Host || Host <- Hosts, can_user_access_host(Host, R)], Table = make_table(20, RPath, [<<"host">>, {<<"registered users">>, right}, {<<"online users">>, right}], [{make_command(echo, R, [{<<"sentence">>, Host}], [{only, value}, {result_links, [{sentence, host, Level, <<"">>}]}]), make_command(stats_host, R, [{<<"name">>, <<"registeredusers">>}, {<<"host">>, Host}], [{only, value}, {result_links, [{stat, arg_host, Level, <<"users/">>}]}]), make_command(stats_host, R, [{<<"name">>, <<"onlineusers">>}, {<<"host">>, Host}], [{only, value}, {result_links, [{stat, arg_host, Level, <<"online-users/">>}]}])} || Host <- HostsAllowed]), VhostsElements = [make_command(registered_vhosts, R, [], [{only, presentation}]), make_command(stats_host, R, [], [{only, presentation}]), ?XE(<<"blockquote">>, [Table])], make_xhtml(?H1GL(translate:translate(Lang, ?T("Virtual Hosts")), <<"basic/#xmpp-domains">>, ?T("XMPP Domains")) ++ VhostsElements, global, R, AJID, Level); process_admin(Host, #request{path = [<<"users">>, <<"diapason">>, Diap | RPath], lang = Lang} = R, AJID) when is_binary(Host) -> Level = 5 + length(RPath), RegisterEl = make_command(register, R, [{<<"host">>, Host}], []), Res = list_users_in_diapason(Host, Level, 30, RPath, R, Diap, RegisterEl), make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, R, AJID, Level); process_admin(Host, #request{path = [<<"users">>, <<"top">>, Attribute | RPath], lang = Lang} = R, AJID) when is_binary(Host) -> Level = 5 + length(RPath), RegisterEl = make_command(register, R, [{<<"host">>, Host}], []), Res = list_users_top(Host, Level, 30, RPath, R, Attribute, RegisterEl), make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, R, AJID, Level); process_admin(Host, #request{path = [<<"users">> | RPath], lang = Lang} = R, AJID) when is_binary(Host) -> Level = 3 + length(RPath), RegisterEl = make_command(register, R, [{<<"host">>, Host}], []), Res = list_users(Host, Level, 30, RPath, R, RegisterEl), make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, R, AJID, Level); process_admin(Host, #request{path = [<<"online-users">> | RPath], lang = Lang} = R, AJID) when is_binary(Host) -> Level = 3 + length(RPath), Res = [make_command(connected_users_vhost, R, [{<<"host">>, Host}], [{table_options, {100, RPath}}, {result_links, [{sessions, user, Level, <<"">>}]}])], make_xhtml([?XCT(<<"h1">>, ?T("Online Users"))] ++ Res, Host, R, AJID, Level); process_admin(Host, #request{path = [<<"last-activity">>], q = Query, lang = Lang} = R, AJID) when is_binary(Host) -> PageH1 = ?H1GL(translate:translate(Lang, ?T("Users Last Activity")), <<"modules/#mod_last">>, <<"mod_last">>), Res = make_command(webadmin_host_last_activity, R, [{<<"host">>, Host}, {<<"query">>, Query}, {<<"lang">>, Lang}], []), make_xhtml(PageH1 ++ [Res], Host, R, AJID, 3); process_admin(Host, #request{path = [<<"user">>, U], lang = Lang} = R, AJID) -> case ejabberd_auth:user_exists(U, Host) of true -> Res = user_info(U, Host, R), make_xhtml(Res, Host, U, R, AJID, 4); false -> make_xhtml([?XCT(<<"h1">>, ?T("Not Found"))], Host, R, AJID, 4) end; process_admin(Host, #request{path = [<<"nodes">>]} = R, AJID) -> Level = case Host of global -> 1; _ -> 3 end, Res = ?H1GLraw(<<"Nodes">>, <<"admin/guide/clustering/">>, <<"Clustering">>) ++ [make_command(list_cluster, R, [], [{result_links, [{node, node, 1, <<"">>}]}])], make_xhtml(Res, Host, R, AJID, Level); process_admin(Host, #request{path = [<<"node">>, SNode | NPath], lang = Lang} = Request, AJID) -> case search_running_node(SNode) of false -> make_xhtml([?XCT(<<"h1">>, ?T("Node not found"))], Host, Request, AJID, 2); Node -> Res = get_node(Host, Node, NPath, Request#request{path = NPath}), Level = case Host of global -> 2 + length(NPath); _ -> 4 + length(NPath) end, make_xhtml(Res, Host, Node, Request, AJID, Level) end; %%%================================== %%%% process_admin default case process_admin(Host, #request{path = Path} = Request, AJID) -> {Username, RPath} = case Path of [<<"user">>, U | UPath] -> {U, UPath}; _ -> {unspecified, Path} end, Request2 = Request#request{path = RPath}, Res = case {Host, Username} of {global, _} -> ejabberd_hooks:run_fold(webadmin_page_main, Host, [], [Request2]); {_, unspecified} -> ejabberd_hooks:run_fold(webadmin_page_host, Host, [], [Host, Request2]); {_Host, Username} -> ejabberd_hooks:run_fold(webadmin_page_hostuser, Host, [], [Host, Username, Request2]) end, Level = case Host of global -> length(Request#request.path); _ -> 2 + length(Request#request.path) end, case Res of [] -> setelement(1, make_xhtml([?XC(<<"h1">>, <<"Not Found">>)], Host, Request, AJID, Level), 404); _ -> make_xhtml(Res, Host, Username, Request, AJID, Level) end. %% @format-end term_to_id([]) -> <<>>; term_to_id(T) -> base64:encode((term_to_binary(T))). id_to_term(<<>>) -> []; id_to_term(I) -> binary_to_term(base64:decode(I)). can_user_access_host(Host, #request{auth = Auth, host = HostHTTP, method = Method}) -> Path = [<<"server">>, Host], case get_auth_admin(Auth, HostHTTP, Path, Method) of {ok, _} -> true; {unauthorized, _Error} -> false end. %%%================================== %%%% list_vhosts list_vhosts_allowed(JID) -> Hosts = ejabberd_option:hosts(), lists:filter(fun (Host) -> any_rules_allowed(Host, [configure], JID) end, Hosts). maybe_disclaimer_not_admin(MenuItems, AJID, _Lang) -> case {MenuItems, list_vhosts_allowed(AJID)} of {[_], []} -> [?BR, ?DIVRES([?C(<<"Apparently your account has no administration rights in " "this server. Please check how to grant admin rights: ">>), ?AC(<<"https://docs.ejabberd.im/admin/install/next-steps/#administration-account">>, <<"ejabberd Docs: Administration Account">>)]) ]; _ -> [] end. %%%================================== %%%% list_users %% @format-begin list_users(Host, Level, PageSize, RPath, R, RegisterEl) -> Usernames = case make_command_raw_value(registered_users, R, [{<<"host">>, Host}]) of As when is_list(As) -> As; _ -> {Aser, Aerver} = R#request.us, ?INFO_MSG("Access to WebAdmin page users/ for account ~s@~s was denied", [Aser, Aerver]), [] end, case length(Usernames) of N when N =< 10 -> list_users(Host, Level, PageSize, RPath, R, Usernames, RegisterEl); N when N > 10 -> list_users_diapason(Host, R, Usernames, N, RegisterEl) end. list_users(Host, Level, PageSize, RPath, R, Usernames, RegisterEl) -> Columns = [<<"user">>, {<<"offline">>, right}, {<<"roster">>, right}, {<<"timestamp">>, left}, {<<"status">>, left}], Rows = [{make_command(echo, R, [{<<"sentence">>, jid:encode( jid:make(Username, Host))}], [{only, raw_and_value}, {result_links, [{sentence, user, Level, <<"">>}]}]), make_command(get_offline_count, R, [{<<"user">>, Username}, {<<"host">>, Host}], [{only, raw_and_value}, {result_links, [{value, arg_host, Level, <<"user/", Username/binary, "/queue/">>}]}]), make_command(get_roster_count, R, [{<<"user">>, Username}, {<<"host">>, Host}], [{only, raw_and_value}, {result_links, [{value, arg_host, Level, <<"user/", Username/binary, "/roster/">>}]}]), ?C(element(1, make_command_raw_value(get_last, R, [{<<"user">>, Username}, {<<"host">>, Host}]))), ?C(element(2, make_command_raw_value(get_last, R, [{<<"user">>, Username}, {<<"host">>, Host}])))} || Username <- Usernames], [RegisterEl, make_command(registered_users, R, [], [{only, presentation}]), make_command(get_offline_count, R, [], [{only, presentation}]), make_command(get_roster_count, R, [], [{only, presentation}]), make_command(get_last, R, [], [{only, presentation}]), make_table(PageSize, RPath, Columns, Rows)]. list_users_diapason(Host, R, Usernames, N, RegisterEl) -> URLFunc = fun url_func/1, SUsers = [{Host, U} || U <- Usernames], NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1, M = trunc(N / NParts) + 1, FUsers = lists:flatmap(fun(K) -> L = K + M - 1, Last = if L < N -> su_to_list(lists:nth(L, SUsers)); true -> su_to_list(lists:last(SUsers)) end, Name = <<(su_to_list(lists:nth(K, SUsers)))/binary, $\s, 226, 128, 148, $\s, Last/binary>>, [?AC(URLFunc({user_diapason, K, L}), Name), ?BR] end, lists:seq(1, N, M)), [RegisterEl, make_command(get_offline_count, R, [], [{only, presentation}]), ?AC(<<"top/offline/">>, <<"View Top Offline Queues">>), make_command(get_roster_count, R, [], [{only, presentation}]), ?AC(<<"top/roster/">>, <<"View Top Rosters">>), make_command(get_last, R, [], [{only, presentation}]), ?AC(<<"top/last/">>, <<"View Top-Oldest Last Activity">>), make_command(registered_users, R, [], [{only, presentation}])] ++ FUsers. list_users_in_diapason(Host, Level, PageSize, RPath, R, Diap, RegisterEl) -> Usernames = case make_command_raw_value(registered_users, R, [{<<"host">>, Host}]) of As when is_list(As) -> As; _ -> {Aser, Aerver} = R#request.us, ?INFO_MSG("Access to WebAdmin page users/ for account ~s@~s was denied", [Aser, Aerver]), [] end, SUsers = lists:sort([{Host, U} || U <- Usernames]), [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>), N1 = binary_to_integer(S1), N2 = binary_to_integer(S2), Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), Usernames2 = [U || {_, U} <- Sub], list_users(Host, Level, PageSize, RPath, R, Usernames2, RegisterEl). list_users_top(Host, Level, PageSize, RPath, R, Operation, RegisterEl) -> Usernames = case make_command_raw_value(registered_users, R, [{<<"host">>, Host}]) of As when is_list(As) -> As; _ -> {Aser, Aerver} = R#request.us, ?INFO_MSG("Access to WebAdmin page users/ for account ~s@~s was denied", [Aser, Aerver]), [] end, {Command, Reverse} = case Operation of <<"roster">> -> {get_roster_count, true}; <<"offline">> -> {get_offline_count, true}; <<"last">> -> {get_last, false} end, UsernamesCounts = [{U, make_command(Command, R, [{<<"user">>, U}, {<<"host">>, Host}], [{only, raw_value}, {result_links, [{value, arg_host, Level, <<"user/", U/binary, "/roster/">>}]}])} || U <- Usernames], USorted = lists:keysort(2, UsernamesCounts), UReversed = case Reverse of true -> lists:reverse(USorted); false -> USorted end, Usernames2 = [U || {U, _} <- lists:sublist(UReversed, 100)], list_users(Host, Level, PageSize, RPath, R, Usernames2, RegisterEl). get_lastactivity_menuitem_list(Server) -> case gen_mod:is_loaded(Server, mod_last) of true -> case mod_last_opt:db_type(Server) of mnesia -> [{<<"last-activity">>, ?T("Last Activity")}]; _ -> [] end; false -> [] end. us_to_list({User, Server}) -> jid:encode({User, Server, <<"">>}). su_to_list({Server, User}) -> jid:encode({User, Server, <<"">>}). %% @format-end %%%================================== %%%% last-activity webadmin_host_last_activity(Host, Query, Lang) -> ?DEBUG("Query: ~p", [Query]), Month = case lists:keysearch(<<"period">>, 1, Query) of {value, {_, Val}} -> Val; _ -> <<"month">> end, Res = case lists:keysearch(<<"ordinary">>, 1, Query) of {value, {_, _}} -> list_last_activity(Host, Lang, false, Month); _ -> list_last_activity(Host, Lang, true, Month) end, [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?CT(?T("Period: ")), ?XAE(<<"select">>, [{<<"name">>, <<"period">>}], (lists:map(fun ({O, V}) -> Sel = if O == Month -> [{<<"selected">>, <<"selected">>}]; true -> [] end, ?XAC(<<"option">>, (Sel ++ [{<<"value">>, O}]), V) end, [{<<"month">>, translate:translate(Lang, ?T("Last month"))}, {<<"year">>, translate:translate(Lang, ?T("Last year"))}, {<<"all">>, translate:translate(Lang, ?T("All activity"))}]))), ?C(<<" ">>), ?INPUTT(<<"submit">>, <<"ordinary">>, ?T("Show Ordinary Table")), ?C(<<" ">>), ?INPUTT(<<"submit">>, <<"integral">>, ?T("Show Integral Table"))])] ++ Res. %%%================================== %%%% get_stats user_info(User, Server, #request{q = Query, lang = Lang} = R) -> LServer = jid:nameprep(Server), US = {jid:nodeprep(User), LServer}, Res = user_parse_query(User, Server, Query), UserItems = ejabberd_hooks:run_fold(webadmin_user, LServer, [], [User, Server, R]), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("User ~ts"), [us_to_list(US)])))] ++ case Res of ok -> [?XREST(?T("Submitted"))]; error -> [?XREST(?T("Bad format"))]; nothing -> [] end ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], ([make_command(user_sessions_info, R, [{<<"user">>, User}, {<<"host">>, Server}], [{result_links, [{node, node, 4, <<>>}]}]), make_command(change_password, R, [{<<"user">>, User}, {<<"host">>, Server}], [{style, danger}]), make_command(get_last, R, [{<<"user">>, User}, {<<"host">>, Server}], []), make_command(set_last, R, [{<<"user">>, User}, {<<"host">>, Server}], [])] ++ UserItems ++ [?P, make_command(unregister, R, [{<<"user">>, User}, {<<"host">>, Server}], [{style, danger}]) ]))]. user_parse_query(User, Server, Query) -> lists:foldl(fun ({Action, _Value}, Acc) when Acc == nothing -> user_parse_query1(Action, User, Server, Query); ({_Action, _Value}, Acc) -> Acc end, nothing, Query). user_parse_query1(Action, User, Server, Query) -> case ejabberd_hooks:run_fold(webadmin_user_parse_query, Server, [], [Action, User, Server, Query]) of [] -> nothing; Res -> Res end. list_last_activity(Host, Lang, Integral, Period) -> TimeStamp = erlang:system_time(second), case Period of <<"all">> -> TS = 0, Days = infinity; <<"year">> -> TS = TimeStamp - 366 * 86400, Days = 366; _ -> TS = TimeStamp - 31 * 86400, Days = 31 end, case catch mnesia:dirty_select(last_activity, [{{last_activity, {'_', Host}, '$1', '_'}, [{'>', '$1', TS}], [{trunc, {'/', {'-', TimeStamp, '$1'}, 86400}}]}]) of {'EXIT', _Reason} -> []; Vals -> Hist = histogram(Vals, Integral), if Hist == [] -> [?CT(?T("No Data"))]; true -> Left = if Days == infinity -> 0; true -> Days - length(Hist) end, Tail = if Integral -> lists:duplicate(Left, lists:last(Hist)); true -> lists:duplicate(Left, 0) end, Max = lists:max(Hist), [?XAE(<<"ol">>, [{<<"id">>, <<"lastactivity">>}, {<<"start">>, <<"0">>}], [?XAE(<<"li">>, [{<<"style">>, <<"width:", (integer_to_binary(trunc(90 * V / Max)))/binary, "%;">>}], [{xmlcdata, pretty_string_int(V)}]) || V <- Hist ++ Tail])] end end. histogram(Values, Integral) -> histogram(lists:sort(Values), Integral, 0, 0, []). histogram([H | T], Integral, Current, Count, Hist) when Current == H -> histogram(T, Integral, Current, Count + 1, Hist); histogram([H | _] = Values, Integral, Current, Count, Hist) when Current < H -> if Integral -> histogram(Values, Integral, Current + 1, Count, [Count | Hist]); true -> histogram(Values, Integral, Current + 1, 0, [Count | Hist]) end; histogram([], _Integral, _Current, Count, Hist) -> if Count > 0 -> lists:reverse([Count | Hist]); true -> lists:reverse(Hist) end. %%%================================== %%%% get_nodes search_running_node(SNode) -> RunningNodes = ejabberd_cluster:get_nodes(), search_running_node(SNode, RunningNodes). search_running_node(_, []) -> false; search_running_node(SNode, [Node | Nodes]) -> case iolist_to_binary(atom_to_list(Node)) of SNode -> Node; _ -> search_running_node(SNode, Nodes) end. get_node(global, Node, [], #request{lang = Lang}) -> Base = get_base_path(global, Node, 2), BaseItems = [{<<"db">>, <<"Mnesia Tables">>}, {<<"backup">>, <<"Mnesia Backup">>}], MenuItems = make_menu_items(global, Node, Base, Lang, BaseItems), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Node ~p"), [Node])))] ++ [?XE(<<"ul">>, MenuItems)]; get_node(Host, Node, [], #request{lang = Lang}) -> Base = get_base_path(Host, Node, 4), MenuItems2 = make_menu_items(Host, Node, Base, Lang, []), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Node ~p"), [Node]))), ?XE(<<"ul">>, MenuItems2)]; get_node(global, Node, [<<"db">> | RPath], R) -> PageTitle = <<"Mnesia Tables">>, Title = ?XC(<<"h1">>, PageTitle), Level = length(RPath), [Title, ?BR | webadmin_db(Node, RPath, R, Level)]; get_node(global, Node, [<<"backup">>], #request{lang = Lang} = R) -> Types = [{<<"#binary">>, <<"Binary">>}, {<<"#plaintext">>, <<"Plain Text">>}, {<<"#piefxis">>, <<"PIEXFIS (XEP-0227)">>}, {<<"#sql">>, <<"SQL">>}, {<<"#prosody">>, <<"Prosody">>}, {<<"#jabberd14">>, <<"jabberd 1.4">>}], [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Backup of ~p"), [Node]))), ?XCT(<<"p">>, ?T("Please note that these options will " "only backup the builtin Mnesia database. " "If you are using the ODBC module, you " "also need to backup your SQL database " "separately.")), ?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- Types]), ?X(<<"hr">>), ?XAC(<<"h2">>, [{<<"id">>, <<"binary">>}], <<"Binary">>), ?XCT(<<"p">>, ?T("Store binary backup:")), ?XE(<<"blockquote">>, [make_command(backup, R)]), ?XCT(<<"p">>, ?T("Restore binary backup immediately:")), ?XE(<<"blockquote">>, [make_command(restore, R, [], [{style, danger}])]), ?XCT(<<"p">>, ?T("Restore binary backup after next ejabberd " "restart (requires less memory):")), ?XE(<<"blockquote">>, [make_command(install_fallback, R, [], [{style, danger}])]), ?X(<<"hr">>), ?XAC(<<"h2">>, [{<<"id">>, <<"plaintext">>}], <<"Plain Text">>), ?XCT(<<"p">>, ?T("Store plain text backup:")), ?XE(<<"blockquote">>, [make_command(dump, R)]), ?XCT(<<"p">>, ?T("Restore plain text backup immediately:")), ?XE(<<"blockquote">>, [make_command(load, R, [], [{style, danger}])]), ?X(<<"hr">>), ?XAC(<<"h2">>, [{<<"id">>, <<"piefxis">>}], <<"PIEFXIS (XEP-0227)">>), ?XCT(<<"p">>, ?T("Import users data from a PIEFXIS file (XEP-0227):")), ?XE(<<"blockquote">>, [make_command(import_piefxis, R)]), ?XCT(<<"p">>, ?T("Export data of all users in the server to PIEFXIS files (XEP-0227):")), ?XE(<<"blockquote">>, [make_command(export_piefxis, R)]), ?XCT(<<"p">>, ?T("Export data of users in a host to PIEFXIS files (XEP-0227):")), ?XE(<<"blockquote">>, [make_command(export_piefxis_host, R)]), ?X(<<"hr">>), ?XAC(<<"h2">>, [{<<"id">>, <<"sql">>}], <<"SQL">>), ?XCT(<<"p">>, ?T("Export all tables as SQL queries to a file:")), ?XE(<<"blockquote">>, [make_command(export2sql, R)]), ?X(<<"hr">>), ?XAC(<<"h2">>, [{<<"id">>, <<"prosody">>}], <<"Prosody">>), ?XCT(<<"p">>, <<"Import data from Prosody:">>), ?XE(<<"blockquote">>, [make_command(import_prosody, R)]), ?X(<<"hr">>), ?XAC(<<"h2">>, [{<<"id">>, <<"jabberd14">>}], <<"jabberd 1.4">>), ?XCT(<<"p">>, ?T("Import user data from jabberd14 spool file:")), ?XE(<<"blockquote">>, [make_command(import_file, R)]), ?XCT(<<"p">>, ?T("Import users data from jabberd14 spool directory:")), ?XE(<<"blockquote">>, [make_command(import_dir, R)]) ]; get_node(Host, Node, _NPath, Request) -> Res = case Host of global -> ejabberd_hooks:run_fold(webadmin_page_node, Host, [], [Node, Request]); _ -> ejabberd_hooks:run_fold(webadmin_page_hostnode, Host, [], [Host, Node, Request]) end, case Res of [] -> [?XC(<<"h1">>, <<"Not Found">>)]; _ -> Res end. %%%================================== %%%% node parse pretty_print_xml(El) -> list_to_binary(pretty_print_xml(El, <<"">>)). pretty_print_xml({xmlcdata, CData}, Prefix) -> IsBlankCData = lists:all( fun($\f) -> true; ($\r) -> true; ($\n) -> true; ($\t) -> true; ($\v) -> true; ($ ) -> true; (_) -> false end, binary_to_list(CData)), if IsBlankCData -> []; true -> [Prefix, CData, $\n] end; pretty_print_xml(#xmlel{name = Name, attrs = Attrs, children = Els}, Prefix) -> [Prefix, $<, Name, case Attrs of [] -> []; [{Attr, Val} | RestAttrs] -> AttrPrefix = [Prefix, str:copies(<<" ">>, byte_size(Name) + 2)], [$\s, Attr, $=, $', fxml:crypt(Val) | [$', lists:map(fun ({Attr1, Val1}) -> [$\n, AttrPrefix, Attr1, $=, $', fxml:crypt(Val1), $'] end, RestAttrs)]] end, if Els == [] -> <<"/>\n">>; true -> OnlyCData = lists:all(fun ({xmlcdata, _}) -> true; (#xmlel{}) -> false end, Els), if OnlyCData -> [$>, fxml:get_cdata(Els), $<, $/, Name, $>, $\n]; true -> [$>, $\n, lists:map(fun (E) -> pretty_print_xml(E, [Prefix, <<" ">>]) end, Els), Prefix, $<, $/, Name, $>, $\n] end end]. url_func({user_diapason, From, To}) -> <<"diapason/", (integer_to_binary(From))/binary, "-", (integer_to_binary(To))/binary, "/">>. last_modified() -> {<<"Last-Modified">>, <<"Mon, 25 Feb 2008 13:23:30 GMT">>}. cache_control_public() -> {<<"Cache-Control">>, <<"public">>}. %% Transform 1234567890 into "1,234,567,890" pretty_string_int(Integer) when is_integer(Integer) -> pretty_string_int(integer_to_binary(Integer)); pretty_string_int(String) when is_binary(String) -> {_, Result} = lists:foldl(fun (NewNumber, {3, Result}) -> {1, <>}; (NewNumber, {CountAcc, Result}) -> {CountAcc + 1, <>} end, {0, <<"">>}, lists:reverse(binary_to_list(String))), Result. %%%================================== %%%% mnesia table view webadmin_node_db_table_page(Node, STable, PageNumber) -> Table = misc:binary_to_atom(STable), TInfo = ejabberd_cluster:call(Node, mnesia, table_info, [Table, all]), {value, {storage_type, Type}} = lists:keysearch(storage_type, 1, TInfo), {value, {size, Size}} = lists:keysearch(size, 1, TInfo), PageSize = 500, TableContentErl = get_table_content(Node, Table, Type, PageNumber, PageSize), TableContent = str:format("~p", [TableContentErl]), PagesLinks = build_elements_pages_list(Size, PageNumber, PageSize), [?P] ++ PagesLinks ++ [?XC(<<"pre">>, TableContent)]. build_elements_pages_list(Size, PageNumber, PageSize) -> PagesNumber = calculate_pages_number(Size, PageSize), PagesSeq = lists:seq(1, PagesNumber), PagesList = [?AC(<<"../", (integer_to_binary(N))/binary, "/">>, <<(integer_to_binary(N))/binary, " ">>) || N <- PagesSeq], lists:keyreplace( [?C(<<(integer_to_binary(PageNumber))/binary, " ">>)], 4, PagesList, ?C(<<" [", (integer_to_binary(PageNumber))/binary, "] ">>)). calculate_pages_number(Size, PageSize) -> Remainder = case Size rem PageSize of 0 -> 0; _ -> 1 end, case (Size div PageSize) + Remainder of 1 -> 0; Res -> Res end. get_table_content(Node, Table, _Type, PageNumber, PageSize) -> Keys1 = lists:sort(ejabberd_cluster:call(Node, mnesia, dirty_all_keys, [Table])), FirstKeyPos = 1 - PageSize + PageNumber*PageSize, Keys = lists:sublist(Keys1, FirstKeyPos, PageSize), Res = [ejabberd_cluster:call(Node, mnesia, dirty_read, [Table, Key]) || Key <- Keys], lists:flatten(Res). %% @format-begin webadmin_db(Node, [<<"table">>, TableName, <<"details">> | RPath], R, Level) -> Service = <<"Mnesia Tables">>, Breadcrumb = make_breadcrumb({table_section, Level, Service, TableName, <<"Details">>, RPath}), Get = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [mnesia_table_details, R, [{<<"table">>, TableName}], []])], Breadcrumb ++ Get; webadmin_db(Node, [<<"table">>, TableName, <<"elements">>, PageNumber | RPath], R, Level) -> Service = <<"Mnesia Tables">>, Breadcrumb = make_breadcrumb({table_section, Level, Service, TableName, <<"Elements">>, RPath}), Get = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [webadmin_node_db_table_page, R, [{<<"node">>, Node}, {<<"table">>, TableName}, {<<"page">>, PageNumber}], []])], Breadcrumb ++ Get; webadmin_db(Node, [<<"table">>, TableName, <<"change-storage">> | RPath], R, Level) -> Service = <<"Mnesia Tables">>, Breadcrumb = make_breadcrumb({table_section, Level, Service, TableName, <<"Change Storage">>, RPath}), Set = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [mnesia_table_change_storage, R, [{<<"table">>, TableName}], []])], Breadcrumb ++ Set; webadmin_db(Node, [<<"table">>, TableName, <<"clear">> | RPath], R, Level) -> Service = <<"Mnesia Tables">>, Breadcrumb = make_breadcrumb({table_section, Level, Service, TableName, <<"Clear Content">>, RPath}), Set = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [mnesia_table_clear, R, [{<<"table">>, TableName}], [{style, danger}]])], Breadcrumb ++ Set; webadmin_db(Node, [<<"table">>, TableName, <<"destroy">> | RPath], R, Level) -> Service = <<"Mnesia Tables">>, Breadcrumb = make_breadcrumb({table_section, Level, Service, TableName, <<"Destroy Table">>, RPath}), Set = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [mnesia_table_destroy, R, [{<<"table">>, TableName}], [{style, danger}]])], Breadcrumb ++ Set; webadmin_db(_Node, [<<"table">>, TableName | _RPath], _R, Level) -> Service = <<"Mnesia Tables">>, Breadcrumb = make_breadcrumb({table, Level, Service, TableName}), MenuItems = [{<<"details/">>, <<"Details">>}, {<<"elements/1/">>, <<"Elements">>}, {<<"change-storage/">>, <<"Change Storage">>}, {<<"clear/">>, <<"Clear Content">>}, {<<"destroy/">>, <<"Destroy Table">>}], Get = [?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- MenuItems])], Breadcrumb ++ Get; webadmin_db(Node, _RPath, R, _Level) -> Service = <<"Mnesia Tables">>, Breadcrumb = make_breadcrumb({service, Service}), Get = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [mnesia_list_tables, R, [], [{result_links, [{name, mnesia_table, 3, <<"">>}]}]])], Breadcrumb ++ Get. make_breadcrumb({service, Service}) -> make_breadcrumb([Service]); make_breadcrumb({table, Level, Service, Name}) -> make_breadcrumb([{Level, Service}, separator, Name]); make_breadcrumb({table_section, Level, Service, Name, Section, RPath}) -> make_breadcrumb([{Level, Service}, separator, {Level - 2, Name}, separator, Section | RPath]); make_breadcrumb(Elements) -> lists:map(fun ({xmlel, _, _, _} = Xmlel) -> Xmlel; (<<"sort">>) -> ?C(<<" +">>); (<<"page">>) -> ?C(<<" #">>); (separator) -> ?C(<<" > ">>); (Bin) when is_binary(Bin) -> ?C(Bin); ({Level, Bin}) when is_integer(Level) and is_binary(Bin) -> ?AC(binary:copy(<<"../">>, Level), Bin) end, Elements). %% @format-end %%%================================== %%%% navigation menu make_navigation(Host, Node, Username, Lang, JID, Level) -> Menu = make_navigation_menu(Host, Node, Username, Lang, JID, Level), make_menu_items(Lang, Menu). -spec make_navigation_menu(Host::global | binary(), Node::cluster | atom(), Username::unspecified | binary(), Lang::binary(), JID::jid(), Level::integer()) -> Menu::{URL::binary(), Title::binary()} | {URL::binary(), Title::binary(), [Menu::any()]}. make_navigation_menu(Host, Node, Username, Lang, JID, Level) -> HostNodeMenu = make_host_node_menu(Host, Node, Lang, JID, Level), HostUserMenu = make_host_user_menu(Host, Username, Lang, JID, Level), HostMenu = make_host_menu(Host, HostNodeMenu, HostUserMenu, Lang, JID, Level), NodeMenu = make_node_menu(Host, Node, Lang, Level), make_server_menu(HostMenu, NodeMenu, Lang, JID, Level). make_menu_items(Host, Node, Base, Lang, Acc) -> Place = case {Host, Node} of {global, cluster} -> server; {global, Node} -> {node, Node}; {Host, cluster} -> {host, Host}; {Host, Node} -> {hostnode, Host, Node} end, HookItems = get_menu_items_hook(Place, Lang), Items = lists:keysort(2, HookItems ++ Acc), make_menu_items(Lang, {Base, <<"">>, Items}). make_host_node_menu(global, _, _Lang, _JID, _Level) -> {<<"">>, <<"">>, []}; make_host_node_menu(_, cluster, _Lang, _JID, _Level) -> {<<"">>, <<"">>, []}; make_host_node_menu(Host, Node, Lang, JID, Level) -> HostNodeBase = get_base_path(Host, Node, Level), HostNodeFixed = get_menu_items_hook({hostnode, Host, Node}, Lang), HostNodeFixed2 = [Tuple || Tuple <- HostNodeFixed, is_allowed_path(Host, Tuple, JID)], {HostNodeBase, iolist_to_binary(atom_to_list(Node)), lists:keysort(2, HostNodeFixed2)}. make_host_user_menu(global, _, _Lang, _JID, _Level) -> {<<"">>, <<"">>, []}; make_host_user_menu(_, unspecified, _Lang, _JID, _Level) -> {<<"">>, <<"">>, []}; make_host_user_menu(Host, Username, Lang, JID, Level) -> HostNodeBase = get_base_path(Host, Username, Level), HostNodeFixed = get_menu_items_hook({hostuser, Host, Username}, Lang), HostNodeFixed2 = [Tuple || Tuple <- HostNodeFixed, is_allowed_path(Host, Tuple, JID)], {HostNodeBase, Username, lists:keysort(2, HostNodeFixed2)}. make_host_menu(global, _HostNodeMenu, _HostUserMenu, _Lang, _JID, _Level) -> {<<"">>, <<"">>, []}; make_host_menu(Host, HostNodeMenu, HostUserMenu, Lang, JID, Level) -> HostBase = get_base_path(Host, cluster, Level), HostFixed = [{<<"users">>, ?T("Users"), HostUserMenu}, {<<"online-users">>, ?T("Online Users")}], HostFixedAdditional = get_lastactivity_menuitem_list(Host) ++ [{<<"nodes">>, ?T("Nodes"), HostNodeMenu}] ++ get_menu_items_hook({host, Host}, Lang), HostFixedAll = HostFixed ++ lists:keysort(2, HostFixedAdditional), HostFixed2 = [Tuple || Tuple <- HostFixedAll, is_allowed_path(Host, Tuple, JID)], {HostBase, Host, HostFixed2}. make_node_menu(_Host, cluster, _Lang, _Level) -> {<<"">>, <<"">>, []}; make_node_menu(global, Node, Lang, Level) -> NodeBase = get_base_path(global, Node, Level), NodeFixed = [{<<"db">>, <<"Mnesia Tables">>}, {<<"backup">>, <<"Mnesia Backup">>}] ++ get_menu_items_hook({node, Node}, Lang), {NodeBase, iolist_to_binary(atom_to_list(Node)), lists:keysort(2, NodeFixed)}; make_node_menu(_Host, _Node, _Lang, _Level) -> {<<"">>, <<"">>, []}. make_server_menu(HostMenu, NodeMenu, Lang, JID, Level) -> Base = get_base_path(global, cluster, Level), Fixed = [{<<"vhosts">>, ?T("Virtual Hosts"), HostMenu}, {<<"nodes">>, ?T("Nodes"), NodeMenu}], FixedAdditional = get_menu_items_hook(server, Lang), FixedAll = Fixed ++ lists:keysort(2, FixedAdditional), Fixed2 = [Tuple || Tuple <- FixedAll, is_allowed_path(global, Tuple, JID)], {Base, <<"">>, Fixed2}. get_menu_items_hook({hostnode, Host, Node}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_hostnode, Host, [], [Host, Node, Lang]); get_menu_items_hook({hostuser, Host, Username}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_hostuser, Host, [], [Host, Username, Lang]); get_menu_items_hook({host, Host}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_host, Host, [], [Host, Lang]); get_menu_items_hook({node, Node}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_node, [], [Node, Lang]); get_menu_items_hook(server, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_main, [], [Lang]). -spec make_menu_items(Lang::binary(), {MURI::binary(), MName::binary(), Items::[{IURI::binary(), IName::binary()} | {IURI::binary(), IName::binary(), Menu::any()}]}) -> [xmlel()]. make_menu_items(Lang, Menu) -> lists:reverse(make_menu_items2(Lang, 1, Menu)). make_menu_items2(Lang, Deep, {MURI, MName, _} = Menu) -> Res = case MName of <<"">> -> []; _ -> [make_menu_item(header, Deep, MURI, MName, Lang)] end, make_menu_items2(Lang, Deep, Menu, Res). make_menu_items2(_, _Deep, {_, _, []}, Res) -> Res; make_menu_items2(Lang, Deep, {MURI, MName, [Item | Items]}, Res) -> Res2 = case Item of {IURI, IName} -> [make_menu_item(item, Deep, <>, IName, Lang) | Res]; {IURI, IName, SubMenu} -> ResTemp = [make_menu_item(item, Deep, <>, IName, Lang) | Res], ResSubMenu = make_menu_items2(Lang, Deep + 1, SubMenu), ResSubMenu ++ ResTemp end, make_menu_items2(Lang, Deep, {MURI, MName, Items}, Res2). make_menu_item(header, 1, URI, Name, _Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navhead">>}], [?AC(URI, Name)])]); make_menu_item(header, 2, URI, Name, _Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navheadsub">>}], [?AC(URI, Name)])]); make_menu_item(header, 3, URI, Name, _Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navheadsubsub">>}], [?AC(URI, Name)])]); make_menu_item(item, 1, URI, Name, Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navitem">>}], [?ACT(URI, Name)])]); make_menu_item(item, 2, URI, Name, Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navitemsub">>}], [?ACT(URI, Name)])]); make_menu_item(item, 3, URI, Name, Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navitemsubsub">>}], [?ACT(URI, Name)])]). any_rules_allowed(Host, Access, Entity) -> lists:any( fun(Rule) -> allow == acl:match_rule(Host, Rule, Entity) end, Access). %%%================================== %%%% login box %%% @format-begin make_login_items(#request{us = {Username, Host}} = R, Level) -> UserBin = jid:encode( jid:make(Username, Host, <<"">>)), UserEl = make_command(echo, R, [{<<"sentence">>, UserBin}], [{only, value}, {result_links, [{sentence, user, Level, <<"">>}]}]), UserEl2 = case UserEl of {xmlcdata, <<>>} -> {xmlel, <<"code">>, [], [{xmlel, <<"a">>, [], [{xmlcdata, UserBin}]}]}; _ -> UserEl end, [{xmlel, <<"li">>, [], [{xmlel, <<"div">>, [{<<"id">>, <<"navitemlogin">>}], [?XE(<<"ul">>, [?LI([?C(unicode:characters_to_binary("👤")), UserEl2]), ?LI([?C(unicode:characters_to_binary("🏭")), make_command(echo, R, [{<<"sentence">>, misc:atom_to_binary(node())}], [{only, value}, {result_links, [{sentence, node, Level, <<"">>}]}])]), ?LI([?C(unicode:characters_to_binary("📤")), ?AC(<<(binary:copy(<<"../">>, Level))/binary, "logout/">>, <<"Logout">>)])])]}]}]. %%%================================== %%%% make_command: API -spec make_command(Name :: atom(), Request :: http_request()) -> xmlel(). make_command(Name, Request) -> make_command2(Name, Request, [], []). -spec make_command(Name :: atom(), Request :: http_request(), BaseArguments :: [{ArgName :: binary(), ArgValue :: binary()}], [Option]) -> xmlel() | {xmlcdata, binary()} | {raw_and_value, any(), xmlel()} when Option :: {only, presentation | without_presentation | button | result | value | raw_and_value} | {input_name_append, [binary()]} | {force_execution, boolean()} | {table_options, {PageSize :: integer(), RemainingPath :: [binary()]}} | {result_named, boolean()} | {result_links, [{ResultName :: atom(), LinkType :: host | node | user | room | shared_roster | arg_host | paragraph, Level :: integer(), Append :: binary()}]} | {style, normal | danger}. make_command(Name, Request, BaseArguments, Options) -> make_command2(Name, Request, BaseArguments, Options). -spec make_command_raw_value(Name :: atom(), Request :: http_request(), BaseArguments :: [{ArgName :: binary(), ArgValue :: binary()}]) -> any(). make_command_raw_value(Name, Request, BaseArguments) -> make_command2(Name, Request, BaseArguments, [{only, raw_value}]). %%%================================== %%%% make_command: main -spec make_command2(Name :: atom(), Request :: http_request(), BaseArguments :: [{ArgName :: binary(), ArgValue :: binary()}], [Option]) -> xmlel() | any() when Option :: {only, presentation | without_presentation | button | result | value | raw_value | raw_and_value} | {input_name_append, [binary()]} | {force_execution, boolean()} | {table_options, {PageSize :: integer(), RemainingPath :: [binary()]}} | {result_named, boolean()} | {result_links, [{ResultName :: atom(), LinkType :: host | node | user | room | shared_roster | arg_host | paragraph, Level :: integer(), Append :: binary()}]} | {style, normal | danger}. make_command2(Name, Request, BaseArguments, Options) -> Only = proplists:get_value(only, Options, all), ForceExecution = proplists:get_value(force_execution, Options, false), InputNameAppend = proplists:get_value(input_name_append, Options, []), Resultnamed = proplists:get_value(result_named, Options, false), ResultLinks = proplists:get_value(result_links, Options, []), TO = proplists:get_value(table_options, Options, {999999, []}), Style = proplists:get_value(style, Options, normal), #request{us = {RUser, RServer}, ip = RIp} = Request, CallerInfo = #{usr => {RUser, RServer, <<"">>}, ip => RIp, caller_host => RServer, caller_module => ?MODULE}, try {ejabberd_commands:get_command_definition(Name), ejabberd_access_permissions:can_access(Name, CallerInfo)} of {C, allow} -> make_command2(Name, Request, CallerInfo, BaseArguments, C, Only, ForceExecution, InputNameAppend, Resultnamed, ResultLinks, Style, TO); {_C, deny} -> ?DEBUG("Blocked access to command ~p for~n CallerInfo: ~p", [Name, CallerInfo]), ?C(<<"">>) catch A:B -> ?INFO_MSG("Problem preparing command ~p: ~p", [Name, {A, B}]), ?C(<<"">>) end. make_command2(Name, Request, CallerInfo, BaseArguments, C, Only, ForceExecution, InputNameAppend, Resultnamed, ResultLinks, Style, TO) -> {ArgumentsFormat, _Rename, ResultFormatApi} = ejabberd_commands:get_command_format(Name), Method = case {ForceExecution, ResultFormatApi} of {true, _} -> auto; {_, {_, rescode}} -> manual; {_, {_, restuple}} -> manual; _ -> auto end, PresentationEls = make_command_presentation(Name, C#ejabberd_commands.tags), Query = Request#request.q, {ArgumentsUsed1, ExecRes} = execute_command(Name, Query, BaseArguments, Method, ArgumentsFormat, CallerInfo, InputNameAppend), ArgumentsFormatDetailed = add_arguments_details(ArgumentsFormat, C#ejabberd_commands.args_desc, C#ejabberd_commands.args_example), ArgumentsEls = make_command_arguments(Name, Query, Only, Method, Style, ArgumentsFormatDetailed, BaseArguments, InputNameAppend), Automated = case ArgumentsEls of [] -> true; _ -> false end, ArgumentsUsed = (catch lists:zip( lists:map(fun({A, _}) -> A end, ArgumentsFormat), ArgumentsUsed1)), ResultEls = make_command_result(ExecRes, ArgumentsUsed, ResultFormatApi, Automated, Resultnamed, ResultLinks, TO), make_command3(Only, ExecRes, PresentationEls, ArgumentsEls, ResultEls). make_command3(presentation, _ExecRes, PresentationEls, _ArgumentsEls, _ResultEls) -> ?XAE(<<"p">>, [{<<"class">>, <<"api">>}], PresentationEls); make_command3(button, _ExecRes, _PresentationEls, [Button], _ResultEls) -> Button; make_command3(result, _ExecRes, _PresentationEls, _ArgumentsEls, [{xmlcdata, _}, Xmlel]) -> ?XAE(<<"p">>, [{<<"class">>, <<"api">>}], [Xmlel]); make_command3(value, _ExecRes, _PresentationEls, _ArgumentsEls, [{xmlcdata, _}, Xmlel]) -> Xmlel; make_command3(value, _ExecRes, _PresentationEls, _ArgumentsEls, [{xmlel, _, _, _} = Xmlel]) -> Xmlel; make_command3(raw_and_value, ExecRes, _PresentationEls, _ArgumentsEls, [{xmlel, _, _, _} = Xmlel]) -> {raw_and_value, ExecRes, Xmlel}; make_command3(raw_value, ExecRes, _PresentationEls, _ArgumentsEls, _ResultEls) -> ExecRes; make_command3(without_presentation, _ExecRes, _PresentationEls, ArgumentsEls, ResultEls) -> ?XAE(<<"p">>, [{<<"class">>, <<"api">>}], [?XE(<<"blockquote">>, ArgumentsEls ++ ResultEls)]); make_command3(all, _ExecRes, PresentationEls, ArgumentsEls, ResultEls) -> ?XAE(<<"p">>, [{<<"class">>, <<"api">>}], PresentationEls ++ [?XE(<<"blockquote">>, ArgumentsEls ++ ResultEls)]). add_arguments_details(ArgumentsFormat, Descriptions, none) -> add_arguments_details(ArgumentsFormat, Descriptions, []); add_arguments_details(ArgumentsFormat, none, Examples) -> add_arguments_details(ArgumentsFormat, [], Examples); add_arguments_details(ArgumentsFormat, Descriptions, Examples) -> lists_zipwith3(fun({A, B}, C, D) -> {A, B, C, D} end, ArgumentsFormat, Descriptions, Examples, {pad, {none, "", ""}}). -ifdef(OTP_BELOW_26). lists_zipwith3(Combine, List1, List2, List3, {pad, {DefaultX, DefaultY, DefaultZ}}) -> lists_zipwith3(Combine, List1, List2, List3, DefaultX, DefaultY, DefaultZ, []). lists_zipwith3(_Combine, [], [], [], _DefaultX, _DefaultY, _DefaultZ, Res) -> lists:reverse(Res); lists_zipwith3(Combine, [E1 | List1], [E2 | List2], [E3 | List3], DefX, DefY, DefZ, Res) -> E123 = Combine(E1, E2, E3), lists_zipwith3(Combine, List1, List2, List3, DefX, DefY, DefZ, [E123 | Res]); lists_zipwith3(Combine, [E1 | List1], [], [], DefX, DefY, DefZ, Res) -> E123 = Combine(E1, DefY, DefZ), lists_zipwith3(Combine, List1, [], [], DefX, DefY, DefZ, [E123 | Res]); lists_zipwith3(Combine, [E1 | List1], [], [E3 | List3], DefX, DefY, DefZ, Res) -> E123 = Combine(E1, DefY, E3), lists_zipwith3(Combine, List1, [], List3, DefX, DefY, DefZ, [E123 | Res]); lists_zipwith3(Combine, [E1 | List1], [E2 | List2], [], DefX, DefY, DefZ, Res) -> E123 = Combine(E1, E2, DefZ), lists_zipwith3(Combine, List1, List2, [], DefX, DefY, DefZ, [E123 | Res]). -else. lists_zipwith3(Combine, List1, List2, List3, How) -> lists:zipwith3(Combine, List1, List2, List3, How). -endif. %%%================================== %%%% make_command: presentation make_command_presentation(Name, Tags) -> NameBin = misc:atom_to_binary(Name), NiceNameBin = nice_this(Name), Text = ejabberd_ctl:get_usage_command(atom_to_list(Name), 100, false, 1000000), AnchorLink = [?ANCHORL(NameBin)], MaybeDocsLink = case lists:member(internal, Tags) of true -> []; false -> [?GL(<<"developer/ejabberd-api/admin-api/#", NameBin/binary>>, NameBin)] end, [?XE(<<"details">>, [?XAE(<<"summary">>, [{<<"id">>, NameBin}], [?XC(<<"strong">>, NiceNameBin)])] ++ MaybeDocsLink ++ AnchorLink ++ [?XC(<<"pre">>, list_to_binary(Text))])]. nice_this(This, integer) -> {nice_this(This), right}; nice_this(This, _Format) -> nice_this(This). -spec nice_this(This :: atom() | string() | [byte()]) -> NiceThis :: binary(). nice_this(This) when is_atom(This) -> nice_this(atom_to_list(This)); nice_this(This) when is_binary(This) -> nice_this(binary_to_list(This)); nice_this(This) when is_list(This) -> list_to_binary(lists:flatten([string:titlecase(Word) || Word <- string:replace(This, "_", " ", all)])). -spec long_this(These :: [This :: atom()]) -> Long :: binary(). long_this(These) -> list_to_binary(lists:join($/, [atom_to_list(This) || This <- These])). %%%================================== %%%% make_command: arguments make_command_arguments(Name, Query, Only, Method, Style, ArgumentsFormat, BaseArguments, InputNameAppend) -> ArgumentsFormat2 = remove_base_arguments(ArgumentsFormat, BaseArguments), ArgumentsFields = make_arguments_fields(Name, Query, ArgumentsFormat2), Button = make_button_element(Name, Method, Style, InputNameAppend), ButtonElement = ?XE(<<"tr">>, [?X(<<"td">>), ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], [Button])]), case {(ArgumentsFields /= []) or (Method == manual), Only} of {false, _} -> []; {true, button} -> [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [Button])]; {true, _} -> [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"table">>, ArgumentsFields ++ [ButtonElement])])] end. remove_base_arguments(ArgumentsFormat, BaseArguments) -> lists:filter(fun({ArgName, _ArgFormat, _ArgDesc, _ArgExample}) -> not lists:keymember( misc:atom_to_binary(ArgName), 1, BaseArguments) end, ArgumentsFormat). make_button_element(Name, _, Style, InputNameAppend) -> Id = term_to_id(InputNameAppend), NameBin = <<(misc:atom_to_binary(Name))/binary, Id/binary>>, NiceNameBin = nice_this(Name), case Style of danger -> ?INPUTD(<<"submit">>, NameBin, NiceNameBin); _ -> ?INPUT(<<"submit">>, NameBin, NiceNameBin) end. make_arguments_fields(Name, Query, ArgumentsFormat) -> lists:map(fun({ArgName, ArgFormat, _ArgDescription, ArgExample}) -> ArgExampleBin = format_result(ArgExample, {ArgName, ArgFormat}), ArgNiceNameBin = nice_this(ArgName), ArgLongNameBin = long_this([Name, ArgName]), ArgValue = case lists:keysearch(ArgLongNameBin, 1, Query) of {value, {ArgLongNameBin, V}} -> V; _ -> <<"">> end, ?XE(<<"tr">>, [?XC(<<"td">>, <>), ?XE(<<"td">>, [?INPUTPH(<<"text">>, ArgLongNameBin, ArgValue, ArgExampleBin)])]) end, ArgumentsFormat). %%%================================== %%%% make_command: execute execute_command(Name, Query, BaseArguments, Method, ArgumentsFormat, CallerInfo, InputNameAppend) -> try Args = prepare_arguments(Name, BaseArguments ++ Query, ArgumentsFormat), {Args, execute_command2(Name, Query, Args, Method, ArgumentsFormat, CallerInfo, InputNameAppend)} of R -> R catch A:E -> {error, {A, E}} end. execute_command2(Name, Query, Arguments, Method, ArgumentsFormat, CallerInfo, InputNameAppend) -> AllArgumentsProvided = length(Arguments) == length(ArgumentsFormat), PressedExecuteButton = is_this_to_execute(Name, Query, Arguments, InputNameAppend), LetsExecute = case {Method, PressedExecuteButton, AllArgumentsProvided} of {auto, _, true} -> true; {manual, true, true} -> true; _ -> false end, case LetsExecute of true -> catch ejabberd_commands:execute_command2(Name, Arguments, CallerInfo); false -> not_executed end. is_this_to_execute(Name, Query, Arguments, InputNameAppend) -> NiceNameBin = nice_this(Name), NameBin = misc:atom_to_binary(Name), AppendBin = term_to_id(lists:sublist(Arguments, length(InputNameAppend))), ArgumentsId = <>, {value, {ArgumentsId, NiceNameBin}} == lists:keysearch(ArgumentsId, 1, Query). prepare_arguments(ComName, Args, ArgsFormat) -> lists:foldl(fun({ArgName, ArgFormat}, FinalArguments) -> %% Give priority to the value enforced in our code %% Otherwise use the value provided by the user case {lists:keyfind( misc:atom_to_binary(ArgName), 1, Args), lists:keyfind(long_this([ComName, ArgName]), 1, Args)} of %% Value enforced in our code {{_, Value}, _} -> [format_arg(Value, ArgFormat) | FinalArguments]; %% User didn't provide value in the field {_, {_, <<>>}} -> FinalArguments; %% Value provided by the user in the form field {_, {_, Value}} -> [format_arg(Value, ArgFormat) | FinalArguments]; {false, false} -> FinalArguments end end, [], lists:reverse(ArgsFormat)). format_arg(Value, any) -> Value; format_arg(Value, atom) when is_atom(Value) -> Value; format_arg(Value, binary) when is_binary(Value) -> Value; format_arg(Value, ArgFormat) -> ejabberd_ctl:format_arg(binary_to_list(Value), ArgFormat). %%%================================== %%%% make_command: result make_command_result(not_executed, _, _, _, _, _, _) -> []; make_command_result({error, ErrorElement}, _, _, _, _, _, _) -> [?DIVRES([?C(<<"Error: ">>), ?XC(<<"code">>, list_to_binary(io_lib:format("~p", [ErrorElement])))])]; make_command_result(Value, ArgumentsUsed, {ResName, _ResFormat} = ResultFormatApi, Automated, Resultnamed, ResultLinks, TO) -> ResNameBin = nice_this(ResName), ResultValueEl = make_command_result_element(ArgumentsUsed, Value, ResultFormatApi, ResultLinks, TO), ResultEls = case Resultnamed of true -> [?C(<>), ResultValueEl]; false -> [ResultValueEl] end, case Automated of true -> ResultEls; false -> [?DIVRES(ResultEls)] end. make_command_result_element(ArgumentsUsed, ListOfTuples, {_ArgName, {list, {_ListElementsName, {tuple, TupleElements}}}}, ResultLinks, {PageSize, RPath}) -> HeadElements = [nice_this(ElementName, ElementFormat) || {ElementName, ElementFormat} <- TupleElements], ContentElements = [list_to_tuple([make_result(format_result(V, {ElementName, ElementFormat}), ElementName, ArgumentsUsed, ResultLinks) || {V, {ElementName, ElementFormat}} <- lists:zip(tuple_to_list(Tuple), TupleElements)]) || Tuple <- ListOfTuples], make_table(PageSize, RPath, HeadElements, ContentElements); make_command_result_element(_ArgumentsUsed, Values, {_ArgName, {tuple, TupleElements}}, _ResultLinks, _TO) -> ?XE(<<"table">>, [?XE(<<"thead">>, [?XE(<<"tr">>, [?XC(<<"td">>, nice_this(ElementName)) || {ElementName, _ElementFormat} <- TupleElements])]), ?XE(<<"tbody">>, [?XE(<<"tr">>, [?XC(<<"td">>, format_result(V, {ElementName, ElementFormat})) || {V, {ElementName, ElementFormat}} <- lists:zip(tuple_to_list(Values), TupleElements)])])]); make_command_result_element(ArgumentsUsed, Value, {_ArgName, {list, {ElementsName, ElementsFormat}}}, ResultLinks, {PageSize, RPath}) -> HeadElements = [nice_this(ElementsName)], ContentElements = [{make_result(format_result(V, {ElementsName, ElementsFormat}), ElementsName, ArgumentsUsed, ResultLinks)} || V <- Value], make_table(PageSize, RPath, HeadElements, ContentElements); make_command_result_element(ArgumentsUsed, Value, ResultFormatApi, ResultLinks, _TO) -> Res = make_result(format_result(Value, ResultFormatApi), unknown_element_name, ArgumentsUsed, ResultLinks), Res2 = case Res of [{xmlel, _, _, _} | _] = X -> X; Z -> [Z] end, ?XE(<<"code">>, Res2). make_result(Binary, ElementName, ArgumentsUsed, [{ResultName, arg_host, Level, Append}]) when (ElementName == ResultName) or (ElementName == unknown_element_name) -> {_, Host} = lists:keyfind(host, 1, ArgumentsUsed), UrlBinary = replace_url_elements([<<"server/">>, host, <<"/">>, Append], [{host, Host}], Level), ?AC(UrlBinary, Binary); make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, host, Level, Append}]) when (ElementName == ResultName) or (ElementName == unknown_element_name) -> UrlBinary = replace_url_elements([<<"server/">>, host, <<"/">>, Append], [{host, Binary}], Level), ?AC(UrlBinary, Binary); make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, mnesia_table, Level, Append}]) when (ElementName == ResultName) or (ElementName == unknown_element_name) -> Node = misc:atom_to_binary(node()), UrlBinary = replace_url_elements([<<"node/">>, node, <<"/db/table/">>, tablename, <<"/">>, Append], [{node, Node}, {tablename, Binary}], Level), ?AC(UrlBinary, Binary); make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, node, Level, Append}]) when (ElementName == ResultName) or (ElementName == unknown_element_name) -> UrlBinary = replace_url_elements([<<"node/">>, node, <<"/">>, Append], [{node, Binary}], Level), ?AC(UrlBinary, Binary); make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, user, Level, Append}]) when (ElementName == ResultName) or (ElementName == unknown_element_name) -> Jid = try jid:decode(Binary) of #jid{} = J -> J catch _:{bad_jid, _} -> %% TODO: Find a method to be able to link to this user to delete it ?INFO_MSG("Error parsing Binary that is not a valid JID:~n ~p", [Binary]), jid:decode(<<"unknown-username@localhost">>) end, {User, Host, _R} = jid:split(Jid), case lists:member(Host, ejabberd_config:get_option(hosts)) of true -> UrlBinary = replace_url_elements([<<"server/">>, host, <<"/user/">>, user, <<"/">>, Append], [{user, misc:url_encode(User)}, {host, Host}], Level), ?AC(UrlBinary, Binary); false -> ?C(Binary) end; make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, room, Level, Append}]) when (ElementName == ResultName) or (ElementName == unknown_element_name) -> Jid = jid:decode(Binary), {Roomname, Service, _} = jid:split(Jid), Host = ejabberd_router:host_of_route(Service), case lists:member(Host, ejabberd_config:get_option(hosts)) of true -> UrlBinary = replace_url_elements([<<"server/">>, host, <<"/muc/rooms/room/">>, room, <<"/">>, Append], [{room, misc:url_encode(Roomname)}, {host, Host}], Level), ?AC(UrlBinary, Binary); false -> ?C(Binary) end; make_result(Binary, ElementName, ArgumentsUsed, [{ResultName, shared_roster, Level, Append}]) when (ElementName == ResultName) or (ElementName == unknown_element_name) -> First = proplists:get_value(first, ArgumentsUsed), Second = proplists:get_value(second, ArgumentsUsed), FirstUrlencoded = hd(string:replace( misc:url_encode(First), "%40", "@")), {GroupId, Host} = case jid:decode(FirstUrlencoded) of #jid{luser = <<"">>, lserver = G} -> {G, Second}; #jid{luser = G, lserver = H} -> {G, H} end, UrlBinary = replace_url_elements([<<"server/">>, host, <<"/shared-roster/group/">>, srg, <<"/">>, Append], [{host, Host}, {srg, GroupId}], Level), ?AC(UrlBinary, Binary); make_result([{xmlcdata, _, _, _} | _] = Any, _ElementName, _ArgumentsUsed, _ResultLinks) -> Any; make_result([{xmlel, _, _, _} | _] = Any, _ElementName, _ArgumentsUsed, _ResultLinks) -> Any; make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, paragraph, _Level, _Append}]) when (ElementName == ResultName) or (ElementName == unknown_element_name) -> ?XC(<<"pre">>, Binary); make_result(Binary, _ElementName, _ArgumentsUsed, _ResultLinks) -> ?C(Binary). replace_url_elements(UrlComponents, Replacements, Level) -> Base = get_base_path_sum(0, 0, Level), Binary2 = lists:foldl(fun (El, Acc) when is_binary(El) -> [El | Acc]; (El, Acc) when is_atom(El) -> {El, Value} = lists:keyfind(El, 1, Replacements), [Value | Acc] end, [], UrlComponents), Binary3 = binary:list_to_bin( lists:reverse(Binary2)), <>. format_result(Value, {_ResultName, integer}) when is_integer(Value) -> integer_to_binary(Value); format_result(Value, {_ResultName, string}) when is_list(Value) -> Value; format_result(Value, {_ResultName, string}) when is_binary(Value) -> Value; format_result(Value, {_ResultName, atom}) when is_atom(Value) -> misc:atom_to_binary(Value); format_result(Value, {_ResultName, any}) -> Value; format_result({ok, String}, {_ResultName, restuple}) when is_list(String) -> list_to_binary(String); format_result({error, Type, Code, Desc}, {_ResultName, restuple}) -> <<"Error: ", (misc:atom_to_binary(Type))/binary, " ", (integer_to_binary(Code))/binary, ": ", (list_to_binary(Desc))/binary>>; format_result([], {_Name, {list, _ElementsDef}}) -> ""; format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) -> Separator = ",", [format_result(FirstElement, ElementsDef) | lists:map(fun(Element) -> [Separator | format_result(Element, ElementsDef)] end, Elements)]; format_result(Value, _ResultFormat) when is_atom(Value) -> misc:atom_to_binary(Value); format_result(Value, _ResultFormat) when is_list(Value) -> list_to_binary(Value); format_result(Value, _ResultFormat) when is_binary(Value) -> Value; format_result(Value, _ResultFormat) -> io_lib:format("~p", [Value]). %%%================================== %%%% make_table -spec make_table(PageSize :: integer(), RemainingPath :: [binary()], NameOptionList :: [Name :: binary() | {Name :: binary(), left | right}], Values :: [tuple()]) -> xmlel(). make_table(PageSize, RPath, NameOptionList, Values1) -> Values = case lists:member(<<"sort">>, RPath) of true -> Values1; false -> GetXmlValue = fun ({xmlcdata, _} = X) -> X; ({xmlel, _, _, _} = X) -> X; ({raw_and_value, _V, X}) -> X end, ConvertTupleToTuple = fun(Row1) -> list_to_tuple(lists:map(GetXmlValue, tuple_to_list(Row1))) end, lists:map(ConvertTupleToTuple, Values1) end, make_table1(PageSize, RPath, <<"">>, <<"">>, 1, NameOptionList, Values). make_table1(PageSize, [<<"page">>, PageNumber | RPath], PageUrlBase, SortUrlBase, _Start, NameOptionList, Values1) -> make_table1(PageSize, RPath, <>, <>, 1 + PageSize * binary_to_integer(PageNumber), NameOptionList, Values1); make_table1(PageSize, [<<"sort">>, SortType | RPath], PageUrlBase, SortUrlBase, Start, NameOptionList, Rows1) -> ColumnToSort = length(lists:takewhile(fun (A) when A == SortType -> false; ({A, _}) when A == SortType -> false; (_) -> true end, NameOptionList)) + 1, Direction = case lists:nth(ColumnToSort, NameOptionList) of {_, right} -> descending; {_, left} -> ascending; _ -> ascending end, ColumnToSort = ColumnToSort, GetRawValue = fun ({xmlcdata, _} = X) -> X; ({xmlel, _, _, _} = X) -> X; ({raw_and_value, R, _X}) -> R end, GetXmlValue = fun ({xmlcdata, _} = X) -> X; ({xmlel, _, _, _} = X) -> X; ({raw_and_value, _R, X}) -> X end, SortTwo = fun(A1, B1) -> A2 = GetRawValue(element(ColumnToSort, A1)), B2 = GetRawValue(element(ColumnToSort, B1)), case Direction of ascending -> A2 < B2; descending -> A2 > B2 end end, Rows1Sorted = lists:sort(SortTwo, Rows1), ConvertTupleToTuple = fun(Row1) -> list_to_tuple(lists:map(GetXmlValue, tuple_to_list(Row1))) end, Rows = lists:map(ConvertTupleToTuple, Rows1Sorted), make_table1(PageSize, RPath, PageUrlBase, <>, Start, NameOptionList, Rows); make_table1(PageSize, [], PageUrlBase, SortUrlBase, Start, NameOptionList, Values1) -> Values = lists:sublist(Values1, Start, PageSize), Table = make_table(NameOptionList, Values), Size = length(Values1), Remaining = case Size rem PageSize of 0 -> 0; _ -> 1 end, NumPages = max(0, Size div PageSize + Remaining - 1), PLinks1 = lists:foldl(fun(N, Acc) -> NBin = integer_to_binary(N), Acc ++ [?C(<<", ">>), ?AC(<>, NBin)] end, [], lists:seq(1, NumPages)), PLinks = case PLinks1 of [] -> []; _ -> [?XE(<<"p">>, [?C(<<"Page: ">>), ?AC(<>, <<"0">>) | PLinks1])] end, Names = lists:map(fun ({Name, _}) -> Name; (Name) -> Name end, NameOptionList), [_ | SLinks1] = lists:foldl(fun(N, Acc) -> [?C(<<", ">>), ?AC(<>, N) | Acc] end, [], lists:reverse(Names)), SLinks = case {PLinks, SLinks1} of {_, []} -> []; {[], _} -> []; {_, [_]} -> []; {_, SLinks2} -> [?XE(<<"p">>, [?C(<<"Sort all pages by: ">>) | SLinks2])] end, ?XE(<<"div">>, [Table | PLinks ++ SLinks]). -spec make_table(NameOptionList :: [Name :: binary() | {Name :: binary(), left | right}], Values :: [tuple()]) -> xmlel(). make_table(NameOptionList, Values) -> NamesAndAttributes = [make_column_attributes(NameOption) || NameOption <- NameOptionList], {Names, ColumnsAttributes} = lists:unzip(NamesAndAttributes), make_table(Names, ColumnsAttributes, Values). make_table(Names, ColumnsAttributes, Values) -> ?XAE(<<"table">>, [{<<"class">>, <<"sortable">>}], [?XE(<<"thead">>, [?XE(<<"tr">>, [?XC(<<"th">>, nice_this(HeadElement)) || HeadElement <- Names])]), ?XE(<<"tbody">>, [?XE(<<"tr">>, [?XAE(<<"td">>, CAs, [V]) || {CAs, V} <- lists:zip(ColumnsAttributes, tuple_to_list(ValueTuple))]) || ValueTuple <- Values])]). make_column_attributes({Name, Option}) -> {Name, [make_column_attribute(Option)]}; make_column_attributes(Name) -> {Name, []}. make_column_attribute(left) -> {<<"class">>, <<"alignleft">>}; make_column_attribute(right) -> {<<"class">>, <<"alignright">>}. %%%================================== %%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: