diff --git a/ChangeLog b/ChangeLog index e9de2947d..34bbb8cb0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2008-10-13 Jean-Sébastien Pédron + + Merge from trunk (r1613 to 1649). + 2008-10-13 Jean-Sébastien Pédron * src/extauth.erl (call_port/2): Replace jlib:nameprep/1 by @@ -8,6 +12,68 @@ * src/ejd2odbc.erl, src/jd2ejd.erl: Convert to exmpp. +2008-10-13 Badlop + + * src/web/ejabberd_web_admin.erl: When requesting page of + nonexistent user, show 'Not Found' page (EJAB-771) + +2008-10-12 Badlop + + * src/web/ejabberd_web_admin.erl: Run new hook + webadmin_user_parse_query when POST in web admin user + page (thanks to Oleg Palij)(EJAB-747) + * src/mod_offline.erl: Add button "Remove All Offline Messages" in + a user page (thanks to Oleg Palij)(EJAB-747) + * src/mod_offline_odbc.erl: Likewise + + * src/web/ejabberd_web_admin.erl: Improve Web Admin navigation + menu for vhosts and nodes (EJAB-734) + + * doc/guide.tex: Explain the new ejabberdctl command 'help' + * doc/guide.html: Likewise + + * src/mod_configure.erl: Update calls from ctl to + commands (EJAB-694) + * src/web/ejabberd_web_admin.erl: Likewise + + * src/ejabberd_sm.erl: Update from ctl to commands (EJAB-694) + * src/ejabberd_s2s.erl: Likewise + + * src/ejabberd_auth.erl: Update from ctl to commands (EJAB-694) + * src/ejabberd_auth_internal.erl: Likewise + * src/ejabberd_auth_ldap.erl: Likewise + * src/ejabberd_auth_odbc.erl: Likewise + + * src/ejabberdctl.template: Move help print to a separate + function (EJAB-694) + + * src/ejabberd_ctl.erl: Add frontend support for + commands (EJAB-694). Categorization and sorting of commands in + ejabberd_ctl help (EJAB-313). Lines in command line help of length + 80, and text formatting (EJAB-473) + + * src/ejabberd_app.erl: Initialize ejabberd_commands and start + ejabbed_admin (EJAB-694) + + * src/ejabberd_admin.erl: Implement commands from old + ejabberd_ctl (EJAB-694) + + * src/ejabberd_commands.erl: New 'ejabberd commands': separate + command definition and calling interface (EJAB-694) + * src/ejabberd_commands.hrl: Likewise + + * src/mod_proxy65/mod_proxy65.erl: Update so the listener starts + correctly (EJAB-303) + * src/mod_proxy65/mod_proxy65_service.erl: Likewise + + * src/ejabberd_app.erl: Start listeners explicitely at server + start after everything else (EJAB-303). Implement support in + ejabberd for 'independent listeners', which handle their + connections themselves: gen_tcp:listen, etc. + * src/ejabberd_listener.erl: Likewise + * src/ejabberd_socket.erl: Likewise + * src/ejabberd_sup.erl: Likewise + 2008-10-10 Jean-Sébastien Pédron * src/ejabberd_c2s.erl (is_auth_packet/1): Fix a bug where @@ -63,6 +129,11 @@ * src/mod_private.erl, src/mod_private_odbc.erl, src/mod_version.erl: Convert to exmpp. Thanks to Pablo Polvorin! +2008-10-07 Christophe Romain + + * src/mod_pubsub/mod_pubsub.erl: uncomment pubsub_publish_item hook + call (EJAB-765) + 2008-10-07 Jerome Sautret * src/mod_roster_odbc.erl: fix MySQL multiple requests issue. diff --git a/doc/guide.html b/doc/guide.html index 320d0db62..c28115c21 100644 --- a/doc/guide.html +++ b/doc/guide.html @@ -413,8 +413,8 @@ you can execute ejabberdctl with either that system account or root.

ejabberdctl start
 
 ejabberdctl status
-Node ejabberd@localhost is started. Status: started
-ejabberd is running
+The node ejabberd@localhost is started with status: started
+ejabberd is running in that node
 
 ejabberdctl stop
 

If ejabberd doesn’t start correctly and a crash dump is generated, @@ -2894,7 +2894,8 @@ the available parameters are: ejabberdctl shows all the available commands in that server. The more interesting ones are:

-status
Check the status of the ejabberd server. +help
Get help about ejabberdctl or any available command. Try ejabberdctl help help. +
status
Check the status of the ejabberd server.
stop
Stop the ejabberd server which is running in the machine.
reopen-log
If you use a tool to rotate logs, you have to configure it so that this command is executed after each rotation. diff --git a/doc/guide.tex b/doc/guide.tex index 6c62769ae..14cc4c627 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -425,8 +425,8 @@ Usage example: ejabberdctl start ejabberdctl status -Node ejabberd@localhost is started. Status: started -ejabberd is running +The node ejabberd@localhost is started with status: started +ejabberd is running in that node ejabberdctl stop \end{verbatim} @@ -3712,6 +3712,7 @@ If there is an \ejabberd{} server running in the system, \term{ejabberdctl} shows all the available commands in that server. The more interesting ones are: \begin{description} +\titem{help} Get help about ejabberdctl or any available command. Try \term{ejabberdctl help help}. \titem{status} Check the status of the \ejabberd{} server. \titem{stop} Stop the \ejabberd{} server which is running in the machine. \titem{reopen-log} If you use a tool to rotate logs, you have to configure it diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index a0f101703..e08765fff 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -1,11 +1,7 @@ %%%------------------------------------------------------------------- %%% File : ejabberd_admin.erl %%% Author : Mickael Remond -%%% Description : This module gathers admin functions used by different -%%% access method: -%%% - ejabberdctl command-line tool -%%% - web admin interface -%%% - adhoc mode +%%% Purpose : Administrative functions and commands %%% Created : 7 May 2006 by Mickael Remond %%% %%% @@ -31,9 +27,237 @@ -module(ejabberd_admin). -author('mickael.remond@process-one.net'). --export([restore/1]). +-export([start/0, stop/0, + %% Server + status/0, reopen_log/0, + %% Accounts + register/3, unregister/2, + registered_users/1, + %% Migration + import_file/1, import_dir/1, + %% Purge DB + delete_expired_messages/0, delete_old_messages/1, + %% Mnesia + backup_mnesia/1, restore_mnesia/1, + dump_mnesia/1, load_mnesia/1, + install_fallback_mnesia/1, + dump_to_textfile/1, + restore/1 % Still used by some modules + ]). -include("ejabberd.hrl"). +-include("ejabberd_commands.hrl"). + +start() -> + ejabberd_commands:register_commands(commands()). + +stop() -> + ejabberd_commands:unregister_commands(commands()). + +%%% +%%% ejabberd commands +%%% + +commands() -> + [ + %% The commands status, stop and restart are implemented also in ejabberd_ctl + %% They are defined here so that other interfaces can use them too + #ejabberd_commands{name = status, tags = [server], + desc = "Get status of the ejabberd server", + module = ?MODULE, function = status, + args = [], result = {res, restuple}}, + #ejabberd_commands{name = stop, tags = [server], + desc = "Stop ejabberd gracefully", + module = init, function = stop, + args = [], result = {res, rescode}}, + #ejabberd_commands{name = restart, tags = [server], + desc = "Restart ejabberd gracefully", + module = init, function = restart, + args = [], result = {res, rescode}}, + #ejabberd_commands{name = reopen_log, tags = [logs, server], + desc = "Reopen the log files", + module = ?MODULE, function = reopen_log, + args = [], result = {res, rescode}}, + + #ejabberd_commands{name = register, tags = [accounts], + desc = "Register a user", + module = ?MODULE, function = register, + args = [{user, string}, {host, string}, {password, string}], + result = {res, restuple}}, + #ejabberd_commands{name = unregister, tags = [accounts], + desc = "Unregister a user", + module = ?MODULE, function = unregister, + args = [{user, string}, {host, string}], + result = {res, restuple}}, + #ejabberd_commands{name = registered_users, tags = [accounts], + desc = "List all registered users in HOST", + module = ?MODULE, function = registered_users, + args = [{host, string}], + result = {users, {list, {username, string}}}}, + + #ejabberd_commands{name = import_file, tags = [mnesia], + desc = "Import user data from jabberd-1.4 spool file", + module = ?MODULE, function = import_file, + args = [{file, string}], result = {res, restuple}}, + #ejabberd_commands{name = import_dir, tags = [mnesia], + desc = "Import user data from jabberd-1.4 spool dir", + module = ?MODULE, function = import_dir, + args = [{file, string}], + result = {res, restuple}}, + + #ejabberd_commands{name = delete_expired_messages, tags = [purge], + desc = "Delete expired offline messages from database", + module = ?MODULE, function = delete_expired_messages, + args = [], result = {res, rescode}}, + #ejabberd_commands{name = delete_old_messages, tags = [purge], + desc = "Delete offline messages older than DAYS", + module = ?MODULE, function = delete_old_messages, + args = [{days, integer}], result = {res, rescode}}, + + #ejabberd_commands{name = backup, tags = [mnesia], + desc = "Store the database to backup file", + module = ?MODULE, function = backup_mnesia, + args = [{file, string}], result = {res, restuple}}, + #ejabberd_commands{name = restore, tags = [mnesia], + desc = "Restore the database from backup file", + module = ?MODULE, function = restore_mnesia, + args = [{file, string}], result = {res, restuple}}, + #ejabberd_commands{name = dump, tags = [mnesia], + desc = "Dump the database to text file", + module = ?MODULE, function = dump_mnesia, + args = [{file, string}], result = {res, restuple}}, + #ejabberd_commands{name = load, tags = [mnesia], + desc = "Restore the database from text file", + module = ?MODULE, function = load_mnesia, + args = [{file, string}], result = {res, restuple}}, + #ejabberd_commands{name = install_fallback, tags = [mnesia], + desc = "Install the database from a fallback file", + module = ?MODULE, function = install_fallback_mnesia, + args = [{file, string}], result = {res, restuple}} + ]. + + +%%% +%%% Server management +%%% + +status() -> + {InternalStatus, ProvidedStatus} = init:get_status(), + String1 = io_lib:format("The node ~p is ~p. Status: ~p", + [node(), InternalStatus, ProvidedStatus]), + {Is_running, String2} = + case lists:keysearch(ejabberd, 1, application:which_applications()) of + false -> + {ejabberd_not_running, "ejabberd is not running in that node."}; + {value, {_, _, Version}} -> + {ok, io_lib:format("ejabberd ~s is running in that node", [Version])} + end, + {Is_running, String1 ++ String2}. + +reopen_log() -> + ejabberd_hooks:run(reopen_log_hook, []), + %% TODO: Use the Reopen log API for logger_h ? + ejabberd_logger_h:reopen_log(), + ok. + + +%%% +%%% Account management +%%% + +register(User, Host, Password) -> + case ejabberd_auth:try_register(User, Host, Password) of + {atomic, ok} -> + {ok, io_lib:format("User ~s@~s succesfully registered", [User, Host])}; + {atomic, exists} -> + String = io_lib:format("User ~s@~s already registered at node ~p", + [User, Host, node()]), + {exists, String}; + {error, Reason} -> + String = io_lib:format("Can't register user ~s@~s at node ~p: ~p", + [User, Host, node(), Reason]), + {cannot_register, String} + end. + +unregister(User, Host) -> + ejabberd_auth:remove_user(User, Host), + {ok, ""}. + +registered_users(Host) -> + Users = ejabberd_auth:get_vh_registered_users(Host), + SUsers = lists:sort(Users), + lists:map(fun({U, _S}) -> U end, SUsers). + + +%%% +%%% Migration management +%%% + +import_file(Path) -> + case jd2ejd:import_file(Path) of + ok -> + {ok, ""}; + {error, Reason} -> + String = io_lib:format("Can't import jabberd 1.4 spool file ~p at node ~p: ~p", + [filename:absname(Path), node(), Reason]), + {cannot_import_file, String} + end. + +import_dir(Path) -> + case jd2ejd:import_dir(Path) of + ok -> + {ok, ""}; + {error, Reason} -> + String = io_lib:format("Can't import jabberd 1.4 spool dir ~p at node ~p: ~p", + [filename:absname(Path), node(), Reason]), + {cannot_import_dir, String} + end. + + +%%% +%%% Purge DB +%%% + +delete_expired_messages() -> + {atomic, ok} = mod_offline:remove_expired_messages(), + ok. + +delete_old_messages(Days) -> + {atomic, _} = mod_offline:remove_old_messages(Days), + ok. + + +%%% +%%% Mnesia management +%%% + +backup_mnesia(Path) -> + case mnesia:backup(Path) of + ok -> + {ok, ""}; + {error, Reason} -> + String = io_lib:format("Can't store backup in ~p at node ~p: ~p", + [filename:absname(Path), node(), Reason]), + {cannot_backup, String} + end. + +restore_mnesia(Path) -> + case ejabberd_admin:restore(Path) of + {atomic, _} -> + {ok, ""}; + {error, Reason} -> + String = io_lib:format("Can't restore backup from ~p at node ~p: ~p", + [filename:absname(Path), node(), Reason]), + {cannot_restore, String}; + {aborted,{no_exists,Table}} -> + String = io_lib:format("Can't restore backup from ~p at node ~p: Table ~p does not exist.", + [filename:absname(Path), node(), Table]), + {table_not_exists, String}; + {aborted,enoent} -> + String = io_lib:format("Can't restore backup from ~p at node ~p: File not found.", + [filename:absname(Path), node()]), + {file_not_found, String} + end. %% Mnesia database restore %% This function is called from ejabberd_ctl, ejabberd_web_admin and @@ -71,3 +295,67 @@ module_tables(mod_roster) -> [roster]; module_tables(mod_shared_roster) -> [sr_group, sr_user]; module_tables(mod_vcard) -> [vcard, vcard_search]; module_tables(_Other) -> []. + +dump_mnesia(Path) -> + case dump_to_textfile(Path) of + ok -> + {ok, ""}; + {error, Reason} -> + String = io_lib:format("Can't store dump in ~p at node ~p: ~p", + [filename:absname(Path), node(), Reason]), + {cannot_dump, String} + end. + +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}. + +dump_tab(F, T) -> + W = mnesia:table_info(T, wild_pattern), + {atomic,All} = mnesia:transaction( + fun() -> mnesia:match_object(T, W, read) end), + lists:foreach( + fun(Term) -> io:format(F,"~p.~n", [setelement(1, Term, T)]) end, All). + +load_mnesia(Path) -> + case mnesia:load_textfile(Path) of + {atomic, ok} -> + {ok, ""}; + {error, Reason} -> + String = io_lib:format("Can't load dump in ~p at node ~p: ~p", + [filename:absname(Path), node(), Reason]), + {cannot_load, String} + end. + +install_fallback_mnesia(Path) -> + case mnesia:install_fallback(Path) of + ok -> + {ok, ""}; + {error, Reason} -> + String = io_lib:format("Can't install fallback from ~p at node ~p: ~p", + [filename:absname(Path), node(), Reason]), + {cannot_fallback, String} + end. + diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 75e026c66..928dd696c 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -29,7 +29,7 @@ -behaviour(application). --export([start/2, prep_stop/1, stop/1, init/0]). +-export([start_modules/0,start/2, prep_stop/1, stop/1, init/0]). -include("ejabberd.hrl"). @@ -47,6 +47,8 @@ start(normal, _Args) -> translate:start(), acl:start(), ejabberd_ctl:init(), + ejabberd_commands:init(), + ejabberd_admin:start(), gen_mod:start(), ejabberd_config:start(), ejabberd_check:config(), @@ -61,6 +63,7 @@ start(normal, _Args) -> %eprof:profile([self()]), %fprof:trace(start, "/tmp/fprof"), start_modules(), + ejabberd_listener:start_listeners(), Sup; start(_, _) -> {error, badarg}. @@ -70,6 +73,7 @@ start(_, _) -> %% before shutting down the processes of the application. prep_stop(State) -> stop_modules(), + ejabberd_admin:stop(), State. %% All the processes were killed when this function is called diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index 33a575e30..2cefb6eab 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -49,14 +49,12 @@ is_user_exists_in_other_modules/3, remove_user/2, remove_user/3, - plain_password_required/1, - ctl_process_get_registered/3 + plain_password_required/1 ]). -export([auth_modules/1]). -include("ejabberd.hrl"). --include("ejabberd_ctl.hrl"). %%%---------------------------------------------------------------------- %%% API @@ -265,15 +263,6 @@ remove_user(User, Server, Password) -> M:remove_user(User, Server, Password) end, auth_modules(Server)). -ctl_process_get_registered(_Val, Host, ["registered-users"]) -> - Users = ejabberd_auth:get_vh_registered_users(Host), - NewLine = io_lib:format("~n", []), - SUsers = lists:sort(Users), - FUsers = lists:map(fun({U, _S}) -> [U, NewLine] end, SUsers), - ?PRINT("~s", [FUsers]), - {stop, ?STATUS_SUCCESS}; -ctl_process_get_registered(Val, _Host, _Args) -> - Val. %%%---------------------------------------------------------------------- %%% Internal functions diff --git a/src/ejabberd_auth_internal.erl b/src/ejabberd_auth_internal.erl index 1abc9f77c..ab49f89f9 100644 --- a/src/ejabberd_auth_internal.erl +++ b/src/ejabberd_auth_internal.erl @@ -53,14 +53,10 @@ %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- -start(Host) -> +start(_Host) -> mnesia:create_table(passwd, [{disc_copies, [node()]}, {attributes, record_info(fields, passwd)}]), update_table(), - ejabberd_ctl:register_commands( - Host, - [{"registered-users", "list all registered users"}], - ejabberd_auth, ctl_process_get_registered), ok. plain_password_required() -> diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index 303300e48..b4128a9d1 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -112,11 +112,8 @@ start_link(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:start_link({local, Proc}, ?MODULE, Host, []). -terminate(_Reason, State) -> - ejabberd_ctl:unregister_commands( - State#state.host, - [{"registered-users", "list all registered users"}], - ejabberd_auth, ctl_process_get_registered). +terminate(_Reason, _State) -> + ok. init(Host) -> State = parse_options(Host), @@ -132,10 +129,6 @@ init(Host) -> State#state.port, State#state.dn, State#state.password), - ejabberd_ctl:register_commands( - Host, - [{"registered-users", "list all registered users"}], - ejabberd_auth, ctl_process_get_registered), {ok, State}. plain_password_required() -> diff --git a/src/ejabberd_auth_odbc.erl b/src/ejabberd_auth_odbc.erl index 0daa24f8f..1938427fb 100644 --- a/src/ejabberd_auth_odbc.erl +++ b/src/ejabberd_auth_odbc.erl @@ -51,11 +51,7 @@ %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- -start(Host) -> - ejabberd_ctl:register_commands( - Host, - [{"registered-users", "list all registered users"}], - ejabberd_auth, ctl_process_get_registered), +start(_Host) -> ok. plain_password_required() -> diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl new file mode 100644 index 000000000..6440959a4 --- /dev/null +++ b/src/ejabberd_commands.erl @@ -0,0 +1,328 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_commands.erl +%%% Author : Badlop +%%% Purpose : Management of ejabberd commands +%%% Created : 20 May 2008 by Badlop +%%% +%%% +%%% 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)] )]).