./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
:-(
mongoose_rdbms:prepare(vcard_remove, vcard, [username, server],
52 <<"DELETE FROM vcard WHERE username=? AND server=?">>),
53
:-(
mongoose_rdbms:prepare(vcard_search_remove, vcard_search, [lusername, server],
54 <<"DELETE FROM vcard_search WHERE lusername=? AND server=?">>),
55
:-(
mongoose_rdbms:prepare(vcard_remove_domain, vcard, [server],
56 <<"DELETE FROM vcard WHERE server=?">>),
57
:-(
mongoose_rdbms:prepare(vcard_search_remove_domain, vcard_search, [server],
58 <<"DELETE FROM vcard_search WHERE server=?">>),
59
:-(
mongoose_rdbms:prepare(vcard_select, vcard,
60 [username, server],
61 <<"SELECT vcard FROM vcard WHERE username=? AND server=?">>),
62
:-(
rdbms_queries:prepare_upsert(HostType, vcard_upsert, vcard,
63 [<<"username">>, <<"server">>, <<"vcard">>],
64 [<<"vcard">>],
65 [<<"username">>, <<"server">>]),
66
:-(
SearchColumns = search_columns(),
67
:-(
rdbms_queries:prepare_upsert(HostType, vcard_search_upsert, vcard_search,
68 [<<"lusername">>, <<"server">>|SearchColumns],
69 SearchColumns,
70 [<<"lusername">>, <<"server">>]),
71
:-(
ok.
72
73 %% Remove user callback
74 remove_user(HostType, LUser, LServer) ->
75
:-(
F = fun() -> remove_user_t(HostType, LUser, LServer) end,
76
:-(
mongoose_rdbms:sql_transaction(HostType, F).
77
78 remove_user_t(HostType, LUser, LServer) ->
79
:-(
mongoose_rdbms:execute(HostType, vcard_remove, [LUser, LServer]),
80
:-(
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
:-(
F = fun() -> remove_domain_t(HostType, Domain) end,
86
:-(
mongoose_rdbms:sql_transaction(HostType, F),
87
:-(
ok.
88
89 remove_domain_t(HostType, Domain) ->
90
:-(
mongoose_rdbms:execute_successfully(HostType, vcard_remove_domain, [Domain]),
91
:-(
mongoose_rdbms:execute_successfully(HostType, vcard_search_remove_domain, [Domain]).
92
93 %% Get a single vCard callback
94 get_vcard(HostType, LUser, LServer) ->
95
:-(
Res = mongoose_rdbms:execute(HostType, vcard_select, [LUser, LServer]),
96
:-(
case Res of
97 {selected, [{SVCARD}]} ->
98
:-(
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
:-(
{ok, [VCARD]}
107 end;
108 {selected, []} ->
109
:-(
{error, mongoose_xmpp_errors:item_not_found()}
110 end.
111
112 %% Set a vCard callback
113 set_vcard(HostType, User, LServer, VCard, Search) ->
114
:-(
LUser = jid:nodeprep(User),
115
:-(
SearchArgs = assert_binaries(search_args(User, Search)),
116
:-(
XML = exml:to_binary(VCard),
117
:-(
F = fun() ->
118
:-(
update_vcard_t(HostType, LUser, LServer, XML),
119
:-(
update_vcard_search_t(HostType, LUser, LServer, SearchArgs),
120
:-(
ok
121 end,
122
:-(
Result = handle_result(rdbms_queries:sql_transaction(HostType, F)),
123
:-(
log_upsert_result(HostType, LServer, LUser, VCard, XML, Result),
124
:-(
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
:-(
case lists:all(fun is_binary/1, Bins) of
132 true ->
133
:-(
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
:-(
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
:-(
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
:-(
InsertParams = [LUser, LServer, XML],
151
:-(
UpdateParams = [XML],
152
:-(
UniqueKeyValues = [LUser, LServer],
153
:-(
rdbms_queries:execute_upsert(HostType, vcard_upsert, InsertParams, UpdateParams, UniqueKeyValues).
154
155 update_vcard_search_t(HostType, LUser, LServer, SearchArgs) ->
156
:-(
InsertParams = [LUser, LServer|SearchArgs],
157
:-(
UpdateParams = SearchArgs,
158
:-(
UniqueKeyValues = [LUser, LServer],
159
:-(
rdbms_queries:execute_upsert(HostType, vcard_search_upsert, InsertParams, UpdateParams, UniqueKeyValues).
160
161 %% Search vCards fields callback
162 search_fields(_HostType, _VHost) ->
163
:-(
mod_vcard:default_search_fields().
164
165 %% Search vCards reported fields callback
166 search_reported_fields(_HostType, _VHost, Lang) ->
167
:-(
mod_vcard:get_default_reported_fields(Lang).
168
169 %% Search vCards callback
170 search(HostType, LServer, Data) ->
171
:-(
Filters = make_filters(LServer, Data),
172
:-(
case Filters of
173 [] ->
174
:-(
[];
175 _ ->
176
:-(
Limit = mod_vcard:get_results_limit(HostType),
177
:-(
LimitType = limit_type(Limit),
178
:-(
StmtName = filters_to_statement_name(Filters, LimitType),
179
:-(
case mongoose_rdbms:prepared(StmtName) of
180 false ->
181 %% Create a new type of a query
182
:-(
SQL = search_sql_binary(Filters, LimitType),
183
:-(
Columns = filters_to_columns(Filters, LimitType),
184
:-(
mongoose_rdbms:prepare(StmtName, vcard_search, Columns, SQL);
185 true ->
186
:-(
ok
187 end,
188
:-(
Args = filters_to_args(Filters, LimitType, Limit),
189
:-(
try mongoose_rdbms:execute(HostType, StmtName, Args) of
190 {selected, Rs} when is_list(Rs) ->
191
:-(
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
:-(
case mongoose_rdbms:db_type() of
211
:-(
mssql -> top;
212
:-(
_ -> limit
213 end.
214
215 %% Encodes filter column names using short format
216 filters_to_statement_name(Filters, LimitType) ->
217
:-(
Ids = [type_to_id(Type) ++ column_to_id(Col) || {Type, Col, _Val} <- Filters],
218
:-(
LimitId = limit_type_to_id(LimitType),
219
:-(
list_to_atom("vcard_search_" ++ LimitId ++ "_" ++ lists:append(Ids)).
220
221 filters_to_columns(Filters, LimitType) ->
222
:-(
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
:-(
case LimitType of
226
:-(
infinity -> Columns;
227
:-(
top -> [<<"limit">>|Columns];
228
:-(
limit -> Columns ++ [<<"limit">>]
229 end.
230
231 filters_to_args(Filters, LimitType, Limit) ->
232
:-(
Args = [Val || {_Type, _Col, Val} <- Filters],
233
:-(
case LimitType of
234
:-(
infinity -> Args;
235
:-(
top -> [Limit|Args];
236
:-(
limit -> Args ++ [Limit]
237 end.
238
239 search_sql_binary(Filters, LimitType) ->
240
:-(
iolist_to_binary(search_sql(Filters, LimitType)).
241
242 search_sql(Filters, LimitType) ->
243
:-(
{TopSQL, LimitSQL} = limit_type_to_sql(LimitType),
244
:-(
RestrictionSQL = filters_to_sql(Filters),
245
:-(
[<<"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
:-(
{<<" TOP (?) ">>, <<>>};
257 limit_type_to_sql(limit) ->
258
:-(
{<<>>, <<" LIMIT ? ">>}.
259
260 filters_to_sql([Filter|Filters]) ->
261
:-(
[" WHERE ", filter_to_sql(Filter)|
262
:-(
[[" AND ", filter_to_sql(F)] || F <- Filters]].
263
264 filter_to_sql({equal, Col, _}) ->
265
:-(
[Col, "=?"];
266 filter_to_sql({like, Col, _}) ->
267
:-(
[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
:-(
Filters = only_tuples([filter_field(Var, Val) || {Var, [Val]} <- Data]),
273
:-(
case Filters of
274 [] ->
275
:-(
[];
276 _ ->
277
:-(
HostFilter = {equal, <<"server">>, LServer},
278
:-(
lists:sort([HostFilter|Filters])
279 end.
280
281 only_tuples(List) ->
282
:-(
[X || X <- List, is_tuple(X)].
283
284 filter_field(Var, Val) when is_binary(Val) and (Val /= <<"">>) ->
285
:-(
case xmpp_field_to_column(Var) of
286 false ->
287
:-(
false;
288 Field ->
289
:-(
Type = value_type(Val),
290
:-(
LVal = prepare_value(Val, Type),
291
:-(
{Type, Field, LVal}
292 end;
293 filter_field(_, _) ->
294
:-(
false.
295
296 prepare_value(Val, like) ->
297
:-(
LVal = without_last_byte(jid:str_tolower(Val)),
298
:-(
<<LVal/binary, "%">>;
299 prepare_value(Val, equal) ->
300
:-(
jid:str_tolower(Val).
301
302 value_type(Val) ->
303
:-(
case binary:last(Val) of
304 $* ->
305
:-(
like;
306 _ ->
307
:-(
equal
308 end.
309
310
:-(
limit_type_to_id(infinity) -> "inf";
311
:-(
limit_type_to_id(limit) -> "lim";
312
:-(
limit_type_to_id(top) -> "top".
313
314
:-(
type_to_id(like) -> "l";
315
:-(
type_to_id(equal) -> "e".
316
317
:-(
xmpp_field_to_column(<<"user">>) -> <<"lusername">>;
318
:-(
xmpp_field_to_column(<<"fn">>) -> <<"lfn">>;
319
:-(
xmpp_field_to_column(<<"last">>) -> <<"lfamily">>;
320
:-(
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
:-(
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
:-(
column_to_id(<<"server">>) -> "s";
332
:-(
column_to_id(<<"lusername">>) -> "u";
333
:-(
column_to_id(<<"lfn">>) -> "f";
334
:-(
column_to_id(<<"lfamily">>) -> "l";
335
:-(
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
:-(
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
:-(
[<<"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
:-(
[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
:-(
binary:part(Bin, 0, byte_size(Bin)-1).
375
376 record_to_items(Records) ->
377
:-(
[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
:-(
#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