]> granicus.if.org Git - ejabberd/commitdiff
Commands refactor, first pass.
authorAlexey Shchepin <alexey@process-one.net>
Thu, 31 Mar 2016 11:53:31 +0000 (14:53 +0300)
committerAlexey Shchepin <alexey@process-one.net>
Thu, 31 Mar 2016 11:53:31 +0000 (14:53 +0300)
- add API versionning
- changed error handling, based on exception
- commands moved/merged from mod_admin_p1 to mod_admin_extra
- command bufixes
- add some elixir unit test cases

Squashed commit of the following:

commit dd59855b3486f78a9349756e4f102e79b3accff8
Merge: 14e8ffc 506e08e
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Oct 30 11:43:18 2015 +0100

    Merge branch '3.2.x' into api

commit 14e8ffce78cbea6c8605371d1fc50a0c1d1e012c
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Oct 27 16:35:17 2015 +0100

    Added OAuth tests to ejabberd_commands

commit f81c550c14628edfe4861c228576cb767924366a
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Oct 27 16:34:55 2015 +0100

    Added some mod_http_api tests

commit 6a64578d5b2ba532a2feb6503ed98561e56d5d53
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Mon Oct 26 15:29:36 2015 +0100

    Fix get_last command test

    Previous version won't work with dst.

commit 27e0cde9e9c1f001effe68f8424a365ad947c068
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Oct 23 17:59:34 2015 +0200

    Add tests on admin command policy

commit 19dad8d54f54c9fabd454280483cccfb06c8e78a
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Oct 23 16:49:36 2015 +0200

    Added command related tests (http api & user policy)

commit e0e596ab4a3f3a70aba5f374f028939ab794de33
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Oct 23 16:49:16 2015 +0200

    Fix command call.

commit 128cd7d1ede3c47a34f8ec3a750c980ccad2c61d
Merge: 60c4c4c 447313c
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Thu Oct 22 14:48:39 2015 +0200

    Merge branch '3.2.x' into api

commit 60c4c4c0751302524c14219c6bc8c56a6069a689
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Thu Oct 22 14:45:57 2015 +0200

    Fix ejabberd_commands spec.

commit 8e145c28c5da762c2b93ee32327eff1db94ebfed
Merge: 397273a f13dc94
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Wed Oct 21 18:26:07 2015 +0200

    Merge branch '3.2.x' into api

commit 397273a23ed415feac87aed33da6452229793387
Merge: c30e89b f289e27
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Wed Oct 21 15:27:45 2015 +0200

    Merge branch '3.2.x' into api

commit c30e89bb8a0013bff37e61e4c6953350c9c1f313
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Wed Oct 21 12:47:02 2015 +0200

    Merge mod_http_api

commit 7b0db22b4acd48ff6fabce41c1b2525e6580a3c5
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Oct 16 11:55:48 2015 +0200

    Fix exunit tests to run with common_test suites

commit d8b1a89800ac7379a57a7eb4a09c3c93c3e1e5eb
Merge: 2879ae8 63455b3
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Thu Oct 15 11:39:45 2015 +0200

    Merge branch '3.2.x' into api

commit 2879ae87ff3eee369ef3d780136b96ecff5285d1
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Wed Oct 14 14:53:44 2015 +0200

    Fix update_roster command.

commit a1d453dd7a3afda9861a8d747494a45057ad574b
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Oct 13 16:14:28 2015 +0200

    API commands refactor

    Moving and/or merging commands from mod_admin_p1 to mod_admin_extra

commit b709ed26b0fc0ca4f3bdd5a59fa58ec7e3db97fa
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Wed Oct 7 15:10:01 2015 +0200

    Add tests on commands

commit 6711687bee9c672cb3d5aed0744e13420ecf6dbd
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Sep 29 15:58:16 2015 +0200

    Add ejabberd_commands tests

commit df8682f419cf3877e77e36a19bca0fc55dc991f8
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Mon Sep 28 14:54:39 2015 +0200

    Added API versioning for ejabberdctl and rest commands

commit cd017b0e3aac431bc3ee807ceb7f8641e1523ef5
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Fri Sep 18 11:21:45 2015 +0200

    Better error handling of HTTP API commands.

commit ca5cb6acd8e4643f9d6c484d2277b0d7e88471e5
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Sep 15 15:03:05 2015 +0200

    add commands to mod_admin_extra:
    - get_offline_count
    - get_presence
    - change_password

commit 7f583fa099e30ac2b0915669fd8f102ac565b833
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Tue Sep 15 15:02:16 2015 +0200

    Improve REST API error handling

commit 14753b1c02cdce434a786b7f80f6c09f0d210075
Author: Jerome Sautret <jerome.sautret@process-one.net>
Date:   Mon Sep 14 10:51:17 2015 +0200

    Change REST API return codes for integer type.

18 files changed:
Makefile.in
include/ejabberd_commands.hrl
rebar.config
src/ejabberd_commands.erl
src/ejabberd_ctl.erl
src/mod_admin_extra.erl
src/mod_http_api.erl
test/ejabberd_admin_test.exs [new file with mode: 0644]
test/ejabberd_auth_mock.exs [new file with mode: 0644]
test/ejabberd_commands_test.exs
test/ejabberd_hooks_test.exs
test/ejabberd_oauth_mock.exs [new file with mode: 0644]
test/ejabberd_sm_mock.exs [new file with mode: 0644]
test/elixir_SUITE.erl
test/mod_admin_extra_test.exs [new file with mode: 0644]
test/mod_http_api_test.exs [new file with mode: 0644]
test/mod_last_mock.exs [new file with mode: 0644]
test/mod_roster_mock.exs [new file with mode: 0644]

index 0d9134485d7ba70f390c86f4822f90b0975eea84..28c05166e70455ab5f61c7e640e44b10ba0c38ed 100644 (file)
@@ -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
index 0742e3ba4d66ce8f52c79d9ca554ea2ea42943f2..5874b3d26c98b80976804d13388fc950c3bd4f59 100644 (file)
          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',
index 210e623ae6ba1a541b76569432e27a3ecfdfd571..dd88a4e5cfe99a99f5ed3725cfbbc2bb56e45182 100644 (file)
@@ -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"}}}}]}.
 
index 21872aa339f29a5d6139969243c146ad9127cf44..fd8ba03fe0a7d67c310270a0844e6b9e97c19beb 100644 (file)
@@ -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>
 %%%
 %%% 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
        ]).
 -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],
index bf4e4675a05cd3cde31d3d81a5e35b7c89f9bc2b..edec5a07e5a9a45cf312b6515e12b4ed2dc5d0a7 100644 (file)
@@ -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,
index f0e5671996b02a113c4ddac2b40ad17c9099503b..7962786af03724834b2095aed82990d5388fdf80 100644 (file)
 
 -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
index 15fe363642ffc972d071cb42adc327e3d3bbc8d1..f2b7a484b7246d9f2fe906aa58b7727065058a0b 100644 (file)
 %%    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 (file)
index 0000000..1c99931
--- /dev/null
@@ -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 (file)
index 0000000..495c527
--- /dev/null
@@ -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
index 0c06fc2cacda2628554925e6162d33cb8ff0ee66..b3f10000ee4b6f70acdf766900b05cad61e00fbb 100644 (file)
 # ----------------------------------------------------------------------
 
 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
index 6493642deee08bf2312343e7ce9e1e2bd2222aab..a69fbbd61abb7cc8912c53022429d9bc747445b5 100644 (file)
 # 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 (file)
index 0000000..2c1b8cf
--- /dev/null
@@ -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 (file)
index 0000000..0c2fc16
--- /dev/null
@@ -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
index b9a0b1a231101f4b42bfe48908a946d36b2e7840..f2c64773b533d4c88612cf15f2636eac0831db4f 100644 (file)
@@ -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 (file)
index 0000000..7fa39ee
--- /dev/null
@@ -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 (file)
index 0000000..ae62f28
--- /dev/null
@@ -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 (file)
index 0000000..7e3dc5a
--- /dev/null
@@ -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 (file)
index 0000000..b4991cf
--- /dev/null
@@ -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