• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 1296

19 Jan 2026 11:25AM UTC coverage: 33.562% (+0.09%) from 33.468%
1296

push

github

badlop
mod_conversejs: Cosmetic change: sort paths alphabetically

0 of 4 new or added lines in 1 file covered. (0.0%)

11245 existing lines in 174 files now uncovered.

15580 of 46421 relevant lines covered (33.56%)

1074.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.5
/src/mod_roster.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_roster.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose : Roster management
5
%%% Created : 11 Dec 2002 by Alexey Shchepin <alexey@process-one.net>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-2026   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 along
21
%%% with this program; if not, write to the Free Software Foundation, Inc.,
22
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
%%%
24
%%%----------------------------------------------------------------------
25

26
%%% @doc Roster management (Mnesia storage).
27
%%%
28
%%% Includes support for XEP-0237: Roster Versioning.
29
%%% The roster versioning follows an all-or-nothing strategy:
30
%%%  - If the version supplied by the client is the latest, return an empty response.
31
%%%  - If not, return the entire new roster (with updated version string).
32
%%% Roster version is a hash digest of the entire roster.
33
%%% No additional data is stored in DB.
34

35
-module(mod_roster).
36

37
-protocol({xep, 237, '1.3', '2.1.0', "complete", ""}).
38

39
-author('alexey@process-one.net').
40

41
-behaviour(gen_mod).
42

43
-export([start/2, stop/1, reload/3, process_iq/1, export/1,
44
         import_info/0, process_local_iq/1, get_user_roster_items/2,
45
         import/5, get_roster/2, push_item/3,
46
         import_start/2, import_stop/2, is_subscribed/2,
47
         c2s_self_presence/1, in_subscription/2,
48
         out_subscription/1, set_items/3, remove_user/2,
49
         get_jid_info/4, encode_item/1, get_versioning_feature/2,
50
         roster_version/2, mod_doc/0,
51
         mod_opt_type/1, mod_options/1, set_roster/1, del_roster/3,
52
         process_rosteritems/5,
53
         depends/2, set_item_and_notify_clients/3]).
54

55
-export([webadmin_page_hostuser/4, webadmin_menu_hostuser/4, webadmin_user/4]).
56

57
-import(ejabberd_web_admin, [make_command/4, make_command_raw_value/3, make_table/4]).
58

59
-include("logger.hrl").
60
-include_lib("xmpp/include/xmpp.hrl").
61
-include("mod_roster.hrl").
62
-include("ejabberd_http.hrl").
63
-include("ejabberd_web_admin.hrl").
64

65
-include("translate.hrl").
66

67
-define(ROSTER_CACHE, roster_cache).
68
-define(ROSTER_ITEM_CACHE, roster_item_cache).
69
-define(ROSTER_VERSION_CACHE, roster_version_cache).
70
-define(SM_MIX_ANNOTATE, roster_mix_annotate).
71

72
-type c2s_state() :: ejabberd_c2s:state().
73
-export_type([subscription/0]).
74

75
-callback init(binary(), gen_mod:opts()) -> any().
76
-callback import(binary(), binary(), #roster{} | [binary()]) -> ok.
77
-callback read_roster_version(binary(), binary()) -> {ok, binary()} | error.
78
-callback write_roster_version(binary(), binary(), boolean(), binary()) -> any().
79
-callback get_roster(binary(), binary()) -> {ok, [#roster{}]} | error.
80
-callback get_roster_item(binary(), binary(), ljid()) -> {ok, #roster{}} | error.
81
-callback read_subscription_and_groups(binary(), binary(), ljid())
82
          -> {ok, {subscription(), ask(), [binary()]}} | error.
83
-callback roster_subscribe(binary(), binary(), ljid(), #roster{}) -> any().
84
-callback transaction(binary(), fun(() -> T)) -> {atomic, T} | {aborted, any()}.
85
-callback remove_user(binary(), binary()) -> any().
86
-callback update_roster(binary(), binary(), ljid(), #roster{}) -> any().
87
-callback del_roster(binary(), binary(), ljid()) -> any().
88
-callback use_cache(binary(), roster | roster_version) -> boolean().
89
-callback cache_nodes(binary()) -> [node()].
90

91
-optional_callbacks([use_cache/2, cache_nodes/1]).
92

93
start(Host, Opts) ->
94
    Mod = gen_mod:db_mod(Opts, ?MODULE),
9✔
95
    Mod:init(Host, Opts),
9✔
96
    init_cache(Mod, Host, Opts),
9✔
97
    {ok, [{hook, roster_get, get_user_roster_items, 50},
9✔
98
          {hook, roster_in_subscription, in_subscription, 50},
99
          {hook, roster_out_subscription, out_subscription, 50},
100
          {hook, roster_get_jid_info, get_jid_info, 50},
101
          {hook, remove_user, remove_user, 50},
102
          {hook, c2s_self_presence, c2s_self_presence, 50},
103
          {hook, c2s_post_auth_features, get_versioning_feature, 50},
104
          {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50},
105
          {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50},
106
          {hook, webadmin_user, webadmin_user, 50},
107
          {iq_handler, ejabberd_sm, ?NS_ROSTER, process_iq}]}.
108

109
stop(_Host) ->
110
    ok.
9✔
111

112
reload(Host, NewOpts, OldOpts) ->
113
    NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
×
114
    OldMod = gen_mod:db_mod(OldOpts, ?MODULE),
×
115
    if NewMod /= OldMod ->
×
116
            NewMod:init(Host, NewOpts);
×
117
       true ->
118
            ok
×
119
    end,
120
    init_cache(NewMod, Host, NewOpts).
×
121

122
depends(_Host, _Opts) ->
123
    [].
9✔
124

125
-spec process_iq(iq()) -> iq().
126
process_iq(#iq{from = #jid{luser = U, lserver = S},
127
               to =   #jid{luser = U, lserver = S}} = IQ) ->
128
    process_local_iq(IQ);
3,537✔
129
process_iq(#iq{lang = Lang, to = To} = IQ) ->
130
    case ejabberd_hooks:run_fold(roster_remote_access,
×
131
                                 To#jid.lserver, false, [IQ]) of
132
        false ->
133
            Txt = ?T("Query to another users is forbidden"),
×
134
            xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang));
×
135
        {true, IQ1} ->
136
            process_local_iq(IQ1)
×
137
    end.
138

139
-spec process_local_iq(iq()) -> iq().
140
process_local_iq(#iq{type = set,lang = Lang,
141
                     sub_els = [#roster_query{
142
                                   items = [#roster_item{ask = Ask}]}]} = IQ)
143
  when Ask /= undefined ->
UNCOV
144
    Txt = ?T("Possessing 'ask' attribute is not allowed by RFC6121"),
8✔
UNCOV
145
    xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
8✔
146
process_local_iq(#iq{type = set, from = From, lang = Lang,
147
                     sub_els = [#roster_query{
148
                                   items = [#roster_item{} = Item]}]} = IQ) ->
UNCOV
149
    case has_duplicated_groups(Item#roster_item.groups) of
368✔
150
        true ->
UNCOV
151
            Txt = ?T("Duplicated groups are not allowed by RFC6121"),
8✔
UNCOV
152
            xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
8✔
153
        false ->
UNCOV
154
            From1 = case xmpp:get_meta(IQ, privilege_from, none) of
360✔
155
                        #jid{} = PrivFrom ->
156
                            PrivFrom;
×
157
                        none ->
UNCOV
158
                            From
360✔
159
                    end,
UNCOV
160
            #jid{lserver = LServer} = From1,
360✔
UNCOV
161
            Access = mod_roster_opt:access(LServer),
360✔
UNCOV
162
            case acl:match_rule(LServer, Access, From) of
360✔
163
                deny ->
164
                    Txt = ?T("Access denied by service policy"),
×
165
                    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
×
166
                allow ->
UNCOV
167
                    process_iq_set(IQ)
360✔
168
            end
169
    end;
170
process_local_iq(#iq{type = set, lang = Lang,
171
                     sub_els = [#roster_query{items = [_|_]}]} = IQ) ->
UNCOV
172
    Txt = ?T("Multiple <item/> elements are not allowed by RFC6121"),
8✔
UNCOV
173
    xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
8✔
174
process_local_iq(#iq{type = get, lang = Lang,
175
                     sub_els = [#roster_query{items = Items}]} = IQ) ->
176
    case Items of
3,137✔
177
        [] ->
178
            process_iq_get(IQ);
3,129✔
179
        [_|_] ->
UNCOV
180
            Txt = ?T("The query must not contain <item/> elements"),
8✔
UNCOV
181
            xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang))
8✔
182
    end;
183
process_local_iq(#iq{lang = Lang} = IQ) ->
UNCOV
184
    Txt = ?T("No module is handling this query"),
16✔
UNCOV
185
    xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
16✔
186

187
-spec roster_hash([#roster{}]) -> binary().
188
roster_hash(Items) ->
189
    str:sha(term_to_binary(lists:sort([R#roster_item{groups = lists:sort(Grs)}
×
190
                                       || R = #roster_item{groups = Grs}
191
                                              <- Items]))).
×
192

193
%% Returns a list that may contain an xmlelement with the XEP-237 feature if it's enabled.
194
-spec get_versioning_feature([xmpp_element()], binary()) -> [xmpp_element()].
195
get_versioning_feature(Acc, Host) ->
196
    case gen_mod:is_loaded(Host, ?MODULE) of
1,827✔
197
        true ->
198
    case mod_roster_opt:versioning(Host) of
1,827✔
199
      true ->
UNCOV
200
          [#rosterver_feature{}|Acc];
1,824✔
201
                false ->
202
                    Acc
3✔
203
            end;
204
        false ->
205
            Acc
×
206
    end.
207

208
-spec roster_version(binary(), binary()) -> undefined | binary().
209
roster_version(LServer, LUser) ->
UNCOV
210
    case mod_roster_opt:store_current_id(LServer) of
1,248✔
211
      true ->
UNCOV
212
          case read_roster_version(LUser, LServer) of
1,248✔
213
            error -> undefined;
×
UNCOV
214
            {ok, V} -> V
1,248✔
215
          end;
216
      false ->
217
          roster_hash(run_roster_get_hook(LUser, LServer))
×
218
    end.
219

220
-spec read_roster_version(binary(), binary()) -> {ok, binary()} | error.
221
read_roster_version(LUser, LServer) ->
UNCOV
222
    ets_cache:lookup(
4,376✔
223
      ?ROSTER_VERSION_CACHE, {LUser, LServer},
224
      fun() ->
UNCOV
225
              Mod = gen_mod:db_mod(LServer, ?MODULE),
3,176✔
UNCOV
226
              Mod:read_roster_version(LUser, LServer)
3,176✔
227
      end).
228

229
-spec write_roster_version(binary(), binary()) -> binary().
230
write_roster_version(LUser, LServer) ->
231
    write_roster_version(LUser, LServer, false).
×
232

233
-spec write_roster_version_t(binary(), binary()) -> binary().
234
write_roster_version_t(LUser, LServer) ->
UNCOV
235
    write_roster_version(LUser, LServer, true).
1,432✔
236

237
-spec write_roster_version(binary(), binary(), boolean()) -> binary().
238
write_roster_version(LUser, LServer, InTransaction) ->
UNCOV
239
    Ver = str:sha(term_to_binary(erlang:unique_integer())),
1,432✔
UNCOV
240
    Mod = gen_mod:db_mod(LServer, ?MODULE),
1,432✔
UNCOV
241
    Mod:write_roster_version(LUser, LServer, InTransaction, Ver),
1,432✔
UNCOV
242
    if InTransaction -> ok;
1,432✔
243
       true ->
244
            ets_cache:delete(?ROSTER_VERSION_CACHE, {LUser, LServer},
×
245
                             cache_nodes(Mod, LServer))
246
    end,
UNCOV
247
    Ver.
1,432✔
248

249
%% Load roster from DB only if necessary.
250
%% It is necessary if
251
%%     - roster versioning is disabled in server OR
252
%%     - roster versioning is not used by the client OR
253
%%     - roster versioning is used by server and client, BUT the server isn't storing versions on db OR
254
%%     - the roster version from client don't match current version.
255
-spec process_iq_get(iq()) -> iq().
256
process_iq_get(#iq{to = To, from = From,
257
                   sub_els = [#roster_query{ver = RequestedVersion, mix_annotate = MixEnabled}]} = IQ) ->
258
    LUser = To#jid.luser,
3,129✔
259
    LServer = To#jid.lserver,
3,129✔
260
    {ItemsToSend, VersionToSend} =
3,129✔
261
        case {mod_roster_opt:versioning(LServer),
262
              mod_roster_opt:store_current_id(LServer)} of
263
            {true, true} when RequestedVersion /= undefined ->
UNCOV
264
                case read_roster_version(LUser, LServer) of
3,128✔
265
                    error ->
266
                        RosterVersion = write_roster_version(LUser, LServer),
×
267
                        {run_roster_get_hook(LUser, LServer), RosterVersion};
×
268
                    {ok, RequestedVersion} ->
UNCOV
269
                        {false, false};
16✔
270
                    {ok, NewVersion} ->
UNCOV
271
                        {run_roster_get_hook(LUser, LServer), NewVersion}
3,112✔
272
                end;
273
            {true, false} when RequestedVersion /= undefined ->
274
                RosterItems = run_roster_get_hook(LUser, LServer),
×
275
                case roster_hash(RosterItems) of
×
276
                    RequestedVersion ->
277
                        {false, false};
×
278
                    New ->
279
                        {RosterItems, New}
×
280
                end;
281
            _ ->
282
                {run_roster_get_hook(LUser, LServer), false}
1✔
283
        end,
284
    % Store that MIX annotation is enabled (for roster pushes)
285
    set_mix_annotation_enabled(From, MixEnabled),
3,129✔
286
    % Only include <channel/> element when MIX annotation is enabled
287
    Items = case ItemsToSend of
3,129✔
UNCOV
288
        false -> false;
16✔
289
        FullItems -> process_items_mix(FullItems, MixEnabled)
3,113✔
290
    end,
291
    xmpp:make_iq_result(
3,129✔
292
      IQ,
293
      case {Items, VersionToSend} of
294
          {false, false} ->
UNCOV
295
              undefined;
16✔
296
          {Items, false} ->
297
              #roster_query{items = Items};
1✔
298
          {Items, Version} ->
UNCOV
299
              #roster_query{items = Items,
3,112✔
300
                            ver = Version}
301
      end).
302

303
-spec run_roster_get_hook(binary(), binary()) -> [#roster_item{}].
304
run_roster_get_hook(LUser, LServer) ->
305
    ejabberd_hooks:run_fold(roster_get, LServer, [], [{LUser, LServer}]).
3,113✔
306

307
-spec get_filtered_roster(binary(), binary()) -> [#roster{}].
308
get_filtered_roster(LUser, LServer) ->
309
    lists:filter(
3,641✔
UNCOV
310
        fun (#roster{subscription = none, ask = in}) -> false;
472✔
UNCOV
311
            (_) -> true
1,456✔
312
        end,
313
        get_roster(LUser, LServer)).
314

315
-spec get_user_roster_items([#roster_item{}], {binary(), binary()}) -> [#roster_item{}].
316
get_user_roster_items(Acc, {LUser, LServer}) ->
317
    lists:map(fun encode_item/1, get_filtered_roster(LUser, LServer)) ++ Acc.
3,625✔
318

319
-spec get_roster(binary(), binary()) -> [#roster{}].
320
get_roster(LUser, LServer) ->
321
    Mod = gen_mod:db_mod(LServer, ?MODULE),
7,543✔
322
    R = case use_cache(Mod, LServer, roster) of
7,543✔
323
            true ->
324
                ets_cache:lookup(
7,543✔
325
                  ?ROSTER_CACHE, {LUser, LServer},
326
                  fun() -> Mod:get_roster(LUser, LServer) end);
3,235✔
327
            false ->
328
                Mod:get_roster(LUser, LServer)
×
329
        end,
330
    case R of
7,543✔
331
        {ok, Items} -> Items;
7,543✔
332
        error -> []
×
333
    end.
334

335
-spec get_roster_item(binary(), binary(), ljid()) -> #roster{}.
336
get_roster_item(LUser, LServer, LJID) ->
UNCOV
337
    Mod = gen_mod:db_mod(LServer, ?MODULE),
3,400✔
UNCOV
338
    case Mod:get_roster_item(LUser, LServer, LJID) of
3,400✔
339
        {ok, Item} ->
UNCOV
340
            Item;
2,352✔
341
        error ->
UNCOV
342
            LBJID = jid:remove_resource(LJID),
1,048✔
UNCOV
343
            #roster{usj = {LUser, LServer, LBJID},
1,048✔
344
                    us = {LUser, LServer}, jid = LBJID}
345
    end.
346

347
-spec get_subscription_and_groups(binary(), binary(), ljid()) ->
348
                                         {subscription(), ask(), [binary()]}.
349
get_subscription_and_groups(LUser, LServer, LJID) ->
UNCOV
350
    LBJID = jid:remove_resource(LJID),
11,962✔
UNCOV
351
    Mod = gen_mod:db_mod(LServer, ?MODULE),
11,962✔
UNCOV
352
    Res = case use_cache(Mod, LServer, roster) of
11,962✔
353
              true ->
UNCOV
354
                  ets_cache:lookup(
11,962✔
355
                    ?ROSTER_ITEM_CACHE, {LUser, LServer, LBJID},
356
                    fun() ->
UNCOV
357
                            Items = get_roster(LUser, LServer),
614✔
UNCOV
358
                            case lists:keyfind(LBJID, #roster.jid, Items) of
614✔
359
                                #roster{subscription = Sub,
360
                                        ask = Ask,
361
                                        groups = Groups} ->
UNCOV
362
                                    {ok, {Sub, Ask, Groups}};
424✔
363
                                false ->
UNCOV
364
                                    error
190✔
365
                            end
366
                    end);
367
              false ->
368
                  case Mod:read_subscription_and_groups(LUser, LServer, LBJID) of
×
369
                      {ok, {Sub, Groups}} ->
370
                          %% Backward compatibility for third-party backends
371
                          {ok, {Sub, none, Groups}};
×
372
                      Other ->
373
                          Other
×
374
                  end
375
          end,
UNCOV
376
    case Res of
11,962✔
377
        {ok, SubAndGroups} ->
UNCOV
378
            SubAndGroups;
3,542✔
379
        error ->
UNCOV
380
            {none, none, []}
8,420✔
381
    end.
382

383
-spec set_roster(#roster{}) -> {atomic | aborted, any()}.
384
set_roster(#roster{us = {LUser, LServer}, jid = LJID} = Item) ->
UNCOV
385
    transaction(
224✔
386
      LUser, LServer, [LJID],
387
      fun() ->
UNCOV
388
              update_roster_t(LUser, LServer, LJID, Item)
224✔
389
      end).
390

391
-spec del_roster(binary(), binary(), ljid()) -> {atomic | aborted, any()}.
392
del_roster(LUser, LServer, LJID) ->
UNCOV
393
    transaction(
256✔
394
      LUser, LServer, [LJID],
395
      fun() ->
UNCOV
396
              del_roster_t(LUser, LServer, LJID)
256✔
397
      end).
398

399
-spec encode_item(#roster{}) -> roster_item().
400
encode_item(Item) ->
UNCOV
401
    #roster_item{jid = jid:make(Item#roster.jid),
3,952✔
402
                 name = Item#roster.name,
403
                 subscription = Item#roster.subscription,
404
                 ask = case ask_to_pending(Item#roster.ask) of
UNCOV
405
                           out -> subscribe;
1,024✔
UNCOV
406
                           both -> subscribe;
152✔
UNCOV
407
                           _ -> undefined
2,776✔
408
                       end,
409
                 groups = Item#roster.groups}.
410

411
-spec decode_item(roster_item(), #roster{}, boolean()) -> #roster{}.
412
decode_item(#roster_item{subscription = remove} = Item, R, _) ->
UNCOV
413
    R#roster{jid = jid:tolower(Item#roster_item.jid),
320✔
414
             name = <<"">>,
415
             subscription = remove,
416
             ask = none,
417
             groups = [],
418
             askmessage = <<"">>,
419
             xs = []};
420
decode_item(Item, R, Managed) ->
UNCOV
421
    R#roster{jid = jid:tolower(Item#roster_item.jid),
40✔
422
             name = Item#roster_item.name,
423
             subscription = case Item#roster_item.subscription of
424
                                Sub when Managed -> Sub;
×
UNCOV
425
                                _ -> R#roster.subscription
40✔
426
                            end,
427
             groups = Item#roster_item.groups}.
428

429
-spec process_iq_set(iq()) -> iq().
430
process_iq_set(#iq{from = _From, to = To, lang = Lang,
431
                   sub_els = [#roster_query{items = [QueryItem]}]} = IQ) ->
UNCOV
432
    case set_item_and_notify_clients(To, QueryItem, false) of
360✔
433
        ok ->
UNCOV
434
            xmpp:make_iq_result(IQ);
360✔
435
        {error, _} ->
436
            Txt = ?T("Database failure"),
×
437
            Err = xmpp:err_internal_server_error(Txt, Lang),
×
438
            xmpp:make_error(IQ, Err)
×
439
    end.
440

441
-spec set_item_and_notify_clients(jid(), #roster_item{}, boolean()) -> ok | {error, any()}.
442
set_item_and_notify_clients(To, #roster_item{jid = PeerJID} = RosterItem,
443
                            OverrideSubscription) ->
UNCOV
444
    #jid{luser = LUser, lserver = LServer} = To,
360✔
UNCOV
445
    PeerLJID = jid:tolower(PeerJID),
360✔
UNCOV
446
    F = fun () ->
360✔
UNCOV
447
                Item1 = get_roster_item(LUser, LServer, PeerLJID),
360✔
UNCOV
448
                Item2 = decode_item(RosterItem, Item1, OverrideSubscription),
360✔
UNCOV
449
                Item3 = ejabberd_hooks:run_fold(roster_process_item,
360✔
450
                                                LServer, Item2,
451
                                                [LServer]),
UNCOV
452
                case Item3#roster.subscription of
360✔
UNCOV
453
                    remove -> del_roster_t(LUser, LServer, PeerLJID);
320✔
UNCOV
454
                    _ -> update_roster_t(LUser, LServer, PeerLJID, Item3)
40✔
455
                end,
UNCOV
456
                case mod_roster_opt:store_current_id(LServer) of
360✔
UNCOV
457
                    true -> write_roster_version_t(LUser, LServer);
360✔
458
                    false -> ok
×
459
                end,
UNCOV
460
                {Item1, Item3}
360✔
461
        end,
UNCOV
462
    case transaction(LUser, LServer, [PeerLJID], F) of
360✔
463
        {atomic, {OldItem, NewItem}} ->
UNCOV
464
            push_item(To, encode_item(OldItem), encode_item(NewItem)),
360✔
UNCOV
465
            case NewItem#roster.subscription of
360✔
466
                remove ->
UNCOV
467
                    send_unsubscribing_presence(To, OldItem);
320✔
468
                _ ->
UNCOV
469
                    ok
40✔
470
            end;
471
        {aborted, Reason} ->
472
            {error, Reason}
×
473
    end.
474

475
-spec push_item(jid(), #roster_item{}, #roster_item{}) -> ok.
476
push_item(To, OldItem, NewItem) ->
UNCOV
477
    #jid{luser = LUser, lserver = LServer} = To,
1,248✔
UNCOV
478
    Ver = case mod_roster_opt:versioning(LServer) of
1,248✔
UNCOV
479
              true -> roster_version(LServer, LUser);
1,248✔
480
              false -> undefined
×
481
          end,
UNCOV
482
    lists:foreach(
1,248✔
483
      fun(Resource) ->
UNCOV
484
              To1 = jid:replace_resource(To, Resource),
1,248✔
UNCOV
485
              push_item(To1, OldItem, NewItem, Ver)
1,248✔
486
      end, ejabberd_sm:get_user_resources(LUser, LServer)).
487

488
-spec push_item(jid(), #roster_item{}, #roster_item{}, undefined | binary()) -> ok.
489
push_item(To, OldItem, NewItem, Ver) ->
UNCOV
490
    route_presence_change(To, OldItem, NewItem),
1,248✔
UNCOV
491
    [Item] = process_items_mix([NewItem], To),
1,248✔
UNCOV
492
    IQ = #iq{type = set, to = To,
1,248✔
493
             from = jid:remove_resource(To),
494
             id = <<"push", (p1_rand:get_string())/binary>>,
495
             sub_els = [#roster_query{ver = Ver,
496
                                      items = [Item]}]},
UNCOV
497
    ejabberd_router:route(IQ).
1,248✔
498

499
-spec route_presence_change(jid(), #roster_item{}, #roster_item{}) -> ok.
500
route_presence_change(From, OldItem, NewItem) ->
UNCOV
501
    OldSub = OldItem#roster_item.subscription,
1,248✔
UNCOV
502
    NewSub = NewItem#roster_item.subscription,
1,248✔
UNCOV
503
    To = NewItem#roster_item.jid,
1,248✔
UNCOV
504
    NewIsFrom = NewSub == both orelse NewSub == from,
1,248✔
UNCOV
505
    OldIsFrom = OldSub == both orelse OldSub == from,
1,248✔
UNCOV
506
    if NewIsFrom andalso not OldIsFrom ->
1,248✔
UNCOV
507
            case ejabberd_sm:get_session_pid(
72✔
508
                   From#jid.luser, From#jid.lserver, From#jid.lresource) of
509
                none ->
510
                    ok;
×
511
                Pid ->
UNCOV
512
                    ejabberd_c2s:resend_presence(Pid, To)
72✔
513
            end;
514
       OldIsFrom andalso not NewIsFrom ->
UNCOV
515
            PU = #presence{from = From, to = To, type = unavailable},
160✔
UNCOV
516
            case ejabberd_hooks:run_fold(
160✔
517
                   privacy_check_packet, allow,
518
                   [From, PU, out]) of
519
                deny ->
520
                    ok;
×
521
                allow ->
UNCOV
522
                    ejabberd_router:route(PU)
160✔
523
            end;
524
       true ->
UNCOV
525
            ok
1,016✔
526
    end.
527

528
-spec ask_to_pending(ask()) -> none | in | out | both.
529
ask_to_pending(subscribe) -> out;
×
530
ask_to_pending(unsubscribe) -> none;
×
UNCOV
531
ask_to_pending(Ask) -> Ask.
3,952✔
532

533
-spec roster_subscribe_t(binary(), binary(), ljid(), #roster{}) -> any().
534
roster_subscribe_t(LUser, LServer, LJID, Item) ->
UNCOV
535
    Mod = gen_mod:db_mod(LServer, ?MODULE),
1,072✔
UNCOV
536
    Mod:roster_subscribe(LUser, LServer, LJID, Item).
1,072✔
537

538
-spec transaction(binary(), binary(), [ljid()], fun(() -> T)) -> {atomic, T} | {aborted, any()}.
539
transaction(LUser, LServer, LJIDs, F) ->
UNCOV
540
    Mod = gen_mod:db_mod(LServer, ?MODULE),
3,880✔
UNCOV
541
    case Mod:transaction(LServer, F) of
3,880✔
542
        {atomic, _} = Result ->
UNCOV
543
            delete_cache(Mod, LUser, LServer, LJIDs),
3,880✔
UNCOV
544
            Result;
3,880✔
545
        Err ->
546
            Err
×
547
    end.
548

549
-spec in_subscription(boolean(), presence()) -> boolean().
550
in_subscription(_, #presence{from = JID, to = To,
551
                             sub_els = SubEls,
552
                             type = Type, status = Status}) ->
UNCOV
553
    #jid{user = User, server = Server} = To,
1,472✔
UNCOV
554
    Reason = if Type == subscribe -> xmpp:get_text(Status);
1,472✔
UNCOV
555
                true -> <<"">>
1,168✔
556
             end,
UNCOV
557
    process_subscription(in, User, Server, JID, Type,
1,472✔
558
                         Reason, SubEls).
559

560
-spec out_subscription(presence()) -> boolean().
561
out_subscription(#presence{from = From, to = JID, type = Type}) ->
UNCOV
562
    #jid{user = User, server = Server} = From,
1,568✔
UNCOV
563
    process_subscription(out, User, Server, JID, Type, <<"">>, []).
1,568✔
564

565
-spec process_subscription(in | out, binary(), binary(), jid(),
566
                           subscribe | subscribed | unsubscribe | unsubscribed,
567
                           binary(), [fxml:xmlel()]) -> boolean().
568
process_subscription(Direction, User, Server, JID1,
569
                     Type, Reason, SubEls) ->
UNCOV
570
    LUser = jid:nodeprep(User),
3,040✔
UNCOV
571
    LServer = jid:nameprep(Server),
3,040✔
UNCOV
572
    LJID = jid:tolower(jid:remove_resource(JID1)),
3,040✔
UNCOV
573
    F = fun () ->
3,040✔
UNCOV
574
                Item = get_roster_item(LUser, LServer, LJID),
3,040✔
UNCOV
575
                NewState = case Direction of
3,040✔
576
                             out ->
UNCOV
577
                                 out_state_change(Item#roster.subscription,
1,568✔
578
                                                  Item#roster.ask, Type);
579
                             in ->
UNCOV
580
                                 in_state_change(Item#roster.subscription,
1,472✔
581
                                                 Item#roster.ask, Type)
582
                           end,
UNCOV
583
                AutoReply = case Direction of
3,040✔
UNCOV
584
                              out -> none;
1,568✔
585
                              in ->
UNCOV
586
                                  in_auto_reply(Item#roster.subscription,
1,472✔
587
                                                Item#roster.ask, Type)
588
                            end,
UNCOV
589
                AskMessage = case NewState of
3,040✔
UNCOV
590
                               {_, both} -> Reason;
40✔
UNCOV
591
                               {_, in} -> Reason;
208✔
UNCOV
592
                               _ -> <<"">>
2,792✔
593
                             end,
UNCOV
594
                case NewState of
3,040✔
595
                    none ->
UNCOV
596
                        {none, AutoReply};
1,856✔
597
                    {none, none} when Item#roster.subscription == none,
598
                                      Item#roster.ask == in ->
UNCOV
599
                        del_roster_t(LUser, LServer, LJID), {none, AutoReply};
112✔
600
                    {Subscription, Pending} ->
UNCOV
601
                        NewItem = Item#roster{subscription = Subscription,
1,072✔
602
                                              ask = Pending,
603
                                              name = get_nick_subels(SubEls, Item#roster.name),
604
                                              xs = SubEls,
605
                                              askmessage = AskMessage},
UNCOV
606
                        roster_subscribe_t(LUser, LServer, LJID, NewItem),
1,072✔
UNCOV
607
                        case mod_roster_opt:store_current_id(LServer) of
1,072✔
UNCOV
608
                            true -> write_roster_version_t(LUser, LServer);
1,072✔
609
                            false -> ok
×
610
                        end,
UNCOV
611
                        {{push, Item, NewItem}, AutoReply}
1,072✔
612
                end
613
        end,
UNCOV
614
    case transaction(LUser, LServer, [LJID], F) of
3,040✔
615
        {atomic, {Push, AutoReply}} ->
UNCOV
616
            case AutoReply of
3,040✔
UNCOV
617
                none -> ok;
2,848✔
618
                _ ->
UNCOV
619
                    ejabberd_router:route(
192✔
620
                      #presence{type = AutoReply,
621
                                from = jid:make(User, Server),
622
                                to = JID1})
623
            end,
UNCOV
624
            case Push of
3,040✔
625
                {push, OldItem, NewItem} ->
UNCOV
626
                    if NewItem#roster.subscription == none,
1,072✔
627
                       NewItem#roster.ask == in ->
UNCOV
628
                            ok;
184✔
629
                       true ->
UNCOV
630
                            push_item(jid:make(User, Server),
888✔
631
                                      encode_item(OldItem),
632
                                      encode_item(NewItem))
633
                    end,
UNCOV
634
                    true;
1,072✔
635
                none ->
UNCOV
636
                    false
1,968✔
637
            end;
638
        _ ->
639
            false
×
640
    end.
641

642
get_nick_subels(SubEls, Default) ->
UNCOV
643
    case xmpp:get_subtag(#presence{sub_els = SubEls}, #nick{}) of
1,072✔
644
        {nick, N} -> N;
×
UNCOV
645
        _ -> Default
1,072✔
646
    end.
647

648
%% in_state_change(Subscription, Pending, Type) -> NewState
649
%% NewState = none | {NewSubscription, NewPending}
650
-ifdef(ROSTER_GATEWAY_WORKAROUND).
651

652
-define(NNSD, {to, none}).
653

654
-define(NISD, {to, in}).
655

656
-else.
657

658
-define(NNSD, none).
659

660
-define(NISD, none).
661

662
-endif.
663

UNCOV
664
in_state_change(none, none, subscribe) -> {none, in};
184✔
UNCOV
665
in_state_change(none, none, subscribed) -> ?NNSD;
104✔
UNCOV
666
in_state_change(none, none, unsubscribe) -> none;
144✔
UNCOV
667
in_state_change(none, none, unsubscribed) -> none;
320✔
UNCOV
668
in_state_change(none, out, subscribe) -> {none, both};
16✔
UNCOV
669
in_state_change(none, out, subscribed) -> {to, none};
8✔
UNCOV
670
in_state_change(none, out, unsubscribe) -> none;
32✔
671
in_state_change(none, out, unsubscribed) ->
UNCOV
672
    {none, none};
16✔
UNCOV
673
in_state_change(none, in, subscribe) -> none;
56✔
UNCOV
674
in_state_change(none, in, subscribed) -> ?NISD;
104✔
UNCOV
675
in_state_change(none, in, unsubscribe) -> {none, none};
96✔
UNCOV
676
in_state_change(none, in, unsubscribed) -> none;
48✔
UNCOV
677
in_state_change(none, both, subscribe) -> none;
16✔
UNCOV
678
in_state_change(none, both, subscribed) -> {to, in};
24✔
679
in_state_change(none, both, unsubscribe) -> {none, out};
×
680
in_state_change(none, both, unsubscribed) -> {none, in};
×
681
in_state_change(to, none, subscribe) -> {to, in};
×
UNCOV
682
in_state_change(to, none, subscribed) -> none;
32✔
683
in_state_change(to, none, unsubscribe) -> none;
×
UNCOV
684
in_state_change(to, none, unsubscribed) -> {none, none};
32✔
685
in_state_change(to, in, subscribe) -> none;
×
UNCOV
686
in_state_change(to, in, subscribed) -> none;
16✔
UNCOV
687
in_state_change(to, in, unsubscribe) -> {to, none};
16✔
688
in_state_change(to, in, unsubscribed) -> {none, in};
×
UNCOV
689
in_state_change(from, none, subscribe) -> none;
16✔
UNCOV
690
in_state_change(from, none, subscribed) -> {both, none};
32✔
691
in_state_change(from, none, unsubscribe) ->
UNCOV
692
    {none, none};
16✔
UNCOV
693
in_state_change(from, none, unsubscribed) -> none;
48✔
UNCOV
694
in_state_change(from, out, subscribe) -> none;
16✔
695
in_state_change(from, out, subscribed) -> {both, none};
×
696
in_state_change(from, out, unsubscribe) -> {none, out};
×
697
in_state_change(from, out, unsubscribed) ->
698
    {from, none};
×
699
in_state_change(both, none, subscribe) -> none;
×
UNCOV
700
in_state_change(both, none, subscribed) -> none;
24✔
UNCOV
701
in_state_change(both, none, unsubscribe) -> {to, none};
32✔
702
in_state_change(both, none, unsubscribed) ->
UNCOV
703
    {from, none};
24✔
704
% Invalid states that can occurs from roster modification from API
705
in_state_change(to, out, subscribe) -> {to, in};
×
706
in_state_change(to, out, subscribed) -> none;
×
707
in_state_change(to, out, unsubscribe) -> none;
×
708
in_state_change(to, out, unsubscribed) -> {none, none};
×
709
in_state_change(to, both, subscribe) -> none;
×
710
in_state_change(to, both, subscribed) -> none;
×
711
in_state_change(to, both, unsubscribe) -> {to, none};
×
712
in_state_change(to, both, unsubscribed) -> {none, in};
×
713
in_state_change(from, in, subscribe) -> none;
×
714
in_state_change(from, in, subscribed) -> {both, none};
×
715
in_state_change(from, in, unsubscribe) ->
716
    {none, none};
×
717
in_state_change(from, in, unsubscribed) -> none;
×
718
in_state_change(from, both, subscribe) -> none;
×
719
in_state_change(from, both, subscribed) -> {both, none};
×
720
in_state_change(from, both, unsubscribe) -> {none, out};
×
721
in_state_change(from, both, unsubscribed) ->
722
    {from, none};
×
723
in_state_change(both, _, subscribe) -> none;
×
724
in_state_change(both, _, subscribed) -> none;
×
725
in_state_change(both, _, unsubscribe) -> {to, none};
×
726
in_state_change(both, _, unsubscribed) ->
727
    {from, none}.
×
728

UNCOV
729
out_state_change(none, none, subscribe) -> {none, out};
176✔
UNCOV
730
out_state_change(none, none, subscribed) -> none;
104✔
UNCOV
731
out_state_change(none, none, unsubscribe) -> none;
144✔
UNCOV
732
out_state_change(none, none, unsubscribed) -> none;
216✔
733
out_state_change(none, out, subscribe) ->
UNCOV
734
    {none, out}; %% We need to resend query (RFC3921, section 9.2)
64✔
UNCOV
735
out_state_change(none, out, subscribed) -> none;
112✔
736
out_state_change(none, out, unsubscribe) ->
UNCOV
737
    {none, none};
104✔
UNCOV
738
out_state_change(none, out, unsubscribed) -> none;
48✔
UNCOV
739
out_state_change(none, in, subscribe) -> {none, both};
24✔
UNCOV
740
out_state_change(none, in, subscribed) -> {from, none};
16✔
UNCOV
741
out_state_change(none, in, unsubscribe) -> none;
32✔
742
out_state_change(none, in, unsubscribed) ->
UNCOV
743
    {none, none};
16✔
UNCOV
744
out_state_change(none, both, subscribe) -> none;
16✔
UNCOV
745
out_state_change(none, both, subscribed) -> {from, out};
16✔
746
out_state_change(none, both, unsubscribe) -> {none, in};
×
747
out_state_change(none, both, unsubscribed) ->
748
    {none, out};
×
UNCOV
749
out_state_change(to, none, subscribe) -> none;
24✔
UNCOV
750
out_state_change(to, none, subscribed) -> {both, none};
32✔
751
out_state_change(to, none, unsubscribe) -> {none, none};
×
752
out_state_change(to, none, unsubscribed) -> none;
×
UNCOV
753
out_state_change(to, in, subscribe) -> none;
16✔
UNCOV
754
out_state_change(to, in, subscribed) -> {both, none};
8✔
755
out_state_change(to, in, unsubscribe) -> {none, in};
×
756
out_state_change(to, in, unsubscribed) -> {to, none};
×
UNCOV
757
out_state_change(from, none, subscribe) -> {from, out};
8✔
UNCOV
758
out_state_change(from, none, subscribed) -> none;
16✔
759
out_state_change(from, none, unsubscribe) -> none;
×
760
out_state_change(from, none, unsubscribed) ->
UNCOV
761
    {none, none};
96✔
762
out_state_change(from, out, subscribe) -> none;
×
UNCOV
763
out_state_change(from, out, subscribed) -> none;
8✔
764
out_state_change(from, out, unsubscribe) ->
UNCOV
765
    {from, none};
24✔
766
out_state_change(from, out, unsubscribed) ->
767
    {none, out};
×
UNCOV
768
out_state_change(both, none, subscribe) -> none;
64✔
UNCOV
769
out_state_change(both, none, subscribed) -> none;
80✔
770
out_state_change(both, none, unsubscribe) ->
UNCOV
771
    {from, none};
88✔
772
out_state_change(both, none, unsubscribed) ->
UNCOV
773
    {to, none};
16✔
774
% Invalid states that can occurs from roster modification from API
775
out_state_change(to, out, subscribe) -> none;
×
776
out_state_change(to, out, subscribed) -> {both, none};
×
777
out_state_change(to, out, unsubscribe) -> {none, none};
×
778
out_state_change(to, out, unsubscribed) -> none;
×
779
out_state_change(to, both, subscribe) -> none;
×
780
out_state_change(to, both, subscribed) -> {both, none};
×
781
out_state_change(to, both, unsubscribe) -> {none, in};
×
782
out_state_change(to, both, unsubscribed) -> {to, none};
×
783
out_state_change(from, in, subscribe) -> {from, out};
×
784
out_state_change(from, in, subscribed) -> none;
×
785
out_state_change(from, in, unsubscribe) -> none;
×
786
out_state_change(from, in, unsubscribed) ->
787
    {none, none};
×
788
out_state_change(from, both, subscribe) -> none;
×
789
out_state_change(from, both, subscribed) -> none;
×
790
out_state_change(from, both, unsubscribe) ->
791
    {from, none};
×
792
out_state_change(from, both, unsubscribed) ->
793
    {none, out};
×
794
out_state_change(both, _, subscribe) -> none;
×
795
out_state_change(both, _, subscribed) -> none;
×
796
out_state_change(both, _, unsubscribe) ->
797
    {from, none};
×
798
out_state_change(both, _, unsubscribed) ->
799
    {to, none}.
×
800

UNCOV
801
in_auto_reply(from, none, subscribe) -> subscribed;
16✔
UNCOV
802
in_auto_reply(from, out, subscribe) -> subscribed;
16✔
803
in_auto_reply(both, none, subscribe) -> subscribed;
×
UNCOV
804
in_auto_reply(none, in, unsubscribe) -> unsubscribed;
96✔
805
in_auto_reply(none, both, unsubscribe) -> unsubscribed;
×
UNCOV
806
in_auto_reply(to, in, unsubscribe) -> unsubscribed;
16✔
UNCOV
807
in_auto_reply(from, none, unsubscribe) -> unsubscribed;
16✔
808
in_auto_reply(from, out, unsubscribe) -> unsubscribed;
×
UNCOV
809
in_auto_reply(both, none, unsubscribe) -> unsubscribed;
32✔
UNCOV
810
in_auto_reply(_, _, _) -> none.
1,280✔
811

812
-spec remove_user(binary(), binary()) -> ok.
813
remove_user(User, Server) ->
UNCOV
814
    LUser = jid:nodeprep(User),
16✔
UNCOV
815
    LServer = jid:nameprep(Server),
16✔
UNCOV
816
    Items = get_filtered_roster(LUser, LServer),
16✔
UNCOV
817
    send_unsubscription_to_rosteritems(LUser, LServer, Items),
16✔
UNCOV
818
    Mod = gen_mod:db_mod(LServer, ?MODULE),
16✔
UNCOV
819
    Mod:remove_user(LUser, LServer),
16✔
UNCOV
820
    delete_cache(Mod, LUser, LServer, [Item#roster.jid || Item <- Items]).
16✔
821

822
%% For each contact with Subscription:
823
%% Both or From, send a "unsubscribed" presence stanza;
824
%% Both or To, send a "unsubscribe" presence stanza.
825
-spec send_unsubscription_to_rosteritems(binary(), binary(), [#roster{}]) -> ok.
826
send_unsubscription_to_rosteritems(LUser, LServer, RosterItems) ->
UNCOV
827
    From = jid:make({LUser, LServer, <<"">>}),
16✔
UNCOV
828
    lists:foreach(fun (RosterItem) ->
16✔
829
                          send_unsubscribing_presence(From, RosterItem)
×
830
                  end,
831
                  RosterItems).
832

833
-spec send_unsubscribing_presence(jid(), #roster{}) -> ok.
834
send_unsubscribing_presence(From, Item) ->
UNCOV
835
    IsTo = case Item#roster.subscription of
320✔
836
             both -> true;
×
UNCOV
837
             to -> true;
16✔
UNCOV
838
             _ -> false
304✔
839
           end,
UNCOV
840
    IsFrom = case Item#roster.subscription of
320✔
841
               both -> true;
×
842
               from -> true;
×
UNCOV
843
               _ -> false
320✔
844
             end,
UNCOV
845
    if IsTo ->
320✔
UNCOV
846
            ejabberd_router:route(
16✔
847
              #presence{type = unsubscribe,
848
                        from = jid:remove_resource(From),
849
                        to = jid:make(Item#roster.jid)});
UNCOV
850
       true -> ok
304✔
851
    end,
UNCOV
852
    if IsFrom ->
320✔
853
            ejabberd_router:route(
×
854
              #presence{type = unsubscribed,
855
                        from = jid:remove_resource(From),
856
                        to = jid:make(Item#roster.jid)});
UNCOV
857
       true -> ok
320✔
858
    end.
859

860
%%%===================================================================
861
%%% MIX
862
%%%===================================================================
863

864
-spec remove_mix_channel([#roster_item{}]) -> [#roster_item{}].
865
remove_mix_channel(Items) ->
866
    lists:map(
4,361✔
867
        fun(Item) ->
868
            Item#roster_item{mix_channel = undefined}
2,705✔
869
        end, Items).
870

871
-spec process_items_mix([#roster_item{}], boolean() | jid()) -> [#roster_item{}].
872
process_items_mix(Items, true) -> Items;
×
873
process_items_mix(Items, false) -> remove_mix_channel(Items);
4,361✔
UNCOV
874
process_items_mix(Items, JID) -> process_items_mix(Items, is_mix_annotation_enabled(JID)).
1,248✔
875

876
-spec is_mix_annotation_enabled(jid()) -> boolean().
877
is_mix_annotation_enabled(#jid{luser = User, lserver = Host, lresource = Res}) ->
878
    case ejabberd_sm:get_user_info(User, Host, Res) of
4,377✔
879
        offline -> false;
×
880
        Info ->
881
            case lists:keyfind(?SM_MIX_ANNOTATE, 1, Info) of
4,377✔
882
                {_, true} -> true;
×
883
                _ -> false
4,377✔
884
            end
885
    end.
886

887
-spec set_mix_annotation_enabled(jid(), boolean()) -> ok | {error, any()}.
888
set_mix_annotation_enabled(#jid{luser = U, lserver = Host, lresource = R} = JID, false) ->
889
    case is_mix_annotation_enabled(JID) of
3,129✔
890
        true ->
891
            ?DEBUG("Disabling roster MIX annotation for ~ts@~ts/~ts", [U, Host, R]),
×
892
            case ejabberd_sm:del_user_info(U, Host, R, ?SM_MIX_ANNOTATE) of
×
893
                ok -> ok;
×
894
                {error, Reason} = Err ->
895
                    ?ERROR_MSG("Failed to disable roster MIX annotation for ~ts@~ts/~ts: ~p",
×
896
                        [U, Host, R, Reason]),
×
897
                    Err
×
898
            end;
899
        false -> ok
3,129✔
900
    end;
901
set_mix_annotation_enabled(#jid{luser = U, lserver = Host, lresource = R}, true)->
902
    ?DEBUG("Enabling roster MIX annotation for ~ts@~ts/~ts", [U, Host, R]),
×
903
    case ejabberd_sm:set_user_info(U, Host, R, ?SM_MIX_ANNOTATE, true) of
×
904
        ok -> ok;
×
905
        {error, Reason} = Err ->
906
            ?ERROR_MSG("Failed to enable roster MIX annotation for ~ts@~ts/~ts: ~p",
×
907
                [U, Host, R, Reason]),
×
908
            Err
×
909
    end.
910

911
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
912

913
-spec set_items(binary(), binary(), roster_query()) -> {atomic, ok} | {aborted, any()}.
914
set_items(User, Server, #roster_query{items = Items}) ->
915
    LUser = jid:nodeprep(User),
×
916
    LServer = jid:nameprep(Server),
×
917
    LJIDs = [jid:tolower(Item#roster_item.jid) || Item <- Items],
×
918
    F = fun () ->
×
919
                lists:foreach(
×
920
                  fun(Item) ->
921
                          process_item_set_t(LUser, LServer, Item)
×
922
                  end, Items)
923
        end,
924
    transaction(LUser, LServer, LJIDs, F).
×
925

926
-spec update_roster_t(binary(), binary(), ljid(), #roster{}) -> any().
927
update_roster_t(LUser, LServer, LJID, Item) ->
UNCOV
928
    Mod = gen_mod:db_mod(LServer, ?MODULE),
264✔
UNCOV
929
    Mod:update_roster(LUser, LServer, LJID, Item).
264✔
930

931
-spec del_roster_t(binary(), binary(), ljid()) -> any().
932
del_roster_t(LUser, LServer, LJID) ->
UNCOV
933
    Mod = gen_mod:db_mod(LServer, ?MODULE),
688✔
UNCOV
934
    Mod:del_roster(LUser, LServer, LJID).
688✔
935

936
-spec process_item_set_t(binary(), binary(), roster_item()) -> any().
937
process_item_set_t(LUser, LServer, #roster_item{jid = JID1} = QueryItem) ->
938
    JID = {JID1#jid.user, JID1#jid.server, <<>>},
×
939
    LJID = {JID1#jid.luser, JID1#jid.lserver, <<>>},
×
940
    Item = #roster{usj = {LUser, LServer, LJID},
×
941
                   us = {LUser, LServer}, jid = JID},
942
    Item2 = decode_item(QueryItem, Item, _Managed = true),
×
943
    case Item2#roster.subscription of
×
944
        remove -> del_roster_t(LUser, LServer, LJID);
×
945
        _ -> update_roster_t(LUser, LServer, LJID, Item2)
×
946
    end;
947
process_item_set_t(_LUser, _LServer, _) -> ok.
×
948

949
-spec c2s_self_presence({presence(), c2s_state()}) -> {presence(), c2s_state()}.
950
c2s_self_presence({_, #{pres_last := _}} = Acc) ->
UNCOV
951
    Acc;
24✔
952
c2s_self_presence({#presence{type = available} = Pkt, State}) ->
UNCOV
953
    Prio = get_priority_from_presence(Pkt),
232✔
UNCOV
954
    if Prio >= 0 ->
232✔
UNCOV
955
            State1 = resend_pending_subscriptions(State),
232✔
UNCOV
956
            {Pkt, State1};
232✔
957
       true ->
958
            {Pkt, State}
×
959
    end;
960
c2s_self_presence(Acc) ->
961
    Acc.
×
962

963
-spec resend_pending_subscriptions(c2s_state()) -> c2s_state().
964
resend_pending_subscriptions(#{jid := JID} = State) ->
UNCOV
965
    BareJID = jid:remove_resource(JID),
232✔
UNCOV
966
    Result = get_roster(JID#jid.luser, JID#jid.lserver),
232✔
UNCOV
967
    lists:foldl(
232✔
968
      fun(#roster{ask = Ask} = R, AccState) when Ask == in; Ask == both ->
969
                    Message = R#roster.askmessage,
×
970
                    Status = if is_binary(Message) -> (Message);
×
971
                                true -> <<"">>
×
972
                             end,
973
              Sub = #presence{from = jid:make(R#roster.jid),
×
974
                              to = BareJID,
975
                              type = subscribe,
976
                              sub_els = R#roster.xs,
977
                              status = xmpp:mk_text(Status)},
978
              ejabberd_c2s:send(AccState, Sub);
×
979
         (_, AccState) ->
980
              AccState
×
981
      end, State, Result).
982

983
-spec get_priority_from_presence(presence()) -> integer().
984
get_priority_from_presence(#presence{priority = Prio}) ->
UNCOV
985
    case Prio of
232✔
UNCOV
986
        undefined -> 0;
232✔
987
        _ -> Prio
×
988
    end.
989

990
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
991
-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid())
992
      -> {subscription(), ask(), [binary()]}.
993
get_jid_info(_, User, Server, JID) ->
UNCOV
994
    LUser = jid:nodeprep(User),
11,962✔
UNCOV
995
    LServer = jid:nameprep(Server),
11,962✔
UNCOV
996
    LJID = jid:tolower(JID),
11,962✔
UNCOV
997
    get_subscription_and_groups(LUser, LServer, LJID).
11,962✔
998

999
%% Check if `From` is subscriberd to `To`s presence
1000
%% note 1: partial subscriptions are also considered, i.e.
1001
%%         `To` has already sent a subscription request to `From`
1002
%% note 2: it's assumed a user is subscribed to self
1003
%% note 3: `To` MUST be a local user, `From` can be any user
1004
-spec is_subscribed(jid(), jid()) -> boolean().
1005
is_subscribed(#jid{luser = LUser, lserver = LServer},
1006
              #jid{luser = LUser, lserver = LServer}) ->
UNCOV
1007
    true;
1,244✔
1008
is_subscribed(From, #jid{luser = LUser, lserver = LServer}) ->
UNCOV
1009
    {Sub, Ask, _} = ejabberd_hooks:run_fold(
5,790✔
1010
                      roster_get_jid_info, LServer,
1011
                      {none, none, []},
1012
                      [LUser, LServer, From]),
UNCOV
1013
    (Sub /= none) orelse (Ask == subscribe)
5,790✔
UNCOV
1014
        orelse (Ask == out) orelse (Ask == both).
5,196✔
1015

1016
process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) ->
1017
    LServer = ejabberd_config:get_myname(),
×
1018
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
1019
    Mod:process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS).
×
1020

1021
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1022

1023
%%% @format-begin
1024

1025
webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) ->
UNCOV
1026
    Acc ++ [{<<"roster">>, <<"Roster">>}].
24✔
1027

1028
webadmin_page_hostuser(_, Host, Username, #request{path = [<<"roster">> | RPath]} = R) ->
1029
    Head = ?H1GL(<<"Roster">>, <<"modules/#mod_roster">>, <<"mod_roster">>),
×
1030
    %% Execute twice: first to perform the action, the second to get new roster
1031
    _ = make_webadmin_roster_table(Host, Username, R, RPath),
×
1032
    RV2 = make_webadmin_roster_table(Host, Username, R, RPath),
×
1033
    Set = [make_command(add_rosteritem,
×
1034
                        R,
1035
                        [{<<"localuser">>, Username}, {<<"localhost">>, Host}],
1036
                        []),
1037
           make_command(push_roster, R, [{<<"user">>, Username}, {<<"host">>, Host}], [])],
1038
    Get = [make_command(get_roster, R, [], [{only, presentation}]),
×
1039
           make_command(delete_rosteritem, R, [], [{only, presentation}]),
1040
           RV2],
1041
    {stop, Head ++ Get ++ Set};
×
1042
webadmin_page_hostuser(Acc, _, _, _) ->
1043
    Acc.
×
1044

1045
make_webadmin_roster_table(Host, Username, R, RPath) ->
1046
    Contacts =
×
1047
        case make_command_raw_value(get_roster, R, [{<<"user">>, Username}, {<<"host">>, Host}])
1048
        of
1049
            Cs when is_list(Cs) ->
1050
                Cs;
×
1051
            _ ->
1052
                []
×
1053
        end,
1054
    Level = 5 + length(RPath),
×
1055
    Columns =
×
1056
        [<<"jid">>, <<"nick">>, <<"subscription">>, <<"pending">>, <<"groups">>, <<"">>],
1057
    Rows =
×
1058
        lists:map(fun({Jid, Nick, Subscriptions, Pending, Groups}) ->
1059
                     {JidSplit, ProblematicBin} =
×
1060
                         try jid:decode(Jid) of
×
1061
                             #jid{} = J ->
1062
                                 {jid:split(J), <<"">>}
×
1063
                         catch
1064
                             _:{bad_jid, _} ->
1065
                                 ?INFO_MSG("Error parsing contact of ~s@~s that is invalid JID: ~s",
×
1066
                                           [Username, Host, Jid]),
×
1067
                                 {{<<"000--error-parsing-jid">>, <<"localhost">>, <<"">>},
×
1068
                                  <<", Error parsing JID: ", Jid/binary>>}
1069
                         end,
1070
                     {make_command(echo,
×
1071
                                   R,
1072
                                   [{<<"sentence">>, jid:encode(JidSplit)}],
1073
                                   [{only, raw_and_value},
1074
                                    {result_links, [{sentence, user, Level, <<"">>}]}]),
1075
                      ?C(<<Nick/binary, ProblematicBin/binary>>),
1076
                      ?C(Subscriptions),
1077
                      ?C(Pending),
1078
                      ?C(Groups),
1079
                      make_command(delete_rosteritem,
1080
                                   R,
1081
                                   [{<<"localuser">>, Username},
1082
                                    {<<"localhost">>, Host},
1083
                                    {<<"user">>, element(1, JidSplit)},
1084
                                    {<<"host">>, element(2, JidSplit)}],
1085
                                   [{only, button},
1086
                                    {style, danger},
1087
                                    {input_name_append,
1088
                                     [Username,
1089
                                      Host,
1090
                                      element(1, JidSplit),
1091
                                      element(2, JidSplit)]}])}
1092
                  end,
1093
                  lists:keysort(1, Contacts)),
1094
    Table = make_table(20, RPath, Columns, Rows),
×
1095
    ?XE(<<"blockquote">>, [Table]).
×
1096

1097
webadmin_user(Acc, User, Server, R) ->
1098
    Acc
UNCOV
1099
    ++ [make_command(get_roster_count,
24✔
1100
                     R,
1101
                     [{<<"user">>, User}, {<<"host">>, Server}],
1102
                     [{result_links,
1103
                       [{value, arg_host, 4, <<"user/", User/binary, "/roster/">>}]}])].
1104
%%% @format-end
1105

1106
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1107
-spec has_duplicated_groups([binary()]) -> boolean().
1108
has_duplicated_groups(Groups) ->
UNCOV
1109
    GroupsPrep = lists:usort([jid:resourceprep(G) || G <- Groups]),
368✔
UNCOV
1110
    not (length(GroupsPrep) == length(Groups)).
368✔
1111

1112
-spec init_cache(module(), binary(), gen_mod:opts()) -> ok.
1113
init_cache(Mod, Host, Opts) ->
1114
    CacheOpts = cache_opts(Opts),
9✔
1115
    case use_cache(Mod, Host, roster_version) of
9✔
1116
        true ->
1117
            ets_cache:new(?ROSTER_VERSION_CACHE, CacheOpts);
9✔
1118
        false ->
1119
            ets_cache:delete(?ROSTER_VERSION_CACHE)
×
1120
    end,
1121
    case use_cache(Mod, Host, roster) of
9✔
1122
        true ->
1123
            ets_cache:new(?ROSTER_CACHE, CacheOpts),
9✔
1124
            ets_cache:new(?ROSTER_ITEM_CACHE, CacheOpts);
9✔
1125
        false ->
1126
            ets_cache:delete(?ROSTER_CACHE),
×
1127
            ets_cache:delete(?ROSTER_ITEM_CACHE)
×
1128
    end.
1129

1130
-spec cache_opts(gen_mod:opts()) -> [proplists:property()].
1131
cache_opts(Opts) ->
1132
    MaxSize = mod_roster_opt:cache_size(Opts),
9✔
1133
    CacheMissed = mod_roster_opt:cache_missed(Opts),
9✔
1134
    LifeTime = mod_roster_opt:cache_life_time(Opts),
9✔
1135
    [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
9✔
1136

1137
-spec use_cache(module(), binary(), roster | roster_version) -> boolean().
1138
use_cache(Mod, Host, Table) ->
1139
    case erlang:function_exported(Mod, use_cache, 2) of
27,315✔
1140
        true -> Mod:use_cache(Host, Table);
11,271✔
UNCOV
1141
        false -> mod_roster_opt:use_cache(Host)
16,044✔
1142
    end.
1143

1144
-spec cache_nodes(module(), binary()) -> [node()].
1145
cache_nodes(Mod, Host) ->
UNCOV
1146
    case erlang:function_exported(Mod, cache_nodes, 1) of
7,792✔
1147
        true -> Mod:cache_nodes(Host);
×
UNCOV
1148
        false -> ejabberd_cluster:get_nodes()
7,792✔
1149
    end.
1150

1151
-spec delete_cache(module(), binary(), binary(), [ljid()]) -> ok.
1152
delete_cache(Mod, LUser, LServer, LJIDs) ->
UNCOV
1153
    case use_cache(Mod, LServer, roster_version) of
3,896✔
1154
        true ->
UNCOV
1155
            ets_cache:delete(?ROSTER_VERSION_CACHE, {LUser, LServer},
3,896✔
1156
                             cache_nodes(Mod, LServer));
1157
        false ->
1158
            ok
×
1159
    end,
UNCOV
1160
    case use_cache(Mod, LServer, roster) of
3,896✔
1161
        true ->
UNCOV
1162
            Nodes = cache_nodes(Mod, LServer),
3,896✔
UNCOV
1163
            ets_cache:delete(?ROSTER_CACHE, {LUser, LServer}, Nodes),
3,896✔
UNCOV
1164
            lists:foreach(
3,896✔
1165
              fun(LJID) ->
UNCOV
1166
                      ets_cache:delete(
3,880✔
1167
                        ?ROSTER_ITEM_CACHE,
1168
                        {LUser, LServer, jid:remove_resource(LJID)},
1169
                        Nodes)
1170
              end, LJIDs);
1171
        false ->
1172
            ok
×
1173
    end.
1174

1175
export(LServer) ->
1176
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
1177
    Mod:export(LServer).
×
1178

1179
import_info() ->
1180
    [{<<"roster_version">>, 2},
×
1181
     {<<"rostergroups">>, 3},
1182
     {<<"rosterusers">>, 10}].
1183

1184
import_start(LServer, DBType) ->
1185
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
1186
    ets:new(rostergroups_tmp, [private, named_table, bag]),
×
1187
    Mod:init(LServer, []),
×
1188
    ok.
×
1189

1190
import_stop(_LServer, _DBType) ->
1191
    ets:delete(rostergroups_tmp),
×
1192
    ok.
×
1193

1194
row_length() ->
1195
    case ejabberd_sql:use_multihost_schema() of
×
1196
        true -> 10;
×
1197
        false -> 9
×
1198
    end.
1199

1200
import(LServer, {sql, _}, _DBType, <<"rostergroups">>, [LUser, SJID, Group]) ->
1201
    LJID = jid:tolower(jid:decode(SJID)),
×
1202
    ets:insert(rostergroups_tmp, {{LUser, LServer, LJID}, Group}),
×
1203
    ok;
×
1204
import(LServer, {sql, _}, DBType, <<"rosterusers">>, Row) ->
1205
    I = mod_roster_sql:raw_to_record(LServer, lists:sublist(Row, row_length())),
×
1206
    Groups = [G || {_, G} <- ets:lookup(rostergroups_tmp, I#roster.usj)],
×
1207
    RosterItem = I#roster{groups = Groups},
×
1208
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
1209
    Mod:import(LServer, <<"rosterusers">>, RosterItem);
×
1210
import(LServer, {sql, _}, DBType, <<"roster_version">>, [LUser, Ver]) ->
1211
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
1212
    Mod:import(LServer, <<"roster_version">>, [LUser, Ver]).
×
1213

1214
mod_opt_type(access) ->
1215
    econf:acl();
9✔
1216
mod_opt_type(store_current_id) ->
1217
    econf:bool();
9✔
1218
mod_opt_type(versioning) ->
1219
    econf:bool();
9✔
1220
mod_opt_type(db_type) ->
1221
    econf:db_type(?MODULE);
9✔
1222
mod_opt_type(use_cache) ->
1223
    econf:bool();
9✔
1224
mod_opt_type(cache_size) ->
1225
    econf:pos_int(infinity);
9✔
1226
mod_opt_type(cache_missed) ->
1227
    econf:bool();
9✔
1228
mod_opt_type(cache_life_time) ->
1229
    econf:timeout(second, infinity).
9✔
1230

1231
mod_options(Host) ->
1232
    [{access, all},
9✔
1233
     {store_current_id, false},
1234
     {versioning, false},
1235
     {db_type, ejabberd_config:default_db(Host, ?MODULE)},
1236
     {use_cache, ejabberd_option:use_cache(Host)},
1237
     {cache_size, ejabberd_option:cache_size(Host)},
1238
     {cache_missed, ejabberd_option:cache_missed(Host)},
1239
     {cache_life_time, ejabberd_option:cache_life_time(Host)}].
1240

1241
mod_doc() ->
1242
    #{desc =>
×
1243
          ?T("This module implements roster management as "
1244
             "defined in https://tools.ietf.org/html/rfc6121#section-2"
1245
             "[RFC6121 Section 2]. The module also adds support for "
1246
             "https://xmpp.org/extensions/xep-0237.html"
1247
             "[XEP-0237: Roster Versioning]."),
1248
      opts =>
1249
          [{access,
1250
            #{value => ?T("AccessName"),
1251
              desc =>
1252
                  ?T("This option can be configured to specify "
1253
                     "rules to restrict roster management. "
1254
                     "If the rule returns 'deny' on the requested "
1255
                     "user name, that user cannot modify their personal "
1256
                     "roster, i.e. they cannot add/remove/modify contacts "
1257
                     "or send presence subscriptions. "
1258
                     "The default value is 'all', i.e. no restrictions.")}},
1259
           {versioning,
1260
            #{value => "true | false",
1261
              desc =>
1262
                  ?T("Enables/disables Roster Versioning. "
1263
                     "The default value is 'false'.")}},
1264
           {store_current_id,
1265
            #{value => "true | false",
1266
              desc =>
1267
                  ?T("If this option is set to 'true', the current "
1268
                     "roster version number is stored on the database. "
1269
                     "If set to 'false', the roster version number is "
1270
                     "calculated on the fly each time. Enabling this "
1271
                     "option reduces the load for both ejabberd and the database. "
1272
                     "This option does not affect the client in any way. "
1273
                     "This option is only useful if option 'versioning' is "
1274
                     "set to 'true'. The default value is 'false'. "
1275
                     "IMPORTANT: if you use _`mod_shared_roster`_ or "
1276
                     " _`mod_shared_roster_ldap`_, you must set the value "
1277
                     "of the option to 'false'.")}},
1278
           {db_type,
1279
            #{value => "mnesia | sql",
1280
              desc =>
1281
                  ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
1282
           {use_cache,
1283
            #{value => "true | false",
1284
              desc =>
1285
                  ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
1286
           {cache_size,
1287
            #{value => "pos_integer() | infinity",
1288
              desc =>
1289
                  ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
1290
           {cache_missed,
1291
            #{value => "true | false",
1292
              desc =>
1293
                  ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
1294
           {cache_life_time,
1295
            #{value => "timeout()",
1296
              desc =>
1297
                  ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}],
1298
      example =>
1299
          ["modules:",
1300
           "  mod_roster:",
1301
           "    versioning: true",
1302
           "    store_current_id: false"]}.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc