./ct_report/coverage/mod_vcard_rdbms.COVER.html

1 %%%----------------------------------------------------------------------
2 %%% File : mod_vcard.erl
3 %%% Author : Alexey Shchepin <alexey@process-one.net>
4 %%% Purpose : vCard support via RDBMS
5 %%% Created : 2 Jan 2003 by Alexey Shchepin <alexey@process-one.net>
6 %%%
7 %%%
8 %%% ejabberd, Copyright (C) 2002-2011 ProcessOne
9 %%%
10 %%% This program is free software; you can redistribute it and/or
11 %%% modify it under the terms of the GNU General Public License as
12 %%% published by the Free Software Foundation; either version 2 of the
13 %%% License, or (at your option) any later version.
14 %%%
15 %%% This program is distributed in the hope that it will be useful,
16 %%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17 %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 %%% General Public License for more details.
19 %%%
20 %%% You should have received a copy of the GNU General Public License
21 %%% along with this program; if not, write to the Free Software
22 %%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23 %%%
24 %%%----------------------------------------------------------------------
25
26 -module(mod_vcard_rdbms).
27
28 -behaviour(mod_vcard_backend).
29
30 %% mod_vcards callbacks
31 -export([init/2,
32 remove_user/3,
33 remove_domain/2,
34 get_vcard/3,
35 set_vcard/5,
36 search/3,
37 search_fields/2,
38 search_reported_fields/3]).
39
40 -include("mongoose.hrl").
41 -include("jlib.hrl").
42 -include("mod_vcard.hrl").
43
44 -type filter_type() :: equal | like.
45 -type limit_type() :: infinity | top | limit.
46 -type sql_column() :: binary().
47 -type sql_value() :: binary().
48 -type sql_filter() :: {filter_type(), sql_column(), sql_value()}.
49
50 init(HostType, _Options) ->
51 297 mongoose_rdbms:prepare(vcard_remove, vcard, [username, server],
52 <<"DELETE FROM vcard WHERE username=? AND server=?">>),
53 297 mongoose_rdbms:prepare(vcard_search_remove, vcard_search, [lusername, server],
54 <<"DELETE FROM vcard_search WHERE lusername=? AND server=?">>),
55 297 mongoose_rdbms:prepare(vcard_remove_domain, vcard, [server],
56 <<"DELETE FROM vcard WHERE server=?">>),
57 297 mongoose_rdbms:prepare(vcard_search_remove_domain, vcard_search, [server],
58 <<"DELETE FROM vcard_search WHERE server=?">>),
59 297 mongoose_rdbms:prepare(vcard_select, vcard,
60 [username, server],
61 <<"SELECT vcard FROM vcard WHERE username=? AND server=?">>),
62 297 rdbms_queries:prepare_upsert(HostType, vcard_upsert, vcard,
63 [<<"username">>, <<"server">>, <<"vcard">>],
64 [<<"vcard">>],
65 [<<"username">>, <<"server">>]),
66 297 SearchColumns = search_columns(),
67 297 rdbms_queries:prepare_upsert(HostType, vcard_search_upsert, vcard_search,
68 [<<"lusername">>, <<"server">>|SearchColumns],
69 SearchColumns,
70 [<<"lusername">>, <<"server">>]),
71 297 ok.
72
73 %% Remove user callback
74 remove_user(HostType, LUser, LServer) ->
75 3545 F = fun() -> remove_user_t(HostType, LUser, LServer) end,
76 3545 mongoose_rdbms:sql_transaction(HostType, F).
77
78 remove_user_t(HostType, LUser, LServer) ->
79 3545 mongoose_rdbms:execute(HostType, vcard_remove, [LUser, LServer]),
80 3545 mongoose_rdbms:execute(HostType, vcard_search_remove, [LUser, LServer]).
81
82 %% Remove domain callback
83 -spec remove_domain(mongooseim:host_type(), jid:lserver()) -> ok.
84 remove_domain(HostType, Domain) ->
85 16 F = fun() -> remove_domain_t(HostType, Domain) end,
86 16 mongoose_rdbms:sql_transaction(HostType, F),
87 16 ok.
88
89 remove_domain_t(HostType, Domain) ->
90 16 mongoose_rdbms:execute_successfully(HostType, vcard_remove_domain, [Domain]),
91 16 mongoose_rdbms:execute_successfully(HostType, vcard_search_remove_domain, [Domain]).
92
93 %% Get a single vCard callback
94 get_vcard(HostType, LUser, LServer) ->
95 83 Res = mongoose_rdbms:execute(HostType, vcard_select, [LUser, LServer]),
96 83 case Res of
97 {selected, [{SVCARD}]} ->
98 25 case exml:parse(SVCARD) of
99 {error, Reason} ->
100
:-(
?LOG_WARNING(#{what => vcard_corrupted,
101 text => <<"Not sending back bad vCard XML">>,
102 reason => Reason, svcard => SVCARD,
103
:-(
user => LUser, host => LServer}),
104
:-(
{error, mongoose_xmpp_errors:service_unavailable()};
105 {ok, VCARD} ->
106 25 {ok, [VCARD]}
107 end;
108 {selected, []} ->
109 58 {error, mongoose_xmpp_errors:item_not_found()}
110 end.
111
112 %% Set a vCard callback
113 set_vcard(HostType, User, LServer, VCard, Search) ->
114 26 LUser = jid:nodeprep(User),
115 26 SearchArgs = assert_binaries(search_args(User, Search)),
116 26 XML = exml:to_binary(VCard),
117 26 F = fun() ->
118 26 update_vcard_t(HostType, LUser, LServer, XML),
119 26 update_vcard_search_t(HostType, LUser, LServer, SearchArgs),
120 26 ok
121 end,
122 26 Result = handle_result(rdbms_queries:sql_transaction(HostType, F)),
123 26 log_upsert_result(HostType, LServer, LUser, VCard, XML, Result),
124 26 Result.
125
126 %% Do not pass unicode strings as a list of bytes into MySQL driver.
127 %% MySQL driver treats lists of integers as lists of codepoints.
128 %% So, it wouldn't be encoded properly.
129 %% Only binaries should be used to avoid confusion.
130 assert_binaries(Bins) ->
131 26 case lists:all(fun is_binary/1, Bins) of
132 true ->
133 26 Bins;
134 false ->
135
:-(
error(#{what => assert_binaries_failed, binaries => Bins})
136 end.
137
138 log_upsert_result(HostType, LServer, LUser, VCard, _XML, ok) ->
139 26 mongoose_hooks:vcard_set(HostType, LServer, LUser, VCard);
140 log_upsert_result(HostType, LServer, LUser, _VCard, XML, {error, Reason}) ->
141
:-(
?LOG_WARNING(#{what => vcard_update_failed, reason => Reason,
142 host_type => HostType,
143
:-(
user => LUser, server => LServer, vcard_xml => XML}).
144
145 26 handle_result({atomic, ok}) -> ok;
146
:-(
handle_result({aborted, Reason}) -> {error, {aborted, Reason}};
147
:-(
handle_result({error, Reason}) -> {error, Reason}.
148
149 update_vcard_t(HostType, LUser, LServer, XML) ->
150 26 InsertParams = [LUser, LServer, XML],
151 26 UpdateParams = [XML],
152 26 UniqueKeyValues = [LUser, LServer],
153 26 rdbms_queries:execute_upsert(HostType, vcard_upsert, InsertParams, UpdateParams, UniqueKeyValues).
154
155 update_vcard_search_t(HostType, LUser, LServer, SearchArgs) ->
156 26 InsertParams = [LUser, LServer|SearchArgs],
157 26 UpdateParams = SearchArgs,
158 26 UniqueKeyValues = [LUser, LServer],
159 26 rdbms_queries:execute_upsert(HostType, vcard_search_upsert, InsertParams, UpdateParams, UniqueKeyValues).
160
161 %% Search vCards fields callback
162 search_fields(_HostType, _VHost) ->
163 2 mod_vcard:default_search_fields().
164
165 %% Search vCards reported fields callback
166 search_reported_fields(_HostType, _VHost, Lang) ->
167 21 mod_vcard:get_default_reported_fields(Lang).
168
169 %% Search vCards callback
170 search(HostType, LServer, Data) ->
171 23 Filters = make_filters(LServer, Data),
172 23 case Filters of
173 [] ->
174 2 [];
175 _ ->
176 21 Limit = mod_vcard:get_results_limit(HostType),
177 21 LimitType = limit_type(Limit),
178 21 StmtName = filters_to_statement_name(Filters, LimitType),
179 21 case mongoose_rdbms:prepared(StmtName) of
180 false ->
181 %% Create a new type of a query
182 5 SQL = search_sql_binary(Filters, LimitType),
183 5 Columns = filters_to_columns(Filters, LimitType),
184 5 mongoose_rdbms:prepare(StmtName, vcard_search, Columns, SQL);
185 true ->
186 16 ok
187 end,
188 21 Args = filters_to_args(Filters, LimitType, Limit),
189 21 try mongoose_rdbms:execute(HostType, StmtName, Args) of
190 {selected, Rs} when is_list(Rs) ->
191 21 record_to_items(Rs);
192 Error ->
193
:-(
?LOG_ERROR(#{what => vcard_db_search_failed, statement => StmtName,
194 sql_query => search_sql_binary(Filters, LimitType),
195
:-(
reason => Error, host => LServer}),
196
:-(
[]
197 catch Class:Error:Stacktrace ->
198
:-(
?LOG_ERROR(#{what => vcard_db_search_failed, statement => StmtName,
199 sql_query => search_sql_binary(Filters, LimitType),
200 class => Class, stacktrace => Stacktrace,
201
:-(
reason => Error, host => LServer}),
202
:-(
[]
203 end
204 end.
205
206 -spec limit_type(infinity | non_neg_integer()) -> limit_type().
207 limit_type(infinity) ->
208
:-(
infinity;
209 limit_type(_Limit) ->
210 21 case mongoose_rdbms:db_type() of
211 21 mssql -> top;
212
:-(
_ -> limit
213 end.
214
215 %% Encodes filter column names using short format
216 filters_to_statement_name(Filters, LimitType) ->
217 21 Ids = [type_to_id(Type) ++ column_to_id(Col) || {Type, Col, _Val} <- Filters],
218 21 LimitId = limit_type_to_id(LimitType),
219 21 list_to_atom("vcard_search_" ++ LimitId ++ "_" ++ lists:append(Ids)).
220
221 filters_to_columns(Filters, LimitType) ->
222 5 Columns = [Col || {_Type, Col, _Val} <- Filters],
223 %% <<"limit">> is a pseudocolumn: does not exist in the schema,
224 %% but mssql's driver code needs to know which type to use for the placeholder.
225 5 case LimitType of
226
:-(
infinity -> Columns;
227 5 top -> [<<"limit">>|Columns];
228
:-(
limit -> Columns ++ [<<"limit">>]
229 end.
230
231 filters_to_args(Filters, LimitType, Limit) ->
232 21 Args = [Val || {_Type, _Col, Val} <- Filters],
233 21 case LimitType of
234
:-(
infinity -> Args;
235 21 top -> [Limit|Args];
236
:-(
limit -> Args ++ [Limit]
237 end.
238
239 search_sql_binary(Filters, LimitType) ->
240 5 iolist_to_binary(search_sql(Filters, LimitType)).
241
242 search_sql(Filters, LimitType) ->
243 5 {TopSQL, LimitSQL} = limit_type_to_sql(LimitType),
244 5 RestrictionSQL = filters_to_sql(Filters),
245 5 [<<"SELECT ">>, TopSQL,
246 <<" username, server, fn, family, given, middle, "
247 "nickname, bday, ctry, locality, "
248 "email, orgname, orgunit "
249 "FROM vcard_search ">>,
250 RestrictionSQL, LimitSQL].
251
252 -spec limit_type_to_sql(limit_type()) -> {binary(), binary()}.
253 limit_type_to_sql(infinity) ->
254
:-(
{<<>>, <<>>};
255 limit_type_to_sql(top) ->
256 5 {<<" TOP (?) ">>, <<>>};
257 limit_type_to_sql(limit) ->
258
:-(
{<<>>, <<" LIMIT ? ">>}.
259
260 filters_to_sql([Filter|Filters]) ->
261 5 [" WHERE ", filter_to_sql(Filter)|
262 6 [[" AND ", filter_to_sql(F)] || F <- Filters]].
263
264 filter_to_sql({equal, Col, _}) ->
265 10 [Col, "=?"];
266 filter_to_sql({like, Col, _}) ->
267 1 [Col, " LIKE ?"].
268
269 %% The result defines everything that is needed to prepare an SQL query.
270 -spec make_filters(jid:lserver(), list()) -> [sql_filter()].
271 make_filters(LServer, Data) ->
272 23 Filters = only_tuples([filter_field(Var, Val) || {Var, [Val]} <- Data]),
273 23 case Filters of
274 [] ->
275 2 [];
276 _ ->
277 21 HostFilter = {equal, <<"server">>, LServer},
278 21 lists:sort([HostFilter|Filters])
279 end.
280
281 only_tuples(List) ->
282 23 [X || X <- List, is_tuple(X)].
283
284 filter_field(Var, Val) when is_binary(Val) and (Val /= <<"">>) ->
285 22 case xmpp_field_to_column(Var) of
286 false ->
287
:-(
false;
288 Field ->
289 22 Type = value_type(Val),
290 22 LVal = prepare_value(Val, Type),
291 22 {Type, Field, LVal}
292 end;
293 filter_field(_, _) ->
294
:-(
false.
295
296 prepare_value(Val, like) ->
297 11 LVal = without_last_byte(jid:str_tolower(Val)),
298 11 <<LVal/binary, "%">>;
299 prepare_value(Val, equal) ->
300 11 jid:str_tolower(Val).
301
302 value_type(Val) ->
303 22 case binary:last(Val) of
304 $* ->
305 11 like;
306 _ ->
307 11 equal
308 end.
309
310
:-(
limit_type_to_id(infinity) -> "inf";
311
:-(
limit_type_to_id(limit) -> "lim";
312 21 limit_type_to_id(top) -> "top".
313
314 11 type_to_id(like) -> "l";
315 32 type_to_id(equal) -> "e".
316
317
:-(
xmpp_field_to_column(<<"user">>) -> <<"lusername">>;
318 18 xmpp_field_to_column(<<"fn">>) -> <<"lfn">>;
319 2 xmpp_field_to_column(<<"last">>) -> <<"lfamily">>;
320 1 xmpp_field_to_column(<<"first">>) -> <<"lgiven">>;
321
:-(
xmpp_field_to_column(<<"middle">>) -> <<"lmiddle">>;
322
:-(
xmpp_field_to_column(<<"nick">>) -> <<"lnickname">>;
323
:-(
xmpp_field_to_column(<<"bday">>) -> <<"lbday">>;
324
:-(
xmpp_field_to_column(<<"ctry">>) -> <<"lctry">>;
325 1 xmpp_field_to_column(<<"locality">>) -> <<"llocality">>;
326
:-(
xmpp_field_to_column(<<"email">>) -> <<"lemail">>;
327
:-(
xmpp_field_to_column(<<"orgname">>) -> <<"lorgname">>;
328
:-(
xmpp_field_to_column(<<"orgunit">>) -> <<"lorgunit">>;
329
:-(
xmpp_field_to_column(_) -> false.
330
331 21 column_to_id(<<"server">>) -> "s";
332
:-(
column_to_id(<<"lusername">>) -> "u";
333 18 column_to_id(<<"lfn">>) -> "f";
334 2 column_to_id(<<"lfamily">>) -> "l";
335 1 column_to_id(<<"lgiven">>) -> "f";
336
:-(
column_to_id(<<"lmiddle">>) -> "m";
337
:-(
column_to_id(<<"lnickname">>) -> "n";
338
:-(
column_to_id(<<"lbday">>) -> "b";
339
:-(
column_to_id(<<"lctry">>) -> "c";
340 1 column_to_id(<<"llocality">>) -> "L";
341
:-(
column_to_id(<<"lemail">>) -> "e";
342
:-(
column_to_id(<<"lorgname">>) -> "N";
343
:-(
column_to_id(<<"lorgunit">>) -> "U".
344
345 search_columns() ->
346 297 [<<"username">>,
347 <<"fn">>, <<"lfn">>,
348 <<"family">>, <<"lfamily">>,
349 <<"given">>, <<"lgiven">>,
350 <<"middle">>, <<"lmiddle">>,
351 <<"nickname">>, <<"lnickname">>,
352 <<"bday">>, <<"lbday">>,
353 <<"ctry">>, <<"lctry">>,
354 <<"locality">>, <<"llocality">>,
355 <<"email">>, <<"lemail">>,
356 <<"orgname">>, <<"lorgname">>,
357 <<"orgunit">>, <<"lorgunit">>].
358
359 search_args(User, Search) ->
360 26 [User,
361 Search#vcard_search.fn, Search#vcard_search.lfn,
362 Search#vcard_search.family, Search#vcard_search.lfamily,
363 Search#vcard_search.given, Search#vcard_search.lgiven,
364 Search#vcard_search.middle, Search#vcard_search.lmiddle,
365 Search#vcard_search.nickname, Search#vcard_search.lnickname,
366 Search#vcard_search.bday, Search#vcard_search.lbday,
367 Search#vcard_search.ctry, Search#vcard_search.lctry,
368 Search#vcard_search.locality, Search#vcard_search.llocality,
369 Search#vcard_search.email, Search#vcard_search.lemail,
370 Search#vcard_search.orgname, Search#vcard_search.lorgname,
371 Search#vcard_search.orgunit, Search#vcard_search.lorgunit].
372
373 without_last_byte(Bin) ->
374 11 binary:part(Bin, 0, byte_size(Bin)-1).
375
376 record_to_items(Records) ->
377 21 [record_to_item(Record) || Record <- Records].
378
379 record_to_item({Username, VCardVHost, FN, Family, Given, Middle,
380 Nickname, BDay, CTRY, Locality,
381 EMail, OrgName, OrgUnit}) ->
382 27 #xmlel{name = <<"item">>,
383 children = [
384 ?FIELD(<<"jid">>, <<Username/binary, "@", VCardVHost/binary>>),
385 ?FIELD(<<"fn">>, FN),
386 ?FIELD(<<"last">>, Family),
387 ?FIELD(<<"first">>, Given),
388 ?FIELD(<<"middle">>, Middle),
389 ?FIELD(<<"nick">>, Nickname),
390 ?FIELD(<<"bday">>, BDay),
391 ?FIELD(<<"ctry">>, CTRY),
392 ?FIELD(<<"locality">>, Locality),
393 ?FIELD(<<"email">>, EMail),
394 ?FIELD(<<"orgname">>, OrgName),
395 ?FIELD(<<"orgunit">>, OrgUnit)
396 ]}.
Line Hits Source