Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/processone/ejabberd.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.in3
-rw-r--r--include/ejabberd_commands.hrl6
-rw-r--r--rebar.config2
-rw-r--r--src/ejabberd_commands.erl293
-rw-r--r--src/ejabberd_ctl.erl141
-rw-r--r--src/mod_admin_extra.erl875
-rw-r--r--src/mod_http_api.erl160
-rw-r--r--test/ejabberd_admin_test.exs79
-rw-r--r--test/ejabberd_auth_mock.exs57
-rw-r--r--test/ejabberd_commands_test.exs437
-rw-r--r--test/ejabberd_hooks_test.exs6
-rw-r--r--test/ejabberd_oauth_mock.exs30
-rw-r--r--test/ejabberd_sm_mock.exs106
-rw-r--r--test/elixir_SUITE.erl28
-rw-r--r--test/mod_admin_extra_test.exs699
-rw-r--r--test/mod_http_api_test.exs188
-rw-r--r--test/mod_last_mock.exs65
-rw-r--r--test/mod_roster_mock.exs192
18 files changed, 2967 insertions, 400 deletions
diff --git a/Makefile.in b/Makefile.in
index 0d9134485..28c05166e 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -336,6 +336,9 @@ test:
quicktest:
$(REBAR) skip_deps=true ct suites=elixir
+eunit:
+ $(REBAR) skip_deps=true exunit
+
.PHONY: src edoc dialyzer Makefile TAGS clean clean-rel distclean rel \
install uninstall uninstall-binary uninstall-all translations deps test spec \
quicktest erlang_plt deps_plt ejabberd_plt
diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl
index 0742e3ba4..5874b3d26 100644
--- a/include/ejabberd_commands.hrl
+++ b/include/ejabberd_commands.hrl
@@ -31,8 +31,10 @@
tags = [] :: [atom()] | '_' | '$2',
desc = "" :: string() | '_' | '$3',
longdesc = "" :: string() | '_',
- module :: atom(),
- function :: atom(),
+ version = 0 :: integer(),
+ jabs = 1 :: integer(),
+ module :: atom() | '_',
+ function :: atom() | '_',
args = [] :: [aterm()] | '_' | '$1' | '$2',
policy = restricted :: open | restricted | admin | user,
result = {res, rescode} :: rterm() | '_' | '$2',
diff --git a/rebar.config b/rebar.config
index 210e623ae..dd88a4e5c 100644
--- a/rebar.config
+++ b/rebar.config
@@ -43,6 +43,8 @@
{tag, "1.0.0"}}}},
{if_var_true, tools, {meck, "0.8.2", {git, "https://github.com/eproxus/meck",
{tag, "0.8.2"}}}},
+ {if_var_true, tools, {moka, ".*", {git, "git://github.com/processone/moka.git",
+ {tag, "1.0.5"}}}},
{if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis",
{tag, "v1.0.8"}}}}]}.
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
index 21872aa33..fd8ba03fe 100644
--- a/src/ejabberd_commands.erl
+++ b/src/ejabberd_commands.erl
@@ -90,7 +90,8 @@
%%% PowFloat = math:pow(Base, Exponent),
%%% round(PowFloat).</pre>
%%%
-%%% Since this function will be called by ejabberd_commands, it must be exported.
+%%% Since this function will be called by ejabberd_commands, it must
+%%% be exported.
%%% Add to your module:
%%% <pre>-export([calc_power/2]).</pre>
%%%
@@ -201,24 +202,33 @@
%%% 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.
+%%% 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').
+-define(DEFAULT_VERSION, 1000000).
+
-export([init/0,
list_commands/0,
+ list_commands/1,
get_command_format/1,
- get_command_format/2,
+ get_command_format/2,
+ get_command_format/3,
get_command_definition/1,
+ get_command_definition/2,
get_tags_commands/0,
+ get_tags_commands/1,
get_commands/0,
register_commands/1,
unregister_commands/1,
execute_command/2,
- execute_command/4,
+ execute_command/3,
+ execute_command/4,
+ execute_command/5,
opt_type/1,
get_commands_spec/0
]).
@@ -226,6 +236,7 @@
-include("ejabberd_commands.hrl").
-include("ejabberd.hrl").
-include("logger.hrl").
+-include_lib("stdlib/include/ms_transform.hrl").
-define(POLICY_ACCESS, '$policy').
@@ -260,23 +271,26 @@ get_commands_spec() ->
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
result_example = ok}].
init() ->
- ets:new(ejabberd_commands, [named_table, set, public,
- {keypos, #ejabberd_commands.name}]),
+ mnesia:delete_table(ejabberd_commands),
+ mnesia:create_table(ejabberd_commands,
+ [{ram_copies, [node()]},
+ {local_content, true},
+ {attributes, record_info(fields, ejabberd_commands)},
+ {type, bag}]),
+ mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
register_commands(get_commands_spec()).
-spec register_commands([ejabberd_commands()]) -> ok.
%% @doc Register ejabberd commands.
-%% If a command is already registered, a warning is printed and the old command is preserved.
+%% 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 ->
- ?DEBUG("This command is already defined:~n~p", [Command])
- end
+ % XXX check if command exists
+ mnesia:dirty_write(Command)
+ % ?DEBUG("This command is already defined:~n~p", [Command])
end,
Commands).
@@ -286,7 +300,7 @@ register_commands(Commands) ->
unregister_commands(Commands) ->
lists:foreach(
fun(Command) ->
- ets:delete_object(ejabberd_commands, Command)
+ mnesia:dirty_delete_object(Command)
end,
Commands).
@@ -294,94 +308,183 @@ unregister_commands(Commands) ->
%% @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 list_commands_policy() -> [{atom(), [aterm()], string(), atom()}].
-
-%% @doc Get a list of all the available commands, arguments, description, and
-%% policy.
-list_commands_policy() ->
- Commands = ets:match(ejabberd_commands,
- #ejabberd_commands{name = '$1',
- args = '$2',
- desc = '$3',
- policy = '$4',
- _ = '_'}),
- [{A, B, C, D} || [A, B, C, D] <- Commands].
-
--spec get_command_format(atom()) -> {[aterm()], rterm()} | {error, command_unknown}.
+ list_commands(?DEFAULT_VERSION).
+
+-spec list_commands(integer()) -> [{atom(), [aterm()], string()}].
+
+%% @doc Get a list of all the available commands, arguments and
+%% description in a given API verion.
+list_commands(Version) ->
+ Commands = get_commands_definition(Version),
+ [{Name, Args, Desc} || #ejabberd_commands{name = Name,
+ args = Args,
+ desc = Desc} <- Commands].
+
+
+-spec list_commands_policy(integer()) ->
+ [{atom(), [aterm()], string(), atom()}].
+
+%% @doc Get a list of all the available commands, arguments,
+%% description, and policy in a given API version.
+list_commands_policy(Version) ->
+ Commands = get_commands_definition(Version),
+ [{Name, Args, Desc, Policy} ||
+ #ejabberd_commands{name = Name,
+ args = Args,
+ desc = Desc,
+ policy = Policy} <- Commands].
+
+-spec get_command_format(atom()) -> {[aterm()], rterm()}.
%% @doc Get the format of arguments and result of a command.
get_command_format(Name) ->
- get_command_format(Name, noauth).
-
-get_command_format(Name, Auth) ->
+ get_command_format(Name, noauth, ?DEFAULT_VERSION).
+get_command_format(Name, Version) when is_integer(Version) ->
+ get_command_format(Name, noauth, Version);
+get_command_format(Name, Auth) ->
+ get_command_format(Name, Auth, ?DEFAULT_VERSION).
+
+-spec get_command_format(atom(),
+ {binary(), binary(), binary(), boolean()} |
+ noauth | admin,
+ integer()) ->
+ {[aterm()], rterm()}.
+
+get_command_format(Name, Auth, Version) ->
Admin = is_admin(Name, Auth),
- Matched = ets:match(ejabberd_commands,
- #ejabberd_commands{name = Name,
- args = '$1',
- result = '$2',
- policy = '$3',
- _ = '_'}),
- case Matched of
- [] ->
- {error, command_unknown};
- [[Args, Result, user]] when Admin;
- Auth == noauth ->
+ #ejabberd_commands{args = Args,
+ result = Result,
+ policy = Policy} =
+ get_command_definition(Name, Version),
+ case Policy of
+ user when Admin;
+ Auth == noauth ->
{[{user, binary}, {server, binary} | Args], Result};
- [[Args, Result, _]] ->
+ _ ->
{Args, Result}
end.
--spec get_command_definition(atom()) -> ejabberd_commands() | command_not_found.
+-spec get_command_definition(atom()) -> ejabberd_commands().
%% @doc Get the definition record of a command.
get_command_definition(Name) ->
- case ets:lookup(ejabberd_commands, Name) of
- [E] -> E;
- [] -> command_not_found
+ get_command_definition(Name, ?DEFAULT_VERSION).
+
+-spec get_command_definition(atom(), integer()) -> ejabberd_commands().
+
+%% @doc Get the definition record of a command in a given API version.
+get_command_definition(Name, Version) ->
+ case lists:reverse(
+ lists:sort(
+ mnesia:dirty_select(
+ ejabberd_commands,
+ ets:fun2ms(
+ fun(#ejabberd_commands{name = N, version = V} = C)
+ when N == Name, V =< Version ->
+ {V, C}
+ end)))) of
+ [{_, Command} | _ ] -> Command;
+ _E -> throw(unknown_command)
end.
-%% @spec (Name::atom(), Arguments) -> ResultTerm | {error, command_unknown}
+-spec get_commands_definition(integer()) -> [ejabberd_commands()].
+
+% @doc Returns all commands for a given API version
+get_commands_definition(Version) ->
+ L = lists:reverse(
+ lists:sort(
+ mnesia:dirty_select(
+ ejabberd_commands,
+ ets:fun2ms(
+ fun(#ejabberd_commands{name = Name, version = V} = C)
+ when V =< Version ->
+ {Name, V, C}
+ end)))),
+ F = fun({_Name, _V, Command}, []) ->
+ [Command];
+ ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
+ Acc;
+ ({_Name, _V, Command}, Acc) -> [Command | Acc]
+ end,
+ lists:foldl(F, [], L).
+
+%% @spec (Name::atom(), Arguments) -> ResultTerm
+%% where
+%% Arguments = [any()]
%% @doc Execute a command.
+%% Can return the following exceptions:
+%% command_unknown | account_unprivileged | invalid_account_data |
+%% no_auth_provided
execute_command(Name, Arguments) ->
- execute_command([], noauth, Name, Arguments).
+ execute_command(Name, Arguments, ?DEFAULT_VERSION).
+
+-spec execute_command(atom(),
+ [any()],
+ integer() |
+ {binary(), binary(), binary(), boolean()} |
+ noauth | admin
+ ) -> any().
+
+%% @spec (Name::atom(), Arguments, integer() | Auth) -> ResultTerm
+%% where
+%% Auth = {User::string(), Server::string(), Password::string(),
+%% Admin::boolean()}
+%% | noauth
+%% | admin
+%% Arguments = [any()]
+%%
+%% @doc Execute a command in a given API version
+%% Can return the following exceptions:
+%% command_unknown | account_unprivileged | invalid_account_data |
+%% no_auth_provided
+execute_command(Name, Arguments, Version) when is_integer(Version) ->
+ execute_command([], noauth, Name, Arguments, Version);
+execute_command(Name, Arguments, Auth) ->
+ execute_command([], Auth, Name, Arguments, ?DEFAULT_VERSION).
+
+%% @spec (AccessCommands, Auth, Name::atom(), Arguments) ->
+%% ResultTerm | {error, Error}
+%% where
+%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined
+%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
+%% | noauth
+%% | admin
+%% Arguments = [any()]
+%%
+%% @doc Execute a command
+%% Can return the following exceptions:
+%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
+execute_command(AccessCommands, Auth, Name, Arguments) ->
+ execute_command(AccessCommands, Auth, Name, Arguments, ?DEFAULT_VERSION).
-spec execute_command([{atom(), [atom()], [any()]}] | undefined,
{binary(), binary(), binary(), boolean()} |
noauth | admin,
atom(),
- [any()]
+ [any()],
+ integer()
) -> any().
-%% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> ResultTerm | {error, Error}
+%% @spec (AccessCommands, Auth, Name::atom(), Arguments, integer()) -> ResultTerm
%% where
%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined
%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
%% | noauth
%% | admin
-%% Method = atom()
%% Arguments = [any()]
-%% Error = command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
-execute_command(AccessCommands1, Auth1, Name, Arguments) ->
+%%
+%% @doc Execute a command in a given API version
+%% Can return the following exceptions:
+%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
+execute_command(AccessCommands1, Auth1, Name, Arguments, Version) ->
Auth = case is_admin(Name, Auth1) of
true -> admin;
false -> Auth1
end,
- case ets:lookup(ejabberd_commands, Name) of
- [Command] ->
- AccessCommands = get_access_commands(AccessCommands1),
- try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of
- ok -> execute_command2(Auth, Command, Arguments)
- catch
- {error, Error} -> {error, Error}
- end;
- [] -> {error, command_unknown}
+ Command = get_command_definition(Name, Version),
+ AccessCommands = get_access_commands(AccessCommands1, Version),
+ case check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of
+ ok -> execute_command2(Auth, Command, Arguments)
end.
execute_command2(
@@ -407,26 +510,25 @@ execute_command2(Command, Arguments) ->
Module = Command#ejabberd_commands.module,
Function = Command#ejabberd_commands.function,
?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]),
- try apply(Module, Function, Arguments) of
- Response ->
- Response
- catch
- Problem ->
- {error, Problem}
- end.
+ apply(Module, Function, Arguments).
-spec get_tags_commands() -> [{string(), [string()]}].
%% @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',
- _ = '_'}),
+ get_tags_commands(?DEFAULT_VERSION).
+
+-spec get_tags_commands(integer()) -> [{string(), [string()]}].
+
+%% @spec (integer) -> [{Tag::string(), [CommandName::string()]}]
+%% @doc Get all the tags and associated commands in a given API version
+get_tags_commands(Version) ->
+ CommandTags = [{Name, Tags} ||
+ #ejabberd_commands{name = Name, tags = Tags}
+ <- get_commands_definition(Version)],
Dict = lists:foldl(
- fun([CommandNameAtom, CTags], D) ->
+ fun({CommandNameAtom, CTags}, D) ->
CommandName = atom_to_list(CommandNameAtom),
case CTags of
[] ->
@@ -445,7 +547,6 @@ get_tags_commands() ->
CommandTags),
orddict:to_list(Dict).
-
%% -----------------------------
%% Access verification
%% -----------------------------
@@ -479,7 +580,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) ->
fun({Access, Commands, ArgumentRestrictions}) ->
case check_access(Command, Access, Auth) of
true ->
- check_access_command(Commands, Command, ArgumentRestrictions,
+ check_access_command(Commands, Command,
+ ArgumentRestrictions,
Method, Arguments);
false ->
false
@@ -488,7 +590,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) ->
ArgumentRestrictions = [],
case check_access(Command, Access, Auth) of
true ->
- check_access_command(Commands, Command, ArgumentRestrictions,
+ check_access_command(Commands, Command,
+ ArgumentRestrictions,
Method, Arguments);
false ->
false
@@ -551,9 +654,11 @@ check_access2(Access, User, Server) ->
deny -> false
end.
-check_access_command(Commands, Command, ArgumentRestrictions, Method, Arguments) ->
+check_access_command(Commands, Command, ArgumentRestrictions,
+ Method, Arguments) ->
case Commands==all orelse lists:member(Method, Commands) of
- true -> check_access_arguments(Command, ArgumentRestrictions, Arguments);
+ true -> check_access_arguments(Command, ArgumentRestrictions,
+ Arguments);
false -> false
end.
@@ -577,18 +682,20 @@ tag_arguments(ArgsDefs, Args) ->
Args).
-get_access_commands(undefined) ->
- Cmds = get_commands(),
+get_access_commands(undefined, Version) ->
+ Cmds = get_commands(Version),
[{?POLICY_ACCESS, Cmds, []}];
-get_access_commands(AccessCommands) ->
+get_access_commands(AccessCommands, _Version) ->
AccessCommands.
get_commands() ->
+ get_commands(?DEFAULT_VERSION).
+get_commands(Version) ->
Opts = ejabberd_config:get_option(
commands,
fun(V) when is_list(V) -> V end,
[]),
- CommandsList = list_commands_policy(),
+ CommandsList = list_commands_policy(Version),
OpenCmds = [N || {N, _, _, open} <- CommandsList],
RestrictedCmds = [N || {N, _, _, restricted} <- CommandsList],
AdminCmds = [N || {N, _, _, admin} <- CommandsList],
diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl
index bf4e4675a..edec5a07e 100644
--- a/src/ejabberd_ctl.erl
+++ b/src/ejabberd_ctl.erl
@@ -48,7 +48,7 @@
-behaviour(ejabberd_config).
-author('alexey@process-one.net').
--export([start/0, init/0, process/1, process2/2,
+-export([start/0, init/0, process/1,
register_commands/3, unregister_commands/3,
opt_type/1]).
@@ -57,6 +57,8 @@
-include("ejabberd.hrl").
-include("logger.hrl").
+-define(DEFAULT_VERSION, 1000000).
+
%%-----------------------------
%% Module
@@ -69,7 +71,7 @@ start() ->
[SNode3 | Args3] ->
[SNode3, 60000, Args3];
_ ->
- print_usage(),
+ print_usage(?DEFAULT_VERSION),
halt(?STATUS_USAGE)
end,
SNode1 = case string:tokens(SNode, "@") of
@@ -93,6 +95,9 @@ start() ->
[Node, Reason]),
%% TODO: show minimal start help
?STATUS_BADRPC;
+ {invalid_version, V} ->
+ print("Invalid API version number: ~p~n", [V]),
+ ?STATUS_ERROR;
S ->
S
end,
@@ -126,11 +131,17 @@ unregister_commands(CmdDescs, Module, Function) ->
%% Process
%%-----------------------------
+
-spec process([string()]) -> non_neg_integer().
+process(Args) ->
+ process(Args, ?DEFAULT_VERSION).
+
+
+-spec process([string()], non_neg_integer()) -> non_neg_integer().
%% The commands status, stop and restart are defined here to ensure
%% they are usable even if ejabberd is completely stopped.
-process(["status"]) ->
+process(["status"], _Version) ->
{InternalStatus, ProvidedStatus} = init:get_status(),
print("The node ~p is ~p with status: ~p~n",
[node(), InternalStatus, ProvidedStatus]),
@@ -146,24 +157,24 @@ process(["status"]) ->
?STATUS_SUCCESS
end;
-process(["stop"]) ->
+process(["stop"], _Version) ->
%%ejabberd_cover:stop(),
init:stop(),
?STATUS_SUCCESS;
-process(["restart"]) ->
+process(["restart"], _Version) ->
init:restart(),
?STATUS_SUCCESS;
-process(["mnesia"]) ->
+process(["mnesia"], _Version) ->
print("~p~n", [mnesia:system_info(all)]),
?STATUS_SUCCESS;
-process(["mnesia", "info"]) ->
+process(["mnesia", "info"], _Version) ->
mnesia:info(),
?STATUS_SUCCESS;
-process(["mnesia", Arg]) ->
+process(["mnesia", Arg], _Version) ->
case catch mnesia:system_info(list_to_atom(Arg)) of
{'EXIT', Error} -> print("Error: ~p~n", [Error]);
Return -> print("~p~n", [Return])
@@ -172,23 +183,23 @@ process(["mnesia", Arg]) ->
%% 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]) ->
+process(["help" | Mode], Version) ->
{MaxC, ShCode} = get_shell_info(),
case Mode of
[] ->
- print_usage(dual, MaxC, ShCode),
+ print_usage(dual, MaxC, ShCode, Version),
?STATUS_USAGE;
["--dual"] ->
- print_usage(dual, MaxC, ShCode),
+ print_usage(dual, MaxC, ShCode, Version),
?STATUS_USAGE;
["--long"] ->
- print_usage(long, MaxC, ShCode),
+ print_usage(long, MaxC, ShCode, Version),
?STATUS_USAGE;
["--tags"] ->
- print_usage_tags(MaxC, ShCode),
+ print_usage_tags(MaxC, ShCode, Version),
?STATUS_SUCCESS;
["--tags", Tag] ->
- print_usage_tags(Tag, MaxC, ShCode),
+ print_usage_tags(Tag, MaxC, ShCode, Version),
?STATUS_SUCCESS;
["help"] ->
print_usage_help(MaxC, ShCode),
@@ -196,13 +207,22 @@ process(["help" | Mode]) ->
[CmdString | _] ->
CmdStringU = ejabberd_regexp:greplace(
list_to_binary(CmdString), <<"-">>, <<"_">>),
- print_usage_commands(binary_to_list(CmdStringU), MaxC, ShCode),
+ print_usage_commands2(binary_to_list(CmdStringU), MaxC, ShCode, Version),
?STATUS_SUCCESS
end;
-process(Args) ->
+process(["--version", Arg | Args], _) ->
+ Version =
+ try
+ list_to_integer(Arg)
+ catch _:_ ->
+ throw({invalid_version, Arg})
+ end,
+ process(Args, Version);
+
+process(Args, Version) ->
AccessCommands = get_accesscommands(),
- {String, Code} = process2(Args, AccessCommands),
+ {String, Code} = process2(Args, AccessCommands, Version),
case String of
[] -> ok;
_ ->
@@ -211,18 +231,21 @@ process(Args) ->
Code.
%% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()}
-process2(["--auth", User, Server, Pass | Args], AccessCommands) ->
- process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass), true}, AccessCommands);
-process2(Args, AccessCommands) ->
- process2(Args, noauth, AccessCommands).
+process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) ->
+ process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server),
+ list_to_binary(Pass), true}, Version);
+process2(Args, AccessCommands, Version) ->
+ process2(Args, AccessCommands, admin, Version).
+
+
-process2(Args, Auth, AccessCommands) ->
- case try_run_ctp(Args, Auth, AccessCommands) of
+process2(Args, AccessCommands, Auth, Version) ->
+ case try_run_ctp(Args, Auth, AccessCommands, Version) of
{String, wrong_command_arguments}
when is_list(String) ->
io:format(lists:flatten(["\n" | String]++["\n"])),
[CommandString | _] = Args,
- process(["help" | [CommandString]]),
+ process(["help" | [CommandString]], Version),
{lists:flatten(String), ?STATUS_ERROR};
{String, Code}
when is_list(String) and is_integer(Code) ->
@@ -246,29 +269,29 @@ get_accesscommands() ->
%%-----------------------------
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()}
-try_run_ctp(Args, Auth, AccessCommands) ->
+try_run_ctp(Args, Auth, AccessCommands, Version) ->
try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of
false when Args /= [] ->
- try_call_command(Args, Auth, AccessCommands);
+ try_call_command(Args, Auth, AccessCommands, Version);
false ->
- print_usage(),
+ print_usage(Version),
{"", ?STATUS_USAGE};
Status ->
{"", Status}
catch
exit:Why ->
- print_usage(),
+ print_usage(Version),
{io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE};
Error:Why ->
%% In this case probably ejabberd is not started, so let's show Status
- process(["status"]),
+ process(["status"], Version),
print("~n", []),
{io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE}
end.
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()}
-try_call_command(Args, Auth, AccessCommands) ->
- try call_command(Args, Auth, AccessCommands) of
+try_call_command(Args, Auth, AccessCommands, Version) ->
+ try call_command(Args, Auth, AccessCommands, Version) of
{error, command_unknown} ->
{io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR};
{error, wrong_command_arguments} ->
@@ -276,24 +299,28 @@ try_call_command(Args, Auth, AccessCommands) ->
Res ->
Res
catch
+ throw:Error ->
+ {io_lib:format("~p", [Error]), ?STATUS_ERROR};
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()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} | {error, ErrorType}
-call_command([CmdString | Args], Auth, AccessCommands) ->
+call_command([CmdString | Args], Auth, AccessCommands, Version) ->
CmdStringU = ejabberd_regexp:greplace(
list_to_binary(CmdString), <<"-">>, <<"_">>),
Command = list_to_atom(binary_to_list(CmdStringU)),
- case ejabberd_commands:get_command_format(Command, Auth) of
+ case ejabberd_commands:get_command_format(Command, Auth, Version) 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(AccessCommands, Auth, Command,
- ArgsFormatted),
+ Result = ejabberd_commands:execute_command(AccessCommands,
+ Auth, Command,
+ ArgsFormatted,
+ Version),
format_result(Result, ResultFormat);
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
{NumCompa, TextCompa} =
@@ -404,8 +431,8 @@ make_status(ok) -> ?STATUS_SUCCESS;
make_status(true) -> ?STATUS_SUCCESS;
make_status(_Error) -> ?STATUS_ERROR.
-get_list_commands() ->
- try ejabberd_commands:list_commands() of
+get_list_commands(Version) ->
+ try ejabberd_commands:list_commands(Version) of
Commands ->
[tuple_command_help(Command)
|| {N,_,_}=Command <- Commands,
@@ -458,10 +485,10 @@ get_list_ctls() ->
-define(U2, "\e[24m").
-define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end).
-print_usage() ->
+print_usage(Version) ->
{MaxC, ShCode} = get_shell_info(),
- print_usage(dual, MaxC, ShCode).
-print_usage(HelpMode, MaxC, ShCode) ->
+ print_usage(dual, MaxC, ShCode, Version).
+print_usage(HelpMode, MaxC, ShCode, Version) ->
AllCommands =
[
{"status", [], "Get ejabberd status"},
@@ -469,11 +496,11 @@ print_usage(HelpMode, MaxC, ShCode) ->
{"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_commands(Version) ++
get_list_ctls(),
print(
- ["Usage: ", ?B("ejabberdctl"), " [--no-timeout] [--node ", ?U("nodename"), "] [--auth ",
+ ["Usage: ", ?B("ejabberdctl"), " [--no-timeout] [--node ", ?U("nodename"), "] [--version ", ?U("api_version"), "] [--auth ",
?U("user"), " ", ?U("host"), " ", ?U("password"), "] ",
?U("command"), " [", ?U("options"), "]\n"
"\n"
@@ -598,9 +625,9 @@ format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) ->
%% Print Tags
%%-----------------------------
-print_usage_tags(MaxC, ShCode) ->
+print_usage_tags(MaxC, ShCode, Version) ->
print("Available tags and commands:", []),
- TagsCommands = ejabberd_commands:get_tags_commands(),
+ TagsCommands = ejabberd_commands:get_tags_commands(Version),
lists:foreach(
fun({Tag, Commands} = _TagCommands) ->
print(["\n\n ", ?B(Tag), "\n "], []),
@@ -611,10 +638,10 @@ print_usage_tags(MaxC, ShCode) ->
TagsCommands),
print("\n\n", []).
-print_usage_tags(Tag, MaxC, ShCode) ->
+print_usage_tags(Tag, MaxC, ShCode, Version) ->
print(["Available commands with tag ", ?B(Tag), ":", "\n"], []),
HelpMode = long,
- TagsCommands = ejabberd_commands:get_tags_commands(),
+ TagsCommands = ejabberd_commands:get_tags_commands(Version),
CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of
{value, {Tag, CNs}} -> CNs;
false -> []
@@ -622,7 +649,7 @@ print_usage_tags(Tag, MaxC, ShCode) ->
CommandsList = lists:map(
fun(NameString) ->
C = ejabberd_commands:get_command_definition(
- list_to_atom(NameString)),
+ list_to_atom(NameString), Version),
#ejabberd_commands{name = Name,
args = Args,
desc = Desc} = C,
@@ -673,20 +700,20 @@ print_usage_help(MaxC, ShCode) ->
%%-----------------------------
%% @spec (CmdSubString::string(), MaxC::integer(), ShCode::boolean()) -> ok
-print_usage_commands(CmdSubString, MaxC, ShCode) ->
+print_usage_commands2(CmdSubString, MaxC, ShCode, Version) ->
%% Get which command names match this substring
- AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands()],
+ AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)],
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)
+ [] -> io:format("Error: no command found that match: ~p~n", [CmdSubString]);
+ _ -> print_usage_commands3(lists:sort(Cmds), MaxC, ShCode, Version)
end.
-print_usage_commands2(Cmds, MaxC, ShCode) ->
+print_usage_commands3(Cmds, MaxC, ShCode, Version) ->
%% Then for each one print it
lists:mapfoldl(
fun(Cmd, Remaining) ->
- print_usage_command(Cmd, MaxC, ShCode),
+ print_usage_command(Cmd, MaxC, ShCode, Version),
case Remaining > 1 of
true -> print([" ", lists:duplicate(MaxC, 126), " \n"], []);
false -> ok
@@ -716,16 +743,16 @@ filter_commands_regexp(All, Glob) ->
All).
%% @spec (Cmd::string(), MaxC::integer(), ShCode::boolean()) -> ok
-print_usage_command(Cmd, MaxC, ShCode) ->
+print_usage_command(Cmd, MaxC, ShCode, Version) ->
Name = list_to_atom(Cmd),
- case ejabberd_commands:get_command_definition(Name) of
+ case ejabberd_commands:get_command_definition(Name, Version) of
command_not_found ->
io:format("Error: command ~p not known.~n", [Cmd]);
C ->
- print_usage_command(Cmd, C, MaxC, ShCode)
+ print_usage_command2(Cmd, C, MaxC, ShCode)
end.
-print_usage_command(Cmd, C, MaxC, ShCode) ->
+print_usage_command2(Cmd, C, MaxC, ShCode) ->
#ejabberd_commands{
tags = TagsAtoms,
desc = Desc,
diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl
index f0e567199..7962786af 100644
--- a/src/mod_admin_extra.erl
+++ b/src/mod_admin_extra.erl
@@ -30,24 +30,59 @@
-include("logger.hrl").
--export([start/2, stop/1, compile/1, get_cookie/0,
- remove_node/1, set_password/3,
- check_password_hash/4, delete_old_users/1,
- delete_old_users_vhost/2, ban_account/3,
- num_active_users/2, num_resources/2, resource_num/3,
+-export([start/2, stop/1, mod_opt_type/1]).
+
+% Commands API
+-export([
+ % Adminsys
+ compile/1, get_cookie/0, remove_node/1,
+ restart_module/2,
+
+ % Sessions
+ get_presence/2, num_active_users/2, num_resources/2, resource_num/3,
kick_session/4, status_num/2, status_num/1,
status_list/2, status_list/1, connected_users_info/0,
connected_users_vhost/1, set_presence/7,
- user_sessions_info/2, set_nickname/3, get_vcard/3,
+ user_sessions_info/2, get_last/2,
+
+ % Accounts
+ change_password/3, check_password_hash/4, delete_old_users/1,
+ delete_old_users_vhost/2, ban_account/3,
+ rename_account/4,
+ check_users_registration/1,
+
+ % vCard
+ set_nickname/3, get_vcard/3,
get_vcard/4, get_vcard_multi/4, set_vcard/4,
- set_vcard/5, add_rosteritem/7, delete_rosteritem/4,
+ set_vcard/5,
+
+ % Roster
+ add_rosteritem/7, delete_rosteritem/4,
process_rosteritems/5, get_roster/2, push_roster/3,
- push_roster_all/1, push_alltoall/2, get_last/2,
- private_get/4, private_set/3, srg_create/5,
+ push_roster_all/1, push_alltoall/2,
+ link_contacts/6, unlink_contacts/2,
+ add_contacts/3, remove_contacts/3,
+ update_roster/4,
+
+ % Private storage
+ private_get/4, private_set/3,
+
+ % Shared roster
+ srg_create/5,
srg_delete/2, srg_list/1, srg_get_info/2,
srg_get_members/2, srg_user_add/4, srg_user_del/4,
- send_message/5, send_stanza/3, send_stanza_c2s/4, privacy_set/3,
- stats/1, stats/2, mod_opt_type/1, get_commands_spec/0]).
+
+ % Send message
+ send_message/5, send_stanza/3, send_stanza_c2s/4,
+
+ % Privacy list
+ privacy_set/3,
+
+ % Stats
+ stats/1, stats/2,
+
+ get_commands_spec/0
+ ]).
-include("ejabberd.hrl").
@@ -79,7 +114,8 @@ get_commands_spec() ->
" TITLE - Work: Position\n"
" ROLE - Work: Role",
- Vcard2FieldsString = "Some vcard field names and subnames in get/set_vcard2 are:\n"
+ Vcard2FieldsString = "Some vcard field names and subnames "
+ "in get/set_vcard2 are:\n"
" N FAMILY - Family name\n"
" N GIVEN - Given name\n"
" N MIDDLE - Middle name\n"
@@ -97,6 +133,7 @@ get_commands_spec() ->
"http://www.xmpp.org/extensions/xep-0054.html",
[
+ % Adminsys
#ejabberd_commands{name = compile, tags = [erlang],
desc = "Recompile and reload Erlang source code file",
module = ?MODULE, function = compile,
@@ -151,8 +188,19 @@ get_commands_spec() ->
result = {res, restuple},
result_example = {ok, <<"Deleted 2 users: [\"oldman@myserver.com\", \"test@myserver.com\"]">>},
result_desc = "Result tuple"},
+ #ejabberd_commands{name = restart_module,
+ tags = [erlang],
+ desc = "Stop an ejabberd module, reload code and start",
+ longdesc = "Returns integer code:\n"
+ " - 0: code reloaded, module restarted\n"
+ " - 1: error: module not loaded\n"
+ " - 2: code not reloaded, but module restarted",
+ module = ?MODULE, function = restart_module,
+ args = [{module, binary}, {host, binary}],
+ result = {res, integer}},
+ %%%%%%%%%%%%%%%%%% Accounts
#ejabberd_commands{name = check_account, tags = [accounts],
- desc = "Check if an account exists or not",
+ desc = "Returns 0 if user exists or 1 if not.",
module = ejabberd_auth, function = is_user_exists,
args = [{user, binary}, {host, binary}],
args_example = [<<"peter">>, <<"myserver.com">>],
@@ -161,7 +209,7 @@ get_commands_spec() ->
result_example = ok,
result_desc = "Status code: 0 on success, 1 otherwise"},
#ejabberd_commands{name = check_password, tags = [accounts],
- desc = "Check if a password is correct",
+ desc = "Check if a password is correct (0 yes, 1 no)",
module = ejabberd_auth, function = check_password,
args = [{user, binary}, {host, binary}, {password, binary}],
args_example = [<<"peter">>, <<"myserver.com">>, <<"secret">>],
@@ -171,7 +219,8 @@ get_commands_spec() ->
result_desc = "Status code: 0 on success, 1 otherwise"},
#ejabberd_commands{name = check_password_hash, tags = [accounts],
desc = "Check if the password hash is correct",
- longdesc = "Allowed hash methods: md5, sha.",
+ longdesc = "Hash must be uppercase.\n"
+ "Allowed hash methods are: md5, sha.",
module = ?MODULE, function = check_password_hash,
args = [{user, binary}, {host, binary}, {passwordhash, string},
{hashmethod, string}],
@@ -184,7 +233,7 @@ get_commands_spec() ->
result_desc = "Status code: 0 on success, 1 otherwise"},
#ejabberd_commands{name = change_password, tags = [accounts],
desc = "Change the password of an account",
- module = ?MODULE, function = set_password,
+ module = ?MODULE, function = change_password,
args = [{user, binary}, {host, binary}, {newpass, binary}],
args_example = [<<"peter">>, <<"myserver.com">>, <<"blank">>],
args_desc = ["User name", "Server name",
@@ -193,7 +242,8 @@ get_commands_spec() ->
result_example = ok,
result_desc = "Status code: 0 on success, 1 otherwise"},
#ejabberd_commands{name = ban_account, tags = [accounts],
- desc = "Ban an account: kick sessions and set random password",
+ desc = "Ban an account: kick sessions and set "
+ "random password",
module = ?MODULE, function = ban_account,
args = [{user, binary}, {host, binary}, {reason, binary}],
args_example = [<<"attacker">>, <<"myserver.com">>, <<"Spaming other users">>],
@@ -202,6 +252,59 @@ get_commands_spec() ->
result = {res, rescode},
result_example = ok,
result_desc = "Status code: 0 on success, 1 otherwise"},
+ % XXX Dangerous if lots of registered users
+ #ejabberd_commands{name = delete_old_users, tags = [accounts, purge],
+ desc = "Delete users that didn't log in last days, "
+ "or that never logged",
+ module = ?MODULE, function = delete_old_users,
+ args = [{days, integer}],
+ result = {res, restuple}},
+ % XXX Dangerous if lots of registered users
+ #ejabberd_commands{name = delete_old_users_vhost, tags = [accounts, purge],
+ desc = "Delete users that didn't log in last days "
+ "in vhost, or that never logged",
+ module = ?MODULE, function = delete_old_users_vhost,
+ args = [{host, binary}, {days, integer}],
+ result = {res, restuple}},
+ #ejabberd_commands{name = rename_account,
+ tags = [accounts], desc = "Change an acount name",
+ longdesc =
+ "Creates a new account and copies the "
+ "roster from the old one, and updates "
+ "the rosters of his contacts. Offline "
+ "messages and private storage are lost.",
+ module = ?MODULE, function = rename_account,
+ args =
+ [{user, binary}, {server, binary},
+ {newuser, binary}, {newserver, binary}],
+ result = {res, integer}},
+ #ejabberd_commands{name = check_users_registration,
+ tags = [roster],
+ desc = "List registration status for a list of users",
+ module = ?MODULE, function = check_users_registration,
+ args =
+ [{users,
+ {list,
+ {auser,
+ {tuple, [{user, binary}, {server, binary}]}}}}],
+ result =
+ {users,
+ {list,
+ {auser,
+ {tuple,
+ [{user, string}, {server, string},
+ {status, integer}]}}}}},
+
+
+ %%%%%%%%%%%%%%%%%% Sessions
+ #ejabberd_commands{name = num_active_users, tags = [accounts, stats],
+ desc = "Get number of users active in the last days",
+ policy = admin,
+ module = ?MODULE, function = num_active_users,
+ args = [{host, binary}, {days, integer}],
+ result = {users, integer}},
+
+
#ejabberd_commands{name = num_resources, tags = [session],
desc = "Get the number of resources of a user",
module = ?MODULE, function = num_resources,
@@ -251,19 +354,22 @@ get_commands_spec() ->
result = {users, integer},
result_example = 23,
result_desc = "Number of connected sessions with given status type"},
+ % XXX Dangerous if lots of online users
#ejabberd_commands{name = status_list_host, tags = [session],
desc = "List of users logged in host with their statuses",
module = ?MODULE, function = status_list,
args = [{host, binary}, {status, binary}],
result = {users, {list,
- {userstatus, {tuple, [
- {user, string},
- {host, string},
- {resource, string},
- {priority, integer},
- {status, string}
- ]}}
+ {userstatus, {tuple,
+ [
+ {user, string},
+ {host, string},
+ {resource, string},
+ {priority, integer},
+ {status, string}
+ ]}}
}}},
+ % XXX Dangerous if lots of online users
#ejabberd_commands{name = status_list, tags = [session],
desc = "List of logged users with this status",
module = ?MODULE, function = status_list,
@@ -277,9 +383,11 @@ get_commands_spec() ->
{status, string}
]}}
}}},
+ % XXX Dangerous if lots of online users
#ejabberd_commands{name = connected_users_info,
tags = [session],
- desc = "List all established sessions and their information",
+ desc = "List all established sessions and their "
+ "information",
module = ?MODULE, function = connected_users_info,
args = [],
result = {connected_users_info,
@@ -294,12 +402,14 @@ get_commands_spec() ->
{uptime, integer}
]}}
}}},
+ % XXX Dangerous if lots of online users
#ejabberd_commands{name = connected_users_vhost,
tags = [session],
desc = "Get the list of established sessions in a vhost",
module = ?MODULE, function = connected_users_vhost,
args = [{host, binary}],
- result = {connected_users_vhost, {list, {sessions, string}}}},
+ result = {connected_users_vhost,
+ {list, {sessions, string}}}},
#ejabberd_commands{name = user_sessions_info,
tags = [session],
desc = "Get information about all sessions of a user",
@@ -319,7 +429,29 @@ get_commands_spec() ->
{statustext, string}
]}}
}}},
-
+ #ejabberd_commands{name = get_presence,
+ tags = [session],
+ desc =
+ "Retrieve the resource with highest priority, "
+ "and its presence (show and status message) "
+ "for a given user.",
+ longdesc =
+ "The 'jid' value contains the user jid "
+ "with resource.\nThe 'show' value contains "
+ "the user presence flag. It can take "
+ "limited values:\n - available\n - chat "
+ "(Free for chat)\n - away\n - dnd (Do "
+ "not disturb)\n - xa (Not available, "
+ "extended away)\n - unavailable (Not "
+ "connected)\n\n'status' is a free text "
+ "defined by the user client.",
+ module = ?MODULE, function = get_presence,
+ args = [{user, binary}, {server, binary}],
+ result =
+ {presence,
+ {tuple,
+ [{jid, string}, {show, string},
+ {status, string}]}}},
#ejabberd_commands{name = set_presence,
tags = [session],
desc = "Set presence of a session",
@@ -327,52 +459,82 @@ get_commands_spec() ->
args = [{user, binary}, {host, binary},
{resource, binary}, {type, binary},
{show, binary}, {status, binary},
- {priority, binary}],
+ {priority, integer}],
result = {res, rescode}},
+ %%%%%%%%%%%%%%%%%% Last info
+ #ejabberd_commands{name = get_last, tags = [last],
+ desc = "Get last activity information "
+ "(timestamp and status)",
+ longdesc = "Timestamp is the seconds since"
+ "1970-01-01 00:00:00 UTC, for example: date +%s",
+ module = ?MODULE, function = get_last,
+ args = [{user, binary}, {host, binary}],
+ result = {last_activity, string}},
+ #ejabberd_commands{name = set_last, tags = [last],
+ desc = "Set last activity information",
+ longdesc = "Timestamp is the seconds since"
+ "1970-01-01 00:00:00 UTC, for example: date +%s",
+ module = mod_last, function = store_last_info,
+ args = [{user, binary}, {host, binary},
+ {timestamp, integer}, {status, binary}],
+ result = {res, rescode}},
+
+ %%%%%%%%%%%%%%%%%% vCard
#ejabberd_commands{name = set_nickname, tags = [vcard],
desc = "Set nickname in a user's vCard",
module = ?MODULE, function = set_nickname,
- args = [{user, binary}, {host, binary}, {nickname, binary}],
+ args = [{user, binary}, {host, binary},
+ {nickname, binary}],
result = {res, rescode}},
#ejabberd_commands{name = get_vcard, tags = [vcard],
desc = "Get content from a vCard field",
- longdesc = Vcard1FieldsString ++ "\n" ++ Vcard2FieldsString ++ "\n\n" ++ VcardXEP,
+ longdesc = Vcard1FieldsString ++ "\n" ++
+ Vcard2FieldsString ++ "\n\n" ++ VcardXEP,
module = ?MODULE, function = get_vcard,
args = [{user, binary}, {host, binary}, {name, binary}],
result = {content, string}},
#ejabberd_commands{name = get_vcard2, tags = [vcard],
desc = "Get content from a vCard field",
- longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP,
+ longdesc = Vcard2FieldsString ++ "\n\n" ++
+ Vcard1FieldsString ++ "\n" ++ VcardXEP,
module = ?MODULE, function = get_vcard,
- args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}],
+ args = [{user, binary}, {host, binary},
+ {name, binary}, {subname, binary}],
result = {content, string}},
#ejabberd_commands{name = get_vcard2_multi, tags = [vcard],
desc = "Get multiple contents from a vCard field",
- longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP,
+ longdesc = Vcard2FieldsString ++ "\n\n" ++
+ Vcard1FieldsString ++ "\n" ++ VcardXEP,
module = ?MODULE, function = get_vcard_multi,
- args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}],
+ args = [{user, binary}, {host, binary}, {name, binary},
+ {subname, binary}],
result = {contents, {list, {value, string}}}},
-
#ejabberd_commands{name = set_vcard, tags = [vcard],
desc = "Set content in a vCard field",
- longdesc = Vcard1FieldsString ++ "\n" ++ Vcard2FieldsString ++ "\n\n" ++ VcardXEP,
+ longdesc = Vcard1FieldsString ++ "\n" ++
+ Vcard2FieldsString ++ "\n\n" ++ VcardXEP,
module = ?MODULE, function = set_vcard,
- args = [{user, binary}, {host, binary}, {name, binary}, {content, binary}],
+ args = [{user, binary}, {host, binary}, {name, binary},
+ {content, binary}],
result = {res, rescode}},
#ejabberd_commands{name = set_vcard2, tags = [vcard],
desc = "Set content in a vCard subfield",
- longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP,
+ longdesc = Vcard2FieldsString ++ "\n\n" ++
+ Vcard1FieldsString ++ "\n" ++ VcardXEP,
module = ?MODULE, function = set_vcard,
- args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {content, binary}],
+ args = [{user, binary}, {host, binary}, {name, binary},
+ {subname, binary}, {content, binary}],
result = {res, rescode}},
#ejabberd_commands{name = set_vcard2_multi, tags = [vcard],
desc = "Set multiple contents in a vCard subfield",
- longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP,
+ longdesc = Vcard2FieldsString ++ "\n\n" ++
+ Vcard1FieldsString ++ "\n" ++ VcardXEP,
module = ?MODULE, function = set_vcard,
args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {contents, {list, {value, binary}}}],
result = {res, rescode}},
+ %%%%%%%%%%%%%%%%%% Roster
#ejabberd_commands{name = add_rosteritem, tags = [roster],
desc = "Add an item to a user's roster (supports ODBC)",
module = ?MODULE, function = add_rosteritem,
@@ -385,13 +547,17 @@ get_commands_spec() ->
%%{"", "example: add-roster peter localhost mike server.com MiKe Employees both"},
%%{"", "will add mike@server.com to peter@localhost roster"},
#ejabberd_commands{name = delete_rosteritem, tags = [roster],
- desc = "Delete an item from a user's roster (supports ODBC)",
+ desc = "Delete an item from a user's roster "
+ "(supports ODBC)",
module = ?MODULE, function = delete_rosteritem,
args = [{localuser, binary}, {localserver, binary},
{user, binary}, {server, binary}],
result = {res, rescode}},
+
+ % XXX Only works with mnesia
#ejabberd_commands{name = process_rosteritems, tags = [roster],
- desc = "List or delete rosteritems that match filtering options",
+ desc = "List or delete rosteritems that match "
+ "filtering options (only if roster is in Mnesia)",
longdesc = "Explanation of each argument:\n"
" - action: what to do with each rosteritem that "
"matches all the filtering options\n"
@@ -447,42 +613,109 @@ get_commands_spec() ->
args = [{file, binary}, {user, binary}, {host, binary}],
result = {res, rescode}},
#ejabberd_commands{name = push_roster_all, tags = [roster],
- desc = "Push template roster from file to all those users",
+ desc = "Push template roster from file to all "
+ "those users",
module = ?MODULE, function = push_roster_all,
args = [{file, binary}],
result = {res, rescode}},
#ejabberd_commands{name = push_alltoall, tags = [roster],
- desc = "Add all the users to all the users of Host in Group",
+ desc = "Add all the users to all the users of "
+ "Host in Group",
module = ?MODULE, function = push_alltoall,
args = [{host, binary}, {group, binary}],
result = {res, rescode}},
- #ejabberd_commands{name = get_last, tags = [last],
- desc = "Get last activity information (timestamp and status)",
- longdesc = "Timestamp is the seconds since"
- "1970-01-01 00:00:00 UTC, for example: date +%s",
- module = ?MODULE, function = get_last,
- args = [{user, binary}, {host, binary}],
- result = {last_activity, string}},
- #ejabberd_commands{name = set_last, tags = [last],
- desc = "Set last activity information",
- longdesc = "Timestamp is the seconds since"
- "1970-01-01 00:00:00 UTC, for example: date +%s",
- module = mod_last, function = store_last_info,
- args = [{user, binary}, {host, binary}, {timestamp, integer}, {status, binary}],
- result = {res, rescode}},
-
+ #ejabberd_commands{name = link_contacts,
+ tags = [roster],
+ desc = "Add a symmetrical entry in two users roster",
+ longdesc =
+ "jid1 is the JabberID of the user1 you "
+ "would like to add in user2 roster on "
+ "the server.\nnick1 is the nick of user1.\ngro"
+ "up1 is the group name when adding user1 "
+ "to user2 roster.\njid2 is the JabberID "
+ "of the user2 you would like to add in "
+ "user1 roster on the server.\nnick2 is "
+ "the nick of user2.\ngroup2 is the group "
+ "name when adding user2 to user1 roster.\n\nTh"
+ "is mechanism bypasses the standard roster "
+ "approval addition mechanism and should "
+ "only be userd for server administration "
+ "or server integration purpose.",
+ module = ?MODULE, function = link_contacts,
+ args =
+ [{jid1, binary}, {nick1, binary}, {group1, binary},
+ {jid2, binary}, {nick2, binary}, {group2, binary}],
+ result = {res, integer}},
+ #ejabberd_commands{name = unlink_contacts,
+ tags = [roster],
+ desc = "Remove a symmetrical entry in two users roster",
+ longdesc =
+ "jid1 is the JabberID of the user1.\njid2 "
+ "is the JabberID of the user2.\n\nThis "
+ "mechanism bypass the standard roster "
+ "approval addition mechanism and should "
+ "only be used for server administration "
+ "or server integration purpose.",
+ module = ?MODULE, function = unlink_contacts,
+ args = [{jid1, binary}, {jid2, binary}],
+ result = {res, integer}},
+ #ejabberd_commands{name = add_contacts,
+ tags = [roster],
+ desc =
+ "Call add_rosteritem with subscription "
+ "\"both\" for a given list of contacts. "
+ "Returns number of added items." ,
+ module = ?MODULE, function = add_contacts,
+ args =
+ [{user, binary}, {server, binary},
+ {contacts,
+ {list,
+ {contact,
+ {tuple,
+ [{jid, binary}, {group, binary},
+ {nick, binary}]}}}}],
+ result = {res, integer}},
+ #ejabberd_commands{name = remove_contacts,
+ tags = [roster],
+ desc = "Call del_rosteritem for a list of contacts",
+ module = ?MODULE, function = remove_contacts,
+ args =
+ [{user, binary}, {server, binary},
+ {contacts, {list, {jid, binary}}}],
+ result = {res, integer}},
+ #ejabberd_commands{name = update_roster, tags = [roster],
+ desc = "Add and remove contacts from user roster in one shot",
+ module = ?MODULE, function = update_roster,
+ args = [{username, binary}, {domain, binary},
+ {add, {list, {contact,
+ {list, {property,
+ {tuple, [{name, binary},
+ {value, binary}
+ ]}}}}}},
+ {delete,
+ {list, {contact,
+ {list, {property,
+ {tuple,
+ [{name, binary},{value, binary}]}
+ }}}}}],
+ result = {res, restuple}},
+
+ %%%%%%%%%%%%%%%%%% Private storage
#ejabberd_commands{name = private_get, tags = [private],
desc = "Get some information from a user private storage",
module = ?MODULE, function = private_get,
- args = [{user, binary}, {host, binary}, {element, binary}, {ns, binary}],
+ args = [{user, binary}, {host, binary},
+ {element, binary}, {ns, binary}],
result = {res, string}},
#ejabberd_commands{name = private_set, tags = [private],
desc = "Set to the user private storage",
module = ?MODULE, function = private_set,
- args = [{user, binary}, {host, binary}, {element, binary}],
+ args = [{user, binary}, {host, binary},
+ {element, binary}],
result = {res, rescode}},
+ %%%%%%%%%%%%%%%%%% Shared roster
#ejabberd_commands{name = srg_create, tags = [shared_roster_group],
desc = "Create a Shared Roster Group",
longdesc = "If you want to specify several group "
@@ -494,7 +727,8 @@ get_commands_spec() ->
"name desc \\\"group1\\\\ngroup2\\\"",
module = ?MODULE, function = srg_create,
args = [{group, binary}, {host, binary},
- {name, binary}, {description, binary}, {display, binary}],
+ {name, binary}, {description, binary},
+ {display, binary}],
result = {res, rescode}},
#ejabberd_commands{name = srg_delete, tags = [shared_roster_group],
desc = "Delete a Shared Roster Group",
@@ -510,7 +744,10 @@ get_commands_spec() ->
desc = "Get info of a Shared Roster Group",
module = ?MODULE, function = srg_get_info,
args = [{group, binary}, {host, binary}],
- result = {informations, {list, {information, {tuple, [{key, string}, {value, string}]}}}}},
+ result = {informations,
+ {list, {information,
+ {tuple, [{key, string},
+ {value, string}]}}}}},
#ejabberd_commands{name = srg_get_members, tags = [shared_roster_group],
desc = "Get members of a Shared Roster Group",
module = ?MODULE, function = srg_get_members,
@@ -519,23 +756,21 @@ get_commands_spec() ->
#ejabberd_commands{name = srg_user_add, tags = [shared_roster_group],
desc = "Add the JID user@host to the Shared Roster Group",
module = ?MODULE, function = srg_user_add,
- args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}],
+ args = [{user, binary}, {host, binary},
+ {group, binary}, {grouphost, binary}],
result = {res, rescode}},
#ejabberd_commands{name = srg_user_del, tags = [shared_roster_group],
- desc = "Delete this JID user@host from the Shared Roster Group",
+ desc = "Delete this JID user@host from the "
+ "Shared Roster Group",
module = ?MODULE, function = srg_user_del,
- args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}],
+ args = [{user, binary}, {host, binary},
+ {group, binary}, {grouphost, binary}],
result = {res, rescode}},
- #ejabberd_commands{name = get_offline_count,
- tags = [offline],
- desc = "Get the number of unread offline messages",
- policy = user,
- module = mod_offline, function = count_offline_messages,
- args = [],
- result = {res, integer}},
+ %%%%%%%%%%%%%%%%%% Stanza
#ejabberd_commands{name = send_message, tags = [stanza],
- desc = "Send a message to a local or remote bare of full JID",
+ desc = "Send a message to a local or remote "
+ "bare of full JID",
module = ?MODULE, function = send_message,
args = [{type, binary}, {from, binary}, {to, binary},
{subject, binary}, {body, binary}],
@@ -543,19 +778,24 @@ get_commands_spec() ->
#ejabberd_commands{name = send_stanza_c2s, tags = [stanza],
desc = "Send a stanza as if sent from a c2s session",
module = ?MODULE, function = send_stanza_c2s,
- args = [{user, binary}, {host, binary}, {resource, binary}, {stanza, binary}],
+ args = [{user, binary}, {host, binary},
+ {resource, binary}, {stanza, binary}],
result = {res, rescode}},
#ejabberd_commands{name = send_stanza, tags = [stanza],
desc = "Send a stanza; provide From JID and valid To JID",
module = ?MODULE, function = send_stanza,
args = [{from, binary}, {to, binary}, {stanza, binary}],
result = {res, rescode}},
+
+ %%%%%%%%%%%%%%%%%% Privacy list
#ejabberd_commands{name = privacy_set, tags = [stanza],
desc = "Send a IQ set privacy stanza for a local account",
module = ?MODULE, function = privacy_set,
- args = [{user, binary}, {host, binary}, {xmlquery, binary}],
+ args = [{user, binary}, {host, binary},
+ {xmlquery, binary}],
result = {res, rescode}},
+ %%%%%%%%%%%%%%%%%% Statistics
#ejabberd_commands{name = stats, tags = [stats],
desc = "Get statistical value: registeredusers onlineusers onlineusersnode uptimeseconds processes",
policy = admin,
@@ -563,13 +803,22 @@ get_commands_spec() ->
args = [{name, binary}],
result = {stat, integer}},
#ejabberd_commands{name = stats_host, tags = [stats],
- desc = "Get statistical value for this host: registeredusers onlineusers",
+ desc = "Get statistical value for this host: "
+ "registeredusers onlineusers",
policy = admin,
module = ?MODULE, function = stats,
args = [{name, binary}, {host, binary}],
- result = {stat, integer}}
- ].
+ result = {stat, integer}},
+ %%%%%%%%%%%%%%%%%% Offline
+ #ejabberd_commands{name = get_offline_count,
+ tags = [offline],
+ desc = "Get the number of unread offline messages",
+ policy = user,
+ module = mod_offline, function = get_queue_length,
+ args = [],
+ result = {res, integer}}
+ ].
%%%
%%% Node
@@ -586,46 +835,78 @@ remove_node(Node) ->
ok.
%%%
-%%% Accounts
+%%% Adminsys
%%%
-set_password(User, Host, Password) ->
- case ejabberd_auth:set_password(User, Host, Password) of
- ok ->
- ok;
- _ ->
- error
+restart_module(Module, Host) when is_binary(Module) ->
+ restart_module(jlib:binary_to_atom(Module), Host);
+restart_module(Module, Host) when is_atom(Module) ->
+ List = gen_mod:loaded_modules_with_opts(Host),
+ case proplists:get_value(Module, List) of
+ undefined ->
+ % not a running module, force code reload anyway
+ code:purge(Module),
+ code:delete(Module),
+ code:load_file(Module),
+ 1;
+ Opts ->
+ gen_mod:stop_module(Host, Module),
+ case code:soft_purge(Module) of
+ true ->
+ code:delete(Module),
+ code:load_file(Module),
+ gen_mod:start_module(Host, Module, Opts),
+ 0;
+ false ->
+ gen_mod:start_module(Host, Module, Opts),
+ 2
+ end
end.
+
+%%%
+%%% Accounts
+%%%
+
+change_password(U, S, P) ->
+ Fun = fun () -> ejabberd_auth:set_password(U, S, P) end,
+ user_action(U, S, Fun, ok).
+
+
%% Copied some code from ejabberd_commands.erl
check_password_hash(User, Host, PasswordHash, HashMethod) ->
AccountPass = ejabberd_auth:get_password_s(User, Host),
AccountPassHash = case {AccountPass, HashMethod} of
{A, _} when is_tuple(A) -> scrammed;
- {_, "md5"} -> get_md5(AccountPass);
- {_, "sha"} -> get_sha(AccountPass);
- _ -> undefined
+ {_, <<"md5">>} -> get_md5(AccountPass);
+ {_, <<"sha">>} -> get_sha(AccountPass);
+ {_, _Method} ->
+ ?ERROR_MSG("check_password_hash called "
+ "with hash method", [_Method]),
+ undefined
end,
case AccountPassHash of
scrammed ->
- ?ERROR_MSG("Passwords are scrammed, and check_password_hash can not work.", []),
+ ?ERROR_MSG("Passwords are scrammed "
+ "and check_password_hash can not work.", []),
throw(passwords_scrammed_command_cannot_work);
- undefined -> error;
+ undefined -> throw(unkown_hash_method);
PasswordHash -> ok;
- _ -> error
+ _ -> false
end.
get_md5(AccountPass) ->
- lists:flatten([io_lib:format("~.16B", [X])
- || X <- binary_to_list(erlang:md5(AccountPass))]).
+ iolist_to_binary([io_lib:format("~2.16.0B", [X])
+ || X <- binary_to_list(erlang:md5(AccountPass))]).
get_sha(AccountPass) ->
- lists:flatten([io_lib:format("~.16B", [X])
- || X <- binary_to_list(p1_sha:sha1(AccountPass))]).
+ iolist_to_binary([io_lib:format("~2.16.0B", [X])
+ || X <- binary_to_list(p1_sha:sha1(AccountPass))]).
num_active_users(Host, Days) ->
- list_last_activity(Host, true, Days).
+ DB_Type = gen_mod:db_type(Host, mod_last),
+ list_last_activity(Host, true, Days, DB_Type).
%% Code based on ejabberd/src/web/ejabberd_web_admin.erl
-list_last_activity(Host, Integral, Days) ->
+list_last_activity(Host, Integral, Days, mnesia) ->
TimeStamp = p1_time_compat:system_time(seconds),
TS = TimeStamp - Days * 86400,
case catch mnesia:dirty_select(
@@ -651,7 +932,11 @@ list_last_activity(Host, Integral, Days) ->
end,
lists:nth(Days, Hist ++ Tail)
end
- end.
+ end;
+list_last_activity(_Host, _Integral, _Days, DB_Type) ->
+ throw({error, iolist_to_binary(io_lib:format("Unsupported backend: ~p",
+ [DB_Type]))}).
+
histogram(Values, Integral) ->
histogram(lists:sort(Values), Integral, 0, 0, []).
histogram([H | T], Integral, Current, Count, Hist) when Current == H ->
@@ -734,6 +1019,77 @@ delete_old_users(Days, Users) ->
Users_removed = lists:filter(F, Users),
{removed, length(Users_removed), Users_removed}.
+rename_account(U, S, NU, NS) ->
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ case ejabberd_auth:get_password(U, S) of
+ false -> 1;
+ Password ->
+ case ejabberd_auth:try_register(NU, NS, Password) of
+ {atomic, ok} ->
+ OldJID = jlib:jid_to_string({U, S, <<"">>}),
+ NewJID = jlib:jid_to_string({NU, NS, <<"">>}),
+ Roster = get_roster2(U, S),
+ lists:foreach(fun (#roster{jid = {RU, RS, RE},
+ name = Nick,
+ groups = Groups}) ->
+ NewGroup = extract_group(Groups),
+ {NewNick, Group} = case
+ lists:filter(fun
+ (#roster{jid
+ =
+ {PU,
+ PS,
+ _}}) ->
+ (PU
+ ==
+ U)
+ and
+ (PS
+ ==
+ S)
+ end,
+ get_roster2(RU,
+ RS))
+ of
+ [#roster{name =
+ OldNick,
+ groups
+ =
+ OldGroups}
+ | _] ->
+ {OldNick,
+ extract_group(OldGroups)};
+ [] -> {NU, []}
+ end,
+ JIDStr = jlib:jid_to_string({RU, RS,
+ RE}),
+ link_contacts2(NewJID, NewNick,
+ NewGroup, JIDStr,
+ Nick, Group),
+ unlink_contacts2(OldJID, JIDStr)
+ end,
+ Roster),
+ ejabberd_auth:remove_user(U, S),
+ 0;
+ {atomic, exists} -> 409;
+ _ -> 1
+ end
+ end;
+ false -> 404
+ end.
+
+
+check_users_registration(Users) ->
+ lists:map(fun ({U, S}) ->
+ Registered = case ejabberd_auth:is_user_exists(U, S) of
+ true -> 1;
+ false -> 0
+ end,
+ {U, S, Registered}
+ end,
+ Users).
+
%%
%% Ban account
@@ -750,6 +1106,22 @@ kick_sessions(User, Server, Reason) ->
end,
ejabberd_sm:get_user_resources(User, Server)).
+get_presence(U, S) ->
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ {Resource, Show, Status} = get_presence2(U, S),
+ FullJID = jlib:jid_to_string({U, S, Resource}),
+ {FullJID, Show, Status};
+ false -> throw({not_found, <<"unknown_user">>})
+ end.
+
+get_sessions(User, Server) ->
+ LUser = jlib:nodeprep(User),
+ LServer = jlib:nameprep(Server),
+ Sessions = mnesia:dirty_index_read(session, {LUser, LServer}, #session.us),
+ true = is_list(Sessions),
+ Sessions.
+
set_random_password(User, Server, Reason) ->
NewPass = build_random_password(Reason),
set_password_auth(User, Server, NewPass).
@@ -782,7 +1154,9 @@ resource_num(User, Host, Num) ->
true ->
lists:nth(Num, Resources);
false ->
- lists:flatten(io_lib:format("Error: Wrong resource number: ~p", [Num]))
+ throw({bad_argument,
+ lists:flatten(io_lib:format("Wrong resource number: ~p",
+ [Num]))})
end.
kick_session(User, Server, Resource, ReasonText) ->
@@ -908,26 +1282,48 @@ user_sessions_info(User, Host) ->
Sessions).
+%% -----------------------------
+%% Internal session handling
+%% -----------------------------
+
+get_presence2(User, Server) ->
+ case get_sessions(User, Server) of
+ [] -> {<<"">>, <<"unavailable">>, <<"">>};
+ Ss ->
+ Session = hd(Ss),
+ if Session#session.priority >= 0 ->
+ Pid = element(2, Session#session.sid),
+ {_User, Resource, Show, Status} =
+ ejabberd_c2s:get_presence(Pid),
+ {Resource, Show, Status};
+ true -> {<<"">>, <<"unavailable">>, <<"">>}
+ end
+ end.
+
%%%
%%% Vcard
%%%
-set_nickname(User, Host, Nickname) ->
- R = mod_vcard:process_sm_iq(
- {jid, User, Host, <<>>, User, Host, <<>>},
- {jid, User, Host, <<>>, User, Host, <<>>},
- {iq, <<>>, set, <<>>, <<"en">>,
- {xmlel, <<"vCard">>, [
- {<<"xmlns">>, <<"vcard-temp">>}], [
- {xmlel, <<"NICKNAME">>, [], [{xmlcdata, Nickname}]}
- ]
- }}),
- case R of
- {iq, <<>>, result, <<>>, _L, []} ->
- ok;
- _ ->
- error
- end.
+set_nickname(U, S, N) ->
+ JID = jlib:make_jid({U, S, <<"">>}),
+ Fun = fun () ->
+ case mod_vcard:process_sm_iq(
+ JID, JID,
+ #iq{type = set,
+ lang = <<"en">>,
+ sub_el =
+ #xmlel{name = <<"vCard">>,
+ attrs = [{<<"xmlns">>, ?NS_VCARD}],
+ children =
+ [#xmlel{name = <<"NICKNAME">>,
+ attrs = [],
+ children =
+ [{xmlcdata, N}]}]}}) of
+ #iq{type = result} -> ok;
+ _ -> error
+ end
+ end,
+ user_action(U, S, Fun, ok).
get_vcard(User, Host, Name) ->
[Res | _] = get_vcard_content(User, Host, [Name]),
@@ -1029,8 +1425,8 @@ take_vcard_tel(_TelType, [], NewEls, Taken) ->
update_vcard_els([<<"TEL">>, TelType], [TelValue], OldEls) ->
{_, NewEls} = take_vcard_tel(TelType, OldEls, [], not_found),
NewEl = {xmlel,<<"TEL">>,[],
- [{xmlel,TelType,[],[]},
- {xmlel,<<"NUMBER">>,[],[{xmlcdata,TelValue}]}]},
+ [{xmlel,TelType,[],[]},
+ {xmlel,<<"NUMBER">>,[],[{xmlcdata,TelValue}]}]},
[NewEl | NewEls];
update_vcard_els(Data, ContentList, Els1) ->
@@ -1061,7 +1457,7 @@ update_vcard_els(Data, ContentList, Els1) ->
add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs) ->
case add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs, []) of
- {atomic, ok} ->
+ {atomic, _} ->
push_roster_item(LocalUser, LocalServer, User, Server, {add, Nick, Subs, Group}),
ok;
_ ->
@@ -1076,12 +1472,12 @@ subscribe(LU, LS, User, Server, Nick, Group, Subscription, _Xattrs) ->
mod_roster:set_items(
LU, LS,
{xmlel, <<"query">>,
- [{<<"xmlns">>, ?NS_ROSTER}],
- [ItemEl]}).
+ [{<<"xmlns">>, ?NS_ROSTER}],
+ [ItemEl]}).
delete_rosteritem(LocalUser, LocalServer, User, Server) ->
case unsubscribe(LocalUser, LocalServer, User, Server) of
- {atomic, ok} ->
+ {atomic, _} ->
push_roster_item(LocalUser, LocalServer, User, Server, remove),
ok;
_ ->
@@ -1093,8 +1489,86 @@ unsubscribe(LU, LS, User, Server) ->
mod_roster:set_items(
LU, LS,
{xmlel, <<"query">>,
- [{<<"xmlns">>, ?NS_ROSTER}],
- [ItemEl]}).
+ [{<<"xmlns">>, ?NS_ROSTER}],
+ [ItemEl]}).
+
+
+link_contacts(JID1, Nick1, Group1, JID2, Nick2, Group2) ->
+ {U1, S1, _} =
+ jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} =
+ jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case {ejabberd_auth:is_user_exists(U1, S1),
+ ejabberd_auth:is_user_exists(U2, S2)}
+ of
+ {true, true} ->
+ case link_contacts2(JID1, Nick1, Group1, JID2, Nick2,
+ Group2)
+ of
+ ok -> 0;
+ _ -> 1
+ end;
+ _ -> 404
+ end.
+
+unlink_contacts(JID1, JID2) ->
+ {U1, S1, _} =
+ jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} =
+ jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case {ejabberd_auth:is_user_exists(U1, S1),
+ ejabberd_auth:is_user_exists(U2, S2)}
+ of
+ {true, true} ->
+ case unlink_contacts2(JID1, JID2) of
+ ok -> 0;
+ _ -> 1
+ end;
+ _ -> 404
+ end.
+
+
+add_contacts(U, S, Contacts) ->
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ JID1 = jlib:jid_to_string({U, S, <<"">>}),
+ lists:foldl(fun ({JID2, Group, Nick}, Acc) ->
+ {PU, PS, _} =
+ jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case ejabberd_auth:is_user_exists(PU, PS) of
+ true ->
+ case link_contacts2(JID1, <<"">>, Group,
+ JID2, Nick, Group)
+ of
+ ok -> Acc + 1;
+ _ -> Acc
+ end;
+ false -> Acc
+ end
+ end,
+ 0, Contacts);
+ false -> 404
+ end.
+
+remove_contacts(U, S, Contacts) ->
+ case ejabberd_auth:is_user_exists(U, S) of
+ true ->
+ JID1 = jlib:jid_to_string({U, S, <<"">>}),
+ lists:foldl(fun (JID2, Acc) ->
+ {PU, PS, _} =
+ jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case ejabberd_auth:is_user_exists(PU, PS) of
+ true ->
+ case unlink_contacts2(JID1, JID2) of
+ ok -> Acc + 1;
+ _ -> Acc
+ end;
+ false -> Acc
+ end
+ end,
+ 0, Contacts);
+ false -> 404
+ end.
%% -----------------------------
%% Get Roster
@@ -1170,6 +1644,7 @@ build_list_users(Group, [{User, Server}|Users], Res) ->
%% @doc Push to the roster of account LU@LS the contact U@S.
%% The specific action to perform is defined in Action.
push_roster_item(LU, LS, U, S, Action) ->
+ mod_roster:invalidate_roster_cache(jlib:nodeprep(LU), jlib:nameprep(LS)),
lists:foreach(fun(R) ->
push_roster_item(LU, LS, R, U, S, Action)
end, ejabberd_sm:get_user_resources(LU, LS)).
@@ -1215,29 +1690,124 @@ build_broadcast(U, S, remove) ->
build_broadcast(U, S, SubsAtom) when is_atom(SubsAtom) ->
{broadcast, {item, {U, S, <<>>}, SubsAtom}}.
+
+
+update_roster(User, Host, Add, Del) when is_list(Add), is_list(Del) ->
+ Server = case Host of
+ <<>> ->
+ [Default|_] = ejabberd_config:get_myhosts(),
+ Default;
+ _ ->
+ Host
+ end,
+ case ejabberd_auth:is_user_exists(User, Server) of
+ true ->
+ AddFun = fun({Item}) ->
+ [Contact, Nick, Sub] = match(Item, [
+ {<<"username">>, <<>>},
+ {<<"nick">>, <<>>},
+ {<<"subscription">>, <<"both">>}]),
+ add_rosteritem(User, Server,
+ Contact, Server, Nick, <<>>, Sub)
+ end,
+ AddRes = [AddFun(I) || I <- Add],
+ case lists:all(fun(X) -> X==ok end, AddRes) of
+ true ->
+ DelFun = fun({Item}) ->
+ [Contact] = match(Item, [{<<"username">>, <<>>}]),
+ delete_rosteritem(User, Server, Contact, Server)
+ end,
+ [DelFun(I) || I <- Del],
+ ok;
+ false ->
+ %% try rollback if errors
+ DelFun = fun({Item}) ->
+ [Contact] = match(Item, [{<<"username">>, <<>>}]),
+ delete_rosteritem(User, Server, Contact, Server)
+ end,
+ [DelFun(I) || I <- Add],
+ String = iolist_to_binary(io_lib:format("Internal error updating "
+ "roster for user ~s@~s at node ~p",
+ [User, Host, node()])),
+ {roster_update_error, String}
+ end;
+ false ->
+ String = iolist_to_binary(io_lib:format("User ~s@~s not found at node ~p",
+ [User, Host, node()])),
+ {invalid_user, String}
+ end.
+
+match(Args, Spec) ->
+ [proplists:get_value(Key, Args, Default) || {Key, Default} <- Spec].
+
+
+%% -----------------------------
+%% Internal roster handling
+%% -----------------------------
+
+get_roster2(User, Server) ->
+ LUser = jlib:nodeprep(User),
+ LServer = jlib:nameprep(Server),
+ ejabberd_hooks:run_fold(roster_get, LServer, [], [{LUser, LServer}]).
+
+extract_group([]) -> [];
+%extract_group([Group|_Groups]) -> Group.
+extract_group(Groups) -> str:join(Groups, <<";">>).
+
+link_contacts2(JID1, Nick1, Group1, JID2, Nick2, Group2) ->
+ {U1, S1, _} =
+ jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} =
+ jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case add_rosteritem2(U1, S1, JID2, Nick2, Group1,
+ <<"both">>)
+ of
+ ok ->
+ add_rosteritem2(U2, S2, JID1, Nick1, Group2, <<"both">>);
+ Error -> Error
+ end.
+
+unlink_contacts2(JID1, JID2) ->
+ {U1, S1, _} =
+ jlib:jid_tolower(jlib:string_to_jid(JID1)),
+ {U2, S2, _} =
+ jlib:jid_tolower(jlib:string_to_jid(JID2)),
+ case delete_rosteritem(U1, S1, JID2) of
+ ok -> delete_rosteritem(U2, S2, JID1);
+ Error -> Error
+ end.
+
+add_rosteritem2(User, Server, JID, Nick, Group, Subscription) ->
+ {U, S, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ add_rosteritem(User, Server, U, S, Nick, Group, Subscription).
+
+delete_rosteritem(User, Server, JID) ->
+ {U, S, _} = jlib:jid_tolower(jlib:string_to_jid(JID)),
+ delete_rosteritem(User, Server, U, S).
+
%%%
%%% Last Activity
%%%
get_last(User, Server) ->
case ejabberd_sm:get_user_resources(User, Server) of
- [] ->
- case mod_last:get_last_info(User, Server) of
- not_found ->
- "Never";
- {ok, Shift, Status} ->
- TimeStamp = {Shift div 1000000,
- Shift rem 1000000,
- 0},
- {{Year, Month, Day}, {Hour, Minute, Second}} =
- calendar:now_to_local_time(TimeStamp),
- lists:flatten(
- io_lib:format(
- "~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w ~s",
- [Year, Month, Day, Hour, Minute, Second, Status]))
- end;
- _ ->
- "Online"
+ [] ->
+ case mod_last:get_last_info(User, Server) of
+ not_found ->
+ "Never";
+ {ok, Shift, Status} ->
+ TimeStamp = {Shift div 1000000,
+ Shift rem 1000000,
+ 0},
+ {{Year, Month, Day}, {Hour, Minute, Second}} =
+ calendar:now_to_local_time(TimeStamp),
+ lists:flatten(
+ io_lib:format(
+ "~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w ~s",
+ [Year, Month, Day, Hour, Minute, Second, Status]))
+ end;
+ _ ->
+ "Online"
end.
%%%
@@ -1294,11 +1864,11 @@ srg_create(Group, Host, Name, Description, Display) ->
Opts = [{name, Name},
{displayed_groups, DisplayList},
{description, Description}],
- {atomic, ok} = mod_shared_roster:create_group(Host, Group, Opts),
+ {atomic, _} = mod_shared_roster:create_group(Host, Group, Opts),
ok.
srg_delete(Group, Host) ->
- {atomic, ok} = mod_shared_roster:delete_group(Host, Group),
+ {atomic, _} = mod_shared_roster:delete_group(Host, Group),
ok.
srg_list(Host) ->
@@ -1322,11 +1892,11 @@ srg_get_members(Group, Host) ->
|| {MUser, MServer} <- Members].
srg_user_add(User, Host, Group, GroupHost) ->
- {atomic, ok} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group),
+ {atomic, _} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group),
ok.
srg_user_del(User, Host, Group, GroupHost) ->
- {atomic, ok} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group),
+ {atomic, _} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group),
ok.
@@ -1569,6 +2139,21 @@ decide_rip_jid({UName, UServer}, Match_list) ->
end,
Match_list).
+user_action(User, Server, Fun, OK) ->
+ case ejabberd_auth:is_user_exists(User, Server) of
+ true ->
+ case catch Fun() of
+ OK -> ok;
+ {error, Error} -> throw(Error);
+ _Error ->
+ ?ERROR_MSG("Command returned: ~p", [_Error]),
+ 1
+ end;
+ false ->
+ throw({not_found, "unknown_user"})
+ end.
+
+
%% Copied from ejabberd-2.0.0/src/acl.erl
is_regexp_match(String, RegExp) ->
case ejabberd_regexp:run(String, RegExp) of
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
index 15fe36364..f2b7a484b 100644
--- a/src/mod_http_api.erl
+++ b/src/mod_http_api.erl
@@ -29,6 +29,11 @@
%% request_handlers:
%% "/api": mod_http_api
%%
+%% To use a specific API version N, add a vN element in the URL path:
+%% in ejabberd_http listener
+%% request_handlers:
+%% "/api/v2": mod_http_api
+%%
%% Access rights are defined with:
%% commands_admin_access: configure
%% commands:
@@ -76,6 +81,8 @@
-include("logger.hrl").
-include("ejabberd_http.hrl").
+-define(DEFAULT_API_VERSION, 0).
+
-define(CT_PLAIN,
{<<"Content-Type">>, <<"text/plain">>}).
@@ -179,7 +186,8 @@ check_permissions2(#request{ip={IP, _Port}}, Call) ->
true -> {allowed, Call, admin};
_ -> unauthorized_response()
end;
- _ ->
+ _E ->
+ ?DEBUG("Unauthorized: ~p", [_E]),
unauthorized_response()
end.
@@ -192,10 +200,13 @@ oauth_check_token(Scope, Token) ->
%% command processing
%% ------------------
+%process(Call, Request) ->
+% ?DEBUG("~p~n~p", [Call, Request]), ok;
process(_, #request{method = 'POST', data = <<>>}) ->
?DEBUG("Bad Request: no data", []),
- badrequest_response();
+ badrequest_response(<<"Missing POST data">>);
process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) ->
+ Version = get_api_version(Req),
try
Args = case jiffy:decode(Data) of
List when is_list(List) -> List;
@@ -205,16 +216,20 @@ process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) ->
log(Call, Args, IP),
case check_permissions(Req, Call) of
{allowed, Cmd, Auth} ->
- {Code, Result} = handle(Cmd, Auth, Args),
+ {Code, Result} = handle(Cmd, Auth, Args, Version),
json_response(Code, jiffy:encode(Result));
ErrorResponse -> %% Should we reply 403 ?
ErrorResponse
end
- catch _:Error ->
- ?DEBUG("Bad Request: ~p", [Error]),
+ catch _:{error,{_,invalid_json}} = _Err ->
+ ?DEBUG("Bad Request: ~p", [_Err]),
+ badrequest_response(<<"Invalid JSON input">>);
+ _:_Error ->
+ ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response()
end;
process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
+ Version = get_api_version(Req),
try
Args = case Data of
[{nokey, <<>>}] -> [];
@@ -223,13 +238,13 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
log(Call, Args, IP),
case check_permissions(Req, Call) of
{allowed, Cmd, Auth} ->
- {Code, Result} = handle(Cmd, Auth, Args),
+ {Code, Result} = handle(Cmd, Auth, Args, Version),
json_response(Code, jiffy:encode(Result));
ErrorResponse ->
ErrorResponse
end
- catch _:Error ->
- ?DEBUG("Bad Request: ~p", [Error]),
+ catch _:_Error ->
+ ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response()
end;
process([], #request{method = 'OPTIONS', data = <<>>}) ->
@@ -238,13 +253,28 @@ process(_Path, Request) ->
?DEBUG("Bad Request: no handler ~p", [Request]),
badrequest_response().
+% get API version N from last "vN" element in URL path
+get_api_version(#request{path = Path}) ->
+ get_api_version(lists:reverse(Path));
+get_api_version([<<"v", String/binary>> | Tail]) ->
+ case catch jlib:binary_to_integer(String) of
+ N when is_integer(N) ->
+ N;
+ _ ->
+ get_api_version(Tail)
+ end;
+get_api_version([_Head | Tail]) ->
+ get_api_version(Tail);
+get_api_version([]) ->
+ ?DEFAULT_API_VERSION.
+
%% ----------------
%% command handlers
%% ----------------
% generic ejabberd command handler
-handle(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
- case ejabberd_commands:get_command_format(Call, Auth) of
+handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
+ case ejabberd_commands:get_command_format(Call, Auth, Version) of
{ArgsSpec, _} when is_list(ArgsSpec) ->
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
Spec = lists:foldr(
@@ -259,22 +289,51 @@ handle(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
({Key, atom}, Acc) ->
[{Key, undefined}|Acc]
end, [], ArgsSpec),
- handle2(Call, Auth, match(Args2, Spec));
+ try
+ handle2(Call, Auth, match(Args2, Spec), Version)
+ catch throw:not_found ->
+ {404, <<"not_found">>};
+ throw:{not_found, Why} when is_atom(Why) ->
+ {404, jlib:atom_to_binary(Why)};
+ throw:{not_found, Msg} ->
+ {404, iolist_to_binary(Msg)};
+ throw:not_allowed ->
+ {401, <<"not_allowed">>};
+ throw:{not_allowed, Why} when is_atom(Why) ->
+ {401, jlib:atom_to_binary(Why)};
+ throw:{not_allowed, Msg} ->
+ {401, iolist_to_binary(Msg)};
+ throw:{invalid_parameter, Msg} ->
+ {400, iolist_to_binary(Msg)};
+ throw:{error, Why} when is_atom(Why) ->
+ {400, jlib:atom_to_binary(Why)};
+ throw:{error, Msg} ->
+ {400, iolist_to_binary(Msg)};
+ throw:Error when is_atom(Error) ->
+ {400, jlib:atom_to_binary(Error)};
+ throw:Msg when is_list(Msg); is_binary(Msg) ->
+ {400, iolist_to_binary(Msg)};
+ _Error ->
+ ?ERROR_MSG("REST API Error: ~p ~p", [_Error, erlang:get_stacktrace()]),
+ {500, <<"internal_error">>}
+ end;
{error, Msg} ->
+ ?ERROR_MSG("REST API Error: ~p", [Msg]),
{400, Msg};
_Error ->
+ ?ERROR_MSG("REST API Error: ~p", [_Error]),
{400, <<"Error">>}
end.
-handle2(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
- {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth),
+handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
+ {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
ArgsFormatted = format_args(Args, ArgsF),
- case ejabberd_command(Auth, Call, ArgsFormatted, 400) of
- 0 -> {200, <<"OK">>};
- 1 -> {500, <<"500 Internal server error">>};
- 400 -> {400, <<"400 Bad Request">>};
- 404 -> {404, <<"404 Not found">>};
- Res -> format_command_result(Call, Auth, Res)
+ case ejabberd_commands:execute_command(undefined, Auth,
+ Call, ArgsFormatted, Version) of
+ {error, Error} ->
+ throw(Error);
+ Res ->
+ format_command_result(Call, Auth, Res, Version)
end.
get_elem_delete(A, L) ->
@@ -339,7 +398,9 @@ format_arg(undefined, binary) -> <<>>;
format_arg(undefined, string) -> <<>>;
format_arg(Arg, Format) ->
?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]),
- error.
+ throw({invalid_parameter,
+ io_lib:format("Arg ~p is not in format ~p",
+ [Arg, Format])}).
process_unicode_codepoints(Str) ->
iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]);
@@ -353,36 +414,26 @@ process_unicode_codepoints(Str) ->
match(Args, Spec) ->
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
-ejabberd_command(Auth, Cmd, Args, Default) ->
- Access = case Auth of
- admin -> [];
- _ -> undefined
- end,
- case catch ejabberd_commands:execute_command(Access, Auth, Cmd, Args) of
- {'EXIT', _} -> Default;
- {error, _} -> Default;
- Result -> Result
- end.
-format_command_result(Cmd, Auth, Result) ->
- {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth),
+format_command_result(Cmd, Auth, Result, Version) ->
+ {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
case {ResultFormat, Result} of
- {{_, rescode}, V} when V == true; V == ok ->
- {200, <<"">>};
- {{_, rescode}, _} ->
- {500, <<"">>};
- {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
- {200, iolist_to_binary(Text1)};
- {{_, restuple}, {_, Text2}} ->
- {500, iolist_to_binary(Text2)};
- {{_, {list, _}}, _V} ->
- {_, L} = format_result(Result, ResultFormat),
- {200, L};
- {{_, {tuple, _}}, _V} ->
- {_, T} = format_result(Result, ResultFormat),
- {200, T};
- _ ->
- {200, {[format_result(Result, ResultFormat)]}}
+ {{_, rescode}, V} when V == true; V == ok ->
+ {200, 0};
+ {{_, rescode}, _} ->
+ {200, 1};
+ {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
+ {200, iolist_to_binary(Text1)};
+ {{_, restuple}, {_, Text2}} ->
+ {500, iolist_to_binary(Text2)};
+ {{_, {list, _}}, _V} ->
+ {_, L} = format_result(Result, ResultFormat),
+ {200, L};
+ {{_, {tuple, _}}, _V} ->
+ {_, T} = format_result(Result, ResultFormat),
+ {200, T};
+ _ ->
+ {200, {[format_result(Result, ResultFormat)]}}
end.
format_result(Atom, {Name, atom}) ->
@@ -421,14 +472,15 @@ format_result(404, {_Name, _}) ->
"not_found".
unauthorized_response() ->
- {401, ?HEADER(?CT_XML),
- #xmlel{name = <<"h1">>, attrs = [],
- children = [{xmlcdata, <<"401 Unauthorized">>}]}}.
+ unauthorized_response(<<"401 Unauthorized">>).
+unauthorized_response(Body) ->
+ json_response(401, jiffy:encode(Body)).
badrequest_response() ->
- {400, ?HEADER(?CT_XML),
- #xmlel{name = <<"h1">>, attrs = [],
- children = [{xmlcdata, <<"400 Bad Request">>}]}}.
+ badrequest_response(<<"400 Bad Request">>).
+badrequest_response(Body) ->
+ json_response(400, jiffy:encode(Body)).
+
json_response(Code, Body) when is_integer(Code) ->
{Code, ?HEADER(?CT_JSON), Body}.
diff --git a/test/ejabberd_admin_test.exs b/test/ejabberd_admin_test.exs
new file mode 100644
index 000000000..1c999314c
--- /dev/null
+++ b/test/ejabberd_admin_test.exs
@@ -0,0 +1,79 @@
+# ----------------------------------------------------------------------
+#
+# ejabberd, Copyright (C) 2002-2015 ProcessOne
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# ----------------------------------------------------------------------
+
+defmodule EjabberdAdminTest do
+ use ExUnit.Case, async: false
+
+ @author "jsautret@process-one.net"
+
+ setup_all do
+ :mnesia.start
+ # For some myterious reason, :ejabberd_commands.init mays
+ # sometimes fails if module is not loaded before
+ {:module, :ejabberd_commands} = Code.ensure_loaded(:ejabberd_commands)
+ :ejabberd_commands.init
+ :ejabberd_admin.start
+ :ok
+ end
+
+ setup do
+ :ok
+ end
+
+ test "Logvel can be set and retrieved" do
+ :ejabberd_logger.start()
+
+ assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [1])
+ assert {1, :critical, 'Critical'} ==
+ :ejabberd_commands.execute_command(:get_loglevel, [])
+
+ assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [2])
+ assert {2, :error, 'Error'} ==
+ :ejabberd_commands.execute_command(:get_loglevel, [])
+
+ assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [3])
+ assert {3, :warning, 'Warning'} ==
+ :ejabberd_commands.execute_command(:get_loglevel, [])
+
+ assert {:wrong_loglevel, 6} ==
+ catch_throw :ejabberd_commands.execute_command(:set_loglevel, [6])
+ assert {3, :warning, 'Warning'} ==
+ :ejabberd_commands.execute_command(:get_loglevel, [])
+
+ assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [4])
+ assert {4, :info, 'Info'} ==
+ :ejabberd_commands.execute_command(:get_loglevel, [])
+
+ assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [5])
+ assert {5, :debug, 'Debug'} ==
+ :ejabberd_commands.execute_command(:get_loglevel, [])
+
+ assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [0])
+ assert {0, :no_log, 'No log'} ==
+ :ejabberd_commands.execute_command(:get_loglevel, [])
+
+ end
+
+ test "command status works with ejabberd stopped" do
+ assert :ejabberd_not_running ==
+ elem(:ejabberd_commands.execute_command(:status, []), 0)
+ end
+
+end
diff --git a/test/ejabberd_auth_mock.exs b/test/ejabberd_auth_mock.exs
new file mode 100644
index 000000000..495c527f2
--- /dev/null
+++ b/test/ejabberd_auth_mock.exs
@@ -0,0 +1,57 @@
+ # ejabberd_auth mock
+ ######################
+
+defmodule EjabberdAuthMock do
+
+ @author "jsautret@process-one.net"
+ @agent __MODULE__
+
+ def init do
+ try do
+ Agent.stop(@agent)
+ catch
+ :exit, _e -> :ok
+ end
+
+ {:ok, _pid} = Agent.start_link(fn -> %{} end, name: @agent)
+
+ mock(:ejabberd_auth, :is_user_exists,
+ fn (user, domain) ->
+ Agent.get(@agent, fn users -> Map.get(users, {user, domain}) end) != nil
+ end)
+ mock(:ejabberd_auth, :get_password_s,
+ fn (user, domain) ->
+ Agent.get(@agent, fn users -> Map.get(users, {user, domain}, "") end )
+ end)
+ mock(:ejabberd_auth, :check_password,
+ fn (user, domain, password) ->
+ Agent.get(@agent, fn users ->
+ Map.get(users, {user, domain}) end) == password
+ end)
+ mock(:ejabberd_auth, :set_password,
+ fn (user, domain, password) ->
+ Agent.update(@agent, fn users ->
+ Map.put(users, {user, domain}, password) end)
+ end)
+ end
+
+ def create_user(user, domain, password) do
+ Agent.update(@agent, fn users -> Map.put(users, {user, domain}, password) end)
+ end
+
+ ####################################################################
+ # Helpers
+ ####################################################################
+
+ # TODO refactor: Move to ejabberd_test_mock
+ def mock(module, function, fun) do
+ try do
+ :meck.new(module)
+ catch
+ :error, {:already_started, _pid} -> :ok
+ end
+
+ :meck.expect(module, function, fun)
+ end
+
+end
diff --git a/test/ejabberd_commands_test.exs b/test/ejabberd_commands_test.exs
index 0c06fc2ca..b3f10000e 100644
--- a/test/ejabberd_commands_test.exs
+++ b/test/ejabberd_commands_test.exs
@@ -19,39 +19,406 @@
# ----------------------------------------------------------------------
defmodule EjabberdCommandsTest do
- @author "mremond@process-one.net"
-
- use ExUnit.Case, async: true
-
- require Record
- Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl")
-
- setup_all do
- :ejabberd_commands.init
- end
-
- test "Check that we can register a command" do
- assert :ejabberd_commands.register_commands([user_test_command]) == :ok
- commands = :ejabberd_commands.list_commands
- assert Enum.member?(commands, {:test_user, [], "Test user"})
- end
-
-# test "Check that a user can use a user command" do
-# [Command] = ets:lookup(ejabberd_commands, test_user),
-# AccessCommands = ejabberd_commands:get_access_commands(undefined),
-# ejabberd_commands:check_access_commands(AccessCommands, {<<"test">>,<<"localhost">>, {oauth,<<"MyToken">>}, false}, test_user, Command, []).
-# end
-
- defp user_test_command do
- ejabberd_commands(name: :test_user, tags: [:roster],
- desc: "Test user",
- policy: :user,
- module: __MODULE__,
- function: :test_user,
- args: [],
- result: {:contacts, {:list, {:contact, {:tuple, [
- {:jid, :string},
- {:nick, :string}
- ]}}}})
- end
+ use ExUnit.Case, async: false
+
+ @author "jsautret@process-one.net"
+
+ # mocked callback module
+ @module :test_module
+ # Admin user
+ @admin "admin"
+ @adminpass "adminpass"
+ # Non admin user
+ @user "user"
+ @userpass "userpass"
+ # XMPP domain
+ @domain "domain"
+
+ require Record
+ Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands,
+ from: "ejabberd_commands.hrl")
+
+ setup_all do
+ try do
+ :stringprep.start
+ rescue
+ _ -> :ok
+ end
+ :mnesia.start
+ EjabberdOauthMock.init
+ :ok
+ end
+
+ setup do
+ :meck.unload
+ :meck.new(@module, [:non_strict])
+ :ejabberd_commands.init
+ end
+
+ test "API command can be registered, listed and unregistered" do
+ command = ejabberd_commands name: :test, module: @module,
+ function: :test_command
+
+ assert :ok == :ejabberd_commands.register_commands [command]
+ commands = :ejabberd_commands.list_commands
+ assert Enum.member? commands, {:test, [], ''}
+
+ assert :ok == :ejabberd_commands.unregister_commands [command]
+ commands = :ejabberd_commands.list_commands
+ refute Enum.member? commands, {:test, [], ''}
+ end
+
+
+ test "API command with versions can be registered, listed and unregistered" do
+ command1 = ejabberd_commands name: :test, module: @module,
+ function: :test_command, version: 1, desc: 'version1'
+ command3 = ejabberd_commands name: :test, module: @module,
+ function: :test_command, version: 3, desc: 'version3'
+ assert :ejabberd_commands.register_commands [command1, command3]
+
+ version1 = {:test, [], 'version1'}
+ version3 = {:test, [], 'version3'}
+
+ # default version is latest one
+ commands = :ejabberd_commands.list_commands
+ refute Enum.member? commands, version1
+ assert Enum.member? commands, version3
+
+ # no such command in APIv0
+ commands = :ejabberd_commands.list_commands 0
+ refute Enum.member? commands, version1
+ refute Enum.member? commands, version3
+
+ commands = :ejabberd_commands.list_commands 1
+ assert Enum.member? commands, version1
+ refute Enum.member? commands, version3
+
+ commands = :ejabberd_commands.list_commands 2
+ assert Enum.member? commands, version1
+ refute Enum.member? commands, version3
+
+ commands = :ejabberd_commands.list_commands 3
+ refute Enum.member? commands, version1
+ assert Enum.member? commands, version3
+
+ commands = :ejabberd_commands.list_commands 4
+ refute Enum.member? commands, version1
+ assert Enum.member? commands, version3
+
+ assert :ok == :ejabberd_commands.unregister_commands [command1]
+
+ commands = :ejabberd_commands.list_commands 1
+ refute Enum.member? commands, version1
+ refute Enum.member? commands, version3
+
+ commands = :ejabberd_commands.list_commands 3
+ refute Enum.member? commands, version1
+ assert Enum.member? commands, version3
+
+ assert :ok == :ejabberd_commands.unregister_commands [command3]
+
+ commands = :ejabberd_commands.list_commands 1
+ refute Enum.member? commands, version1
+ refute Enum.member? commands, version3
+
+ commands = :ejabberd_commands.list_commands 3
+ refute Enum.member? commands, version1
+ refute Enum.member? commands, version3
+ end
+
+
+ test "API command can be registered and executed" do
+ # Create & register a mocked command test() -> :result
+ command_name = :test
+ function = :test_command
+ command = ejabberd_commands(name: command_name,
+ module: @module,
+ function: function)
+ :meck.expect @module, function, fn -> :result end
+ assert :ok == :ejabberd_commands.register_commands [command]
+
+ assert :result == :ejabberd_commands.execute_command(command_name, [])
+
+ assert :meck.validate @module
+ end
+
+ test "API command with versions can be registered and executed" do
+ command_name = :test
+
+ function1 = :test_command1
+ command1 = ejabberd_commands(name: command_name,
+ version: 1,
+ module: @module,
+ function: function1)
+ :meck.expect(@module, function1, fn -> :result1 end)
+
+ function3 = :test_command3
+ command3 = ejabberd_commands(name: command_name,
+ version: 3,
+ module: @module,
+ function: function3)
+ :meck.expect(@module, function3, fn -> :result3 end)
+
+ assert :ok == :ejabberd_commands.register_commands [command1, command3]
+
+ # default version is latest one
+ assert :result3 == :ejabberd_commands.execute_command(command_name, [])
+ # no such command in APIv0
+ assert :unknown_command ==
+ catch_throw :ejabberd_commands.execute_command(command_name, [], 0)
+ assert :result1 == :ejabberd_commands.execute_command(command_name, [], 1)
+ assert :result1 == :ejabberd_commands.execute_command(command_name, [], 2)
+ assert :result3 == :ejabberd_commands.execute_command(command_name, [], 3)
+ assert :result3 == :ejabberd_commands.execute_command(command_name, [], 4)
+
+ assert :meck.validate @module
+ end
+
+
+
+ test "API command with user policy" do
+ mock_commands_config
+
+ # Register a command test(user, domain) -> {:versionN, user, domain}
+ # with policy=user and versions 1 & 3
+ command_name = :test
+ command1 = ejabberd_commands(name: command_name,
+ module: @module,
+ function: :test_command1,
+ policy: :user, version: 1)
+ command3 = ejabberd_commands(name: command_name,
+ module: @module,
+ function: :test_command3,
+ policy: :user, version: 3)
+ :meck.expect(@module, :test_command1,
+ fn(user, domain) when is_binary(user) and is_binary(domain) ->
+ {:version1, user, domain}
+ end)
+ :meck.expect(@module, :test_command3,
+ fn(user, domain) when is_binary(user) and is_binary(domain) ->
+ {:version3, user, domain}
+ end)
+ assert :ok == :ejabberd_commands.register_commands [command1, command3]
+
+ # A normal user must not pass user info as parameter
+ assert {:version1, @user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ @userpass, false},
+ command_name,
+ [], 2)
+ assert {:version3, @user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ @userpass, false},
+ command_name,
+ [], 3)
+ token = EjabberdOauthMock.get_token @user, @domain, command_name
+ assert {:version3, @user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ {:oauth, token}, false},
+ command_name,
+ [], 4)
+ # Expired oauth token
+ token = EjabberdOauthMock.get_token @user, @domain, command_name, 1
+ :timer.sleep 1500
+ assert {:error, :invalid_account_data} ==
+ catch_throw :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ {:oauth, token}, false},
+ command_name,
+ [], 4)
+ # Wrong oauth scope
+ token = EjabberdOauthMock.get_token @user, @domain, :bad_command
+ assert {:error, :invalid_account_data} ==
+ catch_throw :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ {:oauth, token}, false},
+ command_name,
+ [], 4)
+
+
+ assert :function_clause ==
+ catch_error :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ @userpass, false},
+ command_name,
+ [@user, @domain], 2)
+ # @user is not admin
+ assert {:error, :account_unprivileged} ==
+ catch_throw :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ @userpass, true},
+ command_name,
+ [], 2)
+ assert {:error, :account_unprivileged} ==
+ catch_throw :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ @userpass, true},
+ command_name,
+ [@user, @domain], 2)
+ assert {:error, :account_unprivileged} ==
+ catch_throw :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ {:oauth, token}, true},
+ command_name,
+ [@user, @domain], 2)
+
+
+ # An admin must explicitely pass user info
+ assert {:version1, @user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined, :admin,
+ command_name, [@user, @domain], 2)
+ assert {:version3, @user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined, :admin,
+ command_name, [@user, @domain], 4)
+ assert {:version1, @user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain, @adminpass, true},
+ command_name, [@user, @domain], 1)
+ token = EjabberdOauthMock.get_token @admin, @domain, command_name
+ assert {:version3, @user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain, {:oauth, token}, true},
+ command_name, [@user, @domain], 3)
+ # Wrong @admin password
+ assert {:error, :account_unprivileged} ==
+ catch_throw :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ @adminpass<>"bad", true},
+ command_name,
+ [@user, @domain], 3)
+ # @admin calling as a normal user
+ assert {:version3, @admin, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ @adminpass, false},
+ command_name, [], 5)
+ assert {:version3, @admin, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ {:oauth, token}, false},
+ command_name, [], 6)
+ assert :function_clause ==
+ catch_error :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ @adminpass, false},
+ command_name,
+ [@user, @domain], 5)
+ assert :meck.validate @module
+ end
+
+
+
+ test "API command with admin policy" do
+ mock_commands_config
+
+ # Register a command test(user, domain) -> {user, domain}
+ # with policy=admin
+ command_name = :test
+ function = :test_command
+ command = ejabberd_commands(name: command_name,
+ args: [{:user, :binary}, {:host, :binary}],
+ module: @module,
+ function: function,
+ policy: :admin)
+ :meck.expect(@module, function,
+ fn(user, domain) when is_binary(user) and is_binary(domain) ->
+ {user, domain}
+ end)
+ assert :ok == :ejabberd_commands.register_commands [command]
+
+ # A normal user cannot call the command
+ assert {:error, :account_unprivileged} ==
+ catch_throw :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ @userpass, false},
+ command_name,
+ [@user, @domain])
+
+ # An admin can call the command
+ assert {@user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ @adminpass, true},
+ command_name,
+ [@user, @domain])
+
+ # An admin can call the command with oauth token
+ token = EjabberdOauthMock.get_token @admin, @domain, command_name
+ assert {@user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ {:oauth, token}, true},
+ command_name,
+ [@user, @domain])
+
+
+ # An admin with bad password cannot call the command
+ assert {:error, :account_unprivileged} ==
+ catch_throw :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ "bad"<>@adminpass, false},
+ command_name,
+ [@user, @domain])
+
+ # An admin cannot call the command with bad oauth token
+ assert {:error, :account_unprivileged} ==
+ catch_throw :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ {:oauth, "bad"<>token}, true},
+ command_name,
+ [@user, @domain])
+
+ # An admin as a normal user cannot call the command
+ assert {:error, :account_unprivileged} ==
+ catch_throw :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ @adminpass, false},
+ command_name,
+ [@user, @domain])
+
+ # An admin as a normal user cannot call the command with oauth token
+ assert {:error, :account_unprivileged} ==
+ catch_throw :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ {:oauth, token}, false},
+ command_name,
+ [@user, @domain])
+
+ assert :meck.validate @module
+ end
+
+
+ ##########################################################
+ # Utils
+
+ # Mock a config where only @admin user is allowed to call commands
+ # as admin
+ def mock_commands_config do
+ EjabberdAuthMock.init
+ EjabberdAuthMock.create_user @user, @domain, @userpass
+ EjabberdAuthMock.create_user @admin, @domain, @adminpass
+
+ :meck.new :ejabberd_config
+ :meck.expect(:ejabberd_config, :get_option,
+ fn(:commands_admin_access, _, _) -> :commands_admin_access
+ (:oauth_access, _, _) -> :all
+ (_, _, default) -> default
+ end)
+ :meck.expect(:ejabberd_config, :get_myhosts,
+ fn() -> [@domain] end)
+ :meck.new :acl
+ :meck.expect(:acl, :match_rule,
+ fn(@domain, :commands_admin_access, user) ->
+ case :jlib.make_jid(@admin, @domain, "") do
+ ^user -> :allow
+ _ -> :deny
+ end
+ (@domain, :all, _user) ->
+ :allow
+ end)
+ end
+
end
diff --git a/test/ejabberd_hooks_test.exs b/test/ejabberd_hooks_test.exs
index 6493642de..a69fbbd61 100644
--- a/test/ejabberd_hooks_test.exs
+++ b/test/ejabberd_hooks_test.exs
@@ -30,14 +30,14 @@
# log as we are exercising hook handler recovery from that situation.
defmodule EjabberdHooksTest do
- use ExUnit.Case, async: true
-
+ use ExUnit.Case, async: false
+
@author "mremond@process-one.net"
@host <<"domain.net">>
@self __MODULE__
setup_all do
- {:ok, _Pid} = :ejabberd_hooks.start_link
+ {:ok, _pid} = :ejabberd_hooks.start_link
:ok
end
diff --git a/test/ejabberd_oauth_mock.exs b/test/ejabberd_oauth_mock.exs
new file mode 100644
index 000000000..2c1b8cf93
--- /dev/null
+++ b/test/ejabberd_oauth_mock.exs
@@ -0,0 +1,30 @@
+ # ejabberd_oauth mock
+ ######################
+
+defmodule EjabberdOauthMock do
+
+ @author "jsautret@process-one.net"
+
+ def init() do
+ :mnesia.start
+ :mnesia.create_table(:oauth_token,
+ [ram_copies: [node],
+ attributes: [:oauth_token, :us, :scope, :expire]])
+ end
+
+ def get_token(user, domain, command, expiration \\ 3600) do
+ now = {megasecs, secs, _} = :os.timestamp
+ expire = 1000000 * megasecs + secs + expiration
+ :random.seed now
+ token = to_string :random.uniform(100000000)
+
+ {:ok, _} = :ejabberd_oauth.associate_access_token(token,
+ [{"resource_owner",
+ {:user, user, domain}},
+ {"scope", [to_string command]},
+ {"expiry_time", expire}],
+ :undefined)
+ token
+ end
+
+end
diff --git a/test/ejabberd_sm_mock.exs b/test/ejabberd_sm_mock.exs
new file mode 100644
index 000000000..0c2fc1635
--- /dev/null
+++ b/test/ejabberd_sm_mock.exs
@@ -0,0 +1,106 @@
+ # ejabberd_sm mock
+ ######################
+
+defmodule EjabberdSmMock do
+ @author "jsautret@process-one.net"
+
+ require Record
+ Record.defrecord :session, Record.extract(:session,
+ from: "ejabberd_sm.hrl")
+ Record.defrecord :jid, Record.extract(:jid,
+ from: "jlib.hrl")
+
+ @agent __MODULE__
+
+ def init do
+ ModLastMock.init
+
+ try do
+ Agent.stop(@agent)
+ catch
+ :exit, _e -> :ok
+ end
+
+ {:ok, _pid} = Agent.start_link(fn -> [] end, name: @agent)
+
+ mock(:ejabberd_sm, :get_user_resources,
+ fn (user, domain) -> for s <- get_sessions(user, domain), do: s.resource end)
+
+ mock(:ejabberd_sm, :route,
+ fn (_from, to, {:broadcast, {:exit, _reason}}) ->
+ user = jid(to, :user)
+ domain = jid(to, :server)
+ resource = jid(to, :resource)
+ disconnect_resource(user, domain, resource)
+ :ok
+ (_, _, _) -> :ok
+ end)
+
+ end
+
+ def connect_resource(user, domain, resource,
+ opts \\ [priority: 1, conn: :c2s]) do
+ Agent.update(@agent, fn sessions ->
+ session = %{user: user, domain: domain, resource: resource,
+ timestamp: :os.timestamp, pid: self, node: node,
+ auth_module: :ejabberd_auth, ip: :undefined,
+ priority: opts[:priority], conn: opts[:conn]}
+ [session | sessions]
+ end)
+ end
+
+ def disconnect_resource(user, domain, resource) do
+ disconnect_resource(user, domain, resource, ModLastMock.now)
+ end
+
+ def disconnect_resource(user, domain, resource, timestamp) do
+ Agent.update(@agent, fn sessions ->
+ for s <- sessions,
+ s.user != user or s.domain != domain or s.resource != resource, do: s
+ end)
+ ModLastMock.set_last user, domain, "", timestamp
+ end
+
+ def get_sessions() do
+ Agent.get(@agent, fn sessions -> sessions end)
+ end
+
+ def get_sessions(user, domain) do
+ Agent.get(@agent, fn sessions ->
+ for s <- sessions, s.user == user, s.domain == domain, do: s
+ end)
+ end
+
+ def get_session(user, domain, resource) do
+ Agent.get(@agent, fn sessions ->
+ for s <- sessions,
+ s.user == user, s.domain == domain, s.resource == resource, do: s
+ end)
+ end
+
+ def to_record(s) do
+ session(usr: {s.user, s.domain, s.ressource},
+ us: {s.user, s.domain},
+ sid: {s.timestamp, s.pid},
+ priority: s.priority,
+ info: [conn: s.conn, ip: s.ip, node: s.node,
+ oor: false, auth_module: s.auth_module])
+ end
+
+ ####################################################################
+ # Helpers
+ ####################################################################
+
+
+ # TODO refactor: Move to ejabberd_test_mock
+ def mock(module, function, fun) do
+ try do
+ :meck.new(module)
+ catch
+ :error, {:already_started, _pid} -> :ok
+ end
+
+ :meck.expect(module, function, fun)
+ end
+
+end
diff --git a/test/elixir_SUITE.erl b/test/elixir_SUITE.erl
index b9a0b1a23..f2c64773b 100644
--- a/test/elixir_SUITE.erl
+++ b/test/elixir_SUITE.erl
@@ -19,6 +19,7 @@
init_per_suite(Config) ->
check_meck(),
+ code:add_pathz(filename:join(test_dir(), "../include")),
Config.
init_per_testcase(_TestCase, Config) ->
@@ -27,13 +28,13 @@ init_per_testcase(_TestCase, Config) ->
all() ->
case is_elixir_available() of
- true ->
- Dir = test_dir(),
- filelib:fold_files(Dir, ".*\.exs", false,
- fun(Filename, Acc) -> [list_to_atom(filename:basename(Filename)) | Acc] end,
- []);
- false ->
- []
+ true ->
+ Dir = test_dir(),
+ filelib:fold_files(Dir, ".*\.exs", false,
+ fun(Filename, Acc) -> [list_to_atom(filename:basename(Filename)) | Acc] end,
+ []);
+ false ->
+ []
end.
check_meck() ->
@@ -56,16 +57,21 @@ is_elixir_available() ->
undefined_function(?MODULE, Func, Args) ->
case lists:suffix(".exs", atom_to_list(Func)) of
- true ->
- run_elixir_test(Func);
- false ->
- error_handler:undefined_function(?MODULE, Func, Args)
+ true ->
+ run_elixir_test(Func);
+ false ->
+ error_handler:undefined_function(?MODULE, Func, Args)
end;
undefined_function(Module, Func, Args) ->
error_handler:undefined_function(Module, Func,Args).
run_elixir_test(Func) ->
'Elixir.ExUnit':start([]),
+ filelib:fold_files(test_dir(), ".*\\.exs\$", true,
+ fun (File, N) ->
+ 'Elixir.Code':require_file(list_to_binary(File)),
+ N+1
+ end, 0),
'Elixir.Code':load_file(list_to_binary(filename:join(test_dir(), atom_to_list(Func)))),
%% I did not use map syntax, so that this file can still be build under R16
ResultMap = 'Elixir.ExUnit':run(),
diff --git a/test/mod_admin_extra_test.exs b/test/mod_admin_extra_test.exs
new file mode 100644
index 000000000..7fa39eef1
--- /dev/null
+++ b/test/mod_admin_extra_test.exs
@@ -0,0 +1,699 @@
+# ----------------------------------------------------------------------
+#
+# ejabberd, Copyright (C) 2002-2015 ProcessOne
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# ----------------------------------------------------------------------
+
+defmodule EjabberdModAdminExtraTest do
+ use ExUnit.Case, async: false
+
+ @author "jsautret@process-one.net"
+
+ @user "user"
+ @domain "domain"
+ @password "password"
+ @resource "resource"
+
+ require Record
+ Record.defrecord :jid, Record.extract(:jid,
+ from: "jlib.hrl")
+
+ setup_all do
+ try do
+ :stringprep.start
+ :mnesia.start
+ :p1_sha.load_nif
+ rescue
+ _ -> :ok
+ end
+ :ejabberd_commands.init
+ :mod_admin_extra.start(@domain, [])
+ :sel_application.start_app(:moka)
+ {:ok, _pid} = :ejabberd_hooks.start_link
+ :ok
+ end
+
+ setup do
+ :meck.unload
+ EjabberdAuthMock.init
+ EjabberdSmMock.init
+ ModRosterMock.init(@domain, :mod_admin_extra)
+ :ok
+ end
+
+ ###################### Accounts
+ test "check_account works" do
+ EjabberdAuthMock.create_user @user, @domain, @password
+
+ assert :ejabberd_commands.execute_command(:check_account, [@user, @domain])
+ refute :ejabberd_commands.execute_command(:check_account, [@user, "bad_domain"])
+ refute :ejabberd_commands.execute_command(:check_account, ["bad_user", @domain])
+
+ assert :meck.validate :ejabberd_auth
+ end
+
+ test "check_password works" do
+
+ EjabberdAuthMock.create_user @user, @domain, @password
+
+ assert :ejabberd_commands.execute_command(:check_password,
+ [@user, @domain, @password])
+ refute :ejabberd_commands.execute_command(:check_password,
+ [@user, @domain, "bad_password"])
+ refute :ejabberd_commands.execute_command(:check_password,
+ [@user, "bad_domain", @password])
+ refute :ejabberd_commands.execute_command(:check_password,
+ ["bad_user", @domain, @password])
+
+ assert :meck.validate :ejabberd_auth
+
+ end
+
+ test "check_password_hash works" do
+
+ EjabberdAuthMock.create_user @user, @domain, @password
+ hash = "5F4DCC3B5AA765D61D8327DEB882CF99" # echo -n password|md5
+
+ assert :ejabberd_commands.execute_command(:check_password_hash,
+ [@user, @domain, hash, "md5"])
+ refute :ejabberd_commands.execute_command(:check_password_hash,
+ [@user, @domain, "bad_hash", "md5"])
+ refute :ejabberd_commands.execute_command(:check_password_hash,
+ [@user, "bad_domain", hash, "md5"])
+ refute :ejabberd_commands.execute_command(:check_password_hash,
+ ["bad_user", @domain, hash, "md5"])
+
+ hash = "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8" # echo -n password|shasum
+ assert :ejabberd_commands.execute_command(:check_password_hash,
+ [@user, @domain, hash, "sha"])
+
+ assert :unkown_hash_method ==
+ catch_throw :ejabberd_commands.execute_command(:check_password_hash,
+ [@user, @domain, hash, "bad_method"])
+
+ assert :meck.validate :ejabberd_auth
+
+ end
+
+ test "change_password works" do
+ EjabberdAuthMock.create_user @user, @domain, @password
+
+ assert :ejabberd_commands.execute_command(:change_password,
+ [@user, @domain, "new_password"])
+ refute :ejabberd_commands.execute_command(:check_password,
+ [@user, @domain, @password])
+ assert :ejabberd_commands.execute_command(:check_password,
+ [@user, @domain, "new_password"])
+ assert {:not_found, 'unknown_user'} ==
+ catch_throw :ejabberd_commands.execute_command(:change_password,
+ ["bad_user", @domain,
+ @password])
+ assert :meck.validate :ejabberd_auth
+ end
+
+ test "check_users_registration works" do
+ EjabberdAuthMock.create_user @user<>"1", @domain, @password
+ EjabberdAuthMock.create_user @user<>"2", @domain, @password
+ EjabberdAuthMock.create_user @user<>"3", @domain, @password
+
+ assert [{@user<>"0", @domain, 0},
+ {@user<>"1", @domain, 1},
+ {@user<>"2", @domain, 1},
+ {@user<>"3", @domain, 1}] ==
+ :ejabberd_commands.execute_command(:check_users_registration,
+ [[{@user<>"0", @domain},
+ {@user<>"1", @domain},
+ {@user<>"2", @domain},
+ {@user<>"3", @domain}]])
+
+ assert :meck.validate :ejabberd_auth
+
+ end
+
+ ###################### Sessions
+
+ test "num_resources works" do
+ assert 0 == :ejabberd_commands.execute_command(:num_resources,
+ [@user, @domain])
+
+ EjabberdSmMock.connect_resource @user, @domain, @resource
+ assert 1 == :ejabberd_commands.execute_command(:num_resources,
+ [@user, @domain])
+
+ EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
+ assert 2 == :ejabberd_commands.execute_command(:num_resources,
+ [@user, @domain])
+
+ EjabberdSmMock.connect_resource @user<>"1", @domain, @resource
+ assert 2 == :ejabberd_commands.execute_command(:num_resources,
+ [@user, @domain])
+
+ EjabberdSmMock.disconnect_resource @user, @domain, @resource
+ assert 1 == :ejabberd_commands.execute_command(:num_resources,
+ [@user, @domain])
+
+ assert :meck.validate :ejabberd_sm
+ end
+
+ test "resource_num works" do
+ EjabberdSmMock.connect_resource @user, @domain, @resource<>"3"
+ EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
+ EjabberdSmMock.connect_resource @user, @domain, @resource<>"1"
+
+ assert :bad_argument ==
+ elem(catch_throw(:ejabberd_commands.execute_command(:resource_num,
+ [@user, @domain, 0])), 0)
+ assert @resource<>"1" ==
+ :ejabberd_commands.execute_command(:resource_num, [@user, @domain, 1])
+ assert @resource<>"3" ==
+ :ejabberd_commands.execute_command(:resource_num, [@user, @domain, 3])
+ assert :bad_argument ==
+ elem(catch_throw(:ejabberd_commands.execute_command(:resource_num,
+ [@user, @domain, 4])), 0)
+ assert :meck.validate :ejabberd_sm
+ end
+
+ test "kick_session works" do
+ EjabberdSmMock.connect_resource @user, @domain, @resource<>"1"
+ EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
+ EjabberdSmMock.connect_resource @user, @domain, @resource<>"3"
+
+ assert 3 == length EjabberdSmMock.get_sessions @user, @domain
+ assert 1 == length EjabberdSmMock.get_session @user, @domain, @resource<>"2"
+
+ assert :ok ==
+ :ejabberd_commands.execute_command(:kick_session,
+ [@user, @domain,
+ @resource<>"2", "kick"])
+
+ assert 2 == length EjabberdSmMock.get_sessions @user, @domain
+ assert 0 == length EjabberdSmMock.get_session @user, @domain, @resource<>"2"
+
+ assert :meck.validate :ejabberd_sm
+ end
+
+ ###################### Last
+
+ test "get_last works" do
+
+ assert 'Never' ==
+ :ejabberd_commands.execute_command(:get_last, [@user, @domain])
+
+ EjabberdSmMock.connect_resource @user, @domain, @resource<>"1"
+ EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
+
+ assert 'Online' ==
+ :ejabberd_commands.execute_command(:get_last, [@user, @domain])
+
+ EjabberdSmMock.disconnect_resource @user, @domain, @resource<>"1"
+
+ assert 'Online' ==
+ :ejabberd_commands.execute_command(:get_last, [@user, @domain])
+
+ now = {megasecs, secs, _microsecs} = :os.timestamp
+ timestamp = megasecs * 1000000 + secs
+ EjabberdSmMock.disconnect_resource(@user, @domain, @resource<>"2",
+ timestamp)
+ {{year, month, day}, {hour, minute, second}} = :calendar.now_to_local_time now
+ result = List.flatten(:io_lib.format(
+ "~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w ",
+ [year, month, day, hour, minute, second]))
+ assert result ==
+ :ejabberd_commands.execute_command(:get_last, [@user, @domain])
+
+ assert :meck.validate :mod_last
+ end
+
+ ###################### Roster
+
+ test "add_rosteritem and delete_rosteritem work" do
+ # Connect user
+ # Add user1 & user2 to user's roster
+ # Remove user1 & user2 from user's roster
+
+ EjabberdSmMock.connect_resource @user, @domain, @resource
+
+ assert [] == ModRosterMock.get_roster(@user, @domain)
+
+ assert :ok ==
+ :ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
+ @user<>"1", @domain,
+ "nick1",
+ "group1",
+ "both"])
+ # Check that user1 is the only item of the user's roster
+ result = ModRosterMock.get_roster(@user, @domain)
+ assert 1 == length result
+ [{{@user, @domain, jid}, opts}] = result
+ assert @user<>"1@"<>@domain == jid
+ assert "nick1" == opts.nick
+ assert ["group1"] == opts.groups
+ assert :both == opts.subs
+
+ # Check that the item roster user1 was pushed with subscription
+ # 'both' to user online ressource
+ jid = :jlib.make_jid(@user, @domain, @resource)
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
+
+ assert :ok ==
+ :ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
+ @user<>"2", @domain,
+ "nick2",
+ "group2",
+ "both"])
+ result = ModRosterMock.get_roster(@user, @domain)
+ assert 2 == length result
+
+
+ # Check that the item roster user2 was pushed with subscription
+ # 'both' to user online ressource
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"2", @domain, ""}, :both}}])
+
+
+ :ejabberd_commands.execute_command(:delete_rosteritem, [@user, @domain,
+ @user<>"1", @domain])
+ result = ModRosterMock.get_roster(@user, @domain)
+ assert 1 == length result
+ [{{@user, @domain, jid}, opts}] = result
+ assert @user<>"2@"<>@domain == jid
+ assert "nick2" == opts.nick
+ assert ["group2"] == opts.groups
+ assert :both == opts.subs
+
+ # Check that the item roster user1 was pushed with subscription
+ # 'none' to user online ressource
+ jid = :jlib.make_jid(@user, @domain, @resource)
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
+
+ :ejabberd_commands.execute_command(:delete_rosteritem, [@user, @domain,
+ @user<>"2", @domain])
+
+ # Check that the item roster user2 was pushed with subscription
+ # 'none' to user online ressource
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"2", @domain, ""}, :none}}])
+
+ # Check that nothing else was pushed to user resource
+ jid = jid(user: @user, server: @domain, resource: :_,
+ luser: @user, lserver: @domain, lresource: :_)
+ assert 4 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, :_, :_}}])
+
+ assert [] == ModRosterMock.get_roster(@user, @domain)
+ assert :meck.validate :ejabberd_sm
+
+ end
+
+ test "get_roster works" do
+ assert [] == ModRosterMock.get_roster(@user, @domain)
+ assert [] == :ejabberd_commands.execute_command(:get_roster, [@user, @domain],
+ :admin)
+
+ assert :ok ==
+ :ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
+ @user<>"1", @domain,
+ "nick1",
+ "group1",
+ "both"])
+ assert [{@user<>"1@"<>@domain, "", 'both', 'none', "group1"}] ==
+ :ejabberd_commands.execute_command(:get_roster, [@user, @domain], :admin)
+ assert :ok ==
+ :ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
+ @user<>"2", @domain,
+ "nick2",
+ "group2",
+ "none"])
+ result = :ejabberd_commands.execute_command(:get_roster, [@user, @domain], :admin)
+ assert 2 == length result
+ assert Enum.member?(result, {@user<>"1@"<>@domain, "", 'both', 'none', "group1"})
+ assert Enum.member?(result, {@user<>"2@"<>@domain, "", 'none', 'none', "group2"})
+
+ end
+
+
+ test "link_contacts & unlink_contacts work" do
+ # Create user1 and keep it offline
+ EjabberdAuthMock.create_user @user<>"1", @domain, @password
+
+ # fail if one of the users doesn't exist locally
+ assert 404 ==
+ :ejabberd_commands.execute_command(:link_contacts, [@user<>"1@"<>@domain,
+ "nick1",
+ "group1",
+ @user<>"2@"<>@domain,
+ "nick2",
+ "group2"])
+
+ # Create user2 and connect 2 resources
+ EjabberdAuthMock.create_user @user<>"2", @domain, @password
+
+ EjabberdSmMock.connect_resource @user<>"2", @domain, @resource<>"1"
+ EjabberdSmMock.connect_resource @user<>"2", @domain, @resource<>"2"
+
+ # Link both user1 & user2 (returns 0 if OK)
+ assert 0 ==
+ :ejabberd_commands.execute_command(:link_contacts, [@user<>"1@"<>@domain,
+ "nick1",
+ "group2",
+ @user<>"2@"<>@domain,
+ "nick2",
+ "group1"])
+ assert [{@user<>"2@"<>@domain, "", 'both', 'none', "group2"}] ==
+ :ejabberd_commands.execute_command(:get_roster, [@user<>"1", @domain], :admin)
+
+ assert [{@user<>"1@"<>@domain, "", 'both', 'none', "group1"}] ==
+ :ejabberd_commands.execute_command(:get_roster, [@user<>"2", @domain], :admin)
+
+ # Check that the item roster user1 was pushed with subscription
+ # 'both' to the 2 user2 online ressources
+ jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"1")
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
+ jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"2")
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
+
+
+ # Ulink both user1 & user2 (returns 0 if OK)
+ assert 0 ==
+ :ejabberd_commands.execute_command(:unlink_contacts, [@user<>"1@"<>@domain,
+ @user<>"2@"<>@domain])
+ assert [] ==
+ :ejabberd_commands.execute_command(:get_roster, [@user<>"1", @domain], :admin)
+
+ assert [] ==
+ :ejabberd_commands.execute_command(:get_roster, [@user<>"2", @domain], :admin)
+
+ # Check that the item roster user1 was pushed with subscription
+ # 'none' to the 2 user2 online ressources
+ jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"1")
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
+ jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"2")
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
+
+ # Check that nothing else was pushed to user2 resources
+ jid = jid(user: @user<>"2", server: @domain, resource: :_,
+ luser: @user<>"2", lserver: @domain, lresource: :_)
+ assert 4 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, :_, :_}}])
+
+ # Check nothing was pushed to user1
+ jid = jid(user: @user<>"1", server: @domain, resource: :_,
+ luser: @user<>"1", lserver: @domain, lresource: :_)
+ refute :meck.called(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, :_, :_}}])
+
+ assert :meck.validate :ejabberd_sm
+ assert :meck.validate :ejabberd_auth
+
+ end
+
+
+
+
+ test "add_contacts and delete_contacts work" do
+ # Create user, user1 & user2
+ # Connect user & user1
+ # Add user0, user1 & user2 to user's roster
+ # Remove user0, user1 & user2 from user's roster
+
+ # user doesn't exists yet, command must fail
+ assert 404 ==
+ :ejabberd_commands.execute_command(:add_contacts, [@user, @domain,
+ [{@user<>"1"<>@domain,
+ "group1",
+ "nick1"},
+ {@user<>"2"<>@domain,
+ "group2",
+ "nick2"}]
+ ])
+
+ EjabberdAuthMock.create_user @user, @domain, @password
+ EjabberdSmMock.connect_resource @user, @domain, @resource
+ EjabberdAuthMock.create_user @user<>"1", @domain, @password
+ EjabberdSmMock.connect_resource @user<>"1", @domain, @resource
+ EjabberdAuthMock.create_user @user<>"2", @domain, @password
+
+ # Add user1 & user2 in user's roster. Try also to add user0 that
+ # doesn't exists. Command is supposed to return number of added items.
+ assert 2 ==
+ :ejabberd_commands.execute_command(:add_contacts, [@user, @domain,
+ [{@user<>"0@"<>@domain,
+ "group0",
+ "nick0"},
+ {@user<>"1@"<>@domain,
+ "group1",
+ "nick1"},
+ {@user<>"2@"<>@domain,
+ "group2",
+ "nick2"}]
+ ])
+ # Check that user1 & user2 are the only items in user's roster
+ result = ModRosterMock.get_roster(@user, @domain)
+ assert 2 == length result
+ opts1 = %{nick: "nick1", groups: ["group1"], subs: :both,
+ ask: :none, askmessage: ""}
+ assert Enum.member?(result, {{@user, @domain, @user<>"1@"<>@domain}, opts1})
+ opts2 = %{nick: "nick2", groups: ["group2"], subs: :both,
+ ask: :none, askmessage: ""}
+ assert Enum.member?(result, {{@user, @domain, @user<>"2@"<>@domain}, opts2})
+
+ # Check that user is the only item in user1's roster
+ assert [{{@user<>"1", @domain, @user<>"@"<>@domain}, %{opts1|:nick => ""}}] ==
+ ModRosterMock.get_roster(@user<>"1", @domain)
+
+ # Check that user is the only item in user2's roster
+ assert [{{@user<>"2", @domain, @user<>"@"<>@domain}, %{opts2|:nick => ""}}] ==
+ ModRosterMock.get_roster(@user<>"2", @domain)
+
+
+ # Check that the roster items user1 & user2 were pushed with subscription
+ # 'both' to the user online ressource
+ jid = :jlib.make_jid(@user, @domain, @resource)
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"2", @domain, ""}, :both}}])
+
+ # Check that the roster item user was pushed with subscription
+ # 'both' to the user1 online ressource
+ jid = :jlib.make_jid(@user<>"1", @domain, @resource)
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user, @domain, ""}, :both}}])
+
+ # Check that nothing else was pushed to online resources
+ assert 3 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [:_, :_,
+ {:broadcast, {:item, :_, :_}}])
+
+ # Remove user1 & user2 from user's roster. Try also to remove
+ # user0 that doesn't exists. Command is supposed to return number
+ # of removed items.
+ assert 2 ==
+ :ejabberd_commands.execute_command(:remove_contacts, [@user, @domain,
+ [@user<>"0@"<>@domain,
+ @user<>"1@"<>@domain,
+ @user<>"2@"<>@domain]
+ ])
+ # Check that roster of user, user1 & user2 are empty
+ assert [] == ModRosterMock.get_roster(@user, @domain)
+ assert [] == ModRosterMock.get_roster(@user<>"1", @domain)
+ assert [] == ModRosterMock.get_roster(@user<>"2", @domain)
+
+ # Check that the roster items user1 & user2 were pushed with subscription
+ # 'none' to the user online ressource
+ jid = :jlib.make_jid(@user, @domain, @resource)
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"2", @domain, ""}, :none}}])
+
+ # Check that the roster item user was pushed with subscription
+ # 'none' to the user1 online ressource
+ jid = :jlib.make_jid(@user<>"1", @domain, @resource)
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user, @domain, ""}, :none}}])
+
+ # Check that nothing else was pushed to online resources
+ assert 6 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [:_, :_,
+ {:broadcast, {:item, :_, :_}}])
+
+ assert :meck.validate :ejabberd_sm
+ assert :meck.validate :ejabberd_auth
+ end
+
+
+ test "update_roster works" do
+ # user doesn't exists yet, command must fail
+ result =
+ :ejabberd_commands.execute_command(:update_roster,
+ [@user, @domain,
+ [{@user<>"1"<>@domain,
+ "group1",
+ "nick1"},
+ {@user<>"2"<>@domain,
+ "group2",
+ "nick2"}],
+ []
+ ])
+ assert :invalid_user == elem(result, 0)
+
+ EjabberdAuthMock.create_user @user, @domain, @password
+ EjabberdSmMock.connect_resource @user, @domain, @resource
+ EjabberdAuthMock.create_user @user<>"1", @domain, @password
+ EjabberdSmMock.connect_resource @user<>"1", @domain, @resource
+ EjabberdAuthMock.create_user @user<>"2", @domain, @password
+ EjabberdAuthMock.create_user @user<>"3", @domain, @password
+
+ assert :ok ==
+ :ejabberd_commands.execute_command(:update_roster,
+ [@user, @domain,
+ [{[{"username", @user<>"1"},
+ {"nick", "nick1"}]},
+ {[{"username", @user<>"2"},
+ {"nick", "nick2"},
+ {"subscription", "from"}]}],
+ []])
+
+
+ # Check that user1 & user2 are the only items in user's roster
+ result = ModRosterMock.get_roster(@user, @domain)
+
+ assert 2 == length result
+ opts1 = %{nick: "nick1", groups: [""], subs: :both,
+ ask: :none, askmessage: ""}
+ assert Enum.member?(result, {{@user, @domain, @user<>"1@"<>@domain}, opts1})
+ opts2 = %{nick: "nick2", groups: [""], subs: :from,
+ ask: :none, askmessage: ""}
+ assert Enum.member?(result, {{@user, @domain, @user<>"2@"<>@domain}, opts2})
+
+ # Check that the roster items user1 & user2 were pushed with subscription
+ # 'both' to the user online ressource
+ jid = :jlib.make_jid(@user, @domain, @resource)
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"2", @domain, ""}, :from}}])
+
+ # Check that nothing else was pushed to online resources
+ assert 2 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [:_, :_,
+ {:broadcast, {:item, :_, :_}}])
+
+ # Add user3 & remove user1
+ assert :ok ==
+ :ejabberd_commands.execute_command(:update_roster,
+ [@user, @domain,
+ [{[{"username", @user<>"3"},
+ {"nick", "nick3"},
+ {"subscription", "to"}]}],
+ [{[{"username", @user<>"1"}]}]
+ ])
+
+ # Check that user2 & user3 are the only items in user's roster
+ result = ModRosterMock.get_roster(@user, @domain)
+ assert 2 == length result
+ opts2 = %{nick: "nick2", groups: [""], subs: :from,
+ ask: :none, askmessage: ""}
+ assert Enum.member?(result, {{@user, @domain, @user<>"2@"<>@domain}, opts2})
+ opts1 = %{nick: "nick3", groups: [""], subs: :to,
+ ask: :none, askmessage: ""}
+ assert Enum.member?(result, {{@user, @domain, @user<>"3@"<>@domain}, opts1})
+
+ # Check that the roster items user1 & user3 were pushed with subscription
+ # 'none' & 'to' to the user online ressource
+ jid = :jlib.make_jid(@user, @domain, @resource)
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
+ assert 1 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [jid, jid,
+ {:broadcast, {:item, {@user<>"3", @domain, ""}, :to}}])
+
+ # Check that nothing else was pushed to online resources
+ assert 4 ==
+ :meck.num_calls(:ejabberd_sm, :route,
+ [:_, :_,
+ {:broadcast, {:item, :_, :_}}])
+
+ assert :meck.validate :ejabberd_sm
+ assert :meck.validate :ejabberd_auth
+ end
+
+
+# kick_user command is defined in ejabberd_sm, move to extra?
+# test "kick_user works" do
+# assert 0 == :ejabberd_commands.execute_command(:num_resources,
+# [@user, @domain])
+# EjabberdSmMock.connect_resource(@user, @domain, @resource<>"1")
+# EjabberdSmMock.connect_resource(@user, @domain, @resource<>"2")
+# assert 2 ==
+# :ejabberd_commands.execute_command(:kick_user, [@user, @domain])
+# assert 0 == :ejabberd_commands.execute_command(:num_resources,
+# [@user, @domain])
+# assert :meck.validate :ejabberd_sm
+# end
+
+end
diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs
new file mode 100644
index 000000000..ae62f28f9
--- /dev/null
+++ b/test/mod_http_api_test.exs
@@ -0,0 +1,188 @@
+# ----------------------------------------------------------------------
+#
+# ejabberd, Copyright (C) 2002-2015 ProcessOne
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# ----------------------------------------------------------------------
+
+defmodule ModHttpApiTest do
+ use ExUnit.Case, async: false
+
+ @author "jsautret@process-one.net"
+
+ # Admin user
+ @admin "admin"
+ @adminpass "adminpass"
+ # Non admin user
+ @user "user"
+ @userpass "userpass"
+ # XMPP domain
+ @domain "domain"
+ # mocked command
+ @command "command_test"
+ @acommand String.to_atom(@command)
+ # default API version
+ @version 0
+
+ require Record
+ Record.defrecord :request, Record.extract(:request,
+ from: "ejabberd_http.hrl")
+
+ setup_all do
+ try do
+ :stringprep.start
+ rescue
+ _ -> :ok
+ end
+ :mod_http_api.start(@domain, [])
+ EjabberdOauthMock.init
+ :ok
+ end
+
+ setup do
+ :meck.unload
+ :meck.new :ejabberd_commands
+ EjabberdAuthMock.init
+ :ok
+ end
+
+ test "HTTP GET simple command call with Basic Auth" do
+ EjabberdAuthMock.create_user @user, @domain, @userpass
+
+ # Mock a simple command() -> :ok
+ :meck.expect(:ejabberd_commands, :get_command_format,
+ fn (@acommand, {@user, @domain, @userpass, false}, @version) ->
+ {[], {:res, :rescode}}
+ end)
+ :meck.expect(:ejabberd_commands, :execute_command,
+ fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version) ->
+ :ok
+ end)
+
+ #:ejabberd_logger.start
+ #:ejabberd_logger.set 5
+
+ # Correct Basic Auth call
+ req = request(method: :GET,
+ path: ["api", @command],
+ q: [nokey: ""],
+ # Basic auth
+ auth: {@user<>"@"<>@domain, @userpass},
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :mod_http_api.process([@command], req)
+ assert 200 == elem(result, 0) # HTTP code
+ assert "0" == elem(result, 2) # command result
+
+ # Bad password
+ req = request(method: :GET,
+ path: ["api", @command],
+ q: [nokey: ""],
+ # Basic auth
+ auth: {@user<>"@"<>@domain, @userpass<>"bad"},
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :mod_http_api.process([@command], req)
+ assert 401 == elem(result, 0) # HTTP code
+
+ # Check that the command was executed only once
+ assert 1 ==
+ :meck.num_calls(:ejabberd_commands, :execute_command, :_)
+
+ assert :meck.validate :ejabberd_auth
+ assert :meck.validate :ejabberd_commands
+ #assert :ok = :meck.history(:ejabberd_commands)
+ end
+
+
+ test "HTTP GET simple command call with OAuth" do
+ EjabberdAuthMock.create_user @user, @domain, @userpass
+
+ # Mock a simple command() -> :ok
+ :meck.expect(:ejabberd_commands, :get_command_format,
+ fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) ->
+ {[], {:res, :rescode}}
+ end)
+ :meck.expect(:ejabberd_commands, :execute_command,
+ fn (:undefined, {@user, @domain, {:oauth, _token}, false},
+ @acommand, [], @version) ->
+ :ok
+ end)
+
+ #:ejabberd_logger.start
+ #:ejabberd_logger.set 5
+
+ # Correct OAuth call
+ token = EjabberdOauthMock.get_token @user, @domain, @command
+ req = request(method: :GET,
+ path: ["api", @command],
+ q: [nokey: ""],
+ # OAuth
+ auth: {:oauth, token, []},
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :mod_http_api.process([@command], req)
+ assert 200 == elem(result, 0) # HTTP code
+ assert "0" == elem(result, 2) # command result
+
+ # Wrong OAuth token
+ req = request(method: :GET,
+ path: ["api", @command],
+ q: [nokey: ""],
+ # OAuth
+ auth: {:oauth, "bad"<>token, []},
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :mod_http_api.process([@command], req)
+ assert 401 == elem(result, 0) # HTTP code
+
+ # Expired OAuth token
+ token = EjabberdOauthMock.get_token @user, @domain, @command, 1
+ :timer.sleep 1500
+ req = request(method: :GET,
+ path: ["api", @command],
+ q: [nokey: ""],
+ # OAuth
+ auth: {:oauth, token, []},
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :mod_http_api.process([@command], req)
+ assert 401 == elem(result, 0) # HTTP code
+
+ # Wrong OAuth scope
+ token = EjabberdOauthMock.get_token @user, @domain, "bad_command"
+ :timer.sleep 1500
+ req = request(method: :GET,
+ path: ["api", @command],
+ q: [nokey: ""],
+ # OAuth
+ auth: {:oauth, token, []},
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :mod_http_api.process([@command], req)
+ assert 401 == elem(result, 0) # HTTP code
+
+ # Check that the command was executed only once
+ assert 1 ==
+ :meck.num_calls(:ejabberd_commands, :execute_command, :_)
+
+ assert :meck.validate :ejabberd_auth
+ assert :meck.validate :ejabberd_commands
+ #assert :ok = :meck.history(:ejabberd_commands)
+ end
+
+
+end
diff --git a/test/mod_last_mock.exs b/test/mod_last_mock.exs
new file mode 100644
index 000000000..7e3dc5a1d
--- /dev/null
+++ b/test/mod_last_mock.exs
@@ -0,0 +1,65 @@
+ # mod_last mock
+ ######################
+
+
+defmodule ModLastMock do
+
+ require Record
+ Record.defrecord :session, Record.extract(:session,
+ from: "ejabberd_sm.hrl")
+ Record.defrecord :jid, Record.extract(:jid,
+ from: "jlib.hrl")
+
+ @author "jsautret@process-one.net"
+ @agent __MODULE__
+
+ def init do
+ try do
+ Agent.stop(@agent)
+ catch
+ :exit, _e -> :ok
+ end
+
+ {:ok, _pid} = Agent.start_link(fn -> %{} end, name: @agent)
+
+ mock(:mod_last, :get_last_info,
+ fn (user, domain) ->
+ Agent.get(@agent, fn last ->
+ case Map.get(last, {user, domain}, :not_found) do
+ {ts, status} -> {:ok, ts, status}
+ result -> result
+ end
+ end)
+ end)
+ end
+
+ def set_last(user, domain, status) do
+ set_last(user, domain, status, now)
+ end
+
+ def set_last(user, domain, status, timestamp) do
+ Agent.update(@agent, fn last ->
+ Map.put(last, {user, domain}, {timestamp, status})
+ end)
+ end
+
+ ####################################################################
+ # Helpers
+ ####################################################################
+ def now() do
+ {megasecs, secs, _microsecs} = :os.timestamp
+ megasecs * 1000000 + secs
+ end
+
+ # TODO refactor: Move to ejabberd_test_mock
+ def mock(module, function, fun) do
+ try do
+ :meck.new(module)
+ catch
+ :error, {:already_started, _pid} -> :ok
+ end
+
+ :meck.expect(module, function, fun)
+ end
+
+end
diff --git a/test/mod_roster_mock.exs b/test/mod_roster_mock.exs
new file mode 100644
index 000000000..b4991cfde
--- /dev/null
+++ b/test/mod_roster_mock.exs
@@ -0,0 +1,192 @@
+ # mod_roster mock
+ ######################
+
+defmodule ModRosterMock do
+ @author "jsautret@process-one.net"
+
+ require Record
+ Record.defrecord :roster, Record.extract(:roster,
+ from: "mod_roster.hrl")
+
+ @agent __MODULE__
+
+ def init(domain, module) do
+ try do
+ Agent.stop(@agent)
+ catch
+ :exit, _e -> :ok
+ end
+
+ {:ok, _pid} = Agent.start_link(fn -> %{} end, name: @agent)
+
+ mock_with_moka module
+
+ #:mod_roster.stop(domain)
+ :mod_roster.start(domain, [])
+ end
+
+ def mock_with_moka(module) do
+ try do
+
+ module_mock = :moka.start(module)
+ :moka.replace(module_mock, :mod_roster, :invalidate_roster_cache,
+ fn (_user, _server) ->
+ :ok
+ end)
+
+ :moka.load(module_mock)
+
+ roster_mock = :moka.start(:mod_roster)
+
+ :moka.replace(roster_mock, :gen_mod, :db_type,
+ fn (_host, _opts) ->
+ {:none}
+ end)
+
+ :moka.replace(roster_mock, :gen_iq_handler, :add_iq_handler,
+ fn (_module, _host, _ns, _m, _f, _iqdisc) ->
+ :ok
+ end)
+
+ :moka.replace(roster_mock, :gen_iq_handler, :remove_iq_handler,
+ fn (_module, _host, _ns) ->
+ :ok
+ end)
+
+ :moka.replace(roster_mock, :transaction,
+ fn (_server, function) ->
+ {:atomic, function.()}
+ end)
+
+ :moka.replace(roster_mock, :get_roster,
+ fn (user, domain) ->
+ to_records(get_roster(user, domain))
+ end)
+
+ :moka.replace(roster_mock, :update_roster_t,
+ fn (user, domain, {u, d, _r}, item) ->
+ add_roster_item(user, domain, u<>"@"<>d,
+ roster(item, :name),
+ roster(item, :subscription),
+ roster(item, :groups),
+ roster(item, :ask),
+ roster(item, :askmessage))
+ end)
+
+ :moka.replace(roster_mock, :del_roster_t,
+ fn (user, domain, jid) ->
+ remove_roster_item(user, domain, :jlib.jid_to_string(jid))
+ end)
+
+ :moka.load(roster_mock)
+
+ catch
+ {:already_started, _pid} -> :ok
+ end
+
+ end
+
+ def mock_with_meck do
+# mock(:gen_mod, :db_type,
+# fn (_server, :mod_roster) ->
+# :mnesia
+# end)
+#
+# mock(:mnesia, :transaction,
+# fn (_server, function) ->
+# {:atomic, function.()}
+# end)
+#
+# mock(:mnesia, :write,
+# fn (Item) ->
+# throw Item
+# {:atomic, :ok}
+# end)
+
+ mock(:mod_roster, :transaction,
+ fn (_server, function) ->
+ {:atomic, function.()}
+ end)
+
+ mock(:mod_roster, :update_roster_t,
+ fn (user, domain, {u, d, _r}, item) ->
+ add_roster_item(user, domain, u<>"@"<>d,
+ roster(item, :name),
+ roster(item, :subscription),
+ roster(item, :groups),
+ roster(item, :ask),
+ roster(item, :askmessage))
+ end)
+
+ mock(:mod_roster, :invalidate_roster_cache,
+ fn (_user, _server) ->
+ :ok
+ end)
+
+ end
+
+ def add_roster_item(user, domain, jid, nick, subs \\ :none, groups \\ [],
+ ask \\ :none, askmessage \\ "")
+ when is_binary(user) and byte_size(user) > 0
+ and is_binary(domain) and byte_size(domain) > 0
+ and is_binary(jid) and byte_size(jid) > 0
+ and is_binary(nick)
+ and is_atom(subs)
+ and is_list(groups)
+ and is_atom(ask)
+ and is_binary(askmessage)
+ do
+ Agent.update(@agent, fn roster ->
+ Map.put(roster, {user, domain, jid}, %{nick: nick,
+ subs: subs, groups: groups,
+ ask: ask, askmessage: askmessage})
+ end)
+ end
+
+ def remove_roster_item(user, domain, jid) do
+ Agent.update(@agent, fn roster ->
+ Map.delete(roster, {user, domain, jid})
+ end)
+ end
+
+ def get_rosters() do
+ Agent.get(@agent, fn roster -> roster end)
+ end
+
+ def get_roster(user, domain) do
+ Agent.get(@agent, fn roster ->
+ for {u, d, jid} <- Map.keys(roster), u == user, d == domain,
+ do: {{u, d, jid}, Map.fetch!(roster, {u, d, jid})}
+ end)
+ end
+
+ def to_record({{user, domain, jid}, r}) do
+ roster(usj: {user, domain, jid},
+ us: {user, domain},
+ jid: :jlib.string_to_usr(jid),
+ subscription: r.subs,
+ ask: r.ask,
+ groups: r.groups,
+ askmessage: r.askmessage
+ )
+ end
+ def to_records(rosters) do
+ for item <- rosters, do: to_record(item)
+ end
+
+####################################################################
+# Helpers
+####################################################################
+
+ # TODO refactor: Move to ejabberd_test_mock
+ def mock(module, function, fun) do
+ try do
+ :meck.new(module, [:non_strict, :passthrough, :unstick])
+ catch
+ :error, {:already_started, _pid} -> :ok
+ end
+
+ :meck.expect(module, function, fun)
+ end
+
+end