%%%---------------------------------------------------------------------- %%% File : ejabberd_web_admin.erl %%% Author : Alexey Shchepin %%% Purpose : Administration web interface %%% Created : 9 Apr 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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, list_users/4, list_users_in_diapason/4, pretty_print_xml/1, term_to_id/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("translate.hrl"). -define(INPUTATTRS(Type, Name, Value, Attrs), ?XA(<<"input">>, (Attrs ++ [{<<"type">>, Type}, {<<"name">>, Name}, {<<"value">>, Value}]))). %%%================================== %%%% get_acl_access %% @spec (Path::[string()], Method) -> {HostOfRule, [AccessRule]} %% where Method = 'GET' | 'POST' %% 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([<<"logo-fill.png">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"favicon.ico">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"additions.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, webadmin_view]}; 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, webadmin_view]}; 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([<<"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([?XCT(<<"h1">>, ?T("Unauthorized"))])}; {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([?XCT(<<"h1">>, ?T("Unauthorized"))])} 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([?XCT(<<"h1">>, ?T("Unauthorized"))])}; {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([?XCT(<<"h1">>, ?T("Unauthorized"))])} end. 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, Lang, JID, Level) -> make_xhtml(Els, Host, cluster, Lang, JID, Level). %% @spec (Els, Host, Node, Lang, JID, Level::integer()) -> {200, [html], xmlelement()} %% where Host = global | string() %% Node = cluster | atom() %% JID = jid() make_xhtml(Els, Host, Node, Lang, JID, Level) -> Base = get_base_path_sum(0, 0, Level), MenuItems = make_navigation(Host, Node, Lang, JID, 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 = []}]}, ?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-2021 ">>), ?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. logo_fill() -> case misc:read_img("admin-logo-fill.png") of {ok, Img} -> Img; {error, _} -> <<>> end. %%%================================== %%%% process_admin process_admin(global, #request{path = [], lang = Lang}, AJID) -> make_xhtml((?H1GL((translate:translate(Lang, ?T("Administration"))), <<"">>, <<"Contents">>)) ++ [?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- get_menu_items(global, cluster, Lang, AJID, 0)])], global, Lang, AJID, 0); process_admin(Host, #request{path = [], lang = Lang}, AJID) -> make_xhtml([?XCT(<<"h1">>, ?T("Administration")), ?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- get_menu_items(Host, cluster, Lang, AJID, 2)])], Host, Lang, 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 = [<<"logo-fill.png">>]}, _) -> {200, [{<<"Content-Type">>, <<"image/png">>}, last_modified(), cache_control_public()], logo_fill()}; process_admin(_Host, #request{path = [<<"additions.js">>]}, _) -> {200, [{<<"Content-Type">>, <<"text/javascript">>}, last_modified(), cache_control_public()], additions_js()}; process_admin(global, #request{path = [<<"vhosts">>], lang = Lang}, AJID) -> Res = list_vhosts(Lang, AJID), make_xhtml((?H1GL((translate:translate(Lang, ?T("Virtual Hosts"))), <<"basic/#xmpp-domains">>, ?T("XMPP Domains"))) ++ Res, global, Lang, AJID, 1); process_admin(Host, #request{path = [<<"users">>], q = Query, lang = Lang}, AJID) when is_binary(Host) -> Res = list_users(Host, Query, Lang, fun url_func/1), make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, Lang, AJID, 3); process_admin(Host, #request{path = [<<"users">>, Diap], lang = Lang}, AJID) when is_binary(Host) -> Res = list_users_in_diapason(Host, Diap, Lang, fun url_func/1), make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, Lang, AJID, 4); process_admin(Host, #request{path = [<<"online-users">>], lang = Lang}, AJID) when is_binary(Host) -> Res = list_online_users(Host, Lang), make_xhtml([?XCT(<<"h1">>, ?T("Online Users"))] ++ Res, Host, Lang, AJID, 3); process_admin(Host, #request{path = [<<"last-activity">>], q = Query, lang = Lang}, AJID) when is_binary(Host) -> ?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, PageH1 = ?H1GL(translate:translate(Lang, ?T("Users Last Activity")), <<"modules/#mod-last">>, <<"mod_last">>), make_xhtml(PageH1 ++ [?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, Host, Lang, AJID, 3); process_admin(Host, #request{path = [<<"stats">>], lang = Lang}, AJID) -> Res = get_stats(Host, Lang), PageH1 = ?H1GL(translate:translate(Lang, ?T("Statistics")), <<"modules/#mod-stats">>, <<"mod_stats">>), Level = case Host of global -> 1; _ -> 3 end, make_xhtml(PageH1 ++ Res, Host, Lang, AJID, Level); process_admin(Host, #request{path = [<<"user">>, U], q = Query, lang = Lang}, AJID) -> case ejabberd_auth:user_exists(U, Host) of true -> Res = user_info(U, Host, Query, Lang), make_xhtml(Res, Host, Lang, AJID, 4); false -> make_xhtml([?XCT(<<"h1">>, ?T("Not Found"))], Host, Lang, AJID, 4) end; process_admin(Host, #request{path = [<<"nodes">>], lang = Lang}, AJID) -> Res = get_nodes(Lang), Level = case Host of global -> 1; _ -> 3 end, make_xhtml(Res, Host, Lang, AJID, Level); process_admin(Host, #request{path = [<<"node">>, SNode | NPath], q = Query, lang = Lang}, AJID) -> case search_running_node(SNode) of false -> make_xhtml([?XCT(<<"h1">>, ?T("Node not found"))], Host, Lang, AJID, 2); Node -> Res = get_node(Host, Node, NPath, Query, Lang), Level = case Host of global -> 2 + length(NPath); _ -> 4 + length(NPath) end, make_xhtml(Res, Host, Node, Lang, AJID, Level) end; %%%================================== %%%% process_admin default case process_admin(Host, #request{lang = Lang} = Request, AJID) -> Res = case Host of global -> ejabberd_hooks:run_fold( webadmin_page_main, Host, [], [Request]); _ -> ejabberd_hooks:run_fold( webadmin_page_host, Host, [], [Host, Request]) 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, Lang, AJID, Level), 404); _ -> make_xhtml(Res, Host, Lang, AJID, Level) end. term_to_id(T) -> base64:encode((term_to_binary(T))). %%%================================== %%%% list_vhosts list_vhosts(Lang, JID) -> Hosts = ejabberd_option:hosts(), HostsAllowed = lists:filter(fun (Host) -> any_rules_allowed(Host, [configure, webadmin_view], JID) end, Hosts), list_vhosts2(Lang, HostsAllowed). list_vhosts2(Lang, Hosts) -> SHosts = lists:sort(Hosts), [?XE(<<"table">>, [?XE(<<"thead">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Host")), ?XCT(<<"td">>, ?T("Registered Users")), ?XCT(<<"td">>, ?T("Online Users"))])]), ?XE(<<"tbody">>, (lists:map(fun (Host) -> OnlineUsers = length(ejabberd_sm:get_vh_session_list(Host)), RegisteredUsers = ejabberd_auth:count_users(Host), ?XE(<<"tr">>, [?XE(<<"td">>, [?AC(<<"../server/", Host/binary, "/">>, Host)]), ?XC(<<"td">>, (pretty_string_int(RegisteredUsers))), ?XC(<<"td">>, (pretty_string_int(OnlineUsers)))]) end, SHosts)))])]. %%%================================== %%%% list_users list_users(Host, Query, Lang, URLFunc) -> Res = list_users_parse_query(Query, Host), Users = ejabberd_auth:get_users(Host), SUsers = lists:sort([{S, U} || {U, S} <- Users]), FUsers = case length(SUsers) of N when N =< 100 -> [list_given_users(Host, SUsers, <<"../">>, Lang, URLFunc)]; N -> NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1, M = trunc(N / NParts) + 1, 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)) end, case Res of %% Parse user creation query and try register: ok -> [?XREST(?T("Submitted"))]; error -> [?XREST(?T("Bad format"))]; nothing -> [] end ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], ([?XE(<<"table">>, [?XE(<<"tr">>, [?XC(<<"td">>, <<(translate:translate(Lang, ?T("User")))/binary, ":">>), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"newusername">>, <<"">>)]), ?XE(<<"td">>, [?C(<<" @ ", Host/binary>>)])]), ?XE(<<"tr">>, [?XC(<<"td">>, <<(translate:translate(Lang, ?T("Password")))/binary, ":">>), ?XE(<<"td">>, [?INPUT(<<"password">>, <<"newuserpassword">>, <<"">>)]), ?X(<<"td">>)]), ?XE(<<"tr">>, [?X(<<"td">>), ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], [?INPUTT(<<"submit">>, <<"addnewuser">>, ?T("Add User"))]), ?X(<<"td">>)])]), ?P] ++ FUsers))]. list_users_parse_query(Query, Host) -> case lists:keysearch(<<"addnewuser">>, 1, Query) of {value, _} -> {value, {_, Username}} = lists:keysearch(<<"newusername">>, 1, Query), {value, {_, Password}} = lists:keysearch(<<"newuserpassword">>, 1, Query), try jid:decode(<>) of #jid{user = User, server = Server} -> case ejabberd_auth:try_register(User, Server, Password) of {error, _Reason} -> error; _ -> ok end catch _:{bad_jid, _} -> error end; false -> nothing end. list_users_in_diapason(Host, Diap, Lang, URLFunc) -> Users = ejabberd_auth:get_users(Host), SUsers = lists:sort([{S, U} || {U, S} <- Users]), [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>), N1 = binary_to_integer(S1), N2 = binary_to_integer(S2), Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), [list_given_users(Host, Sub, <<"../../">>, Lang, URLFunc)]. list_given_users(Host, Users, Prefix, Lang, URLFunc) -> ModOffline = get_offlinemsg_module(Host), ?XE(<<"table">>, [?XE(<<"thead">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("User")), ?XCT(<<"td">>, ?T("Offline Messages")), ?XCT(<<"td">>, ?T("Last Activity"))])]), ?XE(<<"tbody">>, (lists:map(fun (_SU = {Server, User}) -> US = {User, Server}, QueueLenStr = get_offlinemsg_length(ModOffline, User, Server), FQueueLen = [?AC((URLFunc({users_queue, Prefix, User, Server})), QueueLenStr)], FLast = case ejabberd_sm:get_user_resources(User, Server) of [] -> case get_last_info(User, Server) of not_found -> translate:translate(Lang, ?T("Never")); {ok, Shift, _Status} -> 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; _ -> translate:translate(Lang, ?T("Online")) end, ?XE(<<"tr">>, [?XE(<<"td">>, [?AC((URLFunc({user, Prefix, misc:url_encode(User), Server})), (us_to_list(US)))]), ?XE(<<"td">>, FQueueLen), ?XC(<<"td">>, FLast)]) end, Users)))]). get_offlinemsg_length(ModOffline, User, Server) -> case ModOffline of none -> <<"disabled">>; _ -> pretty_string_int(ModOffline:count_offline_messages(User,Server)) end. get_offlinemsg_module(Server) -> case gen_mod:is_loaded(Server, mod_offline) of true -> mod_offline; false -> none end. 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. 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. us_to_list({User, Server}) -> jid:encode({User, Server, <<"">>}). su_to_list({Server, User}) -> jid:encode({User, Server, <<"">>}). %%%================================== %%%% get_stats get_stats(global, Lang) -> OnlineUsers = ejabberd_sm:connected_users_number(), RegisteredUsers = lists:foldl(fun (Host, Total) -> ejabberd_auth:count_users(Host) + Total end, 0, ejabberd_option:hosts()), OutS2SNumber = ejabberd_s2s:outgoing_s2s_number(), InS2SNumber = ejabberd_s2s:incoming_s2s_number(), [?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Registered Users:")), ?XC(<<"td">>, (pretty_string_int(RegisteredUsers)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Online Users:")), ?XC(<<"td">>, (pretty_string_int(OnlineUsers)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Outgoing s2s Connections:")), ?XC(<<"td">>, (pretty_string_int(OutS2SNumber)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Incoming s2s Connections:")), ?XC(<<"td">>, (pretty_string_int(InS2SNumber)))])])])]; get_stats(Host, Lang) -> OnlineUsers = length(ejabberd_sm:get_vh_session_list(Host)), RegisteredUsers = ejabberd_auth:count_users(Host), [?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Registered Users:")), ?XC(<<"td">>, (pretty_string_int(RegisteredUsers)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Online Users:")), ?XC(<<"td">>, (pretty_string_int(OnlineUsers)))])])])]. list_online_users(Host, _Lang) -> Users = [{S, U} || {U, S, _R} <- ejabberd_sm:get_vh_session_list(Host)], SUsers = lists:usort(Users), lists:flatmap(fun ({_S, U} = SU) -> [?AC(<<"../user/", (misc:url_encode(U))/binary, "/">>, (su_to_list(SU))), ?BR] end, SUsers). user_info(User, Server, Query, Lang) -> LServer = jid:nameprep(Server), US = {jid:nodeprep(User), LServer}, Res = user_parse_query(User, Server, Query), Resources = ejabberd_sm:get_user_resources(User, Server), FResources = case Resources of [] -> [?CT(?T("None"))]; _ -> [?XE(<<"ul">>, (lists:map( fun (R) -> FIP = case ejabberd_sm:get_user_info(User, Server, R) of offline -> <<"">>; Info when is_list(Info) -> Node = proplists:get_value(node, Info), Conn = proplists:get_value(conn, Info), {IP, Port} = proplists:get_value(ip, Info), ConnS = case Conn of c2s -> <<"plain">>; c2s_tls -> <<"tls">>; c2s_compressed -> <<"zlib">>; c2s_compressed_tls -> <<"tls+zlib">>; http_bind -> <<"http-bind">>; websocket -> <<"websocket">>; _ -> <<"unknown">> end, <> end, case direction(Lang) of [{_, <<"rtl">>}] -> ?LI([?C((<>))]); _ -> ?LI([?C((<>))]) end end, lists:sort(Resources))))] end, FPassword = [?INPUT(<<"text">>, <<"password">>, <<"">>), ?C(<<" ">>), ?INPUTT(<<"submit">>, <<"chpassword">>, ?T("Change Password"))], UserItems = ejabberd_hooks:run_fold(webadmin_user, LServer, [], [User, Server, Lang]), LastActivity = case ejabberd_sm:get_user_resources(User, Server) of [] -> case get_last_info(User, Server) of not_found -> translate:translate(Lang, ?T("Never")); {ok, Shift, _Status} -> 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; _ -> translate:translate(Lang, ?T("Online")) end, [?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">>}], ([?XCT(<<"h3">>, ?T("Connected Resources:"))] ++ FResources ++ [?XCT(<<"h3">>, ?T("Password:"))] ++ FPassword ++ [?XCT(<<"h3">>, ?T("Last Activity"))] ++ [?C(LastActivity)] ++ UserItems ++ [?P, ?INPUTTD(<<"submit">>, <<"removeuser">>, ?T("Remove User"))]))]. 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(<<"password">>, _User, _Server, _Query) -> nothing; user_parse_query1(<<"chpassword">>, User, Server, Query) -> case lists:keysearch(<<"password">>, 1, Query) of {value, {_, Password}} -> ejabberd_auth:set_password(User, Server, Password), ok; _ -> error end; user_parse_query1(<<"removeuser">>, User, Server, _Query) -> ejabberd_auth:remove_user(User, Server), ok; 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 get_nodes(Lang) -> RunningNodes = ejabberd_cluster:get_nodes(), StoppedNodes = ejabberd_cluster:get_known_nodes() -- RunningNodes, FRN = if RunningNodes == [] -> ?CT(?T("None")); true -> ?XE(<<"ul">>, (lists:map(fun (N) -> S = iolist_to_binary(atom_to_list(N)), ?LI([?AC(<<"../node/", S/binary, "/">>, S)]) end, lists:sort(RunningNodes)))) end, FSN = if StoppedNodes == [] -> ?CT(?T("None")); true -> ?XE(<<"ul">>, (lists:map(fun (N) -> S = iolist_to_binary(atom_to_list(N)), ?LI([?C(S)]) end, lists:sort(StoppedNodes)))) end, [?XCT(<<"h1">>, ?T("Nodes")), ?XCT(<<"h3">>, ?T("Running Nodes")), FRN, ?XCT(<<"h3">>, ?T("Stopped Nodes")), FSN]. 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, [], Query, Lang) -> Res = node_parse_query(Node, Query), Base = get_base_path(global, Node, 2), MenuItems2 = make_menu_items(global, Node, Base, Lang), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Node ~p"), [Node])))] ++ case Res of ok -> [?XREST(?T("Submitted"))]; error -> [?XREST(?T("Bad format"))]; nothing -> [] end ++ [?XE(<<"ul">>, ([?LI([?ACT(<<"db/">>, ?T("Database"))]), ?LI([?ACT(<<"backup/">>, ?T("Backup"))]), ?LI([?ACT(<<"stats/">>, ?T("Statistics"))]), ?LI([?ACT(<<"update/">>, ?T("Update"))])] ++ MenuItems2)), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?INPUTT(<<"submit">>, <<"restart">>, ?T("Restart")), ?C(<<" ">>), ?INPUTTD(<<"submit">>, <<"stop">>, ?T("Stop"))])]; get_node(Host, Node, [], _Query, 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">>], Query, Lang) -> case ejabberd_cluster:call(Node, mnesia, system_info, [tables]) of {badrpc, _Reason} -> [?XCT(<<"h1">>, ?T("RPC Call Error"))]; Tables -> ResS = case node_db_parse_query(Node, Tables, Query) of nothing -> []; ok -> [?XREST(?T("Submitted"))] end, STables = lists:sort(Tables), Rows = lists:map(fun (Table) -> STable = iolist_to_binary(atom_to_list(Table)), TInfo = case ejabberd_cluster:call(Node, mnesia, table_info, [Table, all]) of {badrpc, _} -> []; I -> I end, {Type, Size, Memory} = case {lists:keysearch(storage_type, 1, TInfo), lists:keysearch(size, 1, TInfo), lists:keysearch(memory, 1, TInfo)} of {{value, {storage_type, T}}, {value, {size, S}}, {value, {memory, M}}} -> {T, S, M}; _ -> {unknown, 0, 0} end, ?XE(<<"tr">>, [?XC(<<"td">>, STable), ?XE(<<"td">>, [db_storage_select(STable, Type, Lang)]), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(Size))), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(Memory)))]) end, STables), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Database Tables at ~p"), [Node])) )] ++ ResS ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XAE(<<"table">>, [], [?XE(<<"thead">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Name")), ?XCT(<<"td">>, ?T("Storage Type")), ?XCT(<<"td">>, ?T("Elements")), ?XCT(<<"td">>, ?T("Memory"))])]), ?XE(<<"tbody">>, (Rows ++ [?XE(<<"tr">>, [?XAE(<<"td">>, [{<<"colspan">>, <<"4">>}, {<<"class">>, <<"alignright">>}], [?INPUTT(<<"submit">>, <<"submit">>, ?T("Submit"))])])]))])])] end; get_node(global, Node, [<<"backup">>], Query, Lang) -> HomeDirRaw = case {os:getenv("HOME"), os:type()} of {EnvHome, _} when is_list(EnvHome) -> list_to_binary(EnvHome); {false, {win32, _Osname}} -> <<"C:/">>; {false, _} -> <<"/tmp/">> end, HomeDir = filename:nativename(HomeDirRaw), ResS = case node_backup_parse_query(Node, Query) of nothing -> []; ok -> [?XREST(?T("Submitted"))]; {error, Error} -> [?XRES(<<(translate:translate(Lang, ?T("Error")))/binary, ": ", ((str:format("~p", [Error])))/binary>>)] end, [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Backup of ~p"), [Node])))] ++ ResS ++ [?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.")), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Store binary backup:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"storepath">>, (filename:join(HomeDir, "ejabberd.backup")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"store">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Restore binary backup immediately:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"restorepath">>, (filename:join(HomeDir, "ejabberd.backup")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"restore">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Restore binary backup after next ejabberd " "restart (requires less memory):")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"fallbackpath">>, (filename:join(HomeDir, "ejabberd.backup")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"fallback">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Store plain text backup:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"dumppath">>, (filename:join(HomeDir, "ejabberd.dump")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"dump">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Restore plain text backup immediately:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"loadpath">>, (filename:join(HomeDir, "ejabberd.dump")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"load">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Import users data from a PIEFXIS file " "(XEP-0227):")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"import_piefxis_filepath">>, (filename:join(HomeDir, "users.xml")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"import_piefxis_file">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Export data of all users in the server " "to PIEFXIS files (XEP-0227):")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"export_piefxis_dirpath">>, HomeDir)]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"export_piefxis_dir">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XE(<<"td">>, [?CT(?T("Export data of users in a host to PIEFXIS " "files (XEP-0227):")), ?C(<<" ">>), make_select_host(Lang, <<"export_piefxis_host_dirhost">>)]), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"export_piefxis_host_dirpath">>, HomeDir)]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"export_piefxis_host_dir">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XE(<<"td">>, [?CT(?T("Export all tables as SQL queries " "to a file:")), ?C(<<" ">>), make_select_host(Lang, <<"export_sql_filehost">>)]), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"export_sql_filepath">>, (filename:join(HomeDir, "db.sql")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"export_sql_file">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Import user data from jabberd14 spool " "file:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"import_filepath">>, (filename:join(HomeDir, "user1.xml")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"import_file">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Import users data from jabberd14 spool " "directory:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"import_dirpath">>, <<"/var/spool/jabber/">>)]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"import_dir">>, ?T("OK"))])])])])])]; get_node(global, Node, [<<"stats">>], _Query, Lang) -> UpTime = ejabberd_cluster:call(Node, erlang, statistics, [wall_clock]), UpTimeS = (str:format("~.3f", [element(1, UpTime) / 1000])), CPUTime = ejabberd_cluster:call(Node, erlang, statistics, [runtime]), CPUTimeS = (str:format("~.3f", [element(1, CPUTime) / 1000])), OnlineUsers = ejabberd_sm:connected_users_number(), TransactionsCommitted = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_commits]), TransactionsAborted = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_failures]), TransactionsRestarted = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_restarts]), TransactionsLogged = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_log_writes]), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Statistics of ~p"), [Node]))), ?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Uptime:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], UpTimeS)]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("CPU Time:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], CPUTimeS)]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Online Users:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(OnlineUsers)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Transactions Committed:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(TransactionsCommitted)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Transactions Aborted:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(TransactionsAborted)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Transactions Restarted:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(TransactionsRestarted)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Transactions Logged:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(TransactionsLogged)))])])])]; get_node(global, Node, [<<"update">>], Query, Lang) -> ejabberd_cluster:call(Node, code, purge, [ejabberd_update]), Res = node_update_parse_query(Node, Query), ejabberd_cluster:call(Node, code, load_file, [ejabberd_update]), {ok, _Dir, UpdatedBeams, Script, LowLevelScript, Check} = ejabberd_cluster:call(Node, ejabberd_update, update_info, []), Mods = case UpdatedBeams of [] -> ?CT(?T("None")); _ -> BeamsLis = lists:map(fun (Beam) -> BeamString = iolist_to_binary(atom_to_list(Beam)), ?LI([?INPUT(<<"checkbox">>, <<"selected">>, BeamString), ?C(BeamString)]) end, UpdatedBeams), SelectButtons = [?BR, ?INPUTATTRS(<<"button">>, <<"selectall">>, ?T("Select All"), [{<<"onClick">>, <<"selectAll()">>}]), ?C(<<" ">>), ?INPUTATTRS(<<"button">>, <<"unselectall">>, ?T("Unselect All"), [{<<"onClick">>, <<"unSelectAll()">>}])], ?XAE(<<"ul">>, [{<<"class">>, <<"nolistyle">>}], (BeamsLis ++ SelectButtons)) end, FmtScript = (?XC(<<"pre">>, (str:format("~p", [Script])))), FmtLowLevelScript = (?XC(<<"pre">>, (str:format("~p", [LowLevelScript])))), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Update ~p"), [Node])))] ++ case Res of ok -> [?XREST(?T("Submitted"))]; {error, ErrorText} -> [?XREST(<<"Error: ", ErrorText/binary>>)]; nothing -> [] end ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XCT(<<"h2">>, ?T("Update plan")), ?XCT(<<"h3">>, ?T("Modified modules")), Mods, ?XCT(<<"h3">>, ?T("Update script")), FmtScript, ?XCT(<<"h3">>, ?T("Low level update script")), FmtLowLevelScript, ?XCT(<<"h3">>, ?T("Script check")), ?XC(<<"pre">>, (misc:atom_to_binary(Check))), ?BR, ?INPUTT(<<"submit">>, <<"update">>, ?T("Update"))])]; get_node(Host, Node, NPath, Query, Lang) -> Res = case Host of global -> ejabberd_hooks:run_fold(webadmin_page_node, Host, [], [Node, NPath, Query, Lang]); _ -> ejabberd_hooks:run_fold(webadmin_page_hostnode, Host, [], [Host, Node, NPath, Query, Lang]) end, case Res of [] -> [?XC(<<"h1">>, <<"Not Found">>)]; _ -> Res end. %%%================================== %%%% node parse node_parse_query(Node, Query) -> case lists:keysearch(<<"restart">>, 1, Query) of {value, _} -> case ejabberd_cluster:call(Node, init, restart, []) of {badrpc, _Reason} -> error; _ -> ok end; _ -> case lists:keysearch(<<"stop">>, 1, Query) of {value, _} -> case ejabberd_cluster:call(Node, init, stop, []) of {badrpc, _Reason} -> error; _ -> ok end; _ -> nothing end end. make_select_host(Lang, Name) -> ?XAE(<<"select">>, [{<<"name">>, Name}], (lists:map(fun (Host) -> ?XACT(<<"option">>, ([{<<"value">>, Host}]), Host) end, ejabberd_config:get_option(hosts)))). db_storage_select(ID, Opt, Lang) -> ?XAE(<<"select">>, [{<<"name">>, <<"table", ID/binary>>}], (lists:map(fun ({O, Desc}) -> Sel = if O == Opt -> [{<<"selected">>, <<"selected">>}]; true -> [] end, ?XACT(<<"option">>, (Sel ++ [{<<"value">>, iolist_to_binary(atom_to_list(O))}]), Desc) end, [{ram_copies, ?T("RAM copy")}, {disc_copies, ?T("RAM and disc copy")}, {disc_only_copies, ?T("Disc only copy")}, {unknown, ?T("Remote copy")}, {delete_content, ?T("Delete content")}, {delete_table, ?T("Delete table")}]))). node_db_parse_query(_Node, _Tables, [{nokey, <<>>}]) -> nothing; node_db_parse_query(Node, Tables, Query) -> lists:foreach(fun (Table) -> STable = iolist_to_binary(atom_to_list(Table)), case lists:keysearch(<<"table", STable/binary>>, 1, Query) of {value, {_, SType}} -> Type = case SType of <<"unknown">> -> unknown; <<"ram_copies">> -> ram_copies; <<"disc_copies">> -> disc_copies; <<"disc_only_copies">> -> disc_only_copies; <<"delete_content">> -> delete_content; <<"delete_table">> -> delete_table; _ -> false end, if Type == false -> ok; Type == delete_content -> mnesia:clear_table(Table); Type == delete_table -> mnesia:delete_table(Table); 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; _ -> ok end end, Tables), ok. node_backup_parse_query(_Node, [{nokey, <<>>}]) -> nothing; node_backup_parse_query(Node, Query) -> lists:foldl(fun (Action, nothing) -> case lists:keysearch(Action, 1, Query) of {value, _} -> case lists:keysearch(<>, 1, Query) of {value, {_, Path}} -> Res = case Action of <<"store">> -> ejabberd_cluster:call(Node, mnesia, backup, [binary_to_list(Path)]); <<"restore">> -> ejabberd_cluster:call(Node, ejabberd_admin, restore, [Path]); <<"fallback">> -> ejabberd_cluster:call(Node, mnesia, install_fallback, [binary_to_list(Path)]); <<"dump">> -> ejabberd_cluster:call(Node, ejabberd_admin, dump_to_textfile, [Path]); <<"load">> -> ejabberd_cluster:call(Node, mnesia, load_textfile, [binary_to_list(Path)]); <<"import_piefxis_file">> -> ejabberd_cluster:call(Node, ejabberd_piefxis, import_file, [Path]); <<"export_piefxis_dir">> -> ejabberd_cluster:call(Node, ejabberd_piefxis, export_server, [Path]); <<"export_piefxis_host_dir">> -> {value, {_, Host}} = lists:keysearch(<>, 1, Query), ejabberd_cluster:call(Node, ejabberd_piefxis, export_host, [Path, Host]); <<"export_sql_file">> -> {value, {_, Host}} = lists:keysearch(<>, 1, Query), ejabberd_cluster:call(Node, ejd2sql, export, [Host, Path]); <<"import_file">> -> ejabberd_cluster:call(Node, ejabberd_admin, import_file, [Path]); <<"import_dir">> -> ejabberd_cluster:call(Node, ejabberd_admin, import_dir, [Path]) end, case Res of {error, Reason} -> {error, Reason}; {badrpc, Reason} -> {badrpc, Reason}; _ -> ok end; OtherError -> {error, OtherError} end; _ -> nothing end; (_Action, Res) -> Res end, nothing, [<<"store">>, <<"restore">>, <<"fallback">>, <<"dump">>, <<"load">>, <<"import_file">>, <<"import_dir">>, <<"import_piefxis_file">>, <<"export_piefxis_dir">>, <<"export_piefxis_host_dir">>, <<"export_sql_file">>]). node_update_parse_query(Node, Query) -> case lists:keysearch(<<"update">>, 1, Query) of {value, _} -> ModulesToUpdateStrings = proplists:get_all_values(<<"selected">>, Query), ModulesToUpdate = [misc:binary_to_atom(M) || M <- ModulesToUpdateStrings], case ejabberd_cluster:call(Node, ejabberd_update, update, [ModulesToUpdate]) of {ok, _} -> ok; {error, Error} -> ?ERROR_MSG("~p~n", [Error]), {error, (str:format("~p", [Error]))}; {badrpc, Error} -> ?ERROR_MSG("Bad RPC: ~p~n", [Error]), {error, <<"Bad RPC: ", ((str:format("~p", [Error])))/binary>>} end; _ -> nothing end. 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}) -> <<(integer_to_binary(From))/binary, "-", (integer_to_binary(To))/binary, "/">>; url_func({users_queue, Prefix, User, _Server}) -> <>; url_func({user, Prefix, User, _Server}) -> <>. 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. %%%================================== %%%% navigation menu %% @spec (Host, Node, Lang, JID::jid(), Level::integer()) -> [LI] make_navigation(Host, Node, Lang, JID, Level) -> Menu = make_navigation_menu(Host, Node, Lang, JID, Level), make_menu_items(Lang, Menu). %% @spec (Host, Node, Lang, JID::jid(), Level::integer()) -> Menu %% where Host = global | string() %% Node = cluster | string() %% Lang = string() %% Menu = {URL, Title} | {URL, Title, [Menu]} %% URL = string() %% Title = string() make_navigation_menu(Host, Node, Lang, JID, Level) -> HostNodeMenu = make_host_node_menu(Host, Node, Lang, JID, Level), HostMenu = make_host_menu(Host, HostNodeMenu, Lang, JID, Level), NodeMenu = make_node_menu(Host, Node, Lang, Level), make_server_menu(HostMenu, NodeMenu, Lang, JID, Level). %% @spec (Host, Node, Base, Lang) -> [LI] make_menu_items(global, cluster, Base, Lang) -> HookItems = get_menu_items_hook(server, Lang), make_menu_items(Lang, {Base, <<"">>, HookItems}); make_menu_items(global, Node, Base, Lang) -> HookItems = get_menu_items_hook({node, Node}, Lang), make_menu_items(Lang, {Base, <<"">>, HookItems}); make_menu_items(Host, cluster, Base, Lang) -> HookItems = get_menu_items_hook({host, Host}, Lang), make_menu_items(Lang, {Base, <<"">>, HookItems}); make_menu_items(Host, Node, Base, Lang) -> HookItems = get_menu_items_hook({hostnode, Host, Node}, Lang), make_menu_items(Lang, {Base, <<"">>, HookItems}). 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)), HostNodeFixed2}. make_host_menu(global, _HostNodeMenu, _Lang, _JID, _Level) -> {<<"">>, <<"">>, []}; make_host_menu(Host, HostNodeMenu, Lang, JID, Level) -> HostBase = get_base_path(Host, cluster, Level), HostFixed = [{<<"users">>, ?T("Users")}, {<<"online-users">>, ?T("Online Users")}] ++ get_lastactivity_menuitem_list(Host) ++ [{<<"nodes">>, ?T("Nodes"), HostNodeMenu}, {<<"stats">>, ?T("Statistics")}] ++ get_menu_items_hook({host, Host}, Lang), HostFixed2 = [Tuple || Tuple <- HostFixed, 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">>, ?T("Database")}, {<<"backup">>, ?T("Backup")}, {<<"stats">>, ?T("Statistics")}, {<<"update">>, ?T("Update")}] ++ get_menu_items_hook({node, Node}, Lang), {NodeBase, iolist_to_binary(atom_to_list(Node)), 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}, {<<"stats">>, ?T("Statistics")}] ++ get_menu_items_hook(server, Lang), Fixed2 = [Tuple || Tuple <- Fixed, 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({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 (Lang::string(), Menu) -> [LI] %% where Menu = {MURI::string(), MName::string(), Items::[Item]} %% Item = {IURI::string(), IName::string()} | {IURI::string(), IName::string(), Menu} 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). %%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: