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