• 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

69.08
/src/mod_privacy.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_privacy.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose : jabber:iq:privacy support
5
%%% Created : 21 Jul 2003 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
-module(mod_privacy).
27

28
-author('alexey@process-one.net').
29

30
-protocol({xep, 16, '1.7', '0.5.0', "complete", ""}).
31

32
-behaviour(gen_mod).
33

34
-export([start/2, stop/1, reload/3, process_iq/1, export/1,
35
         c2s_copy_session/2, push_list_update/2, disco_features/5,
36
         check_packet/4, remove_user/2, encode_list_item/1,
37
         get_user_lists/2, get_user_list/3,
38
         set_list/1, set_list/4, set_default_list/3,
39
         user_send_packet/1, mod_doc/0,
40
         import_start/2, import_stop/2, import/5, import_info/0,
41
         mod_opt_type/1, mod_options/1, depends/2]).
42

43
-export([webadmin_menu_hostuser/4, webadmin_page_hostuser/4]).
44

45
-import(ejabberd_web_admin, [make_command/4, make_command/2]).
46

47
-include("logger.hrl").
48
-include_lib("xmpp/include/xmpp.hrl").
49
-include("ejabberd_http.hrl").
50
-include("ejabberd_web_admin.hrl").
51
-include("mod_privacy.hrl").
52
-include("translate.hrl").
53

54
-define(PRIVACY_CACHE, privacy_cache).
55
-define(PRIVACY_LIST_CACHE, privacy_list_cache).
56

57
-type c2s_state() :: ejabberd_c2s:state().
58
-callback init(binary(), gen_mod:opts()) -> any().
59
-callback import(#privacy{}) -> ok.
60
-callback set_default(binary(), binary(), binary()) ->
61
          ok | {error, notfound | any()}.
62
-callback unset_default(binary(), binary()) -> ok | {error, any()}.
63
-callback remove_list(binary(), binary(), binary()) ->
64
          ok | {error, notfound | conflict | any()}.
65
-callback remove_lists(binary(), binary()) -> ok | {error, any()}.
66
-callback set_lists(#privacy{}) -> ok | {error, any()}.
67
-callback set_list(binary(), binary(), binary(), [listitem()]) ->
68
          ok | {error, any()}.
69
-callback get_list(binary(), binary(), binary() | default) ->
70
          {ok, {binary(), [listitem()]}} | error | {error, any()}.
71
-callback get_lists(binary(), binary()) ->
72
          {ok, #privacy{}} | error | {error, any()}.
73
-callback use_cache(binary()) -> boolean().
74
-callback cache_nodes(binary()) -> [node()].
75

76
-optional_callbacks([use_cache/1, cache_nodes/1]).
77

78
start(Host, Opts) ->
UNCOV
79
    Mod = gen_mod:db_mod(Opts, ?MODULE),
8✔
UNCOV
80
    Mod:init(Host, Opts),
8✔
UNCOV
81
    init_cache(Mod, Host, Opts),
8✔
UNCOV
82
    {ok, [{hook, disco_local_features, disco_features, 50},
8✔
83
          {hook, c2s_copy_session, c2s_copy_session, 50},
84
          {hook, user_send_packet, user_send_packet, 50},
85
          {hook, privacy_check_packet, check_packet, 50},
86
          {hook, remove_user, remove_user, 50},
87
          {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50},
88
          {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50},
89
          {iq_handler, ejabberd_sm, ?NS_PRIVACY, process_iq}]}.
90

91
stop(_Host) ->
UNCOV
92
    ok.
8✔
93

94
reload(Host, NewOpts, OldOpts) ->
95
    NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
×
96
    OldMod = gen_mod:db_mod(OldOpts, ?MODULE),
×
97
    if NewMod /= OldMod ->
×
98
            NewMod:init(Host, NewOpts);
×
99
       true ->
100
            ok
×
101
    end,
102
    init_cache(NewMod, Host, NewOpts).
×
103

104
-spec disco_features({error, stanza_error()} | {result, [binary()]} | empty,
105
                     jid(), jid(), binary(), binary()) ->
106
                            {error, stanza_error()} | {result, [binary()]}.
107
disco_features({error, Err}, _From, _To, _Node, _Lang) ->
108
    {error, Err};
×
109
disco_features(empty, _From, _To, <<"">>, _Lang) ->
110
    {result, [?NS_PRIVACY]};
×
111
disco_features({result, Feats}, _From, _To, <<"">>, _Lang) ->
UNCOV
112
    {result, [?NS_PRIVACY|Feats]};
1,872✔
113
disco_features(Acc, _From, _To, _Node, _Lang) ->
114
    Acc.
×
115

116
-spec process_iq(iq()) -> iq().
117
process_iq(#iq{type = Type,
118
               from = #jid{luser = U, lserver = S},
119
               to = #jid{luser = U, lserver = S}} = IQ) ->
UNCOV
120
    case Type of
1,328✔
UNCOV
121
        get -> process_iq_get(IQ);
112✔
UNCOV
122
        set -> process_iq_set(IQ)
1,216✔
123
    end;
124
process_iq(#iq{lang = Lang} = IQ) ->
125
    Txt = ?T("Query to another users is forbidden"),
×
126
    xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)).
×
127

128
-spec process_iq_get(iq()) -> iq().
129
process_iq_get(#iq{lang = Lang,
130
                      sub_els = [#privacy_query{default = Default,
131
                                             active = Active}]} = IQ)
132
  when Default /= undefined; Active /= undefined ->
UNCOV
133
    Txt = ?T("Only <list/> element is allowed in this query"),
16✔
UNCOV
134
    xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
16✔
135
process_iq_get(#iq{lang = Lang,
136
                   sub_els = [#privacy_query{lists = Lists}]} = IQ) ->
UNCOV
137
    case Lists of
88✔
138
        [] ->
UNCOV
139
            process_lists_get(IQ);
64✔
140
        [#privacy_list{name = ListName}] ->
UNCOV
141
            process_list_get(IQ, ListName);
16✔
142
        _ ->
UNCOV
143
            Txt = ?T("Too many <list/> elements"),
8✔
UNCOV
144
            xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang))
8✔
145
    end;
146
process_iq_get(#iq{lang = Lang} = IQ) ->
UNCOV
147
    Txt = ?T("No module is handling this query"),
8✔
UNCOV
148
    xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
8✔
149

150
-spec process_lists_get(iq()) -> iq().
151
process_lists_get(#iq{from = #jid{luser = LUser, lserver = LServer},
152
                      lang = Lang} = IQ) ->
UNCOV
153
    case get_user_lists(LUser, LServer) of
64✔
154
        {ok, #privacy{default = Default, lists = Lists}} ->
UNCOV
155
            Active = xmpp:get_meta(IQ, privacy_active_list, none),
62✔
UNCOV
156
            xmpp:make_iq_result(
62✔
157
              IQ, #privacy_query{active = Active,
158
                                 default = Default,
UNCOV
159
                                 lists = [#privacy_list{name = Name}
48✔
UNCOV
160
                                          || {Name, _} <- Lists]});
62✔
161
        error ->
UNCOV
162
            xmpp:make_iq_result(
2✔
163
              IQ, #privacy_query{active = none, default = none});
164
        {error, _} ->
165
            Txt = ?T("Database failure"),
×
166
            xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
×
167
    end.
168

169
-spec process_list_get(iq(), binary()) -> iq().
170
process_list_get(#iq{from = #jid{luser = LUser, lserver = LServer},
171
                     lang = Lang} = IQ, Name) ->
UNCOV
172
    case get_user_list(LUser, LServer, Name) of
16✔
173
        {ok, {_, List}} ->
UNCOV
174
            Items = lists:map(fun encode_list_item/1, List),
8✔
UNCOV
175
            xmpp:make_iq_result(
8✔
176
              IQ,
177
              #privacy_query{
178
                 lists = [#privacy_list{name = Name, items = Items}]});
179
        error ->
UNCOV
180
            Txt = ?T("No privacy list with this name found"),
8✔
UNCOV
181
            xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang));
8✔
182
        {error, _} ->
183
            Txt = ?T("Database failure"),
×
184
            xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
×
185
    end.
186

187
-spec encode_list_item(listitem()) -> privacy_item().
188
encode_list_item(#listitem{action = Action,
189
                           order = Order,
190
                           type = Type,
191
                           match_all = MatchAll,
192
                           match_iq = MatchIQ,
193
                           match_message = MatchMessage,
194
                           match_presence_in = MatchPresenceIn,
195
                           match_presence_out = MatchPresenceOut,
196
                           value = Value}) ->
UNCOV
197
    Item = #privacy_item{action = Action,
56✔
198
                         order = Order,
199
                         type = case Type of
UNCOV
200
                                    none -> undefined;
8✔
UNCOV
201
                                    Type -> Type
48✔
202
                                end,
203
                         value = encode_value(Type, Value)},
UNCOV
204
    case MatchAll of
56✔
205
        true ->
UNCOV
206
            Item;
8✔
207
        false ->
UNCOV
208
            Item#privacy_item{message = MatchMessage,
48✔
209
                              iq = MatchIQ,
210
                              presence_in = MatchPresenceIn,
211
                              presence_out = MatchPresenceOut}
212
    end.
213

214
-spec encode_value(listitem_type(), listitem_value()) -> binary().
215
encode_value(Type, Val) ->
UNCOV
216
    case Type of
56✔
UNCOV
217
        jid -> jid:encode(Val);
8✔
UNCOV
218
        group -> Val;
8✔
219
        subscription ->
UNCOV
220
            case Val of
32✔
UNCOV
221
                both -> <<"both">>;
8✔
UNCOV
222
                to -> <<"to">>;
8✔
UNCOV
223
                from -> <<"from">>;
8✔
UNCOV
224
                none -> <<"none">>
8✔
225
            end;
UNCOV
226
        none -> <<"">>
8✔
227
    end.
228

229
-spec decode_value(jid | subscription | group | undefined, binary()) ->
230
                          listitem_value().
231
decode_value(Type, Value) ->
UNCOV
232
    case Type of
632✔
UNCOV
233
        jid -> jid:tolower(jid:decode(Value));
208✔
234
        subscription ->
UNCOV
235
            case Value of
232✔
UNCOV
236
                <<"from">> -> from;
56✔
UNCOV
237
                <<"to">> -> to;
56✔
UNCOV
238
                <<"both">> -> both;
56✔
UNCOV
239
                <<"none">> -> none
56✔
240
            end;
UNCOV
241
        group when Value /= <<"">> -> Value;
56✔
UNCOV
242
        undefined -> none
128✔
243
    end.
244

245
-spec process_iq_set(iq()) -> iq().
246
process_iq_set(#iq{lang = Lang,
247
                      sub_els = [#privacy_query{default = Default,
248
                                                active = Active,
249
                                             lists = Lists}]} = IQ) ->
UNCOV
250
    case Lists of
1,208✔
251
        [#privacy_list{items = Items, name = ListName}]
252
          when Default == undefined, Active == undefined ->
UNCOV
253
            process_lists_set(IQ, ListName, Items);
616✔
254
        [] when Default == undefined, Active /= undefined ->
UNCOV
255
            process_active_set(IQ, Active);
528✔
256
        [] when Active == undefined, Default /= undefined ->
UNCOV
257
            process_default_set(IQ, Default);
48✔
258
        _ ->
UNCOV
259
            Txt = ?T("The stanza MUST contain only one <active/> element, "
16✔
260
                     "one <default/> element, or one <list/> element"),
UNCOV
261
            xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang))
16✔
262
    end;
263
process_iq_set(#iq{lang = Lang} = IQ) ->
UNCOV
264
    Txt = ?T("No module is handling this query"),
8✔
UNCOV
265
    xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
8✔
266

267
-spec process_default_set(iq(), none | binary()) -> iq().
268
process_default_set(#iq{from = #jid{luser = LUser, lserver = LServer},
269
                        lang = Lang} = IQ, Value) ->
UNCOV
270
    case set_default_list(LUser, LServer, Value) of
48✔
271
        ok ->
UNCOV
272
            xmpp:make_iq_result(IQ);
40✔
273
        {error, notfound} ->
UNCOV
274
            Txt = ?T("No privacy list with this name found"),
8✔
UNCOV
275
            xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang));
8✔
276
        {error, _} ->
277
            Txt = ?T("Database failure"),
×
278
            xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
×
279
    end.
280

281
-spec process_active_set(IQ, none | binary()) -> IQ.
282
process_active_set(IQ, none) ->
UNCOV
283
    xmpp:make_iq_result(xmpp:put_meta(IQ, privacy_active_list, none));
8✔
284
process_active_set(#iq{from = #jid{luser = LUser, lserver = LServer},
285
                       lang = Lang} = IQ, Name) ->
UNCOV
286
    case get_user_list(LUser, LServer, Name) of
520✔
287
        {ok, _} ->
UNCOV
288
            xmpp:make_iq_result(xmpp:put_meta(IQ, privacy_active_list, Name));
512✔
289
        error ->
UNCOV
290
            Txt = ?T("No privacy list with this name found"),
8✔
UNCOV
291
            xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang));
8✔
292
        {error, _} ->
293
            Txt = ?T("Database failure"),
×
294
            xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
×
295
    end.
296

297
-spec set_list(privacy()) -> ok | {error, any()}.
298
set_list(#privacy{us = {LUser, LServer}, lists = Lists} = Privacy) ->
299
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
300
    case Mod:set_lists(Privacy) of
×
301
        ok ->
302
            Names = [Name || {Name, _} <- Lists],
×
303
            delete_cache(Mod, LUser, LServer, Names);
×
304
        {error, _} = Err ->
305
            Err
×
306
    end.
307

308
-spec process_lists_set(iq(), binary(), [privacy_item()]) -> iq().
309
process_lists_set(#iq{from = #jid{luser = LUser, lserver = LServer},
310
                      lang = Lang} = IQ, Name, []) ->
UNCOV
311
    case xmpp:get_meta(IQ, privacy_active_list, none) of
32✔
312
        Name ->
UNCOV
313
            Txt = ?T("Cannot remove active list"),
8✔
UNCOV
314
            xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang));
8✔
315
        _ ->
UNCOV
316
            case remove_list(LUser, LServer, Name) of
24✔
317
                ok ->
UNCOV
318
                    xmpp:make_iq_result(IQ);
8✔
319
                {error, conflict} ->
UNCOV
320
                    Txt = ?T("Cannot remove default list"),
8✔
UNCOV
321
                    xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang));
8✔
322
                {error, notfound} ->
UNCOV
323
                    Txt = ?T("No privacy list with this name found"),
8✔
UNCOV
324
                    xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang));
8✔
325
                {error, _} ->
326
                    Txt = ?T("Database failure"),
×
327
                    Err = xmpp:err_internal_server_error(Txt, Lang),
×
328
                    xmpp:make_error(IQ, Err)
×
329
            end
330
    end;
331
process_lists_set(#iq{from = #jid{luser = LUser, lserver = LServer} = From,
332
                      lang = Lang} = IQ, Name, Items) ->
UNCOV
333
    case catch lists:map(fun decode_item/1, Items) of
584✔
334
        {error, Why} ->
UNCOV
335
            Txt = xmpp:io_format_error(Why),
24✔
UNCOV
336
            xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
24✔
337
        List ->
UNCOV
338
            case set_list(LUser, LServer, Name, List) of
560✔
339
                ok ->
UNCOV
340
                    push_list_update(From, Name),
560✔
UNCOV
341
                    xmpp:make_iq_result(IQ);
560✔
342
                {error, _} ->
343
                    Txt = ?T("Database failure"),
×
344
                    xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
×
345
            end
346
    end.
347

348
-spec push_list_update(jid(), binary()) -> ok.
349
push_list_update(From, Name) ->
UNCOV
350
    BareFrom = jid:remove_resource(From),
616✔
UNCOV
351
    lists:foreach(
616✔
352
      fun(R) ->
UNCOV
353
              To = jid:replace_resource(From, R),
616✔
UNCOV
354
              IQ = #iq{type = set, from = BareFrom, to = To,
616✔
355
                       id = <<"push", (p1_rand:get_string())/binary>>,
356
                       sub_els = [#privacy_query{
357
                                     lists = [#privacy_list{name = Name}]}]},
UNCOV
358
              ejabberd_router:route(IQ)
616✔
359
      end, ejabberd_sm:get_user_resources(From#jid.luser, From#jid.lserver)).
360

361
-spec decode_item(privacy_item()) -> listitem().
362
decode_item(#privacy_item{order = Order,
363
                          action = Action,
364
                          type = T,
365
                          value = V,
366
                          message = MatchMessage,
367
                          iq = MatchIQ,
368
                          presence_in = MatchPresenceIn,
369
                          presence_out = MatchPresenceOut}) ->
UNCOV
370
    Value = try decode_value(T, V)
632✔
371
            catch _:_ ->
UNCOV
372
                    throw({error, {bad_attr_value, <<"value">>,
24✔
373
                                   <<"item">>, ?NS_PRIVACY}})
374
            end,
UNCOV
375
    Type = case T of
608✔
UNCOV
376
               undefined -> none;
128✔
UNCOV
377
               _ -> T
480✔
378
           end,
UNCOV
379
    ListItem = #listitem{order = Order,
608✔
380
                         action = Action,
381
                         type = Type,
382
                         value = Value},
UNCOV
383
    if not (MatchMessage or MatchIQ or MatchPresenceIn or MatchPresenceOut) ->
608✔
UNCOV
384
            ListItem#listitem{match_all = true};
160✔
385
       true ->
UNCOV
386
            ListItem#listitem{match_iq = MatchIQ,
448✔
387
                              match_message = MatchMessage,
388
                              match_presence_in = MatchPresenceIn,
389
                              match_presence_out = MatchPresenceOut}
390
    end.
391

392
-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state().
393
c2s_copy_session(State, #{privacy_active_list := List}) ->
394
    State#{privacy_active_list => List};
×
395
c2s_copy_session(State, _) ->
396
    State.
×
397

398
%% Adjust the client's state, so next packets (which can be already queued)
399
%% will take the active list into account.
400
-spec update_c2s_state_with_privacy_list(stanza(), c2s_state()) -> c2s_state().
401
update_c2s_state_with_privacy_list(#iq{type = set,
402
                                       to = #jid{luser = U, lserver = S,
403
                                                 lresource = <<"">>} = To} = IQ,
404
                                   State) ->
405
    %% Match a IQ set containing a new active privacy list
UNCOV
406
    case xmpp:get_subtag(IQ, #privacy_query{}) of
7,064✔
407
        #privacy_query{default = undefined, active = Active} ->
UNCOV
408
            case Active of
1,152✔
409
                none ->
UNCOV
410
                    ?DEBUG("Removing active privacy list for user: ~ts",
8✔
UNCOV
411
                           [jid:encode(To)]),
8✔
UNCOV
412
                    State#{privacy_active_list => none};
8✔
413
                undefined ->
UNCOV
414
                    State;
624✔
415
                _ ->
UNCOV
416
                    case get_user_list(U, S, Active) of
520✔
417
                        {ok, _} ->
UNCOV
418
                            ?DEBUG("Setting active privacy list '~ts' for user: ~ts",
512✔
UNCOV
419
                                   [Active, jid:encode(To)]),
512✔
UNCOV
420
                            State#{privacy_active_list => Active};
512✔
421
                        _ ->
422
                            %% unknown privacy list name
UNCOV
423
                            State
8✔
424
                    end
425
            end;
426
        _ ->
UNCOV
427
            State
5,912✔
428
    end;
429
update_c2s_state_with_privacy_list(_Packet, State) ->
UNCOV
430
    State.
34,856✔
431

432
%% Add the active privacy list to packet metadata
433
-spec user_send_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}.
434
user_send_packet({#iq{type = Type,
435
                      to = #jid{luser = U, lserver = S, lresource = <<"">>},
436
                      from = #jid{luser = U, lserver = S},
437
                      sub_els = [_]} = IQ,
438
                  #{privacy_active_list := Name} = State})
439
  when Type == get; Type == set ->
UNCOV
440
    NewIQ = case xmpp:has_subtag(IQ, #privacy_query{}) of
840✔
UNCOV
441
                true -> xmpp:put_meta(IQ, privacy_active_list, Name);
840✔
442
                false -> IQ
×
443
            end,
UNCOV
444
    {NewIQ, update_c2s_state_with_privacy_list(IQ, State)};
840✔
445
%% For client with no active privacy list, see if there is
446
%% one about to be activated in this packet and update client state
447
user_send_packet({Packet, State}) ->
UNCOV
448
    {Packet, update_c2s_state_with_privacy_list(Packet, State)}.
41,080✔
449

450
-spec set_list(binary(), binary(), binary(), [listitem()]) -> ok | {error, any()}.
451
set_list(LUser, LServer, Name, List) ->
UNCOV
452
    Mod = gen_mod:db_mod(LServer, ?MODULE),
616✔
UNCOV
453
    case Mod:set_list(LUser, LServer, Name, List) of
616✔
454
        ok ->
UNCOV
455
            delete_cache(Mod, LUser, LServer, [Name]);
616✔
456
        {error, _} = Err ->
457
            Err
×
458
    end.
459

460
-spec remove_list(binary(), binary(), binary()) ->
461
      ok | {error, conflict | notfound | any()}.
462
remove_list(LUser, LServer, Name) ->
UNCOV
463
    Mod = gen_mod:db_mod(LServer, ?MODULE),
24✔
UNCOV
464
    case Mod:remove_list(LUser, LServer, Name) of
24✔
465
        ok ->
UNCOV
466
            delete_cache(Mod, LUser, LServer, [Name]);
8✔
467
        Err ->
UNCOV
468
            Err
16✔
469
    end.
470

471
-spec get_user_lists(binary(), binary()) -> {ok, privacy()} | error | {error, any()}.
472
get_user_lists(User, Server) ->
UNCOV
473
    LUser = jid:nodeprep(User),
384✔
UNCOV
474
    LServer = jid:nameprep(Server),
384✔
UNCOV
475
    Mod = gen_mod:db_mod(LServer, ?MODULE),
384✔
UNCOV
476
    case use_cache(Mod, LServer) of
384✔
477
        true ->
UNCOV
478
            ets_cache:lookup(
384✔
479
              ?PRIVACY_CACHE, {LUser, LServer},
UNCOV
480
              fun() -> Mod:get_lists(LUser, LServer) end);
310✔
481
        false ->
482
            Mod:get_lists(LUser, LServer)
×
483
    end.
484

485
-spec get_user_list(binary(), binary(), binary() | default) ->
486
      {ok, {binary(), [listitem()]}} | error | {error, any()}.
487
get_user_list(LUser, LServer, Name) ->
UNCOV
488
    Mod = gen_mod:db_mod(LServer, ?MODULE),
102,920✔
UNCOV
489
    case use_cache(Mod, LServer) of
102,920✔
490
        true ->
UNCOV
491
            ets_cache:lookup(
102,920✔
492
              ?PRIVACY_LIST_CACHE, {LUser, LServer, Name},
493
              fun() ->
UNCOV
494
                      case ets_cache:lookup(
1,122✔
495
                             ?PRIVACY_CACHE, {LUser, LServer}) of
496
                          {ok, Privacy} ->
497
                              get_list_by_name(Privacy, Name);
×
498
                          error ->
UNCOV
499
                              Mod:get_list(LUser, LServer, Name)
1,122✔
500
                      end
501
              end);
502
        false ->
503
            Mod:get_list(LUser, LServer, Name)
×
504
    end.
505

506
-spec get_list_by_name(#privacy{}, binary() | default) ->
507
      {ok, {binary(), [listitem()]}} | error.
508
get_list_by_name(#privacy{default = Default} = Privacy, default) ->
509
    get_list_by_name(Privacy, Default);
×
510
get_list_by_name(#privacy{lists = Lists}, Name) ->
511
    case lists:keyfind(Name, 1, Lists) of
×
512
        {_, List} -> {ok, {Name, List}};
×
513
        false -> error
×
514
    end.
515

516
-spec set_default_list(binary(), binary(), binary() | none) ->
517
      ok | {error, notfound | any()}.
518
set_default_list(LUser, LServer, Name) ->
UNCOV
519
    Mod = gen_mod:db_mod(LServer, ?MODULE),
80✔
UNCOV
520
    Res = case Name of
80✔
UNCOV
521
              none -> Mod:unset_default(LUser, LServer);
8✔
UNCOV
522
              _ -> Mod:set_default(LUser, LServer, Name)
72✔
523
          end,
UNCOV
524
    case Res of
80✔
525
        ok ->
UNCOV
526
            delete_cache(Mod, LUser, LServer, []);
72✔
527
        Err ->
UNCOV
528
            Err
8✔
529
    end.
530

531
-spec check_packet(allow | deny, c2s_state() | jid(), stanza(), in | out) -> allow | deny.
532
check_packet(Acc, #{jid := JID} = State, Packet, Dir) ->
UNCOV
533
    case maps:get(privacy_active_list, State, none) of
94,464✔
534
        none ->
UNCOV
535
            check_packet(Acc, JID, Packet, Dir);
81,512✔
536
        ListName ->
UNCOV
537
            #jid{luser = LUser, lserver = LServer} = JID,
12,952✔
UNCOV
538
            case get_user_list(LUser, LServer, ListName) of
12,952✔
539
                {ok, {_, List}} ->
UNCOV
540
                    do_check_packet(JID, List, Packet, Dir);
12,952✔
541
                _ ->
542
                    ?DEBUG("Non-existing active list '~ts' is set "
×
543
                           "for user '~ts'", [ListName, jid:encode(JID)]),
×
544
                    check_packet(Acc, JID, Packet, Dir)
×
545
            end
546
    end;
547
check_packet(_, JID, Packet, Dir) ->
UNCOV
548
    #jid{luser = LUser, lserver = LServer} = JID,
88,840✔
UNCOV
549
    case get_user_list(LUser, LServer, default) of
88,840✔
550
        {ok, {_, List}} ->
UNCOV
551
            do_check_packet(JID, List, Packet, Dir);
780✔
552
        _ ->
UNCOV
553
            allow
88,060✔
554
    end.
555

556
%% From is the sender, To is the destination.
557
%% If Dir = out, User@Server is the sender account (From).
558
%% If Dir = in, User@Server is the destination account (To).
559
-spec do_check_packet(jid(), [listitem()], stanza(), in | out) -> allow | deny.
560
do_check_packet(_, [], _, _) ->
UNCOV
561
    allow;
84✔
562
do_check_packet(#jid{luser = LUser, lserver = LServer}, List, Packet, Dir) ->
UNCOV
563
    From = xmpp:get_from(Packet),
13,648✔
UNCOV
564
    To = xmpp:get_to(Packet),
13,648✔
UNCOV
565
    case {From, To} of
13,648✔
566
        {#jid{luser = <<"">>, lserver = LServer},
567
         #jid{lserver = LServer}} when Dir == in ->
568
            %% Allow any packets from local server
UNCOV
569
            allow;
160✔
570
        {#jid{lserver = LServer},
571
         #jid{luser = <<"">>, lserver = LServer}} when Dir == out ->
572
            %% Allow any packets to local server
UNCOV
573
    allow;
160✔
574
        {#jid{luser = LUser, lserver = LServer, lresource = <<"">>},
575
         #jid{luser = LUser, lserver = LServer}} when Dir == in ->
576
            %% Allow incoming packets from user's bare jid to his full jid
UNCOV
577
    allow;
1,688✔
578
        {#jid{luser = LUser, lserver = LServer},
579
         #jid{luser = LUser, lserver = LServer, lresource = <<"">>}} when Dir == out ->
580
            %% Allow outgoing packets from user's full jid to his bare JID
UNCOV
581
            allow;
1,600✔
582
      _ ->
UNCOV
583
          PType = case Packet of
10,040✔
UNCOV
584
                    #message{} -> message;
4,224✔
UNCOV
585
                    #iq{} -> iq;
3,904✔
UNCOV
586
                    #presence{type = available} -> presence;
736✔
UNCOV
587
                    #presence{type = unavailable} -> presence;
736✔
UNCOV
588
                    _ -> other
440✔
589
                  end,
UNCOV
590
          PType2 = case {PType, Dir} of
10,040✔
UNCOV
591
                     {message, in} -> message;
1,960✔
UNCOV
592
                     {iq, in} -> iq;
1,952✔
UNCOV
593
                     {presence, in} -> presence_in;
976✔
UNCOV
594
                     {presence, out} -> presence_out;
496✔
UNCOV
595
                     {_, _} -> other
4,656✔
596
                   end,
UNCOV
597
          LJID = case Dir of
10,040✔
UNCOV
598
                   in -> jid:tolower(From);
4,888✔
UNCOV
599
                   out -> jid:tolower(To)
5,152✔
600
                 end,
UNCOV
601
          check_packet_aux(List, PType2, LJID, [LUser, LServer])
10,040✔
602
    end.
603

604
-spec check_packet_aux([listitem()],
605
                       message | iq | presence_in | presence_out | other,
606
                       ljid(), [binary()] | {none | both | from | to, [binary()]}) ->
607
                              allow | deny.
608
%% Ptype = message | iq | presence_in | presence_out | other
609
check_packet_aux([], _PType, _JID, _RosterInfo) ->
UNCOV
610
    allow;
6,288✔
611
check_packet_aux([Item | List], PType, JID, RosterInfo) ->
UNCOV
612
    #listitem{type = Type, value = Value, action = Action} =
10,040✔
613
        Item,
UNCOV
614
    case is_ptype_match(Item, PType) of
10,040✔
615
      true ->
UNCOV
616
            case is_type_match(Type, Value, JID, RosterInfo) of
3,800✔
UNCOV
617
                {true, _} -> Action;
3,752✔
618
                {false, RI} ->
UNCOV
619
                    check_packet_aux(List, PType, JID, RI)
48✔
620
            end;
621
      false ->
UNCOV
622
          check_packet_aux(List, PType, JID, RosterInfo)
6,240✔
623
    end.
624

625
-spec is_ptype_match(listitem(),
626
                     message | iq | presence_in | presence_out | other) ->
627
                            boolean().
628
is_ptype_match(Item, PType) ->
UNCOV
629
    case Item#listitem.match_all of
10,040✔
UNCOV
630
      true -> true;
2,040✔
631
      false ->
UNCOV
632
          case PType of
8,000✔
UNCOV
633
            message -> Item#listitem.match_message;
1,680✔
UNCOV
634
            iq -> Item#listitem.match_iq;
1,600✔
UNCOV
635
            presence_in -> Item#listitem.match_presence_in;
800✔
UNCOV
636
            presence_out -> Item#listitem.match_presence_out;
320✔
UNCOV
637
            other -> false
3,600✔
638
          end
639
    end.
640

641
-spec is_type_match(none | jid | subscription | group, listitem_value(),
642
                    ljid(), [binary()] | {none | both | from | to, [binary()]}) ->
643
    {boolean(), [binary()] | {none | both | from | to, [binary()]}}.
644
is_type_match(none, _Value, _JID, RosterInfo) ->
UNCOV
645
    {true, RosterInfo};
376✔
646
is_type_match(jid, Value, JID, RosterInfo) ->
UNCOV
647
    case Value of
1,624✔
648
        {<<"">>, Server, <<"">>} ->
UNCOV
649
            case JID of
360✔
UNCOV
650
                {_, Server, _} -> {true, RosterInfo};
360✔
651
                _ -> {false, RosterInfo}
×
652
            end;
653
        {User, Server, <<"">>} ->
UNCOV
654
            case JID of
360✔
UNCOV
655
                {User, Server, _} -> {true, RosterInfo};
360✔
656
                _ -> {false, RosterInfo}
×
657
            end;
658
        {<<"">>, Server, Resource} ->
UNCOV
659
            case JID of
360✔
UNCOV
660
                {_, Server, Resource} -> {true, RosterInfo};
360✔
661
                _ -> {false, RosterInfo}
×
662
            end;
UNCOV
663
        _ -> {Value == JID, RosterInfo}
544✔
664
    end;
665
is_type_match(subscription, Value, JID, RosterInfo) ->
UNCOV
666
    {Subscription, _} = RI = resolve_roster_info(JID, RosterInfo),
1,440✔
UNCOV
667
    {Value == Subscription, RI};
1,440✔
668
is_type_match(group, Group, JID, RosterInfo) ->
UNCOV
669
    {_, Groups} = RI = resolve_roster_info(JID, RosterInfo),
360✔
UNCOV
670
    {lists:member(Group, Groups), RI}.
360✔
671

672
-spec resolve_roster_info(ljid(), [binary()] | {none | both | from | to, [binary()]}) ->
673
    {none | both | from | to, [binary()]}.
674
resolve_roster_info(JID, [LUser, LServer]) ->
UNCOV
675
    {Subscription, _Ask, Groups} =
1,800✔
676
    ejabberd_hooks:run_fold(
677
        roster_get_jid_info, LServer,
678
        {none, none, []},
679
        [LUser, LServer, JID]),
UNCOV
680
    {Subscription, Groups};
1,800✔
681
resolve_roster_info(_, RosterInfo) ->
682
    RosterInfo.
×
683

684
-spec remove_user(binary(), binary()) -> ok.
685
remove_user(User, Server) ->
UNCOV
686
    LUser = jid:nodeprep(User),
320✔
UNCOV
687
    LServer = jid:nameprep(Server),
320✔
UNCOV
688
    Privacy = get_user_lists(LUser, LServer),
320✔
UNCOV
689
    Mod = gen_mod:db_mod(LServer, ?MODULE),
320✔
UNCOV
690
    Mod:remove_lists(LUser, LServer),
320✔
UNCOV
691
    case Privacy of
320✔
692
        {ok, #privacy{lists = Lists}} ->
UNCOV
693
            Names = [Name || {Name, _} <- Lists],
288✔
UNCOV
694
            delete_cache(Mod, LUser, LServer, Names);
288✔
695
        _ ->
UNCOV
696
            ok
32✔
697
    end.
698

699
-spec init_cache(module(), binary(), gen_mod:opts()) -> ok.
700
init_cache(Mod, Host, Opts) ->
UNCOV
701
    case use_cache(Mod, Host) of
8✔
702
        true ->
UNCOV
703
            CacheOpts = cache_opts(Opts),
8✔
UNCOV
704
            ets_cache:new(?PRIVACY_CACHE, CacheOpts),
8✔
UNCOV
705
            ets_cache:new(?PRIVACY_LIST_CACHE, CacheOpts);
8✔
706
        false ->
707
            ets_cache:delete(?PRIVACY_CACHE),
×
708
            ets_cache:delete(?PRIVACY_LIST_CACHE)
×
709
    end.
710

711
-spec cache_opts(gen_mod:opts()) -> [proplists:property()].
712
cache_opts(Opts) ->
UNCOV
713
    MaxSize = mod_privacy_opt:cache_size(Opts),
8✔
UNCOV
714
    CacheMissed = mod_privacy_opt:cache_missed(Opts),
8✔
UNCOV
715
    LifeTime = mod_privacy_opt:cache_life_time(Opts),
8✔
UNCOV
716
    [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
8✔
717

718
-spec use_cache(module(), binary()) -> boolean().
719
use_cache(Mod, Host) ->
UNCOV
720
    case erlang:function_exported(Mod, use_cache, 1) of
104,296✔
UNCOV
721
        true -> Mod:use_cache(Host);
26,116✔
UNCOV
722
        false -> mod_privacy_opt:use_cache(Host)
78,180✔
723
    end.
724

725
-spec cache_nodes(module(), binary()) -> [node()].
726
cache_nodes(Mod, Host) ->
UNCOV
727
    case erlang:function_exported(Mod, cache_nodes, 1) of
984✔
728
        true -> Mod:cache_nodes(Host);
×
UNCOV
729
        false -> ejabberd_cluster:get_nodes()
984✔
730
    end.
731

732
-spec delete_cache(module(), binary(), binary(), [binary()]) -> ok.
733
delete_cache(Mod, LUser, LServer, Names) ->
UNCOV
734
    case use_cache(Mod, LServer) of
984✔
735
        true ->
UNCOV
736
            Nodes = cache_nodes(Mod, LServer),
984✔
UNCOV
737
            ets_cache:delete(?PRIVACY_CACHE, {LUser, LServer}, Nodes),
984✔
UNCOV
738
            lists:foreach(
984✔
739
              fun(Name) ->
UNCOV
740
                      ets_cache:delete(
2,192✔
741
                        ?PRIVACY_LIST_CACHE,
742
                        {LUser, LServer, Name},
743
                        Nodes)
744
              end, [default|Names]);
745
        false ->
746
            ok
×
747
    end.
748

749
numeric_to_binary(<<0, 0, _/binary>>) ->
750
    <<"0">>;
×
751
numeric_to_binary(<<0, _, _:6/binary, T/binary>>) ->
752
    Res = lists:foldl(
×
753
            fun(X, Sum) ->
754
                    Sum*10000 + X
×
755
            end, 0, [X || <<X:16>> <= T]),
×
756
    integer_to_binary(Res).
×
757

758
bool_to_binary(<<0>>) -> <<"0">>;
×
759
bool_to_binary(<<1>>) -> <<"1">>.
×
760

761
prepare_list_data(mysql, [ID|Row]) ->
762
    [binary_to_integer(ID)|Row];
×
763
prepare_list_data(pgsql, [<<ID:64>>,
764
                          SType, SValue, SAction, SOrder, SMatchAll,
765
                          SMatchIQ, SMatchMessage, SMatchPresenceIn,
766
                          SMatchPresenceOut]) ->
767
    [ID, SType, SValue, SAction,
×
768
     numeric_to_binary(SOrder),
769
     bool_to_binary(SMatchAll),
770
     bool_to_binary(SMatchIQ),
771
     bool_to_binary(SMatchMessage),
772
     bool_to_binary(SMatchPresenceIn),
773
     bool_to_binary(SMatchPresenceOut)].
774

775
prepare_id(mysql, ID) ->
776
    binary_to_integer(ID);
×
777
prepare_id(pgsql, <<ID:32>>) ->
778
    ID.
×
779

780
import_info() ->
781
    [{<<"privacy_default_list">>, 2},
×
782
     {<<"privacy_list_data">>, 10},
783
     {<<"privacy_list">>, 4}].
784

785
import_start(LServer, DBType) ->
786
    ets:new(privacy_default_list_tmp, [private, named_table]),
×
787
    ets:new(privacy_list_data_tmp, [private, named_table, bag]),
×
788
    ets:new(privacy_list_tmp, [private, named_table, bag,
×
789
                               {keypos, #privacy.us}]),
790
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
791
    Mod:init(LServer, []).
×
792

793
import(LServer, {sql, _}, _DBType, <<"privacy_default_list">>, [LUser, Name]) ->
794
    US = {LUser, LServer},
×
795
    ets:insert(privacy_default_list_tmp, {US, Name}),
×
796
    ok;
×
797
import(LServer, {sql, SQLType}, _DBType, <<"privacy_list_data">>, Row1) ->
798
    [ID|Row] = prepare_list_data(SQLType, Row1),
×
799
    case mod_privacy_sql:raw_to_item(Row) of
×
800
        [Item] ->
801
            IS = {ID, LServer},
×
802
            ets:insert(privacy_list_data_tmp, {IS, Item}),
×
803
            ok;
×
804
        [] ->
805
            ok
×
806
    end;
807
import(LServer, {sql, SQLType}, _DBType, <<"privacy_list">>,
808
       [LUser, Name, ID, _TimeStamp]) ->
809
    US = {LUser, LServer},
×
810
    IS = {prepare_id(SQLType, ID), LServer},
×
811
    Default = case ets:lookup(privacy_default_list_tmp, US) of
×
812
                  [{_, Name}] -> Name;
×
813
                  _ -> none
×
814
              end,
815
    case [Item || {_, Item} <- ets:lookup(privacy_list_data_tmp, IS)] of
×
816
        [_|_] = Items ->
817
            Privacy = #privacy{us = {LUser, LServer},
×
818
                               default = Default,
819
                               lists = [{Name, Items}]},
820
            ets:insert(privacy_list_tmp, Privacy),
×
821
            ets:delete(privacy_list_data_tmp, IS),
×
822
            ok;
×
823
        _ ->
824
            ok
×
825
    end.
826

827
import_stop(_LServer, DBType) ->
828
    import_next(DBType, ets:first(privacy_list_tmp)),
×
829
    ets:delete(privacy_default_list_tmp),
×
830
    ets:delete(privacy_list_data_tmp),
×
831
    ets:delete(privacy_list_tmp),
×
832
    ok.
×
833

834
import_next(_DBType, '$end_of_table') ->
835
    ok;
×
836
import_next(DBType, US) ->
837
    [P|_] = Ps = ets:lookup(privacy_list_tmp, US),
×
838
    Lists = lists:flatmap(
×
839
              fun(#privacy{lists = Lists}) ->
840
                      Lists
×
841
              end, Ps),
842
    Privacy = P#privacy{lists = Lists},
×
843
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
844
    Mod:import(Privacy),
×
845
    import_next(DBType, ets:next(privacy_list_tmp, US)).
×
846

847
export(LServer) ->
848
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
849
    Mod:export(LServer).
×
850

851
%%%
852
%%% WebAdmin
853
%%%
854

855
webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) ->
UNCOV
856
    Acc ++ [{<<"privacy">>, <<"Privacy Lists">>}].
24✔
857

858
webadmin_page_hostuser(_, Host, User,
859
              #request{us = _US, path = [<<"privacy">>]} = R) ->
860
    Res = ?H1GL(<<"Privacy Lists">>, <<"modules/#mod_privacy">>, <<"mod_privacy">>)
×
861
          ++ [make_command(privacy_set, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
862
    {stop, Res};
×
863
webadmin_page_hostuser(Acc, _, _, _) -> Acc.
×
864

865
%%%
866
%%% Documentation
867
%%%
868

869
depends(_Host, _Opts) ->
UNCOV
870
    [].
8✔
871

872
mod_opt_type(db_type) ->
UNCOV
873
    econf:db_type(?MODULE);
8✔
874
mod_opt_type(use_cache) ->
UNCOV
875
    econf:bool();
8✔
876
mod_opt_type(cache_size) ->
UNCOV
877
    econf:pos_int(infinity);
8✔
878
mod_opt_type(cache_missed) ->
UNCOV
879
    econf:bool();
8✔
880
mod_opt_type(cache_life_time) ->
UNCOV
881
    econf:timeout(second, infinity).
8✔
882

883
mod_options(Host) ->
UNCOV
884
    [{db_type, ejabberd_config:default_db(Host, ?MODULE)},
8✔
885
     {use_cache, ejabberd_option:use_cache(Host)},
886
     {cache_size, ejabberd_option:cache_size(Host)},
887
     {cache_missed, ejabberd_option:cache_missed(Host)},
888
     {cache_life_time, ejabberd_option:cache_life_time(Host)}].
889

890
mod_doc() ->
891
    #{desc =>
×
892
          [?T("This module implements "
893
              "https://xmpp.org/extensions/xep-0016.html"
894
              "[XEP-0016: Privacy Lists]."), "",
895
           ?T("NOTE: Nowadays modern XMPP clients rely on "
896
              "https://xmpp.org/extensions/xep-0191.html"
897
              "[XEP-0191: Blocking Command] which is implemented by "
898
              "_`mod_blocking`_. However, you still need "
899
              "'mod_privacy' loaded in order for 'mod_blocking' to work.")],
900
      opts =>
901
          [{db_type,
902
            #{value => "mnesia | sql",
903
              desc =>
904
                  ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
905
           {use_cache,
906
            #{value => "true | false",
907
              desc =>
908
                  ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
909
           {cache_size,
910
            #{value => "pos_integer() | infinity",
911
              desc =>
912
                  ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
913
           {cache_missed,
914
            #{value => "true | false",
915
              desc =>
916
                  ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
917
           {cache_life_time,
918
            #{value => "timeout()",
919
              desc =>
920
                  ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
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