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