+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2008 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+%%% @headerfile "ejabberd_commands.hrl"
+
+%%% @doc Management of ejabberd commands.
+%%%
+%%% An ejabberd command is an abstract function identified by a name,
+%%% with a defined number and type of calling arguments and type of
+%%% result, that can be defined in any Erlang module and executed
+%%% using any valid frontend.
+%%%
+%%%
+%%% == Define a new ejabberd command ==
+%%%
+%%% ejabberd commands can be defined and registered in
+%%% any Erlang module.
+%%%
+%%% Some commands are procedures; and their purpose is to perform an
+%%% action in the server, so the command result is only some result
+%%% code or result tuple. Other commands are inspectors, and their
+%%% purpose is to gather some information about the server and return
+%%% a detailed response: it can be integer, string, atom, tuple, list
+%%% or a mix of those ones.
+%%%
+%%% The arguments and result of an ejabberd command are strictly
+%%% defined. The number and format of the arguments provided when
+%%% calling an ejabberd command must match the definition of that
+%%% command. The format of the result provided by an ejabberd command
+%%% must be exactly its definition. For example, if a command is said
+%%% to return an integer, it must always return an integer (except in
+%%% case of a crash).
+%%%
+%%% If you are developing an Erlang module that will run inside
+%%% ejabberd and you want to provide a new ejabberd command to
+%%% administer some task related to your module, you only need to:
+%%% implement a function, define the command, and register it.
+%%%
+%%%
+%%% === Define a new ejabberd command ===
+%%%
+%%% An ejabberd command is defined using the Erlang record
+%%% 'ejabberd_commands'. This record has several elements that you
+%%% must define. Note that 'tags', 'desc' and 'longdesc' are optional.
+%%%
+%%% For example let's define an ejabberd command 'pow' that gets the
+%%% integers 'base' and 'exponent'. Its result will be an integer
+%%% 'power':
+%%%
+%%% #ejabberd_commands{name = pow, tags = [test],
+%%% desc = "Return the power of base for exponent",
+%%% longdesc = "This is an example command. The formula is:\n"
+%%% " power = base ^ exponent",
+%%% module = ?MODULE, function = pow,
+%%% args = [{base, integer}, {exponent, integer}],
+%%% result = {power, integer}}
+%%%
+%%%
+%%% === Implement the function associated to the command ===
+%%%
+%%% Now implement a function in your module that matches the arguments
+%%% and result of the ejabberd command.
+%%%
+%%% For example the function calc_power gets two integers Base and
+%%% Exponent. It calculates the power and rounds to an integer:
+%%%
+%%% calc_power(Base, Exponent) ->
+%%% PowFloat = math:pow(Base, Exponent),
+%%% round(PowFloat).
+%%%
+%%% Since this function will be called by ejabberd_commands, it must be exported.
+%%% Add to your module:
+%%% -export([calc_power/2]).
+%%%
+%%% Only some types of result formats are allowed.
+%%% If the format is defined as 'rescode', then your function must return:
+%%% ok | true | atom()
+%%% where the atoms ok and true as considered positive answers,
+%%% and any other response atom is considered negative.
+%%%
+%%% If the format is defined as 'restuple', then the command must return:
+%%% {rescode(), string()}
+%%%
+%%% If the format is defined as '{list, something()}', then the command
+%%% must return a list of something().
+%%%
+%%%
+%%% === Register the command ===
+%%%
+%%% Define this function and put inside the #ejabberd_command you
+%%% defined in the beginning:
+%%%
+%%% commands() ->
+%%% [
+%%%
+%%% ].
+%%%
+%%% You need to include this header file in order to use the record:
+%%%
+%%% -include("ejabberd_commands.hrl").
+%%%
+%%% When your module is initialized or started, register your commands:
+%%%
+%%% ejabberd_commands:register_commands(commands()),
+%%%
+%%% And when your module is stopped, unregister your commands:
+%%%
+%%% ejabberd_commands:unregister_commands(commands()),
+%%%
+%%% That's all! Now when your module is started, the command will be
+%%% registered and any frontend can access it. For example:
+%%%
+%%% $ ejabberdctl help pow
+%%%
+%%% Command Name: pow
+%%%
+%%% Arguments: base::integer
+%%% exponent::integer
+%%%
+%%% Returns: power::integer
+%%%
+%%% Tags: test
+%%%
+%%% Description: Return the power of base for exponent
+%%%
+%%% This is an example command. The formula is:
+%%% power = base ^ exponent
+%%%
+%%% $ ejabberdctl pow 3 4
+%%% 81
+%%%
+%%%
+%%%
+%%% == Execute an ejabberd command ==
+%%%
+%%% ejabberd commands are mean to be executed using any valid
+%%% frontend. An ejabberd commands is implemented in a regular Erlang
+%%% function, so it is also possible to execute this function in any
+%%% Erlang module, without dealing with the associated ejabberd
+%%% commands.
+%%%
+%%%
+%%% == Frontend to ejabberd commands ==
+%%%
+%%% Currently there are two frontends to ejabberd commands: the shell
+%%% script {@link ejabberd_ctl. ejabberdctl}, and the XML-RPC server
+%%% ejabberd_xmlrpc.
+%%%
+%%%
+%%% === ejabberdctl as a frontend to ejabberd commands ===
+%%%
+%%% It is possible to use ejabberdctl to get documentation of any
+%%% command. But ejabberdctl does not support all the argument types
+%%% allowed in ejabberd commands, so there are some ejabberd commands
+%%% that cannot be executed using ejabberdctl.
+%%%
+%%% Also note that the ejabberdctl shell administration script also
+%%% manages ejabberdctl commands, which are unrelated to ejabberd
+%%% commands and can only be executed using ejabberdctl.
+%%%
+%%%
+%%% === ejabberd_xmlrpc as a frontend to ejabberd commands ===
+%%%
+%%% ejabberd_xmlrpc provides an XML-RPC server to execute ejabberd commands.
+%%% ejabberd_xmlrpc is a contributed module published in ejabberd-modules SVN.
+%%%
+%%% Since ejabberd_xmlrpc does not provide any method to get documentation
+%%% of the ejabberd commands, please use ejabberdctl to know which
+%%% commands are available, and their usage.
+%%%
+%%% The number and format of the arguments provided when calling an
+%%% ejabberd command must match the definition of that command. Please
+%%% make sure the XML-RPC call provides the required arguments, with
+%%% the specified format. The order of the arguments in an XML-RPC
+%%% call is not important, because all the data is tagged and will be
+%%% correctly prepared by ejabberd_xmlrpc before executing the ejabberd
+%%% command.
+
+%%% TODO: consider this feature:
+%%% All commands are catched. If an error happens, return the restuple:
+%%% {error, flattened error string}
+%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) need to allows this.
+%%% And ejabberd_xmlrpc must be prepared to handle such an unexpected response.
+
+
+-module(ejabberd_commands).
+-author('badlop@process-one.net').
+
+-export([init/0,
+ list_commands/0,
+ get_command_format/1,
+ get_command_definition/1,
+ get_tags_commands/0,
+ register_commands/1,
+ unregister_commands/1,
+ execute_command/2
+ ]).
+
+-include("ejabberd_commands.hrl").
+-include("ejabberd.hrl").
+
+
+init() ->
+ ets:new(ejabberd_commands, [named_table, set, public,
+ {keypos, #ejabberd_commands.name}]).
+
+%% @spec ([ejabberd_commands()]) -> ok
+%% @doc Register ejabberd commands.
+%% If a command is already registered, a warning is printed and the old command is preserved.
+register_commands(Commands) ->
+ lists:foreach(
+ fun(Command) ->
+ case ets:insert_new(ejabberd_commands, Command) of
+ true ->
+ ok;
+ false ->
+ ?WARNING_MSG("This command is already defined:~n~p", [Command])
+ end
+ end,
+ Commands).
+
+%% @spec ([ejabberd_commands()]) -> ok
+%% @doc Unregister ejabberd commands.
+unregister_commands(Commands) ->
+ lists:foreach(
+ fun(Command) ->
+ ets:delete_object(ejabberd_commands, Command)
+ end,
+ Commands).
+
+%% @spec () -> [{Name::atom(), Args::[aterm()], Desc::string()}]
+%% @doc Get a list of all the available commands, arguments and description.
+list_commands() ->
+ Commands = ets:match(ejabberd_commands,
+ #ejabberd_commands{name = '$1',
+ args = '$2',
+ desc = '$3',
+ _ = '_'}),
+ [{A, B, C} || [A, B, C] <- Commands].
+
+%% @spec (Name::atom()) -> {Args::[aterm()], Result::rterm()} | {error, command_unknown}
+%% @doc Get the format of arguments and result of a command.
+get_command_format(Name) ->
+ Matched = ets:match(ejabberd_commands,
+ #ejabberd_commands{name = Name,
+ args = '$1',
+ result = '$2',
+ _ = '_'}),
+ case Matched of
+ [] ->
+ {error, command_unknown};
+ [[Args, Result]] ->
+ {Args, Result}
+ end.
+
+%% @spec (Name::atom()) -> ejabberd_commands() | command_not_found
+%% @doc Get the definition record of a command.
+get_command_definition(Name) ->
+ case ets:lookup(ejabberd_commands, Name) of
+ [E] -> E;
+ [] -> command_not_found
+ end.
+
+%% @spec (Name::atom(), Arguments) -> ResultTerm | {error, command_unknown}
+%% @doc Execute a command.
+execute_command(Name, Arguments) ->
+ case ets:lookup(ejabberd_commands, Name) of
+ [Command] ->
+ execute_command2(Command, Arguments);
+ [] ->
+ {error, command_unknown}
+ end.
+
+execute_command2(Command, Arguments) ->
+ Module = Command#ejabberd_commands.module,
+ Function = Command#ejabberd_commands.function,
+ apply(Module, Function, Arguments).
+
+%% @spec () -> [{Tag::string(), [CommandName::string()]}]
+%% @doc Get all the tags and associated commands.
+get_tags_commands() ->
+ CommandTags = ets:match(ejabberd_commands,
+ #ejabberd_commands{
+ name = '$1',
+ tags = '$2',
+ _ = '_'}),
+ Dict = lists:foldl(
+ fun([CommandNameAtom, CTags], D) ->
+ CommandName = atom_to_list(CommandNameAtom),
+ case CTags of
+ [] ->
+ orddict:append("untagged", CommandName, D);
+ _ ->
+ lists:foldl(
+ fun(TagAtom, DD) ->
+ Tag = atom_to_list(TagAtom),
+ orddict:append(Tag, CommandName, DD)
+ end,
+ D,
+ CTags)
+ end
+ end,
+ orddict:new(),
+ CommandTags),
+ orddict:to_list(Dict).
diff --git a/src/ejabberd_commands.hrl b/src/ejabberd_commands.hrl
new file mode 100644
index 000000000..ce7aadb9b
--- /dev/null
+++ b/src/ejabberd_commands.hrl
@@ -0,0 +1,52 @@
+%%%----------------------------------------------------------------------
+%%%
+%%% ejabberd, Copyright (C) 2002-2008 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+-record(ejabberd_commands, {name, tags = [],
+ desc = "", longdesc = "",
+ module, function,
+ args = [], result = rescode}).
+
+%% @type ejabberd_commands() = #ejabberd_commands{
+%% name = atom(),
+%% tags = [atom()],
+%% desc = string(),
+%% longdesc = string(),
+%% module = atom(),
+%% function = atom(),
+%% args = [aterm()],
+%% result = rterm()
+%% }.
+%% desc: Description of the command
+%% args: Describe the accepted arguments.
+%% This way the function that calls the command can format the
+%% arguments before calling.
+
+%% @type atype() = integer | string | {tuple, [aterm()]} | {list, aterm()}.
+%% Allowed types for arguments are integer, string, tuple and list.
+
+%% @type rtype() = integer | string | atom | {tuple, [rterm()]} | {list, rterm()} | rescode | restuple.
+%% A rtype is either an atom or a tuple with two elements.
+
+%% @type aterm() = {Name::atom(), Type::atype()}.
+%% An argument term is a tuple with the term name and the term type.
+
+%% @type rterm() = {Name::atom(), Type::rtype()}.
+%% A result term is a tuple with the term name and the term type.
diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl
index adc54c44d..362d12c91 100644
--- a/src/ejabberd_ctl.erl
+++ b/src/ejabberd_ctl.erl
@@ -1,7 +1,7 @@
%%%----------------------------------------------------------------------
%%% File : ejabberd_ctl.erl
%%% Author : Alexey Shchepin
-%%% Purpose : Ejabberd admin tool
+%%% Purpose : ejabberd command line admin tool
%%% Created : 11 Jan 2004 by Alexey Shchepin
%%%
%%%
@@ -24,43 +24,68 @@
%%%
%%%----------------------------------------------------------------------
+%%% @headerfile "ejabberd_ctl.hrl"
+
+%%% @doc Management of ejabberdctl commands and frontend to ejabberd commands.
+%%%
+%%% An ejabberdctl command is an abstract function identified by a
+%%% name, with a defined number of calling arguments, that can be
+%%% defined in any Erlang module and executed using ejabberdctl
+%%% administration script.
+%%%
+%%% Note: strings cannot have blankspaces
+%%%
+%%% Does not support commands that have arguments with ctypes: list, tuple
+%%%
+%%% TODO: Update the guide
+%%% TODO: Mention this in the release notes
+%%% Note: the commands with several words use now the underline: _
+%%% It is still possible to call the commands with dash: -
+%%% but this is deprecated, and may be removed in a future version.
+
+
-module(ejabberd_ctl).
-author('alexey@process-one.net').
-export([start/0,
init/0,
process/1,
- dump_to_textfile/1,
+ process2/1,
register_commands/3,
- register_commands/4,
- unregister_commands/3,
- unregister_commands/4]).
+ unregister_commands/3]).
-include("ejabberd_ctl.hrl").
+-include("ejabberd_commands.hrl").
-include("ejabberd.hrl").
+
+%%-----------------------------
+%% Module
+%%-----------------------------
+
start() ->
case init:get_plain_arguments() of
[SNode | Args] ->
SNode1 = case string:tokens(SNode, "@") of
- [_Node, _Server] ->
- SNode;
- _ ->
- case net_kernel:longnames() of
- true ->
- SNode ++ "@" ++ inet_db:gethostname() ++
- "." ++ inet_db:res_option(domain);
- false ->
- SNode ++ "@" ++ inet_db:gethostname();
+ [_Node, _Server] ->
+ SNode;
_ ->
- SNode
- end
- end,
+ case net_kernel:longnames() of
+ true ->
+ SNode ++ "@" ++ inet_db:gethostname() ++
+ "." ++ inet_db:res_option(domain);
+ false ->
+ SNode ++ "@" ++ inet_db:gethostname();
+ _ ->
+ SNode
+ end
+ end,
Node = list_to_atom(SNode1),
Status = case rpc:call(Node, ?MODULE, process, [Args]) of
{badrpc, Reason} ->
- ?PRINT("RPC failed on the node ~p: ~p~n",
- [Node, Reason]),
+ ?PRINT("Failed RPC connection to the node ~p: ~p~n",
+ [Node, Reason]),
+ %% TODO: show minimal start help
?STATUS_BADRPC;
S ->
S
@@ -76,16 +101,41 @@ init() ->
ets:new(ejabberd_ctl_host_cmds, [named_table, set, public]).
+%%-----------------------------
+%% ejabberdctl Command managment
+%%-----------------------------
+
+register_commands(CmdDescs, Module, Function) ->
+ ets:insert(ejabberd_ctl_cmds, CmdDescs),
+ ejabberd_hooks:add(ejabberd_ctl_process,
+ Module, Function, 50),
+ ok.
+
+unregister_commands(CmdDescs, Module, Function) ->
+ lists:foreach(fun(CmdDesc) ->
+ ets:delete_object(ejabberd_ctl_cmds, CmdDesc)
+ end, CmdDescs),
+ ejabberd_hooks:delete(ejabberd_ctl_process,
+ Module, Function, 50),
+ ok.
+
+
+%%-----------------------------
+%% Process
+%%-----------------------------
+
+%% The commands status, stop and restart are defined here to ensure
+%% they are usable even if ejabberd is completely stopped.
process(["status"]) ->
{InternalStatus, ProvidedStatus} = init:get_status(),
- ?PRINT("Node ~p is ~p. Status: ~p~n",
- [node(), InternalStatus, ProvidedStatus]),
+ ?PRINT("The node ~p is ~p with status: ~p~n",
+ [node(), InternalStatus, ProvidedStatus]),
case lists:keysearch(ejabberd, 1, application:which_applications()) of
false ->
- ?PRINT("ejabberd is not running~n", []),
+ ?PRINT("ejabberd is not running in that node~n", []),
?STATUS_ERROR;
- {value,_Version} ->
- ?PRINT("ejabberd is running~n", []),
+ {value, {_, _, Version}} ->
+ ?PRINT("ejabberd ~s is running in that node~n", [Version]),
?STATUS_SUCCESS
end;
@@ -97,121 +147,6 @@ process(["restart"]) ->
init:restart(),
?STATUS_SUCCESS;
-process(["reopen-log"]) ->
- ejabberd_hooks:run(reopen_log_hook, []),
- lists:foreach(fun(Host) ->
- ejabberd_hooks:run(reopen_log_hook, Host, [Host])
- end, ?MYHOSTS),
- %% TODO: Use the Reopen log API for logger_h ?
- ejabberd_logger_h:reopen_log(),
- ?STATUS_SUCCESS;
-
-process(["register", User, Server, Password]) ->
- case ejabberd_auth:try_register(User, Server, Password) of
- {atomic, ok} ->
- ?STATUS_SUCCESS;
- {atomic, exists} ->
- ?PRINT("User ~p already registered at node ~p~n",
- [User ++ "@" ++ Server, node()]),
- ?STATUS_ERROR;
- {error, Reason} ->
- ?PRINT("Can't register user ~p at node ~p: ~p~n",
- [User ++ "@" ++ Server, node(), Reason]),
- ?STATUS_ERROR
- end;
-
-process(["unregister", User, Server]) ->
- case ejabberd_auth:remove_user(User, Server) of
- {error, Reason} ->
- ?PRINT("Can't unregister user ~p at node ~p: ~p~n",
- [User ++ "@" ++ Server, node(), Reason]),
- ?STATUS_ERROR;
- _ ->
- ?STATUS_SUCCESS
- end;
-
-process(["backup", Path]) ->
- case mnesia:backup(Path) of
- ok ->
- ?STATUS_SUCCESS;
- {error, Reason} ->
- ?PRINT("Can't store backup in ~p at node ~p: ~p~n",
- [filename:absname(Path), node(), Reason]),
- ?STATUS_ERROR
- end;
-
-process(["dump", Path]) ->
- case dump_to_textfile(Path) of
- ok ->
- ?STATUS_SUCCESS;
- {error, Reason} ->
- ?PRINT("Can't store dump in ~p at node ~p: ~p~n",
- [filename:absname(Path), node(), Reason]),
- ?STATUS_ERROR
- end;
-
-process(["load", Path]) ->
- case mnesia:load_textfile(Path) of
- {atomic, ok} ->
- ?STATUS_SUCCESS;
- {error, Reason} ->
- ?PRINT("Can't load dump in ~p at node ~p: ~p~n",
- [filename:absname(Path), node(), Reason]),
- ?STATUS_ERROR
- end;
-
-process(["restore", Path]) ->
- case ejabberd_admin:restore(Path) of
- {atomic, _} ->
- ?STATUS_SUCCESS;
- {error, Reason} ->
- ?PRINT("Can't restore backup from ~p at node ~p: ~p~n",
- [filename:absname(Path), node(), Reason]),
- ?STATUS_ERROR;
- {aborted,{no_exists,Table}} ->
- ?PRINT("Can't restore backup from ~p at node ~p: Table ~p does not exist.~n",
- [filename:absname(Path), node(), Table]),
- ?STATUS_ERROR;
- {aborted,enoent} ->
- ?PRINT("Can't restore backup from ~p at node ~p: File not found.~n",
- [filename:absname(Path), node()]),
- ?STATUS_ERROR
- end;
-
-process(["install-fallback", Path]) ->
- case mnesia:install_fallback(Path) of
- ok ->
- ?STATUS_SUCCESS;
- {error, Reason} ->
- ?PRINT("Can't install fallback from ~p at node ~p: ~p~n",
- [filename:absname(Path), node(), Reason]),
- ?STATUS_ERROR
- end;
-
-process(["import-file", Path]) ->
- case jd2ejd:import_file(Path) of
- ok ->
- ?STATUS_SUCCESS;
- {error, Reason} ->
- ?PRINT("Can't import jabberd 1.4 spool file ~p at node ~p: ~p~n",
- [filename:absname(Path), node(), Reason]),
- ?STATUS_ERROR
- end;
-
-process(["import-dir", Path]) ->
- case jd2ejd:import_dir(Path) of
- ok ->
- ?STATUS_SUCCESS;
- {error, Reason} ->
- ?PRINT("Can't import jabberd 1.4 spool dir ~p at node ~p: ~p~n",
- [filename:absname(Path), node(), Reason]),
- ?STATUS_ERROR
- end;
-
-process(["delete-expired-messages"]) ->
- mod_offline:remove_expired_messages(),
- ?STATUS_SUCCESS;
-
process(["mnesia"]) ->
?PRINT("~p~n", [mnesia:system_info(all)]),
?STATUS_SUCCESS;
@@ -227,183 +162,589 @@ process(["mnesia", Arg]) when is_list(Arg) ->
end,
?STATUS_SUCCESS;
-process(["delete-old-messages", Days]) ->
- case catch list_to_integer(Days) of
- {'EXIT',{Reason, _Stack}} ->
- ?PRINT("Can't delete old messages (~p). Please pass an integer as parameter.~n",
- [Reason]),
- ?STATUS_ERROR;
- Integer when Integer >= 0 ->
- {atomic, _} = mod_offline:remove_old_messages(Integer),
- ?PRINT("Removed messages older than ~s days~n", [Days]),
+%% The arguments --long and --dual are not documented because they are
+%% automatically selected depending in the number of columns of the shell
+process(["help" | Mode]) ->
+ {MaxC, ShCode} = get_shell_info(),
+ case Mode of
+ [] ->
+ print_usage(dual, MaxC, ShCode),
+ ?STATUS_USAGE;
+ ["--dual"] ->
+ print_usage(dual, MaxC, ShCode),
+ ?STATUS_USAGE;
+ ["--long"] ->
+ print_usage(long, MaxC, ShCode),
+ ?STATUS_USAGE;
+ ["--tags"] ->
+ print_usage_tags(MaxC, ShCode),
?STATUS_SUCCESS;
- _Integer ->
- ?PRINT("Can't delete old messages. Please pass a positive integer as parameter.~n", []),
- ?STATUS_ERROR
- end;
-
-process(["vhost", H | Args]) ->
- try
- Host = exmpp_stringprep:nameprep(H),
- case ejabberd_hooks:run_fold(
- ejabberd_ctl_process, Host, false, [Host, Args]) of
- false ->
- print_vhost_usage(Host),
- ?STATUS_USAGE;
- Status ->
- Status
- end
- catch
- _ ->
- ?PRINT("Bad hostname: ~p~n", [H]),
- ?STATUS_ERROR
+ ["--tags", Tag] ->
+ print_usage_tags(Tag, MaxC, ShCode),
+ ?STATUS_SUCCESS;
+ ["help"] ->
+ print_usage_help(MaxC, ShCode),
+ ?STATUS_SUCCESS;
+ [CommandString | _] ->
+ print_usage_commands(CommandString, MaxC, ShCode),
+ ?STATUS_SUCCESS
end;
process(Args) ->
- case ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of
- false ->
- print_usage(),
- ?STATUS_USAGE;
- Status ->
- Status
+ {String, Code} = process2(Args),
+ io:format(String),
+ io:format("\n"),
+ Code.
+
+%% @spec (Args::[string()]) -> {String::string(), Code::integer()}
+process2(Args) ->
+ case try_run_ctp(Args) of
+ {String, wrong_command_arguments}
+ when is_list(String) ->
+ io:format(lists:flatten(["\n" | String]++["\n"])),
+ [CommandString | _] = Args,
+ process(["help" | [CommandString]]),
+ {lists:flatten(String), ?STATUS_ERROR};
+ {String, Code}
+ when is_list(String) and is_integer(Code) ->
+ {lists:flatten(String), Code};
+ String
+ when is_list(String) ->
+ {lists:flatten(String), ?STATUS_SUCCESS};
+ Code
+ when is_integer(Code) ->
+ {"", Code};
+ Other ->
+ {"Erroneous result: " ++ io_lib:format("~p", [Other]), ?STATUS_ERROR}
end.
+%%-----------------------------
+%% Command calling
+%%-----------------------------
+
+%% @spec (Args::[string()]) ->
+%% String::string() | Code::integer() | {String::string(), Code::integer()}
+try_run_ctp(Args) ->
+ try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of
+ false when Args /= [] ->
+ try_call_command(Args);
+ false ->
+ print_usage(),
+ {"", ?STATUS_USAGE};
+ Status ->
+ {"", Status}
+ catch
+ exit:Why ->
+ print_usage(),
+ {io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE}
+ end.
+
+%% @spec (Args::[string()]) ->
+%% String::string() | Code::integer() | {String::string(), Code::integer()}
+try_call_command(Args) ->
+ try call_command(Args) of
+ {error, command_unknown} ->
+ {io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR};
+ {error, wrong_number_parameters} ->
+ {"Error: wrong number of parameters", ?STATUS_ERROR};
+ Res ->
+ Res
+ catch
+ A:Why ->
+ Stack = erlang:get_stacktrace(),
+ {io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR}
+ end.
+
+%% @spec (Args::[string()]) ->
+%% String::string() | Code::integer() | {String::string(), Code::integer()} | {error, ErrorType}
+call_command([CmdString | Args]) ->
+ {ok, CmdStringU, _} = regexp:gsub(CmdString, "-", "_"),
+ Command = list_to_atom(CmdStringU),
+ case ejabberd_commands:get_command_format(Command) of
+ {error, command_unknown} ->
+ {error, command_unknown};
+ {ArgsFormat, ResultFormat} ->
+ case (catch format_args(Args, ArgsFormat)) of
+ ArgsFormatted when is_list(ArgsFormatted) ->
+ Result = ejabberd_commands:execute_command(Command,
+ ArgsFormatted),
+ format_result(Result, ResultFormat);
+ {'EXIT', {function_clause,[{lists,zip,[A1, A2]} | _]}} ->
+ {NumCompa, TextCompa} =
+ case {length(A1), length(A2)} of
+ {L1, L2} when L1 < L2 -> {L2-L1, "less argument"};
+ {L1, L2} when L1 > L2 -> {L1-L2, "more argument"}
+ end,
+ {io_lib:format("Error: the command ~p requires ~p ~s.",
+ [CmdString, NumCompa, TextCompa]),
+ wrong_command_arguments}
+ end
+ end.
+
+
+%%-----------------------------
+%% Format arguments
+%%-----------------------------
+
+format_args(Args, ArgsFormat) ->
+ lists:foldl(
+ fun({{_ArgName, ArgFormat}, Arg}, Res) ->
+ Formatted = format_arg(Arg, ArgFormat),
+ Res ++ [Formatted]
+ end,
+ [],
+ lists:zip(ArgsFormat, Args)).
+
+format_arg(Arg, Format) ->
+ Parse = case Format of
+ integer ->
+ "~d";
+ string ->
+ NumChars = integer_to_list(string:len(Arg)),
+ "~" ++ NumChars ++ "c"
+ end,
+ {ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg),
+ Arg2.
+
+
+%%-----------------------------
+%% Format result
+%%-----------------------------
+
+format_result(Atom, {_Name, atom}) ->
+ io_lib:format("~p", [Atom]);
+
+format_result(Int, {_Name, integer}) ->
+ io_lib:format("~p", [Int]);
+
+format_result(String, {_Name, string}) ->
+ io_lib:format("~s", [String]);
+
+format_result(Code, {_Name, rescode}) ->
+ make_status(Code);
+
+format_result({Code, Text}, {_Name, restuple}) ->
+ {io_lib:format("~s", [Text]), make_status(Code)};
+
+%% The result is a list of something: [something()]
+format_result([], {_Name, {list, _ElementsDef}}) ->
+ "";
+format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) ->
+ %% Start formatting the first element
+ [format_result(FirstElement, ElementsDef) |
+ %% If there are more elements, put always first a newline character
+ lists:map(
+ fun(Element) ->
+ ["\n" | format_result(Element, ElementsDef)]
+ end,
+ Elements)];
+
+%% The result is a tuple with several elements: {something1(), something2(),...}
+%% NOTE: the elements in the tuple are separated with tabular characters,
+%% if a string is empty, it will be difficult to notice in the shell,
+%% maybe a different separation character should be used, like ;;?
+format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}) ->
+ ElementsList = tuple_to_list(ElementsTuple),
+ [{FirstE, FirstD} | ElementsAndDef] = lists:zip(ElementsList, ElementsDef),
+ [format_result(FirstE, FirstD) |
+ lists:map(
+ fun({Element, ElementDef}) ->
+ ["\t" | format_result(Element, ElementDef)]
+ end,
+ ElementsAndDef)].
+
+make_status(ok) -> ?STATUS_SUCCESS;
+make_status(true) -> ?STATUS_SUCCESS;
+make_status(_Error) -> ?STATUS_ERROR.
+
+get_list_commands() ->
+ try ejabberd_commands:list_commands() of
+ Commands ->
+ [tuple_command_help(Command)
+ || {N,_,_}=Command <- Commands,
+ %% Don't show again those commands, because they are already
+ %% announced by ejabberd_ctl itself
+ N /= status, N /= stop, N /= restart]
+ catch
+ exit:_ ->
+ []
+ end.
+
+%% Return: {string(), [string()], string()}
+tuple_command_help({Name, Args, Desc}) ->
+ Arguments = [atom_to_list(ArgN) || {ArgN, _ArgF} <- Args],
+ Prepend = case is_supported_args(Args) of
+ true -> "";
+ false -> "*"
+ end,
+ CallString = atom_to_list(Name),
+ {CallString, Arguments, Prepend ++ Desc}.
+
+is_supported_args(Args) ->
+ lists:all(
+ fun({_Name, Format}) ->
+ (Format == integer)
+ or (Format == string)
+ end,
+ Args).
+
+get_list_ctls() ->
+ case catch ets:tab2list(ejabberd_ctl_cmds) of
+ {'EXIT', _} -> [];
+ Cs -> [{NameArgs, [], Desc} || {NameArgs, Desc} <- Cs]
+ end.
+
+
+%%-----------------------------
+%% Print help
+%%-----------------------------
+
+%% Bold
+-define(B1, "\e[1m").
+-define(B2, "\e[22m").
+-define(B(S), case ShCode of true -> [?B1, S, ?B2]; false -> S end).
+
+%% Underline
+-define(U1, "\e[4m").
+-define(U2, "\e[24m").
+-define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end).
+
print_usage() ->
- CmdDescs =
- [{"status", "get ejabberd status"},
- {"stop", "stop ejabberd"},
- {"restart", "restart ejabberd"},
- {"reopen-log", "reopen log file"},
- {"register user server password", "register a user"},
- {"unregister user server", "unregister a user"},
- {"backup file", "store a database backup to file"},
- {"restore file", "restore a database backup from file"},
- {"install-fallback file", "install a database fallback from file"},
- {"dump file", "dump a database to a text file"},
- {"load file", "restore a database from a text file"},
- {"import-file file", "import user data from jabberd 1.4 spool file"},
- {"import-dir dir", "import user data from jabberd 1.4 spool directory"},
- {"delete-expired-messages", "delete expired offline messages from database"},
- {"delete-old-messages n", "delete offline messages older than n days from database"},
- {"mnesia [info]", "show information of Mnesia system"},
- {"vhost host ...", "execute host-specific commands"}] ++
- ets:tab2list(ejabberd_ctl_cmds),
- MaxCmdLen =
- lists:max(lists:map(
- fun({Cmd, _Desc}) ->
- length(Cmd)
- end, CmdDescs)),
- NewLine = io_lib:format("~n", []),
- FmtCmdDescs =
- lists:map(
- fun({Cmd, Desc}) ->
- [" ", Cmd, string:chars($\s, MaxCmdLen - length(Cmd) + 2),
- Desc, NewLine]
- end, CmdDescs),
+ {MaxC, ShCode} = get_shell_info(),
+ print_usage(dual, MaxC, ShCode).
+print_usage(HelpMode, MaxC, ShCode) ->
+ AllCommands =
+ [
+ {"status", [], "Get ejabberd status"},
+ {"stop", [], "Stop ejabberd"},
+ {"restart", [], "Restart ejabberd"},
+ {"help", ["[--tags [tag] | com?*]"], "Show help (try: ejabberdctl help help)"},
+ {"mnesia", ["[info]"], "show information of Mnesia system"}] ++
+ get_list_commands() ++
+ get_list_ctls(),
+
?PRINT(
- "Usage: ejabberdctl [--node nodename] command [options]~n"
- "~n"
- "Available commands in this ejabberd node:~n"
- ++ FmtCmdDescs ++
- "~n"
- "Examples:~n"
- " ejabberdctl restart~n"
- " ejabberdctl --node ejabberd@host restart~n"
- " ejabberdctl vhost jabber.example.org ...~n",
- []).
-
-print_vhost_usage(Host) ->
- CmdDescs =
- ets:select(ejabberd_ctl_host_cmds,
- [{{{Host, '$1'}, '$2'}, [], [{{'$1', '$2'}}]}]),
- MaxCmdLen =
- if
- CmdDescs == [] ->
- 0;
- true ->
- lists:max(lists:map(
- fun({Cmd, _Desc}) ->
- length(Cmd)
- end, CmdDescs))
- end,
- NewLine = io_lib:format("~n", []),
- FmtCmdDescs =
- lists:map(
- fun({Cmd, Desc}) ->
- [" ", Cmd, string:chars($\s, MaxCmdLen - length(Cmd) + 2),
- Desc, NewLine]
- end, CmdDescs),
+ ["Usage: ", ?B("ejabberdctl"), " [--node ", ?U("nodename"), "] ", ?U("command"), " [options]\n"
+ "\n"
+ "Available commands in this ejabberd node:\n"], []),
+ print_usage_commands(HelpMode, MaxC, ShCode, AllCommands),
?PRINT(
- "Usage: ejabberdctl [--node nodename] vhost hostname command [options]~n"
- "~n"
- "Available commands in this ejabberd node and this vhost:~n"
- ++ FmtCmdDescs ++
- "~n"
- "Examples:~n"
- " ejabberdctl vhost "++Host++" registered-users~n",
- []).
+ ["\n"
+ "Examples:\n"
+ " ejabberdctl restart\n"
+ " ejabberdctl --node ejabberd@host restart\n"],
+ []).
-register_commands(CmdDescs, Module, Function) ->
- ets:insert(ejabberd_ctl_cmds, CmdDescs),
- ejabberd_hooks:add(ejabberd_ctl_process,
- Module, Function, 50),
- ok.
+print_usage_commands(HelpMode, MaxC, ShCode, Commands) ->
+ CmdDescsSorted = lists:keysort(1, Commands),
-register_commands(Host, CmdDescs, Module, Function) ->
- ets:insert(ejabberd_ctl_host_cmds,
- [{{Host, Cmd}, Desc} || {Cmd, Desc} <- CmdDescs]),
- ejabberd_hooks:add(ejabberd_ctl_process, Host,
- Module, Function, 50),
- ok.
+ %% What is the length of the largest command?
+ {CmdArgsLenDescsSorted, Lens} =
+ lists:mapfoldl(
+ fun({Cmd, Args, Desc}, Lengths) ->
+ Len =
+ length(Cmd) +
+ lists:foldl(fun(Arg, R) ->
+ R + 1 + length(Arg)
+ end,
+ 0,
+ Args),
+ {{Cmd, Args, Len, Desc}, [Len | Lengths]}
+ end,
+ [],
+ CmdDescsSorted),
+ MaxCmdLen = case Lens of
+ [] -> 80;
+ _ -> lists:max(Lens)
+ end,
-unregister_commands(CmdDescs, Module, Function) ->
- lists:foreach(fun(CmdDesc) ->
- ets:delete_object(ejabberd_ctl_cmds, CmdDesc)
- end, CmdDescs),
- ejabberd_hooks:delete(ejabberd_ctl_process,
- Module, Function, 50),
- ok.
+ %% For each command in the list of commands
+ %% Convert its definition to a line
+ FmtCmdDescs = format_command_lines(CmdArgsLenDescsSorted, MaxCmdLen, MaxC, ShCode, HelpMode),
-unregister_commands(Host, CmdDescs, Module, Function) ->
- lists:foreach(fun({Cmd, Desc}) ->
- ets:delete_object(ejabberd_ctl_host_cmds,
- {{Host, Cmd}, Desc})
- end, CmdDescs),
- ejabberd_hooks:delete(ejabberd_ctl_process,
- Module, Function, 50),
- ok.
-
-dump_to_textfile(File) ->
- dump_to_textfile(mnesia:system_info(is_running), file:open(File, write)).
-dump_to_textfile(yes, {ok, F}) ->
- Tabs1 = lists:delete(schema, mnesia:system_info(local_tables)),
- Tabs = lists:filter(
- fun(T) ->
- case mnesia:table_info(T, storage_type) of
- disc_copies -> true;
- disc_only_copies -> true;
- _ -> false
- end
- end, Tabs1),
- Defs = lists:map(
- fun(T) -> {T, [{record_name, mnesia:table_info(T, record_name)},
- {attributes, mnesia:table_info(T, attributes)}]}
- end,
- Tabs),
- io:format(F, "~p.~n", [{tables, Defs}]),
- lists:foreach(fun(T) -> dump_tab(F, T) end, Tabs),
- file:close(F);
-dump_to_textfile(_, {ok, F}) ->
- file:close(F),
- {error, mnesia_not_running};
-dump_to_textfile(_, {error, Reason}) ->
- {error, Reason}.
+ ?PRINT([FmtCmdDescs], []).
-dump_tab(F, T) ->
- W = mnesia:table_info(T, wild_pattern),
- {atomic,All} = mnesia:transaction(
- fun() -> mnesia:match_object(T, W, read) end),
+%% Get some info about the shell:
+%% how many columns of width
+%% and guess if it supports text formatting codes.
+get_shell_info() ->
+ %% This function was introduced in OTP R12B-0
+ try io:columns() of
+ {ok, C} -> {C-2, true};
+ {error, enotsup} -> {78, false}
+ catch
+ _:_ -> {78, false}
+ end.
+
+%% Split this command description in several lines of proper length
+prepare_description(DescInit, MaxC, Desc) ->
+ Words = string:tokens(Desc, " "),
+ prepare_long_line(DescInit, MaxC, Words).
+
+prepare_long_line(DescInit, MaxC, Words) ->
+ MaxSegmentLen = MaxC - DescInit,
+ MarginString = lists:duplicate(DescInit, $\s), % Put spaces
+ [FirstSegment | MoreSegments] = split_desc_segments(MaxSegmentLen, Words),
+ MoreSegmentsMixed = mix_desc_segments(MarginString, MoreSegments),
+ [FirstSegment | MoreSegmentsMixed].
+
+mix_desc_segments(MarginString, Segments) ->
+ [["\n", MarginString, Segment] || Segment <- Segments].
+
+split_desc_segments(MaxL, Words) ->
+ join(MaxL, Words).
+
+%% Join words in a segment,
+%% but stop adding to a segment if adding this word would pass L
+join(L, Words) ->
+ join(L, Words, 0, [], []).
+
+join(_L, [], _LenLastSeg, LastSeg, ResSeg) ->
+ ResSeg2 = [lists:reverse(LastSeg) | ResSeg],
+ lists:reverse(ResSeg2);
+join(L, [Word | Words], LenLastSeg, LastSeg, ResSeg) ->
+ LWord = length(Word),
+ case LWord + LenLastSeg < L of
+ true ->
+ %% This word fits in the last segment
+ %% If this word ends with "\n", reset column counter
+ case string:str(Word, "\n") of
+ 0 ->
+ join(L, Words, LenLastSeg+LWord+1, [" ", Word | LastSeg], ResSeg);
+ _ ->
+ join(L, Words, LWord+1, [" ", Word | LastSeg], ResSeg)
+ end;
+ false ->
+ join(L, Words, LWord, [" ", Word], [lists:reverse(LastSeg) | ResSeg])
+ end.
+
+format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual)
+ when MaxC - MaxCmdLen < 40 ->
+ %% If the space available for descriptions is too narrow, enforce long help mode
+ format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, long);
+
+format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual) ->
+ lists:map(
+ fun({Cmd, Args, CmdArgsL, Desc}) ->
+ DescFmt = prepare_description(MaxCmdLen+4, MaxC, Desc),
+ [" ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], string:chars($\s, MaxCmdLen - CmdArgsL + 1),
+ DescFmt, "\n"]
+ end, CALD);
+
+format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) ->
+ lists:map(
+ fun({Cmd, Args, _CmdArgsL, Desc}) ->
+ DescFmt = prepare_description(8, MaxC, Desc),
+ ["\n ", ?B(Cmd), " ", [[?U(Arg), " "] || Arg <- Args], "\n", " ",
+ DescFmt, "\n"]
+ end, CALD).
+
+
+%%-----------------------------
+%% Print Tags
+%%-----------------------------
+
+print_usage_tags(MaxC, ShCode) ->
+ ?PRINT("Available tags and commands:", []),
+ TagsCommands = ejabberd_commands:get_tags_commands(),
lists:foreach(
- fun(Term) -> io:format(F,"~p.~n", [setelement(1, Term, T)]) end, All).
+ fun({Tag, Commands} = _TagCommands) ->
+ ?PRINT(["\n\n ", ?B(Tag), "\n "], []),
+ Words = lists:sort(Commands),
+ Desc = prepare_long_line(5, MaxC, Words),
+ ?PRINT(Desc, [])
+ end,
+ TagsCommands),
+ ?PRINT("\n\n", []).
+
+print_usage_tags(Tag, MaxC, ShCode) ->
+ ?PRINT(["Available commands with tag ", ?B(Tag), ":", "\n"], []),
+ HelpMode = long,
+ TagsCommands = ejabberd_commands:get_tags_commands(),
+ CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of
+ {value, {Tag, CNs}} -> CNs;
+ false -> []
+ end,
+ CommandsList = lists:map(
+ fun(NameString) ->
+ C = ejabberd_commands:get_command_definition(list_to_atom(NameString)),
+ #ejabberd_commands{name = Name,
+ args = Args,
+ desc = Desc} = C,
+ tuple_command_help({Name, Args, Desc})
+ end,
+ CommandsNames),
+ print_usage_commands(HelpMode, MaxC, ShCode, CommandsList),
+ ?PRINT("\n", []).
+
+
+%%-----------------------------
+%% Print usage of 'help' command
+%%-----------------------------
+
+print_usage_help(MaxC, ShCode) ->
+ LongDesc =
+ ["The special 'help' ejabberdctl command provides help of ejabberd commands.\n\n"
+ "The format is:\n ", ?B("ejabberdctl"), " ", ?B("help"), " [", ?B("--tags"), " ", ?U("[tag]"), " | ", ?U("com?*"), "]\n\n"
+ "The optional arguments:\n"
+ " ",?B("--tags")," Show all tags and the names of commands in each tag\n"
+ " ",?B("--tags"), " ", ?U("tag")," Show description of commands in this tag\n"
+ " ",?U("command")," Show detailed description of the command\n"
+ " ",?U("com?*")," Show detailed description of commands that match this glob.\n"
+ " You can use ? to match a simple character,\n"
+ " and * to match several characters.\n"
+ "\n",
+ "Some example usages:\n",
+ " ejabberdctl help\n",
+ " ejabberdctl help --tags\n",
+ " ejabberdctl help --tags accounts\n",
+ " ejabberdctl help register\n",
+ " ejabberdctl help regist*\n",
+ "\n",
+ "Please note that 'ejabberdctl help' shows all ejabberd commands,\n",
+ "even those that cannot be used in the shell with ejabberdctl.\n",
+ "Those commands can be identified because the description starts with: *"],
+ ArgsDef = [],
+ C = #ejabberd_commands{
+ desc = "Show help of ejabberd commands",
+ longdesc = LongDesc,
+ args = ArgsDef,
+ result = {help, string}},
+ print_usage_command("help", C, MaxC, ShCode).
+
+
+%%-----------------------------
+%% Print usage command
+%%-----------------------------
+
+%% @spec (CmdSubString::string(), MaxC::integer(), ShCode::boolean()) -> ok
+print_usage_commands(CmdSubString, MaxC, ShCode) ->
+ %% Get which command names match this substring
+ AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands()],
+ Cmds = filter_commands(AllCommandsNames, CmdSubString),
+ case Cmds of
+ [] -> io:format("Error: not command found that match: ~p~n", [CmdSubString]);
+ _ -> print_usage_commands2(lists:sort(Cmds), MaxC, ShCode)
+ end.
+
+print_usage_commands2(Cmds, MaxC, ShCode) ->
+ %% Then for each one print it
+ lists:mapfoldl(
+ fun(Cmd, Remaining) ->
+ print_usage_command(Cmd, MaxC, ShCode),
+ case Remaining > 1 of
+ true -> ?PRINT([" ", lists:duplicate(MaxC, 126), " \n"], []);
+ false -> ok
+ end,
+ {ok, Remaining-1}
+ end,
+ length(Cmds),
+ Cmds).
+
+filter_commands(All, SubString) ->
+ case lists:member(SubString, All) of
+ true -> [SubString];
+ false -> filter_commands_regexp(All, SubString)
+ end.
+
+filter_commands_regexp(All, Glob) ->
+ RegExp = regexp:sh_to_awk(Glob),
+ lists:filter(
+ fun(Command) ->
+ case regexp:first_match(Command, RegExp) of
+ {match, _, _} ->
+ true;
+ _ ->
+ false
+ end
+ end,
+ All).
+
+%% @spec (Cmd::string(), MaxC::integer(), ShCode::boolean()) -> ok
+print_usage_command(Cmd, MaxC, ShCode) ->
+ Name = list_to_atom(Cmd),
+ case ejabberd_commands:get_command_definition(Name) of
+ command_not_found ->
+ io:format("Error: command ~p not known.~n", [Cmd]);
+ C ->
+ print_usage_command(Cmd, C, MaxC, ShCode)
+ end.
+
+print_usage_command(Cmd, C, MaxC, ShCode) ->
+ #ejabberd_commands{
+ tags = TagsAtoms,
+ desc = Desc,
+ longdesc = LongDesc,
+ args = ArgsDef,
+ result = ResultDef} = C,
+
+ NameFmt = [" ", ?B("Command Name"), ": ", Cmd, "\n"],
+
+ %% Initial indentation of result is 13 = length(" Arguments: ")
+ Args = [format_usage_ctype(ArgDef, 13) || ArgDef <- ArgsDef],
+ ArgsMargin = lists:duplicate(13, $\s),
+ ArgsListFmt = case Args of
+ [] -> "\n";
+ _ -> [ [Arg, "\n", ArgsMargin] || Arg <- Args]
+ end,
+ ArgsFmt = [" ", ?B("Arguments"), ": ", ArgsListFmt],
+
+ %% Initial indentation of result is 11 = length(" Returns: ")
+ ResultFmt = format_usage_ctype(ResultDef, 11),
+ ReturnsFmt = [" ",?B("Returns"),": ", ResultFmt],
+
+ XmlrpcFmt = "", %%+++ [" ",?B("XML-RPC"),": ", format_usage_xmlrpc(ArgsDef, ResultDef), "\n\n"],
+
+ TagsFmt = [" ",?B("Tags"),": ", prepare_long_line(8, MaxC, [atom_to_list(TagA) || TagA <- TagsAtoms])],
+
+ DescFmt = [" ",?B("Description"),": ", prepare_description(15, MaxC, Desc)],
+
+ LongDescFmt = case LongDesc of
+ "" -> "";
+ _ -> ["", prepare_description(0, MaxC, LongDesc), "\n\n"]
+ end,
+
+ NoteEjabberdctl = case is_supported_args(ArgsDef) of
+ true -> "";
+ false -> [" ", ?B("Note:"), " This command cannot be executed using ejabberdctl. Try ejabberd_xmlrpc.\n\n"]
+ end,
+
+ ?PRINT(["\n", NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt, "\n\n", XmlrpcFmt, TagsFmt, "\n\n", DescFmt, "\n\n", LongDescFmt, NoteEjabberdctl], []).
+
+format_usage_ctype({Name, Type}, _Indentation)
+ when (Type==atom) or (Type==integer) or (Type==string) or (Type==rescode) or (Type==restuple)->
+ io_lib:format("~p::~p", [Name, Type]);
+
+format_usage_ctype({Name, {list, ElementDef}}, Indentation) ->
+ NameFmt = atom_to_list(Name),
+ Indentation2 = Indentation + length(NameFmt) + 4,
+ ElementFmt = format_usage_ctype(ElementDef, Indentation2),
+ [NameFmt, "::[ ", ElementFmt, " ]"];
+
+format_usage_ctype({Name, {tuple, ElementsDef}}, Indentation) ->
+ NameFmt = atom_to_list(Name),
+ Indentation2 = Indentation + length(NameFmt) + 4,
+ ElementsFmt = format_usage_tuple(ElementsDef, Indentation2),
+ [NameFmt, "::{ " | ElementsFmt].
+
+format_usage_tuple([], _Indentation) ->
+ [];
+format_usage_tuple([ElementDef], Indentation) ->
+ [format_usage_ctype(ElementDef, Indentation) , " }"];
+format_usage_tuple([ElementDef | ElementsDef], Indentation) ->
+ ElementFmt = format_usage_ctype(ElementDef, Indentation),
+ MarginString = lists:duplicate(Indentation, $\s), % Put spaces
+ [ElementFmt, ",\n", MarginString, format_usage_tuple(ElementsDef, Indentation)].
+
+
+%%-----------------------------
+%% Command managment
+%%-----------------------------
+
+%%+++
+%% Struct(Integer res) create_account(Struct(String user, String server, String password))
+%%format_usage_xmlrpc(ArgsDef, ResultDef) ->
+%% ["aaaa bbb ccc"].
+
diff --git a/src/ejabberd_listener.erl b/src/ejabberd_listener.erl
index e8bddf00e..b3753cc84 100644
--- a/src/ejabberd_listener.erl
+++ b/src/ejabberd_listener.erl
@@ -29,10 +29,11 @@
-export([start_link/0, init/1, start/3,
init/3,
+ start_listeners/0,
start_listener/3,
- stop_listener/1,
+ stop_listener/2,
add_listener/3,
- delete_listener/1
+ delete_listener/2
]).
-include("ejabberd.hrl").
@@ -42,24 +43,27 @@ start_link() ->
init(_) ->
+ {ok, {{one_for_one, 10, 1}, []}}.
+
+start_listeners() ->
case ejabberd_config:get_local_option(listen) of
undefined ->
ignore;
Ls ->
- {ok, {{one_for_one, 10, 1},
- lists:map(
- fun({Port, Module, Opts}) ->
- {Port,
- {?MODULE, start, [Port, Module, Opts]},
- transient,
- brutal_kill,
- worker,
- [?MODULE]}
- end, Ls)}}
+ lists:map(
+ fun({Port, Module, Opts}) ->
+ start_listener(Port, Module, Opts)
+ end, Ls)
end.
-
start(Port, Module, Opts) ->
+ %% Check if the module is an ejabberd listener or an independent listener
+ case Module:socket_type() of
+ independent -> Module:start_listener(Port, Opts);
+ _ -> start_dependent(Port, Module, Opts)
+ end.
+
+start_dependent(Port, Module, Opts) ->
case includes_deprecated_ssl_option(Opts) of
false ->
{ok, proc_lib:spawn_link(?MODULE, init,
@@ -130,6 +134,21 @@ accept(ListenSocket, Module, Opts) ->
end.
start_listener(Port, Module, Opts) ->
+ start_module_sup(Port, Module),
+ start_listener_sup(Port, Module, Opts).
+
+start_module_sup(_Port, Module) ->
+ Proc1 = gen_mod:get_module_proc("sup", Module),
+ ChildSpec1 =
+ {Proc1,
+ {ejabberd_tmp_sup, start_link, [Proc1, Module]},
+ permanent,
+ infinity,
+ supervisor,
+ [ejabberd_tmp_sup]},
+ catch supervisor:start_child(ejabberd_sup, ChildSpec1).
+
+start_listener_sup(Port, Module, Opts) ->
ChildSpec = {Port,
{?MODULE, start, [Port, Module, Opts]},
transient,
@@ -138,9 +157,13 @@ start_listener(Port, Module, Opts) ->
[?MODULE]},
supervisor:start_child(ejabberd_listeners, ChildSpec).
-stop_listener(Port) ->
+stop_listener(Port, Module) ->
supervisor:terminate_child(ejabberd_listeners, Port),
- supervisor:delete_child(ejabberd_listeners, Port).
+ supervisor:delete_child(ejabberd_listeners, Port),
+
+ Proc1 = gen_mod:get_module_proc("sup", Module),
+ supervisor:terminate_child(ejabberd_sup, Proc1),
+ supervisor:delete_child(ejabberd_sup, Proc1).
add_listener(Port, Module, Opts) ->
Ports = case ejabberd_config:get_local_option(listen) of
@@ -154,7 +177,7 @@ add_listener(Port, Module, Opts) ->
ejabberd_config:add_local_option(listen, Ports2),
start_listener(Port, Module, Opts).
-delete_listener(Port) ->
+delete_listener(Port, Module) ->
Ports = case ejabberd_config:get_local_option(listen) of
undefined ->
[];
@@ -163,5 +186,5 @@ delete_listener(Port) ->
end,
Ports1 = lists:keydelete(Port, 1, Ports),
ejabberd_config:add_local_option(listen, Ports1),
- stop_listener(Port).
+ stop_listener(Port, Module).
diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl
index 53cc65e28..0806ffe5a 100644
--- a/src/ejabberd_s2s.erl
+++ b/src/ejabberd_s2s.erl
@@ -39,7 +39,8 @@
remove_connection/3,
dirty_get_connections/0,
allow_host/2,
- ctl_process/2
+ incoming_s2s_number/0,
+ outgoing_s2s_number/0
]).
%% gen_server callbacks
@@ -49,7 +50,7 @@
-include_lib("exmpp/include/exmpp.hrl").
-include("ejabberd.hrl").
--include("ejabberd_ctl.hrl").
+-include("ejabberd_commands.hrl").
-define(DEFAULT_MAX_S2S_CONNECTIONS_NUMBER, 1).
-define(DEFAULT_MAX_S2S_CONNECTIONS_NUMBER_PER_NODE, 1).
@@ -182,10 +183,7 @@ init([]) ->
{attributes, record_info(fields, s2s)}]),
mnesia:add_table_copy(s2s, node(), ram_copies),
mnesia:subscribe(system),
- ejabberd_ctl:register_commands(
- [{"incoming-s2s-number", "print number of incoming s2s connections on the node"},
- {"outgoing-s2s-number", "print number of outgoing s2s connections on the node"}],
- ?MODULE, ctl_process),
+ ejabberd_commands:register_commands(commands()),
{ok, #state{}}.
%%--------------------------------------------------------------------
@@ -249,6 +247,7 @@ handle_info(_Info, State) ->
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
+ ejabberd_commands:unregister_commands(commands()),
ok.
%%--------------------------------------------------------------------
@@ -453,16 +452,35 @@ is_subdomain(Domain1, Domain2) ->
send_element(Pid, El) ->
Pid ! {send_element, El}.
-ctl_process(_Val, ["incoming-s2s-number"]) ->
- N = length(supervisor:which_children(ejabberd_s2s_in_sup)),
- ?PRINT("~p~n", [N]),
- {stop, ?STATUS_SUCCESS};
-ctl_process(_Val, ["outgoing-s2s-number"]) ->
- N = length(supervisor:which_children(ejabberd_s2s_out_sup)),
- ?PRINT("~p~n", [N]),
- {stop, ?STATUS_SUCCESS};
-ctl_process(Val, _Args) ->
- Val.
+
+%%%----------------------------------------------------------------------
+%%% ejabberd commands
+
+commands() ->
+ [
+ #ejabberd_commands{name = incoming_s2s_number,
+ tags = [stats, s2s],
+ desc = "Number of incoming s2s connections on the node",
+ module = ?MODULE, function = incoming_s2s_number,
+ args = [],
+ result = {s2s_incoming, integer}},
+ #ejabberd_commands{name = outgoing_s2s_number,
+ tags = [stats, s2s],
+ desc = "Number of outgoing s2s connections on the node",
+ module = ?MODULE, function = outgoing_s2s_number,
+ args = [],
+ result = {s2s_outgoing, integer}}
+ ].
+
+incoming_s2s_number() ->
+ length(supervisor:which_children(ejabberd_s2s_in_sup)).
+
+outgoing_s2s_number() ->
+ length(supervisor:which_children(ejabberd_s2s_out_sup)).
+
+
+%%%----------------------------------------------------------------------
+%%% Update Mnesia tables
update_tables() ->
case catch mnesia:table_info(s2s, type) of
diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl
index 0ce7505bf..f4f1b0923 100644
--- a/src/ejabberd_sm.erl
+++ b/src/ejabberd_sm.erl
@@ -46,7 +46,9 @@
register_iq_handler/4,
register_iq_handler/5,
unregister_iq_handler/2,
- ctl_process/2,
+ connected_users/0,
+ connected_users_number/0,
+ user_resources/2,
get_session_pid/3,
get_user_info/3,
get_user_ip/3
@@ -59,7 +61,7 @@
-include_lib("exmpp/include/exmpp.hrl").
-include("ejabberd.hrl").
--include("ejabberd_ctl.hrl").
+-include("ejabberd_commands.hrl").
-record(session, {sid, usr, us, priority, info}).
-record(state, {}).
@@ -269,11 +271,7 @@ init([]) ->
ejabberd_hooks:add(remove_user, Host,
ejabberd_sm, disconnect_removed_user, 100)
end, ?MYHOSTS),
- ejabberd_ctl:register_commands(
- [{"connected-users", "list all established sessions"},
- {"connected-users-number", "print a number of established sessions"},
- {"user-resources user server", "print user's connected resources"}],
- ?MODULE, ctl_process),
+ ejabberd_commands:register_commands(commands()),
{ok, #state{}}.
@@ -353,6 +351,7 @@ handle_info(_Info, State) ->
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
+ ejabberd_commands:unregister_commands(commands()),
ok.
%%--------------------------------------------------------------------
@@ -670,27 +669,46 @@ process_iq(From, To, Packet) ->
end.
-ctl_process(_Val, ["connected-users"]) ->
- USRs = dirty_get_sessions_list(),
- NewLine = io_lib:format("~n", []),
- SUSRs = lists:sort(USRs),
- FUSRs = lists:map(fun({U, S, R}) -> [U, $@, S, $/, R, NewLine] end, SUSRs),
- ?PRINT("~s", [FUSRs]),
- {stop, ?STATUS_SUCCESS};
-ctl_process(_Val, ["connected-users-number"]) ->
- N = length(dirty_get_sessions_list()),
- ?PRINT("~p~n", [N]),
- {stop, ?STATUS_SUCCESS};
-ctl_process(_Val, ["user-resources", User, Server]) ->
- Resources = get_user_resources(User, Server),
- NewLine = io_lib:format("~n", []),
- SResources = lists:sort(Resources),
- FResources = lists:map(fun(R) -> [R, NewLine] end, SResources),
- ?PRINT("~s", [FResources]),
- {stop, ?STATUS_SUCCESS};
-ctl_process(Val, _Args) ->
- Val.
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% ejabberd commands
+commands() ->
+ [
+ #ejabberd_commands{name = connected_users,
+ tags = [session],
+ desc = "List all established sessions",
+ module = ?MODULE, function = connected_users,
+ args = [],
+ result = {connected_users, {list, {sessions, string}}}},
+ #ejabberd_commands{name = connected_users_number,
+ tags = [session, stats],
+ desc = "Get the number of established sessions",
+ module = ?MODULE, function = connected_users_number,
+ args = [],
+ result = {num_sessions, integer}},
+ #ejabberd_commands{name = user_resources,
+ tags = [session],
+ desc = "List user's connected resources",
+ module = ?MODULE, function = user_resources,
+ args = [{user, string}, {host, string}],
+ result = {resources, {list, {resource, string}}}}
+ ].
+
+connected_users() ->
+ USRs = dirty_get_sessions_list(),
+ SUSRs = lists:sort(USRs),
+ lists:map(fun({U, S, R}) -> [U, $@, S, $/, R] end, SUSRs).
+
+connected_users_number() ->
+ length(dirty_get_sessions_list()).
+
+user_resources(User, Server) ->
+ Resources = get_user_resources(User, Server),
+ lists:sort(Resources).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% Update Mnesia tables
update_tables() ->
case catch mnesia:table_info(session, attributes) of
diff --git a/src/ejabberd_socket.erl b/src/ejabberd_socket.erl
index 115ffc750..ba706e4e8 100644
--- a/src/ejabberd_socket.erl
+++ b/src/ejabberd_socket.erl
@@ -77,6 +77,8 @@ start(Module, SockMod, Socket, Opts) ->
{error, _Reason} ->
SockMod:close(Socket)
end;
+ independent ->
+ ok;
raw ->
case Module:start({SockMod, Socket}, Opts) of
{ok, Pid} ->
diff --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl
index 01efbfd76..8475e2592 100644
--- a/src/ejabberd_sup.erl
+++ b/src/ejabberd_sup.erl
@@ -99,21 +99,6 @@ init([]) ->
infinity,
supervisor,
[ejabberd_tmp_sup]},
- C2SSupervisor =
- {ejabberd_c2s_sup,
- {ejabberd_tmp_sup, start_link, [ejabberd_c2s_sup, ejabberd_c2s]},
- permanent,
- infinity,
- supervisor,
- [ejabberd_tmp_sup]},
- S2SInSupervisor =
- {ejabberd_s2s_in_sup,
- {ejabberd_tmp_sup, start_link,
- [ejabberd_s2s_in_sup, ejabberd_s2s_in]},
- permanent,
- infinity,
- supervisor,
- [ejabberd_tmp_sup]},
S2SOutSupervisor =
{ejabberd_s2s_out_sup,
{ejabberd_tmp_sup, start_link,
@@ -130,14 +115,6 @@ init([]) ->
infinity,
supervisor,
[ejabberd_tmp_sup]},
- HTTPSupervisor =
- {ejabberd_http_sup,
- {ejabberd_tmp_sup, start_link,
- [ejabberd_http_sup, ejabberd_http]},
- permanent,
- infinity,
- supervisor,
- [ejabberd_tmp_sup]},
HTTPPollSupervisor =
{ejabberd_http_poll_sup,
{ejabberd_tmp_sup, start_link,
@@ -171,11 +148,8 @@ init([]) ->
S2S,
Local,
ReceiverSupervisor,
- C2SSupervisor,
- S2SInSupervisor,
S2SOutSupervisor,
ServiceSupervisor,
- HTTPSupervisor,
HTTPPollSupervisor,
IQSupervisor,
FrontendSocketSupervisor,
diff --git a/src/ejabberdctl.template b/src/ejabberdctl.template
index 02ebd56b5..15e252cdf 100644
--- a/src/ejabberdctl.template
+++ b/src/ejabberdctl.template
@@ -163,6 +163,23 @@ live ()
$ERLANG_OPTS $ARGS \"$@\""
}
+help ()
+{
+ echo ""
+ echo "Commands to start an ejabberd node:"
+ echo " start Start an ejabberd node in server mode"
+ echo " debug Attach an interactive Erlang shell to a running ejabberd node"
+ echo " live Start an ejabberd node in live (interactive) mode"
+ echo ""
+ echo "Optional parameters when starting an ejabberd node:"
+ echo " --config file Config ejabberd: $EJABBERD_CONFIG_PATH"
+ echo " --ctl-config file Config ejabberdctl: $EJABBERDCTL_CONFIG_PATH"
+ echo " --logs dir Directory for logs: $LOGS_DIR"
+ echo " --spool dir Database spool dir: $SPOOLDIR"
+ echo " --node nodename ejabberd node name: $ERLANG_NODE"
+ echo ""
+}
+
# common control function
ctl ()
{
@@ -175,20 +192,9 @@ ctl ()
result=$?
case $result in
0) :;;
- *)
- echo ""
- echo "Commands to start an ejabberd node:"
- echo " start Start an ejabberd node in server mode"
- echo " debug Attach an interactive Erlang shell to a running ejabberd node"
- echo " live Start an ejabberd node in live (interactive) mode"
- echo ""
- echo "Optional parameters when starting an ejabberd node:"
- echo " --config file Config file of ejabberd: $EJABBERD_CONFIG_PATH"
- echo " --ctl-config file Config file of ejabberdctl: $EJABBERDCTL_CONFIG_PATH"
- echo " --logs dir Directory for logs: $LOGS_DIR"
- echo " --spool dir Database spool dir: $SPOOLDIR"
- echo " --node nodename ejabberd node name: $ERLANG_NODE"
- echo "";;
+ 1) :;;
+ 2) help;;
+ 3) help;;
esac
return $result
}
diff --git a/src/jlib.erl b/src/jlib.erl
index 73fc5fc0f..18c1c4e1a 100644
--- a/src/jlib.erl
+++ b/src/jlib.erl
@@ -27,32 +27,7 @@
-module(jlib).
-author('alexey@process-one.net').
--export([make_result_iq_reply/1,
- make_error_reply/3,
- make_error_reply/2,
- make_error_element/2,
- make_correct_from_to_attrs/3,
- replace_from_to_attrs/3,
- replace_from_to/3,
- remove_attr/2,
- make_jid/3,
- make_jid/1,
- string_to_jid/1,
- jid_to_string/1,
- is_nodename/1,
- tolower/1,
- nodeprep/1,
- nameprep/1,
- resourceprep/1,
- jid_tolower/1,
- jid_remove_resource/1,
- jid_replace_resource/2,
- get_iq_namespace/1,
- iq_query_info/1,
- iq_query_or_response_info/1,
- is_iq_request_type/1,
- iq_to_xml/1,
- parse_xdata_submit/1,
+-export([parse_xdata_submit/1,
timestamp_to_iso/1,
timestamp_to_xml/1,
now_to_utc_string/1,
@@ -68,410 +43,21 @@
short_prepd_jid/1,
short_prepd_bare_jid/1]).
--include_lib("exmpp/include/exmpp_xml.hrl").
-
--include("jlib.hrl").
-
-%send_iq(From, To, ID, SubTags) ->
-% ok.
-
-make_result_iq_reply({xmlelement, Name, Attrs, SubTags}) ->
- NewAttrs = make_result_iq_reply_attrs(Attrs),
- {xmlelement, Name, NewAttrs, SubTags}.
-
-make_result_iq_reply_attrs(Attrs) ->
- To = xml:get_attr("to", Attrs),
- From = xml:get_attr("from", Attrs),
- Attrs1 = lists:keydelete("to", 1, Attrs),
- Attrs2 = lists:keydelete("from", 1, Attrs1),
- Attrs3 = case To of
- {value, ToVal} ->
- [{"from", ToVal} | Attrs2];
- _ ->
- Attrs2
- end,
- Attrs4 = case From of
- {value, FromVal} ->
- [{"to", FromVal} | Attrs3];
- _ ->
- Attrs3
- end,
- Attrs5 = lists:keydelete("type", 1, Attrs4),
- Attrs6 = [{"type", "result"} | Attrs5],
- Attrs6.
-
-make_error_reply({xmlelement, Name, Attrs, SubTags}, Code, Desc) ->
- NewAttrs = make_error_reply_attrs(Attrs),
- {xmlelement, Name, NewAttrs, SubTags ++ [{xmlelement, "error",
- [{"code", Code}],
- [{xmlcdata, Desc}]}]}.
-
-make_error_reply({xmlelement, Name, Attrs, SubTags}, Error) ->
- NewAttrs = make_error_reply_attrs(Attrs),
- {xmlelement, Name, NewAttrs, SubTags ++ [Error]}.
-
-make_error_reply_attrs(Attrs) ->
- To = xml:get_attr("to", Attrs),
- From = xml:get_attr("from", Attrs),
- Attrs1 = lists:keydelete("to", 1, Attrs),
- Attrs2 = lists:keydelete("from", 1, Attrs1),
- Attrs3 = case To of
- {value, ToVal} ->
- [{"from", ToVal} | Attrs2];
- _ ->
- Attrs2
- end,
- Attrs4 = case From of
- {value, FromVal} ->
- [{"to", FromVal} | Attrs3];
- _ ->
- Attrs3
- end,
- Attrs5 = lists:keydelete("type", 1, Attrs4),
- Attrs6 = [{"type", "error"} | Attrs5],
- Attrs6.
-
-make_error_element(Code, Desc) ->
- {xmlelement, "error",
- [{"code", Code}],
- [{xmlcdata, Desc}]}.
-
-make_correct_from_to_attrs(From, To, Attrs) ->
- Attrs1 = lists:keydelete("from", 1, Attrs),
- Attrs2 = case xml:get_attr("to", Attrs) of
- {value, _} ->
- Attrs1;
- _ ->
- [{"to", To} | Attrs1]
- end,
- Attrs3 = [{"from", From} | Attrs2],
- Attrs3.
+-include_lib("exmpp/include/exmpp.hrl").
-replace_from_to_attrs(From, To, Attrs) ->
- Attrs1 = lists:keydelete("to", 1, Attrs),
- Attrs2 = lists:keydelete("from", 1, Attrs1),
- Attrs3 = [{"to", To} | Attrs2],
- Attrs4 = [{"from", From} | Attrs3],
- Attrs4.
-
-replace_from_to(From, To, {xmlelement, Name, Attrs, Els}) ->
- NewAttrs = replace_from_to_attrs(jlib:jid_to_string(From),
- jlib:jid_to_string(To),
- Attrs),
- {xmlelement, Name, NewAttrs, Els}.
-
-
-remove_attr(Attr, {xmlelement, Name, Attrs, Els}) ->
- NewAttrs = lists:keydelete(Attr, 1, Attrs),
- {xmlelement, Name, NewAttrs, Els}.
-
-
-make_jid(User, Server, Resource) ->
- case nodeprep(User) of
- error -> error;
- LUser ->
- case nameprep(Server) of
- error -> error;
- LServer ->
- case resourceprep(Resource) of
- error -> error;
- LResource ->
- #jid{user = User,
- server = Server,
- resource = Resource,
- luser = LUser,
- lserver = LServer,
- lresource = LResource}
- end
- end
- end.
-
-make_jid({User, Server, Resource}) ->
- make_jid(User, Server, Resource).
-
-string_to_jid(J) ->
- string_to_jid1(J, "").
-
-string_to_jid1([$@ | _J], "") ->
- error;
-string_to_jid1([$@ | J], N) ->
- string_to_jid2(J, lists:reverse(N), "");
-string_to_jid1([$/ | _J], "") ->
- error;
-string_to_jid1([$/ | J], N) ->
- string_to_jid3(J, "", lists:reverse(N), "");
-string_to_jid1([C | J], N) ->
- string_to_jid1(J, [C | N]);
-string_to_jid1([], "") ->
- error;
-string_to_jid1([], N) ->
- make_jid("", lists:reverse(N), "").
-
-%% Only one "@" is admitted per JID
-string_to_jid2([$@ | _J], _N, _S) ->
- error;
-string_to_jid2([$/ | _J], _N, "") ->
- error;
-string_to_jid2([$/ | J], N, S) ->
- string_to_jid3(J, N, lists:reverse(S), "");
-string_to_jid2([C | J], N, S) ->
- string_to_jid2(J, N, [C | S]);
-string_to_jid2([], _N, "") ->
- error;
-string_to_jid2([], N, S) ->
- make_jid(N, lists:reverse(S), "").
-
-string_to_jid3([C | J], N, S, R) ->
- string_to_jid3(J, N, S, [C | R]);
-string_to_jid3([], N, S, R) ->
- make_jid(N, S, lists:reverse(R)).
-
-jid_to_string(#jid{user = User, server = Server, resource = Resource}) ->
- jid_to_string({User, Server, Resource});
-jid_to_string({Node, Server, Resource}) ->
- S1 = case Node of
- "" ->
- "";
- _ ->
- Node ++ "@"
- end,
- S2 = S1 ++ Server,
- S3 = case Resource of
- "" ->
- S2;
- _ ->
- S2 ++ "/" ++ Resource
- end,
- S3.
-
-
-is_nodename([]) ->
- false;
-is_nodename(J) ->
- nodeprep(J) /= error.
-
-
-%tolower_c(C) when C >= $A, C =< $Z ->
-% C + 32;
-%tolower_c(C) ->
-% C.
-
--define(LOWER(Char),
- if
- Char >= $A, Char =< $Z ->
- Char + 32;
- true ->
- Char
- end).
-
-%tolower(S) ->
-% lists:map(fun tolower_c/1, S).
-
-%tolower(S) ->
-% [?LOWER(Char) || Char <- S].
-
-% Not tail-recursive but it seems works faster than variants above
-tolower([C | Cs]) ->
- if
- C >= $A, C =< $Z ->
- [C + 32 | tolower(Cs)];
- true ->
- [C | tolower(Cs)]
- end;
-tolower([]) ->
- [].
-
-%tolower([C | Cs]) when C >= $A, C =< $Z ->
-% [C + 32 | tolower(Cs)];
-%tolower([C | Cs]) ->
-% [C | tolower(Cs)];
-%tolower([]) ->
-% [].
-
-
-nodeprep(S) when length(S) < 1024 ->
- R = stringprep:nodeprep(S),
- if
- length(R) < 1024 -> R;
- true -> error
- end;
-nodeprep(_) ->
- error.
-
-nameprep(S) when length(S) < 1024 ->
- R = stringprep:nameprep(S),
- if
- length(R) < 1024 -> R;
- true -> error
- end;
-nameprep(_) ->
- error.
-
-resourceprep(S) when length(S) < 1024 ->
- R = stringprep:resourceprep(S),
- if
- length(R) < 1024 -> R;
- true -> error
- end;
-resourceprep(_) ->
- error.
-
-
-jid_tolower(#jid{luser = U, lserver = S, lresource = R}) ->
- {U, S, R};
-jid_tolower({U, S, R}) ->
- case nodeprep(U) of
- error -> error;
- LUser ->
- case nameprep(S) of
- error -> error;
- LServer ->
- case resourceprep(R) of
- error -> error;
- LResource ->
- {LUser, LServer, LResource}
- end
- end
- end.
-
-jid_remove_resource(#jid{} = JID) ->
- JID#jid{resource = "", lresource = ""};
-jid_remove_resource({U, S, _R}) ->
- {U, S, ""}.
-
-jid_replace_resource(JID, Resource) ->
- case resourceprep(Resource) of
- error -> error;
- LResource ->
- JID#jid{resource = Resource, lresource = LResource}
- end.
-
-
-get_iq_namespace({xmlelement, Name, _Attrs, Els}) when Name == "iq" ->
- case xml:remove_cdata(Els) of
- [{xmlelement, _Name2, Attrs2, _Els2}] ->
- xml:get_attr_s("xmlns", Attrs2);
- _ ->
- ""
- end;
-get_iq_namespace(_) ->
- "".
-
-iq_query_info(El) ->
- iq_info_internal(El, request).
-
-iq_query_or_response_info(El) ->
- iq_info_internal(El, any).
-
-iq_info_internal({xmlel, NS, _, _, _, _} = El, Filter) ->
- ElOld = exmpp_xml:xmlel_to_xmlelement(El, [NS],
- [{'http://etherx.jabber.org/streams', "stream"}]),
- iq_info_internal2(ElOld, Filter);
-iq_info_internal(El, Filter) ->
- catch throw(for_stacktrace),
- io:format("~nJLIB: old #xmlelement:~n~p~n~p~n~n",
- [El, erlang:get_stacktrace()]),
- iq_info_internal2(El, Filter).
-
-iq_info_internal2({xmlelement, Name, Attrs, Els}, Filter) when Name == "iq" ->
- %% Filter is either request or any. If it is request, any replies
- %% are converted to the atom reply.
- ID = xml:get_attr_s("id", Attrs),
- Type = xml:get_attr_s("type", Attrs),
- Lang = xml:get_attr_s("xml:lang", Attrs),
- {Type1, Class} = case Type of
- "set" -> {set, request};
- "get" -> {get, request};
- "result" -> {result, reply};
- "error" -> {error, reply};
- _ -> {invalid, invalid}
- end,
- if
- Type1 == invalid ->
- invalid;
- Class == request; Filter == any ->
- %% The iq record is a bit strange. The sub_el field is an
- %% XML tuple for requests, but a list of XML tuples for
- %% responses.
- FilteredEls = xml:remove_cdata(Els),
- {XMLNS, SubEl} =
- case {Class, FilteredEls} of
- {request, [{xmlelement, _Name2, Attrs2, _Els2}]} ->
- {xml:get_attr_s("xmlns", Attrs2),
- hd(FilteredEls)};
- {reply, _} ->
- %% Find the namespace of the first non-error
- %% element, if there is one.
- NonErrorEls = [El ||
- {xmlelement, SubName, _, _} = El
- <- FilteredEls,
- SubName /= "error"],
- {case NonErrorEls of
- [NonErrorEl] -> xml:get_tag_attr_s("xmlns", NonErrorEl);
- _ -> invalid
- end,
- FilteredEls};
- _ ->
- {invalid, invalid}
- end,
- if XMLNS == "", Class == request ->
- invalid;
- true ->
- #iq{id = ID,
- type = Type1,
- xmlns = XMLNS,
- lang = Lang,
- sub_el = SubEl}
- end;
- Class == reply, Filter /= any ->
- reply
- end;
-iq_info_internal2(_, _) ->
- not_iq.
-
-is_iq_request_type(set) -> true;
-is_iq_request_type(get) -> true;
-is_iq_request_type(_) -> false.
-
-iq_type_to_string(set) -> "set";
-iq_type_to_string(get) -> "get";
-iq_type_to_string(result) -> "result";
-iq_type_to_string(error) -> "error";
-iq_type_to_string(_) -> invalid.
-
-
-iq_to_xml(#iq{id = ID, type = Type, sub_el = SubEl}) ->
- if
- ID /= "" ->
- {xmlelement, "iq",
- [{"id", ID}, {"type", iq_type_to_string(Type)}], SubEl};
- true ->
- {xmlelement, "iq",
- [{"type", iq_type_to_string(Type)}], SubEl}
- end.
-
-
-parse_xdata_submit({xmlel, _, _, _, Attrs, Els}) ->
+parse_xdata_submit(#xmlel{attrs = Attrs, children = Els}) ->
case exmpp_xml:get_attribute_from_list(Attrs, 'type', "") of
"submit" ->
lists:reverse(parse_xdata_fields(Els, []));
_ ->
invalid
- end;
-parse_xdata_submit(El) ->
- {xmlelement, _Name, Attrs, Els} = El,
- case xml:get_attr_s("type", Attrs) of
- "submit" ->
- lists:reverse(parse_xdata_fields(Els, []));
- _ ->
- invalid
end.
parse_xdata_fields([], Res) ->
Res;
-parse_xdata_fields([{xmlel, _, _, 'field', Attrs, SubEls} | Els],
- Res) ->
+parse_xdata_fields([#xmlel{name = 'field', attrs = Attrs, children = SubEls} |
+ Els], Res) ->
case exmpp_xml:get_attribute_from_list(Attrs, 'var', "") of
"" ->
parse_xdata_fields(Els, Res);
@@ -479,36 +65,14 @@ parse_xdata_fields([{xmlel, _, _, 'field', Attrs, SubEls} | Els],
Field = {Var, lists:reverse(parse_xdata_values(SubEls, []))},
parse_xdata_fields(Els, [Field | Res])
end;
-parse_xdata_fields([{xmlelement, Name, Attrs, SubEls} | Els], Res) ->
- case Name of
- "field" ->
- case xml:get_attr_s("var", Attrs) of
- "" ->
- parse_xdata_fields(Els, Res);
- Var ->
- Field =
- {Var, lists:reverse(parse_xdata_values(SubEls, []))},
- parse_xdata_fields(Els, [Field | Res])
- end;
- _ ->
- parse_xdata_fields(Els, Res)
- end;
parse_xdata_fields([_ | Els], Res) ->
parse_xdata_fields(Els, Res).
parse_xdata_values([], Res) ->
Res;
-parse_xdata_values([{xmlel, _, _, 'value', _, SubEls} | Els], Res) ->
+parse_xdata_values([#xmlel{name = 'value', children = SubEls} | Els], Res) ->
Val = exmpp_xml:get_cdata_from_list_as_list(SubEls),
parse_xdata_values(Els, [Val | Res]);
-parse_xdata_values([{xmlelement, Name, _Attrs, SubEls} | Els], Res) ->
- case Name of
- "value" ->
- Val = xml:get_cdata(SubEls),
- parse_xdata_values(Els, [Val | Res]);
- _ ->
- parse_xdata_values(Els, Res)
- end;
parse_xdata_values([_ | Els], Res) ->
parse_xdata_values(Els, Res).
@@ -522,7 +86,7 @@ timestamp_to_xml({{Year, Month, Day}, {Hour, Minute, Second}}) ->
Timestamp = lists:flatten(
io_lib:format("~4..0w~2..0w~2..0wT~2..0w:~2..0w:~2..0w",
[Year, Month, Day, Hour, Minute, Second])),
- exmpp_xml:set_attribute(#xmlel{ns = ?NS_DELAY, name = 'x'},
+ exmpp_xml:set_attribute(#xmlel{ns = ?NS_DELAY_OLD, name = 'x'},
'stamp', Timestamp).
now_to_utc_string({MegaSecs, Secs, MicroSecs}) ->
@@ -731,8 +295,8 @@ ip_to_list({A,B,C,D}) ->
%%
%% Empty fields are set to `undefined', not the empty string.
-from_old_jid(#jid{user = Node, resource = Resource,
- luser = LNode, lresource = LResource} = JID) ->
+from_old_jid(#jid{node = Node, resource = Resource,
+ lnode = LNode, lresource = LResource} = JID) ->
{Node1, LNode1} = case Node of
"" -> {undefined, undefined};
_ -> {Node, LNode}
@@ -741,8 +305,8 @@ from_old_jid(#jid{user = Node, resource = Resource,
"" -> {undefined, undefined};
_ -> {Resource, LResource}
end,
- JID#jid{user = Node1, resource = Resource1,
- luser = LNode1, lresource = LResource1}.
+ JID#jid{node = Node1, resource = Resource1,
+ lnode = LNode1, lresource = LResource1}.
%% @spec (JID) -> New_JID
%% JID = jid()
@@ -751,8 +315,8 @@ from_old_jid(#jid{user = Node, resource = Resource,
%%
%% Empty fields are set to the empty string, not `undefined'.
-to_old_jid(#jid{user = Node, resource = Resource,
- luser = LNode, lresource = LResource} = JID) ->
+to_old_jid(#jid{node = Node, resource = Resource,
+ lnode = LNode, lresource = LResource} = JID) ->
{Node1, LNode1} = case Node of
undefined -> {"", ""};
_ -> {Node, LNode}
@@ -761,19 +325,19 @@ to_old_jid(#jid{user = Node, resource = Resource,
undefined -> {"", ""};
_ -> {Resource, LResource}
end,
- JID#jid{user = Node1, resource = Resource1,
- luser = LNode1, lresource = LResource1}.
+ JID#jid{node = Node1, resource = Resource1,
+ lnode = LNode1, lresource = LResource1}.
short_jid(JID) ->
- {JID#jid.user, JID#jid.server, JID#jid.resource}.
+ {JID#jid.node, JID#jid.domain, JID#jid.resource}.
short_bare_jid(JID) ->
Bare_JID = exmpp_jid:jid_to_bare_jid(JID),
- {Bare_JID#jid.user, Bare_JID#jid.server, Bare_JID#jid.resource}.
+ {Bare_JID#jid.node, Bare_JID#jid.domain, Bare_JID#jid.resource}.
short_prepd_jid(JID) ->
- {JID#jid.luser, JID#jid.lserver, JID#jid.lresource}.
+ {JID#jid.lnode, JID#jid.ldomain, JID#jid.lresource}.
short_prepd_bare_jid(JID) ->
Bare_JID = exmpp_jid:jid_to_bare_jid(JID),
- {Bare_JID#jid.luser, Bare_JID#jid.lserver, Bare_JID#jid.lresource}.
+ {Bare_JID#jid.lnode, Bare_JID#jid.ldomain, Bare_JID#jid.lresource}.
diff --git a/src/mod_configure.erl b/src/mod_configure.erl
index 47c6e1336..5a760972e 100644
--- a/src/mod_configure.erl
+++ b/src/mod_configure.erl
@@ -1406,7 +1406,7 @@ set_form(_From, _Host, ["running nodes", ENode, "backup", "textfile"], _Lang, XD
false ->
{error, 'bad-request'};
{value, {_, [String]}} ->
- case rpc:call(Node, ejabberd_ctl, dump_to_textfile, [String]) of
+ case rpc:call(Node, ejabberd_admin, dump_to_textfile, [String]) of
{badrpc, _Reason} ->
{error, 'internal-server-error'};
{error, _Reason} ->
diff --git a/src/mod_offline.erl b/src/mod_offline.erl
index ecabbf5c5..129c41b52 100644
--- a/src/mod_offline.erl
+++ b/src/mod_offline.erl
@@ -39,7 +39,8 @@
remove_old_messages/1,
remove_user/2,
webadmin_page/3,
- webadmin_user/4]).
+ webadmin_user/4,
+ webadmin_user_parse_query/5]).
-include_lib("exmpp/include/exmpp.hrl").
@@ -75,6 +76,8 @@ start(Host, Opts) ->
?MODULE, webadmin_page, 50),
ejabberd_hooks:add(webadmin_user, Host,
?MODULE, webadmin_user, 50),
+ ejabberd_hooks:add(webadmin_user_parse_query, Host,
+ ?MODULE, webadmin_user_parse_query, 50),
MaxOfflineMsgs = gen_mod:get_opt(user_max_messages, Opts, infinity),
register(gen_mod:get_module_proc(Host, ?PROCNAME),
spawn(?MODULE, init, [MaxOfflineMsgs])).
@@ -143,6 +146,8 @@ stop(Host) ->
?MODULE, webadmin_page, 50),
ejabberd_hooks:delete(webadmin_user, Host,
?MODULE, webadmin_user, 50),
+ ejabberd_hooks:delete(webadmin_user_parse_query, Host,
+ ?MODULE, webadmin_user_parse_query, 50),
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
exit(whereis(Proc), stop),
{wait, Proc}.
@@ -642,4 +647,24 @@ webadmin_user(Acc, User, Server, Lang) ->
_ ->
[?C("?")]
end,
- Acc ++ [?XCT("h3", "Offline Messages:")] ++ FQueueLen.
+ Acc ++ [?XCT("h3", "Offline Messages:")] ++ FQueueLen ++ [?C(" "), ?INPUTT("submit", "removealloffline", "Remove All Offline Messages")].
+
+webadmin_user_parse_query(_, "removealloffline", User, Server, _Query) ->
+ US = {User, Server},
+ F = fun() ->
+ mnesia:write_lock_table(offline_msg),
+ lists:foreach(
+ fun(Msg) ->
+ mnesia:delete_object(Msg)
+ end, mnesia:dirty_read({offline_msg, US}))
+ end,
+ case mnesia:transaction(F) of
+ {aborted, Reason} ->
+ ?ERROR_MSG("Failed to remove offline messages: ~p", [Reason]),
+ {stop, error};
+ {atomic, ok} ->
+ ?INFO_MSG("Removed all offline messages for ~s@~s", [User, Server]),
+ {stop, ok}
+ end;
+webadmin_user_parse_query(Acc, _Action, _User, _Server, _Query) ->
+ Acc.
diff --git a/src/mod_offline_odbc.erl b/src/mod_offline_odbc.erl
index bc6a57ee7..a5db7a9c5 100644
--- a/src/mod_offline_odbc.erl
+++ b/src/mod_offline_odbc.erl
@@ -38,7 +38,8 @@
pop_offline_messages/3,
remove_user/2,
webadmin_page/3,
- webadmin_user/4]).
+ webadmin_user/4,
+ webadmin_user_parse_query/5]).
-include_lib("exmpp/include/exmpp.hrl").
@@ -69,6 +70,8 @@ start(Host, Opts) ->
?MODULE, webadmin_page, 50),
ejabberd_hooks:add(webadmin_user, Host,
?MODULE, webadmin_user, 50),
+ ejabberd_hooks:add(webadmin_user_parse_query, Host,
+ ?MODULE, webadmin_user_parse_query, 50),
MaxOfflineMsgs = gen_mod:get_opt(user_max_messages, Opts, infinity),
register(gen_mod:get_module_proc(Host, ?PROCNAME),
spawn(?MODULE, init, [Host, MaxOfflineMsgs])).
@@ -151,6 +154,8 @@ stop(Host) ->
?MODULE, webadmin_page, 50),
ejabberd_hooks:delete(webadmin_user, Host,
?MODULE, webadmin_user, 50),
+ ejabberd_hooks:delete(webadmin_user_parse_query, Host,
+ ?MODULE, webadmin_user_parse_query, 50),
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
exit(whereis(Proc), stop),
ok.
@@ -446,7 +451,22 @@ webadmin_user(Acc, User, Server, Lang) ->
_ ->
[?C("?")]
end,
- Acc ++ [?XCT("h3", "Offline Messages:")] ++ FQueueLen.
+ Acc ++ [?XCT("h3", "Offline Messages:")] ++ FQueueLen ++ [?C(" "), ?INPUTT("submit", "removealloffline", "Remove All Offline Messages")].
+
+webadmin_user_parse_query(_, "removealloffline", User, Server, _Query) ->
+ case catch odbc_queries:del_spool_msg(Server, User) of
+ {'EXIT', Reason} ->
+ ?ERROR_MSG("Failed to remove offline messages: ~p", [Reason]),
+ {stop, error};
+ {error, Reason} ->
+ ?ERROR_MSG("Failed to remove offline messages: ~p", [Reason]),
+ {stop, error};
+ _ ->
+ ?INFO_MSG("Removed all offline messages for ~s@~s", [User, Server]),
+ {stop, ok}
+ end;
+webadmin_user_parse_query(Acc, _Action, _User, _Server, _Query) ->
+ Acc.
%% ------------------------------------------------
%% mod_offline: number of messages quota management
diff --git a/src/mod_proxy65/mod_proxy65.erl b/src/mod_proxy65/mod_proxy65.erl
index 67f900520..cf00777af 100644
--- a/src/mod_proxy65/mod_proxy65.erl
+++ b/src/mod_proxy65/mod_proxy65.erl
@@ -42,6 +42,7 @@
-define(PROCNAME, ejabberd_mod_proxy65).
start(Host, Opts) ->
+ mod_proxy65_service:add_listener(Host, Opts),
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
ChildSpec = {
Proc, {?MODULE, start_link, [Host, Opts]},
@@ -50,6 +51,7 @@ start(Host, Opts) ->
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
+ mod_proxy65_service:delete_listener(Host),
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
supervisor:terminate_child(ejabberd_sup, Proc),
supervisor:delete_child(ejabberd_sup, Proc).
diff --git a/src/mod_proxy65/mod_proxy65_service.erl b/src/mod_proxy65/mod_proxy65_service.erl
index 36d01b9c3..2b252c122 100644
--- a/src/mod_proxy65/mod_proxy65_service.erl
+++ b/src/mod_proxy65/mod_proxy65_service.erl
@@ -39,7 +39,7 @@
]).
%% API.
--export([start_link/2]).
+-export([start_link/2, add_listener/2, delete_listener/1]).
-include("ejabberd.hrl").
-include("jlib.hrl").
@@ -55,28 +55,21 @@
acl
}).
-%% Unused callbacks.
-handle_cast(_Request, State) ->
- {noreply, State}.
-code_change(_OldVsn, State, _Extra) ->
- {ok, State}.
-handle_call(_Request, _From, State) ->
- {reply, ok, State}.
-%%----------------
+
+%%%------------------------
+%%% gen_server callbacks
+%%%------------------------
start_link(Host, Opts) ->
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
init([Host, Opts]) ->
- {IP, State} = parse_options(Host, Opts),
- NewOpts = [Host, {ip, IP} | Opts],
- ejabberd_listener:add_listener(State#state.port, mod_proxy65_stream, NewOpts),
+ {_IP, State} = parse_options(Host, Opts),
ejabberd_router:register_route(State#state.myhost),
{ok, State}.
-terminate(_Reason, #state{myhost=MyHost, port=Port}) ->
- catch ejabberd_listener:delete_listener(Port),
+terminate(_Reason, #state{myhost=MyHost}) ->
ejabberd_router:unregister_route(MyHost),
ok.
@@ -93,10 +86,34 @@ handle_info({route, From, To, {xmlelement, "iq", _, _} = Packet}, State) ->
ok
end,
{noreply, State};
-
handle_info(_Info, State) ->
{noreply, State}.
+handle_call(get_port, _From, State) ->
+ {reply, {port, State#state.port}, State};
+handle_call(_Request, _From, State) ->
+ {reply, ok, State}.
+
+handle_cast(_Request, State) ->
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%------------------------
+%%% Listener management
+%%%------------------------
+
+add_listener(Host, Opts) ->
+ {IP, State} = parse_options(Host, Opts),
+ NewOpts = [Host, {ip, IP} | Opts],
+ ejabberd_listener:add_listener(State#state.port,mod_proxy65_stream,NewOpts).
+
+delete_listener(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ {port, Port} = gen_server:call(Proc, get_port),
+ catch ejabberd_listener:delete_listener(Port, mod_proxy65_stream).
+
%%%------------------------
%%% IQ Processing
%%%------------------------
diff --git a/src/mod_pubsub/mod_pubsub.erl b/src/mod_pubsub/mod_pubsub.erl
index a7084fad6..f140d6e9d 100644
--- a/src/mod_pubsub/mod_pubsub.erl
+++ b/src/mod_pubsub/mod_pubsub.erl
@@ -1486,7 +1486,7 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) ->
node_call(Type, publish_item, [Host, Node, Publisher, PublishModel, MaxItems, ItemId, Payload])
end
end,
- %%ejabberd_hooks:run(pubsub_publish_item, Host, [Host, Node, JID, service_jid(Host), ItemId, Payload]),
+ ejabberd_hooks:run(pubsub_publish_item, Host, [Host, Node, Publisher, service_jid(Host), ItemId, Payload]),
Reply = [],
case transaction(Host, Node, Action, sync_dirty) of
{error, ?ERR_ITEM_NOT_FOUND} ->
diff --git a/src/web/ejabberd_web_admin.erl b/src/web/ejabberd_web_admin.erl
index 9497efeb9..c7da8cf69 100644
--- a/src/web/ejabberd_web_admin.erl
+++ b/src/web/ejabberd_web_admin.erl
@@ -104,60 +104,15 @@ get_auth(Auth) ->
unauthorized
end.
-make_xhtml(Els, global, Lang) ->
- MenuItems1 = ejabberd_hooks:run_fold(webadmin_menu_main, [], [Lang]),
- MenuItems2 = [?LI([?AC("/admin/"++MI_uri++"/", MI_name)]) || {MI_uri, MI_name} <- MenuItems1],
- {200, [html],
- {xmlelement, "html", [{"xmlns", "http://www.w3.org/1999/xhtml"},
- {"xml:lang", Lang},
- {"lang", Lang}],
- [{xmlelement, "head", [],
- [?XCT("title", "ejabberd Web Admin"),
- {xmlelement, "meta", [{"http-equiv", "Content-Type"},
- {"content", "text/html; charset=utf-8"}], []},
- {xmlelement, "link", [{"href", "/admin/favicon.ico"},
- {"type", "image/x-icon"},
- {"rel", "shortcut icon"}], []},
- {xmlelement, "link", [{"href", "/admin/style.css"},
- {"type", "text/css"},
- {"rel", "stylesheet"}], []}]},
- ?XE("body",
- [?XAE("div",
- [{"id", "container"}],
- [?XAE("div",
- [{"id", "header"}],
- [?XE("h1",
- [?ACT("/admin/", "Administration")]
- )]),
- ?XAE("div",
- [{"id", "navigation"}],
- [?XE("ul",
- [?LI([?ACT("/admin/acls/", "Access Control Lists")]),
- ?LI([?ACT("/admin/access/", "Access Rules")]),
- ?LI([?ACT("/admin/vhosts/", "Virtual Hosts")]),
- ?LI([?ACT("/admin/nodes/", "Nodes")]),
- ?LI([?ACT("/admin/stats/", "Statistics")])
- ] ++ MenuItems2
- )]),
- ?XAE("div",
- [{"id", "content"}],
- Els),
- ?XAE("div",
- [{"id", "clearcopyright"}],
- [{xmlcdata, ""}])]),
- ?XAE("div",
- [{"id", "copyrightouter"}],
- [?XAE("div",
- [{"id", "copyright"}],
- [?XC("p",
- "ejabberd (c) 2002-2008 ProcessOne")
- ])])])
- ]}};
-
make_xhtml(Els, Host, Lang) ->
- Base = "/admin/server/" ++ Host ++ "/",
- MenuItems1 = ejabberd_hooks:run_fold(webadmin_menu_host, Host, [], [Host, Lang]),
- MenuItems2 = [?LI([?AC(Base ++ MI_uri ++ "/", MI_name)]) || {MI_uri, MI_name} <- MenuItems1],
+ make_xhtml(Els, Host, cluster, Lang).
+
+%% @spec (Els, Host, Node, Lang)
+%% where Host = global | string()
+%% Node = cluster | atom()
+make_xhtml(Els, Host, Node, Lang) ->
+ Base = get_base_path(Host, cluster), %% Enforcing 'cluster' on purpose here
+ MenuItems = make_navigation(Host, Node, Lang),
{200, [html],
{xmlelement, "html", [{"xmlns", "http://www.w3.org/1999/xhtml"},
{"xml:lang", Lang},
@@ -166,7 +121,7 @@ make_xhtml(Els, Host, Lang) ->
[?XCT("title", "ejabberd Web Admin"),
{xmlelement, "meta", [{"http-equiv", "Content-Type"},
{"content", "text/html; charset=utf-8"}], []},
- {xmlelement, "link", [{"href", "/admin/favicon.ico"},
+ {xmlelement, "link", [{"href", Base ++ "favicon.ico"},
{"type", "image/x-icon"},
{"rel", "shortcut icon"}], []},
{xmlelement, "link", [{"href", Base ++ "style.css"},
@@ -183,18 +138,7 @@ make_xhtml(Els, Host, Lang) ->
?XAE("div",
[{"id", "navigation"}],
[?XE("ul",
- [?LI([?XAE("div",
- [{"id", "navheadhost"}],
- [?AC(Base, Host)]
- )]),
- ?LI([?ACT(Base ++ "acls/", "Access Control Lists")]),
- ?LI([?ACT(Base ++ "access/", "Access Rules")]),
- ?LI([?ACT(Base ++ "users/", "Users")]),
- ?LI([?ACT(Base ++ "online-users/", "Online Users")]),
- ?LI([?ACT(Base ++ "last-activity/", "Last Activity")]),
- ?LI([?ACT(Base ++ "nodes/", "Nodes")]),
- ?LI([?ACT(Base ++ "stats/", "Statistics")])
- ] ++ MenuItems2
+ MenuItems
)]),
?XAE("div",
[{"id", "content"}],
@@ -211,13 +155,13 @@ make_xhtml(Els, Host, Lang) ->
])])])
]}}.
+get_base_path(global, cluster) -> "/admin/";
+get_base_path(Host, cluster) -> "/admin/server/" ++ Host ++ "/";
+get_base_path(global, Node) -> "/admin/node/" ++ atom_to_list(Node) ++ "/";
+get_base_path(Host, Node) -> "/admin/server/" ++ Host ++ "/node/" ++ atom_to_list(Node) ++ "/".
+
css(Host) ->
- Base = case Host of
- global ->
- "/admin/";
- _ ->
- "/admin/server/" ++ Host ++ "/"
- end,
+ Base = get_base_path(Host, cluster),
"
html,body {
background: white;
@@ -298,7 +242,7 @@ html>body #container {
#navigation ul {
position: absolute;
- top: 54px;
+ top: 65px;
left: 0;
padding: 0 1px 1px 1px;
margin: 0;
@@ -306,7 +250,7 @@ html>body #container {
font-size: 8pt;
font-weight: bold;
background: #d47911;
- width: 13em;
+ width: 17em;
}
#navigation ul li {
@@ -340,9 +284,20 @@ html>body #container {
background: #332;
}
-#navheadhost {
- text-align: left;
- border-bottom: 2px solid #d47911;
+ul li #navhead a, ul li #navheadsub a, ul li #navheadsubsub a {
+ text-align: center;
+ border-top: 2px solid #d47911;
+ border-bottom: 1px solid #d47911;
+}
+
+#navheadsub, #navitemsub {
+ border-left: 7px solid white;
+ margin-left: 2px solid #d47911;
+}
+
+#navheadsubsub, #navitemsubsub {
+ border-left: 14px solid white;
+ margin-left: 4px solid #d47911;
}
#lastactivity li {
@@ -550,7 +505,7 @@ h3 {
#content {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 10pt;
- padding-left: 13em;
+ padding-left: 17em;
padding-top: 5px;
}
@@ -595,11 +550,12 @@ logo_fill() ->
"1c5dvhSU2BpKqBXl6R0ljYGS50R5zVC+tVD+vfE6YyUexE9x7g4AAAAASUVO"
"RK5CYII=").
+
process_admin(global,
#request{path = [],
lang = Lang}) ->
- MenuItems1 = ejabberd_hooks:run_fold(webadmin_menu_main, [], [Lang]),
- MenuItems2 = [?LI([?AC("/admin/"++MI_uri++"/", MI_name)]) || {MI_uri, MI_name} <- MenuItems1],
+ Base = get_base_path(global, cluster),
+ MenuItems2 = make_menu_items(global, cluster, Base, Lang),
make_xhtml([?XCT("h1", "Administration"),
?XE("ul",
[?LI([?ACT("/admin/acls/", "Access Control Lists"), ?C(" "),
@@ -616,9 +572,8 @@ process_admin(global,
process_admin(Host,
#request{path = [],
lang = Lang}) ->
- Base = "/admin/server/" ++ Host ++ "/",
- MenuItems1 = ejabberd_hooks:run_fold(webadmin_menu_host, Host, [], [Host, Lang]),
- MenuItems2 = [?LI([?AC(Base ++ MI_uri ++ "/", MI_name)]) || {MI_uri, MI_name} <- MenuItems1],
+ Base = get_base_path(Host, cluster),
+ MenuItems2 = make_menu_items(Host, cluster, Base, Lang),
make_xhtml([?XCT("h1", "Administration"),
?XE("ul",
[?LI([?ACT(Base ++ "acls/", "Access Control Lists"), ?C(" "),
@@ -953,8 +908,13 @@ process_admin(Host,
#request{path = ["user", U],
q = Query,
lang = Lang}) ->
- Res = user_info(U, Host, Query, Lang),
- make_xhtml(Res, Host, Lang);
+ case ejabberd_auth:is_user_exists(U, Host) of
+ true ->
+ Res = user_info(U, Host, Query, Lang),
+ make_xhtml(Res, Host, Lang);
+ false ->
+ make_xhtml([?XCT("h1", "Not Found")], Host, Lang)
+ end;
process_admin(Host,
#request{path = ["nodes"],
@@ -971,7 +931,7 @@ process_admin(Host,
make_xhtml([?XCT("h1", "Node not found")], Host, Lang);
Node ->
Res = get_node(Host, Node, NPath, Query, Lang),
- make_xhtml(Res, Host, Lang)
+ make_xhtml(Res, Host, Node, Lang)
end;
process_admin(Host, #request{lang = Lang} = Request) ->
@@ -1515,25 +1475,31 @@ user_info(User, Server, Query, Lang) ->
user_parse_query(User, Server, Query) ->
- case lists:keysearch("chpassword", 1, Query) of
- {value, _} ->
- case lists:keysearch("password", 1, Query) of
- {value, {_, undefined}} ->
- error;
- {value, {_, Password}} ->
- ejabberd_auth:set_password(User, Server, Password),
- ok;
- _ ->
- error
- end;
- _ ->
- case lists:keysearch("removeuser", 1, Query) of
- {value, _} ->
- ejabberd_auth:remove_user(User, Server),
- ok;
- false ->
- nothing
- end
+ lists:foldl(fun({Action, _Value}, Acc) when Acc == nothing ->
+ user_parse_query1(Action, User, Server, Query);
+ ({_Action, _Value}, Acc) ->
+ Acc
+ end, nothing, Query).
+
+user_parse_query1("password", _User, _Server, _Query) ->
+ nothing;
+user_parse_query1("chpassword", User, Server, Query) ->
+ case lists:keysearch("password", 1, Query) of
+ {value, {_, undefined}} ->
+ error;
+ {value, {_, Password}} ->
+ ejabberd_auth:set_password(User, Server, Password),
+ ok;
+ _ ->
+ error
+ end;
+user_parse_query1("removeuser", User, Server, _Query) ->
+ ejabberd_auth:remove_user(User, Server),
+ ok;
+user_parse_query1(Action, User, Server, Query) ->
+ case ejabberd_hooks:run_fold(webadmin_user_parse_query, Server, [], [Action, User, Server, Query]) of
+ [] -> nothing;
+ Res -> Res
end.
@@ -1661,8 +1627,8 @@ search_running_node(SNode, [Node | Nodes]) ->
get_node(global, Node, [], Query, Lang) ->
Res = node_parse_query(Node, Query),
- MenuItems1 = ejabberd_hooks:run_fold(webadmin_menu_node, [], [Node, Lang]),
- MenuItems2 = [?LI([?AC(MI_uri++"/", MI_name)]) || {MI_uri, MI_name} <- MenuItems1],
+ Base = get_base_path(global, Node),
+ MenuItems2 = make_menu_items(global, Node, Base, Lang),
[?XC("h1", ?T("Node ") ++ atom_to_list(Node))] ++
case Res of
ok -> [?CT("Submitted"), ?P];
@@ -1670,11 +1636,11 @@ get_node(global, Node, [], Query, Lang) ->
nothing -> []
end ++
[?XE("ul",
- [?LI([?ACT("db/", "Database")]),
- ?LI([?ACT("backup/", "Backup")]),
- ?LI([?ACT("ports/", "Listened Ports")]),
- ?LI([?ACT("stats/", "Statistics")]),
- ?LI([?ACT("update/", "Update")])
+ [?LI([?ACT(Base ++ "db/", "Database")]),
+ ?LI([?ACT(Base ++ "backup/", "Backup")]),
+ ?LI([?ACT(Base ++ "ports/", "Listened Ports")]),
+ ?LI([?ACT(Base ++ "stats/", "Statistics")]),
+ ?LI([?ACT(Base ++ "update/", "Update")])
] ++ MenuItems2),
?XAE("form", [{"action", ""}, {"method", "post"}],
[?INPUTT("submit", "restart", "Restart"),
@@ -1683,11 +1649,11 @@ get_node(global, Node, [], Query, Lang) ->
];
get_node(Host, Node, [], _Query, Lang) ->
- MenuItems1 = ejabberd_hooks:run_fold(webadmin_menu_hostnode, Host, [], [Host, Node, Lang]),
- MenuItems2 = [?LI([?AC(MI_uri++"/", MI_name)]) || {MI_uri, MI_name} <- MenuItems1],
+ Base = get_base_path(Host, Node),
+ MenuItems2 = make_menu_items(global, Node, Base, Lang),
[?XC("h1", ?T("Node ") ++ atom_to_list(Node)),
?XE("ul",
- [?LI([?ACT("modules/", "Modules")])] ++ MenuItems2)
+ [?LI([?ACT(Base ++ "modules/", "Modules")])] ++ MenuItems2)
];
get_node(global, Node, ["db"], Query, Lang) ->
@@ -2022,7 +1988,7 @@ node_backup_parse_query(Node, Query) ->
rpc:call(Node, mnesia,
install_fallback, [Path]);
"dump" ->
- rpc:call(Node, ejabberd_ctl,
+ rpc:call(Node, ejabberd_admin,
dump_to_textfile, [Path]);
"load" ->
rpc:call(Node, mnesia,
@@ -2087,7 +2053,7 @@ node_ports_to_xhtml(Ports, Lang) ->
node_ports_parse_query(Node, Ports, Query) ->
lists:foreach(
- fun({Port, _Module1, _Opts1}) ->
+ fun({Port, Module1, _Opts1}) ->
SPort = integer_to_list(Port),
case lists:keysearch("add" ++ SPort, 1, Query) of
{value, _} ->
@@ -2097,13 +2063,13 @@ node_ports_parse_query(Node, Ports, Query) ->
Module = list_to_atom(SModule),
{ok, Tokens, _} = erl_scan:string(SOpts ++ "."),
{ok, Opts} = erl_parse:parse_term(Tokens),
- rpc:call(Node, ejabberd_listener, delete_listener, [Port]),
+ rpc:call(Node, ejabberd_listener, delete_listener, [Port, Module]),
rpc:call(Node, ejabberd_listener, add_listener, [Port, Module, Opts]),
throw(submitted);
_ ->
case lists:keysearch("delete" ++ SPort, 1, Query) of
{value, _} ->
- rpc:call(Node, ejabberd_listener, delete_listener, [Port]),
+ rpc:call(Node, ejabberd_listener, delete_listener, [Port, Module1]),
throw(submitted);
_ ->
ok
@@ -2276,3 +2242,134 @@ last_modified() ->
{"Last-Modified", "Mon, 25 Feb 2008 13:23:30 GMT"}.
cache_control_public() ->
{"Cache-Control", "public"}.
+
+
+%%%
+%%% Navigation Menu
+%%%
+
+%% @spec (Host, Node, Lang) -> [LI]
+make_navigation(Host, Node, Lang) ->
+ HostNodeMenu = make_host_node_menu(Host, Node, Lang),
+ HostMenu = make_host_menu(Host, HostNodeMenu, Lang),
+ NodeMenu = make_node_menu(Host, Node, Lang),
+ Menu = make_server_menu(HostMenu, NodeMenu, Lang),
+ make_menu_items(Lang, Menu).
+
+%% @spec (Host, Node, Base, Lang) -> [LI]
+make_menu_items(global, cluster, Base, Lang) ->
+ HookItems = get_menu_items_hook(server, Lang),
+ make_menu_items(Lang, {Base, "", HookItems});
+
+make_menu_items(global, _Node, Base, Lang) ->
+ HookItems = get_menu_items_hook(node, 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}, Lang),
+ make_menu_items(Lang, {Base, "", HookItems}).
+
+
+make_host_node_menu(global, _, _Lang) ->
+ {"", "", []};
+make_host_node_menu(_, cluster, _Lang) ->
+ {"", "", []};
+make_host_node_menu(Host, Node, Lang) ->
+ HostNodeBase = get_base_path(Host, Node),
+ HostNodeFixed = [{"modules/", "Modules"}],
+ HostNodeHook = get_menu_items_hook({hostnode, Host}, Lang),
+ {HostNodeBase, atom_to_list(Node), HostNodeFixed ++ HostNodeHook}.
+
+make_host_menu(global, _HostNodeMenu, _Lang) ->
+ {"", "", []};
+make_host_menu(Host, HostNodeMenu, Lang) ->
+ HostBase = get_base_path(Host, cluster),
+ HostFixed = [{"acls", "Access Control Lists"},
+ {"access", "Access Rules"},
+ {"users", "Users"},
+ {"online-users", "Online Users"},
+ {"last-activity", "Last Activity"},
+ {"nodes", "Nodes", HostNodeMenu},
+ {"stats", "Statistics"}],
+ HostHook = get_menu_items_hook({host, Host}, Lang),
+ {HostBase, Host, HostFixed ++ HostHook}.
+
+make_node_menu(_Host, cluster, _Lang) ->
+ {"", "", []};
+make_node_menu(global, Node, Lang) ->
+ NodeBase = get_base_path(global, Node),
+ NodeFixed = [{"db/", "Database"},
+ {"backup/", "Backup"},
+ {"ports/", "Listened Ports"},
+ {"stats/", "Statistics"},
+ {"update/", "Update"}],
+ NodeHook = get_menu_items_hook(node, Lang),
+ {NodeBase, atom_to_list(Node), NodeFixed ++ NodeHook};
+make_node_menu(_Host, _Node, _Lang) ->
+ {"", "", []}.
+
+make_server_menu(HostMenu, NodeMenu, Lang) ->
+ Base = get_base_path(global, cluster),
+ Fixed = [{"acls", "Access Control Lists"},
+ {"access", "Access Rules"},
+ {"vhosts", "Virtual Hosts", HostMenu},
+ {"nodes", "Nodes", NodeMenu},
+ {"stats", "Statistics"}],
+ Hook = get_menu_items_hook(server, Lang),
+ {Base, "ejabberd", Fixed ++ Hook}.
+
+
+get_menu_items_hook({hostnode, Host}, Lang) ->
+ ejabberd_hooks:run_fold(webadmin_menu_hostnode, Host, [], [Host, Lang]);
+get_menu_items_hook({host, Host}, Lang) ->
+ ejabberd_hooks:run_fold(webadmin_menu_host, Host, [], [Host, Lang]);
+get_menu_items_hook(node, Lang) ->
+ ejabberd_hooks:run_fold(webadmin_menu_node, [], [Lang]);
+get_menu_items_hook(server, Lang) ->
+ ejabberd_hooks:run_fold(webadmin_menu_main, [], [Lang]).
+
+
+%% @spec (Lang::string(), Menu) -> [LI]
+%% where Menu = {MURI::string(), MName::string(), Items::[Item]}
+%% Item = {IURI::string(), IName::string()} | {IURI::string(), IName::string(), Menu}
+make_menu_items(Lang, Menu) ->
+ lists:reverse(make_menu_items2(Lang, 1, Menu)).
+
+make_menu_items2(Lang, Deep, {MURI, MName, _} = Menu) ->
+ Res = case MName of
+ "" -> [];
+ _ -> [make_menu_item(header, Deep, MURI, MName, Lang) ]
+ end,
+ make_menu_items2(Lang, Deep, Menu, Res).
+
+make_menu_items2(_, _Deep, {_, _, []}, Res) ->
+ Res;
+
+make_menu_items2(Lang, Deep, {MURI, MName, [Item | Items]}, Res) ->
+ Res2 = case Item of
+ {IURI, IName} ->
+ [make_menu_item(item, Deep, MURI++IURI++"/", IName, Lang) | Res];
+ {IURI, IName, SubMenu} ->
+ %%ResTemp = [?LI([?ACT(MURI ++ IURI ++ "/", IName)]) | Res],
+ ResTemp = [make_menu_item(item, Deep, MURI++IURI++"/", IName, Lang) | Res],
+ ResSubMenu = make_menu_items2(Lang, Deep+1, SubMenu),
+ ResSubMenu ++ ResTemp
+ end,
+ make_menu_items2(Lang, Deep, {MURI, MName, Items}, Res2).
+
+make_menu_item(header, 1, URI, Name, _Lang) ->
+ ?LI([?XAE("div", [{"id", "navhead"}], [?AC(URI, "~ "++Name++" ~")] )]);
+make_menu_item(header, 2, URI, Name, _Lang) ->
+ ?LI([?XAE("div", [{"id", "navheadsub"}], [?AC(URI, "~ "++Name++" ~")] )]);
+make_menu_item(header, 3, URI, Name, _Lang) ->
+ ?LI([?XAE("div", [{"id", "navheadsubsub"}], [?AC(URI, "~ "++Name++" ~")] )]);
+make_menu_item(item, 1, URI, Name, Lang) ->
+ ?LI([?XAE("div", [{"id", "navitem"}], [?ACT(URI, Name)] )]);
+make_menu_item(item, 2, URI, Name, Lang) ->
+ ?LI([?XAE("div", [{"id", "navitemsub"}], [?ACT(URI, Name)] )]);
+make_menu_item(item, 3, URI, Name, Lang) ->
+ ?LI([?XAE("div", [{"id", "navitemsubsub"}], [?ACT(URI, Name)] )]).