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