mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-20 16:15:59 +01:00
2511 lines
94 KiB
Erlang
2511 lines
94 KiB
Erlang
%%%----------------------------------------------------------------------
|
|
%%% File : ejabberd_web_admin.erl
|
|
%%% Author : Alexey Shchepin <alexey@process-one.net>
|
|
%%% Purpose : Administration web interface
|
|
%%% Created : 9 Apr 2004 by Alexey Shchepin <alexey@process-one.net>
|
|
%%%
|
|
%%%
|
|
%%% 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}) ->
|
|
{<<URI/binary, "/">>, Name};
|
|
({URI, Name, _SubMenu}) ->
|
|
{<<URI/binary, "/">>, 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}) ->
|
|
{<<URI/binary, "/">>, Name};
|
|
({URI, Name, _SubMenu}) ->
|
|
{<<URI/binary, "/">>, 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">>, <<RawPath/binary, "/">>}], <<>>}
|
|
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">>,
|
|
<<Base/binary, "additions.js">>},
|
|
{<<"type">>, <<"text/javascript">>}],
|
|
children = [?C(<<" ">>)]},
|
|
#xmlel{name = <<"link">>,
|
|
attrs =
|
|
[{<<"href">>,
|
|
<<Base/binary, "favicon.ico">>},
|
|
{<<"type">>, <<"image/x-icon">>},
|
|
{<<"rel">>, <<"shortcut icon">>}],
|
|
children = []},
|
|
#xmlel{name = <<"link">>,
|
|
attrs =
|
|
[{<<"href">>,
|
|
<<Base/binary, "style.css">>},
|
|
{<<"type">>, <<"text/css">>},
|
|
{<<"rel">>, <<"stylesheet">>}],
|
|
children = []},
|
|
#xmlel{name = <<"link">>,
|
|
attrs =
|
|
[{<<"href">>,
|
|
<<Base/binary, "sortable.min.css">>},
|
|
{<<"type">>, <<"text/css">>},
|
|
{<<"rel">>, <<"stylesheet">>}],
|
|
children = []},
|
|
#xmlel{name = <<"script">>,
|
|
attrs =
|
|
[{<<"src">>,
|
|
<<Base/binary, "sortable.min.js">>},
|
|
{<<"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, $,, Result/binary>>};
|
|
(NewNumber, {CountAcc, Result}) ->
|
|
{CountAcc + 1, <<NewNumber, Result/binary>>}
|
|
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,
|
|
<<MURI/binary, IURI/binary, "/">>, IName, Lang)
|
|
| Res];
|
|
{IURI, IName, SubMenu} ->
|
|
ResTemp = [make_menu_item(item, Deep,
|
|
<<MURI/binary, IURI/binary, "/">>,
|
|
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">>, <<ArgNiceNameBin/binary, ":">>),
|
|
?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 = <<NameBin/binary, AppendBin/binary>>,
|
|
{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(<<ResNameBin/binary, ": ">>), 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)),
|
|
<<Base/binary, Binary3/binary>>.
|
|
|
|
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,
|
|
<<PageUrlBase/binary, "../../">>,
|
|
<<SortUrlBase/binary, "../../">>,
|
|
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,
|
|
<<SortUrlBase/binary, "../../">>,
|
|
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(<<PageUrlBase/binary, "page/", NBin/binary, "/">>, NBin)]
|
|
end,
|
|
[],
|
|
lists:seq(1, NumPages)),
|
|
PLinks =
|
|
case PLinks1 of
|
|
[] ->
|
|
[];
|
|
_ ->
|
|
[?XE(<<"p">>, [?C(<<"Page: ">>), ?AC(<<PageUrlBase/binary>>, <<"0">>) | PLinks1])]
|
|
end,
|
|
|
|
Names =
|
|
lists:map(fun ({Name, _}) ->
|
|
Name;
|
|
(Name) ->
|
|
Name
|
|
end,
|
|
NameOptionList),
|
|
[_ | SLinks1] =
|
|
lists:foldl(fun(N, Acc) ->
|
|
[?C(<<", ">>), ?AC(<<SortUrlBase/binary, "sort/", N/binary, "/">>, 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=%%%%,%%%=:
|