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