• 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

51.2
/src/mod_vcard.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_vcard.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose : Vcard management
5
%%% Created :  2 Jan 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_vcard).
27

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

30
-protocol({xep, 54, '1.3.0', '0.1.0', "complete", ""}).
31
-protocol({xep, 55, '1.3', '0.1.0', "complete", ""}).
32
-protocol({xep, 153, '1.1.1', '17.09', "complete", ""}).
33

34
-behaviour(gen_server).
35
-behaviour(gen_mod).
36

37
-export([start/2, stop/1, get_sm_features/5, mod_options/1, mod_doc/0,
38
         process_local_iq/1, process_sm_iq/1, string2lower/1,
39
         remove_user/2, export/1, import_info/0, import/5, import_start/2,
40
         depends/2, process_search/1, process_vcard/1, get_vcard/2,
41
         disco_items/5, disco_features/5, disco_identity/5,
42
         vcard_iq_set/1, mod_opt_type/1, set_vcard/3, make_vcard_search/4]).
43
-export([init/1, handle_call/3, handle_cast/2,
44
         handle_info/2, terminate/2, code_change/3]).
45
-export([route/1]).
46
-export([webadmin_menu_hostuser/4, webadmin_page_hostuser/4]).
47

48
-import(ejabberd_web_admin, [make_command/4, make_command/2, make_table/2]).
49

50
-include("logger.hrl").
51
-include_lib("xmpp/include/xmpp.hrl").
52
-include("mod_vcard.hrl").
53
-include("translate.hrl").
54

55
-include("ejabberd_http.hrl").
56
-include("ejabberd_web_admin.hrl").
57

58
-define(VCARD_CACHE, vcard_cache).
59

60
-callback init(binary(), gen_mod:opts()) -> any().
61
-callback stop(binary()) -> any().
62
-callback import(binary(), binary(), [binary()]) -> ok.
63
-callback get_vcard(binary(), binary()) -> {ok, [xmlel()]} | error.
64
-callback set_vcard(binary(), binary(),
65
                    xmlel(), #vcard_search{}) -> {atomic, any()}.
66
-callback search_fields(binary()) -> [{binary(), binary()}].
67
-callback search_reported(binary()) -> [{binary(), binary()}].
68
-callback search(binary(), [{binary(), [binary()]}], boolean(),
69
                 infinity | pos_integer()) -> [{binary(), binary()}].
70
-callback remove_user(binary(), binary()) -> {atomic, any()}.
71
-callback is_search_supported(binary()) -> boolean().
72
-callback use_cache(binary()) -> boolean().
73
-callback cache_nodes(binary()) -> [node()].
74

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

77
-record(state, {hosts :: [binary()], server_host :: binary()}).
78

79
%%====================================================================
80
%% gen_mod callbacks
81
%%====================================================================
82
start(Host, Opts) ->
83
    gen_mod:start_child(?MODULE, Host, Opts).
9✔
84

85
stop(Host) ->
86
    gen_mod:stop_child(?MODULE, Host).
9✔
87

88
%%====================================================================
89
%% gen_server callbacks
90
%%====================================================================
91
init([Host|_]) ->
92
    process_flag(trap_exit, true),
9✔
93
    Opts = gen_mod:get_module_opts(Host, ?MODULE),
9✔
94
    Mod = gen_mod:db_mod(Opts, ?MODULE),
9✔
95
    Mod:init(Host, Opts),
9✔
96
    init_cache(Mod, Host, Opts),
9✔
97
    ejabberd_hooks:add(remove_user, Host, ?MODULE,
9✔
98
                       remove_user, 50),
99
    gen_iq_handler:add_iq_handler(ejabberd_local, Host,
9✔
100
                                  ?NS_VCARD, ?MODULE, process_local_iq),
101
    gen_iq_handler:add_iq_handler(ejabberd_sm, Host,
9✔
102
                                  ?NS_VCARD, ?MODULE, process_sm_iq),
103
    ejabberd_hooks:add(disco_sm_features, Host, ?MODULE,
9✔
104
                       get_sm_features, 50),
105
    ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50),
9✔
106
    ejabberd_hooks:add(webadmin_menu_hostuser, Host, ?MODULE, webadmin_menu_hostuser, 50),
9✔
107
    ejabberd_hooks:add(webadmin_page_hostuser, Host, ?MODULE, webadmin_page_hostuser, 50),
9✔
108
    MyHosts = gen_mod:get_opt_hosts(Opts),
9✔
109
    Search = mod_vcard_opt:search(Opts),
9✔
110
    if Search ->
9✔
111
            lists:foreach(
×
112
              fun(MyHost) ->
113
                      ejabberd_hooks:add(
×
114
                        disco_local_items, MyHost, ?MODULE, disco_items, 100),
115
                      ejabberd_hooks:add(
×
116
                        disco_local_features, MyHost, ?MODULE, disco_features, 100),
117
                      ejabberd_hooks:add(
×
118
                        disco_local_identity, MyHost, ?MODULE, disco_identity, 100),
119
                      gen_iq_handler:add_iq_handler(
×
120
                        ejabberd_local, MyHost, ?NS_SEARCH, ?MODULE, process_search),
121
                      gen_iq_handler:add_iq_handler(
×
122
                        ejabberd_local, MyHost, ?NS_VCARD, ?MODULE, process_vcard),
123
                      gen_iq_handler:add_iq_handler(
×
124
                        ejabberd_local, MyHost, ?NS_DISCO_ITEMS, mod_disco,
125
                        process_local_iq_items),
126
                      gen_iq_handler:add_iq_handler(
×
127
                        ejabberd_local, MyHost, ?NS_DISCO_INFO, mod_disco,
128
                        process_local_iq_info),
129
                      case Mod:is_search_supported(Host) of
×
130
                          false ->
131
                              ?WARNING_MSG("vCard search functionality is "
×
132
                                           "not implemented for ~ts backend",
133
                                           [mod_vcard_opt:db_type(Opts)]);
×
134
                          true ->
135
                              ejabberd_router:register_route(
×
136
                                MyHost, Host, {apply, ?MODULE, route})
137
                      end
138
              end, MyHosts);
139
       true ->
140
            ok
9✔
141
    end,
142
    {ok, #state{hosts = MyHosts, server_host = Host}}.
9✔
143

144
handle_call(Call, From, State) ->
145
    ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Call]),
×
146
    {noreply, State}.
×
147

148
handle_cast(Cast, State) ->
149
    ?WARNING_MSG("Unexpected cast: ~p", [Cast]),
×
150
    {noreply, State}.
×
151

152
handle_info({route, Packet}, State) ->
153
    try route(Packet)
×
154
    catch
155
        Class:Reason:StackTrace ->
156
            ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts",
×
157
                       [xmpp:pp(Packet),
158
                        misc:format_exception(2, Class, Reason, StackTrace)])
×
159
    end,
160
    {noreply, State};
×
161
handle_info(Info, State) ->
162
    ?WARNING_MSG("Unexpected info: ~p", [Info]),
×
163
    {noreply, State}.
×
164

165
terminate(_Reason, #state{hosts = MyHosts, server_host = Host}) ->
166
    ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50),
9✔
167
    gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD),
9✔
168
    gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_VCARD),
9✔
169
    ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50),
9✔
170
    ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50),
9✔
171
    ejabberd_hooks:delete(webadmin_menu_hostuser, Host, ?MODULE, webadmin_menu_hostuser, 50),
9✔
172
    ejabberd_hooks:delete(webadmin_page_hostuser, Host, ?MODULE, webadmin_page_hostuser, 50),
9✔
173
    Mod = gen_mod:db_mod(Host, ?MODULE),
9✔
174
    Mod:stop(Host),
9✔
175
    lists:foreach(
9✔
176
      fun(MyHost) ->
177
              ejabberd_router:unregister_route(MyHost),
9✔
178
              ejabberd_hooks:delete(disco_local_items, MyHost, ?MODULE, disco_items, 100),
9✔
179
              ejabberd_hooks:delete(disco_local_features, MyHost, ?MODULE, disco_features, 100),
9✔
180
              ejabberd_hooks:delete(disco_local_identity, MyHost, ?MODULE, disco_identity, 100),
9✔
181
              gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_SEARCH),
9✔
182
              gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_VCARD),
9✔
183
              gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS),
9✔
184
              gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO)
9✔
185
      end, MyHosts).
186

187
code_change(_OldVsn, State, _Extra) ->
188
    {ok, State}.
×
189

190
-spec route(stanza()) -> ok.
191
route(#iq{} = IQ) ->
192
    ejabberd_router:process_iq(IQ);
×
193
route(_) ->
194
    ok.
×
195

196
-spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]},
197
                      jid(), jid(), binary(), binary()) ->
198
                             {error, stanza_error()} | empty | {result, [binary()]}.
199
get_sm_features({error, _Error} = Acc, _From, _To,
200
                _Node, _Lang) ->
201
    Acc;
×
202
get_sm_features(Acc, _From, _To, Node, _Lang) ->
UNCOV
203
    case Node of
56✔
204
      <<"">> ->
UNCOV
205
          case Acc of
32✔
206
            {result, Features} ->
UNCOV
207
                {result, [?NS_VCARD | Features]};
32✔
208
            empty -> {result, [?NS_VCARD]}
×
209
          end;
UNCOV
210
      _ -> Acc
24✔
211
    end.
212

213
-spec process_local_iq(iq()) -> iq().
214
process_local_iq(#iq{type = set, lang = Lang} = IQ) ->
215
    Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
×
216
    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
×
217
process_local_iq(#iq{type = get, to = To, lang = Lang} = IQ) ->
UNCOV
218
    ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
8✔
UNCOV
219
    VCard = case mod_vcard_opt:vcard(ServerHost) of
8✔
220
                undefined ->
221
                    #vcard_temp{fn = <<"ejabberd">>,
×
222
                                url = ejabberd_config:get_uri(),
223
                                desc = misc:get_descr(Lang, ?T("Erlang XMPP Server")),
224
                                bday = <<"2002-11-16">>};
225
                V ->
UNCOV
226
                    V
8✔
227
            end,
UNCOV
228
    xmpp:make_iq_result(IQ, VCard).
8✔
229

230
-spec process_sm_iq(iq()) -> iq().
231
process_sm_iq(#iq{type = set, lang = Lang, from = From} = IQ) ->
UNCOV
232
    #jid{lserver = LServer} = From,
8✔
UNCOV
233
    case lists:member(LServer, ejabberd_option:hosts()) of
8✔
234
        true ->
UNCOV
235
            case ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []) of
8✔
236
                drop -> ignore;
×
237
                #stanza_error{} = Err -> xmpp:make_error(IQ, Err);
×
UNCOV
238
                _ -> xmpp:make_iq_result(IQ)
8✔
239
            end;
240
        false ->
241
            Txt = ?T("The query is only allowed from local users"),
×
242
            xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang))
×
243
    end;
244
process_sm_iq(#iq{type = get, from = From, to = To, lang = Lang} = IQ) ->
245
    #jid{luser = LUser, lserver = LServer} = To,
9✔
246
    case get_vcard(LUser, LServer) of
9✔
247
        error ->
248
            Txt = ?T("Database failure"),
×
249
            xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang));
×
250
        [] ->
251
            xmpp:make_iq_result(IQ, #vcard_temp{});
×
252
        Els ->
253
            IQ#iq{type = result, to = From, from = To, sub_els = Els}
9✔
254
    end.
255

256
-spec process_vcard(iq()) -> iq().
257
process_vcard(#iq{type = set, lang = Lang} = IQ) ->
258
    Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
×
259
    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
×
260
process_vcard(#iq{type = get, lang = Lang} = IQ) ->
261
    xmpp:make_iq_result(
×
262
      IQ, #vcard_temp{fn = <<"ejabberd/mod_vcard">>,
263
                      url = ejabberd_config:get_uri(),
264
                      desc = misc:get_descr(Lang, ?T("ejabberd vCard module"))}).
265

266
-spec process_search(iq()) -> iq().
267
process_search(#iq{type = get, to = To, lang = Lang} = IQ) ->
268
    ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
×
269
    xmpp:make_iq_result(IQ, mk_search_form(To, ServerHost, Lang));
×
270
process_search(#iq{type = set, to = To, lang = Lang,
271
                   sub_els = [#search{xdata = #xdata{type = submit,
272
                                                     fields = Fs}}]} = IQ) ->
273
    ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
×
274
    ResultXData = search_result(Lang, To, ServerHost, Fs),
×
275
    xmpp:make_iq_result(IQ, #search{xdata = ResultXData});
×
276
process_search(#iq{type = set, lang = Lang} = IQ) ->
277
    Txt = ?T("Incorrect data form"),
×
278
    xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)).
×
279

280
-spec disco_items({error, stanza_error()} | {result, [disco_item()]} | empty,
281
                  jid(), jid(), binary(), binary()) ->
282
                         {error, stanza_error()} | {result, [disco_item()]}.
283
disco_items(empty, _From, _To, <<"">>, _Lang) ->
284
    {result, []};
×
285
disco_items(empty, _From, _To, _Node, Lang) ->
286
    {error, xmpp:err_item_not_found(?T("No services available"), Lang)};
×
287
disco_items(Acc, _From, _To, _Node, _Lang) ->
288
    Acc.
×
289

290
-spec disco_features({error, stanza_error()} | {result, [binary()]} | empty,
291
                     jid(), jid(), binary(), binary()) ->
292
                            {error, stanza_error()} | {result, [binary()]}.
293
disco_features({error, _Error} = Acc, _From, _To, _Node, _Lang) ->
294
    Acc;
×
295
disco_features(Acc, _From, _To, <<"">>, _Lang) ->
296
    Features = case Acc of
×
297
                   {result, Fs} -> Fs;
×
298
                   empty -> []
×
299
               end,
300
    {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
×
301
              ?NS_VCARD, ?NS_SEARCH | Features]};
302
disco_features(empty, _From, _To, _Node, Lang) ->
303
    Txt = ?T("No features available"),
×
304
    {error, xmpp:err_item_not_found(Txt, Lang)};
×
305
disco_features(Acc, _From, _To, _Node, _Lang) ->
306
    Acc.
×
307

308
-spec disco_identity([identity()], jid(), jid(),
309
                     binary(),  binary()) -> [identity()].
310
disco_identity(Acc, _From, To, <<"">>, Lang) ->
311
    Host = ejabberd_router:host_of_route(To#jid.lserver),
×
312
    Name = mod_vcard_opt:name(Host),
×
313
    [#identity{category = <<"directory">>,
×
314
               type = <<"user">>,
315
               name = translate:translate(Lang, Name)}|Acc];
316
disco_identity(Acc, _From, _To, _Node, _Lang) ->
317
    Acc.
×
318

319
-spec get_vcard(binary(), binary()) -> [xmlel()] | error.
320
get_vcard(LUser, LServer) ->
321
    Mod = gen_mod:db_mod(LServer, ?MODULE),
41✔
322
    Result = case use_cache(Mod, LServer) of
41✔
323
                 true ->
324
                     ets_cache:lookup(
41✔
325
                       ?VCARD_CACHE, {LUser, LServer},
326
                       fun() -> Mod:get_vcard(LUser, LServer) end);
33✔
327
                 false ->
328
                     Mod:get_vcard(LUser, LServer)
×
329
             end,
330
    case Result of
41✔
331
        {ok, Els} -> Els;
41✔
332
        error -> error
×
333
    end.
334

335
-spec make_vcard_search(binary(), binary(), binary(), xmlel()) -> #vcard_search{}.
336
make_vcard_search(User, LUser, LServer, VCARD) ->
UNCOV
337
    FN = fxml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]),
8✔
UNCOV
338
    Family = fxml:get_path_s(VCARD,
8✔
339
                            [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]),
UNCOV
340
    Given = fxml:get_path_s(VCARD,
8✔
341
                           [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]),
UNCOV
342
    Middle = fxml:get_path_s(VCARD,
8✔
343
                            [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]),
UNCOV
344
    Nickname = fxml:get_path_s(VCARD,
8✔
345
                              [{elem, <<"NICKNAME">>}, cdata]),
UNCOV
346
    BDay = fxml:get_path_s(VCARD,
8✔
347
                          [{elem, <<"BDAY">>}, cdata]),
UNCOV
348
    CTRY = fxml:get_path_s(VCARD,
8✔
349
                          [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]),
UNCOV
350
    Locality = fxml:get_path_s(VCARD,
8✔
351
                              [{elem, <<"ADR">>}, {elem, <<"LOCALITY">>},
352
                               cdata]),
UNCOV
353
    EMail1 = fxml:get_path_s(VCARD,
8✔
354
                            [{elem, <<"EMAIL">>}, {elem, <<"USERID">>}, cdata]),
UNCOV
355
    EMail2 = fxml:get_path_s(VCARD,
8✔
356
                            [{elem, <<"EMAIL">>}, cdata]),
UNCOV
357
    OrgName = fxml:get_path_s(VCARD,
8✔
358
                             [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]),
UNCOV
359
    OrgUnit = fxml:get_path_s(VCARD,
8✔
360
                             [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]),
UNCOV
361
    EMail = case EMail1 of
8✔
362
              <<"">> -> EMail2;
×
UNCOV
363
              _ -> EMail1
8✔
364
            end,
UNCOV
365
    LFN = string2lower(FN),
8✔
UNCOV
366
    LFamily = string2lower(Family),
8✔
UNCOV
367
    LGiven = string2lower(Given),
8✔
UNCOV
368
    LMiddle = string2lower(Middle),
8✔
UNCOV
369
    LNickname = string2lower(Nickname),
8✔
UNCOV
370
    LBDay = string2lower(BDay),
8✔
UNCOV
371
    LCTRY = string2lower(CTRY),
8✔
UNCOV
372
    LLocality = string2lower(Locality),
8✔
UNCOV
373
    LEMail = string2lower(EMail),
8✔
UNCOV
374
    LOrgName = string2lower(OrgName),
8✔
UNCOV
375
    LOrgUnit = string2lower(OrgUnit),
8✔
UNCOV
376
    US = {LUser, LServer},
8✔
UNCOV
377
    #vcard_search{us = US,
8✔
378
                  user = {User, LServer},
379
                  luser = LUser, fn = FN,
380
                  lfn = LFN,
381
                  family = Family,
382
                  lfamily = LFamily,
383
                  given = Given,
384
                  lgiven = LGiven,
385
                  middle = Middle,
386
                  lmiddle = LMiddle,
387
                  nickname = Nickname,
388
                  lnickname = LNickname,
389
                  bday = BDay,
390
                  lbday = LBDay,
391
                  ctry = CTRY,
392
                  lctry = LCTRY,
393
                  locality = Locality,
394
                  llocality = LLocality,
395
                  email = EMail,
396
                  lemail = LEMail,
397
                  orgname = OrgName,
398
                  lorgname = LOrgName,
399
                  orgunit = OrgUnit,
400
                  lorgunit = LOrgUnit}.
401

402
-spec vcard_iq_set(iq()) -> iq() | {stop, stanza_error()}.
403
vcard_iq_set(#iq{from = #jid{user = FromUser, lserver = FromLServer},
404
                 to = #jid{user = ToUser, lserver = ToLServer},
405
                 lang = Lang})
406
  when (FromUser /= ToUser) or (FromLServer /= ToLServer) ->
407
    Txt = ?T("User not allowed to perform an IQ set on another user's vCard."),
×
408
    {stop, xmpp:err_forbidden(Txt, Lang)};
×
409
vcard_iq_set(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) ->
UNCOV
410
    #jid{user = User, lserver = LServer} = From,
8✔
UNCOV
411
    case set_vcard(User, LServer, VCard) of
8✔
412
        {error, badarg} ->
413
            %% Should not be here?
414
            Txt = ?T("Nodeprep has failed"),
×
415
            {stop, xmpp:err_internal_server_error(Txt, Lang)};
×
416
        {error, not_implemented} ->
417
            Txt = ?T("Updating the vCard is not supported by the vCard storage backend"),
×
418
            {stop, xmpp:err_feature_not_implemented(Txt, Lang)};
×
419
        ok ->
UNCOV
420
            IQ
8✔
421
    end;
422
vcard_iq_set(Acc) ->
423
    Acc.
×
424

425
-spec set_vcard(binary(), binary(), xmlel() | vcard_temp()) ->
426
    {error, badarg | not_implemented | binary()} | ok.
427
set_vcard(User, LServer, VCARD) ->
UNCOV
428
    case jid:nodeprep(User) of
8✔
429
        error ->
430
            {error, badarg};
×
431
        LUser ->
UNCOV
432
            VCardEl = xmpp:encode(VCARD),
8✔
UNCOV
433
            VCardSearch = make_vcard_search(User, LUser, LServer, VCardEl),
8✔
UNCOV
434
            Mod = gen_mod:db_mod(LServer, ?MODULE),
8✔
UNCOV
435
            case Mod:set_vcard(LUser, LServer, VCardEl, VCardSearch) of
8✔
436
                {atomic, ok} ->
UNCOV
437
                    ets_cache:delete(?VCARD_CACHE, {LUser, LServer},
8✔
438
                             cache_nodes(Mod, LServer)),
UNCOV
439
                    ok;
8✔
440
                {atomic, Error} ->
441
                    {error, Error}
×
442
            end
443
    end.
444

445
-spec string2lower(binary()) -> binary().
446
string2lower(String) ->
UNCOV
447
    case stringprep:tolower_nofilter(String) of
88✔
UNCOV
448
      Lower when is_binary(Lower) -> Lower;
88✔
449
      error -> String
×
450
    end.
451

452
-spec mk_tfield(binary(), binary(), binary()) -> xdata_field().
453
mk_tfield(Label, Var, Lang) ->
454
    #xdata_field{type = 'text-single',
×
455
                 label = translate:translate(Lang, Label),
456
                 var = Var}.
457

458
-spec mk_field(binary(), binary()) -> xdata_field().
459
mk_field(Var, Val) ->
460
    #xdata_field{var = Var, values = [Val]}.
×
461

462
-spec mk_search_form(jid(), binary(), binary()) -> search().
463
mk_search_form(JID, ServerHost, Lang) ->
464
    Title = <<(translate:translate(Lang, ?T("Search users in ")))/binary,
×
465
              (jid:encode(JID))/binary>>,
466
    Mod = gen_mod:db_mod(ServerHost, ?MODULE),
×
467
    SearchFields = Mod:search_fields(ServerHost),
×
468
    Fs = [mk_tfield(Label, Var, Lang) || {Label, Var} <- SearchFields],
×
469
    X = #xdata{type = form,
×
470
               title = Title,
471
               instructions = [make_instructions(Mod, Lang)],
472
               fields = Fs},
473
    #search{instructions =
×
474
                translate:translate(
475
                  Lang, ?T("You need an x:data capable client to search")),
476
            xdata = X}.
477

478
-spec make_instructions(module(), binary()) -> binary().
479
make_instructions(Mod, Lang) ->
480
    Fill = translate:translate(
×
481
             Lang,
482
             ?T("Fill in the form to search for any matching "
483
                "XMPP User")),
484
    Add = translate:translate(
×
485
            Lang,
486
            ?T(" (Add * to the end of field to match substring)")),
487
    case Mod of
×
488
        mod_vcard_mnesia -> Fill;
×
489
        _ -> str:concat(Fill, Add)
×
490
    end.
491

492
-spec search_result(binary(), jid(), binary(), [xdata_field()]) -> xdata().
493
search_result(Lang, JID, ServerHost, XFields) ->
494
    Mod = gen_mod:db_mod(ServerHost, ?MODULE),
×
495
    Reported = [mk_tfield(Label, Var, Lang) ||
×
496
                   {Label, Var} <- Mod:search_reported(ServerHost)],
×
497
    #xdata{type = result,
×
498
           title = <<(translate:translate(Lang,
499
                                          ?T("Search Results for ")))/binary,
500
                     (jid:encode(JID))/binary>>,
501
           reported = Reported,
502
           items = lists:map(fun (Item) -> item_to_field(Item) end,
×
503
                             search(ServerHost, XFields))}.
504

505
-spec item_to_field([{binary(), binary()}]) -> [xdata_field()].
506
item_to_field(Items) ->
507
    [mk_field(Var, Value) || {Var, Value} <- Items].
×
508

509
-spec search(binary(), [xdata_field()]) -> [binary()].
510
search(LServer, XFields) ->
511
    Data = [{Var, Vals} || #xdata_field{var = Var, values = Vals} <- XFields],
×
512
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
513
    AllowReturnAll = mod_vcard_opt:allow_return_all(LServer),
×
514
    MaxMatch = mod_vcard_opt:matches(LServer),
×
515
    Mod:search(LServer, Data, AllowReturnAll, MaxMatch).
×
516

517
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
518
-spec remove_user(binary(), binary()) -> ok.
519
remove_user(User, Server) ->
UNCOV
520
    LUser = jid:nodeprep(User),
16✔
UNCOV
521
    LServer = jid:nameprep(Server),
16✔
UNCOV
522
    Mod = gen_mod:db_mod(LServer, ?MODULE),
16✔
UNCOV
523
    Mod:remove_user(LUser, LServer),
16✔
UNCOV
524
    ets_cache:delete(?VCARD_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)).
16✔
525

526
-spec init_cache(module(), binary(), gen_mod:opts()) -> ok.
527
init_cache(Mod, Host, Opts) ->
528
    case use_cache(Mod, Host) of
9✔
529
        true ->
530
            CacheOpts = cache_opts(Host, Opts),
9✔
531
            ets_cache:new(?VCARD_CACHE, CacheOpts);
9✔
532
        false ->
533
            ets_cache:delete(?VCARD_CACHE)
×
534
    end.
535

536
-spec cache_opts(binary(), gen_mod:opts()) -> [proplists:property()].
537
cache_opts(_Host, Opts) ->
538
    MaxSize = mod_vcard_opt:cache_size(Opts),
9✔
539
    CacheMissed = mod_vcard_opt:cache_missed(Opts),
9✔
540
    LifeTime = mod_vcard_opt:cache_life_time(Opts),
9✔
541
    [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
9✔
542

543
-spec use_cache(module(), binary()) -> boolean().
544
use_cache(Mod, Host) ->
545
    case erlang:function_exported(Mod, use_cache, 1) of
50✔
546
        true -> Mod:use_cache(Host);
×
547
        false -> mod_vcard_opt:use_cache(Host)
50✔
548
    end.
549

550
-spec cache_nodes(module(), binary()) -> [node()].
551
cache_nodes(Mod, Host) ->
UNCOV
552
    case erlang:function_exported(Mod, cache_nodes, 1) of
24✔
553
        true -> Mod:cache_nodes(Host);
×
UNCOV
554
        false -> ejabberd_cluster:get_nodes()
24✔
555
    end.
556

557
import_info() ->
558
    [{<<"vcard">>, 3}, {<<"vcard_search">>, 24}].
×
559

560
import_start(LServer, DBType) ->
561
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
562
    Mod:init(LServer, []).
×
563

564
import(LServer, {sql, _}, DBType, Tab, L) ->
565
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
566
    Mod:import(LServer, Tab, L).
×
567

568
export(LServer) ->
569
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
570
    Mod:export(LServer).
×
571

572
%%%
573
%%% WebAdmin
574
%%%
575

576
webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) ->
UNCOV
577
    Acc ++ [{<<"vcard">>, <<"vCard">>}].
24✔
578

579
webadmin_page_hostuser(_, Host, User,
580
              #request{path = [<<"vcard">>]} = R) ->
581
    Head = ?H1GL(<<"vCard">>, <<"modules/#mod_vcard">>, <<"mod_vcard">>),
×
582
    Set = [make_command(set_nickname, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
×
583
           make_command(set_vcard, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
584
           make_command(set_vcard2, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
585
           make_command(set_vcard2_multi, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
586
    timer:sleep(100), % setting vcard takes a while, let's delay the get commands
×
587
    FieldNames = [<<"VERSION">>, <<"FN">>, <<"NICKNAME">>, <<"BDAY">>],
×
588
    FieldNames2 =
×
589
        [{<<"N">>, <<"FAMILY">>},
590
         {<<"N">>, <<"GIVEN">>},
591
         {<<"N">>, <<"MIDDLE">>},
592
         {<<"ADR">>, <<"CTRY">>},
593
         {<<"ADR">>, <<"LOCALITY">>},
594
         {<<"EMAIL">>, <<"USERID">>}],
595
    Get = [make_command(get_vcard, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
×
596
           ?XE(<<"blockquote">>,
597
               [make_table([<<"name">>, <<"value">>],
598
                           [{?C(FieldName),
×
599
                             make_command(get_vcard,
600
                                          R,
601
                                          [{<<"user">>, User},
602
                                           {<<"host">>, Host},
603
                                           {<<"name">>, FieldName}],
604
                                          [{only, value}])}
605
                            || FieldName <- FieldNames])]),
×
606
           make_command(get_vcard2, R, [{<<"user">>, User}, {<<"host">>, Host}], []),
607
           ?XE(<<"blockquote">>,
608
               [make_table([<<"name">>, <<"subname">>, <<"value">>],
609
                           [{?C(FieldName),
×
610
                             ?C(FieldSubName),
611
                             make_command(get_vcard2,
612
                                          R,
613
                                          [{<<"user">>, User},
614
                                           {<<"host">>, Host},
615
                                           {<<"name">>, FieldName},
616
                                           {<<"subname">>, FieldSubName}],
617
                                          [{only, value}])}
618
                            || {FieldName, FieldSubName} <- FieldNames2])]),
×
619
           make_command(get_vcard2_multi, R, [{<<"user">>, User}, {<<"host">>, Host}], [])],
620
    {stop, Head ++ Get ++ Set};
×
621
webadmin_page_hostuser(Acc, _, _, _) -> Acc.
×
622

623
%%%
624
%%% Documentation
625
%%%
626

627
depends(_Host, _Opts) ->
628
    [].
9✔
629

630
mod_opt_type(allow_return_all) ->
631
    econf:bool();
9✔
632
mod_opt_type(name) ->
633
    econf:binary();
9✔
634
mod_opt_type(matches) ->
635
    econf:pos_int(infinity);
9✔
636
mod_opt_type(search) ->
637
    econf:bool();
9✔
638
mod_opt_type(host) ->
639
    econf:host();
9✔
640
mod_opt_type(hosts) ->
641
    econf:hosts();
9✔
642
mod_opt_type(db_type) ->
643
    econf:db_type(?MODULE);
9✔
644
mod_opt_type(use_cache) ->
645
    econf:bool();
9✔
646
mod_opt_type(cache_size) ->
647
    econf:pos_int(infinity);
9✔
648
mod_opt_type(cache_missed) ->
649
    econf:bool();
9✔
650
mod_opt_type(cache_life_time) ->
651
    econf:timeout(second, infinity);
9✔
652
mod_opt_type(vcard) ->
653
    econf:vcard_temp().
9✔
654

655
mod_options(Host) ->
656
    [{allow_return_all, false},
9✔
657
     {host, <<"vjud.", Host/binary>>},
658
     {hosts, []},
659
     {matches, 30},
660
     {search, false},
661
     {name, ?T("vCard User Search")},
662
     {vcard, undefined},
663
     {db_type, ejabberd_config:default_db(Host, ?MODULE)},
664
     {use_cache, ejabberd_option:use_cache(Host)},
665
     {cache_size, ejabberd_option:cache_size(Host)},
666
     {cache_missed, ejabberd_option:cache_missed(Host)},
667
     {cache_life_time, ejabberd_option:cache_life_time(Host)}].
668

669
mod_doc() ->
670
    #{desc =>
×
671
          ?T("This module allows end users to store and retrieve "
672
             "their vCard, and to retrieve other users vCards, "
673
             "as defined in https://xmpp.org/extensions/xep-0054.html"
674
             "[XEP-0054: vcard-temp]. The module also implements an "
675
             "uncomplicated Jabber User Directory based on the vCards "
676
             "of these users. Moreover, it enables the server to send "
677
             "its vCard when queried."),
678
      opts =>
679
          [{allow_return_all,
680
            #{value => "true | false",
681
              desc =>
682
                  ?T("This option enables you to specify if search "
683
                     "operations with empty input fields should return "
684
                     "all users who added some information to their vCard. "
685
                     "The default value is 'false'.")}},
686
           {host,
687
            #{desc => ?T("Deprecated. Use 'hosts' instead.")}},
688
           {hosts,
689
            #{value => ?T("[Host, ...]"),
690
              desc =>
691
                  ?T("This option defines the Jabber IDs of the service. "
692
                     "If the 'hosts' option is not specified, the only Jabber ID will "
693
                     "be the hostname of the virtual host with the prefix \"vjud.\". "
694
                     "The keyword '@HOST@' is replaced with the real virtual host name.")}},
695
           {name,
696
            #{value => ?T("Name"),
697
              desc =>
698
                  ?T("The value of the service name. This name is only visible in some "
699
                     "clients that support https://xmpp.org/extensions/xep-0030.html"
700
                     "[XEP-0030: Service Discovery]. The default is 'vCard User Search'.")}},
701
           {matches,
702
            #{value => "pos_integer() | infinity",
703
              desc =>
704
                  ?T("With this option, the number of reported search results "
705
                     "can be limited. If the option's value is set to 'infinity', "
706
                     "all search results are reported. The default value is '30'.")}},
707
           {search,
708
            #{value => "true | false",
709
              desc =>
710
                  ?T("This option specifies whether the search functionality "
711
                     "is enabled or not. If disabled, the options 'hosts', 'name' "
712
                     "and 'vcard' will be ignored and the Jabber User Directory "
713
                     "service will not appear in the Service Discovery item list. "
714
                     "The default value is 'false'.")}},
715
           {db_type,
716
            #{value => "mnesia | sql | ldap",
717
              desc =>
718
                  ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
719
           {use_cache,
720
            #{value => "true | false",
721
              desc =>
722
                  ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
723
           {cache_size,
724
            #{value => "pos_integer() | infinity",
725
              desc =>
726
                  ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
727
           {cache_missed,
728
            #{value => "true | false",
729
              desc =>
730
                  ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
731
           {cache_life_time,
732
            #{value => "timeout()",
733
              desc =>
734
                  ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}},
735
           {vcard,
736
            #{value => ?T("vCard"),
737
              desc =>
738
                  ?T("A custom vCard of the server that will be displayed "
739
                     "by some XMPP clients in Service Discovery. The value of "
740
                     "'vCard' is a YAML map constructed from an XML representation "
741
                     "of vCard. Since the representation has no attributes, "
742
                     "the mapping is straightforward."),
743
              example =>
744
                  ["# This XML representation of vCard:",
745
                   "# ",
746
                   "#   <vCard xmlns='vcard-temp'>",
747
                   "#     <FN>Conferences</FN>",
748
                   "#     <ADR>",
749
                   "#       <WORK/>",
750
                   "#       <STREET>Elm Street</STREET>",
751
                   "#     </ADR>",
752
                   "#   </vCard>",
753
                   "# ",
754
                   "# is translated to:",
755
                   "# ",
756
                   "vcard:",
757
                   "  fn: Conferences",
758
                   "  adr:",
759
                   "    -",
760
                   "      work: true",
761
                   "      street: Elm Street"]}}]}.
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