diff --git a/src/http_p1.erl b/src/http_p1.erl new file mode 100644 index 000000000..1a8a1e630 --- /dev/null +++ b/src/http_p1.erl @@ -0,0 +1,337 @@ +%%%---------------------------------------------------------------------- +%%% File : http_p1.erl +%%% Author : Emilio Bustos +%%% Purpose : Provide a common API for inets / lhttpc / ibrowse +%%% Created : 29 Jul 2010 by Emilio Bustos +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2010 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License +%%% along with this program; if not, write to the Free Software +%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-module(http_p1). +-author('ebustos@process-one.net'). + +-export([ + start/0, + stop/0, + get/1, + get/2, + post/2, + post/3, + request/3, + request/4, + request/5 +]). + +% -define(USE_INETS, 1). +% -define(USE_LHTTPC, 1). +% -define(USE_IBROWSE, 1). +% inets used as default if none specified + +-ifdef(USE_IBROWSE). + -define(start(), start_ibrowse()). + -define(request(M, U, H, B, O), request_ibrowse(M, U, H, B, O)). + -define(stop(), stop_ibrowse()). +-else. + -ifdef(USE_LHTTPC). + -define(start(), start_lhttpc()). + -define(request(M, U, H, B, O), request_lhttpc(M, U, H, B, O)). + -define(stop(), stop_lhttpc()). + -else. + -define(start(), start_inets()). + -define(request(M, U, H, B, O), request_inets(M, U, H, B, O)). + -define(stop(), stop_inets()). + -endif. +-endif. + +-type header() :: {string() | atom(), string()}. +-type headers() :: [header()]. + +-type option() :: + {connect_timeout, timeout()} | + {timeout, timeout()} | + + {send_retry, non_neg_integer()} | + {partial_upload, non_neg_integer() | infinity} | + {partial_download, pid(), non_neg_integer() | infinity}. + +-type options() :: [option()]. + +-type result() :: {ok, {{pos_integer(), string()}, headers(), string()}} | + {error, atom()}. + +%% @spec () -> ok | {error, Reason} +%% Reason = term() +%% @doc +%% Start the application. +%% This is a helper function that will start the corresponding backend. +%% It allows the library to be started using the `-s' flag. +%% For instance: +%% `$ erl -s http_p1' +%% +%% @end +-spec start() -> ok | {error, any()}. +start() -> + ?start(). + +start_inets()-> + inets:start(), + ssl:start(). + +start_lhttpc()-> + application:start(crypto), + application:start(ssl), + lhttpc:start(). + +start_ibrowse()-> + ibrowse:start(), + ssl:start(). + +%% @spec () -> ok | {error, Reason} +%% Reason = term() +%% @doc +%% Stops the application. +%% This is a helper function that will stop the corresponding backend. +%% +%% @end +-spec stop() -> ok | {error, any()}. +stop() -> + ?stop(). + +stop_inets()-> + inets:stop(), + ssl:stop(). + +stop_lhttpc()-> + lhttpc:stop(), + application:stop(ssl). + +stop_ibrowse()-> + ibrowse:stop(). + +%% @spec (URL) -> Result +%% URL = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a GET request. +%% Would be the same as calling `request(get, URL, [])', +%% that is {@link request/3} with an empty header list. +%% @end +%% @see request/3 +-spec get(string()) -> result(). +get(URL) -> + request(get, URL, []). + +%% @spec (URL, Hdrs) -> Result +%% URL = string() +%% Hdrs = [{Header, Value}] +%% Header = string() +%% Value = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a GET request. +%% Would be the same as calling `request(get, URL, Hdrs)'. +%% @end +%% @see request/3 +-spec get(string(), headers()) -> result(). +get(URL, Hdrs) -> + request(get, URL, Hdrs). + +%% @spec (URL, RequestBody) -> Result +%% URL = string() +%% RequestBody = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a POST request with form data. +%% Would be the same as calling +%% `request(post, URL, [{"content-type", "x-www-form-urlencoded"}], Body)'. +%% @end +%% @see request/4 +-spec post(string(), string()) -> result(). +post(URL, Body) -> + request(post, URL, [{"content-type", "x-www-form-urlencoded"}], Body). + +%% @spec (URL, Hdrs, RequestBody) -> Result +%% URL = string() +%% Hdrs = [{Header, Value}] +%% Header = string() +%% Value = string() +%% RequestBody = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a POST request. +%% Would be the same as calling +%% `request(post, URL, Hdrs, Body)'. +%% @end +%% @see request/4 +-spec post(string(), headers(), string()) -> result(). +post(URL, Hdrs, Body) -> + NewHdrs = case [X || {X,_}<-Hdrs, string:to_lower(X) == "content-type"] of + [] -> + [{"content-type", "x-www-form-urlencoded"} | Hdrs]; + _ -> + Hdrs + end, + request(post, URL, NewHdrs, Body). + +%% @spec (Method, URL, Hdrs) -> Result +%% Method = atom() +%% URL = string() +%% Hdrs = [{Header, Value}] +%% Header = string() +%% Value = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a request without a body. +%% Would be the same as calling `request(Method, URL, Hdrs, [], [])', +%% that is {@link request/5} with an empty body. +%% @end +%% @see request/5 +-spec request(atom(), string(), headers()) -> result(). +request(Method, URL, Hdrs) -> + request(Method, URL, Hdrs, [], []). + +%% @spec (Method, URL, Hdrs, RequestBody) -> Result +%% Method = atom() +%% URL = string() +%% Hdrs = [{Header, Value}] +%% Header = string() +%% Value = string() +%% RequestBody = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a request with a body. +%% Would be the same as calling +%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5} +%% with no options. +%% @end +%% @see request/5 +-spec request(atom(), string(), headers(), string()) -> result(). +request(Method, URL, Hdrs, Body) -> + request(Method, URL, Hdrs, Body, []). + +%% @spec (Method, URL, Hdrs, RequestBody, Options) -> Result +%% Method = atom() +%% URL = string() +%% Hdrs = [{Header, Value}] +%% Header = string() +%% Value = string() +%% RequestBody = string() +%% Options = [Option] +%% Option = {timeout, Milliseconds | infinity} | +%% {connect_timeout, Milliseconds | infinity} | +%% {socket_options, [term()]} | + +%% Milliseconds = integer() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a request with a body. +%% Would be the same as calling +%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5} +%% with no options. +%% @end +%% @see request/5 +-spec request(atom(), string(), headers(), string(), options()) -> result(). +request(Method, URL, Hdrs, Body, Opts) -> +% ?DEBUG("Making request with headers: ~p~n~n", [Hdrs]), +% Headers = lists:map(fun({H, V}) -> +% H2 = if +% is_atom(H) -> +% string:to_lower(atom_to_list(H)); +% is_list(H) -> +% string:to_lower(H); +% true -> +% H +% end, +% {H2, V} +% end, Hdrs), + ?request(Method, URL, Hdrs, Body, Opts). + +request_inets(Method, URL, Hdrs, Body, Opts) -> + Request = case Method of + get -> + {URL, Hdrs}; + head -> + {URL, Hdrs}; + _ -> % post, etc. + {URL, Hdrs, proplists:get_value("content-type", Hdrs, []), Body} + end, + Options = case proplists:get_value(timeout, Opts, infinity) of + infinity -> + proplists:delete(timeout, Opts); + _ -> + Opts + end, + case http:request(Method, Request, Options, []) of + {ok, {{_, Status, _}, Headers, Response}} -> + {ok, Status, Headers, Response}; + {error, Reason} -> + {error, Reason} + end. + +request_lhttpc(Method, URL, Hdrs, Body, Opts) -> + TimeOut = proplists:get_value(timeout, Opts, infinity), + SockOpt = proplists:get_value(socket_options, Opts, []), + Options = [{connect_options, SockOpt} | proplists:delete(timeout, Opts)], + case lhttpc:request(URL, Method, Hdrs, Body, TimeOut, Options) of + {ok, {{Status, _Reason}, Headers, Response}} -> + {ok, Status, Headers, binary_to_list(Response)}; + {error, Reason} -> + {error, Reason} + end. + +request_ibrowse(Method, URL, Hdrs, Body, Opts) -> + TimeOut = proplists:get_value(timeout, Opts, infinity), + Options = [{inactivity_timeout, TimeOut} | proplists:delete(timeout, Opts)], + case ibrowse:send_req(URL, Hdrs, Method, Body, Options) of + {ok, Status, Headers, Response} -> + {ok, list_to_integer(Status), Headers, Response}; + {error, Reason} -> + {error, Reason} + end. + +% ibrowse {response_format, response_format()} | +% Options - [option()] +% Option - {sync, boolean()} | {stream, StreamTo} | {body_format, body_format()} | {full_result, +% boolean()} | {headers_as_is, boolean()} +%body_format() = string() | binary() +% The body_format option is only valid for the synchronous request and the default is string. +% When making an asynchronous request the body will always be received as a binary. +% lhttpc: always binary diff --git a/src/mod_admin_p1.erl b/src/mod_admin_p1.erl new file mode 100644 index 000000000..bcfc8232c --- /dev/null +++ b/src/mod_admin_p1.erl @@ -0,0 +1,1166 @@ +%%%------------------------------------------------------------------- +%%% File : mod_admin_p1.erl +%%% Author : Badlop / Mickael Remond / Christophe Romain +%%% Purpose : Administrative functions and commands for ProcessOne customers +%%% Created : 21 May 2008 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2008 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License +%%% along with this program; if not, write to the Free Software +%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +%%% @doc Administrative functions and commands for ProcessOne customers +%%% +%%% This ejabberd module defines and registers many ejabberd commands +%%% that can be used for performing administrative tasks in ejabberd. +%%% +%%% The documentation of all those commands can be read using ejabberdctl +%%% in the shell. +%%% +%%% The commands can be executed using any frontend to ejabberd commands. +%%% Currently ejabberd_xmlrpc and ejabberdctl. Using ejabberd_xmlrpc it is possible +%%% to call any ejabberd command. However using ejabberdctl not all commands +%%% can be called. + +%%% Changelog: +%%% +%%% 0.8 - 26 September 2008 - badlop +%%% - added patch for parameter 'Push' +%%% +%%% 0.7 - 20 August 2008 - badlop +%%% - module converted to ejabberd commands +%%% +%%% 0.6 - 02 June 2008 - cromain +%%% - add user existance checking +%%% - improve parameter checking +%%% - allow orderless parameter +%%% +%%% 0.5 - 17 March 2008 - cromain +%%% - add user changing and higher level methods +%%% +%%% 0.4 - 18 February 2008 - cromain +%%% - add roster handling +%%% - add message sending +%%% - code and api clean-up +%%% +%%% 0.3 - 18 October 2007 - cromain +%%% - presence improvement +%%% - add new functionality +%%% +%%% 0.2 - 4 March 2006 - mremond +%%% - Code clean-up +%%% - Made it compatible with current ejabberd SVN version +%%% +%%% 0.1.2 - 28 December 2005 +%%% - Now compatible with ejabberd 1.0.0 +%%% - The XMLRPC server is started only once, not once for every virtual host +%%% - Added comments for handlers. Every available handler must be explained +%%% + +-module(mod_admin_p1). +-author('ProcessOne'). + +-export([start/2, stop/1, + %% Erlang + restart_module/2, + %% Accounts + create_account/3, + delete_account/2, + change_password/3, + rename_account/4, + check_users_registration/1, + %% Sessions + get_presence/2, + get_resources/2, + %% Vcard + set_nickname/3, + %% Roster + add_rosteritem/6, + delete_rosteritem/3, + link_contacts/4, + unlink_contacts/2, + link_contacts/5, unlink_contacts/3, % Versions with Push parameter + get_roster/2, + get_roster_with_presence/2, + add_contacts/3, + remove_contacts/3, + %% PubSub + update_status/4, + delete_status/3, + %% Transports + transport_register/5, + %% Stanza + send_chat/3, + send_message/4, + send_notification/6, %% ON + send_stanza/3 + ]). + +-include("ejabberd.hrl"). +-include("ejabberd_commands.hrl"). +-include("mod_roster.hrl"). +-include("jlib.hrl"). + +-ifdef(EJABBERD1). +-record(session, {sid, usr, us, priority}). %% ejabberd 1.1.x +-else. +-record(session, {sid, usr, us, priority, info}). %% ejabberd 2.x.x +-endif. + +start(_Host, _Opts) -> + ejabberd_commands:register_commands(commands()). + +stop(_Host) -> + ejabberd_commands:unregister_commands(commands()). + +%%% +%%% Register commands +%%% + +commands() -> + [ + #ejabberd_commands{name = restart_module, tags = [erlang], + desc = "Stop an ejabberd module, reload code and start", + module = ?MODULE, function = restart_module, + args = [{module, string}, {host, string}], + result = {res, rescode}}, + + %% Similar to ejabberd_admin register + #ejabberd_commands{name = create_account, tags = [accounts], + desc = "Create an ejabberd user account", + longdesc = "This command is similar to 'register'.", + module = ?MODULE, function = create_account, + args = [{user, string}, {server, string}, + {password, string}], + result = {res, integer}}, + + %% Similar to ejabberd_admin unregister + #ejabberd_commands{name = delete_account, tags = [accounts], + desc = "Remove an account from the server", + longdesc = "This command is similar to 'unregister'.", + module = ?MODULE, function = delete_account, + args = [{user, string}, {server, string}], + result = {res, integer}}, + + #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. " + "Offline messages and private storage are lost.", + module = ?MODULE, function = rename_account, + args = [{user, string}, {server, string}, + {newuser, string}, {newserver, string}], + result = {res, integer}}, + + %% This command is also implemented in mod_admin_contrib + #ejabberd_commands{name = change_password, tags = [accounts], + desc = "Change the password on behalf of the given user", + module = ?MODULE, function = change_password, + args = [{user, string}, {server, string}, + {newpass, string}], + result = {res, integer}}, + + %% This command is also implemented in mod_admin_contrib + #ejabberd_commands{name = set_nickname, tags = [vcard], + desc = "Define user nickname", + longdesc = "Set/updated nickname in the user Vcard. " + "Other informations are unchanged.", + module = ?MODULE, function = set_nickname, + args = [{user, string}, {server, string}, {nick,string}], + result = {res, integer}}, + + %% This command is also implemented in mod_admin_contrib + #ejabberd_commands{name = add_rosteritem, tags = [roster], + desc = "Add an entry in a user's roster", + longdesc = "Some arguments are:\n" + " - jid: the JabberID of the user you would " + "like to add in user roster on the server.\n" + " - subs: the state of the roster item subscription.\n\n" + "The allowed values of the 'subs' argument are: both, to, from or none.\n" + " - none: presence packets are not sent between parties.\n" + " - both: presence packets are sent in both direction.\n" + " - to: the user sees the presence of the given JID.\n" + " - from: the JID specified sees the user presence.\n\n" + "Don't forget that roster items should keep symmetric: " + "when adding a roster item for a user, " + "you have to do the symmetric roster item addition.\n\n", + module = ?MODULE, function = add_rosteritem, + args = [{user, string}, {server, string}, {jid, string}, + {group, string}, {nick, string}, {subs, string}], + result = {res, integer}}, + + %% This command is also implemented in mod_admin_contrib + #ejabberd_commands{name = delete_rosteritem, tags = [roster], + desc = "Remove an entry for a user roster", + longdesc = "Roster items should be kept symmetric: " + "when removing a roster item for a user you have to do " + "the symmetric roster item removal. \n\n" + "This mechanism bypass the standard roster approval " + "addition mechanism and should only be used for server " + "administration or server integration purpose.", + module = ?MODULE, function = delete_rosteritem, + args = [{user, string}, {server, string}, {jid, string}], + result = {res, integer}}, + + #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.\n" + "nick1 is the nick of user1.\n" + "jid2 is the JabberID of the user2 you would like to " + "add in user1 roster on the server.\n" + "nick2 is the nick of user2.\n\n" + "This mechanism bypass 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, string}, {nick1, string}, {jid2, string}, {nick2, string}], + 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.\n" + "jid2 is the JabberID of the user2.\n\n" + "This mechanism bypass the standard roster approval " + "addition mechanism " + "and should only be userd for server administration or " + "server integration purpose.", + module = ?MODULE, function = unlink_contacts, + args = [{jid1, string}, {jid2, string}], + result = {res, integer}}, + + %% TODO: test + %% This command is not supported by ejabberdctl + #ejabberd_commands{name = add_contacts, tags = [roster], + desc = "Call add_rosteritem with subscription \"both\" " + "for a given list of contacts", + module = ?MODULE, function = add_contacts, + args = [{user, string}, + {server, string}, + {contacts, {list, + {contact, {tuple, [ + {jid, string}, + {group, string}, + {nick, string} + ]}} + }} + ], + result = {res, integer}}, + %% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, add_contacts, [{struct, + %% [{user, "badlop"}, + %% {server, "localhost"}, + %% {contacts, {array, [{struct, [ + %% {contact, {array, [{struct, [ + %% {group, "Friends"}, + %% {jid, "tom@localhost"}, + %% {nick, "Tom"} + %% ]}]}} + %% ]}]}} + %% ] + %% }]}). + + %% TODO: test + %% This command is not supported by ejabberdctl + #ejabberd_commands{name = remove_contacts, tags = [roster], + desc = "Call del_rosteritem for a list of contacts", + module = ?MODULE, function = remove_contacts, + args = [{user, string}, + {server, string}, + {contacts, {list, + {jid, string} + }} + ], + result = {res, integer}}, + %% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, remove_contacts, [{struct, + %% [{user, "badlop"}, + %% {server, "localhost"}, + %% {contacts, {array, [{struct, [ + %% {jid, "tom@localhost"} + %% ]}]}} + %% ] + %% }]}). + + %% TODO: test + %% This command is not supported by ejabberdctl + #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, string}, + {server, string} + ]}} + }} + ], + result = {users, {list, + {auser, {tuple, [ + {user, string}, + {server, string}, + {status, integer} + ]}} + }}}, + %% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, check_users_registration, [{struct, + %% [{users, {array, [{struct, [ + %% {auser, {array, [{struct, [ + %% {user, "badlop"}, + %% {server, "localhost"} + %% ]}]}} + %% ]}]}}] + %% }]}). + + %% This command is also implemented in mod_admin_contrib + #ejabberd_commands{name = get_roster, tags = [roster], + desc = "Retrieve the roster for a given user", + longdesc = "Returns a list of the contacts in a user " + "roster.\n\n" + "Also returns the state of the contact subscription. " + "Subscription can be either " + " \"none\", \"from\", \"to\", \"both\". " + "Pending can be \"in\", \"out\" or \"none\".", + module = ?MODULE, function = get_roster, + args = [{user, string}, {server, string}], + result = {contacts, {list, {contact, {tuple, [{jid, string}, {group, string}, + {nick, string}, {subscription, string}, {pending, string}]}}}}}, + + #ejabberd_commands{name = get_roster_with_presence, tags = [roster], + desc = "Retrieve the roster for a given user including " + "presence information", + longdesc = "The 'show' value contains the user presence. " + "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.\n\n" + "Also returns the state of the contact subscription. " + "Subscription can be either " + "\"none\", \"from\", \"to\", \"both\". " + "Pending can be \"in\", \"out\" or \"none\".\n\n" + "Note: If user is connected several times, only keep the" + " resource with the highest non-negative priority.", + module = ?MODULE, function = get_roster_with_presence, + args = [{user, string}, {server, string}], + result = {contacts, {list, {contact, {tuple, [{jid, string}, {resource, string}, {group, string}, {nick, string}, {subscription, string}, {pending, string}, {show, string}, {status, 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.\n" + "The '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, string}, {server, string}], + result = {presence, {tuple, [{jid, string}, + {show, string}, + {status, string}]}}}, + + #ejabberd_commands{name = get_resources, tags = [session], + desc = "Get all available resources for a given user", + module = ?MODULE, function = get_resources, + args = [{user, string}, {server, string}], + result = {resources, {list, {resource, string}}}}, + + %% PubSub + #ejabberd_commands{name = update_status, tags = [pubsub], + desc = "Update the status on behalf of a user", + longdesc = + "jid: the JabberID of the user. Example: user@domain.\n\n" + "node: the reference of the node to publish on.\n" + "Example: http://process-one.net/protocol/availability\n\n" + "itemid: the reference of the item (in our case profile ID).\n\n" + "payload: the payload of the publish operation in XML.\n" + "The string has to be properly escaped to comply with XML formalism of XML RPC.", + module = ?MODULE, function = update_status, + args = [{jid, string}, {node, string}, {itemid, string}, {payload, string}], + result = {res, string}}, + + #ejabberd_commands{name = delete_status, tags = [pubsub], + desc = "Delete the status on behalf of a user", + longdesc = + "jid: the JabberID of the user. Example: user@domain.\n\n" + "node: the reference of the node to publish on.\n" + "Example: http://process-one.net/protocol/availability\n\n" + "itemid: the reference of the item (in our case profile ID).", + module = ?MODULE, function = delete_status, + args = [{jid, string}, {node, string}, {itemid, string}], + result = {res, string}}, + + #ejabberd_commands{name = transport_register, tags = [transports], + desc = "Register a user in a transport", + module = ?MODULE, function = transport_register, + args = [{host, string}, {transport, string}, + {jidstring, string}, {username, string}, {password, string}], + result = {res, string}}, + + %% Similar to mod_admin_contrib send_message which sends a headline + #ejabberd_commands{name = send_chat, tags = [stanza], + desc = "Send chat message to a given user", + module = ?MODULE, function = send_chat, + args = [{from, string}, {to, string}, {body, string}], + result = {res, integer}}, + + #ejabberd_commands{name = send_message, tags = [stanza], + desc = "Send normal message to a given user", + module = ?MODULE, function = send_message, + args = [{from, string}, {to, string}, + {subject, string}, {body, string}], + result = {res, integer}}, + + #ejabberd_commands{name = send_notification, tags = [stanza], + desc = "Send ON notification to XMPP client sessions", + module = ?MODULE, function = send_notification, + args = [{send_from, string}, {send_to, string}, {host, string}, + {unread_items, string}, {message, string}, + {type, string}], + result = {res, integer}}, + + #ejabberd_commands{name = send_stanza, tags = [stanza], + desc = "Send stanza to a given user", + longdesc = "If Stanza contains a \"from\" field, " + "then it overrides the passed from argument." + "If Stanza contains a \"to\" field, then it overrides " + "the passed to argument.", + module = ?MODULE, function = send_stanza, + args = [{user, string}, {server, string}, + {stanza, string}], + result = {res, integer}} + ]. + + +%%% +%%% Erlang +%%% + +restart_module(ModuleString, Host) -> + Module = list_to_atom(ModuleString), + List = gen_mod:loaded_modules_with_opts(Host), + Opts = case lists:keysearch(Module,1, List) of + {value, {_, O}} -> O; + _ -> [] + end, + gen_mod:stop_module(Host, Module), + code:delete(Module), + code:purge(Module), + gen_mod:start_module(Host, Module, Opts), + ok. + + +%%% +%%% Accounts +%%% + +create_account(U, S, P) -> + case ejabberd_auth:try_register(U, S, P) of + {atomic, ok} -> + 0; + {atomic, exists} -> + 409; + _ -> + 1 + end. + +delete_account(U, S) -> + Fun = fun() -> ejabberd_auth:remove_user(U, S) end, + user_action(U, S, Fun, ok). + +change_password(U, S, P) -> + Fun = fun() -> ejabberd_auth:set_password(U, S, P) end, + user_action(U, S, Fun, ok). + +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, true), + unlink_contacts2(OldJID, JIDStr, true) + end, Roster), + ejabberd_auth:remove_user(U, S), + 0; + {atomic, exists} -> + 409; + _ -> + 1 + end + end; + false -> + 404 + end. + + +%%% +%%% Sessions +%%% + +get_presence(U, S) -> + case ejabberd_auth:is_user_exists(U, S) of + true -> + {Resource, Show, Status} = get_presence2(U, S), + FullJID = case Resource of + [] -> + lists:flatten([U,"@",S]); + _ -> + lists:flatten([U,"@",S,"/",Resource]) + end, + {FullJID, Show, Status}; + false -> + 404 + end. + +get_resources(U, S) -> + case ejabberd_auth:is_user_exists(U, S) of + true -> + get_resources2(U, S); + false -> + 404 + end. + + +%%% +%%% Vcard +%%% + +set_nickname(U, S, N) -> + Fun = fun() -> case mod_vcard:process_sm_iq( + {jid, U, S, "", U, S, ""}, + {jid, U, S, "", U, S, ""}, + {iq, "", set, "", "en", + {xmlelement, "vCard", + [{"xmlns", "vcard-temp"}], [ + {xmlelement, "NICKNAME", [], [{xmlcdata, N}]} + ] + }}) of + {iq, [], result, [], _, []} -> ok; + _ -> error + end + end, + user_action(U, S, Fun, ok). + + +%%% +%%% Roster +%%% + +add_rosteritem(U, S, JID, G, N, Subs) -> + add_rosteritem(U, S, JID, G, N, Subs, true). + +add_rosteritem(U, S, JID, G, N, Subs, Push) -> + Fun = fun() -> add_rosteritem2(U, S, JID, N, G, Subs, Push) end, + user_action(U, S, Fun, {atomic, ok}). + +link_contacts(JID1, Nick1, JID2, Nick2) -> + link_contacts(JID1, Nick1, JID2, Nick2, true). + +link_contacts(JID1, Nick1, JID2, Nick2, Push) -> + {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, JID2, Nick2, Push) of + {atomic, ok} -> + 0; + _ -> + 1 + end; + _ -> + 404 + end. + +delete_rosteritem(U, S, JID) -> + Fun = fun() -> del_rosteritem(U, S, JID) end, + user_action(U, S, Fun, {atomic, ok}). + +unlink_contacts(JID1, JID2) -> + unlink_contacts(JID1, JID2, true). + +unlink_contacts(JID1, JID2, Push) -> + {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, Push) of + {atomic, ok} -> + 0; + _ -> + 1 + end; + _ -> + 404 + end. + +get_roster(U, S) -> + case ejabberd_auth:is_user_exists(U, S) of + true -> + format_roster(get_roster2(U, S)); + false -> + 404 + end. + +get_roster_with_presence(U, S) -> + case ejabberd_auth:is_user_exists(U, S) of + true -> + format_roster_with_presence(get_roster2(U, S)); + false -> + 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, true) of + {atomic, ok} -> Acc; + _ -> 1 + 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, true) of + {atomic, ok} -> Acc; + _ -> 1 + end; + false -> + Acc + end + end, 0, Contacts); + 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). + + +%%% +%%% PubSub +%%% + +update_status(JidString, NodeString, Itemid, PayloadString) -> + Publisher = jlib:string_to_jid(JidString), + Host = jlib:jid_tolower(jlib:jid_remove_resource(Publisher)), + ServerHost = Publisher#jid.lserver, + Node = mod_pubsub_on:string_to_node(NodeString), + Payload = [xml_stream:parse_element(PayloadString)], + ?DEBUG("PayloadString: ~n~p~nPayload elements: ~n~p", [PayloadString, Payload]), + case mod_pubsub_on:publish_item_nothook(Host, ServerHost, Node, Publisher, Itemid, Payload) of + {result, _} -> + "OK"; + {error, {xmlelement, _, _, _} = XmlEl} -> + "ERROR: " ++ xml:element_to_string(XmlEl); + {error, ErrorAtom} when is_atom(ErrorAtom) -> + "ERROR: " ++ atom_to_list(ErrorAtom); + {error, ErrorString} when is_list(ErrorString) -> + "ERROR: " ++ ErrorString + end. + +delete_status(JidString, NodeString, Itemid) -> + Publisher = jlib:string_to_jid(JidString), + Host = jlib:jid_tolower(jlib:jid_remove_resource(Publisher)), + Node = mod_pubsub_on:string_to_node(NodeString), + case mod_pubsub_on:delete_item_nothook(Host, Node, Publisher, Itemid, true) of + {result, _} -> + "OK"; + {error, {xmlelement, _, _, _} = XmlEl} -> + "ERROR: " ++ xml:element_to_string(XmlEl); + {error, ErrorAtom} when is_atom(ErrorAtom) -> + "ERROR: " ++ atom_to_list(ErrorAtom); + {error, ErrorString} when is_list(ErrorString) -> + "ERROR: " ++ ErrorString + end. + +transport_register(Host, TransportString, JIDString, Username, Password) -> + TransportAtom = list_to_atom(TransportString), + case {lists:member(Host, ?MYHOSTS), jlib:string_to_jid(JIDString)} of + {true, JID} when is_record(JID, jid) -> + case catch gen_transport:register(Host, TransportAtom, JIDString, + Username, Password) of + ok -> + "OK"; + {error, Reason} -> + "ERROR: " ++ atom_to_list(Reason); + {'EXIT', {timeout,_}} -> + "ERROR: timed_out"; + {'EXIT', _} -> + "ERROR: unexpected_error" + end; + {false, _} -> + "ERROR: unknown_host"; + _ -> + "ERROR: bad_jid" + end. + +%%% +%%% Stanza +%%% + +send_chat(FromJID, ToJID, Msg) -> + From = jlib:string_to_jid(FromJID), + To = jlib:string_to_jid(ToJID), + Stanza = {xmlelement, "message", [{"type", "chat"}], + [{xmlelement, "body", [], [{xmlcdata, Msg}]}]}, + ejabberd_router:route(From, To, Stanza), + 0. + +send_message(FromJID, ToJID, Sub, Msg) -> + From = jlib:string_to_jid(FromJID), + To = jlib:string_to_jid(ToJID), + Stanza = {xmlelement, "message", [{"type", "normal"}], + [{xmlelement, "subject", [], [{xmlcdata, Sub}]}, + {xmlelement, "body", [], [{xmlcdata, Msg}]}]}, + ejabberd_router:route(From, To, Stanza), + 0. + +send_notification(SendFromUsername, SendToUsername, Host, UnreadItemsInteger, MessageBody, Type) -> + case get_resources(SendToUsername, Host) of + 404 -> + -1; + [] -> + -2; + [A|_] when is_list(A) -> + send_notification_really(SendFromUsername, SendToUsername, Host, UnreadItemsInteger, MessageBody, Type), + 0 + end. + +send_notification_really(SendFromUsername, SendToUsername, Host, UnreadItemsInteger, MessageBody, Type) -> + FromString = Host ++ "/voicemail-notifier", + ToString = SendToUsername ++ "@" ++ Host, + + XAttrs = [{"type", Type}, + {"send_from", SendFromUsername}, + {"unread_items", UnreadItemsInteger}], + XChildren = [{xmlelement, "text", [], [{xmlcdata, MessageBody}]}], + XEl = {xmlelement, "x", XAttrs, XChildren}, + + Attrs = [{"from", FromString}, {"to", ToString}, {"type", "chat"}], + Children = [XEl], + Stanza = {xmlelement, "message", Attrs, Children}, + + From = jlib:string_to_jid(FromString), + To = jlib:string_to_jid(ToString), + ejabberd_router:route(From, To, Stanza). + +send_stanza(FromJID, ToJID, StanzaStr) -> + case xml_stream:parse_element(StanzaStr) of + {error, _} -> + 1; + Stanza -> + {xmlelement, _, Attrs, _} = Stanza, + From = jlib:string_to_jid(proplists:get_value("from", Attrs, FromJID)), + To = jlib:string_to_jid(proplists:get_value("to", Attrs, ToJID)), + ejabberd_router:route(From, To, Stanza), + 0 + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Internal functions + +%% ----------------------------- +%% Internal roster handling +%% ----------------------------- + +get_roster2(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + case roster_backend(LServer) of + mnesia -> mod_roster:get_user_roster([], {LUser, LServer}); + odbc -> mod_roster_odbc:get_user_roster([], {LUser, LServer}) + end. + +add_rosteritem2(User, Server, JID, Nick, Group, Subscription, Push) -> + {RU, RS, _} = jlib:jid_tolower(jlib:string_to_jid(JID)), + LJID = {RU,RS,[]}, + Groups = case Group of + [] -> []; + _ -> [Group] + end, + Roster = #roster{ + usj = {User,Server,LJID}, + us = {User,Server}, + jid = LJID, + name = Nick, + ask = none, + subscription = list_to_atom(Subscription), + groups = Groups}, + Result = + case roster_backend(Server) of + mnesia -> + mnesia:transaction(fun() -> + case mnesia:read({roster,{User,Server,LJID}}) of + [#roster{subscription=both}] -> + already_added; + _ -> + mnesia:write(Roster) + end + end); + odbc -> + %% MREMOND: TODO: check if already_added + case ejabberd_odbc:sql_transaction(Server, + fun() -> + Username = ejabberd_odbc:escape(User), + SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), + case ejabberd_odbc:sql_query_t( + ["select username from rosterusers " + " where username='", Username, "' " + " and jid='", SJID, + "' and subscription = 'B';"]) of + {selected, ["username"],[]} -> + ItemVals = record_to_string(Roster), + ItemGroups = groups_to_string(Roster), + odbc_queries:update_roster(Server, Username, + SJID, ItemVals, + ItemGroups); + _ -> + already_added + end + end) of + {atomic, already_added} -> {atomic, already_added}; + {atomic, _} -> {atomic, ok}; + Error -> Error + end + end, + case {Result, Push} of + {{atomic, already_added}, _} -> ok; %% No need for roster push + {{atomic, ok}, true} -> roster_push(User, Server, JID, Nick, Subscription, Groups); + {{atomic, ok}, false} -> ok; + _ -> error + end, + Result. + +del_rosteritem(User, Server, JID) -> + del_rosteritem(User, Server, JID, true). + +del_rosteritem(User, Server, JID, Push) -> + {RU, RS, _} = jlib:jid_tolower(jlib:string_to_jid(JID)), + LJID = {RU,RS,[]}, + Result = case roster_backend(Server) of + mnesia -> + mnesia:transaction(fun() -> + mnesia:delete({roster, {User,Server,LJID}}) + end); + odbc -> + case ejabberd_odbc:sql_transaction(Server, fun() -> + Username = ejabberd_odbc:escape(User), + SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), + odbc_queries:del_roster(Server, Username, SJID) + end) of + {atomic, _} -> {atomic, ok}; + Error -> Error + end + end, + case {Result, Push} of + {{atomic, ok}, true} -> roster_push(User, Server, JID, "", "remove", []); + {{atomic, ok}, false} -> ok; + _ -> error + end, + Result. + +link_contacts2(JID1, Nick1, JID2, Nick2, Push) -> + link_contacts2(JID1, Nick1, [], JID2, Nick2, [], Push). + +link_contacts2(JID1, Nick1, Group1, JID2, Nick2, Group2, Push) -> + {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", Push) of + {atomic, ok} -> add_rosteritem2(U2, S2, JID1, Nick1, Group2, "both", Push); + Error -> Error + end. + +unlink_contacts2(JID1, JID2, Push) -> + {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID1)), + {U2, S2, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)), + case del_rosteritem(U1, S1, JID2, Push) of + {atomic, ok} -> del_rosteritem(U2, S2, JID1, Push); + Error -> Error + end. + +roster_push(User, Server, JID, Nick, Subscription, Groups) -> + LJID = jlib:make_jid(User, Server, ""), + TJID = jlib:string_to_jid(JID), + {TU, TS, _} = jlib:jid_tolower(TJID), + + %% TODO: Problem: We assume that both user are local. More test + %% are needed to check if the JID is remote or not: + + %% TODO: We need to probe user2 especially, if it is not local. + %% As a quick fix, I do not go for the probe solution however, because all users + %% are local + case Subscription of + "to" -> %% Probe second user to route his presence to modified user + %% TODO: For now we assume both user are local so we do not, but we need to move to probe. + set_roster(User, Server, TJID, Nick, Subscription, Groups); + "from" -> + %% Send roster updates + set_roster(User, Server, TJID, Nick, Subscription, Groups); + "both" -> + %% Update both presence + set_roster(User, Server, TJID, Nick, Subscription, Groups), + UJID = jlib:make_jid(User, Server, ""), + set_roster(TU, TS, UJID, Nick, Subscription, Groups); + _ -> + %% Remove subscription + set_roster(User, Server, TJID, Nick, "none", Groups) + end. + + +set_roster(User, Server, TJID, Nick, Subscription, Groups) -> + GroupsXML = [{xmlelement, "group", [], [{xmlcdata, GroupString}]} || GroupString <- Groups], + Item = case Nick of + "" -> [{"jid", jlib:jid_to_string(TJID)}, {"subscription", Subscription}]; + _ -> [{"jid", jlib:jid_to_string(TJID)}, {"name", Nick}, {"subscription", Subscription}] + end, + Result = jlib:iq_to_xml(#iq{type = set, xmlns = ?NS_ROSTER, id = "push", + sub_el = [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}], + [{xmlelement, "item", Item, GroupsXML}]}]}), + lists:foreach(fun(Session) -> + JID = jlib:make_jid(Session#session.usr), + ejabberd_router:route(JID, JID, Result), + PID = element(2, Session#session.sid), + ejabberd_c2s:add_rosteritem(PID, TJID, list_to_atom(Subscription)) %% TODO: Better error management + end, get_sessions(User, Server)). + + +roster_backend(Server) -> + case lists:member(mod_roster, gen_mod:loaded_modules(Server)) of + true -> mnesia; + _ -> odbc % we assume that + end. + +record_to_string(#roster{us = {User, _Server}, + jid = JID, + name = Name, + subscription = Subscription, + ask = Ask, + askmessage = AskMessage}) -> + Username = ejabberd_odbc:escape(User), + SJID = ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))), + Nick = ejabberd_odbc:escape(Name), + SSubscription = case Subscription of + both -> "B"; + to -> "T"; + from -> "F"; + none -> "N" + end, + SAsk = case Ask of + subscribe -> "S"; + unsubscribe -> "U"; + both -> "B"; + out -> "O"; + in -> "I"; + none -> "N" + end, + SAskMessage = ejabberd_odbc:escape(AskMessage), + ["'", Username, "'," + "'", SJID, "'," + "'", Nick, "'," + "'", SSubscription, "'," + "'", SAsk, "'," + "'", SAskMessage, "'," + "'N', '', 'item'"]. + +groups_to_string(#roster{us = {User, _Server}, + jid = JID, + groups = Groups}) -> + Username = ejabberd_odbc:escape(User), + SJID = ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))), + %% Empty groups do not need to be converted to string to be inserted in + %% the database + lists:foldl(fun([], Acc) -> Acc; + (Group, Acc) -> + String = ["'", Username, "'," + "'", SJID, "'," + "'", ejabberd_odbc:escape(Group), "'"], + [String|Acc] + end, [], Groups). + +%% Format roster items as a list of: +%% [{struct, [{jid, "test@localhost"},{group, "Friends"},{nick, "Nicktest"}]}] +format_roster([]) -> + []; +format_roster(Items) -> + format_roster(Items, []). +format_roster([], Structs) -> + Structs; +format_roster([#roster{jid=JID, name=Nick, groups=Group, + subscription=Subs, ask=Ask}|Items], Structs) -> + {User,Server,_Resource} = JID, + Struct = {lists:flatten([User,"@",Server]), + extract_group(Group), + Nick, + atom_to_list(Subs), + atom_to_list(Ask) + }, + format_roster(Items, [Struct|Structs]). + +%% Note: If user is connected several times, only keep the resource with the +%% highest non-negative priority +format_roster_with_presence([]) -> + []; +format_roster_with_presence(Items) -> + format_roster_with_presence(Items, []). +format_roster_with_presence([], Structs) -> + Structs; +format_roster_with_presence([#roster{jid=JID, name=Nick, groups=Group, + subscription=Subs, ask=Ask}|Items], Structs) -> + {User,Server,_R} = JID, + Presence = case Subs of + both -> get_presence2(User, Server); + from -> get_presence2(User, Server); + _Other -> {"", "unavailable", ""} + end, + {Resource, Show, Status} = + case Presence of + {_R, "invisible", _S} -> {"", "unavailable", ""}; + _Status -> Presence + end, + Struct = {lists:flatten([User,"@",Server]), + Resource, + extract_group(Group), + Nick, + atom_to_list(Subs), + atom_to_list(Ask), + Show, + Status + }, + format_roster_with_presence(Items, [Struct|Structs]). + +extract_group([]) -> []; +extract_group([Group|_Groups]) -> Group. + +%% ----------------------------- +%% Internal session handling +%% ----------------------------- + +%% This is inspired from ejabberd_sm.erl +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} = rpc:call(node(Pid), ejabberd_c2s, get_presence, [Pid]), + {_User, Resource, Show, Status} = ejabberd_c2s:get_presence(Pid), + {Resource, Show, Status}; + true -> + {"", "unavailable", ""} + end + end. + +get_resources2(User, Server) -> + lists:map(fun(S) -> element(3, S#session.usr) + end, get_sessions(User, Server)). + +get_sessions(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + case catch mnesia:dirty_index_read(session, {LUser, LServer}, #session.us) of + {'EXIT', _Reason} -> []; + [] -> []; + Result -> lists:reverse(lists:keysort(#session.priority, clean_session_list(Result))) + end. + +clean_session_list(Ss) -> + clean_session_list(lists:keysort(#session.usr, Ss), []). + +clean_session_list([], Res) -> + Res; +clean_session_list([S], Res) -> + [S | Res]; +clean_session_list([S1, S2 | Rest], Res) -> + if + S1#session.usr == S2#session.usr -> + if + S1#session.sid > S2#session.sid -> + clean_session_list([S1 | Rest], Res); + true -> + clean_session_list([S2 | Rest], Res) + end; + true -> + clean_session_list([S2 | Rest], [S1 | Res]) + end. + + +%% ----------------------------- +%% Internal function pattern +%% ----------------------------- + +user_action(User, Server, Fun, OK) -> + case ejabberd_auth:is_user_exists(User, Server) of + true -> + case catch Fun() of + OK -> + 0; + _ -> + 1 + end; + false -> + 404 + end. diff --git a/src/mod_ip_blacklist.erl b/src/mod_ip_blacklist.erl index c9102f2c0..16b70deaa 100644 --- a/src/mod_ip_blacklist.erl +++ b/src/mod_ip_blacklist.erl @@ -85,7 +85,7 @@ loop(_State) -> %% TODO: Support comment lines starting by % update_bl_c2s() -> ?INFO_MSG("Updating C2S Blacklist", []), - case http:request(?BLC2S) of + case http_p1:request(?BLC2S) of {ok, {{_Version, 200, _Reason}, _Headers, Body}} -> IPs = string:tokens(Body,"\n"), ets:delete_all_objects(bl_c2s), diff --git a/src/mod_xmlrpc.erl b/src/mod_xmlrpc.erl new file mode 100644 index 000000000..8266d9727 --- /dev/null +++ b/src/mod_xmlrpc.erl @@ -0,0 +1,875 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_xmlrpc.erl +%%% Author : Badlop / Mickael Remond / Christophe Romain +%%% Purpose : XML-RPC server +%%% Created : +%%% Id : +%%%---------------------------------------------------------------------- + +%%%/*************************************************************************** +%%% * * +%%% * 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. * +%%% * * +%%% ***************************************************************************/ +%%% +%%% +%%% MOD_XMLRPC - an XML-RPC server module for ejabberd +%%% +%%% v0.5 - 17 March 2008 +%%% +%%% http://ejabberd.jabber.ru/mod_xmlrpc +%%% +%%% (C) 2005, Badlop +%%% 2006, Process-one +%%% 2007, Process-one +%%% 2008, Process-one +%%% +%%% Changelog: +%%% +%%% 0.7 - 02 April 2009 - cromain +%%% - add user nick change +%%% +%%% 0.6 - 02 June 2008 - cromain +%%% - add user existance checking +%%% - improve parameter checking +%%% - allow orderless parameter +%%% +%%% 0.5 - 17 March 2008 - cromain +%%% - add user changing and higher level methods +%%% +%%% 0.4 - 18 February 2008 - cromain +%%% - add roster handling +%%% - add message sending +%%% - code and api clean-up +%%% +%%% 0.3 - 18 October 2007 - cromain +%%% - presence improvement +%%% - add new functionality +%%% +%%% 0.2 - 4 March 2006 - mremond +%%% - Code clean-up +%%% - Made it compatible with current ejabberd SVN version +%%% +%%% 0.1.2 - 28 December 2005 +%%% - Now compatible with ejabberd 1.0.0 +%%% - The XMLRPC server is started only once, not once for every virtual host +%%% - Added comments for handlers. Every available handler must be explained +%%% + +-module(mod_xmlrpc). +-author('Process-one'). +-vsn('0.6'). + +-behaviour(gen_mod). + +-export([start/2, + handler/2, + link_contacts/5, unlink_contacts/3, %% used by Nimbuzz + loop/1, + stop/1]). + +-export([add_rosteritem/6]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("mod_roster.hrl"). + +-ifdef(EJABBERD1). +-record(session, {sid, usr, us, priority}). %% ejabberd 1.1.x +-else. +-record(session, {sid, usr, us, priority, info}). %% ejabberd 2.0.x +-endif. + + +-define(PROCNAME, ejabberd_mod_xmlrpc). +-define(PORT, 4560). +-define(TIMEOUT, 5000). + +%% ----------------------------- +%% Module interface +%% ----------------------------- + +start(_Host, Opts) -> + case whereis(?PROCNAME) of + undefined -> + %% get options + Port = gen_mod:get_opt(port, Opts, ?PORT), + MaxSessions = 10, + Timeout = gen_mod:get_opt(timeout, Opts, ?TIMEOUT), + Handler = {mod_xmlrpc, handler}, + State = tryit, + + %% TODO: this option gives + %% error_info: {function_clause,[{gen_tcp,mod,[{ip,{127,0,0,1}}]}, + %%case gen_mod:get_opt(listen_all, Opts, false) of + %% true -> Ip = all; + %% false -> Ip = {127, 0, 0, 1} + %%end, + Ip = all, + + %% start the XML-RPC server + {ok, Pid} = xmlrpc:start_link(Ip, Port, MaxSessions, Timeout, Handler, State), + + %% start the loop process + register(?PROCNAME, spawn(?MODULE, loop, [Pid])), + ok; + _ -> + ok + end. + +loop(Pid) -> + receive + stop -> + xmlrpc:stop(Pid) + end. + +stop(_Host) -> + case whereis(?PROCNAME) of + undefined -> + ok; + _Pid -> + ?PROCNAME ! stop, + unregister(?PROCNAME) + end. + + +%% ----------------------------- +%% Handlers +%% ----------------------------- + +handler(tryit, Call) -> + try handler(notry, Call) of + Result -> Result + catch + A:B -> + ?ERROR_MSG("Problem '~p' in~nCall: ~p~nError: ~p", [A, Call, B]), + {false, {response, [-100]}} + end; + +% Call: Arguments: Returns: + +%% ............................. +%% Debug + +%% echothis String String +handler(_State, {call, echothis, [A]}) -> + {false, {response, [A]}}; + +%% multhis struct[{a, Integer}, {b, Integer}] Integer +handler(_State, {call, multhis, [{struct, Struct}]}) -> + [{a, A}, {b, B}] = lists:sort(Struct), + {false, {response, [A*B]}}; + +%% ............................. +%% User administration + +%% create_account struct[{user, String}, {server, Server}, {password, String}] Integer +handler(_State, {call, create_account, [{struct, Struct}]}) -> + [{password, P}, {server, S}, {user, U}] = lists:sort(Struct), + case ejabberd_auth:try_register(U, S, P) of + {atomic, ok} -> + {false, {response, [0]}}; + {atomic, exists} -> + {false, {response, [409]}}; + _ -> + {false, {response, [1]}} + end; + +%% delete_account struct[{user, String}, {server, Server}] Integer +handler(_State, {call, delete_account, [{struct, Struct}]}) -> + [{server, S}, {user, U}] = lists:sort(Struct), + Fun = fun() -> ejabberd_auth:remove_user(U, S) end, + user_action(U, S, Fun, ok); + +%% change_password struct[{user, String}, {server, String}, {newpass, String}] Integer +handler(_State, {call, change_password, [{struct, Struct}]}) -> + [{newpass, P}, {server, S}, {user, U}] = lists:sort(Struct), + Fun = fun() -> ejabberd_auth:set_password(U, S, P) end, + user_action(U, S, Fun, ok); + +%% set_nickname struct[{user, String}, {server, String}, {nick, String}] Integer +handler(_State, {call, set_nickname, [{struct, Struct}]}) -> + [{nick, N}, {server, S}, {user, U}] = lists:sort(Struct), + Fun = fun() -> case mod_vcard:process_sm_iq( + {jid, U, S, "", U, S, ""}, + {jid, U, S, "", U, S, ""}, + {iq, "", set, "", "en", + {xmlelement, "vCard", + [{"xmlns", "vcard-temp"}], [ + {xmlelement, "NICKNAME", [], [{xmlcdata, N}]} + ] + }}) of + {iq, [], result, [], _, []} -> ok; + _ -> error + end + end, + user_action(U, S, Fun, ok); + +%% set_rosternick struct[{user, String}, {server, String}, {nick, String}] Integer +handler(_State, {call, set_rosternick, [{struct, Struct}]}) -> + [{nick, N}, {server, S}, {user, U}] = lists:sort(Struct), + Fun = fun() -> change_rosternick(U, S, N) end, + user_action(U, S, Fun, ok); + +%% add_rosteritem struct[{user, String}, {server, String}, +%% {jid, String}, {group, String}, {nick, String}, {subs, String}] Integer +handler(_State, {call, add_rosteritem, [{struct, Struct}]}) -> + [{group, G},{jid, JID},{nick, N},{server, S},{subs, Subs},{user, U}] = lists:sort(Struct), + Fun = fun() -> add_rosteritem(U, S, JID, N, G, Subs) end, + user_action(U, S, Fun, {atomic, ok}); + +%% link_contacts struct[{jid1, String}, {nick1, String}, {jid2, String}, {nick2, String}] Integer +handler(_State, {call, link_contacts, [{struct, Struct}]}) -> + [{jid1, JID1}, {jid2, JID2}, {nick1, Nick1}, {nick2, Nick2}] = lists:sort(Struct), + {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_contacts(JID1, Nick1, JID2, Nick2) of + {atomic, ok} -> + {false, {response, [0]}}; + _ -> + {false, {response, [1]}} + end; + _ -> + {false, {response, [404]}} + end; + +%% delete_rosteritem struct[{user, String}, {server, String}, {jid, String}] Integer +handler(_State, {call, delete_rosteritem, [{struct, Struct}]}) -> + [{jid, JID}, {server, S}, {user, U}] = lists:sort(Struct), + Fun = fun() -> del_rosteritem(U, S, JID) end, + user_action(U, S, Fun, {atomic, ok}); + +%% unlink_contacts struct[{jid1, String}, {jid2, String}] Integer +handler(_State, {call, unlink_contacts, [{struct, Struct}]}) -> + [{jid1, JID1}, {jid2, JID2}] = lists:sort(Struct), + {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_contacts(JID1, JID2) of + {atomic, ok} -> + {false, {response, [0]}}; + _ -> + {false, {response, [1]}} + end; + _ -> + {false, {response, [404]}} + end; + +%% get_roster struct[{user, String}, {server, String}] +%% array[struct[{jid, String}, {group, String}, {nick, String}, +%% {subscription, String}, {pending, String}]] +handler(_State, {call, get_roster, [{struct, Struct}]}) -> + [{server, S}, {user, U}] = lists:sort(Struct), + case ejabberd_auth:is_user_exists(U, S) of + true -> + Roster = format_roster(get_roster(U, S)), + {false, {response, [{array, Roster}]}}; + false -> + {false, {response, [404]}} + end; + +%% get_roster_with_presence struct[{user, String}, {server, String}] +%% array[struct[{jid, String}, {resource, String}, {group, String}, {nick, String}, +%% {subscription, String}, {pending, String}, +%% {show, String}, {status, String}]] +handler(_State, {call, get_roster_with_presence, [{struct, Struct}]}) -> + [{server, S}, {user, U}] = lists:sort(Struct), + case ejabberd_auth:is_user_exists(U, S) of + true -> + Roster = format_roster_with_presence(get_roster(U, S)), + {false, {response, [{array, Roster}]}}; + false -> + {false, {response, [404]}} + end; + +%% get_presence struct[{user, String}, {server, String}] +%% array[struct[{jid, String}, {show, String}, {status, String}]] +handler(_State, {call, get_presence, [{struct, Struct}]}) -> + [{server, S}, {user, U}] = lists:sort(Struct), + case ejabberd_auth:is_user_exists(U, S) of + true -> + {Resource, Show, Status} = get_presence(U, S), + FullJID = case Resource of + [] -> + lists:flatten([U,"@",S]); + _ -> + lists:flatten([U,"@",S,"/",Resource]) + end, + R = {struct, [{jid, FullJID}, {show, Show}, {status, Status} ]}, + {false, {response, [R]}}; + false -> + {false, {response, [404]}} + end; + +%% get_resources struct[{user, String}, {server, String}] +%% array[String] +handler(_State, {call, get_resources, [{struct, Struct}]}) -> + [{server, S}, {user, U}] = lists:sort(Struct), + case ejabberd_auth:is_user_exists(U, S) of + true -> + Resources = get_resources(U, S), + {false, {response, [{array, Resources}]}}; + false -> + {false, {response, [404]}} + end; + +%% send_chat struct[{from, String}, {to, String}, {body, String}] +%% Integer +handler(_State, {call, send_chat, [{struct, Struct}]}) -> + [{body, Msg}, {from, FromJID}, {to, ToJID}] = lists:sort(Struct), + From = jlib:string_to_jid(FromJID), + To = jlib:string_to_jid(ToJID), + Stanza = {xmlelement, "message", [{"type", "chat"}], + [{xmlelement, "body", [], [{xmlcdata, Msg}]}]}, + ejabberd_router:route(From, To, Stanza), + {false, {response, [0]}}; + +%% send_message struct[{from, String}, {to, String}, {subject, String}, {body, String}] +%% Integer +handler(_State, {call, send_message, [{struct, Struct}]}) -> + [{body, Msg}, {from, FromJID}, {subject, Sub}, {to, ToJID}] = lists:sort(Struct), + From = jlib:string_to_jid(FromJID), + To = jlib:string_to_jid(ToJID), + Stanza = {xmlelement, "message", [{"type", "normal"}], + [{xmlelement, "subject", [], [{xmlcdata, Sub}]}, + {xmlelement, "body", [], [{xmlcdata, Msg}]}]}, + ejabberd_router:route(From, To, Stanza), + {false, {response, [0]}}; + +%% send_stanza struct[{from, String}, {to, String}, {stanza, String}] +%% Integer +handler(_State, {call, send_stanza, [{struct, Struct}]}) -> + [{from, FromJID}, {stanza, StanzaStr}, {to, ToJID}] = lists:sort(Struct), + case xml_stream:parse_element(StanzaStr) of + {error, _} -> + {false, {response, [1]}}; + Stanza -> + {xmlelement, _, Attrs, _} = Stanza, + From = jlib:string_to_jid(proplists:get_value("from", Attrs, FromJID)), + To = jlib:string_to_jid(proplists:get_value("to", Attrs, ToJID)), + ejabberd_router:route(From, To, Stanza), + {false, {response, [0]}} + end; + +%% rename_account struct[{user, String}, {server, String}, {newuser, String}, {newserver, String}] +%% Integer +handler(_State, {call, rename_account, [{struct, Struct}]}) -> + [{newserver, NS}, {newuser, NU}, {server, S}, {user, U}] = lists:sort(Struct), + case ejabberd_auth:is_user_exists(U, S) of + true -> + case ejabberd_auth:get_password(U, S) of + false -> + {false, {response, [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_roster(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_roster(RU, RS)) of + [#roster{name=OldNick, groups=OldGroups}|_] -> {OldNick, extract_group(OldGroups)}; + [] -> {NU, []} + end, + JIDStr = jlib:jid_to_string({RU, RS, RE}), + link_contacts(NewJID, NewNick, NewGroup, JIDStr, Nick, Group), + unlink_contacts(OldJID, JIDStr) + end, Roster), + ejabberd_auth:remove_user(U, S), + {false, {response, [0]}}; + {atomic, exists} -> + {false, {response, [409]}}; + _ -> + {false, {response, [1]}} + end + end; + false -> + {false, {response, [404]}} + end; + +%% add_contacts struct[{user, String}, {server, String}, +%% array[struct[{jid, String}, {group, String}, {nick, String}]]] +%% Integer +handler(_State, {call, add_contacts, [{struct, Struct}]}) -> + [{array, Contacts}, {server, S}, {user, U}] = lists:sort(Struct), + case ejabberd_auth:is_user_exists(U, S) of + true -> + JID1 = jlib:jid_to_string({U, S, ""}), + Response = lists:foldl(fun({struct, Struct2}, Acc) -> + [{group, Group}, {jid, JID2}, {nick, Nick}] = lists:sort(Struct2), + {PU, PS, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)), + case ejabberd_auth:is_user_exists(PU, PS) of + true -> + case link_contacts(JID1, "", "", JID2, Nick, Group) of + {atomic, ok} -> Acc; + _ -> 1 + end; + false -> + Acc + end + end, 0, element(2, Contacts)), + {false, {response, [Response]}}; + false -> + {false, {response, [404]}} + end; + +%% remove_contacts struct[{user, String}, {server, String}, array[String]] +%% Integer +handler(_State, {call, remove_contacts, [{struct, Struct}]}) -> + [{array, Contacts}, {server, S}, {user, U}] = lists:sort(Struct), + case ejabberd_auth:is_user_exists(U, S) of + true -> + JID1 = jlib:jid_to_string({U, S, ""}), + Response = 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_contacts(JID1, JID2) of + {atomic, ok} -> Acc; + _ -> 1 + end; + false -> + Acc + end + end, 0, element(2, Contacts)), + {false, {response, [Response]}}; + false -> + {false, {response, [404]}} + end; + +%% check_users_registration array[struct[{user, String}, {server, String}]] +%% array[struct[{user, String}, {server, String}, {status, Integer}]] +handler(_State, {call, check_users_registration, [{array, Users}]}) -> + Response = lists:map(fun({struct, Struct}) -> + [{server, S}, {user, U}] = lists:sort(Struct), + Registered = case ejabberd_auth:is_user_exists(U, S) of + true -> 1; + false -> 0 + end, + {struct, [{user, U}, {server, S}, {status, Registered}]} + end, Users), + {false, {response, [{array, Response}]}}; + + +%% If no other guard matches +handler(_State, Payload) -> + FaultString = lists:flatten(io_lib:format("Unknown call: ~p", [Payload])), + {false, {response, {fault, -1, FaultString}}}. + + +%% ----------------------------- +%% Internal roster handling +%% ----------------------------- + +get_roster(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + ejabberd_hooks:run_fold(roster_get, LServer, [], [{LUser, LServer}]). + +change_rosternick(User, Server, Nick) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + LJID = {LUser, LServer, []}, + JID = jlib:jid_to_string(LJID), + Push = fun(Subscription) -> + jlib:iq_to_xml(#iq{type = set, xmlns = ?NS_ROSTER, id = "push", + sub_el = [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}], + [{xmlelement, "item", [{"jid", JID}, {"name", Nick}, {"subscription", atom_to_list(Subscription)}], + []}]}]}) + end, + Result = case roster_backend(Server) of + mnesia -> + %% XXX This way of doing can not work with s2s + mnesia:transaction( + fun() -> + lists:foreach(fun(Roster) -> + {U, S} = Roster#roster.us, + mnesia:write(Roster#roster{name = Nick}), + lists:foreach(fun(R) -> + UJID = jlib:make_jid(U, S, R), + ejabberd_router:route(UJID, UJID, Push(Roster#roster.subscription)) + end, get_resources(U, S)) + end, mnesia:match_object(#roster{jid = LJID, _ = '_'})) + end); + odbc -> + %%% XXX This way of doing does not work with several domains + ejabberd_odbc:sql_transaction(Server, + fun() -> + SNick = ejabberd_odbc:escape(Nick), + SJID = ejabberd_odbc:escape(JID), + ejabberd_odbc:sql_query_t( + ["update rosterusers" + " set nick='", SNick, "'" + " where jid='", SJID, "';"]), + case ejabberd_odbc:sql_query_t( + ["select username from rosterusers" + " where jid='", SJID, "'" + " and subscription = 'B';"]) of + {selected, ["username"], Users} -> + lists:foreach(fun({RU}) -> + lists:foreach(fun(R) -> + UJID = jlib:make_jid(RU, Server, R), + ejabberd_router:route(UJID, UJID, Push(both)) + end, get_resources(RU, Server)) + end, Users); + _ -> + ok + end + end); + none -> + {error, no_roster} + end, + case Result of + {atomic, ok} -> ok; + _ -> error + end. + +add_rosteritem(User, Server, JID, Nick, Group, Subscription) -> + add_rosteritem(User, Server, JID, Nick, Group, Subscription, true). +add_rosteritem(User, Server, JID, Nick, Group, Subscription, Push) -> + {RU, RS, _} = jlib:jid_tolower(jlib:string_to_jid(JID)), + LJID = {RU,RS,[]}, + Groups = case Group of + [] -> []; + _ -> [Group] + end, + Roster = #roster{ + usj = {User,Server,LJID}, + us = {User,Server}, + jid = LJID, + name = Nick, + ask = none, + subscription = list_to_atom(Subscription), + groups = Groups}, + Result = + case roster_backend(Server) of + mnesia -> + mnesia:transaction(fun() -> + case mnesia:read({roster,{User,Server,LJID}}) of + [#roster{subscription=both}] -> + already_added; + _ -> + mnesia:write(Roster) + end + end); + odbc -> + %% MREMOND: TODO: check if already_added + case ejabberd_odbc:sql_transaction(Server, + fun() -> + Username = ejabberd_odbc:escape(User), + SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), + case ejabberd_odbc:sql_query_t( + ["select username from rosterusers " + " where username='", Username, "' " + " and jid='", SJID, + "' and subscription = 'B';"]) of + {selected, ["username"],[]} -> + ItemVals = record_to_string(Roster), + ItemGroups = groups_to_string(Roster), + odbc_queries:update_roster(Server, Username, + SJID, ItemVals, + ItemGroups); + _ -> + already_added + end + end) of + {atomic, already_added} -> {atomic, already_added}; + {atomic, _} -> {atomic, ok}; + Error -> Error + end; + none -> + {error, no_roster} + end, + case {Result, Push} of + {{atomic, already_added}, _} -> ok; %% No need for roster push + {{atomic, ok}, true} -> roster_push(User, Server, JID, Nick, Subscription); + {{error, no_roster}, true} -> roster_push(User, Server, JID, Nick, Subscription); + {{atomic, ok}, false} -> ok; + _ -> error + end, + Result. + +del_rosteritem(User, Server, JID) -> + del_rosteritem(User, Server, JID, true). +del_rosteritem(User, Server, JID, Push) -> + {RU, RS, _} = jlib:jid_tolower(jlib:string_to_jid(JID)), + LJID = {RU,RS,[]}, + Result = case roster_backend(Server) of + mnesia -> + mnesia:transaction(fun() -> + mnesia:delete({roster, {User,Server,LJID}}) + end); + odbc -> + case ejabberd_odbc:sql_transaction(Server, fun() -> + Username = ejabberd_odbc:escape(User), + SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), + odbc_queries:del_roster(Server, Username, SJID) + end) of + {atomic, _} -> {atomic, ok}; + Error -> Error + end; + none -> + {error, no_roster} + end, + case {Result, Push} of + {{atomic, ok}, true} -> roster_push(User, Server, JID, "", "remove"); + {{error, no_roster}, true} -> roster_push(User, Server, JID, "", "remove"); + {{atomic, ok}, false} -> ok; + _ -> error + end, + Result. + +link_contacts(JID1, Nick1, JID2, Nick2) -> + link_contacts(JID1, Nick1, JID2, Nick2, true). +link_contacts(JID1, Nick1, JID2, Nick2, Push) -> + link_contacts(JID1, Nick1, [], JID2, Nick2, [], Push). + +link_contacts(JID1, Nick1, Group1, JID2, Nick2, Group2) -> + link_contacts(JID1, Nick1, Group1, JID2, Nick2, Group2, true). +link_contacts(JID1, Nick1, Group1, JID2, Nick2, Group2, Push) -> + {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID1)), + {U2, S2, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)), + case add_rosteritem(U1, S1, JID2, Nick2, Group1, "both", Push) of + {atomic, ok} -> add_rosteritem(U2, S2, JID1, Nick1, Group2, "both", Push); + Error -> Error + end. + +unlink_contacts(JID1, JID2) -> + unlink_contacts(JID1, JID2, true). +unlink_contacts(JID1, JID2, Push) -> + {U1, S1, _} = jlib:jid_tolower(jlib:string_to_jid(JID1)), + {U2, S2, _} = jlib:jid_tolower(jlib:string_to_jid(JID2)), + case del_rosteritem(U1, S1, JID2, Push) of + {atomic, ok} -> del_rosteritem(U2, S2, JID1, Push); + Error -> Error + end. + +roster_push(User, Server, JID, Nick, Subscription) -> + LJID = jlib:make_jid(User, Server, ""), + TJID = jlib:string_to_jid(JID), + {TU, TS, _} = jlib:jid_tolower(TJID), + Presence = {xmlelement, "presence", [{"type", + case Subscription of + "remove" -> "unsubscribed"; + "none" -> "unsubscribe"; + "both" -> "subscribed"; + _ -> "subscribe" + end}], []}, + Item = case Nick of + "" -> [{"jid", JID}, {"subscription", Subscription}]; + _ -> [{"jid", JID}, {"name", Nick}, {"subscription", Subscription}] + end, + Result = jlib:iq_to_xml(#iq{type = set, xmlns = ?NS_ROSTER, id = "push", + sub_el = [{xmlelement, "query", [{"xmlns", ?NS_ROSTER}], + [{xmlelement, "item", Item, []}]}]}), + ejabberd_router:route(TJID, LJID, Presence), + ejabberd_router:route(LJID, LJID, Result), + lists:foreach(fun(Resource) -> + UJID = jlib:make_jid(User, Server, Resource), + ejabberd_router:route(TJID, UJID, Presence), + ejabberd_router:route(UJID, UJID, Result), + case Subscription of + "remove" -> none; + _ -> + lists:foreach(fun(TR) -> + ejabberd_router:route(jlib:make_jid(TU, TS, TR), UJID, + {xmlelement, "presence", [], []}) + end, get_resources(TU, TS)) + end + end, [R || R <- get_resources(User, Server), Subscription =/= "remove"]). + +roster_backend(Server) -> + Modules = gen_mod:loaded_modules(Server), + Mnesia = lists:member(mod_roster, Modules), + Odbc = lists:member(mod_roster_odbc, Modules), + if Mnesia -> mnesia; + true -> + if Odbc -> odbc; + true -> none + end + end. + +record_to_string(#roster{us = {User, _Server}, + jid = JID, + name = Name, + subscription = Subscription, + ask = Ask, + askmessage = AskMessage}) -> + Username = ejabberd_odbc:escape(User), + SJID = ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))), + Nick = ejabberd_odbc:escape(Name), + SSubscription = case Subscription of + both -> "B"; + to -> "T"; + from -> "F"; + none -> "N" + end, + SAsk = case Ask of + subscribe -> "S"; + unsubscribe -> "U"; + both -> "B"; + out -> "O"; + in -> "I"; + none -> "N" + end, + SAskMessage = ejabberd_odbc:escape(AskMessage), + ["'", Username, "'," + "'", SJID, "'," + "'", Nick, "'," + "'", SSubscription, "'," + "'", SAsk, "'," + "'", SAskMessage, "'," + "'N', '', 'item'"]. + +groups_to_string(#roster{us = {User, _Server}, + jid = JID, + groups = Groups}) -> + Username = ejabberd_odbc:escape(User), + SJID = ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))), + %% Empty groups do not need to be converted to string to be inserted in + %% the database + lists:foldl(fun([], Acc) -> Acc; + (Group, Acc) -> + String = ["'", Username, "'," + "'", SJID, "'," + "'", ejabberd_odbc:escape(Group), "'"], + [String|Acc] + end, [], Groups). + +%% Format roster items as a list of: +%% [{struct, [{jid, "test@localhost"},{group, "Friends"},{nick, "Nicktest"}]}] +format_roster([]) -> + []; +format_roster(Items) -> + format_roster(Items, []). +format_roster([], Structs) -> + Structs; +format_roster([#roster{jid=JID, name=Nick, groups=Group, + subscription=Subs, ask=Ask}|Items], Structs) -> + {User,Server,_Resource} = JID, + Struct = {struct, [{jid,lists:flatten([User,"@",Server])}, + {group, extract_group(Group)}, + {nick, Nick}, + {subscription, atom_to_list(Subs)}, + {pending, atom_to_list(Ask)} + ]}, + format_roster(Items, [Struct|Structs]). + +%% Format roster items as a list of: +%% [{struct, [{jid, "test@localhost"}, {resource, "Messenger"}, {group, "Friends"}, +%% {nick, "Nicktest"},{show, "available"}, {status, "Currently at office"}]}] +%% Note: If user is connected several times, only keep the resource with the +%% highest non-negative priority +format_roster_with_presence([]) -> + []; +format_roster_with_presence(Items) -> + format_roster_with_presence(Items, []). +format_roster_with_presence([], Structs) -> + Structs; +format_roster_with_presence([#roster{jid=JID, name=Nick, groups=Group, + subscription=Subs, ask=Ask}|Items], Structs) -> + {User,Server,_R} = JID, + Presence = case Subs of + both -> get_presence(User, Server); + from -> get_presence(User, Server); + _Other -> {"", "unavailable", ""} + end, + {Resource, Show, Status} = + case Presence of + {_R, "invisible", _S} -> {"", "unavailable", ""}; + _Status -> Presence + end, + Struct = {struct, [{jid,lists:flatten([User,"@",Server])}, + {resource, Resource}, + {group, extract_group(Group)}, + {nick, Nick}, + {subscription, atom_to_list(Subs)}, + {pending, atom_to_list(Ask)}, + {show, Show}, + {status, Status} + ]}, + format_roster_with_presence(Items, [Struct|Structs]). + +extract_group([]) -> []; +extract_group([Group|_Groups]) -> Group. + +%% ----------------------------- +%% Internal session handling +%% ----------------------------- + +%% This is inspired from ejabberd_sm.erl +get_presence(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} = rpc:call(node(Pid), ejabberd_c2s, get_presence, [Pid]), + {_User, Resource, Show, Status} = ejabberd_c2s:get_presence(Pid), + {Resource, Show, Status}; + true -> + {"", "unavailable", ""} + end + end. + +get_resources(User, Server) -> + lists:map(fun(S) -> element(3, S#session.usr) + end, get_sessions(User, Server)). + +get_sessions(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + case catch mnesia:dirty_index_read(session, {LUser, LServer}, #session.us) of + {'EXIT', _Reason} -> []; + [] -> []; + Result -> lists:reverse(lists:keysort(#session.priority, clean_session_list(Result))) + end. + +clean_session_list(Ss) -> + clean_session_list(lists:keysort(#session.usr, Ss), []). + +clean_session_list([], Res) -> + Res; +clean_session_list([S], Res) -> + [S | Res]; +clean_session_list([S1, S2 | Rest], Res) -> + if + S1#session.usr == S2#session.usr -> + if + S1#session.sid > S2#session.sid -> + clean_session_list([S1 | Rest], Res); + true -> + clean_session_list([S2 | Rest], Res) + end; + true -> + clean_session_list([S2 | Rest], [S1 | Res]) + end. + + +%% ----------------------------- +%% Internal function pattern +%% ----------------------------- + +user_action(User, Server, Fun, OK) -> + case ejabberd_auth:is_user_exists(User, Server) of + true -> + case catch Fun() of + OK -> + {false, {response, [0]}}; + _ -> + {false, {response, [1]}} + end; + false -> + {false, {response, [404]}} + end.