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

processone / ejabberd / 1258

12 Dec 2025 03:57PM UTC coverage: 33.638% (-0.006%) from 33.644%
1258

push

github

badlop
Container: Apply commit a22c88a

ejabberdctl.template: Show meaningful error when ERL_DIST_PORT is in use

15554 of 46240 relevant lines covered (33.64%)

1078.28 hits per line

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

66.95
/src/mod_caps.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_caps.erl
3
%%% Author  : Magnus Henoch <henoch@dtek.chalmers.se>
4
%%% Purpose : Request and cache Entity Capabilities (XEP-0115)
5
%%% Created : 7 Oct 2006 by Magnus Henoch <henoch@dtek.chalmers.se>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-2025   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
%%% 2009, improvements from ProcessOne to support correct PEP handling
25
%%% through s2s, use less memory, and speedup global caps handling
26
%%%----------------------------------------------------------------------
27

28
-module(mod_caps).
29

30
-author('henoch@dtek.chalmers.se').
31

32
-protocol({xep, 115, '1.6.0', '2.1.4', "complete", ""}).
33

34
-behaviour(gen_server).
35

36
-behaviour(gen_mod).
37

38
-export([read_caps/1, list_features/1, caps_stream_features/2,
39
         disco_features/5, disco_identity/5, disco_info/5,
40
         get_features/2, export/1, import_info/0, import/5,
41
         get_user_caps/2, import_start/2, import_stop/2,
42
         compute_disco_hash/2, is_valid_node/1]).
43

44
%% gen_mod callbacks
45
-export([start/2, stop/1, reload/3, depends/2]).
46

47
%% gen_server callbacks
48
-export([init/1, handle_info/2, handle_call/3,
49
         handle_cast/2, terminate/2, code_change/3]).
50

51
-export([user_send_packet/1, user_receive_packet/1,
52
         c2s_presence_in/2, c2s_copy_session/2,
53
         mod_opt_type/1, mod_options/1, mod_doc/0]).
54

55
-include("logger.hrl").
56

57
-include_lib("xmpp/include/xmpp.hrl").
58
-include("mod_caps.hrl").
59
-include("translate.hrl").
60

61
-define(BAD_HASH_LIFETIME, 600).
62

63
-record(state, {host = <<"">> :: binary()}).
64

65
-type digest_type() :: md5 | sha | sha224 | sha256 | sha384 | sha512.
66

67
-callback init(binary(), gen_mod:opts()) -> any().
68
-callback import(binary(), {binary(), binary()}, [binary() | pos_integer()]) -> ok.
69
-callback caps_read(binary(), {binary(), binary()}) ->
70
    {ok, non_neg_integer() | [binary()]} | error.
71
-callback caps_write(binary(), {binary(), binary()},
72
                     non_neg_integer() | [binary()]) -> any().
73
-callback use_cache(binary()) -> boolean().
74

75
-optional_callbacks([use_cache/1]).
76

77
start(Host, Opts) ->
78
    gen_mod:start_child(?MODULE, Host, Opts).
8✔
79

80
stop(Host) ->
81
    gen_mod:stop_child(?MODULE, Host).
8✔
82

83
-spec get_features(binary(), nothing | caps()) -> [binary()].
84
get_features(_Host, nothing) -> [];
×
85
get_features(Host, #caps{node = Node, version = Version,
86
                   exts = Exts}) ->
87
    SubNodes = [Version | Exts],
24✔
88
    Mod = gen_mod:db_mod(Host, ?MODULE),
24✔
89
    lists:foldl(
24✔
90
      fun(SubNode, Acc) ->
91
              NodePair = {Node, SubNode},
24✔
92
              Res = case use_cache(Mod, Host) of
24✔
93
                        true ->
94
                            ets_cache:lookup(caps_features_cache, NodePair,
24✔
95
                                             caps_read_fun(Host, NodePair));
96
                        false ->
97
                            Mod:caps_read(Host, NodePair)
×
98
                    end,
99
              case Res of
24✔
100
                  {ok, Features} when is_list(Features) ->
101
                      Features ++ Acc;
8✔
102
                  _ -> Acc
16✔
103
              end
104
      end, [], SubNodes).
105

106
-spec list_features(ejabberd_c2s:state()) -> [{ljid(), caps()}].
107
list_features(C2SState) ->
108
    Rs = maps:get(caps_resources, C2SState, gb_trees:empty()),
24✔
109
    gb_trees:to_list(Rs).
24✔
110

111
-spec get_user_caps(jid() | ljid(), ejabberd_c2s:state()) -> {ok, caps()} | error.
112
get_user_caps(JID, C2SState) ->
113
    Rs = maps:get(caps_resources, C2SState, gb_trees:empty()),
×
114
    LJID = jid:tolower(JID),
×
115
    case gb_trees:lookup(LJID, Rs) of
×
116
        {value, Caps} ->
117
            {ok, Caps};
×
118
        none ->
119
            error
×
120
    end.
121

122
-spec read_caps(#presence{}) -> nothing | caps().
123
read_caps(Presence) ->
124
    case xmpp:get_subtag(Presence, #caps{}) of
8,228✔
125
        false -> nothing;
6,476✔
126
        Caps -> Caps
1,752✔
127
    end.
128

129
-spec user_send_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}.
130
user_send_packet({#presence{type = available,
131
                            from = #jid{luser = U, lserver = LServer} = From,
132
                            to = #jid{luser = U, lserver = LServer,
133
                                      lresource = <<"">>}} = Pkt,
134
                  #{jid := To} = State}) ->
135
    case read_caps(Pkt) of
232✔
136
        nothing -> ok;
224✔
137
        #caps{version = Version, exts = Exts} = Caps ->
138
            feature_request(LServer, From, To, Caps, [Version | Exts])
8✔
139
    end,
140
    {Pkt, State};
232✔
141
user_send_packet(Acc) ->
142
    Acc.
41,688✔
143

144
-spec user_receive_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}.
145
user_receive_packet({#presence{from = From, type = available} = Pkt,
146
                     #{lserver := LServer, jid := To} = State}) ->
147
    IsRemote = case From#jid.lresource of
3,650✔
148
                   % Don't store caps for presences sent by our muc rooms
149
                   <<>> ->
150
                       try ejabberd_router:host_of_route(From#jid.lserver) of
1,736✔
151
                           MaybeMuc ->
152
                               not lists:member(From#jid.lserver,
1,736✔
153
                                                gen_mod:get_module_opt_hosts(MaybeMuc, mod_muc))
154
                       catch error:{unregistered_route, _} ->
155
                           true
×
156
                       end;
157
                   _ ->
158
                       not ejabberd_router:is_my_host(From#jid.lserver)
1,914✔
159
               end,
160
    if IsRemote ->
3,650✔
161
           case read_caps(Pkt) of
1,360✔
162
             nothing -> ok;
1,360✔
163
             #caps{version = Version, exts = Exts} = Caps ->
164
                    feature_request(LServer, To, From, Caps, [Version | Exts])
×
165
           end;
166
       true -> ok
2,290✔
167
    end,
168
    {Pkt, State};
3,650✔
169
user_receive_packet(Acc) ->
170
    Acc.
42,606✔
171

172
-spec caps_stream_features([xmpp_element()], binary()) -> [xmpp_element()].
173
caps_stream_features(Acc, MyHost) ->
174
    case gen_mod:is_loaded(MyHost, ?MODULE) of
1,824✔
175
        true ->
176
            case make_my_disco_hash(MyHost) of
1,824✔
177
                <<"">> ->
178
                    Acc;
×
179
                Hash ->
180
                    [#caps{hash = <<"sha-1">>, node = ejabberd_config:get_uri(),
1,824✔
181
                           version = Hash} | Acc]
182
            end;
183
        false ->
184
            Acc
×
185
    end.
186

187
-spec disco_features({error, stanza_error()} | {result, [binary()]} | empty,
188
                     jid(), jid(),
189
                     binary(), binary()) ->
190
                            {error, stanza_error()} | {result, [binary()]} | empty.
191
disco_features(Acc, From, To, Node, Lang) ->
192
    case is_valid_node(Node) of
1,872✔
193
        true ->
194
            ejabberd_hooks:run_fold(disco_local_features,
×
195
                                    To#jid.lserver, empty,
196
                                    [From, To, <<"">>, Lang]);
197
        false ->
198
            Acc
1,872✔
199
    end.
200

201
-spec disco_identity([identity()], jid(), jid(),
202
                     binary(), binary()) ->
203
                            [identity()].
204
disco_identity(Acc, From, To, Node, Lang) ->
205
    case is_valid_node(Node) of
1,872✔
206
        true ->
207
            ejabberd_hooks:run_fold(disco_local_identity,
×
208
                                    To#jid.lserver, [],
209
                                    [From, To, <<"">>, Lang]);
210
        false ->
211
            Acc
1,872✔
212
    end.
213

214
-spec disco_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()];
215
                ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()].
216
disco_info(Acc, Host, Module, Node, Lang) when is_atom(Module) ->
217
    case is_valid_node(Node) of
1,896✔
218
        true ->
219
            ejabberd_hooks:run_fold(disco_info, Host, [],
×
220
                                    [Host, Module, <<"">>, Lang]);
221
        false ->
222
            Acc
1,896✔
223
    end;
224
disco_info(Acc, _, _, _Node, _Lang) ->
225
    Acc.
56✔
226

227
-spec c2s_presence_in(ejabberd_c2s:state(), presence()) -> ejabberd_c2s:state().
228
c2s_presence_in(C2SState,
229
                #presence{from = From, to = To, type = Type} = Presence) ->
230
    ToSelf = (From#jid.luser == To#jid.luser)
6,636✔
231
               andalso (From#jid.lserver == To#jid.lserver),
464✔
232
    Caps = read_caps(Presence),
6,636✔
233
    Operation =
6,636✔
234
    case {Type, ToSelf, Caps} of
235
        {unavailable, _, _} -> delete;
2,042✔
236
        {error, _, _} -> delete;
104✔
237
        {available, _, nothing} -> skip;
2,154✔
238
        {available, true, _} -> insert;
8✔
239
        {available, _, _} ->
240
            {Subscription, _, _} = ejabberd_hooks:run_fold(
1,736✔
241
                roster_get_jid_info, To#jid.lserver,
242
                {none, none, []},
243
                [To#jid.luser, To#jid.lserver, From]),
244
            case Subscription of
1,736✔
245
                from -> insert;
×
246
                both -> insert;
×
247
                _ -> skip
1,736✔
248
            end;
249
        _ ->
250
            skip
592✔
251
    end,
252
    case Operation of
6,636✔
253
        skip ->
254
            C2SState;
4,482✔
255
        delete ->
256
            LFrom = jid:tolower(From),
2,146✔
257
            Rs = maps:get(caps_resources, C2SState, gb_trees:empty()),
2,146✔
258
            C2SState#{caps_resources => gb_trees:delete_any(LFrom, Rs)};
2,146✔
259
        insert ->
260
            LFrom = jid:tolower(From),
8✔
261
            Rs = maps:get(caps_resources, C2SState, gb_trees:empty()),
8✔
262
            NewRs = case gb_trees:lookup(LFrom, Rs) of
8✔
263
                        {value, Caps} -> Rs;
×
264
                        none ->
265
                            ejabberd_hooks:run(caps_add, To#jid.lserver,
8✔
266
                                               [From, To,
267
                                                get_features(To#jid.lserver, Caps)]),
268
                            gb_trees:insert(LFrom, Caps, Rs);
8✔
269
                        _ ->
270
                            ejabberd_hooks:run(caps_update, To#jid.lserver,
×
271
                                               [From, To,
272
                                                get_features(To#jid.lserver, Caps)]),
273
                            gb_trees:update(LFrom, Caps, Rs)
×
274
                    end,
275
            C2SState#{caps_resources => NewRs}
8✔
276
    end.
277

278
-spec c2s_copy_session(ejabberd_c2s:state(), ejabberd_c2s:state())
279
      -> ejabberd_c2s:state().
280
c2s_copy_session(C2SState, #{caps_resources := Rs}) ->
281
    C2SState#{caps_resources => Rs};
×
282
c2s_copy_session(C2SState, _) ->
283
    C2SState.
×
284

285
-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}].
286
depends(_Host, _Opts) ->
287
    [].
8✔
288

289
reload(Host, NewOpts, OldOpts) ->
290
    NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
×
291
    OldMod = gen_mod:db_mod(OldOpts, ?MODULE),
×
292
    if OldMod /= NewMod ->
×
293
            NewMod:init(Host, NewOpts);
×
294
       true ->
295
            ok
×
296
    end,
297
    init_cache(NewMod, Host, NewOpts).
×
298

299
init([Host|_]) ->
300
    process_flag(trap_exit, true),
8✔
301
    Opts = gen_mod:get_module_opts(Host, ?MODULE),
8✔
302
    Mod = gen_mod:db_mod(Opts, ?MODULE),
8✔
303
    init_cache(Mod, Host, Opts),
8✔
304
    Mod:init(Host, Opts),
8✔
305
    ejabberd_hooks:add(c2s_presence_in, Host, ?MODULE,
8✔
306
                       c2s_presence_in, 75),
307
    ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
8✔
308
                       user_send_packet, 75),
309
    ejabberd_hooks:add(user_receive_packet, Host, ?MODULE,
8✔
310
                       user_receive_packet, 75),
311
    ejabberd_hooks:add(c2s_post_auth_features, Host, ?MODULE,
8✔
312
                       caps_stream_features, 75),
313
    ejabberd_hooks:add(s2s_in_post_auth_features, Host, ?MODULE,
8✔
314
                       caps_stream_features, 75),
315
    ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE,
8✔
316
                       c2s_copy_session, 75),
317
    ejabberd_hooks:add(disco_local_features, Host, ?MODULE,
8✔
318
                       disco_features, 75),
319
    ejabberd_hooks:add(disco_local_identity, Host, ?MODULE,
8✔
320
                       disco_identity, 75),
321
    ejabberd_hooks:add(disco_info, Host, ?MODULE,
8✔
322
                       disco_info, 75),
323
    {ok, #state{host = Host}}.
8✔
324

325
handle_call(stop, _From, State) ->
326
    {stop, normal, ok, State};
×
327
handle_call(Request, From, State) ->
328
    ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
×
329
    {noreply, State}.
×
330

331
handle_cast(Msg, State) ->
332
    ?WARNING_MSG("Unexpected cast: ~p", [Msg]),
×
333
    {noreply, State}.
×
334

335
handle_info({iq_reply, IQReply, {Host, From, To, Caps, SubNodes}}, State) ->
336
    feature_response(IQReply, Host, From, To, Caps, SubNodes),
8✔
337
    {noreply, State};
8✔
338
handle_info(Info, State) ->
339
    ?WARNING_MSG("Unexpected info: ~p", [Info]),
×
340
    {noreply, State}.
×
341

342
terminate(_Reason, State) ->
343
    Host = State#state.host,
8✔
344
    ejabberd_hooks:delete(c2s_presence_in, Host, ?MODULE,
8✔
345
                          c2s_presence_in, 75),
346
    ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
8✔
347
                          user_send_packet, 75),
348
    ejabberd_hooks:delete(user_receive_packet, Host,
8✔
349
                          ?MODULE, user_receive_packet, 75),
350
    ejabberd_hooks:delete(c2s_post_auth_features, Host,
8✔
351
                          ?MODULE, caps_stream_features, 75),
352
    ejabberd_hooks:delete(s2s_in_post_auth_features, Host,
8✔
353
                          ?MODULE, caps_stream_features, 75),
354
    ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE,
8✔
355
                          c2s_copy_session, 75),
356
    ejabberd_hooks:delete(disco_local_features, Host,
8✔
357
                          ?MODULE, disco_features, 75),
358
    ejabberd_hooks:delete(disco_local_identity, Host,
8✔
359
                          ?MODULE, disco_identity, 75),
360
    ejabberd_hooks:delete(disco_info, Host, ?MODULE,
8✔
361
                          disco_info, 75),
362
    ok.
8✔
363

364
code_change(_OldVsn, State, _Extra) -> {ok, State}.
×
365

366
-spec feature_request(binary(), jid(), jid(), caps(), [binary()]) -> any().
367
feature_request(Host, From, To, Caps,
368
                [SubNode | Tail] = SubNodes) ->
369
    Node = Caps#caps.node,
8✔
370
    NodePair = {Node, SubNode},
8✔
371
    Mod = gen_mod:db_mod(Host, ?MODULE),
8✔
372
    Res = case use_cache(Mod, Host) of
8✔
373
              true ->
374
                  ets_cache:lookup(caps_features_cache, NodePair,
8✔
375
                                   caps_read_fun(Host, NodePair));
376
              false ->
377
                  Mod:caps_read(Host, NodePair)
×
378
          end,
379
    case Res of
8✔
380
        {ok, Fs} when is_list(Fs) ->
381
            feature_request(Host, From, To, Caps, Tail);
×
382
        _ ->
383
            LTo = jid:tolower(To),
8✔
384
            case ets_cache:insert_new(caps_requests_cache, {LTo, NodePair}, ok) of
8✔
385
                true ->
386
                    IQ = #iq{type = get,
8✔
387
                             from = From,
388
                             to = To,
389
                             sub_els = [#disco_info{node = <<Node/binary, "#",
390
                                                             SubNode/binary>>}]},
391
                    ejabberd_router:route_iq(
8✔
392
                      IQ, {Host, From, To, Caps, SubNodes},
393
                      gen_mod:get_module_proc(Host, ?MODULE));
394
                false ->
395
                    ok
×
396
            end,
397
            feature_request(Host, From, To, Caps, Tail)
8✔
398
    end;
399
feature_request(_Host, _From, _To, _Caps, []) -> ok.
16✔
400

401
-spec feature_response(iq(), binary(), jid(), jid(), caps(), [binary()]) -> any().
402
feature_response(#iq{type = result, sub_els = [El]},
403
                 Host, From, To, Caps, [SubNode | SubNodes]) ->
404
    NodePair = {Caps#caps.node, SubNode},
8✔
405
    try
8✔
406
        DiscoInfo = xmpp:decode(El),
8✔
407
        case check_hash(Caps, DiscoInfo) of
8✔
408
            true ->
409
                Features = DiscoInfo#disco_info.features,
8✔
410
                LServer = jid:nameprep(Host),
8✔
411
                Mod = gen_mod:db_mod(LServer, ?MODULE),
8✔
412
                case Mod:caps_write(LServer, NodePair, Features) of
8✔
413
                    ok ->
414
                        case use_cache(Mod, LServer) of
8✔
415
                            true ->
416
                                ets_cache:delete(caps_features_cache, NodePair);
8✔
417
                            false ->
418
                                ok
×
419
                        end;
420
                    {error, _} ->
421
                        ok
×
422
                end;
423
            false -> ok
×
424
        end
425
    catch _:{xmpp_codec, _Why} ->
426
            ok
×
427
    end,
428
    feature_request(Host, From, To, Caps, SubNodes);
8✔
429
feature_response(_IQResult, Host, From, To, Caps,
430
                 [_SubNode | SubNodes]) ->
431
    feature_request(Host, From, To, Caps, SubNodes).
×
432

433
-spec caps_read_fun(binary(), {binary(), binary()})
434
      -> fun(() -> {ok, [binary()] | non_neg_integer()} | error).
435
caps_read_fun(Host, Node) ->
436
    LServer = jid:nameprep(Host),
32✔
437
    Mod = gen_mod:db_mod(LServer, ?MODULE),
32✔
438
    fun() -> Mod:caps_read(LServer, Node) end.
32✔
439

440
-spec make_my_disco_hash(binary()) -> binary().
441
make_my_disco_hash(Host) ->
442
    JID = jid:make(Host),
1,824✔
443
    case {ejabberd_hooks:run_fold(disco_local_features,
1,824✔
444
                                  Host, empty, [JID, JID, <<"">>, <<"">>]),
445
          ejabberd_hooks:run_fold(disco_local_identity, Host, [],
446
                                  [JID, JID, <<"">>, <<"">>]),
447
          ejabberd_hooks:run_fold(disco_info, Host, [],
448
                                  [Host, undefined, <<"">>, <<"">>])}
449
        of
450
      {{result, Features}, Identities, Info} ->
451
          Feats = lists:map(fun ({{Feat, _Host}}) -> Feat;
1,824✔
452
                                (Feat) -> Feat
112,602✔
453
                            end,
454
                            Features),
455
          DiscoInfo = #disco_info{identities = Identities,
1,824✔
456
                                  features = Feats,
457
                                  xdata = Info},
458
          compute_disco_hash(DiscoInfo, sha);
1,824✔
459
      _Err -> <<"">>
×
460
    end.
461

462
-spec compute_disco_hash(disco_info(), digest_type()) -> binary().
463
compute_disco_hash(DiscoInfo, Algo) ->
464
    Concat = list_to_binary([concat_identities(DiscoInfo),
3,688✔
465
                             concat_features(DiscoInfo), concat_info(DiscoInfo)]),
466
    base64:encode(case Algo of
3,688✔
467
                      md5 -> erlang:md5(Concat);
×
468
                      sha -> crypto:hash(sha, Concat);
3,688✔
469
                      sha224 -> crypto:hash(sha224, Concat);
×
470
                      sha256 -> crypto:hash(sha256, Concat);
×
471
                      sha384 -> crypto:hash(sha384, Concat);
×
472
                      sha512 -> crypto:hash(sha512, Concat)
×
473
                  end).
474

475
-spec check_hash(caps(), disco_info()) -> boolean().
476
check_hash(Caps, DiscoInfo) ->
477
    case Caps#caps.hash of
8✔
478
      <<"md5">> ->
479
          Caps#caps.version == compute_disco_hash(DiscoInfo, md5);
×
480
      <<"sha-1">> ->
481
          Caps#caps.version == compute_disco_hash(DiscoInfo, sha);
8✔
482
      <<"sha-224">> ->
483
          Caps#caps.version == compute_disco_hash(DiscoInfo, sha224);
×
484
      <<"sha-256">> ->
485
          Caps#caps.version == compute_disco_hash(DiscoInfo, sha256);
×
486
      <<"sha-384">> ->
487
          Caps#caps.version == compute_disco_hash(DiscoInfo, sha384);
×
488
      <<"sha-512">> ->
489
          Caps#caps.version == compute_disco_hash(DiscoInfo, sha512);
×
490
      _ -> true
×
491
    end.
492

493
-spec concat_features(disco_info()) -> iolist().
494
concat_features(#disco_info{features = Features}) ->
495
    lists:usort([[Feat, $<] || Feat <- Features]).
3,688✔
496

497
-spec concat_identities(disco_info()) -> iolist().
498
concat_identities(#disco_info{identities = Identities}) ->
499
    lists:sort(
3,688✔
500
      [[Cat, $/, T, $/, Lang, $/, Name, $<] ||
5,512✔
501
          #identity{category = Cat, type = T,
502
                    lang = Lang, name = Name} <- Identities]).
3,688✔
503

504
-spec concat_info(disco_info()) -> iolist().
505
concat_info(#disco_info{xdata = Xs}) ->
506
    lists:sort(
3,688✔
507
      [concat_xdata_fields(X) || #xdata{type = result} = X <- Xs]).
3,680✔
508

509
-spec concat_xdata_fields(xdata()) -> iolist().
510
concat_xdata_fields(#xdata{fields = Fields} = X) ->
511
    Form = xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X),
3,680✔
512
    Res = [[Var, $<, lists:sort([[Val, $<] || Val <- Values])]
3,680✔
513
           || #xdata_field{var = Var, values = Values} <- Fields,
3,680✔
514
              is_binary(Var), Var /= <<"FORM_TYPE">>],
18,554✔
515
    [Form, $<, lists:sort(Res)].
3,680✔
516

517
-spec is_valid_node(binary()) -> boolean().
518
is_valid_node(Node) ->
519
    case str:tokens(Node, <<"#">>) of
5,640✔
520
        [H|_] ->
521
            H == ejabberd_config:get_uri();
×
522
        [] ->
523
            false
5,640✔
524
    end.
525

526
init_cache(Mod, Host, Opts) ->
527
    CacheOpts = cache_opts(Opts),
8✔
528
    case use_cache(Mod, Host) of
8✔
529
        true ->
530
            ets_cache:new(caps_features_cache, CacheOpts);
8✔
531
        false ->
532
            ets_cache:delete(caps_features_cache)
×
533
    end,
534
    CacheSize = proplists:get_value(max_size, CacheOpts),
8✔
535
    ets_cache:new(caps_requests_cache,
8✔
536
                  [{max_size, CacheSize},
537
                   {life_time, timer:seconds(?BAD_HASH_LIFETIME)}]).
538

539
use_cache(Mod, Host) ->
540
    case erlang:function_exported(Mod, use_cache, 1) of
48✔
541
        true -> Mod:use_cache(Host);
×
542
        false -> mod_caps_opt:use_cache(Host)
48✔
543
    end.
544

545
cache_opts(Opts) ->
546
    MaxSize = mod_caps_opt:cache_size(Opts),
8✔
547
    CacheMissed = mod_caps_opt:cache_missed(Opts),
8✔
548
    LifeTime = mod_caps_opt:cache_life_time(Opts),
8✔
549
    [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
8✔
550

551
export(LServer) ->
552
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
553
    Mod:export(LServer).
×
554

555
import_info() ->
556
    [{<<"caps_features">>, 4}].
×
557

558
import_start(LServer, DBType) ->
559
    ets:new(caps_features_tmp, [private, named_table, bag]),
×
560
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
561
    Mod:init(LServer, []),
×
562
    ok.
×
563

564
import(_LServer, {sql, _}, _DBType, <<"caps_features">>,
565
       [Node, SubNode, Feature, _TimeStamp]) ->
566
    Feature1 = case catch binary_to_integer(Feature) of
×
567
                   I when is_integer(I), I>0 -> I;
×
568
                   _ -> Feature
×
569
               end,
570
    ets:insert(caps_features_tmp, {{Node, SubNode}, Feature1}),
×
571
    ok.
×
572

573
import_stop(LServer, DBType) ->
574
    import_next(LServer, DBType, ets:first(caps_features_tmp)),
×
575
    ets:delete(caps_features_tmp),
×
576
    ok.
×
577

578
import_next(_LServer, _DBType, '$end_of_table') ->
579
    ok;
×
580
import_next(LServer, DBType, NodePair) ->
581
    Features = [F || {_, F} <- ets:lookup(caps_features_tmp, NodePair)],
×
582
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
583
    Mod:import(LServer, NodePair, Features),
×
584
    import_next(LServer, DBType, ets:next(caps_features_tmp, NodePair)).
×
585

586
mod_opt_type(db_type) ->
587
    econf:db_type(?MODULE);
8✔
588
mod_opt_type(use_cache) ->
589
    econf:bool();
8✔
590
mod_opt_type(cache_size) ->
591
    econf:pos_int(infinity);
8✔
592
mod_opt_type(cache_missed) ->
593
    econf:bool();
8✔
594
mod_opt_type(cache_life_time) ->
595
    econf:timeout(second, infinity).
8✔
596

597
mod_options(Host) ->
598
    [{db_type, ejabberd_config:default_db(Host, ?MODULE)},
8✔
599
     {use_cache, ejabberd_option:use_cache(Host)},
600
     {cache_size, ejabberd_option:cache_size(Host)},
601
     {cache_missed, ejabberd_option:cache_missed(Host)},
602
     {cache_life_time, ejabberd_option:cache_life_time(Host)}].
603

604
mod_doc() ->
605
    #{desc =>
×
606
          [?T("This module implements "
607
              "https://xmpp.org/extensions/xep-0115.html"
608
              "[XEP-0115: Entity Capabilities]."),
609
           ?T("The main purpose of the module is to provide "
610
              "PEP functionality (see _`mod_pubsub`_).")],
611
      opts =>
612
          [{db_type,
613
            #{value => "mnesia | sql",
614
              desc =>
615
                  ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
616
           {use_cache,
617
            #{value => "true | false",
618
              desc =>
619
                  ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
620
           {cache_size,
621
            #{value => "pos_integer() | infinity",
622
              desc =>
623
                  ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
624
           {cache_missed,
625
            #{value => "true | false",
626
              desc =>
627
                  ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
628
           {cache_life_time,
629
            #{value => "timeout()",
630
              desc =>
631
                  ?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

© 2025 Coveralls, Inc