./ct_report/coverage/mod_vcard.COVER.html

1 %%%----------------------------------------------------------------------
2 %%% File : mod_vcard.erl
3 %%% Author : Alexey Shchepin <alexey@process-one.net>
4 %%% Purpose : Vcard management in Mnesia
5 %%% Created : 2 Jan 2003 by Alexey Shchepin <alexey@process-one.net>
6 %%%
7 %%% Store vCards in mnesia to provide "XEP-0054: vcard-temp"
8 %%% and "XEP-0055: Jabber Search"
9 %%%
10 %%% Most of this is now using binaries. The search fields l* in vcard_search
11 %%% are still stored as lists to allow string prefix search using the match
12 %%% spec with a trailing element String ++ '_'.
13 %%%
14 %%%----------------------------------------------------------------------
15 %%% ejabberd, Copyright (C) 2002-2011 ProcessOne
16 %%%
17 %%% This program is free software; you can redistribute it and/or
18 %%% modify it under the terms of the GNU General Public License as
19 %%% published by the Free Software Foundation; either version 2 of the
20 %%% License, or (at your option) any later version.
21 %%%
22 %%% This program is distributed in the hope that it will be useful,
23 %%% but WITHOUT ANY WARRANTY; without even the implied warranty of
24 %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
25 %%% General Public License for more details.
26 %%%
27 %%% You should have received a copy of the GNU General Public License
28 %%% along with this program; if not, write to the Free Software
29 %%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
30 %%%
31 %%%----------------------------------------------------------------------
32
33 -module(mod_vcard).
34 -author('alexey@process-one.net').
35 -xep([{xep, 54}, {version, "1.2"}]).
36 -xep([{xep, 55}, {version, "1.3"}]).
37 -behaviour(gen_mod).
38 -behaviour(gen_server).
39 -behaviour(mongoose_module_metrics).
40
41 -include("mongoose.hrl").
42 -include("jlib.hrl").
43 -include("mod_vcard.hrl").
44 -include("mongoose_rsm.hrl").
45 -include("mongoose_config_spec.hrl").
46
47 %% gen_mod handlers
48 -export([start/2, stop/1,
49 hooks/1,
50 supported_features/0]).
51
52 %% config_spec
53 -export([config_spec/0,
54 process_map_spec/1,
55 process_search_spec/1,
56 process_search_reported_spec/1]).
57
58 %% gen_server handlers
59 -export([init/1,
60 handle_info/2,
61 handle_call/3,
62 handle_cast/2,
63 terminate/2,
64 code_change/3]).
65
66 %% mongoose_packet_handler export
67 -export([process_packet/5]).
68
69 %% Hook handlers
70 -export([process_local_iq/5,
71 process_sm_iq/5,
72 remove_user/3,
73 remove_domain/3,
74 set_vcard/3]).
75
76 -export([start_link/2]).
77 -export([default_search_fields/0]).
78 -export([get_results_limit/1]).
79 -export([get_default_reported_fields/1]).
80 -export([unsafe_set_vcard/3]).
81
82 %% GDPR related
83 -export([get_personal_data/3]).
84
85 -export([config_metrics/1]).
86
87 -ignore_xref([
88 process_packet/5,
89 start_link/2
90 ]).
91
92 -define(PROCNAME, ejabberd_mod_vcard).
93
94 -record(state, {search :: boolean(),
95 host_type :: mongooseim:host_type()}).
96
97 %%--------------------------------------------------------------------
98 %% gdpr callback
99 %%--------------------------------------------------------------------
100
101 -spec get_personal_data(Acc, Params, Extra) -> {ok, Acc} when
102 Acc :: gdpr:personal_data(),
103 Params :: #{jid := jid:jid()},
104 Extra :: gen_hook:extra().
105 get_personal_data(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, #{host_type := HostType}) ->
106 87 Jid = jid:to_binary({LUser, LServer}),
107 87 Schema = ["jid", "vcard"],
108 87 Entries = case mod_vcard_backend:get_vcard(HostType, LUser, LServer) of
109 {ok, Record} ->
110 1 SerializedRecords = exml:to_binary(Record),
111 1 [{Jid, SerializedRecords}];
112 86 _ -> []
113 end,
114 87 {ok, [{vcard, Schema, Entries} | Acc]}.
115
116 -spec default_search_fields() -> list().
117 default_search_fields() ->
118 2 [{<<"User">>, <<"user">>},
119 {<<"Full Name">>, <<"fn">>},
120 {<<"Given Name">>, <<"first">>},
121 {<<"Middle Name">>, <<"middle">>},
122 {<<"Family Name">>, <<"last">>},
123 {<<"Nickname">>, <<"nick">>},
124 {<<"Birthday">>, <<"bday">>},
125 {<<"Country">>, <<"ctry">>},
126 {<<"City">>, <<"locality">>},
127 {<<"Email">>, <<"email">>},
128 {<<"Organization Name">>, <<"orgname">>},
129 {<<"Organization Unit">>, <<"orgunit">>}].
130
131 -spec get_results_limit(mongooseim:host_type()) -> non_neg_integer() | infinity.
132 get_results_limit(HostType) ->
133 21 case gen_mod:get_module_opt(HostType, mod_vcard, matches) of
134 infinity ->
135
:-(
infinity;
136 Val when is_integer(Val) and (Val > 0) ->
137 21 Val
138 end.
139
140 %%--------------------------------------------------------------------
141 %% gen_mod callbacks
142 %%--------------------------------------------------------------------
143
144 start(HostType, Opts) ->
145 224 mod_vcard_backend:init(HostType, Opts),
146 224 start_iq_handlers(HostType, Opts),
147 224 Proc = gen_mod:get_module_proc(HostType, ?PROCNAME),
148 224 ChildSpec = {Proc, {?MODULE, start_link, [HostType, Opts]},
149 transient, 1000, worker, [?MODULE]},
150 224 ejabberd_sup:start_child(ChildSpec).
151
152 -spec stop(mongooseim:host_type()) -> ok.
153 stop(HostType) ->
154 224 Proc = gen_mod:get_module_proc(HostType, ?PROCNAME),
155 224 stop_iq_handlers(HostType),
156 224 stop_backend(HostType),
157 224 gen_server:call(Proc, stop),
158 224 ejabberd_sup:stop_child(Proc),
159 224 ok.
160
161 100 supported_features() -> [dynamic_domains].
162
163 -spec hooks(mongooseim:host_type()) -> gen_hook:hook_list().
164 hooks(HostType) ->
165 448 [{remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 50},
166 {anonymous_purge_hook, HostType, fun ?MODULE:remove_user/3, #{}, 50},
167 {remove_domain, HostType, fun ?MODULE:remove_domain/3, #{}, 50},
168 {set_vcard, HostType, fun ?MODULE:set_vcard/3, #{}, 50},
169 {get_personal_data, HostType, fun ?MODULE:get_personal_data/3, #{}, 50}].
170
171 start_iq_handlers(HostType, #{iqdisc := IQDisc}) ->
172 224 gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_VCARD, ejabberd_sm,
173 fun ?MODULE:process_sm_iq/5, #{}, IQDisc),
174 224 gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_VCARD, ejabberd_local,
175 fun ?MODULE:process_local_iq/5, #{}, IQDisc).
176
177 stop_iq_handlers(HostType) ->
178 224 gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_VCARD, ejabberd_local),
179 224 gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_VCARD, ejabberd_sm).
180
181 stop_backend(HostType) ->
182 224 mod_vcard_backend:tear_down(HostType).
183
184 %% Domain registration
185 maybe_register_search(false, _HostType, _Opts) ->
186 2 ok;
187 maybe_register_search(true, HostType, Opts) ->
188 222 SubdomainPattern = gen_mod:get_opt(host, Opts),
189 222 PacketHandler = mongoose_packet_handler:new(?MODULE, #{pid => self()}),
190 %% Always register, even if search functionality is disabled.
191 %% So, we can send 503 error, instead of 404 error.
192 222 mongoose_domain_api:register_subdomain(HostType, SubdomainPattern, PacketHandler).
193
194 maybe_unregister_search(false, _HostType) ->
195 2 ok;
196 maybe_unregister_search(true, HostType) ->
197 224 SubdomainPattern = gen_mod:get_module_opt(HostType, ?MODULE, host),
198 224 mongoose_domain_api:unregister_subdomain(HostType, SubdomainPattern).
199
200 %%--------------------------------------------------------------------
201 %% config_spec
202 %%--------------------------------------------------------------------
203
204 -spec config_spec() -> mongoose_config_spec:config_section().
205 config_spec() ->
206 106 #section{
207 items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc(),
208 <<"host">> => #option{type = string,
209 validate = subdomain_template,
210 process = fun mongoose_subdomain_utils:make_subdomain_pattern/1},
211 <<"search">> => #option{type = boolean},
212 <<"backend">> => #option{type = atom,
213 validate = {module, mod_vcard}},
214 <<"matches">> => #option{type = int_or_infinity,
215 validate = non_negative},
216 <<"ldap">> => ldap_section()
217 },
218 defaults = #{<<"iqdisc">> => parallel,
219 <<"host">> => mongoose_subdomain_utils:make_subdomain_pattern("vjud.@HOST@"),
220 <<"search">> => true,
221 <<"backend">> => mnesia,
222 <<"matches">> => 30
223 },
224 process = fun remove_unused_backend_opts/1
225 }.
226
227 ldap_section() ->
228 106 CommonLDAPSpec = mongoose_ldap_config:spec(),
229 106 Items = #{
230 <<"uids">> => #list{items = mongoose_ldap_config:uids()},
231 <<"vcard_map">> => #list{items = ldap_vcard_map_spec()},
232 <<"search_fields">> => #list{items = ldap_search_fields_spec()},
233 <<"search_reported">> => #list{items = ldap_search_reported_spec()},
234 <<"search_operator">> => #option{type = atom,
235 validate = {enum, ['or', 'and']}},
236 <<"binary_search_fields">> => #list{items = #option{type = binary,
237 validate = non_empty}}},
238 106 Defaults = #{<<"uids">> => [{<<"uid">>, <<"%u">>}],
239 <<"vcard_map">> => mod_vcard_ldap:default_vcard_map(),
240 <<"search_fields">> => mod_vcard_ldap:default_search_fields(),
241 <<"search_reported">> => mod_vcard_ldap:default_search_reported(),
242 <<"search_operator">> => 'and',
243 <<"binary_search_fields">> => []},
244 106 CommonLDAPSpec#section{items = maps:merge(CommonLDAPSpec#section.items, Items),
245 defaults = maps:merge(CommonLDAPSpec#section.defaults, Defaults),
246 include = always}.
247
248 ldap_vcard_map_spec() ->
249 106 #section{
250 items = #{<<"vcard_field">> => #option{type = binary,
251 validate = non_empty},
252 <<"ldap_pattern">> => #option{type = binary,
253 validate = non_empty},
254 <<"ldap_field">> => #option{type = binary,
255 validate = non_empty}
256 },
257 required = all,
258 process = fun ?MODULE:process_map_spec/1
259 }.
260
261 ldap_search_fields_spec() ->
262 106 #section{
263 items = #{<<"search_field">> => #option{type = binary,
264 validate = non_empty},
265 <<"ldap_field">> => #option{type = binary,
266 validate = non_empty}
267 },
268 required = all,
269 process = fun ?MODULE:process_search_spec/1
270 }.
271
272 ldap_search_reported_spec() ->
273 106 #section{
274 items = #{<<"search_field">> => #option{type = binary,
275 validate = non_empty},
276 <<"vcard_field">> => #option{type = binary,
277 validate = non_empty}
278 },
279 required = all,
280 process = fun ?MODULE:process_search_reported_spec/1
281 }.
282
283 process_map_spec(#{vcard_field := VF, ldap_pattern := LP, ldap_field := LF}) ->
284
:-(
{VF, LP, [LF]}.
285
286 process_search_spec(#{search_field := SF, ldap_field := LF}) ->
287
:-(
{SF, LF}.
288
289 process_search_reported_spec(#{search_field := SF, vcard_field := VF}) ->
290
:-(
{SF, VF}.
291
292
:-(
remove_unused_backend_opts(Opts = #{backend := ldap}) -> Opts;
293 53 remove_unused_backend_opts(Opts) -> maps:remove(ldap, Opts).
294
295 %%--------------------------------------------------------------------
296 %% mongoose_packet_handler callbacks for search
297 %%--------------------------------------------------------------------
298
299 -spec process_packet(Acc :: mongoose_acc:t(), From ::jid:jid(), To ::jid:jid(),
300 Packet :: exml:element(), #{}) -> mongoose_acc:t().
301 process_packet(Acc, From, To, _Packet, _Extra) ->
302 28 handle_route(Acc, From, To),
303 28 Acc.
304
305 handle_route(Acc, From, To) ->
306 28 HostType = mongoose_acc:host_type(Acc),
307 28 {IQ, Acc1} = mongoose_iq:info(Acc),
308 28 LServer = directory_jid_to_server_host(To),
309 28 try do_route(HostType, LServer, From, To, Acc1, IQ)
310 catch
311 Class:Reason:Stacktrace ->
312
:-(
?LOG_ERROR(#{what => vcard_route_failed, acc => Acc,
313
:-(
class => Class, reason => Reason, stacktrace => Stacktrace})
314 end.
315
316 %%--------------------------------------------------------------------
317 %% gen_server callbacks for search
318 %%--------------------------------------------------------------------
319 start_link(HostType, Opts) ->
320 224 Proc = gen_mod:get_module_proc(HostType, ?PROCNAME),
321 224 gen_server:start_link({local, Proc}, ?MODULE, [HostType, Opts], []).
322
323 init([HostType, Opts]) ->
324 224 process_flag(trap_exit, true),
325 224 Search = gen_mod:get_opt(search, Opts),
326 224 maybe_register_search(Search, HostType, Opts),
327 224 {ok, #state{host_type = HostType, search = Search}}.
328
329 handle_call(get_state, _From, State) ->
330
:-(
{reply, {ok, State}, State};
331 handle_call(stop, _From, State) ->
332 224 {stop, normal, ok, State};
333 handle_call(_Request, _From, State) ->
334
:-(
{reply, bad_request, State}.
335
336 handle_info(_, State) ->
337
:-(
{noreply, State}.
338
339 handle_cast(_Request, State) ->
340
:-(
{noreply, State}.
341
342 code_change(_OldVsn, State, _Extra) ->
343
:-(
{ok, State}.
344
345 terminate(_Reason, #state{host_type = HostType, search = Search}) ->
346 226 maybe_unregister_search(Search, HostType).
347
348 %%--------------------------------------------------------------------
349 %% Hook handlers
350 %%--------------------------------------------------------------------
351 process_local_iq(Acc, _From, _To, IQ = #iq{type = set, sub_el = SubEl}, _Extra) ->
352
:-(
{Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:not_allowed()]}};
353 process_local_iq(Acc, _From, _To, IQ = #iq{type = get}, _Extra) ->
354 1 DescCData = #xmlcdata{content = [<<"MongooseIM XMPP Server">>,
355 <<"\nCopyright (c) Erlang Solutions Ltd.">>]},
356 1 {Acc, IQ#iq{type = result,
357 sub_el = [#xmlel{name = <<"vCard">>, attrs = [{<<"xmlns">>, ?NS_VCARD}],
358 children = [#xmlel{name = <<"FN">>,
359 children = [#xmlcdata{content = <<"MongooseIM">>}]},
360 #xmlel{name = <<"URL">>,
361 children = [#xmlcdata{content = ?MONGOOSE_URI}]},
362 #xmlel{name = <<"DESC">>,
363 children = [DescCData]}
364 ]}]}}.
365
366 -spec process_sm_iq(Acc :: mongoose_acc:t(),
367 From :: jid:jid(),
368 To :: jid:jid(),
369 IQ :: jlib:iq(),
370 Extra :: map()) ->
371 {stop, mongoose_acc:t()} | {mongoose_acc:t(), jlib:iq()}.
372 process_sm_iq(Acc, From, To, IQ = #iq{type = set, sub_el = VCARD}, _Extra) ->
373 31 HostType = mongoose_acc:host_type(Acc),
374 31 process_sm_iq_set(HostType, From, To, Acc, IQ, VCARD);
375 process_sm_iq(Acc, From, To, IQ = #iq{type = get, sub_el = VCARD}, _Extra) ->
376 19 HostType = mongoose_acc:host_type(Acc),
377 19 process_sm_iq_get(HostType, From, To, Acc, IQ, VCARD).
378
379 process_sm_iq_set(HostType, From, To, Acc, IQ, VCARD) ->
380 31 #jid{luser = FromUser, lserver = FromVHost} = From,
381 31 #jid{luser = ToUser, lserver = ToVHost, lresource = ToResource} = To,
382 31 Local = ((FromUser == ToUser) andalso (FromVHost == ToVHost) andalso (ToResource == <<>>))
383 2 orelse ((ToUser == <<>>) andalso (ToVHost == <<>>)),
384 31 Res = case Local of
385 true ->
386 29 try unsafe_set_vcard(HostType, From, VCARD) of
387 ok ->
388 27 IQ#iq{type = result, sub_el = []};
389 {error, {invalid_input, {Field, Value}}} ->
390 2 ?LOG_WARNING(#{what => vcard_sm_iq_set_failed, value => Value,
391
:-(
reason => invalid_input, field => Field, acc => Acc}),
392 2 Text = io_lib:format("Invalid input for vcard field ~s", [Field]),
393 2 ReasonEl = mongoose_xmpp_errors:bad_request(<<"en">>, erlang:iolist_to_binary(Text)),
394 2 vcard_error(IQ, ReasonEl);
395 {error, Reason} ->
396
:-(
?LOG_WARNING(#{what => vcard_sm_iq_set_failed,
397
:-(
reason => Reason, acc => Acc}),
398
:-(
vcard_error(IQ, mongoose_xmpp_errors:unexpected_request_cancel())
399 catch
400 E:R:Stack ->
401
:-(
?LOG_ERROR(#{what => vcard_sm_iq_set_failed,
402
:-(
class => E, reason => R, stacktrace => Stack, acc => Acc}),
403
:-(
vcard_error(IQ, mongoose_xmpp_errors:internal_server_error())
404 end;
405 _ ->
406 2 ?LOG_WARNING(#{what => vcard_sm_iq_get_failed,
407
:-(
reason => not_allowed, acc => Acc}),
408 2 vcard_error(IQ, mongoose_xmpp_errors:not_allowed())
409 end,
410 31 {Acc, Res}.
411
412 process_sm_iq_get(HostType, _From, To, Acc, IQ, SubEl) ->
413 19 #jid{luser = LUser, lserver = LServer} = To,
414 19 Res = try mod_vcard_backend:get_vcard(HostType, LUser, LServer) of
415 {ok, VCARD} ->
416 16 IQ#iq{type = result, sub_el = VCARD};
417 {error, Reason} ->
418 3 IQ#iq{type = error, sub_el = [SubEl, Reason]}
419 catch E:R:Stack ->
420
:-(
?LOG_ERROR(#{what => vcard_sm_iq_get_failed,
421
:-(
class => E, reason => R, stacktrace => Stack, acc => Acc}),
422
:-(
vcard_error(IQ, mongoose_xmpp_errors:internal_server_error())
423 end,
424 19 {Acc, Res}.
425
426 unsafe_set_vcard(HostType, From, VCARD) ->
427 37 #jid{luser = FromUser, lserver = FromVHost} = From,
428 37 case parse_vcard(FromUser, FromVHost, VCARD) of
429 {ok, VcardSearch} ->
430 35 mod_vcard_backend:set_vcard(HostType, FromUser, FromVHost, VCARD, VcardSearch);
431 {error, Reason} ->
432 2 {error, Reason}
433 end.
434
435
436 -spec set_vcard(Acc, Params, Extra) -> {ok, Acc} when
437 Acc :: ok | {error, term()},
438 Params :: map(),
439 Extra :: gen_hook:extra().
440 set_vcard(ok, _Params, _Extra) ->
441
:-(
?LOG_DEBUG(#{what => hook_call_already_handled}),
442
:-(
{ok, ok};
443 set_vcard({error, no_handler_defined}, #{user := From, vcard := VCARD}, #{host_type := HostType}) ->
444 1 Result = try unsafe_set_vcard(HostType, From, VCARD) of
445 1 ok -> ok;
446 {error, Reason} ->
447
:-(
?LOG_ERROR(#{what => unsafe_set_vcard_failed, reason => Reason}),
448
:-(
{error, Reason}
449 catch
450
:-(
E:R:S -> ?LOG_ERROR(#{what => unsafe_set_vcard_failed, class => E,
451
:-(
reason => R, stacktrace => S}),
452
:-(
{error, {E, R}}
453 end,
454 1 {ok, Result};
455 set_vcard({error, _} = Error, _Params, _Extra) ->
456
:-(
{ok, Error}.
457
458 -spec remove_domain(Acc, Params, Extra) -> {ok, Acc} when
459 Acc :: mongoose_domain_api:remove_domain_acc(),
460 Params :: map(),
461 Extra :: gen_hook:extra().
462 remove_domain(Acc, #{domain := Domain}, #{host_type := HostType}) ->
463 18 mod_vcard_backend:remove_domain(HostType, Domain),
464 18 {ok, Acc}.
465
466 -spec remove_user(Acc, Params, Extra) -> {ok, Acc} when
467 Acc :: mongoose_acc:t(),
468 Params :: map(),
469 Extra :: gen_hook:extra().
470 remove_user(Acc, #{jid := #jid{luser = User, lserver = Server}}, #{host_type := HostType}) ->
471 6128 LUser = jid:nodeprep(User),
472 6128 LServer = jid:nodeprep(Server),
473 6128 mod_vcard_backend:remove_user(HostType, LUser, LServer),
474 6128 {ok, Acc}.
475
476 %% ------------------------------------------------------------------
477 %% Internal
478 %% ------------------------------------------------------------------
479 do_route(_HostType, _LServer, From,
480 #jid{luser = LUser, lresource = LResource} = To, Acc, _IQ)
481 when (LUser /= <<>>) or (LResource /= <<>>) ->
482
:-(
{Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:service_unavailable()),
483
:-(
ejabberd_router:route(To, From, Acc1, Err);
484 do_route(HostType, LServer, From, To, Acc,
485 #iq{type = set, xmlns = ?NS_SEARCH, lang = Lang, sub_el = SubEl} = IQ) ->
486 24 route_search_iq_set(HostType, LServer, From, To, Acc, Lang, SubEl, IQ);
487 do_route(HostType, LServer, From, To, Acc,
488 #iq{type = get, xmlns = ?NS_SEARCH, lang = Lang} = IQ) ->
489 2 Instr = search_instructions(Lang),
490 2 Form = search_form(To, mod_vcard_backend:search_fields(HostType, LServer), Lang),
491 2 ResIQ = make_search_form_result_iq(IQ, [Instr, Form]),
492 2 ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ));
493 do_route(_HostType, _LServer, From, To, Acc,
494 #iq{type = set, xmlns = ?NS_DISCO_INFO}) ->
495
:-(
{Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:not_allowed()),
496
:-(
ejabberd_router:route(To, From, Acc1, Err);
497 do_route(HostType, _LServer, From, To, Acc,
498 #iq{type = get, xmlns = ?NS_DISCO_INFO, lang = Lang} = IQ) ->
499 1 IdentityXML = mongoose_disco:identities_to_xml([identity(Lang)]),
500 1 FeatureXML = mongoose_disco:features_to_xml(features()),
501 1 InfoXML = mongoose_disco:get_info(HostType, ?MODULE, <<>>, <<>>),
502 1 ResIQ = IQ#iq{type = result,
503 sub_el = [#xmlel{name = <<"query">>,
504 attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}],
505 children = IdentityXML ++ FeatureXML ++ InfoXML}]},
506 1 ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ));
507 do_route(_HostType, _LServer, From, To, Acc,
508 #iq{type = set, xmlns = ?NS_DISCO_ITEMS}) ->
509
:-(
{Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:not_allowed()),
510
:-(
ejabberd_router:route(To, From, Acc1, Err);
511 do_route(_HostType, _LServer, From, To, Acc,
512 #iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ) ->
513
:-(
ResIQ =
514 IQ#iq{type = result,
515 sub_el = [#xmlel{name = <<"query">>,
516 attrs = [{<<"xmlns">>, ?NS_DISCO_ITEMS}]}]},
517
:-(
ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ));
518 do_route(_HostType, _LServer, From, To, Acc,
519 #iq{type = get, xmlns = ?NS_VCARD} = IQ) ->
520 1 ResIQ =
521 IQ#iq{type = result,
522 sub_el = [#xmlel{name = <<"vCard">>,
523 attrs = [{<<"xmlns">>, ?NS_VCARD}],
524 children = iq_get_vcard()}]},
525 1 ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ));
526 do_route(_HostType, _LServer, From, To, Acc, _IQ) ->
527
:-(
{Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:service_unavailable()),
528
:-(
ejabberd_router:route(To, From, Acc1, Err).
529
530 make_search_form_result_iq(IQ, Elements) ->
531 2 IQ#iq{type = result,
532 sub_el = [#xmlel{name = <<"query">>,
533 attrs = [{<<"xmlns">>, ?NS_SEARCH}],
534 children = Elements
535 }]}.
536
537 search_instructions(Lang) ->
538 2 Text = translate:translate(Lang, <<"You need an x:data capable client to search">>),
539 2 #xmlel{name = <<"instructions">>, attrs = [], children = [#xmlcdata{content = Text}]}.
540
541 search_form(JID, SearchFields, Lang) ->
542 2 Title = <<(translate:translate(Lang, <<"Search users in ">>))/binary,
543 (jid:to_binary(JID))/binary>>,
544 2 Instructions = <<"Fill in fields to search for any matching Jabber User">>,
545 2 Fields = lists:map(fun ({X, Y}) -> ?TLFIELD(<<"text-single">>, X, Y) end, SearchFields),
546 2 mongoose_data_forms:form(#{title => Title, instructions => Instructions, fields => Fields}).
547
548 route_search_iq_set(HostType, LServer, From, To, Acc, Lang, SubEl, IQ) ->
549 24 XDataEl = mongoose_data_forms:find_form(SubEl),
550 24 RSMIn = jlib:rsm_decode(IQ),
551 24 case XDataEl of
552 undefined ->
553 2 {Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:bad_request()),
554 2 ejabberd_router:route(To, From, Acc1, Err);
555 _ ->
556 22 case mongoose_data_forms:parse_form_fields(XDataEl) of
557 #{type := <<"submit">>, kvs := KVs} ->
558 21 {SearchResult, RSMOutEls} = search_result(HostType, LServer, Lang, To, KVs, RSMIn),
559 21 ResIQ = make_search_result_iq(IQ, SearchResult, RSMOutEls),
560 21 ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ));
561 _ ->
562 1 {Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:bad_request()),
563 1 ejabberd_router:route(To, From, Acc1, Err)
564 end
565 end.
566
567 make_search_result_iq(IQ, SearchResult, RSMOutEls) ->
568 21 Form = mongoose_data_forms:form(SearchResult),
569 21 IQ#iq{
570 type = result,
571 sub_el = [#xmlel{name = <<"query">>,
572 attrs = [{<<"xmlns">>, ?NS_SEARCH}],
573 children = [Form | RSMOutEls]}
574 ]}.
575
576 iq_get_vcard() ->
577 1 [#xmlel{name = <<"FN">>,
578 children = [#xmlcdata{content = <<"MongooseIM/mod_vcard">>}]},
579 #xmlel{name = <<"URL">>, children = [#xmlcdata{content = ?MONGOOSE_URI}]},
580 #xmlel{name = <<"DESC">>,
581 children = [#xmlcdata{content = [<<"MongooseIM vCard module">>,
582 <<"\nCopyright (c) Erlang Solutions Ltd.">>]}]}].
583
584 features() ->
585 1 [?NS_DISCO_INFO, ?NS_SEARCH, ?NS_VCARD].
586
587 identity(Lang) ->
588 1 #{category => <<"directory">>,
589 type => <<"user">>,
590 name => translate:translate(Lang, <<"vCard User Search">>)}.
591
592 search_result(HostType, LServer, Lang, JID, Data, RSMIn) ->
593 21 Title = translate:translate(Lang, <<"Search Results for ", (jid:to_binary(JID))/binary>>),
594 21 ReportedFields = mod_vcard_backend:search_reported_fields(HostType, LServer, Lang),
595 21 Results1 = mod_vcard_backend:search(HostType, LServer, maps:to_list(Data)),
596 21 Results2 = lists:filtermap(
597 fun(Result) ->
598 26 case search_result_get_jid(Result) of
599 [ResultJID] ->
600 26 {true, {ResultJID, Result}};
601 [] ->
602
:-(
false
603 end
604 end,
605 Results1),
606 %% mnesia does not guarantee sorting order
607 21 Results3 = lists:sort(Results2),
608 21 {Results4, RSMOutEls} = apply_rsm_to_search_results(Results3, RSMIn, none),
609 21 Results5 = [Result || {_, Result} <- Results4],
610 21 Form = #{type => <<"result">>, title => Title, reported => ReportedFields, items => Results5},
611 21 {Form, RSMOutEls}.
612
613 %% No RSM input, create empty
614 apply_rsm_to_search_results(Results, none, RSMOut) ->
615 12 apply_rsm_to_search_results(Results, #rsm_in{}, RSMOut);
616
617 %% Create RSM output
618 apply_rsm_to_search_results(Results, #rsm_in{} = RSMIn, none) ->
619 21 RSMOut = #rsm_out{count = length(Results)},
620 21 apply_rsm_to_search_results(Results, RSMIn, RSMOut);
621
622 %% Skip by <after>$id</after>
623 apply_rsm_to_search_results(Results1, #rsm_in{direction = aft,
624 id = After} = RSMIn, RSMOut)
625 when is_binary(After) ->
626 2 Results2 = lists:dropwhile(
627 fun({JID, _Result}) ->
628 3 JID == After
629 end,
630 lists:dropwhile(
631 fun({JID, _Result}) ->
632 3 JID =/= After
633 end,
634 Results1
635 )),
636 2 Index = length(Results1) - length(Results2),
637 2 apply_rsm_to_search_results(
638 Results2,
639 RSMIn#rsm_in{direction = undefined, id = undefined},
640 RSMOut#rsm_out{index = Index}
641 );
642
643 %% Seek by <before>$id</before>
644 apply_rsm_to_search_results(Results1, #rsm_in{max = Max,
645 direction = before,
646 id = Before} = RSMIn, RSMOut)
647 when is_binary(Before) ->
648 3 Results2 = lists:takewhile(
649 fun({JID, _Result}) ->
650 5 JID =/= Before
651 end, Results1),
652 3 if
653 is_integer(Max) ->
654 3 Index = max(0, length(Results2) - Max),
655 3 Results3 = lists:nthtail(Index, Results2);
656 true ->
657
:-(
Index = 0,
658
:-(
Results3 = Results2
659 end,
660 3 apply_rsm_to_search_results(
661 Results3,
662 RSMIn#rsm_in{direction = undefined, id = undefined},
663 RSMOut#rsm_out{index = Index}
664 );
665
666 %% Skip by page number <index>371</index>
667 apply_rsm_to_search_results(Results1,
668 #rsm_in{max = Max,
669 index = Index} = RSMIn1,
670 RSMOut)
671 when is_integer(Max), is_integer(Index) ->
672 1 Results2 = lists:nthtail(min(Index, length(Results1)), Results1),
673 1 RSMIn2 = RSMIn1#rsm_in{index = undefined},
674 1 apply_rsm_to_search_results(
675 Results2,
676 RSMIn2,
677 RSMOut#rsm_out{index = Index}
678 );
679
680 %% Limit to <max>10</max> items
681 apply_rsm_to_search_results(Results1, #rsm_in{max = Max} = RSMIn, RSMOut)
682 when is_integer(Max) ->
683 9 Results2 = lists:sublist(Results1, Max),
684 9 apply_rsm_to_search_results(Results2,
685 RSMIn#rsm_in{max = undefined}, RSMOut);
686
687 %% Encode RSM output
688 apply_rsm_to_search_results([_ | _] = Results, _, #rsm_out{} = RSMOut1) ->
689 13 {FirstJID, _} = hd(Results),
690 13 {LastJID, _} = lists:last(Results),
691 13 RSMOut2 = RSMOut1#rsm_out{first = FirstJID,
692 last = LastJID},
693 13 {Results, jlib:rsm_encode(RSMOut2)};
694
695 apply_rsm_to_search_results([], _, #rsm_out{} = RSMOut1) ->
696 %% clear `index' without `after'
697 8 RSMOut2 = RSMOut1#rsm_out{index = undefined},
698 8 {[], jlib:rsm_encode(RSMOut2)}.
699
700 search_result_get_jid(Fields) ->
701 26 [JID || #{var := <<"jid">>, values := [JID]} <- Fields].
702
703 parse_vcard(LUser, VHost, VCARD) ->
704 37 FN = xml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]),
705 37 Family = xml:get_path_s(VCARD, [{elem, <<"N">>},
706 {elem, <<"FAMILY">>}, cdata]),
707 37 Given = xml:get_path_s(VCARD, [{elem, <<"N">>},
708 {elem, <<"GIVEN">>}, cdata]),
709 37 Middle = xml:get_path_s(VCARD, [{elem, <<"N">>},
710 {elem, <<"MIDDLE">>}, cdata]),
711 37 Nickname = xml:get_path_s(VCARD, [{elem, <<"NICKNAME">>}, cdata]),
712 37 BDay = xml:get_path_s(VCARD, [{elem, <<"BDAY">>}, cdata]),
713 37 CTRY = xml:get_path_s(VCARD, [{elem, <<"ADR">>},
714 {elem, <<"CTRY">>}, cdata]),
715 37 Locality = xml:get_path_s(VCARD, [{elem, <<"ADR">>},
716 {elem, <<"LOCALITY">>}, cdata]),
717 37 EMail1 = xml:get_path_s(VCARD, [{elem, <<"EMAIL">>},
718 {elem, <<"USERID">>}, cdata]),
719 37 EMail2 = xml:get_path_s(VCARD, [{elem, <<"EMAIL">>}, cdata]),
720 37 OrgName = xml:get_path_s(VCARD, [{elem, <<"ORG">>},
721 {elem, <<"ORGNAME">>}, cdata]),
722 37 OrgUnit = xml:get_path_s(VCARD, [{elem, <<"ORG">>},
723 {elem, <<"ORGUNIT">>}, cdata]),
724 37 EMail = case EMail1 of
725 28 <<"">> -> EMail2;
726 9 _ -> EMail1
727 end,
728 37 try
729 37 LFN = prepare_index(<<"FN">>, FN),
730 35 LFamily = prepare_index(<<"FAMILY">>, Family),
731 35 LGiven = prepare_index(<<"GIVEN">>, Given),
732 35 LMiddle = prepare_index(<<"MIDDLE">>, Middle),
733 35 LNickname = prepare_index_allow_emoji(<<"NICKNAME">>, Nickname),
734 35 LBDay = prepare_index(<<"BDAY">>, BDay),
735 35 LCTRY = prepare_index(<<"CTRY">>, CTRY),
736 35 LLocality = prepare_index(<<"LOCALITY">>, Locality),
737 35 LEMail = prepare_index(<<"EMAIL">>, EMail),
738 35 LOrgName = prepare_index(<<"ORGNAME">>, OrgName),
739 35 LOrgUnit = prepare_index(<<"ORGUNIT">>, OrgUnit),
740
741 35 US = {LUser, VHost},
742
743 35 {ok, #vcard_search{us = US,
744 user = {LUser, VHost},
745 luser = LUser,
746 fn = FN, lfn = LFN,
747 family = Family, lfamily = LFamily,
748 given = Given, lgiven = LGiven,
749 middle = Middle, lmiddle = LMiddle,
750 nickname = Nickname, lnickname = LNickname,
751 bday = BDay, lbday = LBDay,
752 ctry = CTRY, lctry = LCTRY,
753 locality = Locality, llocality = LLocality,
754 email = EMail, lemail = LEMail,
755 orgname = OrgName, lorgname = LOrgName,
756 orgunit = OrgUnit, lorgunit = LOrgUnit
757 }}
758 catch
759 throw:{invalid_input, Info} ->
760 2 {error, {invalid_input, Info}}
761 end.
762
763 prepare_index(FieldName, Value) ->
764 387 case jid:str_tolower(Value) of
765 error ->
766 2 throw({invalid_input, {FieldName, Value}});
767 LValue ->
768 385 LValue
769 end.
770
771 prepare_index_allow_emoji(FieldName, Value) ->
772 35 {ok, Re} = re:compile(<<"[^[:alnum:][:space:][:punct:]]">>, [unicode, ucp]),
773 35 Sanitized = re:replace(Value, Re, <<"">>, [global]),
774 35 prepare_index(FieldName, Sanitized).
775
776
777 -spec get_default_reported_fields(binary()) -> [mongoose_data_forms:field()].
778 get_default_reported_fields(Lang) ->
779 21 [
780 ?TLFIELD(<<"jid-single">>, <<"Jabber ID">>, <<"jid">>),
781 ?TLFIELD(<<"text-single">>, <<"Full Name">>, <<"fn">>),
782 ?TLFIELD(<<"text-single">>, <<"Name">>, <<"first">>),
783 ?TLFIELD(<<"text-single">>, <<"Middle Name">>, <<"middle">>),
784 ?TLFIELD(<<"text-single">>, <<"Family Name">>, <<"last">>),
785 ?TLFIELD(<<"text-single">>, <<"Nickname">>, <<"nick">>),
786 ?TLFIELD(<<"text-single">>, <<"Birthday">>, <<"bday">>),
787 ?TLFIELD(<<"text-single">>, <<"Country">>, <<"ctry">>),
788 ?TLFIELD(<<"text-single">>, <<"City">>, <<"locality">>),
789 ?TLFIELD(<<"text-single">>, <<"Email">>, <<"email">>),
790 ?TLFIELD(<<"text-single">>, <<"Organization Name">>, <<"orgname">>),
791 ?TLFIELD(<<"text-single">>, <<"Organization Unit">>, <<"orgunit">>)
792 ].
793
794 config_metrics(Host) ->
795 252 mongoose_module_metrics:opts_for_module(Host, ?MODULE, [backend]).
796
797 vcard_error(IQ = #iq{sub_el = VCARD}, ReasonEl) ->
798 4 IQ#iq{type = error, sub_el = [VCARD, ReasonEl]}.
799
800 directory_jid_to_server_host(#jid{lserver = DirHost}) ->
801 28 case mongoose_domain_api:get_subdomain_info(DirHost) of
802 {ok, #{parent_domain := ServerHost}} when is_binary(ServerHost) ->
803 28 ServerHost;
804 Other ->
805
:-(
error({dir_jid_to_server_host_failed, DirHost, Other})
806 end.
Line Hits Source