./ct_report/coverage/service_admin_extra_roster.COVER.html

1 %%%-------------------------------------------------------------------
2 %%% File : service_admin_extra_roster.erl
3 %%% Author : Badlop <badlop@process-one.net>, Piotr Nosek <piotr.nosek@erlang-solutions.com>
4 %%% Purpose : Contributed administrative functions and commands
5 %%% Created : 10 Aug 2008 by Badlop <badlop@process-one.net>
6 %%%
7 %%%
8 %%% ejabberd, Copyright (C) 2002-2008 ProcessOne
9 %%%
10 %%% This program is free software; you can redistribute it and/or
11 %%% modify it under the terms of the GNU General Public License as
12 %%% published by the Free Software Foundation; either version 2 of the
13 %%% License, or (at your option) any later version.
14 %%%
15 %%% This program is distributed in the hope that it will be useful,
16 %%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17 %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 %%% General Public License for more details.
19 %%%
20 %%% You should have received a copy of the GNU General Public License
21 %%% along with this program; if not, write to the Free Software
22 %%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23 %%%
24 %%%-------------------------------------------------------------------
25
26 -module(service_admin_extra_roster).
27 -author('badlop@process-one.net').
28 -export([
29 commands/0,
30 add_rosteritem/7,
31 delete_rosteritem/4,
32 process_rosteritems/5,
33 get_roster/2,
34 push_roster/3,
35 push_roster_all/1,
36 push_alltoall/2
37 ]).
38
39 -ignore_xref([
40 commands/0, add_rosteritem/7, delete_rosteritem/4, process_rosteritems/5,
41 get_roster/2, push_roster/3, push_roster_all/1, push_alltoall/2
42 ]).
43
44 -include("mongoose.hrl").
45 -include("ejabberd_commands.hrl").
46 -include("mod_roster.hrl").
47 -include("jlib.hrl").
48 -include_lib("exml/include/exml.hrl").
49
50 -type simple_roster() :: {User :: jid:user(),
51 Server :: jid:server(),
52 Group :: binary(),
53 Nick :: binary()}.
54 -type jids_nick_subs_ask_grp() :: {Jids :: list(),
55 Nick :: binary(),
56 Subs :: subs(),
57 _Ask,
58 _Group}.
59 -type subs() :: atom() | binary().
60 -type push_action() :: remove
61 | none
62 | {add, Nick :: binary(), Subs :: subs(),
63 Group :: binary() | string()}.
64
65 -type delete_action() :: {'delete', Subs :: [atom()], Asks :: [atom()],
66 [jid:user()], Contacts :: [binary()]}.
67 -type list_action() :: {'list', Subs :: [atom()], Asks :: [atom()],
68 [jid:user()], Contacts :: [binary()]}.
69
70
71 %%%
72 %%% Register commands
73 %%%
74
75 -spec commands() -> [ejabberd_commands:cmd(), ...].
76 commands() ->
77 146 [
78 #ejabberd_commands{name = add_rosteritem, tags = [roster],
79 desc = "Add an item to a user's roster (supports RDBMS)",
80 module = ?MODULE, function = add_rosteritem,
81 args = [{localuser, binary}, {localserver, binary},
82 {user, binary}, {server, binary},
83 {nick, binary}, {group, binary},
84 {subs, binary}],
85 result = {res, restuple}},
86 %%{"", "subs= none, from, to or both"},
87 %%{"", "example: add-roster peter localhost mike server.com MiKe Employees both"},
88 %%{"", "will add mike@server.com to peter@localhost roster"},
89 #ejabberd_commands{name = delete_rosteritem, tags = [roster],
90 desc = "Delete an item from a user's roster (supports RDBMS)",
91 module = ?MODULE, function = delete_rosteritem,
92 args = [{localuser, binary}, {localserver, binary},
93 {user, binary}, {server, binary}],
94 result = {res, restuple}},
95 #ejabberd_commands{name = process_rosteritems, tags = [roster],
96 desc = "List or delete rosteritems that"
97 " match filtering options (Mnesia only!)",
98 longdesc = "Explanation of each argument:\n"
99 " - action: what to do with each rosteritem that "
100 "matches all the filtering options\n"
101 " - subs: subscription type\n"
102 " - asks: pending subscription\n"
103 " - users: the JIDs of the local user\n"
104 " - contacts: the JIDs of the contact in the roster\n"
105 "\n"
106 "Allowed values in the arguments:\n"
107 " ACTION = list | delete\n"
108 " SUBS = SUB[:SUB]* | any\n"
109 " SUB = none | from | to | both\n"
110 " ASKS = ASK[:ASK]* | any\n"
111 " ASK = none | out | in\n"
112 " USERS = JID[:JID]* | any\n"
113 " CONTACTS = JID[:JID]* | any\n"
114 " JID = characters valid in a JID, and can use the "
115 "Regular expression syntax:"
116 " http://www.erlang.org/doc/man/re.html#id212737\n"
117 "\n"
118 "This example will list roster items with subscription "
119 "'none', 'from' or 'to' that have any ask property, of "
120 "local users which JID is in the virtual host "
121 "'example.org' and that the contact JID is either a "
122 "bare server name (without user part) or that has a "
123 "user part and the server part contains the word 'icq'"
124 ":\n list none:from:to any *@example.org *:*@*icq*",
125 module = ?MODULE, function = process_rosteritems,
126 args = [{action, string}, {subs, string},
127 {asks, string}, {users, string},
128 {contacts, string}],
129 result = {res, binary}},
130 #ejabberd_commands{name = get_roster, tags = [roster],
131 desc = "Get roster of a local user",
132 module = ?MODULE, function = get_roster,
133 args = [{user, binary}, {host, binary}],
134 result = {contacts, {list, {contact, {tuple, [
135 {jid, binary},
136 {nick, binary},
137 {subscription, binary},
138 {ask, binary},
139 {group, binary}
140 ]}}}}},
141 #ejabberd_commands{name = push_roster, tags = [roster],
142 desc = "Push template roster from file to a user",
143 module = ?MODULE, function = push_roster,
144 args = [{file, string}, {user, binary}, {host, binary}],
145 result = {res, rescode}},
146 #ejabberd_commands{name = push_roster_all, tags = [roster],
147 desc = "Push template roster from file to all those users",
148 module = ?MODULE, function = push_roster_all,
149 args = [{file, string}],
150 result = {res, rescode}},
151 #ejabberd_commands{name = push_roster_alltoall, tags = [roster],
152 desc = "Add all the users to all the users of Host in Group",
153 module = ?MODULE, function = push_alltoall,
154 args = [{host, binary}, {group, binary}],
155 result = {res, rescode}}
156 ].
157
158 %%%
159 %%% Roster
160 %%%
161
162 -spec add_rosteritem(LocalUser :: jid:user(),
163 LocalServer :: jid:server(),
164 User :: jid:user(),
165 Server :: jid:server(),
166 Nick :: binary(),
167 Group :: binary() | string(),
168 Subs :: subs()) -> {Res, string()} when
169 Res :: user_doest_not_exist | error | bad_subs | ok.
170 add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs) ->
171 5 LocalJID = jid:make(LocalUser, LocalServer, <<>>),
172 5 case ejabberd_auth:does_user_exist(LocalJID) of
173 true ->
174 5 RemoteJID = jid:make(User, Server, <<>>),
175 5 case subscribe(LocalJID, RemoteJID, Nick, Group, Subs, []) of
176 ok ->
177 5 do_add_rosteritem(LocalJID, RemoteJID, Nick, Group, Subs);
178 {error, Reason} ->
179
:-(
{error, io_lib:format("~p", [Reason])}
180 end;
181 false ->
182
:-(
{user_does_not_exist,
183 io_lib:format("Cannot add the item because user ~s@~s does not exist",
184 [LocalUser, LocalServer])}
185 end.
186
187 do_add_rosteritem(LocalJID, RemoteJID, Nick, Group, Subs) ->
188 5 case lists:member(Subs, possible_subs_binary()) of
189 true ->
190 5 push_roster_item(LocalJID, RemoteJID, {add, Nick, Subs, Group}),
191 5 {ok, io_lib:format("Added the item to the roster of ~s", [jid:to_binary(LocalJID)])};
192 false ->
193
:-(
{bad_subs, io_lib:format("Sub ~s is incorrect."
194 " Choose one of the following:~nnone~nfrom~nto~nboth",
195 [binary_to_list(Subs)])}
196 end.
197
198
199 %% @doc returns result of mnesia or rdbms transaction
200 -spec subscribe(LocalJID :: jid:jid(),
201 RemoteJID :: jid:jid(),
202 Nick :: binary(),
203 Group :: binary() | string(),
204 Subs :: subs(),
205 _Xattrs :: [jlib:binary_pair()]) -> ok | {error, any()}.
206 subscribe(LocalJID, RemoteJID, Nick, Group, SubscriptionS, _Xattrs) ->
207 42 ItemEl = build_roster_item(RemoteJID, {add, Nick, SubscriptionS, Group}),
208 42 QueryEl = #xmlel{ name = <<"query">>,
209 attrs = [{<<"xmlns">>, <<"jabber:iq:roster">>}],
210 children = [ItemEl]},
211 42 {ok, HostType} = mongoose_domain_api:get_domain_host_type(LocalJID#jid.lserver),
212 42 mod_roster:set_items(HostType, LocalJID, QueryEl).
213
214
215 -spec delete_rosteritem(LocalUser :: jid:user(),
216 LocalServer :: jid:server(),
217 User :: jid:user(),
218 Server :: jid:server()) -> {Res, string()} when
219 Res :: ok | error | user_does_not_exist.
220 delete_rosteritem(LocalUser, LocalServer, User, Server) ->
221 3 LocalJID = jid:make(LocalUser, LocalServer, <<>>),
222 3 case ejabberd_auth:does_user_exist(LocalJID) of
223 true ->
224 3 RemoteJID = jid:make(User, Server, <<>>),
225 3 case unsubscribe(LocalJID, RemoteJID) of
226 ok ->
227 3 push_roster_item(LocalJID, RemoteJID, remove),
228 3 {ok, io_lib:format("The item removed from roster of ~s",
229 [jid:to_binary(LocalJID)])};
230 {error, Reason} ->
231
:-(
{error, io_lib:format("~p", [Reason])}
232 end;
233 false ->
234
:-(
{user_does_not_exist,
235 io_lib:format("Cannot delete the item because user ~s@~s doest not exist",
236 [LocalUser, LocalServer])}
237 end.
238
239
240 %% @doc returns result of mnesia or rdbms transaction
241 -spec unsubscribe(LocalJID :: jid:jid(), RemoteJID :: jid:jid()) -> ok | {error, any()}.
242 unsubscribe(LocalJID, RemoteJID) ->
243 3 ItemEl = build_roster_item(RemoteJID, remove),
244 3 QueryEl = #xmlel{ name = <<"query">>,
245 attrs = [{<<"xmlns">>, <<"jabber:iq:roster">>}],
246 children = [ItemEl]},
247 3 {ok, HostType} = mongoose_domain_api:get_domain_host_type(LocalJID#jid.lserver),
248 3 mod_roster:set_items(HostType, LocalJID, QueryEl).
249
250 %% -----------------------------
251 %% Get Roster
252 %% -----------------------------
253
254 -spec get_roster(jid:user(), jid:server()) ->
255 [jids_nick_subs_ask_grp()].
256 get_roster(User, Server) ->
257 2 UserJID = jid:make(User, Server, <<>>),
258 2 {ok, HostType} = mongoose_domain_api:get_domain_host_type(UserJID#jid.lserver),
259 2 Acc = mongoose_acc:new(#{location => ?LOCATION,
260 host_type => HostType,
261 lserver => UserJID#jid.lserver,
262 element => undefined}),
263 2 Acc2 = mongoose_hooks:roster_get(Acc, UserJID),
264 2 Items = mongoose_acc:get(roster, items, [], Acc2),
265 2 make_roster(Items).
266
267
268 %% @doc Note: if a contact is in several groups, the contact is returned
269 %% several times, each one in a different group.
270 -spec make_roster([mod_roster:roster()]) -> [jids_nick_subs_ask_grp()].
271 make_roster(Roster) ->
272 2 lists:foldl(
273 fun(Item, Res) ->
274 3 JIDS = jid:to_binary(Item#roster.jid),
275 3 Nick = Item#roster.name,
276 3 Subs = atom_to_list(Item#roster.subscription),
277 3 Ask = atom_to_list(Item#roster.ask),
278 3 Groups = case Item#roster.groups of
279
:-(
[] -> [""];
280 3 Gs -> Gs
281 end,
282 3 ItemsX = [{JIDS, Nick, Subs, Ask, Group}
283 3 || Group <- Groups],
284 3 ItemsX ++ Res
285 end,
286 [],
287 Roster).
288
289
290 %%-----------------------------
291 %% Push Roster from file
292 %%-----------------------------
293
294 -spec push_roster(file:name(), jid:user(), jid:server()) -> 'ok'.
295 push_roster(File, User, Server) ->
296 1 {ok, [Roster]} = file:consult(File),
297 1 subscribe_roster({User, Server, <<"">>, User}, roster_list_to_binary(Roster)).
298
299
300 -spec push_roster_all(file:name()) -> 'ok'.
301 push_roster_all(File) ->
302 1 {ok, [Roster]} = file:consult(File),
303 1 subscribe_all(roster_list_to_binary(Roster)).
304
305
306 -spec roster_list_to_binary([mod_roster:roster()]) -> [simple_roster()].
307 roster_list_to_binary(Roster) ->
308 2 [{
309 mongoose_bin:string_to_binary(Usr),
310 mongoose_bin:string_to_binary(Srv),
311 mongoose_bin:string_to_binary(Grp),
312 2 mongoose_bin:string_to_binary(Nick)} || {Usr, Srv, Grp, Nick} <- Roster].
313
314
315 -spec subscribe_all([simple_roster()]) -> 'ok'.
316 subscribe_all(Roster) ->
317 2 subscribe_all(Roster, Roster).
318 subscribe_all([], _) ->
319 2 ok;
320 subscribe_all([User1 | Users], Roster) ->
321 9 subscribe_roster(User1, Roster),
322 9 subscribe_all(Users, Roster).
323
324
325 -spec subscribe_roster(simple_roster(), [simple_roster()]) -> 'ok'.
326 subscribe_roster(_, []) ->
327 10 ok;
328 %% Do not subscribe a user to itself
329 subscribe_roster({Name, Server, Group, Nick}, [{Name, Server, _, _} | Roster]) ->
330 9 subscribe_roster({Name, Server, Group, Nick}, Roster);
331 %% Subscribe Name2 to Name1
332 subscribe_roster({Name1, Server1, Group1, Nick1}, [{Name2, Server2, Group2, Nick2} | Roster]) ->
333 37 subscribe(jid:make(Name1, Server1, <<>>),
334 jid:make(Name2, Server2, <<>>),
335 Nick2, Group2, <<"both">>, []),
336 37 subscribe_roster({Name1, Server1, Group1, Nick1}, Roster).
337
338
339 -spec push_alltoall(jid:server(), binary()) -> 'ok'.
340 push_alltoall(S, G) ->
341 1 Users = ejabberd_auth:get_vh_registered_users(S),
342 1 Users2 = build_list_users(G, Users, []),
343 1 subscribe_all(Users2),
344 1 ok.
345
346
347 -spec build_list_users(Group :: binary(),
348 [jid:simple_bare_jid()],
349 Res :: [simple_roster()]) -> [].
350 build_list_users(_Group, [], Res) ->
351 1 Res;
352 build_list_users(Group, [{User, Server}|Users], Res) ->
353 4 build_list_users(Group, Users, [{User, Server, Group, User}|Res]).
354
355
356 %% @doc Push to the roster of account LU@LS the contact U@S.
357 %% The specific action to perform is defined in Action.
358 -spec push_roster_item(jid:jid(), jid:jid(), Action :: push_action()) -> 'ok'.
359 push_roster_item(JID, #jid{luser = U, lserver = S} = RemJID, Action) ->
360 8 lists:foreach(fun(R) ->
361 6 RJID = jid:replace_resource(JID, R),
362 6 BroadcastEl = build_broadcast(U, S, Action),
363 6 ejabberd_sm:route(RJID, RJID, BroadcastEl),
364 6 Item = build_roster_item(RemJID, Action),
365 6 ResIQ = build_iq_roster_push(Item),
366 6 ejabberd_router:route(RJID, RJID, ResIQ)
367 end, ejabberd_sm:get_user_resources(JID)).
368
369 -spec build_roster_item(jid:jid(), push_action()) -> exml:element().
370 build_roster_item(#jid{resource = <<>>} = JID, {add, Nick, Subs, Group}) ->
371 46 #xmlel{ name = <<"item">>,
372 attrs = [{<<"jid">>, jid:to_binary(JID)},
373 {<<"name">>, Nick},
374 {<<"subscription">>, Subs}],
375 children = [#xmlel{name = <<"group">>, children = [#xmlcdata{content = Group}]}]
376 };
377 build_roster_item(#jid{resource = <<>>} = JID, remove) ->
378 5 #xmlel{ name = <<"item">>,
379 attrs = [{<<"jid">>, jid:to_binary(JID)},
380 {<<"subscription">>, <<"remove">>}]};
381 build_roster_item(#jid{} = JID, Action) ->
382
:-(
build_roster_item(jid:replace_resource(JID, <<>>), Action).
383
384
385 -spec build_iq_roster_push(jlib:xmlcdata() | exml:element()) -> exml:element().
386 build_iq_roster_push(Item) ->
387 6 #xmlel{ name = <<"iq">>,
388 attrs = [{<<"type">>, <<"set">>}, {<<"id">>, <<"push">>}],
389 children = [#xmlel{ name = <<"query">>,
390 attrs = [{<<"xmlns">>, ?NS_ROSTER}],
391 children = [Item]}] }.
392
393 -spec build_broadcast(U :: jid:user(), S :: jid:server(),
394 push_action()) -> ejabberd_c2s:broadcast().
395 build_broadcast(U, S, {add, _Nick, Subs, _Group}) ->
396 4 build_broadcast(U, S, list_to_existing_atom(binary_to_list(Subs)));
397 build_broadcast(U, S, remove) ->
398 2 build_broadcast(U, S, none);
399 %% Subs = both | from | to | none
400 build_broadcast(U, S, SubsAtom) when is_atom(SubsAtom) ->
401 6 {broadcast, {item, {U, S, <<"">>}, SubsAtom}}.
402
403 %%-----------------------------
404 %% Purge roster items
405 %%-----------------------------
406
407 -spec process_rosteritems(Act :: string(), SubsS :: string(), AsksS :: string(),
408 UsersS :: string(), ContactsS :: string()) -> binary().
409 process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) ->
410
:-(
Action = case ActionS of
411
:-(
"list" -> list;
412
:-(
"delete" -> delete
413 end,
414
415
:-(
Subs = lists:foldl(
416
:-(
fun(any, _) -> [none, from, to, both];
417
:-(
(Sub, Subs) -> [Sub | Subs]
418 end,
419 [],
420
:-(
[list_to_existing_atom(S) || S <- string:tokens(SubsS, ":")]
421 ),
422
423
:-(
Asks = lists:foldl(
424
:-(
fun(any, _) -> [none, out, in];
425
:-(
(Ask, Asks) -> [Ask | Asks]
426 end,
427 [],
428
:-(
[list_to_existing_atom(S) || S <- string:tokens(AsksS, ":")]
429 ),
430
431
:-(
Users = lists:foldl(
432
:-(
fun(<<"any">>, _) -> [<<".*">>, <<".*@.*">>];
433
:-(
(U, Us) -> [U | Us]
434 end,
435 [],
436
:-(
[mongoose_bin:string_to_binary(S) || S <- string:tokens(UsersS, ":")]
437 ),
438
439
:-(
Contacts = lists:foldl(
440
:-(
fun(<<"any">>, _) -> [<<".*">>, <<".*@.*">>];
441
:-(
(U, Us) -> [U | Us]
442 end,
443 [],
444
:-(
[mongoose_bin:string_to_binary(S) || S <- string:tokens(ContactsS, ":")]
445 ),
446
447
:-(
case validate_regexps(Users ++ Contacts) of
448 <<>> ->
449
:-(
Options = {Action, Subs, Asks, Users, Contacts},
450
451
:-(
case mnesia:table_info(roster, size) of
452 0 ->
453
:-(
<<"Roster table is empty.\n">>;
454 NumRosteritems ->
455
:-(
Msg1 = <<"There are ", (integer_to_binary(NumRosteritems))/binary,
456 " roster items in total.\n">>,
457
:-(
Key = mnesia:dirty_first(roster),
458
:-(
Msg2 = rip(Key, Options, <<>>),
459
:-(
<<Msg1/binary, Msg2/binary>>
460 end;
461 ErrorMsg ->
462
:-(
ErrorMsg
463 end.
464
465 validate_regexps(ListOfRegexps) ->
466
:-(
lists:foldl(
467 fun(RegExp, MsgAcc) ->
468
:-(
case re:compile(RegExp) of
469
:-(
{ok, _} -> MsgAcc;
470 {error, Error} ->
471
:-(
NewErr = iolist_to_binary(io_lib:format("Wrong regexp ~p: ~p~n",
472 [RegExp, Error])),
473
:-(
<<MsgAcc/binary, NewErr/binary>>
474 end
475 end, <<>>, ListOfRegexps).
476
477 -spec rip('$end_of_table' | any(), delete_action() | list_action(), binary()) -> binary().
478 rip('$end_of_table', _Options, Acc) ->
479
:-(
Acc;
480 rip(Key, Options, Acc) ->
481
:-(
KeyNext = mnesia:dirty_next(roster, Key),
482
:-(
{Action, _, _, _, _} = Options,
483
:-(
Msg = case decide_rip(Key, Options) of
484 true ->
485
:-(
apply_action(Action, Key);
486 false ->
487
:-(
<<>>
488 end,
489
:-(
rip(KeyNext, Options, <<Acc/binary, Msg/binary>>).
490
491 apply_action(list, Key) ->
492
:-(
{User, Server, JID} = Key,
493
:-(
{RUser, RServer, _} = JID,
494
:-(
<<"Matches: ", User/binary, "@", Server/binary, " ", RUser/binary, "@", RServer/binary, "\n">>;
495 apply_action(delete, Key) ->
496
:-(
Msg = apply_action(list, Key),
497
:-(
mnesia:dirty_delete(roster, Key),
498
:-(
Msg.
499
500 decide_rip(Key, {_Action, Subs, Asks, User, Contact}) ->
501
:-(
case catch mnesia:dirty_read(roster, Key) of
502 [RI] ->
503
:-(
lists:member(RI#roster.subscription, Subs)
504
:-(
andalso lists:member(RI#roster.ask, Asks)
505
:-(
andalso decide_rip_jid(RI#roster.us, User)
506
:-(
andalso decide_rip_jid(RI#roster.jid, Contact);
507 _ ->
508
:-(
false
509 end.
510
511 %% Returns true if the server of the JID is included in the servers
512 decide_rip_jid({UName, UServer, _UResource}, MatchList) ->
513
:-(
decide_rip_jid({UName, UServer}, MatchList);
514 decide_rip_jid({UName, UServer}, MatchList) ->
515
:-(
lists:any(
516 fun(MatchString) ->
517
:-(
MJID = jid:from_binary(MatchString),
518
:-(
MName = MJID#jid.luser,
519
:-(
MServer = MJID#jid.lserver,
520
:-(
IsServer = is_regexp_match(UServer, MServer),
521
:-(
case {MName, UName} of
522
:-(
{<<>>, <<>>} -> IsServer;
523
:-(
{<<>>, _} -> false;
524
:-(
_ -> IsServer andalso is_regexp_match(UName, MName)
525 end
526 end,
527 MatchList).
528
529 is_regexp_match(String, RegExp) ->
530
:-(
case catch re:run(String, RegExp) of
531 nomatch ->
532
:-(
false;
533 {match, List} ->
534
:-(
Size = length(binary_to_list(String)),
535
:-(
case lists:member({0, Size}, List) of
536 true ->
537
:-(
true;
538 false ->
539
:-(
false
540 end;
541 Error ->
542
:-(
?LOG_ERROR(#{what => regexp_match_failed,
543
:-(
regex => RegExp, string => String, reason => Error}),
544
:-(
false
545 end.
546
547 possible_subs_binary() ->
548 5 [<<"none">>, <<"from">>, <<"to">>, <<"both">>].
549
Line Hits Source