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