diff --git a/Makefile.in b/Makefile.in
index ec6f911eb1b..c89b312496f 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -261,6 +261,16 @@ _build/edoc/docs.md: edoc_compile
_build/edoc/logo.png: edoc_compile
wget https://docs.ejabberd.im/assets/img/footer_logo_e.png -O _build/edoc/logo.png
+#.
+#' format / indent
+#
+
+format:
+ tools/rebar3-format.sh $(REBAR)
+
+indent:
+ tools/emacs-indent.sh
+
#.
#' copy-files
#
@@ -650,7 +660,7 @@ test:
.PHONY: src edoc dialyzer Makefile TAGS clean clean-rel distclean prod rel \
install uninstall uninstall-binary uninstall-all translations deps test \
all dev doap help install-rel relive scripts uninstall-rel update \
- erlang_plt deps_plt ejabberd_plt xref hooks options
+ erlang_plt deps_plt ejabberd_plt xref hooks options format indent
#.
#' help
@@ -682,6 +692,9 @@ help:
@echo " translations Extract translation files"
@echo " TAGS Generate tags file for text editors"
@echo ""
+ @echo " format Format source code using rebar3_format [rebar3]"
+ @echo " indent Indent source code using erlang-mode [emacs]"
+ @echo ""
@echo " dialyzer Run Dialyzer static analyzer"
@echo " hooks Run hooks validator"
@echo " test Run Common Tests suite [rebar3]"
diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl
index 00001bb0ab5..a6a960d8c78 100644
--- a/include/ejabberd_commands.hrl
+++ b/include/ejabberd_commands.hrl
@@ -19,13 +19,30 @@
%%%----------------------------------------------------------------------
-type aterm() :: {atom(), atype()}.
--type atype() :: integer | string | binary |
+-type atype() :: integer | string | binary | any | atom |
{tuple, [aterm()]} | {list, aterm()}.
-type rterm() :: {atom(), rtype()}.
--type rtype() :: integer | string | atom |
+-type rtype() :: integer | string | atom | any |
{tuple, [rterm()]} | {list, rterm()} |
rescode | restuple.
+%% The 'any' and 'atom' argument types and 'any' result type
+%% should only be used %% by commands with tag 'internal',
+%% which are meant to be used only internally in ejabberd,
+%% and not called using external frontends.
+
+%% The purpose of a command can either be:
+%% - informative: its purpose is to obtain information
+%% - modifier: its purpose is to produce some change in the server
+%%
+%% A modifier command should be designed just to produce its desired side-effect,
+%% and its result term should just be success or failure: rescode or restuple.
+%%
+%% ejabberd_web_admin:make_command/2 considers that commands
+%% with result type different than rescode or restuple
+%% are commands that can be safely executed automatically
+%% to get information and build the web page.
+
-type oauth_scope() :: atom().
%% ejabberd_commands OAuth ReST ACL definition:
diff --git a/include/ejabberd_web_admin.hrl b/include/ejabberd_web_admin.hrl
index 7e4df96cef5..fb2019a056f 100644
--- a/include/ejabberd_web_admin.hrl
+++ b/include/ejabberd_web_admin.hrl
@@ -62,6 +62,11 @@
[{<<"type">>, Type}, {<<"name">>, Name},
{<<"value">>, Value}])).
+-define(INPUTPH(Type, Name, Value, PlaceHolder),
+ ?XA(<<"input">>,
+ [{<<"type">>, Type}, {<<"name">>, Name},
+ {<<"value">>, Value}, {<<"placeholder">>, PlaceHolder}])).
+
-define(INPUTT(Type, Name, Value),
?INPUT(Type, Name, (translate:translate(Lang, Value)))).
@@ -95,16 +100,27 @@
-define(XRES(Text),
?XAC(<<"p">>, [{<<"class">>, <<"result">>}], Text)).
+-define(DIVRES(Elements),
+ ?XAE(<<"div">>, [{<<"class">>, <<"result">>}], Elements)).
+
%% Guide Link
-define(XREST(Text), ?XRES((translate:translate(Lang, Text)))).
-define(GL(Ref, Title),
?XAE(<<"div">>, [{<<"class">>, <<"guidelink">>}],
[?XAE(<<"a">>,
- [{<<"href">>, <<"https://docs.ejabberd.im/admin/configuration/", Ref/binary>>},
+ [{<<"href">>, <<"https://docs.ejabberd.im/", Ref/binary>>},
{<<"target">>, <<"_blank">>}],
[?C(<<"docs: ", Title/binary>>)])])).
%% h1 with a Guide Link
--define(H1GL(Name, Ref, Title),
- [?XC(<<"h1">>, Name), ?GL(Ref, Title)]).
+-define(H1GLraw(Name, Ref, Title),
+ [?XC(<<"h1">>, Name), ?GL(Ref, Title), ?BR]).
+-define(H1GL(Name, RefConf, Title),
+ ?H1GLraw(Name, <<"admin/configuration/", RefConf/binary>>, Title)).
+
+-define(ANCHORL(Ref),
+ ?XAE(<<"div">>, [{<<"class">>, <<"anchorlink">>}],
+ [?XAE(<<"a">>,
+ [{<<"href">>, <<"#", Ref/binary>>}],
+ [?C(<<"<=">>)])])).
diff --git a/mix.exs b/mix.exs
index d2afd331b86..47e49682bba 100644
--- a/mix.exs
+++ b/mix.exs
@@ -112,6 +112,7 @@ defmodule Ejabberd.MixProject do
if_version_below(~c"24", [{:d, :SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL}]) ++
if_version_below(~c"24", [{:d, :OTP_BELOW_24}]) ++
if_version_below(~c"25", [{:d, :OTP_BELOW_25}]) ++
+ if_version_below(~c"26", [{:d, :OTP_BELOW_26}]) ++
if_version_below(~c"27", [{:d, :OTP_BELOW_27}]) ++
if_type_exported(:odbc, {:opaque, :connection_reference, 0}, [{:d, :ODBC_HAS_TYPES}])
defines = for {:d, value} <- result, do: {:d, value}
diff --git a/priv/css/admin.css b/priv/css/admin.css
index 276bff63774..9bb34a105b9 100644
--- a/priv/css/admin.css
+++ b/priv/css/admin.css
@@ -136,11 +136,7 @@ ul li #navhead a, ul li #navheadsub a, ul li #navheadsubsub a {
margin-bottom: -1px;
}
thead tr td {
- background: #3eaffa;
- color: #fff;
-}
-thead tr td a {
- color: #fff;
+ background: #cae7e4;
}
td.copy {
text-align: center;
@@ -227,22 +223,29 @@ h3 {
padding-top: 25px;
width: 70%;
}
+div.anchorlink {
+ display: inline-block;
+ float: right;
+ margin-top: 1em;
+ margin-right: 1em;
+}
+div.anchorlink a {
+ padding: 3px;
+ background: #cae7e4;
+ font-size: 0.75em;
+ color: black;
+}
div.guidelink,
p[dir=ltr] {
display: inline-block;
float: right;
-
- margin: 0;
+ margin-top: 1em;
margin-right: 1em;
}
div.guidelink a,
p[dir=ltr] a {
- display: inline-block;
- border-radius: 3px;
padding: 3px;
-
background: #3eaffa;
-
font-size: 0.75em;
color: #fff;
}
@@ -265,7 +268,7 @@ input,
select {
font-size: 1em;
}
-p.result {
+.result {
border: 1px;
border-style: dashed;
border-color: #FE8A02;
@@ -284,3 +287,18 @@ p.result {
color: #cb2431;
transition: none;
}
+h3.api {
+ border-bottom: 1px solid #b6b6b6;
+}
+details > summary {
+ background-color: #dbeceb;
+ border: none;
+ cursor: pointer;
+ list-style: none;
+ padding: 8px;
+}
+details > pre, details > p {
+ background-color: #e6f1f0;
+ margin: 0;
+ padding: 10px;
+}
diff --git a/priv/css/sortable.min.css b/priv/css/sortable.min.css
new file mode 100644
index 00000000000..5296c0f9f88
--- /dev/null
+++ b/priv/css/sortable.min.css
@@ -0,0 +1 @@
+.sortable thead th:not(.no-sort){cursor:pointer}.sortable thead th:not(.no-sort)::after,.sortable thead th:not(.no-sort)::before{transition:color .1s ease-in-out;font-size:1.2em;color:rgba(0,0,0,0)}.sortable thead th:not(.no-sort)::after{margin-left:3px;content:"▸"}.sortable thead th:not(.no-sort):hover::after{color:inherit}.sortable thead th:not(.no-sort)[aria-sort=descending]::after{color:inherit;content:"▾"}.sortable thead th:not(.no-sort)[aria-sort=ascending]::after{color:inherit;content:"▴"}.sortable thead th:not(.no-sort).indicator-left::after{content:""}.sortable thead th:not(.no-sort).indicator-left::before{margin-right:3px;content:"▸"}.sortable thead th:not(.no-sort).indicator-left:hover::before{color:inherit}.sortable thead th:not(.no-sort).indicator-left[aria-sort=descending]::before{color:inherit;content:"▾"}.sortable thead th:not(.no-sort).indicator-left[aria-sort=ascending]::before{color:inherit;content:"▴"}/*# sourceMappingURL=sortable-base.min.css.map */
diff --git a/priv/img/admin-logo-fill.png b/priv/img/admin-logo-fill.png
deleted file mode 100644
index 862163c5044..00000000000
Binary files a/priv/img/admin-logo-fill.png and /dev/null differ
diff --git a/priv/img/admin-logo.png b/priv/img/admin-logo.png
index 0088eddc8a5..041b37c69bb 100644
Binary files a/priv/img/admin-logo.png and b/priv/img/admin-logo.png differ
diff --git a/priv/js/sortable.min.js b/priv/js/sortable.min.js
new file mode 100644
index 00000000000..eb5443135e6
--- /dev/null
+++ b/priv/js/sortable.min.js
@@ -0,0 +1,3 @@
+document.addEventListener("click",function(c){try{function h(b,a){return b.nodeName===a?b:h(b.parentNode,a)}var v=c.shiftKey||c.altKey,d=h(c.target,"TH"),m=d.parentNode,n=m.parentNode,g=n.parentNode;function p(b){var a;return v?b.dataset.sortAlt:null!==(a=b.dataset.sort)&&void 0!==a?a:b.textContent}if("THEAD"===n.nodeName&&g.classList.contains("sortable")&&!d.classList.contains("no-sort")){var q,f=m.cells,r=+d.dataset.sortTbr;for(c=0;c
fun(L) when is_list(L) ->
lists:map(
fun({K, V}) -> {(econf:enum([tag]))(K), (econf:binary())(V)};
- (A) -> (econf:enum([ejabberd_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl]))(A)
+ (A) -> (econf:enum([ejabberd_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl, ejabberd_web_admin]))(A)
end, lists:flatten(L));
(A) ->
- [(econf:enum([ejabberd_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl]))(A)]
+ [(econf:enum([ejabberd_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl, ejabberd_web_admin]))(A)]
end;
validator(what) ->
econf:and_then(
diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl
index 4e997da9b72..dcd2bb428f1 100644
--- a/src/ejabberd_acme.erl
+++ b/src/ejabberd_acme.erl
@@ -32,11 +32,16 @@
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
+%% WebAdmin
+-export([webadmin_menu_node/3, webadmin_page_node/3]).
-include("logger.hrl").
-include("ejabberd_commands.hrl").
+-include("ejabberd_http.hrl").
+-include("ejabberd_web_admin.hrl").
-include_lib("public_key/include/public_key.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
+-include_lib("xmpp/include/xmpp.hrl").
-define(CALL_TIMEOUT, timer:minutes(10)).
@@ -108,6 +113,8 @@ init([]) ->
ejabberd_hooks:add(config_reloaded, ?MODULE, register_certfiles, 40),
ejabberd_hooks:add(ejabberd_started, ?MODULE, ejabberd_started, 110),
ejabberd_hooks:add(config_reloaded, ?MODULE, ejabberd_started, 110),
+ ejabberd_hooks:add(webadmin_menu_node, ?MODULE, webadmin_menu_node, 110),
+ ejabberd_hooks:add(webadmin_page_node, ?MODULE, webadmin_page_node, 110),
ejabberd_commands:register_commands(get_commands_spec()),
register_certfiles(),
{ok, #state{}}.
@@ -153,6 +160,8 @@ terminate(_Reason, _State) ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, register_certfiles, 40),
ejabberd_hooks:delete(ejabberd_started, ?MODULE, ejabberd_started, 110),
ejabberd_hooks:delete(config_reloaded, ?MODULE, ejabberd_started, 110),
+ ejabberd_hooks:delete(webadmin_menu_node, ?MODULE, webadmin_menu_node, 110),
+ ejabberd_hooks:delete(webadmin_page_node, ?MODULE, webadmin_page_node, 110),
ejabberd_commands:unregister_commands(get_commands_spec()).
code_change(_OldVsn, State, _Extra) ->
@@ -547,6 +556,21 @@ list_certificates() ->
{Domain, Path, sets:is_element(E, Used)}
end, Known)).
+%%%===================================================================
+%%% WebAdmin
+%%%===================================================================
+
+webadmin_menu_node(Acc, _Node, _Lang) ->
+ Acc ++ [{<<"acme">>, <<"ACME">>}].
+
+webadmin_page_node(_, Node, #request{path = [<<"acme">>]} = R) ->
+ Head = ?H1GLraw(<<"ACME Certificates">>, <<"admin/configuration/basic/#acme">>, <<"ACME">>),
+ Set = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [request_certificate, R]),
+ ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [revoke_certificate, R])],
+ Get = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [list_certificates, R])],
+ {stop, Head ++ Get ++ Set};
+webadmin_page_node(Acc, _, _) -> Acc.
+
%%%===================================================================
%%% Other stuff
%%%===================================================================
diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl
index 28215491ca6..86fe2a9be3d 100644
--- a/src/ejabberd_admin.erl
+++ b/src/ejabberd_admin.erl
@@ -39,7 +39,9 @@
dump_config/1,
convert_to_yaml/2,
%% Cluster
- join_cluster/1, leave_cluster/1, list_cluster/0,
+ join_cluster/1, leave_cluster/1,
+ list_cluster/0, list_cluster_detailed/0,
+ get_cluster_node_details3/0,
%% Erlang
update_list/0, update/1, update/0,
%% Accounts
@@ -50,7 +52,7 @@
%% Purge DB
delete_expired_messages/0, delete_old_messages/1,
%% Mnesia
- set_master/1,
+ get_master/0, set_master/1,
backup_mnesia/1, restore_mnesia/1,
dump_mnesia/1, dump_table/2, load_mnesia/1,
mnesia_info/0, mnesia_table_info/1,
@@ -61,13 +63,27 @@
clear_cache/0,
gc/0,
get_commands_spec/0,
- delete_old_messages_batch/4, delete_old_messages_status/1, delete_old_messages_abort/1]).
+ delete_old_messages_batch/4, delete_old_messages_status/1, delete_old_messages_abort/1,
+ %% Internal
+ mnesia_list_tables/0,
+ mnesia_table_details/1,
+ mnesia_table_change_storage/2,
+ mnesia_table_clear/1,
+ mnesia_table_delete/1,
+ echo/1, echo3/3]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
--include("logger.hrl").
+-export([web_menu_main/2, web_page_main/2,
+ web_menu_node/3, web_page_node/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"). %+++ TODO
-record(state, {}).
@@ -77,6 +93,10 @@ start_link() ->
init([]) ->
process_flag(trap_exit, true),
ejabberd_commands:register_commands(get_commands_spec()),
+ ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50),
+ ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50),
+ ejabberd_hooks:add(webadmin_menu_node, ?MODULE, web_menu_node, 50),
+ ejabberd_hooks:add(webadmin_page_node, ?MODULE, web_page_node, 50),
{ok, #state{}}.
handle_call(Request, From, State) ->
@@ -92,6 +112,10 @@ handle_info(Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
+ ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50),
+ ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50),
+ ejabberd_hooks:delete(webadmin_menu_node, ?MODULE, web_menu_node, 50),
+ ejabberd_hooks:delete(webadmin_page_node, ?MODULE, web_page_node, 50),
ejabberd_commands:unregister_commands(get_commands_spec()).
code_change(_OldVsn, State, _Extra) ->
@@ -179,8 +203,9 @@ get_commands_spec() ->
desc = "Update the given module",
longdesc = "To update all the possible modules, use `all`.",
module = ?MODULE, function = update,
- args_example = ["mod_vcard"],
+ args_example = ["all"],
args = [{module, string}],
+ result_example = {ok, <<"Updated modules: mod_configure, mod_vcard">>},
result = {res, restuple}},
#ejabberd_commands{name = register, tags = [accounts],
@@ -225,14 +250,12 @@ get_commands_spec() ->
#ejabberd_commands{name = join_cluster, tags = [cluster],
desc = "Join this node into the cluster handled by Node",
- longdesc = "This command works only with ejabberdctl, "
- "not mod_http_api or other code that runs inside the "
- "same ejabberd node that will be joined.",
+ note = "improved in 24.xx",
module = ?MODULE, function = join_cluster,
args_desc = ["Nodename of the node to join"],
args_example = [<<"ejabberd1@machine7">>],
args = [{node, binary}],
- result = {res, rescode}},
+ result = {res, restuple}},
#ejabberd_commands{name = leave_cluster, tags = [cluster],
desc = "Remove and shutdown Node from the running cluster",
longdesc = "This command can be run from any running "
@@ -247,11 +270,27 @@ get_commands_spec() ->
result = {res, rescode}},
#ejabberd_commands{name = list_cluster, tags = [cluster],
- desc = "List nodes that are part of the cluster handled by Node",
+ desc = "List running nodes that are part of this cluster",
module = ?MODULE, function = list_cluster,
result_example = [ejabberd1@machine7, ejabberd1@machine8],
args = [],
result = {nodes, {list, {node, atom}}}},
+ #ejabberd_commands{name = list_cluster_detailed, tags = [cluster],
+ desc = "List nodes (both running and known) and some stats",
+ note = "added in 24.xx",
+ module = ?MODULE, function = list_cluster_detailed,
+ args = [],
+ result_example = [{'ejabberd@localhost', "true",
+ "The node ejabberd is started. Status...",
+ 7, 348, 60, none}],
+ result = {nodes, {list, {node, {tuple, [{name, atom},
+ {running, string},
+ {status, string},
+ {online_users, integer},
+ {processes, integer},
+ {uptime_seconds, integer},
+ {master_node, atom}
+ ]}}}}},
#ejabberd_commands{name = import_file, tags = [mnesia],
desc = "Import user data from jabberd14 spool file",
@@ -377,6 +416,12 @@ get_commands_spec() ->
args_example = ["example.com", "/var/lib/ejabberd/example.com.sql"],
args = [{host, string}, {file, string}],
result = {res, rescode}},
+ #ejabberd_commands{name = get_master, tags = [cluster],
+ desc = "Get master node of the clustered Mnesia tables",
+ note = "added in 24.xx",
+ longdesc = "If there is no master, returns `none`.",
+ module = ?MODULE, function = get_master,
+ result = {nodename, atom}},
#ejabberd_commands{name = set_master, tags = [cluster],
desc = "Set master node of the clustered Mnesia tables",
longdesc = "If `nodename` is set to `self`, then this "
@@ -472,10 +517,101 @@ get_commands_spec() ->
desc = "Generate Unix manpage for current ejabberd version",
note = "added in 20.01",
module = ejabberd_doc, function = man,
- args = [], result = {res, restuple}}
+ args = [], result = {res, restuple}},
+
+ #ejabberd_commands{name = webadmin_host_user_queue, tags = [internal],
+ desc = "Generate WebAdmin offline queue HTML",
+ module = mod_offline, function = webadmin_host_user_queue,
+ args = [{user, binary}, {host, binary}, {query, any}, {lang, binary}],
+ result = {res, any}},
+
+ #ejabberd_commands{name = webadmin_host_last_activity, tags = [internal],
+ desc = "Generate WebAdmin Last Activity HTML",
+ module = ejabberd_web_admin, function = webadmin_host_last_activity,
+ args = [{host, binary}, {query, any}, {lang, binary}],
+ result = {res, any}},
+ #ejabberd_commands{name = webadmin_host_srg, tags = [internal],
+ desc = "Generate WebAdmin Shared Roster Group HTML",
+ module = mod_shared_roster, function = webadmin_host_srg,
+ args = [{host, binary}, {query, any}, {lang, binary}],
+ result = {res, any}},
+ #ejabberd_commands{name = webadmin_host_srg_group, tags = [internal],
+ desc = "Generate WebAdmin Shared Roster Group HTML for a group",
+ module = mod_shared_roster, function = webadmin_host_srg_group,
+ args = [{host, binary}, {group, binary}, {query, any}, {lang, binary}],
+ result = {res, any}},
+
+ #ejabberd_commands{name = webadmin_node_contrib, tags = [internal],
+ desc = "Generate WebAdmin ejabberd-contrib HTML",
+ module = ext_mod, function = webadmin_node_contrib,
+ args = [{node, atom}, {query, any}, {lang, binary}],
+ result = {res, any}},
+ #ejabberd_commands{name = webadmin_node_db, tags = [internal],
+ desc = "Generate WebAdmin Mnesia database HTML",
+ module = ejabberd_web_admin, function = webadmin_node_db,
+ args = [{node, atom}, {query, any}, {lang, binary}],
+ result = {res, any}},
+ #ejabberd_commands{name = webadmin_node_db_table, tags = [internal],
+ desc = "Generate WebAdmin Mnesia database HTML for a table",
+ module = ejabberd_web_admin, function = webadmin_node_db_table,
+ args = [{node, atom}, {table, binary}, {lang, binary}],
+ result = {res, any}},
+ #ejabberd_commands{name = webadmin_node_db_table_page, tags = [internal],
+ desc = "Generate WebAdmin Mnesia database HTML for a table content",
+ module = ejabberd_web_admin, function = webadmin_node_db_table_page,
+ args = [{node, atom}, {table, binary}, {page, integer}],
+ result = {res, any}},
+
+ #ejabberd_commands{name = mnesia_list_tables, tags = [internal, mnesia],
+ desc = "List of Mnesia tables",
+ module = ?MODULE, function = mnesia_list_tables,
+ result = {tables, {list, {table, {tuple, [{name, atom},
+ {storage_type, binary},
+ {elements, integer},
+ {memory_kb, integer},
+ {memory_mb, integer}
+ ]}}}}},
+ #ejabberd_commands{name = mnesia_table_details, tags = [internal, mnesia],
+ desc = "Get details of a Mnesia table",
+ module = ?MODULE, function = mnesia_table_details,
+ args = [{table, binary}],
+ result = {details, {list, {detail, {tuple, [{name, atom},
+ {value, binary}
+ ]}}}}},
+
+ #ejabberd_commands{name = mnesia_table_change_storage, tags = [internal, mnesia],
+ desc = "Change storage type of a Mnesia table to: ram_copies, disc_copies, or disc_only_copies.",
+ module = ?MODULE, function = mnesia_table_change_storage,
+ args = [{table, binary}, {storage_type, binary}],
+ result = {res, restuple}},
+ #ejabberd_commands{name = mnesia_table_clear, tags = [internal, mnesia],
+ desc = "Delete all content in a Mnesia table",
+ module = ?MODULE, function = mnesia_table_clear,
+ args = [{table, binary}],
+ result = {res, restuple}},
+ #ejabberd_commands{name = mnesia_table_destroy, tags = [internal, mnesia],
+ desc = "Destroy a Mnesia table",
+ module = ?MODULE, function = mnesia_table_destroy,
+ args = [{table, binary}],
+ result = {res, restuple}},
+ #ejabberd_commands{name = echo, tags = [internal],
+ desc = "Return the same sentence that was provided",
+ module = ?MODULE, function = echo,
+ args_desc = ["Sentence to echoe"],
+ args_example = [<<"Test Sentence">>],
+ args = [{sentence, binary}],
+ result = {sentence, string},
+ result_example = "Test Sentence"},
+ #ejabberd_commands{name = echo3, tags = [internal],
+ desc = "Return the same sentence that was provided",
+ module = ?MODULE, function = echo3,
+ args_desc = ["First argument", "Second argument", "Sentence to echoe"],
+ args_example = [<<"example.com">>, <<"Group1">>, <<"Test Sentence">>],
+ args = [{first, binary}, {second, binary}, {sentence, binary}],
+ result = {sentence, string},
+ result_example = "Test Sentence"}
].
-
%%%
%%% Server management
%%%
@@ -491,7 +627,7 @@ status() ->
{value, {_, _, Version}} ->
{ok, io_lib:format("ejabberd ~s is running in that node", [Version])}
end,
- {Is_running, String1 ++ String2}.
+ {Is_running, String1 ++ " " ++String2}.
stop() ->
_ = supervisor:terminate_child(ejabberd_sup, ejabberd_sm),
@@ -583,8 +719,14 @@ update_list() ->
[atom_to_list(Beam) || Beam <- UpdatedBeams].
update("all") ->
- [update_module(ModStr) || ModStr <- update_list()],
- {ok, []};
+ ResList = [{ModStr, update_module(ModStr)} || ModStr <- update_list()],
+ String = case string:join([Mod || {Mod, {ok, _}} <- ResList], ", ") of
+ [] ->
+ "No modules updated";
+ ModulesString ->
+ "Updated modules: " ++ ModulesString
+ end,
+ {ok, String};
update(ModStr) ->
update_module(ModStr).
@@ -593,7 +735,10 @@ update_module(ModuleNameBin) when is_binary(ModuleNameBin) ->
update_module(ModuleNameString) ->
ModuleName = list_to_atom(ModuleNameString),
case ejabberd_update:update([ModuleName]) of
- {ok, _Res} -> {ok, []};
+ {ok, []} ->
+ {ok, "Not updated: "++ModuleNameString};
+ {ok, [{ModuleName, _}]} ->
+ {ok, "Updated: "++ModuleNameString};
{error, Reason} -> {error, Reason}
end.
@@ -681,7 +826,25 @@ convert_to_yaml(In, Out) ->
%%%
join_cluster(NodeBin) ->
- ejabberd_cluster:join(list_to_atom(binary_to_list(NodeBin))).
+ Node = list_to_atom(binary_to_list(NodeBin)),
+ IsNodes = lists:member(Node, ejabberd_cluster:get_nodes()),
+ IsKnownNodes = lists:member(Node, ejabberd_cluster:get_known_nodes()),
+ Ping = net_adm:ping(Node),
+ join_cluster(Node, IsNodes, IsKnownNodes, Ping).
+
+join_cluster(_Node, true, _IsKnownNodes, _Ping) ->
+ {error, "This node already joined that running node."};
+join_cluster(_Node, _IsNodes, true, _Ping) ->
+ {error, "This node already joined that known node."};
+join_cluster(_Node, _IsNodes, _IsKnownNodes, pang) ->
+ {error, "This node cannot reach that node."};
+join_cluster(Node, false, false, pong) ->
+ case timer:apply_after(1000, ejabberd_cluster, join, [Node]) of
+ {ok, _} ->
+ {ok, "Trying to join the cluster, wait a few seconds and check the list of nodes."};
+ Error ->
+ {error, io_lib:format("Can't join cluster: ~p", [Error])}
+ end.
leave_cluster(NodeBin) ->
ejabberd_cluster:leave(list_to_atom(binary_to_list(NodeBin))).
@@ -689,6 +852,33 @@ leave_cluster(NodeBin) ->
list_cluster() ->
ejabberd_cluster:get_nodes().
+list_cluster_detailed() ->
+ KnownNodes = ejabberd_cluster:get_known_nodes(),
+ RunningNodes = ejabberd_cluster:get_nodes(),
+ [get_cluster_node_details(Node, RunningNodes) || Node <- KnownNodes].
+
+get_cluster_node_details(Node, RunningNodes) ->
+ get_cluster_node_details2(Node, lists:member(Node, RunningNodes)).
+
+get_cluster_node_details2(Node, false) ->
+ {Node, "false", "", -1, -1, -1, "unknown"};
+get_cluster_node_details2(Node, true) ->
+ try ejabberd_cluster:call(Node, ejabberd_admin, get_cluster_node_details3, []) of
+ Result -> Result
+ catch
+ E:R ->
+ Status = io_lib:format("~p: ~p", [E, R]),
+ {Node, "true", Status, -1, -1, -1, "unknown"}
+ end.
+
+get_cluster_node_details3() ->
+ {ok, StatusString} = status(),
+ UptimeSeconds = mod_admin_extra:stats(<<"uptimeseconds">>),
+ Processes = mod_admin_extra:stats(<<"processes">>),
+ OnlineUsers = mod_admin_extra:stats(<<"onlineusersnode">>),
+ GetMaster = get_master(),
+ {node(), "true", StatusString, OnlineUsers, Processes, UptimeSeconds, GetMaster}.
+
%%%
%%% Migration management
%%%
@@ -791,6 +981,12 @@ delete_old_messages_abort(Server) ->
%%% Mnesia management
%%%
+get_master() ->
+ case mnesia:table_info(session, master_nodes) of
+ [] -> none;
+ [Node] -> Node
+ end.
+
set_master("self") ->
set_master(node());
set_master(NodeString) when is_list(NodeString) ->
@@ -798,7 +994,7 @@ set_master(NodeString) when is_list(NodeString) ->
set_master(Node) when is_atom(Node) ->
case mnesia:set_master_nodes([Node]) of
ok ->
- {ok, ""};
+ {ok, "ok"};
{error, Reason} ->
String = io_lib:format("Can't set master node ~p at node ~p:~n~p",
[Node, node(), Reason]),
@@ -1008,3 +1204,222 @@ is_my_host(Host) ->
try ejabberd_router:is_my_host(Host)
catch _:{invalid_domain, _} -> false
end.
+
+%%%
+%%% Internal
+%%%
+
+%% @format-begin
+
+%% mnesia:del_table_copy(Table, Node);
+%% mnesia:change_table_copy_type(Table, Node, Type);
+
+mnesia_table_change_storage(STable, SType) ->
+ Table = binary_to_existing_atom(STable, latin1),
+ Type =
+ case SType of
+ <<"ram_copies">> ->
+ ram_copies;
+ <<"disc_copies">> ->
+ disc_copies;
+ <<"disc_only_copies">> ->
+ disc_only_copies;
+ _ ->
+ false
+ end,
+ mnesia:add_table_copy(Table, node(), Type).
+
+mnesia_table_clear(STable) ->
+ Table = binary_to_existing_atom(STable, latin1),
+ mnesia:clear_table(Table).
+
+mnesia_table_delete(STable) ->
+ Table = binary_to_existing_atom(STable, latin1),
+ mnesia:delete_table(Table).
+
+mnesia_table_details(STable) ->
+ Table = binary_to_existing_atom(STable, latin1),
+ [{Name, iolist_to_binary(str:format("~p", [Value]))}
+ || {Name, Value} <- mnesia:table_info(Table, all)].
+
+mnesia_list_tables() ->
+ STables =
+ lists:sort(
+ mnesia:system_info(tables)),
+ lists:map(fun(Table) ->
+ TInfo = mnesia:table_info(Table, all),
+ {value, {storage_type, Type}} = lists:keysearch(storage_type, 1, TInfo),
+ {value, {size, Size}} = lists:keysearch(size, 1, TInfo),
+ {value, {memory, Memory}} = lists:keysearch(memory, 1, TInfo),
+ MemoryB = Memory * erlang:system_info(wordsize),
+ MemoryKB = MemoryB div 1024,
+ MemoryMB = MemoryKB div 1024,
+ {Table, storage_type_bin(Type), Size, MemoryKB, MemoryMB}
+ end,
+ STables).
+
+storage_type_bin(ram_copies) ->
+ <<"RAM copy">>;
+storage_type_bin(disc_copies) ->
+ <<"RAM and disc copy">>;
+storage_type_bin(disc_only_copies) ->
+ <<"Disc only copy">>;
+storage_type_bin(unknown) ->
+ <<"Remote copy">>.
+
+echo(Sentence) ->
+ Sentence.
+
+echo3(_, _, Sentence) ->
+ Sentence.
+
+%%%
+%%% Web Admin: Main
+%%%
+
+web_menu_main(Acc, _Lang) ->
+ Acc ++ [{<<"purge">>, <<"Purge">>}, {<<"stanza">>, <<"Stanza">>}].
+
+web_page_main(_, #request{path = [<<"purge">>]} = R) ->
+ Types =
+ [{<<"#erlang">>, <<"Erlang">>},
+ {<<"#users">>, <<"Users">>},
+ {<<"#offline">>, <<"Offline">>},
+ {<<"#mam">>, <<"MAM">>},
+ {<<"#pubsub">>, <<"PubSub">>},
+ {<<"#push">>, <<"Push">>}],
+ Head = [?XC(<<"h1">>, <<"Purge">>)],
+ Set = [?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- Types]),
+ ?X(<<"hr">>),
+ ?XAC(<<"h2">>, [{<<"id">>, <<"erlang">>}], <<"Erlang">>),
+ ?XE(<<"blockquote">>,
+ [ejabberd_web_admin:make_command(clear_cache, R),
+ ejabberd_web_admin:make_command(gc, R)]),
+ ?X(<<"hr">>),
+ ?XAC(<<"h2">>, [{<<"id">>, <<"users">>}], <<"Users">>),
+ ?XE(<<"blockquote">>, [ejabberd_web_admin:make_command(delete_old_users, R)]),
+ ?X(<<"hr">>),
+ ?XAC(<<"h2">>, [{<<"id">>, <<"offline">>}], <<"Offline">>),
+ ?XE(<<"blockquote">>,
+ [ejabberd_web_admin:make_command(delete_expired_messages, R),
+ ejabberd_web_admin:make_command(delete_old_messages, R),
+ ejabberd_web_admin:make_command(delete_old_messages_batch, R),
+ ejabberd_web_admin:make_command(delete_old_messages_status, R)]),
+ ?X(<<"hr">>),
+ ?XAC(<<"h2">>, [{<<"id">>, <<"mam">>}], <<"MAM">>),
+ ?XE(<<"blockquote">>,
+ [ejabberd_web_admin:make_command(delete_old_mam_messages, R),
+ ejabberd_web_admin:make_command(delete_old_mam_messages_batch, R),
+ ejabberd_web_admin:make_command(delete_old_mam_messages_status, R)]),
+ ?X(<<"hr">>),
+ ?XAC(<<"h2">>, [{<<"id">>, <<"pubsub">>}], <<"PubSub">>),
+ ?XE(<<"blockquote">>,
+ [ejabberd_web_admin:make_command(delete_expired_pubsub_items, R),
+ ejabberd_web_admin:make_command(delete_old_pubsub_items, R)]),
+ ?X(<<"hr">>),
+ ?XAC(<<"h2">>, [{<<"id">>, <<"push">>}], <<"Push">>),
+ ?XE(<<"blockquote">>, [ejabberd_web_admin:make_command(delete_old_push_sessions, R)])],
+ {stop, Head ++ Set};
+web_page_main(_, #request{path = [<<"stanza">>]} = R) ->
+ Head = [?XC(<<"h1">>, <<"Stanza">>)],
+ Set = [ejabberd_web_admin:make_command(send_message, R),
+ ejabberd_web_admin:make_command(send_stanza, R),
+ ejabberd_web_admin:make_command(send_stanza_c2s, R)],
+ {stop, Head ++ Set};
+web_page_main(Acc, _) ->
+ Acc.
+
+%%%
+%%% Web Admin: Node
+%%%
+
+web_menu_node(Acc, _Node, _Lang) ->
+ Acc
+ ++ [{<<"cluster">>, <<"Clustering">>},
+ {<<"update">>, <<"Code Update">>},
+ {<<"config-file">>, <<"Configuration File">>},
+ {<<"logs">>, <<"Logs">>},
+ {<<"stop">>, <<"Stop Node">>}].
+
+web_page_node(_, Node, #request{path = [<<"cluster">>]} = R) ->
+ {ok, Names} = net_adm:names(),
+ NodeNames = lists:join(", ", [Name || {Name, _Port} <- Names]),
+ Hint =
+ list_to_binary(io_lib:format("Hint: Erlang nodes found in this machine that may be running ejabberd: ~s",
+ [NodeNames])),
+ Head = ?H1GLraw(<<"Clustering">>, <<"admin/guide/clustering/">>, <<"Clustering">>),
+ Set1 =
+ [ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [join_cluster, R, [], [{style, danger}]]),
+ ?XE(<<"blockquote">>, [?C(Hint)]),
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [leave_cluster, R, [], [{style, danger}]])],
+ Set2 =
+ [ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [set_master, R, [], [{style, danger}]])],
+ timer:sleep(100), % leaving a cluster takes a while, let's delay the get commands
+ Get1 =
+ [ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [list_cluster_detailed,
+ R,
+ [],
+ [{result_links, [{name, node, 3, <<"">>}]}]])],
+ Get2 =
+ [ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [get_master,
+ R,
+ [],
+ [{result_named, true},
+ {result_links, [{nodename, node, 3, <<"">>}]}]])],
+ {stop, Head ++ Get1 ++ Set1 ++ Get2 ++ Set2};
+web_page_node(_, Node, #request{path = [<<"update">>]} = R) ->
+ Head = [?XC(<<"h1">>, <<"Code Update">>)],
+ Set = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [update, R])],
+ Get = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [update_list, R])],
+ {stop, Head ++ Get ++ Set};
+web_page_node(_, Node, #request{path = [<<"config-file">>]} = R) ->
+ Res = ?H1GLraw(<<"Configuration File">>,
+ <<"admin/configuration/file-format/">>,
+ <<"File Format">>)
+ ++ [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [convert_to_yaml, R]),
+ ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [dump_config, R]),
+ ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [reload_config, R])],
+ {stop, Res};
+web_page_node(_, Node, #request{path = [<<"stop">>]} = R) ->
+ Res = [?XC(<<"h1">>, <<"Stop This Node">>),
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [restart, R, [], [{style, danger}]]),
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [stop_kindly, R, [], [{style, danger}]]),
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [stop, R, [], [{style, danger}]]),
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [halt, R, [], [{style, danger}]])],
+ {stop, Res};
+web_page_node(_, Node, #request{path = [<<"logs">>]} = R) ->
+ Res = ?H1GLraw(<<"Logs">>, <<"admin/configuration/basic/#logging">>, <<"Logging">>)
+ ++ [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [set_loglevel, R]),
+ ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [get_loglevel, R]),
+ ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [reopen_log, R]),
+ ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [rotate_log, R])],
+ {stop, Res};
+web_page_node(Acc, _, _) ->
+ Acc.
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
index 9323ad6561b..509f3622ddf 100644
--- a/src/ejabberd_commands.erl
+++ b/src/ejabberd_commands.erl
@@ -188,7 +188,9 @@ list_commands(Version) ->
Commands = get_commands_definition(Version),
[{Name, Args, Desc} || #ejabberd_commands{name = Name,
args = Args,
- desc = Desc} <- Commands].
+ tags = Tags,
+ desc = Desc} <- Commands,
+ not lists:member(internal, Tags)].
-spec get_command_format(atom()) -> {[aterm()], [{atom(),atom()}], rterm()}.
@@ -264,10 +266,16 @@ execute_command2(Name, Arguments, CallerInfo) ->
execute_command2(Name, Arguments, CallerInfo, Version) ->
Command = get_command_definition(Name, Version),
- case ejabberd_access_permissions:can_access(Name, CallerInfo) of
- allow ->
+ FrontedCalledInternal =
+ maps:get(caller_module, CallerInfo, none) /= ejabberd_web_admin
+ andalso lists:member(internal, Command#ejabberd_commands.tags),
+ case {ejabberd_access_permissions:can_access(Name, CallerInfo),
+ FrontedCalledInternal} of
+ {allow, false} ->
do_execute_command(Command, Arguments);
- _ ->
+ {_, true} ->
+ throw({error, frontend_cannot_call_an_internal_command});
+ {deny, false} ->
throw({error, access_rules_unauthorized})
end.
@@ -289,7 +297,8 @@ get_tags_commands() ->
get_tags_commands(Version) ->
CommandTags = [{Name, Tags} ||
#ejabberd_commands{name = Name, tags = Tags}
- <- get_commands_definition(Version)],
+ <- get_commands_definition(Version),
+ not lists:member(internal, Tags)],
Dict = lists:foldl(
fun({CommandNameAtom, CTags}, D) ->
CommandName = atom_to_list(CommandNameAtom),
diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl
index d42ac23931e..aad3e0eb3aa 100644
--- a/src/ejabberd_ctl.erl
+++ b/src/ejabberd_ctl.erl
@@ -32,7 +32,8 @@
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
--export([get_commands_spec/0]).
+-export([get_commands_spec/0, format_arg/2,
+ get_usage_command/4]).
-include("ejabberd_ctl.hrl").
-include("ejabberd_commands.hrl").
@@ -366,9 +367,9 @@ format_arg(Arg, string) ->
Parse = "~" ++ NumChars ++ "c",
format_arg2(Arg, Parse);
format_arg(Arg, {list, {_ArgName, ArgFormat}}) ->
- [format_arg(Element, ArgFormat) || Element <- string:tokens(Arg, ",")];
+ [format_arg(string:trim(Element), ArgFormat) || Element <- string:tokens(Arg, ",")];
format_arg(Arg, {list, ArgFormat}) ->
- [format_arg(Element, ArgFormat) || Element <- string:tokens(Arg, ",")];
+ [format_arg(string:trim(Element), ArgFormat) || Element <- string:tokens(Arg, ",")];
format_arg(Arg, {tuple, Elements}) ->
Args = string:tokens(Arg, ":"),
list_to_tuple(format_args(Args, Elements));
@@ -786,7 +787,7 @@ print_usage_help(MaxC, ShCode) ->
longdesc = lists:flatten(LongDesc),
args = ArgsDef,
result = {help, string}},
- print_usage_command2("help", C, MaxC, ShCode).
+ print(get_usage_command2("help", C, MaxC, ShCode), []).
%%-----------------------------
@@ -848,11 +849,14 @@ maybe_add_policy_arguments(Args, _) ->
-spec print_usage_command(Cmd::string(), MaxC::integer(),
ShCode::boolean(), Version::integer()) -> ok.
print_usage_command(Cmd, MaxC, ShCode, Version) ->
+ print(get_usage_command(Cmd, MaxC, ShCode, Version), []).
+
+get_usage_command(Cmd, MaxC, ShCode, Version) ->
Name = list_to_atom(Cmd),
C = ejabberd_commands:get_command_definition(Name, Version),
- print_usage_command2(Cmd, C, MaxC, ShCode).
+ get_usage_command2(Cmd, C, MaxC, ShCode).
-print_usage_command2(Cmd, C, MaxC, ShCode) ->
+get_usage_command2(Cmd, C, MaxC, ShCode) ->
#ejabberd_commands{
tags = TagsAtoms,
definer = Definer,
@@ -926,12 +930,12 @@ print_usage_command2(Cmd, C, MaxC, ShCode) ->
false -> ""
end,
- case Cmd of
- "help" -> ok;
- _ -> print([NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt,
- "\n\n", ExampleFmt, TagsFmt, "\n\n", ModuleFmt, NoteFmt, DescFmt, "\n\n"], [])
+ First = case Cmd of
+ "help" -> "";
+ _ -> [NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt,
+ "\n\n", ExampleFmt, TagsFmt, "\n\n", ModuleFmt, NoteFmt, DescFmt, "\n\n"]
end,
- print([LongDescFmt, NoteEjabberdctlList, NoteEjabberdctlTuple], []).
+ [First, LongDescFmt, NoteEjabberdctlList, NoteEjabberdctlTuple].
%%-----------------------------
%% Format Arguments Help
@@ -972,11 +976,14 @@ format_usage_ctype1({Name, Type, Description}, Indentation, ShCode) ->
format_usage_ctype(Type, _Indentation)
- when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary) or (Type==rescode) or (Type==restuple)->
+ when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary)
+ or (Type==rescode) or (Type==restuple) ->
io_lib:format("~p", [Type]);
format_usage_ctype({Name, Type}, _Indentation)
- when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary) or (Type==rescode) or (Type==restuple)->
+ when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary)
+ or (Type==rescode) or (Type==restuple)
+ or (Type==any) ->
io_lib:format("~p::~p", [Name, Type]);
format_usage_ctype({Name, {list, ElementDef}}, Indentation) ->
diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl
index 96c664acf18..8c42dbe775e 100644
--- a/src/ejabberd_oauth.erl
+++ b/src/ejabberd_oauth.erl
@@ -54,6 +54,8 @@
oauth_add_client_implicit/3,
oauth_remove_client/1]).
+-export([web_menu_main/2, web_page_main/2]).
+
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
-include("ejabberd_http.hrl").
@@ -230,6 +232,8 @@ init([]) ->
application:set_env(oauth2, expiry_time, Expire div 1000),
application:start(oauth2),
ejabberd_commands:register_commands(get_commands_spec()),
+ ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50),
+ ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50),
ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50),
erlang:send_after(expire(), self(), clean),
{ok, ok}.
@@ -255,6 +259,8 @@ handle_info(Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
+ ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50),
+ ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50),
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50).
code_change(_OldVsn, State, _Extra) -> {ok, State}.
@@ -794,3 +800,30 @@ logo() ->
{error, _} ->
<<>>
end.
+
+%%%
+%%% WebAdmin
+%%%
+
+%% @format-begin
+
+web_menu_main(Acc, _Lang) ->
+ Acc ++ [{<<"oauth">>, <<"OAuth">>}].
+
+web_page_main(_, #request{path = [<<"oauth">>]} = R) ->
+ Head = ?H1GLraw(<<"OAuth">>, <<"developer/ejabberd-api/oauth/">>, <<"OAuth">>),
+ Set = [?X(<<"hr">>),
+ ?XAC(<<"h2">>, [{<<"id">>, <<"token">>}], <<"Token">>),
+ ?XE(<<"blockquote">>,
+ [ejabberd_web_admin:make_command(oauth_list_tokens, R),
+ ejabberd_web_admin:make_command(oauth_issue_token, R),
+ ejabberd_web_admin:make_command(oauth_revoke_token, R)]),
+ ?X(<<"hr">>),
+ ?XAC(<<"h2">>, [{<<"id">>, <<"client">>}], <<"Client">>),
+ ?XE(<<"blockquote">>,
+ [ejabberd_web_admin:make_command(oauth_add_client_implicit, R),
+ ejabberd_web_admin:make_command(oauth_add_client_password, R),
+ ejabberd_web_admin:make_command(oauth_remove_client, R)])],
+ {stop, Head ++ Set};
+web_page_main(Acc, _) ->
+ Acc.
diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl
index b986d540027..93973dba187 100644
--- a/src/ejabberd_sm.erl
+++ b/src/ejabberd_sm.erl
@@ -62,6 +62,7 @@
user_resources/2,
kick_user/2,
kick_user/3,
+ kick_user_restuple/2,
get_session_pid/3,
get_session_sid/3,
get_session_sids/2,
@@ -1047,7 +1048,19 @@ get_commands_spec() ->
args_example = [<<"user1">>, <<"example.com">>],
result_desc = "Number of resources that were kicked",
result_example = 3,
- result = {num_resources, integer}}].
+ result = {num_resources, integer}},
+
+ #ejabberd_commands{name = kick_user, tags = [session],
+ desc = "Disconnect user's active sessions",
+ module = ?MODULE, function = kick_user_restuple,
+ version = 2,
+ note = "modified in 24.xx",
+ args = [{user, binary}, {host, binary}],
+ args_desc = ["User name", "Server name"],
+ args_example = [<<"user1">>, <<"example.com">>],
+ result_desc = "The result text indicates the number of sessions that were kicked",
+ result_example = {ok, <<"Kicked sessions: 2">>},
+ result = {res, restuple}}].
-spec connected_users() -> [binary()].
@@ -1082,5 +1095,9 @@ kick_user(User, Server, Resource) ->
Pid -> ejabberd_c2s:route(Pid, kick)
end.
+kick_user_restuple(User, Server) ->
+ NumberBin = integer_to_binary(kick_user(User, Server)),
+ {ok, <<"Kicked sessions: ", NumberBin/binary>>}.
+
make_sid() ->
{misc:unique_timestamp(), self()}.
diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl
index 742ac262c94..37150abefdc 100644
--- a/src/ejabberd_web_admin.erl
+++ b/src/ejabberd_web_admin.erl
@@ -29,18 +29,20 @@
-author('alexey@process-one.net').
--export([process/2, list_users/4,
- list_users_in_diapason/4, pretty_print_xml/1,
- term_to_id/1]).
+-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]).
--include("logger.hrl").
+%% 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),
@@ -61,25 +63,27 @@ 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]};
+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, webadmin_view]};
+ {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, webadmin_view]};
+ {global, [configure]};
get_acl_rule(_RPath, 'POST') ->
{global, [configure]}.
@@ -104,7 +108,7 @@ get_menu_items(global, cluster, Lang, JID, Level) ->
end,
Items);
get_menu_items(Host, cluster, Lang, JID, Level) ->
- {_Base, _, Items} = make_host_menu(Host, [], Lang, JID, Level),
+ {_Base, _, Items} = make_host_menu(Host, [], [], Lang, JID, Level),
lists:map(fun ({URI, Name}) ->
{<>, Name};
({URI, Name, _SubMenu}) ->
@@ -174,8 +178,7 @@ process2([<<"server">>, SHost | RPath] = Path,
{401,
[{<<"WWW-Authenticate">>,
<<"basic realm=\"ejabberd\"">>}],
- ejabberd_web:make_xhtml([?XCT(<<"h1">>,
- ?T("Unauthorized"))])};
+ ejabberd_web:make_xhtml(make_unauthorized(Lang))};
{unauthorized, Error} ->
{BadUser, _BadPass} = Auth,
{IPT, _Port} = Request#request.ip,
@@ -186,8 +189,7 @@ process2([<<"server">>, SHost | RPath] = Path,
[{<<"WWW-Authenticate">>,
<<"basic realm=\"auth error, retry login "
"to ejabberd\"">>}],
- ejabberd_web:make_xhtml([?XCT(<<"h1">>,
- ?T("Unauthorized"))])}
+ ejabberd_web:make_xhtml(make_unauthorized(Lang))}
end;
false -> ejabberd_web:error(not_found)
end;
@@ -206,8 +208,7 @@ process2(RPath,
{401,
[{<<"WWW-Authenticate">>,
<<"basic realm=\"ejabberd\"">>}],
- ejabberd_web:make_xhtml([?XCT(<<"h1">>,
- ?T("Unauthorized"))])};
+ ejabberd_web:make_xhtml(make_unauthorized(Lang))};
{unauthorized, Error} ->
{BadUser, _BadPass} = Auth,
{IPT, _Port} = Request#request.ip,
@@ -218,10 +219,14 @@ process2(RPath,
[{<<"WWW-Authenticate">>,
<<"basic realm=\"auth error, retry login "
"to ejabberd\"">>}],
- ejabberd_web:make_xhtml([?XCT(<<"h1">>,
- ?T("Unauthorized"))])}
+ 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} ->
@@ -273,18 +278,26 @@ get_auth_account2(HostOfRule, AccessRule, User, Server,
%%%% make_xhtml
make_xhtml(Els, Host, Lang, JID, Level) ->
- make_xhtml(Els, Host, cluster, Lang, JID, Level).
+ make_xhtml(Els, Host, cluster, unspecified, Lang, JID, Level).
+
+make_xhtml(Els, Host, Username, Lang, JID, Level) when
+ (Username == unspecified) or (is_binary(Username)) ->
+ make_xhtml(Els, Host, cluster, Username, Lang, JID, Level);
+
+make_xhtml(Els, Host, Node, Lang, JID, Level) ->
+ make_xhtml(Els, Host, Node, unspecified, Lang, JID, Level).
-spec make_xhtml([xmlel()],
Host::global | binary(),
Node::cluster | atom(),
+ Username::unspecified | binary(),
Lang::binary(),
jid(),
Level::integer()) ->
{200, [html], xmlel()}.
-make_xhtml(Els, Host, Node, Lang, JID, Level) ->
+make_xhtml(Els, Host, Node, Username, Lang, JID, Level) ->
Base = get_base_path_sum(0, 0, Level),
- MenuItems = make_navigation(Host, Node, Lang, JID, Level),
+ MenuItems = make_navigation(Host, Node, Username, Lang, JID, Level),
{200, [html],
#xmlel{name = <<"html">>,
attrs =
@@ -319,7 +332,20 @@ make_xhtml(Els, Host, Node, Lang, JID, Level) ->
<>},
{<<"type">>, <<"text/css">>},
{<<"rel">>, <<"stylesheet">>}],
- children = []}]},
+ children = []},
+ #xmlel{name = <<"link">>,
+ attrs =
+ [{<<"href">>,
+ <>},
+ {<<"type">>, <<"text/css">>},
+ {<<"rel">>, <<"stylesheet">>}],
+ children = []},
+ #xmlel{name = <<"script">>,
+ attrs =
+ [{<<"src">>,
+ <>},
+ {<<"type">>, <<"text/javascript">>}],
+ children = [?C(<<" ">>)]}]},
?XE(<<"body">>,
[?XAE(<<"div">>, [{<<"id">>, <<"container">>}],
[?XAE(<<"div">>, [{<<"id">>, <<"header">>}],
@@ -387,25 +413,59 @@ logo() ->
{error, _} -> <<>>
end.
-logo_fill() ->
- case misc:read_img("admin-logo-fill.png") of
- {ok, Img} -> Img;
+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}, AJID) ->
+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),
- make_xhtml((?H1GL((translate:translate(Lang, ?T("Administration"))), <<"">>,
- <<"Configuration">>))
- ++ Disclaimer ++
- [?XE(<<"ul">>,
- [?LI([?ACT(MIU, MIN)])
- || {MIU, MIN}
- <- MenuItems])],
+ WelcomeText =
+ [?BR,
+ ?XAE(<<"p">>, [{<<"align">>, <<"center">>}],
+ [?XA(<<"img">>, [{<<"src">>, <<"logo.png">>},
+ {<<"style">>, <<"border-radius:10px; background:#49cbc1; padding: 1.1em;">>}])
+ ]),
+ ?BR,
+ ?X(<<"hr">>)] ++ Title ++
+ [?XE(<<"blockquote">>,
+ [
+ ?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)]),
+ ?X(<<"hr">>), ?BR],
+ make_xhtml(Disclaimer ++ WelcomeText ++
+ [?XE(<<"ul">>,
+ [?LI([?ACT(MIU, MIN)])
+ || {MIU, MIN}
+ <- MenuItems])],
global, Lang, AJID, 0);
process_admin(Host, #request{path = [], lang = Lang}, AJID) ->
make_xhtml([?XCT(<<"h1">>, ?T("Administration")),
@@ -414,6 +474,7 @@ process_admin(Host, #request{path = [], lang = Lang}, AJID) ->
|| {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(),
@@ -429,203 +490,228 @@ process_admin(_Host, #request{path = [<<"logo.png">>]}, _) ->
[{<<"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)
+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,
+ Lang,
+ AJID,
+ Level);
+process_admin(Host,
+ #request{path = [<<"users">>, <<"diapason">>, Diap | RPath], lang = Lang} = R,
+ 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)
+ 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, Lang, AJID, Level);
+process_admin(Host,
+ #request{path = [<<"users">>, <<"top">>, Attribute | RPath], lang = Lang} = R,
+ 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)
+ 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, Lang, AJID, Level);
+process_admin(Host, #request{path = [<<"users">> | RPath], lang = Lang} = R, 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)
+ 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, Lang, AJID, Level);
+process_admin(Host, #request{path = [<<"online-users">> | RPath], lang = Lang} = R, 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) ->
+ Level = 3 + length(RPath),
+ Res = [make_command(connected_users_vhost,
+ R,
+ [{<<"host">>, Host}],
+ [{table_options, {2, RPath}},
+ {result_links, [{sessions, user, Level, <<"">>}]}])],
+ make_xhtml([?XCT(<<"h1">>, ?T("Online Users"))] ++ Res, Host, Lang, 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, Lang, 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, Query, Lang),
- make_xhtml(Res, Host, Lang, AJID, 4);
- false ->
- make_xhtml([?XCT(<<"h1">>, ?T("Not Found"))], Host,
- Lang, AJID, 4)
+ true ->
+ Res = user_info(U, Host, R),
+ make_xhtml(Res, Host, U, 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,
+process_admin(Host, #request{path = [<<"nodes">>], lang = Lang} = 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, Lang, AJID, Level);
-process_admin(Host, #request{path = [<<"node">>, SNode | NPath],
- q = Query, lang = Lang}, AJID) ->
+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,
- 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)
+ false ->
+ make_xhtml([?XCT(<<"h1">>, ?T("Node not found"))], Host, Lang, 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, 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,
+process_admin(Host, #request{path = Path, lang = Lang} = 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, Lang,
- AJID, Level),
- 404);
- _ -> make_xhtml(Res, Host, Lang, AJID, Level)
+ [] ->
+ setelement(1,
+ make_xhtml([?XC(<<"h1">>, <<"Not Found">>)], Host, Lang, AJID, Level),
+ 404);
+ _ ->
+ make_xhtml(Res, Host, Username, Lang, 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(Lang, JID) ->
- list_vhosts2(Lang, list_vhosts_allowed(JID)).
-
list_vhosts_allowed(JID) ->
Hosts = ejabberd_option:hosts(),
lists:filter(fun (Host) ->
any_rules_allowed(Host,
- [configure, webadmin_view],
+ [configure],
JID)
end,
Hosts).
-list_vhosts2(Lang, Hosts) ->
- SHosts = lists:sort(Hosts),
- [?XE(<<"table">>,
- [?XE(<<"thead">>,
- [?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Host")),
- ?XACT(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- ?T("Registered Users")),
- ?XACT(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- ?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)]),
- ?XAE(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- [?AC(<<"../server/", Host/binary, "/users/">>,
- pretty_string_int(RegisteredUsers))]),
- ?XAE(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- [?AC(<<"../server/", Host/binary, "/online-users/">>,
- pretty_string_int(OnlineUsers))])])
- end,
- SHosts)))])].
-
-maybe_disclaimer_not_admin(MenuItems, AJID, Lang) ->
+maybe_disclaimer_not_admin(MenuItems, AJID, _Lang) ->
case {MenuItems, list_vhosts_allowed(AJID)} of
{[_], []} ->
- [?XREST(?T("Apparently your account has no administration rights in this server. "
- "Please check how to grant admin rights in: "
- "https://docs.ejabberd.im/admin/install/next-steps/#administration-account"))
+ [?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">>)])
];
_ ->
[]
@@ -634,186 +720,173 @@ maybe_disclaimer_not_admin(MenuItems, AJID, Lang) ->
%%%==================================
%%%% 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
+%% @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_in_diapason(Host, Diap, Lang, URLFunc) ->
- Users = ejabberd_auth:get_users(Host),
- SUsers = lists:sort([{S, U} || {U, S} <- Users]),
+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),
- [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")),
- ?XACT(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- ?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)))]),
- ?XAE(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- 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.
+ 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.
-
-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
+ true ->
+ case mod_last_opt:db_type(Server) of
+ mnesia ->
+ [{<<"last-activity">>, ?T("Last Activity")}];
+ _ ->
+ []
+ end;
+ false ->
+ []
end.
us_to_list({User, Server}) ->
@@ -821,157 +894,58 @@ us_to_list({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
-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:")),
- ?XAC(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- (pretty_string_int(RegisteredUsers)))]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Online Users:")),
- ?XAC(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- (pretty_string_int(OnlineUsers)))]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Outgoing s2s Connections:")),
- ?XAC(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- (pretty_string_int(OutS2SNumber)))]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Incoming s2s Connections:")),
- ?XAC(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- (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:")),
- ?XAC(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- (pretty_string_int(RegisteredUsers)))]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Online Users:")),
- ?XAC(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- (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) ->
+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),
- 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,
+ LServer, [], [User, Server, R]),
[?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("User ~ts"),
[us_to_list(US)])))]
++
@@ -983,16 +957,24 @@ user_info(User, Server, Query, Lang) ->
++
[?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"))]))].
+ ([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)
@@ -1002,19 +984,6 @@ user_parse_query(User, Server, Query) ->
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])
@@ -1086,33 +1055,6 @@ histogram([], _Integral, _Current, Count, Hist) ->
%%%==================================
%%%% 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).
@@ -1124,602 +1066,104 @@ search_running_node(SNode, [Node | Nodes]) ->
_ -> search_running_node(SNode, Nodes)
end.
-get_node(global, Node, [], Query, Lang) ->
- Res = node_parse_query(Node, Query),
+get_node(global, Node, [], #request{lang = Lang}) ->
Base = get_base_path(global, Node, 2),
- MenuItems2 = make_menu_items(global, Node, Base, Lang),
+ 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])))]
++
- 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) ->
+ [?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),
+ 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,
- MemoryB = Memory*erlang:system_info(wordsize),
- ?XE(<<"tr">>,
- [?XE(<<"td">>,
- [?AC(<<"./", STable/binary,
- "/">>,
- STable)]),
- ?XE(<<"td">>,
- [db_storage_select(STable, Type,
- Lang)]),
- ?XAE(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- [?AC(<<"./", STable/binary,
- "/1/">>,
- (pretty_string_int(Size)))]),
- ?XAC(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- (pretty_string_int(MemoryB)))])
- 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")),
- ?XACT(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- ?T("Elements")),
- ?XACT(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- ?T("Memory"))])]),
- ?XE(<<"tbody">>,
- (Rows ++
- [?XE(<<"tr">>,
- [?XAE(<<"td">>,
- [{<<"colspan">>, <<"4">>},
- {<<"class">>, <<"alignright">>}],
- [?INPUTT(<<"submit">>,
- <<"submit">>,
- ?T("Submit"))])])]))])])]
- end;
-get_node(global, Node, [<<"db">>, TableName], _Query, Lang) ->
- make_table_view(Node, TableName, Lang);
-get_node(global, Node, [<<"db">>, TableName, PageNumber], _Query, Lang) ->
- make_table_elements_view(Node, TableName, Lang, binary_to_integer(PageNumber));
-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])),
- UpTimeDate = uptime_date(Node),
- 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]),
- ?H1GL(str:translate_and_format(Lang, ?T("Statistics of ~p"), [Node]),
- <<"modules/#mod_stats">>,
- <<"mod_stats">>) ++ [
- ?XAE(<<"table">>, [],
- [?XE(<<"tbody">>,
- [?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Uptime:")),
- ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}],
- UpTimeS)]),
- ?XE(<<"tr">>,
- [?X(<<"td">>),
- ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}],
- UpTimeDate)]),
- ?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) ->
+
+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, NPath, Query, Lang]);
+ [Node, Request]);
_ ->
ejabberd_hooks:run_fold(webadmin_page_hostnode, Host, [],
- [Host, Node, NPath, Query, Lang])
+ [Host, Node, Request])
end,
case Res of
[] -> [?XC(<<"h1">>, <<"Not Found">>)];
_ -> Res
end.
-uptime_date(Node) ->
- Localtime = ejabberd_cluster:call(Node, erlang, localtime, []),
- Now = calendar:datetime_to_gregorian_seconds(Localtime),
- {Wall, _} = ejabberd_cluster:call(Node, erlang, statistics, [wall_clock]),
- LastRestart = Now - (Wall div 1000),
- {{Year, Month, Day}, {Hour, Minute, Second}} =
- calendar:gregorian_seconds_to_datetime(LastRestart),
- str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w",
- [Year, Month, Day, Hour, Minute, Second]).
-
%%%==================================
%%%% 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, <<"">>)).
@@ -1778,12 +1222,8 @@ pretty_print_xml(#xmlel{name = Name, attrs = Attrs,
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}) ->
- <>.
+ <<"diapason/", (integer_to_binary(From))/binary, "-",
+ (integer_to_binary(To))/binary, "/">>.
last_modified() ->
{<<"Last-Modified">>,
@@ -1807,49 +1247,7 @@ pretty_string_int(String) when is_binary(String) ->
%%%==================================
%%%% mnesia table view
-make_table_view(Node, STable, Lang) ->
- 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),
- {value, {memory, Memory}} = lists:keysearch(memory, 1, TInfo),
- MemoryB = Memory*erlang:system_info(wordsize),
- TableInfo = str:format("~p", [TInfo]),
- [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Database Tables at ~p"),
- [Node]))),
- ?XAE(<<"table">>, [],
- [?XE(<<"tbody">>,
- [?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Name")),
- ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}],
- STable
- )]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Node")),
- ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}],
- misc:atom_to_binary(Node)
- )]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Storage Type")),
- ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}],
- misc:atom_to_binary(Type)
- )]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Elements")),
- ?XAE(<<"td">>,
- [{<<"class">>, <<"alignright">>}],
- [?AC(<<"1/">>,
- (pretty_string_int(Size)))])
- ]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Memory")),
- ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}],
- (pretty_string_int(MemoryB))
- )])
- ])]),
- ?XC(<<"pre">>, TableInfo)].
-
-make_table_elements_view(Node, STable, Lang, PageNumber) ->
+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),
@@ -1858,10 +1256,7 @@ make_table_elements_view(Node, STable, Lang, PageNumber) ->
TableContentErl = get_table_content(Node, Table, Type, PageNumber, PageSize),
TableContent = str:format("~p", [TableContentErl]),
PagesLinks = build_elements_pages_list(Size, PageNumber, PageSize),
- [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Database Tables at ~p"),
- [Node]))),
- ?P, ?AC(<<"../">>, STable), ?P
- ] ++ PagesLinks ++ [?XC(<<"pre">>, TableContent)].
+ [?P] ++ PagesLinks ++ [?XC(<<"pre">>, TableContent)].
build_elements_pages_list(Size, PageNumber, PageSize) ->
PagesNumber = calculate_pages_number(Size, PageSize),
@@ -1893,39 +1288,147 @@ get_table_content(Node, Table, _Type, PageNumber, PageSize) ->
|| 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, Lang, JID, Level) ->
- Menu = make_navigation_menu(Host, Node, Lang, JID, Level),
+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, Lang, JID, Level) ->
+make_navigation_menu(Host, Node, Username, Lang, JID, Level) ->
HostNodeMenu = make_host_node_menu(Host, Node, Lang,
JID, Level),
- HostMenu = make_host_menu(Host, HostNodeMenu, Lang,
+ 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(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_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) ->
{<<"">>, <<"">>, []};
@@ -1938,21 +1441,34 @@ make_host_node_menu(Host, Node, Lang, JID, Level) ->
|| Tuple <- HostNodeFixed,
is_allowed_path(Host, Tuple, JID)],
{HostNodeBase, iolist_to_binary(atom_to_list(Node)),
- HostNodeFixed2}.
+ lists:keysort(2, HostNodeFixed2)}.
-make_host_menu(global, _HostNodeMenu, _Lang, _JID, _Level) ->
+make_host_user_menu(global, _, _Lang, _JID, _Level) ->
{<<"">>, <<"">>, []};
-make_host_menu(Host, HostNodeMenu, 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")},
- {<<"online-users">>, ?T("Online Users")}]
- ++
+ HostFixed = [{<<"users">>, ?T("Users"), HostUserMenu},
+ {<<"online-users">>, ?T("Online Users")}],
+ HostFixedAdditional =
get_lastactivity_menuitem_list(Host) ++
- [{<<"nodes">>, ?T("Nodes"), HostNodeMenu},
- {<<"stats">>, ?T("Statistics")}]
+ [{<<"nodes">>, ?T("Nodes"), HostNodeMenu}]
++ get_menu_items_hook({host, Host}, Lang),
+ HostFixedAll = HostFixed ++ lists:keysort(2, HostFixedAdditional),
HostFixed2 = [Tuple
- || Tuple <- HostFixed,
+ || Tuple <- HostFixedAll,
is_allowed_path(Host, Tuple, JID)],
{HostBase, Host, HostFixed2}.
@@ -1960,30 +1476,31 @@ 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")}]
+ NodeFixed = [{<<"db">>, <<"Mnesia Tables">>},
+ {<<"backup">>, <<"Mnesia Backup">>}]
++ get_menu_items_hook({node, Node}, Lang),
{NodeBase, iolist_to_binary(atom_to_list(Node)),
- NodeFixed};
+ 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},
- {<<"stats">>, ?T("Statistics")}]
- ++ get_menu_items_hook(server, Lang),
+ {<<"nodes">>, ?T("Nodes"), NodeMenu}],
+ FixedAdditional = get_menu_items_hook(server, Lang),
+ FixedAll = Fixed ++ lists:keysort(2, FixedAdditional),
Fixed2 = [Tuple
- || Tuple <- Fixed,
+ || 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]);
@@ -2052,4 +1569,897 @@ any_rules_allowed(Host, Access, Entity) ->
allow == acl:match_rule(Host, Rule, Entity)
end, Access).
+%%%==================================
+
+%%% @format-begin
+
+%%%% 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() | {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,
+ host = RHost} =
+ Request,
+ CallerInfo =
+ #{usr => {RUser, RServer, <<"">>},
+ ip => RIp,
+ caller_host => RHost,
+ caller_module => ?MODULE},
+ try {ejabberd_commands:get_command_definition(Name),
+ ejabberd_access_permissions:can_access(Name, CallerInfo)}
+ of
+ {C, allow} ->
+ make_command2(Name,
+ Request,
+ CallerInfo,
+ BaseArguments,
+ C,
+ Only,
+ ForceExecution,
+ InputNameAppend,
+ Resultnamed,
+ ResultLinks,
+ Style,
+ TO);
+ {_C, deny} ->
+ ?DEBUG("Blocked access to command ~p for~n CallerInfo: ~p", [Name, CallerInfo]),
+ ?C(<<"">>)
+ catch
+ A:B ->
+ ?INFO_MSG("Problem preparing command ~p: ~p", [Name, {A, B}]),
+ ?C(<<"">>)
+ end.
+
+make_command2(Name,
+ Request,
+ CallerInfo,
+ BaseArguments,
+ C,
+ Only,
+ ForceExecution,
+ InputNameAppend,
+ Resultnamed,
+ ResultLinks,
+ Style,
+ TO) ->
+ {ArgumentsFormat, _Rename, ResultFormatApi} = ejabberd_commands:get_command_format(Name),
+ Method =
+ case {ForceExecution, ResultFormatApi} of
+ {true, _} ->
+ auto;
+ {_, {_, rescode}} ->
+ manual;
+ {_, {_, restuple}} ->
+ manual;
+ _ ->
+ auto
+ end,
+ PresentationEls = make_command_presentation(Name, C#ejabberd_commands.tags),
+ Query = Request#request.q,
+ {ArgumentsUsed1, ExecRes} =
+ execute_command(Name,
+ Query,
+ BaseArguments,
+ Method,
+ ArgumentsFormat,
+ CallerInfo,
+ InputNameAppend),
+ ArgumentsFormatDetailed =
+ add_arguments_details(ArgumentsFormat,
+ C#ejabberd_commands.args_desc,
+ C#ejabberd_commands.args_example),
+ ArgumentsEls =
+ make_command_arguments(Name,
+ Query,
+ Only,
+ Method,
+ Style,
+ ArgumentsFormatDetailed,
+ BaseArguments,
+ InputNameAppend),
+ Automated =
+ case ArgumentsEls of
+ [] ->
+ true;
+ _ ->
+ false
+ end,
+ ArgumentsUsed =
+ (catch lists:zip(
+ lists:map(fun({A, _}) -> A end, ArgumentsFormat), ArgumentsUsed1)),
+ ResultEls =
+ make_command_result(ExecRes,
+ ArgumentsUsed,
+ ResultFormatApi,
+ Automated,
+ Resultnamed,
+ ResultLinks,
+ TO),
+ make_command3(Only, ExecRes, PresentationEls, ArgumentsEls, ResultEls).
+
+make_command3(presentation, _ExecRes, PresentationEls, _ArgumentsEls, _ResultEls) ->
+ ?XAE(<<"p">>, [{<<"class">>, <<"api">>}], PresentationEls);
+make_command3(button, _ExecRes, _PresentationEls, [Button], _ResultEls) ->
+ Button;
+make_command3(result,
+ _ExecRes,
+ _PresentationEls,
+ _ArgumentsEls,
+ [{xmlcdata, _}, Xmlel]) ->
+ ?XAE(<<"p">>, [{<<"class">>, <<"api">>}], [Xmlel]);
+make_command3(value, _ExecRes, _PresentationEls, _ArgumentsEls, [{xmlcdata, _}, Xmlel]) ->
+ Xmlel;
+make_command3(value,
+ _ExecRes,
+ _PresentationEls,
+ _ArgumentsEls,
+ [{xmlel, _, _, _} = Xmlel]) ->
+ Xmlel;
+make_command3(raw_and_value,
+ ExecRes,
+ _PresentationEls,
+ _ArgumentsEls,
+ [{xmlel, _, _, _} = Xmlel]) ->
+ {raw_and_value, ExecRes, Xmlel};
+make_command3(raw_value, ExecRes, _PresentationEls, _ArgumentsEls, _ResultEls) ->
+ ExecRes;
+make_command3(without_presentation,
+ _ExecRes,
+ _PresentationEls,
+ ArgumentsEls,
+ ResultEls) ->
+ ?XAE(<<"p">>,
+ [{<<"class">>, <<"api">>}],
+ [?XE(<<"blockquote">>, ArgumentsEls ++ ResultEls)]);
+make_command3(all, _ExecRes, PresentationEls, ArgumentsEls, ResultEls) ->
+ ?XAE(<<"p">>,
+ [{<<"class">>, <<"api">>}],
+ PresentationEls ++ [?XE(<<"blockquote">>, ArgumentsEls ++ ResultEls)]).
+
+add_arguments_details(ArgumentsFormat, Descriptions, none) ->
+ add_arguments_details(ArgumentsFormat, Descriptions, []);
+add_arguments_details(ArgumentsFormat, none, Examples) ->
+ add_arguments_details(ArgumentsFormat, [], Examples);
+add_arguments_details(ArgumentsFormat, Descriptions, Examples) ->
+ lists_zipwith3(fun({A, B}, C, D) -> {A, B, C, D} end,
+ ArgumentsFormat,
+ Descriptions,
+ Examples,
+ {pad, {none, "", ""}}).
+
+-ifdef(OTP_BELOW_26).
+
+lists_zipwith3(Combine, List1, List2, List3, {pad, {DefaultX, DefaultY, DefaultZ}}) ->
+ lists_zipwith3(Combine, List1, List2, List3, DefaultX, DefaultY, DefaultZ, []).
+
+lists_zipwith3(_Combine, [], [], [], _DefaultX, _DefaultY, _DefaultZ, Res) ->
+ lists:reverse(Res);
+lists_zipwith3(Combine,
+ [E1 | List1],
+ [E2 | List2],
+ [E3 | List3],
+ DefX,
+ DefY,
+ DefZ,
+ Res) ->
+ E123 = Combine(E1, E2, E3),
+ lists_zipwith3(Combine, List1, List2, List3, DefX, DefY, DefZ, [E123 | Res]);
+lists_zipwith3(Combine, [E1 | List1], [], [], DefX, DefY, DefZ, Res) ->
+ E123 = Combine(E1, DefY, DefZ),
+ lists_zipwith3(Combine, List1, [], [], DefX, DefY, DefZ, [E123 | Res]);
+lists_zipwith3(Combine, [E1 | List1], [], [E3 | List3], DefX, DefY, DefZ, Res) ->
+ E123 = Combine(E1, DefY, E3),
+ lists_zipwith3(Combine, List1, [], List3, DefX, DefY, DefZ, [E123 | Res]);
+lists_zipwith3(Combine, [E1 | List1], [E2 | List2], [], DefX, DefY, DefZ, Res) ->
+ E123 = Combine(E1, E2, DefZ),
+ lists_zipwith3(Combine, List1, List2, [], DefX, DefY, DefZ, [E123 | Res]).
+
+-else.
+
+lists_zipwith3(Combine, List1, List2, List3, How) ->
+ lists:zipwith3(Combine, List1, List2, List3, How).
+
+-endif.
+
+%%%==================================
+%%%% make_command: presentation
+
+make_command_presentation(Name, Tags) ->
+ NameBin = misc:atom_to_binary(Name),
+ NiceNameBin = nice_this(Name),
+ Text = ejabberd_ctl:get_usage_command(atom_to_list(Name), 100, false, 1000000),
+ AnchorLink = [?ANCHORL(NameBin)],
+ MaybeDocsLink =
+ case lists:member(internal, Tags) of
+ true ->
+ [];
+ false ->
+ [?GL(<<"developer/ejabberd-api/admin-api/#", NameBin/binary>>, NameBin)]
+ end,
+ [?XE(<<"details">>,
+ [?XAE(<<"summary">>, [{<<"id">>, NameBin}], [?XC(<<"strong">>, NiceNameBin)])]
+ ++ MaybeDocsLink
+ ++ AnchorLink
+ ++ [?XC(<<"pre">>, list_to_binary(Text))])].
+
+nice_this(This, integer) ->
+ {nice_this(This), right};
+nice_this(This, _Format) ->
+ nice_this(This).
+
+-spec nice_this(This :: atom() | string() | [byte()]) -> NiceThis :: binary().
+nice_this(This) when is_atom(This) ->
+ nice_this(atom_to_list(This));
+nice_this(This) when is_binary(This) ->
+ nice_this(binary_to_list(This));
+nice_this(This) when is_list(This) ->
+ list_to_binary(lists:flatten([string:titlecase(Word)
+ || Word <- string:replace(This, "_", " ", all)])).
+
+-spec long_this(These :: [This :: atom()]) -> Long :: binary().
+long_this(These) ->
+ list_to_binary(lists:join($/, [atom_to_list(This) || This <- These])).
+
+%%%==================================
+%%%% make_command: arguments
+
+make_command_arguments(Name,
+ Query,
+ Only,
+ Method,
+ Style,
+ ArgumentsFormat,
+ BaseArguments,
+ InputNameAppend) ->
+ ArgumentsFormat2 = remove_base_arguments(ArgumentsFormat, BaseArguments),
+ ArgumentsFields = make_arguments_fields(Name, Query, ArgumentsFormat2),
+ Button = make_button_element(Name, Method, Style, InputNameAppend),
+ ButtonElement =
+ ?XE(<<"tr">>,
+ [?X(<<"td">>), ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], [Button])]),
+ case {(ArgumentsFields /= []) or (Method == manual), Only} of
+ {false, _} ->
+ [];
+ {true, button} ->
+ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [Button])];
+ {true, _} ->
+ [?XAE(<<"form">>,
+ [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
+ [?XE(<<"table">>, ArgumentsFields ++ [ButtonElement])])]
+ end.
+
+remove_base_arguments(ArgumentsFormat, BaseArguments) ->
+ lists:filter(fun({ArgName, _ArgFormat, _ArgDesc, _ArgExample}) ->
+ not
+ lists:keymember(
+ misc:atom_to_binary(ArgName), 1, BaseArguments)
+ end,
+ ArgumentsFormat).
+
+make_button_element(Name, _, Style, InputNameAppend) ->
+ Id = term_to_id(InputNameAppend),
+ NameBin = <<(misc:atom_to_binary(Name))/binary, Id/binary>>,
+ NiceNameBin = nice_this(Name),
+ case Style of
+ danger ->
+ ?INPUTD(<<"submit">>, NameBin, NiceNameBin);
+ _ ->
+ ?INPUT(<<"submit">>, NameBin, NiceNameBin)
+ end.
+
+make_arguments_fields(Name, Query, ArgumentsFormat) ->
+ lists:map(fun({ArgName, ArgFormat, _ArgDescription, ArgExample}) ->
+ ArgExampleBin = format_result(ArgExample, {ArgName, ArgFormat}),
+ ArgNiceNameBin = nice_this(ArgName),
+ ArgLongNameBin = long_this([Name, ArgName]),
+ ArgValue =
+ case lists:keysearch(ArgLongNameBin, 1, Query) of
+ {value, {ArgLongNameBin, V}} ->
+ V;
+ _ ->
+ <<"">>
+ end,
+ ?XE(<<"tr">>,
+ [?XC(<<"td">>, <>),
+ ?XE(<<"td">>,
+ [?INPUTPH(<<"text">>, ArgLongNameBin, ArgValue, ArgExampleBin)])])
+ end,
+ ArgumentsFormat).
+
+%%%==================================
+%%%% make_command: execute
+
+execute_command(Name,
+ Query,
+ BaseArguments,
+ Method,
+ ArgumentsFormat,
+ CallerInfo,
+ InputNameAppend) ->
+ try Args = prepare_arguments(Name, BaseArguments ++ Query, ArgumentsFormat),
+ {Args,
+ execute_command2(Name, Query, Args, Method, ArgumentsFormat, CallerInfo, InputNameAppend)}
+ of
+ R ->
+ R
+ catch
+ A:E ->
+ {error, {A, E}}
+ end.
+
+execute_command2(Name,
+ Query,
+ Arguments,
+ Method,
+ ArgumentsFormat,
+ CallerInfo,
+ InputNameAppend) ->
+ AllArgumentsProvided = length(Arguments) == length(ArgumentsFormat),
+ PressedExecuteButton = is_this_to_execute(Name, Query, Arguments, InputNameAppend),
+ LetsExecute =
+ case {Method, PressedExecuteButton, AllArgumentsProvided} of
+ {auto, _, true} ->
+ true;
+ {manual, true, true} ->
+ true;
+ _ ->
+ false
+ end,
+ case LetsExecute of
+ true ->
+ catch ejabberd_commands:execute_command2(Name, Arguments, CallerInfo);
+ false ->
+ not_executed
+ end.
+
+is_this_to_execute(Name, Query, Arguments, InputNameAppend) ->
+ NiceNameBin = nice_this(Name),
+ NameBin = misc:atom_to_binary(Name),
+ AppendBin = term_to_id(lists:sublist(Arguments, length(InputNameAppend))),
+ ArgumentsId = <>,
+ {value, {ArgumentsId, NiceNameBin}} == lists:keysearch(ArgumentsId, 1, Query).
+
+prepare_arguments(ComName, Args, ArgsFormat) ->
+ lists:foldl(fun({ArgName, ArgFormat}, FinalArguments) ->
+ %% Give priority to the value enforced in our code
+ %% Otherwise use the value provided by the user
+ case {lists:keyfind(
+ misc:atom_to_binary(ArgName), 1, Args),
+ lists:keyfind(long_this([ComName, ArgName]), 1, Args)}
+ of
+ %% Value enforced in our code
+ {{_, Value}, _} ->
+ [format_arg(Value, ArgFormat) | FinalArguments];
+ %% User didn't provide value in the field
+ {_, {_, <<>>}} ->
+ FinalArguments;
+ %% Value provided by the user in the form field
+ {_, {_, Value}} ->
+ [format_arg(Value, ArgFormat) | FinalArguments];
+ {false, false} ->
+ FinalArguments
+ end
+ end,
+ [],
+ lists:reverse(ArgsFormat)).
+
+format_arg(Value, any) ->
+ Value;
+format_arg(Value, atom) when is_atom(Value) ->
+ Value;
+format_arg(Value, binary) when is_binary(Value) ->
+ Value;
+format_arg(Value, ArgFormat) ->
+ ejabberd_ctl:format_arg(binary_to_list(Value), ArgFormat).
+
+%%%==================================
+%%%% make_command: result
+
+make_command_result(not_executed, _, _, _, _, _, _) ->
+ [];
+make_command_result({error, ErrorElement}, _, _, _, _, _, _) ->
+ [?DIVRES([?C(<<"Error: ">>),
+ ?XC(<<"code">>, list_to_binary(io_lib:format("~p", [ErrorElement])))])];
+make_command_result(Value,
+ ArgumentsUsed,
+ {ResName, _ResFormat} = ResultFormatApi,
+ Automated,
+ Resultnamed,
+ ResultLinks,
+ TO) ->
+ ResNameBin = nice_this(ResName),
+ ResultValueEl =
+ make_command_result_element(ArgumentsUsed, Value, ResultFormatApi, ResultLinks, TO),
+ ResultEls =
+ case Resultnamed of
+ true ->
+ [?C(<>), ResultValueEl];
+ false ->
+ [ResultValueEl]
+ end,
+ case Automated of
+ true ->
+ ResultEls;
+ false ->
+ [?DIVRES(ResultEls)]
+ end.
+
+make_command_result_element(ArgumentsUsed,
+ ListOfTuples,
+ {_ArgName, {list, {_ListElementsName, {tuple, TupleElements}}}},
+ ResultLinks,
+ {PageSize, RPath}) ->
+ HeadElements =
+ [nice_this(ElementName, ElementFormat) || {ElementName, ElementFormat} <- TupleElements],
+ ContentElements =
+ [list_to_tuple([make_result(format_result(V, {ElementName, ElementFormat}),
+ ElementName,
+ ArgumentsUsed,
+ ResultLinks)
+ || {V, {ElementName, ElementFormat}}
+ <- lists:zip(tuple_to_list(Tuple), TupleElements)])
+ || Tuple <- ListOfTuples],
+ make_table(PageSize, RPath, HeadElements, ContentElements);
+make_command_result_element(_ArgumentsUsed,
+ Values,
+ {_ArgName, {tuple, TupleElements}},
+ _ResultLinks,
+ _TO) ->
+ ?XE(<<"table">>,
+ [?XE(<<"thead">>,
+ [?XE(<<"tr">>,
+ [?XC(<<"td">>, nice_this(ElementName))
+ || {ElementName, _ElementFormat} <- TupleElements])]),
+ ?XE(<<"tbody">>,
+ [?XE(<<"tr">>,
+ [?XC(<<"td">>, format_result(V, {ElementName, ElementFormat}))
+ || {V, {ElementName, ElementFormat}}
+ <- lists:zip(tuple_to_list(Values), TupleElements)])])]);
+make_command_result_element(ArgumentsUsed,
+ Value,
+ {_ArgName, {list, {ElementsName, ElementsFormat}}},
+ ResultLinks,
+ {PageSize, RPath}) ->
+ HeadElements = [nice_this(ElementsName)],
+ ContentElements =
+ [{make_result(format_result(V, {ElementsName, ElementsFormat}),
+ ElementsName,
+ ArgumentsUsed,
+ ResultLinks)}
+ || V <- Value],
+ make_table(PageSize, RPath, HeadElements, ContentElements);
+make_command_result_element(ArgumentsUsed, Value, ResultFormatApi, ResultLinks, _TO) ->
+ Res = make_result(format_result(Value, ResultFormatApi),
+ unknown_element_name,
+ ArgumentsUsed,
+ ResultLinks),
+ Res2 =
+ case Res of
+ [{xmlel, _, _, _} | _] = X ->
+ X;
+ Z ->
+ [Z]
+ end,
+ ?XE(<<"code">>, Res2).
+
+make_result(Binary, ElementName, ArgumentsUsed, [{ResultName, arg_host, Level, Append}])
+ when (ElementName == ResultName) or (ElementName == unknown_element_name) ->
+ {_, Host} = lists:keyfind(host, 1, ArgumentsUsed),
+ UrlBinary =
+ replace_url_elements([<<"server/">>, host, <<"/">>, Append], [{host, Host}], Level),
+ ?AC(UrlBinary, Binary);
+make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, host, Level, Append}])
+ when (ElementName == ResultName) or (ElementName == unknown_element_name) ->
+ UrlBinary =
+ replace_url_elements([<<"server/">>, host, <<"/">>, Append], [{host, Binary}], Level),
+ ?AC(UrlBinary, Binary);
+make_result(Binary,
+ ElementName,
+ _ArgumentsUsed,
+ [{ResultName, mnesia_table, Level, Append}])
+ when (ElementName == ResultName) or (ElementName == unknown_element_name) ->
+ Node = misc:atom_to_binary(node()),
+ UrlBinary =
+ replace_url_elements([<<"node/">>, node, <<"/db/table/">>, tablename, <<"/">>, Append],
+ [{node, Node}, {tablename, Binary}],
+ Level),
+ ?AC(UrlBinary, Binary);
+make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, node, Level, Append}])
+ when (ElementName == ResultName) or (ElementName == unknown_element_name) ->
+ UrlBinary =
+ replace_url_elements([<<"node/">>, node, <<"/">>, Append], [{node, Binary}], Level),
+ ?AC(UrlBinary, Binary);
+make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, user, Level, Append}])
+ when (ElementName == ResultName) or (ElementName == unknown_element_name) ->
+ Jid = try jid:decode(Binary) of
+ #jid{} = J ->
+ J
+ catch
+ _:{bad_jid, _} ->
+ %% TODO: Find a method to be able to link to this user to delete it
+ ?INFO_MSG("Error parsing Binary that is not a valid JID:~n ~p", [Binary]),
+ jid:decode(<<"unknown-username@localhost">>)
+ end,
+ {User, Host, _R} = jid:split(Jid),
+ case lists:member(Host, ejabberd_config:get_option(hosts)) of
+ true ->
+ UrlBinary =
+ replace_url_elements([<<"server/">>, host, <<"/user/">>, user, <<"/">>, Append],
+ [{user, misc:url_encode(User)}, {host, Host}],
+ Level),
+ ?AC(UrlBinary, Binary);
+ false ->
+ ?C(Binary)
+ end;
+make_result(Binary, ElementName, _ArgumentsUsed, [{ResultName, room, Level, Append}])
+ when (ElementName == ResultName) or (ElementName == unknown_element_name) ->
+ Jid = jid:decode(Binary),
+ {Roomname, Service, _} = jid:split(Jid),
+ Host = ejabberd_router:host_of_route(Service),
+ case lists:member(Host, ejabberd_config:get_option(hosts)) of
+ true ->
+ UrlBinary =
+ replace_url_elements([<<"server/">>,
+ host,
+ <<"/muc/rooms/room/">>,
+ room,
+ <<"/">>,
+ Append],
+ [{room, misc:url_encode(Roomname)}, {host, Host}],
+ Level),
+ ?AC(UrlBinary, Binary);
+ false ->
+ ?C(Binary)
+ end;
+make_result(Binary,
+ ElementName,
+ ArgumentsUsed,
+ [{ResultName, shared_roster, Level, Append}])
+ when (ElementName == ResultName) or (ElementName == unknown_element_name) ->
+ First = proplists:get_value(first, ArgumentsUsed),
+ Second = proplists:get_value(second, ArgumentsUsed),
+ {GroupId, Host} =
+ case jid:decode(First) of
+ #jid{luser = <<"">>, lserver = G} ->
+ {G, Second};
+ #jid{luser = G, lserver = H} ->
+ {G, H}
+ end,
+ UrlBinary =
+ replace_url_elements([<<"server/">>,
+ host,
+ <<"/shared-roster/group/">>,
+ srg,
+ <<"/">>,
+ Append],
+ [{host, Host}, {srg, GroupId}],
+ Level),
+ ?AC(UrlBinary, Binary);
+make_result([{xmlcdata, _, _, _} | _] = Any,
+ _ElementName,
+ _ArgumentsUsed,
+ _ResultLinks) ->
+ Any;
+make_result([{xmlel, _, _, _} | _] = Any, _ElementName, _ArgumentsUsed, _ResultLinks) ->
+ Any;
+make_result(Binary,
+ ElementName,
+ _ArgumentsUsed,
+ [{ResultName, paragraph, _Level, _Append}])
+ when (ElementName == ResultName) or (ElementName == unknown_element_name) ->
+ ?XC(<<"pre">>, Binary);
+make_result(Binary, _ElementName, _ArgumentsUsed, _ResultLinks) ->
+ ?C(Binary).
+
+replace_url_elements(UrlComponents, Replacements, Level) ->
+ Base = get_base_path_sum(0, 0, Level),
+ Binary2 =
+ lists:foldl(fun (El, Acc) when is_binary(El) ->
+ [El | Acc];
+ (El, Acc) when is_atom(El) ->
+ {El, Value} = lists:keyfind(El, 1, Replacements),
+ [Value | Acc]
+ end,
+ [],
+ UrlComponents),
+ Binary3 =
+ binary:list_to_bin(
+ lists:reverse(Binary2)),
+ <>.
+
+format_result(Value, {_ResultName, integer}) when is_integer(Value) ->
+ integer_to_binary(Value);
+format_result(Value, {_ResultName, string}) when is_list(Value) ->
+ Value;
+format_result(Value, {_ResultName, string}) when is_binary(Value) ->
+ Value;
+format_result(Value, {_ResultName, atom}) when is_atom(Value) ->
+ misc:atom_to_binary(Value);
+format_result(Value, {_ResultName, any}) ->
+ Value;
+format_result({ok, String}, {_ResultName, restuple}) when is_list(String) ->
+ list_to_binary(String);
+format_result({error, Type, Code, Desc}, {_ResultName, restuple}) ->
+ <<"Error: ",
+ (misc:atom_to_binary(Type))/binary,
+ " ",
+ (integer_to_binary(Code))/binary,
+ ": ",
+ (list_to_binary(Desc))/binary>>;
+format_result([], {_Name, {list, _ElementsDef}}) ->
+ "";
+format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) ->
+ Separator = ",",
+ [format_result(FirstElement, ElementsDef) | lists:map(fun(Element) ->
+ [Separator | format_result(Element,
+ ElementsDef)]
+ end,
+ Elements)];
+format_result(Value, _ResultFormat) when is_atom(Value) ->
+ misc:atom_to_binary(Value);
+format_result(Value, _ResultFormat) when is_list(Value) ->
+ list_to_binary(Value);
+format_result(Value, _ResultFormat) when is_binary(Value) ->
+ Value;
+format_result(Value, _ResultFormat) ->
+ io_lib:format("~p", [Value]).
+
+%%%==================================
+%%%% make_table
+
+-spec make_table(PageSize :: integer(),
+ RemainingPath :: [binary()],
+ NameOptionList :: [Name :: binary() | {Name :: binary(), left | right}],
+ Values :: [tuple()]) ->
+ xmlel().
+make_table(PageSize, RPath, NameOptionList, Values1) ->
+ Values =
+ case lists:member(<<"sort">>, RPath) of
+ true ->
+ Values1;
+ false ->
+ GetXmlValue =
+ fun ({xmlcdata, _} = X) ->
+ X;
+ ({xmlel, _, _, _} = X) ->
+ X;
+ ({raw_and_value, _V, X}) ->
+ X
+ end,
+ ConvertTupleToTuple =
+ fun(Row1) -> list_to_tuple(lists:map(GetXmlValue, tuple_to_list(Row1))) end,
+ lists:map(ConvertTupleToTuple, Values1)
+ end,
+ make_table1(PageSize, RPath, <<"">>, <<"">>, 1, NameOptionList, Values).
+
+make_table1(PageSize,
+ [<<"page">>, PageNumber | RPath],
+ PageUrlBase,
+ SortUrlBase,
+ _Start,
+ NameOptionList,
+ Values1) ->
+ make_table1(PageSize,
+ RPath,
+ <>,
+ <>,
+ 1 + PageSize * binary_to_integer(PageNumber),
+ NameOptionList,
+ Values1);
+make_table1(PageSize,
+ [<<"sort">>, SortType | RPath],
+ PageUrlBase,
+ SortUrlBase,
+ Start,
+ NameOptionList,
+ Rows1) ->
+ ColumnToSort =
+ length(lists:takewhile(fun (A) when A == SortType ->
+ false;
+ ({A, _}) when A == SortType ->
+ false;
+ (_) ->
+ true
+ end,
+ NameOptionList))
+ + 1,
+ Direction =
+ case lists:nth(ColumnToSort, NameOptionList) of
+ {_, right} ->
+ descending;
+ {_, left} ->
+ ascending;
+ _ ->
+ ascending
+ end,
+ ColumnToSort = ColumnToSort,
+ GetRawValue =
+ fun ({xmlcdata, _} = X) ->
+ X;
+ ({xmlel, _, _, _} = X) ->
+ X;
+ ({raw_and_value, R, _X}) ->
+ R
+ end,
+ GetXmlValue =
+ fun ({xmlcdata, _} = X) ->
+ X;
+ ({xmlel, _, _, _} = X) ->
+ X;
+ ({raw_and_value, _R, X}) ->
+ X
+ end,
+ SortTwo =
+ fun(A1, B1) ->
+ A2 = GetRawValue(element(ColumnToSort, A1)),
+ B2 = GetRawValue(element(ColumnToSort, B1)),
+ case Direction of
+ ascending ->
+ A2 < B2;
+ descending ->
+ A2 > B2
+ end
+ end,
+ Rows1Sorted = lists:sort(SortTwo, Rows1),
+ ConvertTupleToTuple =
+ fun(Row1) -> list_to_tuple(lists:map(GetXmlValue, tuple_to_list(Row1))) end,
+ Rows = lists:map(ConvertTupleToTuple, Rows1Sorted),
+ make_table1(PageSize,
+ RPath,
+ PageUrlBase,
+ <>,
+ Start,
+ NameOptionList,
+ Rows);
+make_table1(PageSize, [], PageUrlBase, SortUrlBase, Start, NameOptionList, Values1) ->
+ Values = lists:sublist(Values1, Start, PageSize),
+ Table = make_table(NameOptionList, Values),
+ Size = length(Values1),
+ Remaining =
+ case Size rem PageSize of
+ 0 ->
+ 0;
+ _ ->
+ 1
+ end,
+ NumPages = max(0, Size div PageSize + Remaining - 1),
+ PLinks1 =
+ lists:foldl(fun(N, Acc) ->
+ NBin = integer_to_binary(N),
+ Acc
+ ++ [?C(<<", ">>),
+ ?AC(<>, NBin)]
+ end,
+ [],
+ lists:seq(1, NumPages)),
+ PLinks =
+ case PLinks1 of
+ [] ->
+ [];
+ _ ->
+ [?XE(<<"p">>, [?C(<<"Page: ">>), ?AC(<>, <<"0">>) | PLinks1])]
+ end,
+
+ Names =
+ lists:map(fun ({Name, _}) ->
+ Name;
+ (Name) ->
+ Name
+ end,
+ NameOptionList),
+ [_ | SLinks1] =
+ lists:foldl(fun(N, Acc) ->
+ [?C(<<", ">>), ?AC(<>, N) | Acc]
+ end,
+ [],
+ lists:reverse(Names)),
+ SLinks =
+ case {PLinks, SLinks1} of
+ {_, []} ->
+ [];
+ {[], _} ->
+ [];
+ {_, [_]} ->
+ [];
+ {_, SLinks2} ->
+ [?XE(<<"p">>, [?C(<<"Sort all pages by: ">>) | SLinks2])]
+ end,
+
+ ?XE(<<"div">>, [Table | PLinks ++ SLinks]).
+
+-spec make_table(NameOptionList :: [Name :: binary() | {Name :: binary(), left | right}],
+ Values :: [tuple()]) ->
+ xmlel().
+make_table(NameOptionList, Values) ->
+ NamesAndAttributes = [make_column_attributes(NameOption) || NameOption <- NameOptionList],
+ {Names, ColumnsAttributes} = lists:unzip(NamesAndAttributes),
+ make_table(Names, ColumnsAttributes, Values).
+
+make_table(Names, ColumnsAttributes, Values) ->
+ ?XAE(<<"table">>,
+ [{<<"class">>, <<"sortable">>}],
+ [?XE(<<"thead">>,
+ [?XE(<<"tr">>, [?XC(<<"th">>, nice_this(HeadElement)) || HeadElement <- Names])]),
+ ?XE(<<"tbody">>,
+ [?XE(<<"tr">>,
+ [?XAE(<<"td">>, CAs, [V])
+ || {CAs, V} <- lists:zip(ColumnsAttributes, tuple_to_list(ValueTuple))])
+ || ValueTuple <- Values])]).
+
+make_column_attributes({Name, Option}) ->
+ {Name, [make_column_attribute(Option)]};
+make_column_attributes(Name) ->
+ {Name, []}.
+
+make_column_attribute(left) ->
+ {<<"class">>, <<"alignleft">>};
+make_column_attribute(right) ->
+ {<<"class">>, <<"alignright">>}.
+
+%%%==================================
%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=:
diff --git a/src/ext_mod.erl b/src/ext_mod.erl
index 3c3182415b4..fb7d5d438cc 100644
--- a/src/ext_mod.erl
+++ b/src/ext_mod.erl
@@ -37,13 +37,14 @@
config_dir/0, get_commands_spec/0]).
-export([modules_configs/0, module_ebin_dir/1]).
-export([compile_erlang_file/2, compile_elixir_file/2]).
--export([web_menu_node/3, web_page_node/5, get_page/3]).
+-export([web_menu_node/3, web_page_node/3, webadmin_node_contrib/3]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-include("ejabberd_commands.hrl").
+-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.hrl").
-include("logger.hrl").
-include("translate.hrl").
@@ -924,26 +925,156 @@ parse_details(Body) ->
)
).
-web_menu_node(Acc, _Node, Lang) ->
- Acc ++ [{<<"contrib">>, translate:translate(Lang, ?T("Contrib Modules"))}].
-
-web_page_node(_, Node, [<<"contrib">>], Query, Lang) ->
- Res = rpc:call(Node, ?MODULE, get_page, [Node, Query, Lang]),
- {stop, Res};
-web_page_node(Acc, _, _, _, _) ->
+%% @format-begin
+
+web_menu_node(Acc, _Node, _Lang) ->
+ Acc
+ ++ [{<<"contrib">>, <<"Contrib Modules (Detailed)">>},
+ {<<"contrib-api">>, <<"Contrib Modules (API)">>}].
+
+web_page_node(_,
+ Node,
+ #request{path = [<<"contrib">>],
+ q = Query,
+ lang = Lang} =
+ R) ->
+ Title =
+ ?H1GL(<<"Contrib Modules (Detailed)">>,
+ <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>,
+ <<"ejabberd-contrib">>),
+ Res = [ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [webadmin_node_contrib,
+ R,
+ [{<<"node">>, Node}, {<<"query">>, Query}, {<<"lang">>, Lang}],
+ []])],
+ {stop, Title ++ Res};
+web_page_node(_, Node, #request{path = [<<"contrib-api">> | RPath]} = R) ->
+ Title =
+ ?H1GL(<<"Contrib Modules (API)">>,
+ <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>,
+ <<"ejabberd-contrib">>),
+ _TableInstalled = make_table_installed(Node, R, RPath),
+ _TableAvailable = make_table_available(Node, R, RPath),
+ TableInstalled = make_table_installed(Node, R, RPath),
+ TableAvailable = make_table_available(Node, R, RPath),
+ Res = [?X(<<"hr">>),
+ ?XAC(<<"h2">>, [{<<"id">>, <<"specs">>}], <<"Specs">>),
+ ?XE(<<"blockquote">>,
+ [ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [modules_update_specs, R])]),
+ ?X(<<"hr">>),
+ ?XAC(<<"h2">>, [{<<"id">>, <<"installed">>}], <<"Installed">>),
+ ?XE(<<"blockquote">>,
+ [ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [modules_installed, R, [], [{only, presentation}]]),
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [module_uninstall, R, [], [{only, presentation}]]),
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [module_upgrade, R, [], [{only, presentation}]]),
+ TableInstalled]),
+ ?X(<<"hr">>),
+ ?XAC(<<"h2">>, [{<<"id">>, <<"available">>}], <<"Available">>),
+ ?XE(<<"blockquote">>,
+ [ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [modules_available, R, [], [{only, presentation}]]),
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [module_install, R, [], [{only, presentation}]]),
+ TableAvailable,
+ ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [module_check, R])])],
+ {stop, Title ++ Res};
+web_page_node(Acc, _, _) ->
Acc.
-get_page(Node, Query, Lang) ->
+make_table_installed(Node, R, RPath) ->
+ Columns = [<<"Name">>, <<"Summary">>, <<"">>, <<"">>],
+ ModulesInstalled =
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command_raw_value,
+ [modules_installed, R, []]),
+ Rows =
+ lists:map(fun({Name, Summary}) ->
+ NameBin = misc:atom_to_binary(Name),
+ Upgrade =
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [module_upgrade,
+ R,
+ [{<<"module">>, NameBin}],
+ [{only, button}, {input_name_append, [NameBin]}]]),
+ Uninstall =
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [module_uninstall,
+ R,
+ [{<<"module">>, NameBin}],
+ [{only, button},
+ {style, danger},
+ {input_name_append, [NameBin]}]]),
+ {?C(NameBin), ?C(list_to_binary(Summary)), Upgrade, Uninstall}
+ end,
+ ModulesInstalled),
+ ejabberd_web_admin:make_table(200, RPath, Columns, Rows).
+
+make_table_available(Node, R, RPath) ->
+ Columns = [<<"Name">>, <<"Summary">>, <<"">>],
+ ModulesAll =
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command_raw_value,
+ [modules_available, R, []]),
+ ModulesInstalled =
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command_raw_value,
+ [modules_installed, R, []]),
+ ModulesNotInstalled =
+ lists:filter(fun({Mod, _}) -> not lists:keymember(Mod, 1, ModulesInstalled) end,
+ ModulesAll),
+ Rows =
+ lists:map(fun({Name, Summary}) ->
+ NameBin = misc:atom_to_binary(Name),
+ Install =
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [module_install,
+ R,
+ [{<<"module">>, NameBin}],
+ [{only, button}, {input_name_append, [NameBin]}]]),
+ {?C(NameBin), ?C(list_to_binary(Summary)), Install}
+ end,
+ ModulesNotInstalled),
+ ejabberd_web_admin:make_table(200, RPath, Columns, Rows).
+
+webadmin_node_contrib(Node, Query, Lang) ->
QueryRes = list_modules_parse_query(Query),
- Title = ?H1GL(translate:translate(Lang, ?T("Contrib Modules")),
- <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>,
- <<"ejabberd-contrib">>),
Contents = get_content(Node, Query, Lang),
- Result = case QueryRes of
- ok -> [?XREST(?T("Submitted"))];
- nothing -> []
- end,
- Title ++ Result ++ Contents.
+ Result =
+ case QueryRes of
+ ok ->
+ [?XREST(?T("Submitted"))];
+ nothing ->
+ []
+ end,
+ Result ++ Contents.
+%% @format-end
get_module_home(Module, Attrs) ->
case get_module_information(home, Attrs) of
diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl
index 8616bdd2491..1bfbeb4d1a6 100644
--- a/src/mod_admin_extra.erl
+++ b/src/mod_admin_extra.erl
@@ -59,7 +59,7 @@
% Roster
add_rosteritem/7, delete_rosteritem/4,
- get_roster/2, push_roster/3,
+ get_roster/2, get_roster_count/2, push_roster/3,
push_roster_all/1, push_alltoall/2,
push_roster_item/5, build_roster_item/3,
@@ -67,8 +67,10 @@
private_get/4, private_set/3,
% Shared roster
- srg_create/5,
+ srg_create/5, srg_add/2,
srg_delete/2, srg_list/1, srg_get_info/2,
+ srg_set_info/4,
+ srg_get_displayed/2, srg_add_displayed/3, srg_del_displayed/3,
srg_get_members/2, srg_user_add/4, srg_user_del/4,
% Send message
@@ -80,9 +82,17 @@
% Stats
stats/1, stats/2
]).
+-export([web_menu_main/2, web_page_main/2,
+ web_menu_host/3, web_page_host/3,
+ web_menu_hostuser/4, web_page_hostuser/4,
+ web_menu_hostnode/4, web_page_hostnode/4,
+ web_menu_node/3, web_page_node/3]).
+-import(ejabberd_web_admin, [make_command/4, make_table/2]).
-include("ejabberd_commands.hrl").
+-include("ejabberd_http.hrl").
+-include("ejabberd_web_admin.hrl").
-include("mod_roster.hrl").
-include("mod_privacy.hrl").
-include("ejabberd_sm.hrl").
@@ -94,7 +104,17 @@
%%%
start(_Host, _Opts) ->
- ejabberd_commands:register_commands(?MODULE, get_commands_spec()).
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
+ {ok, [{hook, webadmin_menu_main, web_menu_main, 50, global},
+ {hook, webadmin_page_main, web_page_main, 50, global},
+ {hook, webadmin_menu_host, web_menu_host, 50},
+ {hook, webadmin_page_host, web_page_host, 50},
+ {hook, webadmin_menu_hostuser, web_menu_hostuser, 50},
+ {hook, webadmin_page_hostuser, web_page_hostuser, 50},
+ {hook, webadmin_menu_hostnode, web_menu_hostnode, 50},
+ {hook, webadmin_page_hostnode, web_page_hostnode, 50},
+ {hook, webadmin_menu_node, web_menu_node, 50, global},
+ {hook, webadmin_page_node, web_page_node, 50, global}]}.
stop(Host) ->
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
@@ -670,6 +690,16 @@ get_commands_spec() ->
{pending, string},
{groups, {list, {group, string}}}
]}}}}},
+ #ejabberd_commands{name = get_roster_count, tags = [roster],
+ desc = "Get number of contacts in a local user roster",
+ note = "added in 24.xx",
+ policy = user,
+ module = ?MODULE, function = get_roster_count,
+ args = [],
+ args_rename = [{server, host}],
+ result_example = 5,
+ result_desc = "Number",
+ result = {value, integer}},
#ejabberd_commands{name = push_roster, tags = [roster],
desc = "Push template roster from file to a user",
longdesc = "The text file must contain an erlang term: a list "
@@ -777,6 +807,14 @@ get_commands_spec() ->
args_desc = ["Group identifier", "Group server name", "Group name",
"Group description", "List of groups to display"],
result = {res, rescode}},
+ #ejabberd_commands{name = srg_add, tags = [shared_roster_group],
+ desc = "Add/Create a Shared Roster Group (without details)",
+ module = ?MODULE, function = srg_add,
+ note = "added in 24.xx",
+ args = [{group, binary}, {host, binary}],
+ args_example = [<<"group3">>, <<"myserver.com">>],
+ args_desc = ["Group identifier", "Group server name"],
+ result = {res, rescode}},
#ejabberd_commands{name = srg_delete, tags = [shared_roster_group],
desc = "Delete a Shared Roster Group",
module = ?MODULE, function = srg_delete,
@@ -802,6 +840,48 @@ get_commands_spec() ->
result_example = [{<<"name">>, "Group 3"}, {<<"displayed_groups">>, "group1"}],
result_desc = "List of group information, as key and value",
result = {informations, {list, {information, {tuple, [{key, string}, {value, string}]}}}}},
+ #ejabberd_commands{name = srg_set_info, tags = [shared_roster_group],
+ desc = "Set info of a Shared Roster Group",
+ module = ?MODULE, function = srg_set_info,
+ note = "added in 24.xx",
+ args = [{group, binary}, {host, binary}, {key, binary}, {value, binary}],
+ args_example = [<<"group3">>, <<"myserver.com">>, <<"label">>, <<"Family">>],
+ args_desc = ["Group identifier", "Group server name",
+ "Information key: label, description",
+ "Information value"],
+ result = {res, rescode}},
+
+ #ejabberd_commands{name = srg_get_displayed, tags = [shared_roster_group],
+ desc = "Get displayed groups of a Shared Roster Group",
+ module = ?MODULE, function = srg_get_displayed,
+ note = "added in 24.xx",
+ args = [{group, binary}, {host, binary}],
+ args_example = [<<"group3">>, <<"myserver.com">>],
+ args_desc = ["Group identifier", "Group server name"],
+ result_example = [<<"group1">>, <<"group2">>],
+ result_desc = "List of groups to display",
+ result = {display, {list, {group, binary}}}},
+ #ejabberd_commands{name = srg_add_displayed, tags = [shared_roster_group],
+ desc = "Add a group to displayed_groups of a Shared Roster Group",
+ module = ?MODULE, function = srg_add_displayed,
+ note = "added in 24.xx",
+ args = [{group, binary}, {host, binary},
+ {add, binary}],
+ args_example = [<<"group3">>, <<"myserver.com">>, <<"group1">>],
+ args_desc = ["Group identifier", "Group server name",
+ "Group to add to displayed_groups"],
+ result = {res, rescode}},
+ #ejabberd_commands{name = srg_del_displayed, tags = [shared_roster_group],
+ desc = "Delete a group from displayed_groups of a Shared Roster Group",
+ module = ?MODULE, function = srg_del_displayed,
+ note = "added in 24.xx",
+ args = [{group, binary}, {host, binary},
+ {del, binary}],
+ args_example = [<<"group3">>, <<"myserver.com">>, <<"group1">>],
+ args_desc = ["Group identifier", "Group server name",
+ "Group to delete from displayed_groups"],
+ result = {res, rescode}},
+
#ejabberd_commands{name = srg_get_members, tags = [shared_roster_group],
desc = "Get members of a Shared Roster Group",
module = ?MODULE, function = srg_get_members,
@@ -836,6 +916,18 @@ get_commands_spec() ->
result_example = 5,
result_desc = "Number",
result = {value, integer}},
+ #ejabberd_commands{name = get_offline_messages,
+ tags = [internal, offline],
+ desc = "Get the offline messages",
+ policy = user,
+ module = mod_offline, function = get_offline_messages,
+ args = [],
+ result = {queue, {list, {messages, {tuple, [{time, string},
+ {from, string},
+ {to, string},
+ {packet, string}
+ ]}}}}},
+
#ejabberd_commands{name = send_message, tags = [stanza],
desc = "Send a message to a local or remote bare of full JID",
longdesc = "When sending a groupchat message to a MUC room, "
@@ -1586,6 +1678,15 @@ make_roster_xmlrpc(Roster) ->
end,
Roster).
+get_roster_count(User, Server) ->
+ case jid:make(User, Server) of
+ error ->
+ throw({error, "Invalid 'user'/'server'"});
+ #jid{luser = U, lserver = S} ->
+ Items = ejabberd_hooks:run_fold(roster_get, S, [], [{U, S}]),
+ length(Items)
+ end.
+
%%-----------------------------
%% Push Roster from file
%%-----------------------------
@@ -1748,12 +1849,29 @@ srg_create(Group, Host, Label, Description, Display) when is_binary(Display) ->
srg_create(Group, Host, Label, Description, DisplayList);
srg_create(Group, Host, Label, Description, DisplayList) ->
+ {_DispGroups, WrongDispGroups} = filter_groups_existence(Host, DisplayList),
+ case (WrongDispGroups -- [Group]) /= [] of
+ true ->
+ {wrong_displayed_groups, WrongDispGroups};
+ false ->
+ srg_create2(Group, Host, Label, Description, DisplayList)
+ end.
+
+srg_create2(Group, Host, Label, Description, DisplayList) ->
Opts = [{label, Label},
{displayed_groups, DisplayList},
{description, Description}],
{atomic, _} = mod_shared_roster:create_group(Host, Group, Opts),
ok.
+srg_add(Group, Host) ->
+ Opts = [{label, <<"">>},
+ {description, <<"">>},
+ {displayed_groups, []}
+ ],
+ {atomic, _} = mod_shared_roster:create_group(Host, Group, Opts),
+ ok.
+
srg_delete(Group, Host) ->
{atomic, _} = mod_shared_roster:delete_group(Host, Group),
ok.
@@ -1769,9 +1887,109 @@ srg_get_info(Group, Host) ->
[{misc:atom_to_binary(Title), to_list(Value)} || {Title, Value} <- Opts].
to_list([]) -> [];
-to_list([H|T]) -> [to_list(H)|to_list(T)];
+to_list([H|_]=List) when is_binary(H) -> lists:join(", ", [to_list(E) || E <- List]);
to_list(E) when is_atom(E) -> atom_to_list(E);
-to_list(E) -> binary_to_list(E).
+to_list(E) when is_binary(E) -> binary_to_list(E).
+
+%% @format-begin
+
+srg_set_info(Group, Host, Key, Value) ->
+ Opts =
+ case mod_shared_roster:get_group_opts(Host, Group) of
+ Os when is_list(Os) ->
+ Os;
+ error ->
+ []
+ end,
+ Opts2 = srg_set_info(Key, Value, Opts),
+ case mod_shared_roster:set_group_opts(Host, Group, Opts2) of
+ {atomic, ok} ->
+ ok;
+ Problem ->
+ ?INFO_MSG("Problem: ~n ~p", [Problem]), %+++
+ error
+ end.
+
+srg_set_info(<<"description">>, Value, Opts) ->
+ [{description, Value} | proplists:delete(description, Opts)];
+srg_set_info(<<"label">>, Value, Opts) ->
+ [{label, Value} | proplists:delete(label, Opts)];
+srg_set_info(<<"all_users">>, <<"true">>, Opts) ->
+ [{all_users, true} | proplists:delete(all_users, Opts)];
+srg_set_info(<<"online_users">>, <<"true">>, Opts) ->
+ [{online_users, true} | proplists:delete(online_users, Opts)];
+srg_set_info(<<"all_users">>, _, Opts) ->
+ proplists:delete(all_users, Opts);
+srg_set_info(<<"online_users">>, _, Opts) ->
+ proplists:delete(online_users, Opts);
+srg_set_info(Key, _Value, Opts) ->
+ ?ERROR_MSG("Unknown Key in srg_set_info: ~p", [Key]),
+ Opts.
+
+srg_get_displayed(Group, Host) ->
+ Opts =
+ case mod_shared_roster:get_group_opts(Host, Group) of
+ Os when is_list(Os) ->
+ Os;
+ error ->
+ []
+ end,
+ proplists:get_value(displayed_groups, Opts).
+
+srg_add_displayed(Group, Host, NewGroup) ->
+ Opts =
+ case mod_shared_roster:get_group_opts(Host, Group) of
+ Os when is_list(Os) ->
+ Os;
+ error ->
+ []
+ end,
+ {DispGroups, WrongDispGroups} = filter_groups_existence(Host, [NewGroup]),
+ case WrongDispGroups /= [] of
+ true ->
+ {wrong_displayed_groups, WrongDispGroups};
+ false ->
+ DisplayedOld = proplists:get_value(displayed_groups, Opts, []),
+ Opts2 =
+ [{displayed_groups, lists:flatten(DisplayedOld, DispGroups)}
+ | proplists:delete(displayed_groups, Opts)],
+ case mod_shared_roster:set_group_opts(Host, Group, Opts2) of
+ {atomic, ok} ->
+ ok;
+ Problem ->
+ ?INFO_MSG("Problem: ~n ~p", [Problem]), %+++
+ error
+ end
+ end.
+
+srg_del_displayed(Group, Host, OldGroup) ->
+ Opts =
+ case mod_shared_roster:get_group_opts(Host, Group) of
+ Os when is_list(Os) ->
+ Os;
+ error ->
+ []
+ end,
+ DisplayedOld = proplists:get_value(displayed_groups, Opts, []),
+ {DispGroups, OldDispGroups} = lists:partition(fun(G) -> G /= OldGroup end, DisplayedOld),
+ case OldDispGroups == [] of
+ true ->
+ {inexistent_displayed_groups, OldGroup};
+ false ->
+ Opts2 = [{displayed_groups, DispGroups} | proplists:delete(displayed_groups, Opts)],
+ case mod_shared_roster:set_group_opts(Host, Group, Opts2) of
+ {atomic, ok} ->
+ ok;
+ Problem ->
+ ?INFO_MSG("Problem: ~n ~p", [Problem]), %+++
+ error
+ end
+ end.
+
+filter_groups_existence(Host, Groups) ->
+ lists:partition(fun(Group) -> error /= mod_shared_roster:get_group_opts(Host, Group) end,
+ Groups).
+%% @format-end
srg_get_members(Group, Host) ->
Members = mod_shared_roster:get_group_explicit_users(Host,Group),
@@ -1915,6 +2133,256 @@ num_prio(Priority) when is_integer(Priority) ->
num_prio(_) ->
-1.
+%%%
+%%% Web Admin
+%%%
+
+%% @format-begin
+
+%%% Main
+
+web_menu_main(Acc, _Lang) ->
+ Acc ++ [{<<"stats">>, <<"Statistics">>}].
+
+web_page_main(_, #request{path = [<<"stats">>]} = R) ->
+ Res = ?H1GL(<<"Statistics">>, <<"modules/#mod_stats">>, <<"mod_stats">>)
+ ++ [make_command(stats_host, R, [], [{only, presentation}]),
+ make_command(incoming_s2s_number, R, [], [{only, presentation}]),
+ make_command(outgoing_s2s_number, R, [], [{only, presentation}]),
+ make_table([<<"stat name">>, {<<"stat value">>, right}],
+ [{?C(<<"Registered Users:">>),
+ make_command(stats,
+ R,
+ [{<<"name">>, <<"registeredusers">>}],
+ [{only, value}])},
+ {?C(<<"Online Users:">>),
+ make_command(stats,
+ R,
+ [{<<"name">>, <<"onlineusers">>}],
+ [{only, value}])},
+ {?C(<<"S2S Connections Incoming:">>),
+ make_command(incoming_s2s_number, R, [], [{only, value}])},
+ {?C(<<"S2S Connections Outgoing:">>),
+ make_command(outgoing_s2s_number, R, [], [{only, value}])}])],
+ {stop, Res};
+web_page_main(Acc, _) ->
+ Acc.
+
+%%% Host
+
+web_menu_host(Acc, _Host, _Lang) ->
+ Acc ++ [{<<"purge">>, <<"Purge">>}, {<<"stats">>, <<"Statistics">>}].
+
+web_page_host(_, Host, #request{path = [<<"purge">>]} = R) ->
+ Head = [?XC(<<"h1">>, <<"Purge">>)],
+ Set = [ejabberd_web_admin:make_command(delete_old_users_vhost,
+ R,
+ [{<<"host">>, Host}],
+ [])],
+ {stop, Head ++ Set};
+web_page_host(_, Host, #request{path = [<<"stats">>]} = R) ->
+ Res = ?H1GL(<<"Statistics">>, <<"modules/#mod_stats">>, <<"mod_stats">>)
+ ++ [make_command(stats_host, R, [], [{only, presentation}]),
+ make_table([<<"stat name">>, {<<"stat value">>, right}],
+ [{?C(<<"Registered Users:">>),
+ make_command(stats_host,
+ R,
+ [{<<"host">>, Host}, {<<"name">>, <<"registeredusers">>}],
+ [{only, value},
+ {result_links, [{stat, arg_host, 3, <<"users">>}]}])},
+ {?C(<<"Online Users:">>),
+ make_command(stats_host,
+ R,
+ [{<<"host">>, Host}, {<<"name">>, <<"onlineusers">>}],
+ [{only, value},
+ {result_links,
+ [{stat, arg_host, 3, <<"online-users">>}]}])}])],
+ {stop, Res};
+web_page_host(Acc, _, _) ->
+ Acc.
+
+%%% HostUser
+
+web_menu_hostuser(Acc, _Host, _Username, _Lang) ->
+ Acc
+ ++ [{<<"auth">>, <<"Authentication">>},
+ {<<"mam">>, <<"MAM">>},
+ {<<"privacy">>, <<"Privacy Lists">>},
+ {<<"private">>, <<"Private XML Storage">>},
+ {<<"session">>, <<"Sessions">>},
+ {<<"vcard">>, <<"vCard">>}].
+
+web_page_hostuser(_, Host, User, #request{path = [<<"auth">>]} = R) ->
+ Ban = make_command(ban_account,
+ R,
+ [{<<"user">>, User}, {<<"host">>, Host}],
+ [{style, danger}]),
+ Unban = make_command(unban_account, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ Res = ?H1GLraw(<<"Authentication">>,
+ <<"admin/configuration/authentication/">>,
+ <<"Authentication">>)
+ ++ [make_command(register, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(check_account, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ ?X(<<"hr">>),
+ make_command(check_password, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(check_password_hash, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(change_password,
+ R,
+ [{<<"user">>, User}, {<<"host">>, Host}],
+ [{style, danger}]),
+ ?X(<<"hr">>),
+ make_command(get_ban_details, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ Ban,
+ Unban,
+ ?X(<<"hr">>),
+ make_command(unregister,
+ R,
+ [{<<"user">>, User}, {<<"host">>, Host}],
+ [{style, danger}])],
+ {stop, Res};
+web_page_hostuser(_, Host, User, #request{path = [<<"mam">>]} = R) ->
+ Res = ?H1GL(<<"MAM">>, <<"modules/#mod_mam">>, <<"mod_mam">>)
+ ++ [make_command(remove_mam_for_user,
+ R,
+ [{<<"user">>, User}, {<<"host">>, Host}],
+ [{style, danger}]),
+ make_command(remove_mam_for_user_with_peer,
+ R,
+ [{<<"user">>, User}, {<<"host">>, Host}],
+ [{style, danger}])],
+ {stop, Res};
+web_page_hostuser(_, Host, User, #request{path = [<<"privacy">>]} = R) ->
+ Res = ?H1GL(<<"Privacy Lists">>, <<"modules/#mod_privacy">>, <<"mod_privacy">>)
+ ++ [make_command(privacy_set, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
+ {stop, Res};
+web_page_hostuser(_, Host, User, #request{path = [<<"private">>]} = R) ->
+ Res = ?H1GL(<<"Private XML Storage">>, <<"modules/#mod_private">>, <<"mod_private">>)
+ ++ [make_command(private_set, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(private_get, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
+ {stop, Res};
+web_page_hostuser(_, Host, User, #request{path = [<<"session">>]} = R) ->
+ Head = [?XC(<<"h1">>, <<"Sessions">>), ?BR],
+ Set = [make_command(resource_num, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(set_presence, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(kick_user, R, [{<<"user">>, User}, {<<"host">>, Host}], [{style, danger}]),
+ make_command(kick_session,
+ R,
+ [{<<"user">>, User}, {<<"host">>, Host}],
+ [{style, danger}])],
+ timer:sleep(100), % kicking sessions takes a while, let's delay the get commands
+ Get = [make_command(user_sessions_info,
+ R,
+ [{<<"user">>, User}, {<<"host">>, Host}],
+ [{result_links, [{node, node, 5, <<>>}]}]),
+ make_command(user_resources, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(get_presence, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(num_resources, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
+ {stop, Head ++ Get ++ Set};
+web_page_hostuser(_, Host, User, #request{path = [<<"vcard">>]} = R) ->
+ Head = ?H1GL(<<"vCard">>, <<"modules/#mod_vcard">>, <<"mod_vcard">>),
+ Set = [make_command(set_nickname, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(set_vcard, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(set_vcard2, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(set_vcard2_multi, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
+ timer:sleep(100), % setting vcard takes a while, let's delay the get commands
+ FieldNames = [<<"VERSION">>, <<"FN">>, <<"NICKNAME">>, <<"BDAY">>],
+ FieldNames2 =
+ [{<<"N">>, <<"FAMILY">>},
+ {<<"N">>, <<"GIVEN">>},
+ {<<"N">>, <<"MIDDLE">>},
+ {<<"ADR">>, <<"CTRY">>},
+ {<<"ADR">>, <<"LOCALITY">>},
+ {<<"EMAIL">>, <<"USERID">>}],
+ Get = [make_command(get_vcard, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ ?XE(<<"blockquote">>,
+ [make_table([<<"name">>, <<"value">>],
+ [{?C(FieldName),
+ make_command(get_vcard,
+ R,
+ [{<<"user">>, User},
+ {<<"host">>, Host},
+ {<<"name">>, FieldName}],
+ [{only, value}])}
+ || FieldName <- FieldNames])]),
+ make_command(get_vcard2, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ ?XE(<<"blockquote">>,
+ [make_table([<<"name">>, <<"subname">>, <<"value">>],
+ [{?C(FieldName),
+ ?C(FieldSubName),
+ make_command(get_vcard2,
+ R,
+ [{<<"user">>, User},
+ {<<"host">>, Host},
+ {<<"name">>, FieldName},
+ {<<"subname">>, FieldSubName}],
+ [{only, value}])}
+ || {FieldName, FieldSubName} <- FieldNames2])]),
+ make_command(get_vcard2_multi, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
+ {stop, Head ++ Get ++ Set};
+web_page_hostuser(Acc, _, _, _) ->
+ Acc.
+
+%%% HostNode
+
+web_menu_hostnode(Acc, _Host, _Username, _Lang) ->
+ Acc ++ [{<<"modules">>, <<"Modules">>}].
+
+web_page_hostnode(_, Host, Node, #request{path = [<<"modules">>]} = R) ->
+ Res = ?H1GLraw(<<"Modules">>, <<"admin/configuration/modules/">>, <<"Modules Options">>)
+ ++ [ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [restart_module, R, [{<<"host">>, Host}], []])],
+ {stop, Res};
+web_page_hostnode(Acc, _Host, _Node, _Request) ->
+ Acc.
+
+%%% Node
+
+web_menu_node(Acc, _Node, _Lang) ->
+ Acc ++ [{<<"stats">>, <<"Statistics">>}].
+
+web_page_node(_, Node, #request{path = [<<"stats">>]} = R) ->
+ UpSecs =
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [stats, R, [{<<"name">>, <<"uptimeseconds">>}], [{only, value}]]),
+ UpDaysBin = integer_to_binary(binary_to_integer(fxml:get_tag_cdata(UpSecs)) div 24000),
+ UpDays =
+ #xmlel{name = <<"code">>,
+ attrs = [],
+ children = [{xmlcdata, UpDaysBin}]},
+ Res = ?H1GL(<<"Statistics">>, <<"modules/#mod_stats">>, <<"mod_stats">>)
+ ++ [make_command(stats, R, [], [{only, presentation}]),
+ make_table([<<"stat name">>, {<<"stat value">>, right}],
+ [{?C(<<"Online Users in this node:">>),
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [stats,
+ R,
+ [{<<"name">>, <<"onlineusersnode">>}],
+ [{only, value}]])},
+ {?C(<<"Uptime Seconds:">>), UpSecs},
+ {?C(<<"Uptime Seconds (rounded to days):">>), UpDays},
+ {?C(<<"Processes:">>),
+ ejabberd_cluster:call(Node,
+ ejabberd_web_admin,
+ make_command,
+ [stats,
+ R,
+ [{<<"name">>, <<"processes">>}],
+ [{only, value}]])}])],
+ {stop, Res};
+web_page_node(Acc, _, _) ->
+ Acc.
+%% @format-end
+
+%%%
+%%% Document
+%%%
+
mod_options(_) -> [].
mod_doc() ->
diff --git a/src/mod_mix_pam.erl b/src/mod_mix_pam.erl
index fcca0c33787..eb94877d288 100644
--- a/src/mod_mix_pam.erl
+++ b/src/mod_mix_pam.erl
@@ -34,7 +34,7 @@
process_iq/1,
get_mix_roster_items/2,
webadmin_user/4,
- webadmin_page/3]).
+ webadmin_menu_hostuser/4, webadmin_page_hostuser/4]).
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
@@ -69,7 +69,8 @@ start(Host, Opts) ->
ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50),
ejabberd_hooks:add(roster_get, Host, ?MODULE, get_mix_roster_items, 50),
ejabberd_hooks:add(webadmin_user, Host, ?MODULE, webadmin_user, 50),
- ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, webadmin_page, 50),
+ ejabberd_hooks:add(webadmin_menu_hostuser, Host, ?MODULE, webadmin_menu_hostuser, 50),
+ ejabberd_hooks:add(webadmin_page_hostuser, Host, ?MODULE, webadmin_page_hostuser, 50),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0, ?MODULE, process_iq),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_2, ?MODULE, process_iq);
Err ->
@@ -82,7 +83,8 @@ stop(Host) ->
ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50),
ejabberd_hooks:delete(roster_get, Host, ?MODULE, get_mix_roster_items, 50),
ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, webadmin_user, 50),
- ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, webadmin_page, 50),
+ ejabberd_hooks:delete(webadmin_menu_hostuser, Host, ?MODULE, webadmin_menu_hostuser, 50),
+ ejabberd_hooks:delete(webadmin_page_hostuser, Host, ?MODULE, webadmin_page_hostuser, 50),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_2).
@@ -469,7 +471,7 @@ delete_cache(Mod, JID, Channel) ->
%%%===================================================================
%%% Webadmin interface
%%%===================================================================
-webadmin_user(Acc, User, Server, Lang) ->
+webadmin_user(Acc, User, Server, #request{lang = Lang}) ->
QueueLen = case get_channels({jid:nodeprep(User), jid:nameprep(Server), <<>>}) of
{ok, Channels} -> length(Channels);
error -> -1
@@ -482,12 +484,13 @@ webadmin_user(Acc, User, Server, Lang) ->
?C(<<" | ">>),
FQueueView].
-webadmin_page(_, Host,
- #request{us = _US, path = [<<"user">>, U, <<"mix_channels">>],
- lang = Lang} = _Request) ->
+webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) ->
+ Acc ++ [{<<"mix_channels">>, <<"MIX Channels">>}].
+
+webadmin_page_hostuser(_, Host, U, #request{path = [<<"mix_channels">>], lang = Lang}) ->
Res = web_mix_channels(U, Host, Lang),
{stop, Res};
-webadmin_page(Acc, _, _) -> Acc.
+webadmin_page_hostuser(Acc, _, _, _) -> Acc.
web_mix_channels(User, Server, Lang) ->
LUser = jid:nodeprep(User),
diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl
index 484700fc6e1..b9e8400224d 100644
--- a/src/mod_muc_admin.erl
+++ b/src/mod_muc_admin.erl
@@ -34,20 +34,24 @@
create_room_with_opts/4, create_room/3, destroy_room/2,
create_rooms_file/1, destroy_rooms_file/1,
rooms_unused_list/2, rooms_unused_destroy/2,
- rooms_empty_list/1, rooms_empty_destroy/1,
+ rooms_empty_list/1, rooms_empty_destroy/1, rooms_empty_destroy_restuple/1,
get_user_rooms/2, get_user_subscriptions/2, get_room_occupants/2,
get_room_occupants_number/2, send_direct_invitation/5,
change_room_option/4, get_room_options/2,
set_room_affiliation/4, get_room_affiliations/2, get_room_affiliation/3,
- web_menu_main/2, web_page_main/2, web_menu_host/3,
subscribe_room/4, subscribe_room_many/3,
unsubscribe_room/2, get_subscribers/2,
get_room_serverhost/1,
- web_page_host/3,
+ web_menu_main/2, web_page_main/2,
+ web_menu_host/3, web_page_host/3,
+ web_menu_hostuser/4, web_page_hostuser/4,
+ webadmin_muc/2,
mod_opt_type/1, mod_options/1,
get_commands_spec/0, find_hosts/1, room_diagnostics/2,
get_room_pid/2, get_room_history/2]).
+-import(ejabberd_web_admin, [make_command/4, make_command_raw_value/3, make_table/4]).
+
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("mod_muc.hrl").
@@ -66,7 +70,10 @@ start(_Host, _Opts) ->
{ok, [{hook, webadmin_menu_main, web_menu_main, 50, global},
{hook, webadmin_page_main, web_page_main, 50, global},
{hook, webadmin_menu_host, web_menu_host, 50},
- {hook, webadmin_page_host, web_page_host, 50}]}.
+ {hook, webadmin_page_host, web_page_host, 50},
+ {hook, webadmin_menu_hostuser, web_menu_hostuser, 50},
+ {hook, webadmin_page_hostuser, web_page_hostuser, 50}
+ ]}.
stop(Host) ->
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
@@ -235,6 +242,19 @@ get_commands_spec() ->
args = [{service, binary}],
args_rename = [{host, service}],
result = {rooms, {list, {room, string}}}},
+ #ejabberd_commands{name = rooms_empty_destroy, tags = [muc],
+ desc = "Destroy the rooms that have no messages in archive",
+ longdesc = "The MUC service argument can be `global` to get all hosts.",
+ module = ?MODULE, function = rooms_empty_destroy_restuple,
+ version = 2,
+ note = "modified in 24.xx",
+ args_desc = ["MUC service, or `global` for all"],
+ args_example = ["conference.example.com"],
+ result_desc = "List of empty rooms that have been destroyed",
+ result_example = {ok, <<"Destroyed rooms: 2">>},
+ args = [{service, binary}],
+ args_rename = [{host, service}],
+ result = {res, restuple}},
#ejabberd_commands{name = get_user_rooms, tags = [muc],
desc = "Get the list of rooms where this user is occupant",
@@ -478,7 +498,13 @@ get_commands_spec() ->
result = {history, {list,
{entry, {tuple,
[{timestamp, string},
- {message, string}]}}}}}
+ {message, string}]}}}}},
+
+ #ejabberd_commands{name = webadmin_muc, tags = [internal],
+ desc = "Generate WebAdmin MUC Rooms HTML",
+ module = ?MODULE, function = webadmin_muc,
+ args = [{request, any}, {lang, binary}],
+ result = {res, any}}
].
@@ -580,6 +606,8 @@ get_user_subscriptions(User, Server) ->
%% Web Admin
%%----------------------------
+%% @format-begin
+
%%---------------
%% Web Admin Menu
@@ -589,112 +617,404 @@ web_menu_main(Acc, Lang) ->
web_menu_host(Acc, _Host, Lang) ->
Acc ++ [{<<"muc">>, translate:translate(Lang, ?T("Multi-User Chat"))}].
-
%%---------------
%% Web Admin Page
-define(TDTD(L, N),
- ?XE(<<"tr">>, [?XCT(<<"td">>, L),
- ?XC(<<"td">>, integer_to_binary(N))
- ])).
-
-web_page_main(_, #request{path=[<<"muc">>], lang = Lang} = _Request) ->
- OnlineRoomsNumber = lists:foldl(
- fun(Host, Acc) ->
- Acc + mod_muc:count_online_rooms(Host)
- end, 0, find_hosts(global)),
+ ?XE(<<"tr">>, [?XCT(<<"td">>, L), ?XC(<<"td">>, integer_to_binary(N))])).
+
+web_page_main(_, #request{path = [<<"muc">>], lang = Lang} = R) ->
PageTitle = translate:translate(Lang, ?T("Multi-User Chat")),
- Res = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>) ++
- [?XCT(<<"h3">>, ?T("Statistics")),
- ?XAE(<<"table">>, [],
- [?XE(<<"tbody">>, [?TDTD(?T("Total rooms"), OnlineRoomsNumber)
- ])
- ]),
- ?XE(<<"ul">>, [?LI([?ACT(<<"rooms/">>, ?T("List of rooms"))])])
- ],
- {stop, Res};
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Res = [make_command(webadmin_muc, R, [{<<"request">>, R}, {<<"lang">>, Lang}], [])],
+ {stop, Title ++ Res};
+web_page_main(Acc, _) ->
+ Acc.
-web_page_main(_, #request{path=[<<"muc">>, <<"rooms">>], q = Q, lang = Lang} = _Request) ->
- Sort_query = get_sort_query(Q),
- Res = make_rooms_page(global, Lang, Sort_query),
+web_page_host(_, Host, #request{path = [<<"muc">> | RPath], lang = Lang} = R) ->
+ PageTitle = translate:translate(Lang, ?T("Multi-User Chat")),
+ Service = find_service(Host),
+ Level = length(RPath),
+ Res = webadmin_muc_host(Host, Service, RPath, R, Lang, Level, PageTitle),
{stop, Res};
+web_page_host(Acc, _, _) ->
+ Acc.
-web_page_main(Acc, _) -> Acc.
-
-web_page_host(_, Host,
- #request{path = [<<"muc">>],
- q = Q,
- lang = Lang} = _Request) ->
- Sort_query = get_sort_query(Q),
- Res = make_rooms_page(Host, Lang, Sort_query),
- {stop, Res};
-web_page_host(Acc, _, _) -> Acc.
+%%---------------
+%% WebAdmin MUC Host Page
+
+webadmin_muc_host(Host,
+ Service,
+ [<<"create-room">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb = make_breadcrumb({service_section, Level, Service, <<"Create Room">>, RPath}),
+ Set = [make_command(create_room, R, [{<<"service">>, Service}, {<<"host">>, Host}], []),
+ make_command(create_room_with_opts,
+ R,
+ [{<<"service">>, Service}, {<<"host">>, Host}],
+ [])],
+ Title ++ Breadcrumb ++ Set;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"nick-register">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb =
+ make_breadcrumb({service_section, Level, Service, <<"Nick Register">>, RPath}),
+ Set = [make_command(muc_register_nick, R, [{<<"service">>, Service}], []),
+ make_command(muc_unregister_nick, R, [{<<"service">>, Service}], [])],
+ Title ++ Breadcrumb ++ Set;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"rooms-empty">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb = make_breadcrumb({service_section, Level, Service, <<"Rooms Empty">>, RPath}),
+ Set = [make_command(rooms_empty_list,
+ R,
+ [{<<"service">>, Service}],
+ [{table_options, {2, RPath}},
+ {result_links, [{room, room, 3 + Level, <<"">>}]}]),
+ make_command(rooms_empty_destroy, R, [{<<"service">>, Service}], [])],
+ Title ++ Breadcrumb ++ Set;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"rooms-unused">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb =
+ make_breadcrumb({service_section, Level, Service, <<"Rooms Unused">>, RPath}),
+ Set = [make_command(rooms_unused_list,
+ R,
+ [{<<"service">>, Service}],
+ [{result_links, [{room, room, 3 + Level, <<"">>}]}]),
+ make_command(rooms_unused_destroy, R, [{<<"service">>, Service}], [])],
+ Title ++ Breadcrumb ++ Set;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"rooms-regex">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb =
+ make_breadcrumb({service_section, Level, Service, <<"Rooms by Regex">>, RPath}),
+ Set = [make_command(muc_online_rooms_by_regex,
+ R,
+ [{<<"service">>, Service}],
+ [{result_links, [{jid, room, 3 + Level, <<"">>}]}])],
+ Title ++ Breadcrumb ++ Set;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"rooms">>, <<"room">>, Name, <<"affiliations">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb =
+ make_breadcrumb({room_section, Level, Service, <<"Affiliations">>, Name, R, RPath}),
+ Set = [make_command(set_room_affiliation,
+ R,
+ [{<<"name">>, Name}, {<<"service">>, Service}],
+ [])],
+ Get = [make_command(get_room_affiliations,
+ R,
+ [{<<"name">>, Name}, {<<"service">>, Service}],
+ [{table_options, {20, RPath}}])],
+ Title ++ Breadcrumb ++ Get ++ Set;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"rooms">>, <<"room">>, Name, <<"history">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb =
+ make_breadcrumb({room_section, Level, Service, <<"History">>, Name, R, RPath}),
+ Get = [make_command(get_room_history,
+ R,
+ [{<<"name">>, Name}, {<<"service">>, Service}],
+ [{table_options, {10, RPath}},
+ {result_links, [{message, paragraph, 1, <<"">>}]}])],
+ Title ++ Breadcrumb ++ Get;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"rooms">>, <<"room">>, Name, <<"invite">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb =
+ make_breadcrumb({room_section, Level, Service, <<"Invite">>, Name, R, RPath}),
+ Set = [make_command(send_direct_invitation,
+ R,
+ [{<<"name">>, Name}, {<<"service">>, Service}],
+ [])],
+ Title ++ Breadcrumb ++ Set;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"rooms">>, <<"room">>, Name, <<"occupants">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb =
+ make_breadcrumb({room_section, Level, Service, <<"Occupants">>, Name, R, RPath}),
+ Get = [make_command(get_room_occupants,
+ R,
+ [{<<"name">>, Name}, {<<"service">>, Service}],
+ [{table_options, {20, RPath}},
+ {result_links, [{jid, user, 3 + Level, <<"">>}]}])],
+ Title ++ Breadcrumb ++ Get;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"rooms">>, <<"room">>, Name, <<"options">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb =
+ make_breadcrumb({room_section, Level, Service, <<"Options">>, Name, R, RPath}),
+ Set = [make_command(change_room_option,
+ R,
+ [{<<"name">>, Name}, {<<"service">>, Service}],
+ [])],
+ Get = [make_command(get_room_options,
+ R,
+ [{<<"name">>, Name}, {<<"service">>, Service}],
+ [])],
+ Title ++ Breadcrumb ++ Get ++ Set;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"rooms">>, <<"room">>, Name, <<"subscribers">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title =
+ ?H1GLraw(PageTitle,
+ <<"developer/xmpp-clients-bots/extensions/muc-sub/">>,
+ <<"MUC/Sub Extension">>),
+ Breadcrumb =
+ make_breadcrumb({room_section, Level, Service, <<"Subscribers">>, Name, R, RPath}),
+ Set = [make_command(subscribe_room,
+ R,
+ [{<<"room">>, jid:encode({Name, Service, <<"">>})}],
+ []),
+ make_command(unsubscribe_room,
+ R,
+ [{<<"room">>, jid:encode({Name, Service, <<"">>})}],
+ [{style, danger}])],
+ Get = [make_command(get_subscribers,
+ R,
+ [{<<"name">>, Name}, {<<"service">>, Service}],
+ [{table_options, {20, RPath}},
+ {result_links, [{jid, user, 3 + Level, <<"">>}]}])],
+ Title ++ Breadcrumb ++ Get ++ Set;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"rooms">>, <<"room">>, Name, <<"destroy">> | RPath],
+ R,
+ _Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb =
+ make_breadcrumb({room_section, Level, Service, <<"Destroy">>, Name, R, RPath}),
+ Set = [make_command(destroy_room,
+ R,
+ [{<<"name">>, Name}, {<<"service">>, Service}],
+ [{style, danger}])],
+ Title ++ Breadcrumb ++ Set;
+webadmin_muc_host(_Host,
+ Service,
+ [<<"rooms">>, <<"room">>, Name | _RPath],
+ _R,
+ Lang,
+ Level,
+ PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb = make_breadcrumb({room, Level, Service, Name}),
+ MenuItems =
+ [{<<"affiliations/">>, <<"Affiliations">>},
+ {<<"history/">>, <<"History">>},
+ {<<"invite/">>, <<"Invite">>},
+ {<<"occupants/">>, <<"Occupants">>},
+ {<<"options/">>, <<"Options">>},
+ {<<"subscribers/">>, <<"Subscribers">>},
+ {<<"destroy/">>, <<"Destroy">>}],
+ Get = [?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- MenuItems])],
+ Title ++ Breadcrumb ++ Get;
+webadmin_muc_host(_Host, Service, [<<"rooms">> | RPath], R, _Lang, Level, PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb = make_breadcrumb({service_section, Level, Service, <<"Rooms">>, RPath}),
+ Columns = [<<"jid">>, <<"occupants">>],
+ Rows =
+ lists:map(fun(NameService) ->
+ #jid{user = Name} = jid:decode(NameService),
+ {make_command(echo,
+ R,
+ [{<<"sentence">>, jid:encode({Name, Service, <<"">>})}],
+ [{only, raw_and_value},
+ {result_links, [{sentence, room, 3 + Level, <<"">>}]}]),
+ make_command(get_room_occupants_number,
+ R,
+ [{<<"name">>, Name}, {<<"service">>, Service}],
+ [{only, raw_and_value}])}
+ end,
+ make_command_raw_value(muc_online_rooms, R, [{<<"service">>, Service}])),
+ Get = [make_command(muc_online_rooms, R, [], [{only, presentation}]),
+ make_command(get_room_occupants_number, R, [], [{only, presentation}]),
+ make_table(20, RPath, Columns, Rows)],
+ Title ++ Breadcrumb ++ Get;
+webadmin_muc_host(_Host, Service, [], _R, Lang, _Level, PageTitle) ->
+ Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>),
+ Breadcrumb = make_breadcrumb({service, Service}),
+ MenuItems =
+ [{<<"create-room/">>, <<"Create Room">>},
+ {<<"rooms/">>, <<"Rooms">>},
+ {<<"rooms-regex/">>, <<"Rooms by Regex">>},
+ {<<"rooms-empty/">>, <<"Rooms Empty">>},
+ {<<"rooms-unused/">>, <<"Rooms Unused">>},
+ {<<"nick-register/">>, <<"Nick Register">>}],
+ Get = [?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- MenuItems])],
+ Title ++ Breadcrumb ++ Get;
+webadmin_muc_host(_Host, _Service, _RPath, _R, _Lang, _Level, _PageTitle) ->
+ [].
+
+make_breadcrumb({service, Service}) ->
+ make_breadcrumb([Service]);
+make_breadcrumb({service_section, Level, Service, Section, RPath}) ->
+ make_breadcrumb([{Level, Service}, separator, Section | RPath]);
+make_breadcrumb({room, Level, Service, Name}) ->
+ make_breadcrumb([{Level, Service},
+ separator,
+ {Level - 1, <<"Rooms">>},
+ separator,
+ jid:encode({Name, Service, <<"">>})]);
+make_breadcrumb({room_section, Level, Service, Section, Name, R, RPath}) ->
+ make_breadcrumb([{Level, Service},
+ separator,
+ {Level - 1, <<"Rooms">>},
+ separator,
+ make_command(echo,
+ R,
+ [{<<"sentence">>, jid:encode({Name, Service, <<"">>})}],
+ [{only, value},
+ {result_links, [{sentence, room, 3 + Level, <<"">>}]}]),
+ 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).
+%%---------------
+%%
%% Returns: {normal | reverse, Integer}
get_sort_query(Q) ->
case catch get_sort_query2(Q) of
- {ok, Res} -> Res;
- _ -> {normal, 1}
+ {ok, Res} ->
+ Res;
+ _ ->
+ {normal, 1}
end.
get_sort_query2(Q) ->
{value, {_, Binary}} = lists:keysearch(<<"sort">>, 1, Q),
Integer = list_to_integer(string:strip(binary_to_list(Binary), right, $/)),
case Integer >= 0 of
- true -> {ok, {normal, Integer}};
- false -> {ok, {reverse, abs(Integer)}}
+ true ->
+ {ok, {normal, Integer}};
+ false ->
+ {ok, {reverse, abs(Integer)}}
end.
-make_rooms_page(Host, Lang, {Sort_direction, Sort_column}) ->
+webadmin_muc(#request{q = Q} = R, Lang) ->
+ {Sort_direction, Sort_column} = get_sort_query(Q),
+ Host = global,
Service = find_service(Host),
Rooms_names = get_online_rooms(Service),
Rooms_infos = build_info_rooms(Rooms_names),
Rooms_sorted = sort_rooms(Sort_direction, Sort_column, Rooms_infos),
Rooms_prepared = prepare_rooms_infos(Rooms_sorted),
- TList = lists:map(
- fun(Room) ->
- ?XE(<<"tr">>, [?XC(<<"td">>, E) || E <- Room])
- end, Rooms_prepared),
- Titles = [?T("Jabber ID"),
- ?T("# participants"),
- ?T("Last message"),
- ?T("Public"),
- ?T("Persistent"),
- ?T("Logging"),
- ?T("Just created"),
- ?T("Room title"),
- ?T("Node")],
+ TList =
+ lists:map(fun([RoomJid | Room]) ->
+ JidLink =
+ make_command(echo,
+ R,
+ [{<<"sentence">>, RoomJid}],
+ [{only, value},
+ {result_links, [{sentence, room, 1, <<"">>}]}]),
+ ?XE(<<"tr">>, [?XE(<<"td">>, [JidLink]) | [?XC(<<"td">>, E) || E <- Room]])
+ end,
+ Rooms_prepared),
+ Titles =
+ [?T("Jabber ID"),
+ ?T("# participants"),
+ ?T("Last message"),
+ ?T("Public"),
+ ?T("Persistent"),
+ ?T("Logging"),
+ ?T("Just created"),
+ ?T("Room title"),
+ ?T("Node")],
{Titles_TR, _} =
- lists:mapfoldl(
- fun(Title, Num_column) ->
- NCS = integer_to_binary(Num_column),
- TD = ?XE(<<"td">>, [?CT(Title),
- ?C(<<" ">>),
- ?AC(<<"?sort=", NCS/binary>>, <<"<">>),
- ?C(<<" ">>),
- ?AC(<<"?sort=-", NCS/binary>>, <<">">>)]),
- {TD, Num_column+1}
- end,
- 1,
- Titles),
- PageTitle = translate:translate(Lang, ?T("Multi-User Chat")),
- ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>) ++
+ lists:mapfoldl(fun(Title, Num_column) ->
+ NCS = integer_to_binary(Num_column),
+ TD = ?XE(<<"td">>,
+ [?CT(Title),
+ ?C(<<" ">>),
+ ?AC(<<"?sort=", NCS/binary>>, <<"<">>),
+ ?C(<<" ">>),
+ ?AC(<<"?sort=-", NCS/binary>>, <<">">>)]),
+ {TD, Num_column + 1}
+ end,
+ 1,
+ Titles),
[?XCT(<<"h2">>, ?T("Chatrooms")),
?XE(<<"table">>,
- [?XE(<<"thead">>,
- [?XE(<<"tr">>, Titles_TR)]
- ),
- ?XE(<<"tbody">>, TList)
- ]
- )
- ].
+ [?XE(<<"thead">>, [?XE(<<"tr">>, Titles_TR)]), ?XE(<<"tbody">>, TList)])].
sort_rooms(Direction, Column, Rooms) ->
Rooms2 = lists:keysort(Column, Rooms),
case Direction of
- normal -> Rooms2;
- reverse -> lists:reverse(Rooms2)
+ normal ->
+ Rooms2;
+ reverse ->
+ lists:reverse(Rooms2)
end.
build_info_rooms(Rooms) ->
@@ -712,16 +1032,16 @@ build_info_room({Name, Host, _ServerHost, Pid}) ->
Num_participants = maps:size(S#state.users),
Node = node(Pid),
- History = (S#state.history)#lqueue.queue,
+ History = S#state.history#lqueue.queue,
Ts_last_message =
- case p1_queue:is_empty(History) of
- true ->
- <<"A long time ago">>;
- false ->
- Last_message1 = get_queue_last(History),
- {_, _, _, Ts_last, _} = Last_message1,
- xmpp_util:encode_timestamp(Ts_last)
- end,
+ case p1_queue:is_empty(History) of
+ true ->
+ <<"A long time ago">>;
+ false ->
+ Last_message1 = get_queue_last(History),
+ {_, _, _, Ts_last, _} = Last_message1,
+ xmpp_util:encode_timestamp(Ts_last)
+ end,
{<>,
Num_participants,
@@ -739,6 +1059,7 @@ get_queue_last(Queue) ->
prepare_rooms_infos(Rooms) ->
[prepare_room_info(Room) || Room <- Rooms].
+
prepare_room_info(Room_info) ->
{NameHost,
Num_participants,
@@ -748,7 +1069,8 @@ prepare_room_info(Room_info) ->
Logging,
Just_created,
Title,
- Node} = Room_info,
+ Node} =
+ Room_info,
[NameHost,
integer_to_binary(Num_participants),
Ts_last_message,
@@ -763,10 +1085,61 @@ justcreated_to_binary(J) when is_integer(J) ->
JNow = misc:usec_to_now(J),
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(JNow),
str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w",
- [Year, Month, Day, Hour, Minute, Second]);
+ [Year, Month, Day, Hour, Minute, Second]);
justcreated_to_binary(J) when is_atom(J) ->
misc:atom_to_binary(J).
+%%--------------------
+%% Web Admin Host User
+
+web_menu_hostuser(Acc, _Host, _Username, _Lang) ->
+ Acc
+ ++ [{<<"muc-rooms">>, <<"MUC Rooms Online">>},
+ {<<"muc-affiliations">>, <<"MUC Rooms Affiliations">>},
+ {<<"muc-sub">>, <<"MUC Rooms Subscriptions">>},
+ {<<"muc-register">>, <<"MUC Service Registration">>}].
+
+web_page_hostuser(_, Host, User, #request{path = [<<"muc-rooms">> | RPath]} = R) ->
+ Level = 5 + length(RPath),
+ Res = ?H1GL(<<"MUC Rooms Online">>, <<"modules/#mod_muc">>, <<"mod_muc">>)
+ ++ [make_command(get_user_rooms,
+ R,
+ [{<<"user">>, User}, {<<"host">>, Host}],
+ [{table_options, {2, RPath}},
+ {result_links, [{room, room, Level, <<"">>}]}])],
+ {stop, Res};
+web_page_hostuser(_, Host, User, #request{path = [<<"muc-affiliations">>]} = R) ->
+ Jid = jid:encode(
+ jid:make(User, Host)),
+ Res = ?H1GL(<<"MUC Rooms Affiliations">>, <<"modules/#mod_muc">>, <<"mod_muc">>)
+ ++ [make_command(set_room_affiliation, R, [{<<"jid">>, Jid}], []),
+ make_command(get_room_affiliation, R, [{<<"jid">>, Jid}], [])],
+ {stop, Res};
+web_page_hostuser(_, Host, User, #request{path = [<<"muc-sub">> | RPath]} = R) ->
+ Title =
+ ?H1GLraw(<<"MUC Rooms Subscriptions">>,
+ <<"developer/xmpp-clients-bots/extensions/muc-sub/">>,
+ <<"MUC/Sub">>),
+ Level = 5 + length(RPath),
+ Set = [make_command(subscribe_room, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
+ make_command(unsubscribe_room, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
+ Get = [make_command(get_user_subscriptions,
+ R,
+ [{<<"user">>, User}, {<<"host">>, Host}],
+ [{table_options, {20, RPath}},
+ {result_links, [{roomjid, room, Level, <<"">>}]}])],
+ {stop, Title ++ Get ++ Set};
+web_page_hostuser(_, Host, User, #request{path = [<<"muc-register">>]} = R) ->
+ Jid = jid:encode(
+ jid:make(User, Host)),
+ Res = ?H1GL(<<"MUC Service Registration">>, <<"modules/#mod_muc">>, <<"mod_muc">>)
+ ++ [make_command(muc_register_nick, R, [{<<"jid">>, Jid}], []),
+ make_command(muc_unregister_nick, R, [{<<"jid">>, Jid}], [])],
+ {stop, Res};
+web_page_hostuser(Acc, _, _, _) ->
+ Acc.
+%% @format-end
+
%%----------------------------
%% Create/Delete Room
%%----------------------------
@@ -898,6 +1271,10 @@ rooms_empty_list(Service) ->
rooms_empty_destroy(Service) ->
rooms_report(empty, destroy, Service, 0).
+rooms_empty_destroy_restuple(Service) ->
+ DestroyedRooms = rooms_report(empty, destroy, Service, 0),
+ NumberBin = integer_to_binary(length(DestroyedRooms)),
+ {ok, <<"Destroyed rooms: ", NumberBin/binary>>}.
rooms_report(Method, Action, Service, Days) ->
{NA, NP, RP} = muc_unused(Method, Action, Service, Days),
@@ -1413,7 +1790,8 @@ get_room_history(Name, Service) ->
History = p1_queue:to_list((StateData#state.history)#lqueue.queue),
lists:map(
fun({_Nick, Packet, _HaveSubject, TimeStamp, _Size}) ->
- {xmpp_util:encode_timestamp(TimeStamp), fxml:element_to_binary(xmpp:encode(Packet))}
+ {xmpp_util:encode_timestamp(TimeStamp),
+ ejabberd_web_admin:pretty_print_xml(xmpp:encode(Packet))}
end, History);
_ ->
throw({error, "Unable to fetch room state."})
diff --git a/src/mod_offline.erl b/src/mod_offline.erl
index f50452876f2..e8c8c52bf3b 100644
--- a/src/mod_offline.erl
+++ b/src/mod_offline.erl
@@ -60,13 +60,17 @@
find_x_expire/2,
c2s_handle_info/2,
c2s_copy_session/2,
- webadmin_page/3,
+ get_offline_messages/2,
+ webadmin_menu_hostuser/4,
+ webadmin_page_hostuser/4,
webadmin_user/4,
webadmin_user_parse_query/5,
c2s_handle_bind2_inline/1]).
-export([mod_opt_type/1, mod_options/1, mod_doc/0, depends/2]).
+-import(ejabberd_web_admin, [make_command/4, make_command/2]).
+
-deprecated({get_queue_length,2}).
-include("logger.hrl").
@@ -133,7 +137,8 @@ start(Host, Opts) ->
{hook, c2s_handle_info, c2s_handle_info, 50},
{hook, c2s_copy_session, c2s_copy_session, 50},
{hook, c2s_handle_bind2_inline, c2s_handle_bind2_inline, 50},
- {hook, webadmin_page_host, webadmin_page, 50},
+ {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50},
+ {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50},
{hook, webadmin_user, webadmin_user, 50},
{hook, webadmin_user_parse_query, webadmin_user_parse_query, 50},
{iq_handler, ejabberd_sm, ?NS_FLEX_OFFLINE, handle_offline_query}]}.
@@ -730,12 +735,39 @@ discard_warn_sender(Packet, Reason) ->
ok
end.
-webadmin_page(_, Host,
- #request{us = _US, path = [<<"user">>, U, <<"queue">>],
- q = Query, lang = Lang} =
- _Request) ->
- Res = user_queue(U, Host, Query, Lang), {stop, Res};
-webadmin_page(Acc, _, _) -> Acc.
+%%%
+%%% Commands
+%%%
+
+get_offline_messages(User, Server) ->
+ LUser = jid:nodeprep(User),
+ LServer = jid:nameprep(Server),
+ Mod = gen_mod:db_mod(LServer, ?MODULE),
+ HdrsAll = case Mod:read_message_headers(LUser, LServer) of
+ error -> [];
+ L -> L
+ end,
+ format_user_queue(HdrsAll).
+
+%%%
+%%% WebAdmin
+%%%
+
+webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) ->
+ Acc ++ [{<<"queue">>, <<"Offline Queue">>}].
+
+webadmin_page_hostuser(_, Host, U,
+ #request{us = _US, path = [<<"queue">> | RPath],
+ lang = Lang} = R) ->
+ US = {U, Host},
+ PageTitle = str:translate_and_format(Lang, ?T("~ts's Offline Messages Queue"), [us_to_list(US)]),
+ Head = ?H1GL(PageTitle, <<"modules/#mod_offline">>, <<"mod_offline">>),
+ Res = make_command(get_offline_messages, R, [{<<"user">>, U},
+ {<<"host">>, Host}],
+ [{table_options, {10, RPath}},
+ {result_links, [{packet, paragraph, 1, <<"">>}]}]),
+ {stop, Head ++ [Res]};
+webadmin_page_hostuser(Acc, _, _, _) -> Acc.
get_offline_els(LUser, LServer) ->
[Packet || {_Seq, Packet} <- read_messages(LUser, LServer)].
@@ -939,8 +971,7 @@ count_mam_messages(LUser, LServer, ReadMsgs) ->
format_user_queue(Hdrs) ->
lists:map(
- fun({Seq, From, To, TS, El}) ->
- ID = integer_to_binary(Seq),
+ fun({_Seq, From, To, TS, El}) ->
FPacket = ejabberd_web_admin:pretty_print_xml(El),
SFrom = jid:encode(From),
STo = jid:encode(To),
@@ -956,14 +987,7 @@ format_user_queue(Hdrs) ->
{_, _, _} = Now ->
format_time(Now)
end,
- ?XE(<<"tr">>,
- [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
- [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
- ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], Time),
- ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], SFrom),
- ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], STo),
- ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
- [?XC(<<"pre">>, FPacket)])])
+ {Time, SFrom, STo, FPacket}
end, Hdrs).
format_time(Now) ->
@@ -971,111 +995,18 @@ format_time(Now) ->
str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w",
[Year, Month, Day, Hour, Minute, Second]).
-user_queue(User, Server, Query, Lang) ->
- LUser = jid:nodeprep(User),
- LServer = jid:nameprep(Server),
- US = {LUser, LServer},
- Mod = gen_mod:db_mod(LServer, ?MODULE),
- user_queue_parse_query(LUser, LServer, Query),
- HdrsAll = case Mod:read_message_headers(LUser, LServer) of
- error -> [];
- L -> L
- end,
- Hdrs = get_messages_subset(User, Server, HdrsAll),
- FMsgs = format_user_queue(Hdrs),
- PageTitle = str:translate_and_format(Lang, ?T("~ts's Offline Messages Queue"), [us_to_list(US)]),
- (?H1GL(PageTitle, <<"modules/#mod_offline">>, <<"mod_offline">>))
- ++ [?XREST(?T("Submitted"))] ++
- [?XAE(<<"form">>,
- [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
- [?XE(<<"table">>,
- [?XE(<<"thead">>,
- [?XE(<<"tr">>,
- [?X(<<"td">>), ?XCT(<<"td">>, ?T("Time")),
- ?XCT(<<"td">>, ?T("From")),
- ?XCT(<<"td">>, ?T("To")),
- ?XCT(<<"td">>, ?T("Packet"))])]),
- ?XE(<<"tbody">>,
- if FMsgs == [] ->
- [?XE(<<"tr">>,
- [?XAC(<<"td">>, [{<<"colspan">>, <<"4">>}],
- <<" ">>)])];
- true -> FMsgs
- end)]),
- ?BR,
- ?INPUTTD(<<"submit">>, <<"delete">>,
- ?T("Delete Selected"))])].
-
-user_queue_parse_query(LUser, LServer, Query) ->
- Mod = gen_mod:db_mod(LServer, ?MODULE),
- case lists:keysearch(<<"delete">>, 1, Query) of
- {value, _} ->
- case user_queue_parse_query(LUser, LServer, Query, Mod, false) of
- true ->
- flush_cache(Mod, LUser, LServer);
- false ->
- ok
- end;
- _ ->
- ok
- end.
-
-user_queue_parse_query(LUser, LServer, Query, Mod, Acc) ->
- case lists:keytake(<<"selected">>, 1, Query) of
- {value, {_, Seq}, Query2} ->
- NewAcc = case catch binary_to_integer(Seq) of
- I when is_integer(I), I>=0 ->
- Mod:remove_message(LUser, LServer, I),
- true;
- _ ->
- Acc
- end,
- user_queue_parse_query(LUser, LServer, Query2, Mod, NewAcc);
- false ->
- Acc
- end.
-
us_to_list({User, Server}) ->
jid:encode({User, Server, <<"">>}).
get_queue_length(LUser, LServer) ->
count_offline_messages(LUser, LServer).
-get_messages_subset(User, Host, MsgsAll) ->
- MaxOfflineMsgs = case get_max_user_messages(User, Host) of
- Number when is_integer(Number) -> Number;
- _ -> 100
- end,
- Length = length(MsgsAll),
- get_messages_subset2(MaxOfflineMsgs, Length, MsgsAll).
-
-get_messages_subset2(Max, Length, MsgsAll) when Length =< Max * 2 ->
- MsgsAll;
-get_messages_subset2(Max, Length, MsgsAll) ->
- FirstN = Max,
- {MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll),
- MsgsLastN = lists:nthtail(Length - FirstN - FirstN,
- Msgs2),
- NoJID = jid:make(<<"...">>, <<"...">>),
- Seq = <<"0">>,
- IntermediateMsg = #xmlel{name = <<"...">>, attrs = [],
- children = []},
- MsgsFirstN ++ [{Seq, NoJID, NoJID, IntermediateMsg}] ++ MsgsLastN.
-
-webadmin_user(Acc, User, Server, Lang) ->
- QueueLen = count_offline_messages(jid:nodeprep(User),
- jid:nameprep(Server)),
- FQueueLen = ?C(integer_to_binary(QueueLen)),
- FQueueView = ?AC(<<"queue/">>,
- ?T("View Queue")),
- Acc ++
- [?XCT(<<"h3">>, ?T("Offline Messages:")),
- FQueueLen,
- ?C(<<" | ">>),
- FQueueView,
- ?C(<<" | ">>),
- ?INPUTTD(<<"submit">>, <<"removealloffline">>,
- ?T("Remove All Offline Messages"))].
+webadmin_user(Acc, User, Server, R) ->
+ Acc ++ [make_command(get_offline_count, R, [{<<"user">>, User}, {<<"host">>, Server}], [])].
+
+%%%
+%%%
+%%%
-spec delete_all_msgs(binary(), binary()) -> {atomic, any()}.
delete_all_msgs(User, Server) ->
diff --git a/src/mod_roster.erl b/src/mod_roster.erl
index 498146d6348..90298694369 100644
--- a/src/mod_roster.erl
+++ b/src/mod_roster.erl
@@ -46,13 +46,16 @@
import_start/2, import_stop/2, is_subscribed/2,
c2s_self_presence/1, in_subscription/2,
out_subscription/1, set_items/3, remove_user/2,
- get_jid_info/4, encode_item/1, webadmin_page/3,
- webadmin_user/4, get_versioning_feature/2,
+ get_jid_info/4, encode_item/1, get_versioning_feature/2,
roster_version/2, mod_doc/0,
mod_opt_type/1, mod_options/1, set_roster/1, del_roster/3,
process_rosteritems/5,
depends/2, set_item_and_notify_clients/3]).
+-export([webadmin_page_hostuser/4, webadmin_menu_hostuser/4, webadmin_user/4]).
+
+-import(ejabberd_web_admin, [make_command/4, make_command_raw_value/3, make_table/4]).
+
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("mod_roster.hrl").
@@ -98,7 +101,8 @@ start(Host, Opts) ->
{hook, remove_user, remove_user, 50},
{hook, c2s_self_presence, c2s_self_presence, 50},
{hook, c2s_post_auth_features, get_versioning_feature, 50},
- {hook, webadmin_page_host, webadmin_page, 50},
+ {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50},
+ {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50},
{hook, webadmin_user, webadmin_user, 50},
{iq_handler, ejabberd_sm, ?NS_ROSTER, process_iq}]}.
@@ -1016,205 +1020,84 @@ process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-webadmin_page(_, Host,
- #request{us = _US, path = [<<"user">>, U, <<"roster">>],
- q = Query, lang = Lang} =
- _Request) ->
- Res = user_roster(U, Host, Query, Lang), {stop, Res};
-webadmin_page(Acc, _, _) -> Acc.
-
-user_roster(User, Server, Query, Lang) ->
- LUser = jid:nodeprep(User),
- LServer = jid:nameprep(Server),
- US = {LUser, LServer},
- Items1 = get_roster(LUser, LServer),
- Res = user_roster_parse_query(User, Server, Items1,
- Query),
- Items = get_roster(LUser, LServer),
- SItems = lists:sort(Items),
- FItems = case SItems of
- [] -> [?CT(?T("None"))];
- _ ->
- [?XE(<<"table">>,
- [?XE(<<"thead">>,
- [?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Jabber ID")),
- ?XCT(<<"td">>, ?T("Nickname")),
- ?XCT(<<"td">>, ?T("Subscription")),
- ?XCT(<<"td">>, ?T("Pending")),
- ?XCT(<<"td">>, ?T("Groups"))])]),
- ?XE(<<"tbody">>,
- (lists:map(fun (R) ->
- Groups = lists:flatmap(fun
- (Group) ->
- [?C(Group),
- ?BR]
- end,
- R#roster.groups),
- Pending =
- ask_to_pending(R#roster.ask),
- TDJID =
- build_contact_jid_td(R#roster.jid),
- ?XE(<<"tr">>,
- [TDJID,
- ?XAC(<<"td">>,
- [{<<"class">>,
- <<"valign">>}],
- (R#roster.name)),
- ?XAC(<<"td">>,
- [{<<"class">>,
- <<"valign">>}],
- (iolist_to_binary(atom_to_list(R#roster.subscription)))),
- ?XAC(<<"td">>,
- [{<<"class">>,
- <<"valign">>}],
- (iolist_to_binary(atom_to_list(Pending)))),
- ?XAE(<<"td">>,
- [{<<"class">>,
- <<"valign">>}],
- Groups),
- if Pending == in ->
- ?XAE(<<"td">>,
- [{<<"class">>,
- <<"valign">>}],
- [?INPUTT(<<"submit">>,
- <<"validate",
- (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>,
- ?T("Validate"))]);
- true -> ?X(<<"td">>)
- end,
- ?XAE(<<"td">>,
- [{<<"class">>,
- <<"valign">>}],
- [?INPUTTD(<<"submit">>,
- <<"remove",
- (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>,
- ?T("Remove"))])])
- end,
- SItems)))])]
- end,
- PageTitle = str:translate_and_format(Lang, ?T("Roster of ~ts"), [us_to_list(US)]),
- (?H1GL(PageTitle, <<"modules/#mod_roster">>, <<"mod_roster">>))
- ++
- case Res of
- ok -> [?XREST(?T("Submitted"))];
- error -> [?XREST(?T("Bad format"))];
- nothing -> []
- end
- ++
- [?XAE(<<"form">>,
- [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
- ( [?P, ?INPUT(<<"text">>, <<"newjid">>, <<"">>),
- ?C(<<" ">>),
- ?INPUTT(<<"submit">>, <<"addjid">>,
- ?T("Add Jabber ID"))]
- ++ FItems))].
-
-build_contact_jid_td(RosterJID) ->
- ContactJID = jid:make(RosterJID),
- JIDURI = case {ContactJID#jid.luser,
- ContactJID#jid.lserver}
- of
- {<<"">>, _} -> <<"">>;
- {CUser, CServer} ->
- case lists:member(CServer, ejabberd_option:hosts()) of
- false -> <<"">>;
- true ->
- <<"../../../../../server/", CServer/binary, "/user/",
- CUser/binary, "/">>
- end
- end,
- case JIDURI of
- <<>> ->
- ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}],
- (jid:encode(RosterJID)));
- URI when is_binary(URI) ->
- ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
- [?AC(JIDURI, (jid:encode(RosterJID)))])
- end.
-
-user_roster_parse_query(User, Server, Items, Query) ->
- case lists:keysearch(<<"addjid">>, 1, Query) of
- {value, _} ->
- case lists:keysearch(<<"newjid">>, 1, Query) of
- {value, {_, SJID}} ->
- try jid:decode(SJID) of
- JID ->
- user_roster_subscribe_jid(User, Server, JID), ok
- catch _:{bad_jid, _} ->
- error
- end;
- false -> error
- end;
- false ->
- case catch user_roster_item_parse_query(User, Server,
- Items, Query)
- of
- submitted -> ok;
- {'EXIT', _Reason} -> error;
- _ -> nothing
- end
- end.
+%%% @format-begin
+
+webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) ->
+ Acc ++ [{<<"roster">>, <<"Roster">>}].
+
+webadmin_page_hostuser(_, Host, Username, #request{path = [<<"roster">> | RPath]} = R) ->
+ Head = ?H1GL(<<"Roster">>, <<"modules/#mod_roster">>, <<"mod_roster">>),
+ %% Execute twice: first to perform the action, the second to get new roster
+ _ = make_webadmin_roster_table(Host, Username, R, RPath),
+ RV2 = make_webadmin_roster_table(Host, Username, R, RPath),
+ Set = [make_command(add_rosteritem,
+ R,
+ [{<<"localuser">>, Username}, {<<"localhost">>, Host}],
+ []),
+ make_command(push_roster, R, [{<<"user">>, Username}, {<<"host">>, Host}], [])],
+ Get = [make_command(get_roster, R, [], [{only, presentation}]),
+ make_command(delete_rosteritem, R, [], [{only, presentation}]),
+ RV2],
+ {stop, Head ++ Get ++ Set};
+webadmin_page_hostuser(Acc, _, _, _) ->
+ Acc.
-user_roster_subscribe_jid(User, Server, JID) ->
- UJID = jid:make(User, Server),
- Presence = #presence{from = UJID, to = JID, type = subscribe},
- out_subscription(Presence),
- ejabberd_router:route(Presence).
-
-user_roster_item_parse_query(User, Server, Items,
- Query) ->
- lists:foreach(fun (R) ->
- JID = R#roster.jid,
- case lists:keysearch(<<"validate",
- (ejabberd_web_admin:term_to_id(JID))/binary>>,
- 1, Query)
- of
- {value, _} ->
- JID1 = jid:make(JID),
- UJID = jid:make(User, Server),
- Pres = #presence{from = UJID, to = JID1,
- type = subscribed},
- out_subscription(Pres),
- ejabberd_router:route(Pres),
- throw(submitted);
- false ->
- case lists:keysearch(<<"remove",
- (ejabberd_web_admin:term_to_id(JID))/binary>>,
- 1, Query)
- of
- {value, _} ->
- UJID = jid:make(User, Server),
- RosterItem = #roster_item{
- jid = jid:make(JID),
- subscription = remove},
- process_iq_set(
- #iq{type = set,
- from = UJID,
- to = UJID,
- id = p1_rand:get_string(),
- sub_els = [#roster_query{
- items = [RosterItem]}]}),
- throw(submitted);
- false -> ok
- end
- end
- end,
- Items),
- nothing.
-
-us_to_list({User, Server}) ->
- jid:encode({User, Server, <<"">>}).
-
-webadmin_user(Acc, User, Server, Lang) ->
- QueueLen = length(get_roster(jid:nodeprep(User), jid:nameprep(Server))),
- FQueueLen = ?C(integer_to_binary(QueueLen)),
- FQueueView = ?AC(<<"roster/">>, ?T("View Roster")),
- Acc ++
- [?XCT(<<"h3">>, ?T("Roster:")),
- FQueueLen,
- ?C(<<" | ">>),
- FQueueView].
+make_webadmin_roster_table(Host, Username, R, RPath) ->
+ Contacts =
+ case make_command_raw_value(get_roster, R, [{<<"user">>, Username}, {<<"host">>, Host}])
+ of
+ Cs when is_list(Cs) ->
+ Cs;
+ _ ->
+ []
+ end,
+ Level = 5 + length(RPath),
+ Columns =
+ [<<"jid">>, <<"nick">>, <<"subscription">>, <<"pending">>, <<"groups">>, <<"">>],
+ Rows =
+ lists:map(fun({Jid, Nick, Subscriptions, Pending, Groups}) ->
+ {JidSplit, ProblematicBin} =
+ try jid:decode(Jid) of
+ #jid{} = J ->
+ {jid:split(J), <<"">>}
+ catch
+ _:{bad_jid, _} ->
+ ?INFO_MSG("Error parsing contact of ~s@~s that is invalid JID: ~s",
+ [Username, Host, Jid]),
+ {{<<"000--error-parsing-jid">>, <<"localhost">>, <<"">>},
+ <<", Error parsing JID: ", Jid/binary>>}
+ end,
+ {make_command(echo,
+ R,
+ [{<<"sentence">>, jid:encode(JidSplit)}],
+ [{only, raw_and_value},
+ {result_links, [{sentence, user, Level, <<"">>}]}]),
+ ?C(<>),
+ ?C(Subscriptions),
+ ?C(Pending),
+ ?C(Groups),
+ make_command(delete_rosteritem,
+ R,
+ [{<<"localuser">>, Username},
+ {<<"localhost">>, Host},
+ {<<"user">>, element(1, JidSplit)},
+ {<<"host">>, element(2, JidSplit)}],
+ [{only, button},
+ {style, danger},
+ {input_name_append,
+ [Username,
+ Host,
+ element(1, JidSplit),
+ element(2, JidSplit)]}])}
+ end,
+ lists:keysort(1, Contacts)),
+ Table = make_table(20, RPath, Columns, Rows),
+ ?XE(<<"blockquote">>, [Table]).
+
+webadmin_user(Acc, User, Server, R) ->
+ Acc
+ ++ [make_command(get_roster_count, R, [{<<"user">>, User}, {<<"host">>, Server}], [])].
+%%% @format-end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec has_duplicated_groups([binary()]) -> boolean().
diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl
index 447ed46665a..cf1e0f5360c 100644
--- a/src/mod_shared_roster.erl
+++ b/src/mod_shared_roster.erl
@@ -41,6 +41,8 @@
is_user_in_group/3, add_user_to_group/3, opts_to_binary/1,
remove_user_from_group/3, mod_opt_type/1, mod_options/1, mod_doc/0, depends/2]).
+-import(ejabberd_web_admin, [make_command/4, make_command_raw_value/3, make_table/2, make_table/4]).
+
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
@@ -861,286 +863,402 @@ unset_presence(User, Server, Resource, Status) ->
end.
%%---------------------
-%% Web Admin
+%% Web Admin: Page Frontend
%%---------------------
+%% @format-begin
+
webadmin_menu(Acc, _Host, Lang) ->
- [{<<"shared-roster">>, translate:translate(Lang, ?T("Shared Roster Groups"))}
- | Acc].
-
-webadmin_page(_, Host,
- #request{us = _US, path = [<<"shared-roster">>],
- q = Query, lang = Lang} =
- _Request) ->
- Res = list_shared_roster_groups(Host, Query, Lang),
- {stop, Res};
-webadmin_page(_, Host,
- #request{us = _US, path = [<<"shared-roster">>, Group],
- q = Query, lang = Lang} =
- _Request) ->
- Res = shared_roster_group(Host, Group, Query, Lang),
- {stop, Res};
-webadmin_page(Acc, _, _) -> Acc.
-
-list_shared_roster_groups(Host, Query, Lang) ->
- Res = list_sr_groups_parse_query(Host, Query),
- SRGroups = list_groups(Host),
- FGroups = (?XAE(<<"table">>, [],
- [?XE(<<"tbody">>,
- [?XE(<<"tr">>,
- [?X(<<"td">>),
- ?XE(<<"td">>, [?CT(?T("Name:"))])
- ])]++
- (lists:map(fun (Group) ->
- ?XE(<<"tr">>,
- [?XE(<<"td">>,
- [?INPUT(<<"checkbox">>,
- <<"selected">>,
- Group)]),
- ?XE(<<"td">>,
- [?AC(<>,
- Group)])])
- end,
- lists:sort(SRGroups))
- ++
- [?XE(<<"tr">>,
- [?X(<<"td">>),
- ?XE(<<"td">>,
- [?INPUT(<<"text">>, <<"namenew">>,
- <<"">>),
- ?C(<<" ">>),
- ?INPUTT(<<"submit">>, <<"addnew">>,
- ?T("Add New"))])])]))])),
- (?H1GL((translate:translate(Lang, ?T("Shared Roster Groups"))),
- <<"modules/#mod_shared_roster">>, <<"mod_shared_roster">>))
- ++
- case Res of
- ok -> [?XREST(?T("Submitted"))];
- error -> [?XREST(?T("Bad format"))];
- nothing -> []
- end
- ++
- [?XAE(<<"form">>,
- [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
- [FGroups, ?BR,
- ?INPUTTD(<<"submit">>, <<"delete">>,
- ?T("Delete Selected"))])].
-
-list_sr_groups_parse_query(Host, Query) ->
- case lists:keysearch(<<"addnew">>, 1, Query) of
- {value, _} -> list_sr_groups_parse_addnew(Host, Query);
- _ ->
- case lists:keysearch(<<"delete">>, 1, Query) of
- {value, _} -> list_sr_groups_parse_delete(Host, Query);
- _ -> nothing
- end
- end.
+ [{<<"shared-roster">>, translate:translate(Lang, ?T("Shared Roster Groups"))} | Acc].
+
+webadmin_page(_,
+ Host,
+ #request{us = _US,
+ path = [<<"shared-roster">> | RPath],
+ lang = Lang} =
+ R) ->
+ PageTitle = translate:translate(Lang, ?T("Shared Roster Groups")),
+ Head = ?H1GL(PageTitle, <<"modules/#mod_shared_roster">>, <<"mod_shared_roster">>),
+ Level = length(RPath),
+ Res = case check_group_exists(Host, RPath) of
+ true ->
+ webadmin_page_backend(Host, RPath, R, Lang, Level);
+ false ->
+ [?XREST(<<"Group does not exist.">>)]
+ end,
+ {stop, Head ++ Res};
+webadmin_page(Acc, _, _) ->
+ Acc.
-list_sr_groups_parse_addnew(Host, Query) ->
- case lists:keysearch(<<"namenew">>, 1, Query) of
- {value, {_, Group}} when Group /= <<"">> ->
- create_group(Host, Group),
- ok;
- _ ->
- error
- end.
+check_group_exists(Host, [<<"group">>, Id | _]) ->
+ case get_group_opts(Host, Id) of
+ error ->
+ false;
+ _ ->
+ true
+ end;
+check_group_exists(_, _) ->
+ true.
-list_sr_groups_parse_delete(Host, Query) ->
- SRGroups = list_groups(Host),
- lists:foreach(fun (Group) ->
- case lists:member({<<"selected">>, Group}, Query) of
- true -> delete_group(Host, Group);
- _ -> ok
- end
- end,
- SRGroups),
- ok.
+%%---------------------
+%% Web Admin: Page Backend
+%%---------------------
-shared_roster_group(Host, Group, Query, Lang) ->
- Res = shared_roster_group_parse_query(Host, Group,
- Query),
- GroupOpts = get_group_opts(Host, Group),
- Label = get_opt(GroupOpts, label, <<"">>), %%++
- Description = get_opt(GroupOpts, description, <<"">>),
- AllUsers = get_opt(GroupOpts, all_users, false),
- OnlineUsers = get_opt(GroupOpts, online_users, false),
- DisplayedGroups = get_opt(GroupOpts, displayed_groups,
- []),
- Members = get_group_explicit_users(Host,
- Group),
- FMembers = iolist_to_binary(
- [if AllUsers -> <<"@all@\n">>;
- true -> <<"">>
- end,
- if OnlineUsers -> <<"@online@\n">>;
- true -> <<"">>
- end,
- [[us_to_list(Member), $\n] || Member <- Members]]),
- FDisplayedGroups = [<> || DG <- DisplayedGroups],
- DescNL = length(ejabberd_regexp:split(Description,
- <<"\n">>)),
- FGroup = (?XAE(<<"table">>,
- [{<<"class">>, <<"withtextareas">>}],
- [?XE(<<"tbody">>,
- [?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Name:")),
- ?XE(<<"td">>, [?C(Group)]),
- ?XE(<<"td">>, [?C(<<"">>)])]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Label:")),
- ?XE(<<"td">>,
- [?INPUT(<<"text">>, <<"label">>, Label)]),
- ?XE(<<"td">>, [?CT(?T("Name in the rosters where this group will be displayed"))])]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Description:")),
- ?XE(<<"td">>,
- [?TEXTAREA(<<"description">>,
- integer_to_binary(lists:max([3,
- DescNL])),
- <<"20">>, Description)]),
- ?XE(<<"td">>, [?CT(?T("Only admins can see this"))])
-]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Members:")),
- ?XE(<<"td">>,
- [?TEXTAREA(<<"members">>,
- integer_to_binary(lists:max([3,
- length(Members)+3])),
- <<"20">>, FMembers)]),
- ?XE(<<"td">>, [?C(<<"JIDs, @all@, @online@">>)])
-]),
- ?XE(<<"tr">>,
- [?XCT(<<"td">>, ?T("Displayed:")),
- ?XE(<<"td">>,
- [?TEXTAREA(<<"dispgroups">>,
- integer_to_binary(lists:max([3, length(FDisplayedGroups)])),
- <<"20">>,
- list_to_binary(FDisplayedGroups))]),
- ?XE(<<"td">>, [?CT(?T("Groups that will be displayed to the members"))])
-])])])),
- (?H1GL((translate:translate(Lang, ?T("Shared Roster Groups"))),
- <<"modules/#mod_shared_roster">>, <<"mod_shared_roster">>))
- ++
- [?XC(<<"h2">>, translate:translate(Lang, ?T("Group")))] ++
- case Res of
- ok -> [?XREST(?T("Submitted"))];
- {error_elements, NonAddedList1, NG1} ->
- make_error_el(Lang,
- ?T("Members not added (inexistent vhost!): "),
- [jid:encode({U,S,<<>>}) || {U,S} <- NonAddedList1])
- ++ make_error_el(Lang, ?T("'Displayed groups' not added (they do not exist!): "), NG1);
- error -> [?XREST(?T("Bad format"))];
- nothing -> []
- end
- ++
- [?XAE(<<"form">>,
- [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
- [FGroup, ?BR,
- ?INPUTT(<<"submit">>, <<"submit">>, ?T("Submit"))])].
-
-make_error_el(_, _, []) ->
- [];
-make_error_el(Lang, Message, BinList) ->
- NG2 = str:join(BinList, <<", ">>),
- NG3 = translate:translate(Lang, Message),
- NG4 = str:concat(NG3, NG2),
- [?XRES(NG4)].
-
-shared_roster_group_parse_query(Host, Group, Query) ->
- case lists:keysearch(<<"submit">>, 1, Query) of
- {value, _} ->
- {value, {_, Label}} = lists:keysearch(<<"label">>, 1,
- Query), %++
- {value, {_, Description}} =
- lists:keysearch(<<"description">>, 1, Query),
- {value, {_, SMembers}} = lists:keysearch(<<"members">>,
- 1, Query),
- {value, {_, SDispGroups}} =
- lists:keysearch(<<"dispgroups">>, 1, Query),
- LabelOpt = if Label == <<"">> -> [];
- true -> [{label, Label}] %++
- end,
- DescriptionOpt = if Description == <<"">> -> [];
- true -> [{description, Description}]
- end,
- DispGroups1 = str:tokens(SDispGroups, <<"\r\n">>),
- {DispGroups, WrongDispGroups} = filter_groups_existence(Host, DispGroups1),
- DispGroupsOpt = if DispGroups == [] -> [];
- true -> [{displayed_groups, DispGroups}]
- end,
- OldMembers = get_group_explicit_users(Host,
- Group),
- SJIDs = str:tokens(SMembers, <<", \r\n">>),
- NewMembers = lists:foldl(fun (_SJID, error) -> error;
- (SJID, USs) ->
- case SJID of
- <<"@all@">> -> USs;
- <<"@online@">> -> USs;
- _ ->
- try jid:decode(SJID) of
- JID ->
- [{JID#jid.luser,
- JID#jid.lserver}
- | USs]
- catch _:{bad_jid, _} ->
- error
- end
- end
- end,
- [], SJIDs),
- AllUsersOpt = case lists:member(<<"@all@">>, SJIDs) of
- true -> [{all_users, true}];
- false -> []
- end,
- OnlineUsersOpt = case lists:member(<<"@online@">>,
- SJIDs)
- of
- true -> [{online_users, true}];
- false -> []
- end,
- CurrentDisplayedGroups = get_displayed_groups(Group, Host),
- AddedDisplayedGroups = DispGroups -- CurrentDisplayedGroups,
- RemovedDisplayedGroups = CurrentDisplayedGroups -- DispGroups,
- displayed_groups_update(OldMembers, RemovedDisplayedGroups, remove),
- displayed_groups_update(OldMembers, AddedDisplayedGroups, both),
- set_group_opts(Host, Group,
- LabelOpt ++
- DispGroupsOpt ++
- DescriptionOpt ++
- AllUsersOpt ++ OnlineUsersOpt),
- if NewMembers == error -> error;
- true ->
- AddedMembers = NewMembers -- OldMembers,
- RemovedMembers = OldMembers -- NewMembers,
- lists:foreach(
- fun(US) ->
- remove_user_from_group(Host,
- US,
- Group)
- end,
- RemovedMembers),
- NonAddedMembers = lists:filter(
- fun(US) ->
- error == add_user_to_group(Host, US,
- Group)
- end,
- AddedMembers),
- case (NonAddedMembers /= []) or (WrongDispGroups /= []) of
- true -> {error_elements, NonAddedMembers, WrongDispGroups};
- false -> ok
- end
- end;
- _ -> nothing
- end.
+webadmin_page_backend(Host, [<<"group">>, Id, <<"info">> | RPath], R, _Lang, Level) ->
+ Breadcrumb =
+ make_breadcrumb({group_section,
+ Level,
+ <<"Groups of ", Host/binary>>,
+ Id,
+ <<"Information">>,
+ RPath}),
+ SetLabel =
+ make_command(srg_set_info,
+ R,
+ [{<<"host">>, Host}, {<<"group">>, Id}, {<<"key">>, <<"label">>}],
+ [{only, without_presentation}, {input_name_append, [Id, Host, <<"label">>]}]),
+ SetDescription =
+ make_command(srg_set_info,
+ R,
+ [{<<"host">>, Host}, {<<"group">>, Id}, {<<"key">>, <<"description">>}],
+ [{only, without_presentation},
+ {input_name_append, [Id, Host, <<"description">>]}]),
+ SetAll =
+ make_command(srg_set_info,
+ R,
+ [{<<"host">>, Host},
+ {<<"group">>, Id},
+ {<<"key">>, <<"all_users">>},
+ {<<"value">>, <<"true">>}],
+ [{only, button},
+ {input_name_append, [Id, Host, <<"all_users">>, <<"true">>]}]),
+ UnsetAll =
+ make_command(srg_set_info,
+ R,
+ [{<<"host">>, Host},
+ {<<"group">>, Id},
+ {<<"key">>, <<"all_users">>},
+ {<<"value">>, <<"false">>}],
+ [{only, button},
+ {input_name_append, [Id, Host, <<"all_users">>, <<"false">>]}]),
+ SetOnline =
+ make_command(srg_set_info,
+ R,
+ [{<<"host">>, Host},
+ {<<"group">>, Id},
+ {<<"key">>, <<"online_users">>},
+ {<<"value">>, <<"true">>}],
+ [{only, button},
+ {input_name_append, [Id, Host, <<"online_users">>, <<"true">>]}]),
+ UnsetOnline =
+ make_command(srg_set_info,
+ R,
+ [{<<"host">>, Host},
+ {<<"group">>, Id},
+ {<<"key">>, <<"online_users">>},
+ {<<"value">>, <<"false">>}],
+ [{only, button},
+ {input_name_append, [Id, Host, <<"online_users">>, <<"false">>]}]),
+ GetInfo =
+ make_command_raw_value(srg_get_info, R, [{<<"group">>, Id}, {<<"host">>, Host}]),
+ AllElement =
+ case proplists:get_value(<<"all_users">>, GetInfo, not_found) of
+ "true" ->
+ {?C("Unset @all@: "), UnsetAll};
+ _ ->
+ {?C("Set @all@: "), SetAll}
+ end,
+ OnlineElement =
+ case proplists:get_value(<<"online_users">>, GetInfo, not_found) of
+ "true" ->
+ {?C("Unset @online@: "), UnsetOnline};
+ _ ->
+ {?C("Set @online@: "), SetOnline}
+ end,
+ Types =
+ [{?C("Set label: "), SetLabel},
+ {?C("Set description: "), SetDescription},
+ AllElement,
+ OnlineElement],
+ Get = [?BR,
+ make_command(srg_get_info, R, [{<<"host">>, Host}, {<<"group">>, Id}], []),
+ make_command(srg_set_info, R, [], [{only, presentation}]),
+ make_table(20, [], [{<<"">>, right}, <<"">>], Types)],
+ Breadcrumb ++ Get;
+webadmin_page_backend(Host,
+ [<<"group">>, Id, <<"displayed">> | RPath],
+ R,
+ _Lang,
+ Level) ->
+ Breadcrumb =
+ make_breadcrumb({group_section,
+ Level,
+ <<"Groups of ", Host/binary>>,
+ Id,
+ <<"Displayed Groups">>,
+ RPath}),
+ AddDisplayed =
+ make_command(srg_add_displayed, R, [{<<"host">>, Host}, {<<"group">>, Id}], []),
+ _ = make_webadmin_displayed_table(Host, Id, R),
+ DisplayedTable = make_webadmin_displayed_table(Host, Id, R),
+ Get = [?BR,
+ make_command(srg_get_displayed, R, [], [{only, presentation}]),
+ make_command(srg_del_displayed, R, [], [{only, presentation}]),
+ ?XE(<<"blockquote">>, [DisplayedTable]),
+ AddDisplayed],
+ Breadcrumb ++ Get;
+webadmin_page_backend(Host, [<<"group">>, Id, <<"members">> | RPath], R, _Lang, Level) ->
+ Breadcrumb =
+ make_breadcrumb({group_section,
+ Level,
+ <<"Groups of ", Host/binary>>,
+ Id,
+ <<"Members">>,
+ RPath}),
+ UserAdd = make_command(srg_user_add, R, [{<<"grouphost">>, Host}, {<<"group">>, Id}], []),
+ _ = make_webadmin_members_table(Host, Id, R),
+ MembersTable = make_webadmin_members_table(Host, Id, R),
+ Get = [make_command(srg_get_members, R, [], [{only, presentation}]),
+ make_command(srg_user_del, R, [], [{only, presentation}]),
+ ?XE(<<"blockquote">>, [MembersTable]),
+ UserAdd],
+ Breadcrumb ++ Get;
+webadmin_page_backend(Host, [<<"group">>, Id, <<"delete">> | RPath], R, _Lang, Level) ->
+ Breadcrumb =
+ make_breadcrumb({group_section,
+ Level,
+ <<"Groups of ", Host/binary>>,
+ Id,
+ <<"Delete">>,
+ RPath}),
+ Get = [make_command(srg_delete,
+ R,
+ [{<<"host">>, Host}, {<<"group">>, Id}],
+ [{style, danger}])],
+ Breadcrumb ++ Get;
+webadmin_page_backend(Host, [<<"group">>, Id | _RPath], _R, _Lang, Level) ->
+ Breadcrumb = make_breadcrumb({group, Level, <<"Groups of ", Host/binary>>, Id}),
+ MenuItems =
+ [{<<"info/">>, <<"Information">>},
+ {<<"members/">>, <<"Members">>},
+ {<<"displayed/">>, <<"Displayed Groups">>},
+ {<<"delete/">>, <<"Delete">>}],
+ Get = [?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- MenuItems])],
+ Breadcrumb ++ Get;
+webadmin_page_backend(Host, RPath, R, _Lang, Level) ->
+ Breadcrumb = make_breadcrumb({groups, <<"Groups of ", Host/binary>>}),
+ _ = make_webadmin_srg_table(Host, R, 3 + Level, RPath),
+ Set = [make_command(srg_add, R, [{<<"host">>, Host}], []),
+ make_command(srg_create, R, [{<<"host">>, Host}], [])],
+ RV2 = make_webadmin_srg_table(Host, R, 3 + Level, RPath),
+ Get = [make_command(srg_list, R, [{<<"host">>, Host}], [{only, presentation}]),
+ make_command(srg_get_info, R, [{<<"host">>, Host}], [{only, presentation}]),
+ make_command(srg_delete, R, [{<<"host">>, Host}], [{only, presentation}]),
+ ?XE(<<"blockquote">>, [RV2])],
+ Breadcrumb ++ Get ++ Set.
-get_opt(Opts, Opt, Default) ->
- case lists:keysearch(Opt, 1, Opts) of
- {value, {_, Val}} -> Val;
- false -> Default
- end.
+%%---------------------
+%% Web Admin: Table Generation
+%%---------------------
-us_to_list({User, Server}) ->
- jid:encode({User, Server, <<"">>}).
+make_webadmin_srg_table(Host, R, Level, RPath) ->
+ Groups =
+ case make_command_raw_value(srg_list, R, [{<<"host">>, Host}]) of
+ Gs when is_list(Gs) ->
+ Gs;
+ _ ->
+ []
+ end,
+ Columns =
+ [<<"id">>,
+ <<"label">>,
+ <<"description">>,
+ <<"all">>,
+ <<"online">>,
+ {<<"members">>, right},
+ {<<"displayed">>, right},
+ <<"">>],
+ Rows =
+ [{make_command(echo3,
+ R,
+ [{<<"first">>, Id}, {<<"second">>, Host}, {<<"sentence">>, Id}],
+ [{only, value}, {result_links, [{sentence, shared_roster, Level, <<"">>}]}]),
+ make_command(echo3,
+ R,
+ [{<<"first">>, Id},
+ {<<"second">>, Host},
+ {<<"sentence">>,
+ iolist_to_binary(proplists:get_value(<<"label">>,
+ make_command_raw_value(srg_get_info,
+ R,
+ [{<<"group">>,
+ Id},
+ {<<"host">>,
+ Host}]),
+ ""))}],
+ [{only, value},
+ {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]),
+ make_command(echo3,
+ R,
+ [{<<"first">>, Id},
+ {<<"second">>, Host},
+ {<<"sentence">>,
+ iolist_to_binary(proplists:get_value(<<"description">>,
+ make_command_raw_value(srg_get_info,
+ R,
+ [{<<"group">>,
+ Id},
+ {<<"host">>,
+ Host}]),
+ ""))}],
+ [{only, value},
+ {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]),
+ make_command(echo3,
+ R,
+ [{<<"first">>, Id},
+ {<<"second">>, Host},
+ {<<"sentence">>,
+ iolist_to_binary(proplists:get_value(<<"all_users">>,
+ make_command_raw_value(srg_get_info,
+ R,
+ [{<<"group">>,
+ Id},
+ {<<"host">>,
+ Host}]),
+ ""))}],
+ [{only, value},
+ {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]),
+ make_command(echo3,
+ R,
+ [{<<"first">>, Id},
+ {<<"second">>, Host},
+ {<<"sentence">>,
+ iolist_to_binary(proplists:get_value(<<"online_users">>,
+ make_command_raw_value(srg_get_info,
+ R,
+ [{<<"group">>,
+ Id},
+ {<<"host">>,
+ Host}]),
+ ""))}],
+ [{only, value},
+ {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]),
+ make_command(echo3,
+ R,
+ [{<<"first">>, Id},
+ {<<"second">>, Host},
+ {<<"sentence">>,
+ integer_to_binary(length(make_command_raw_value(srg_get_members,
+ R,
+ [{<<"group">>, Id},
+ {<<"host">>, Host}])))}],
+ [{only, value},
+ {result_links, [{sentence, shared_roster, Level, <<"members">>}]}]),
+ make_command(echo3,
+ R,
+ [{<<"first">>, Id},
+ {<<"second">>, Host},
+ {<<"sentence">>,
+ integer_to_binary(length(make_command_raw_value(srg_get_displayed,
+ R,
+ [{<<"group">>, Id},
+ {<<"host">>, Host}])))}],
+ [{only, value},
+ {result_links, [{sentence, shared_roster, Level, <<"displayed">>}]}]),
+ make_command(srg_delete,
+ R,
+ [{<<"group">>, Id}, {<<"host">>, Host}],
+ [{only, button}, {style, danger}, {input_name_append, [Id, Host]}])}
+ || Id <- Groups],
+ make_table(20, RPath, Columns, Rows).
+
+make_webadmin_members_table(Host, Id, R) ->
+ Members =
+ case make_command_raw_value(srg_get_members, R, [{<<"host">>, Host}, {<<"group">>, Id}])
+ of
+ Ms when is_list(Ms) ->
+ Ms;
+ _ ->
+ []
+ end,
+ make_table([<<"member">>, <<"">>],
+ [{make_command(echo,
+ R,
+ [{<<"sentence">>, Jid}],
+ [{only, value}, {result_links, [{sentence, user, 6, <<"">>}]}]),
+ make_command(srg_user_del,
+ R,
+ [{<<"user">>,
+ element(1,
+ jid:split(
+ jid:decode(Jid)))},
+ {<<"host">>,
+ element(2,
+ jid:split(
+ jid:decode(Jid)))},
+ {<<"group">>, Id},
+ {<<"grouphost">>, Host}],
+ [{only, button},
+ {style, danger},
+ {input_name_append,
+ [element(1,
+ jid:split(
+ jid:decode(Jid))),
+ element(2,
+ jid:split(
+ jid:decode(Jid))),
+ Id,
+ Host]}])}
+ || Jid <- Members]).
+
+make_webadmin_displayed_table(Host, Id, R) ->
+ Displayed =
+ case make_command_raw_value(srg_get_displayed, R, [{<<"host">>, Host}, {<<"group">>, Id}])
+ of
+ Ms when is_list(Ms) ->
+ Ms;
+ _ ->
+ []
+ end,
+ make_table([<<"group">>, <<"">>],
+ [{make_command(echo3,
+ R,
+ [{<<"first">>, ThisId},
+ {<<"second">>, Host},
+ {<<"sentence">>, ThisId}],
+ [{only, value},
+ {result_links, [{sentence, shared_roster, 6, <<"">>}]}]),
+ make_command(srg_del_displayed,
+ R,
+ [{<<"group">>, Id}, {<<"host">>, Host}, {<<"del">>, ThisId}],
+ [{only, button},
+ {style, danger},
+ {input_name_append, [Id, Host, ThisId]}])}
+ || ThisId <- Displayed]).
+
+make_breadcrumb({groups, Service}) ->
+ make_breadcrumb([Service]);
+make_breadcrumb({group, Level, Service, Name}) ->
+ make_breadcrumb([{Level, Service}, separator, Name]);
+make_breadcrumb({group_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
split_grouphost(Host, Group) ->
case str:tokens(Group, <<"@">>) of
@@ -1148,17 +1266,6 @@ split_grouphost(Host, Group) ->
[_] -> {Host, Group}
end.
-filter_groups_existence(Host, Groups) ->
- lists:partition(
- fun(Group) -> error /= get_group_opts(Host, Group) end,
- Groups).
-
-displayed_groups_update(Members, DisplayedGroups, Subscription) ->
- lists:foreach(
- fun({U, S}) ->
- push_displayed_to_user(U, S, S, Subscription, DisplayedGroups)
- end, Members).
-
opts_to_binary(Opts) ->
lists:map(
fun({label, Label}) ->
diff --git a/test/ejabberd_SUITE_data/ejabberd.yml b/test/ejabberd_SUITE_data/ejabberd.yml
index d3a24315f61..0155e8f9024 100644
--- a/test/ejabberd_SUITE_data/ejabberd.yml
+++ b/test/ejabberd_SUITE_data/ejabberd.yml
@@ -108,6 +108,7 @@ max_fsm_queue: 1000
queue_type: file
modules:
mod_adhoc: []
+ mod_admin_extra: []
mod_admin_update_sql: []
mod_announce: []
mod_configure: []
diff --git a/test/webadmin_tests.erl b/test/webadmin_tests.erl
index a8251dca7ea..753f8793087 100644
--- a/test/webadmin_tests.erl
+++ b/test/webadmin_tests.erl
@@ -76,10 +76,11 @@ adduser(Config) ->
Body = make_query(
Config,
"server/" ++ binary_to_list(Server) ++ "/users/",
- <<"newusername=", (mue(User))/binary, "&newuserpassword=",
- (mue(Password))/binary, "&addnewuser=Add+User">>),
+ <<"register/user=", (mue(User))/binary, "®ister/password=",
+ (mue(Password))/binary, "®ister=Register">>),
Password = ejabberd_auth:get_password(User, Server),
- ?match({_, _}, binary:match(Body, <<"Submitted
">>)).
+ ?match({_, _}, binary:match(Body, <<"ok
">>)).
removeuser(Config) ->
User = <<"userwebadmin-", (?config(user, Config))/binary>>,
@@ -101,7 +102,7 @@ removeuser(Config) ->
Config,
"server/" ++ binary_to_list(Server)
++ "/user/" ++ binary_to_list(mue(User)) ++ "/",
- <<"password=&removeuser=Remove+User">>),
+ <<"&unregister=Unregister">>),
false = ejabberd_auth:user_exists(User, Server),
?match(nomatch, binary:match(Body, <<"Last Activity
20">>)).
diff --git a/tools/emacs-indent.sh b/tools/emacs-indent.sh
new file mode 100755
index 00000000000..f3caecf4cff
--- /dev/null
+++ b/tools/emacs-indent.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+# To indent and remove tabs, surround the piece of code with:
+# %% @indent-begin
+# %% @indent-end
+#
+# Then run:
+# make indent
+#
+# Please note this script only indents the first occurrence.
+
+FILES=$(git grep --name-only @indent-begin src/)
+
+for FILENAME in $FILES; do
+ echo "==> Indenting $FILENAME..."
+ emacs -batch $FILENAME \
+ -f "erlang-mode" \
+ --eval "(goto-char (point-min))" \
+ --eval "(re-search-forward \"@indent-begin\" nil t)" \
+ --eval "(setq begin (line-beginning-position))" \
+ --eval "(re-search-forward \"@indent-end\" nil t)" \
+ --eval "(setq end (line-beginning-position))" \
+ --eval "(erlang-indent-region begin end)" \
+ --eval "(untabify begin end)" \
+ -f "delete-trailing-whitespace" \
+ -f "save-buffer"
+done
diff --git a/tools/rebar3-format.sh b/tools/rebar3-format.sh
new file mode 100755
index 00000000000..bb0f521d00d
--- /dev/null
+++ b/tools/rebar3-format.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# To start formatting a file, add a line that contains:
+# @format-begin
+# Formatting in that file can later be disabled adding another line with:
+# @format-end
+#
+# It can be reenabled again later in the file.
+#
+# Finally, call: make format
+
+REBAR=$1
+
+ERLS=$(git grep --name-only @format-begin src/)
+
+for ERL in $ERLS; do
+ csplit --quiet --prefix=$ERL-format- $ERL /@format-/ "{*}"
+done
+
+EFMTS=$(find src/*-format-* -type f -exec grep --files-with-matches "@format-begin" '{}' ';')
+EFMTS2=""
+for EFMT in $EFMTS; do
+ EFMTS2="$EFMTS2 --files $EFMT"
+done
+$REBAR format $EFMTS2
+
+for ERL in $ERLS; do
+ SPLITS=$(find $ERL-format-* -type f)
+ rm $ERL
+ for SPLIT in $SPLITS; do
+ cat $SPLIT >> $ERL
+ rm $SPLIT
+ done
+done