• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 1212

18 Nov 2025 12:37PM UTC coverage: 33.784% (+0.003%) from 33.781%
1212

push

github

badlop
mod_conversejs: Improve link to conversejs in WebAdmin (#4495)

Until now, the WebAdmin menu included a link to the first request handler
with mod_conversejs that the admin configured in ejabberd.yml
That link included the authentication credentials hashed as URI arguments
if using HTTPS. Then process/2 extracted those arguments and passed them
as autologin options to Converse.

From now, mod_conversejs automatically adds a request_handler nested in
webadmin subpath. The webadmin menu links to that converse URI; this allows
to access the HTTP auth credentials, no need to explicitly pass them.
process/2 extracts this HTTP auth and passes autologin options to Converse.
Now scram password storage is supported too.

This minimum configuration allows WebAdmin to access Converse:

listen:
  -
    port: 5443
    module: ejabberd_http
    tls: true
    request_handlers:
      /admin: ejabberd_web_admin
      /ws: ejabberd_http_ws
modules:
  mod_conversejs:
    conversejs_resources: "/home/conversejs/12.0.0/dist"

0 of 12 new or added lines in 1 file covered. (0.0%)

11290 existing lines in 174 files now uncovered.

15515 of 45924 relevant lines covered (33.78%)

1277.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.78
/src/mod_mam_sql.erl
1
%%%-------------------------------------------------------------------
2
%%% File    : mod_mam_sql.erl
3
%%% Author  : Evgeny Khramtsov <ekhramtsov@process-one.net>
4
%%% Created : 15 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
5
%%%
6
%%%
7
%%% ejabberd, Copyright (C) 2002-2025   ProcessOne
8
%%%
9
%%% This program is free software; you can redistribute it and/or
10
%%% modify it under the terms of the GNU General Public License as
11
%%% published by the Free Software Foundation; either version 2 of the
12
%%% License, or (at your option) any later version.
13
%%%
14
%%% This program is distributed in the hope that it will be useful,
15
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
16
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17
%%% General Public License for more details.
18
%%%
19
%%% You should have received a copy of the GNU General Public License along
20
%%% with this program; if not, write to the Free Software Foundation, Inc.,
21
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
%%%
23
%%%----------------------------------------------------------------------
24

25
-module(mod_mam_sql).
26

27

28
-behaviour(mod_mam).
29

30
%% API
31
-export([init/2, remove_user/2, remove_room/3, delete_old_messages/3,
32
         extended_fields/1, store/10, write_prefs/4, get_prefs/2, select/7, export/1, remove_from_archive/3,
33
         is_empty_for_user/2, is_empty_for_room/3, select_with_mucsub/6,
34
         delete_old_messages_batch/4, count_messages_to_delete/3]).
35
-export([sql_schemas/0]).
36

37
-include_lib("stdlib/include/ms_transform.hrl").
38
-include_lib("xmpp/include/xmpp.hrl").
39
-include("mod_mam.hrl").
40
-include("logger.hrl").
41
-include("ejabberd_sql_pt.hrl").
42
-include("mod_muc_room.hrl").
43

44
%%%===================================================================
45
%%% API
46
%%%===================================================================
47
init(Host, _Opts) ->
UNCOV
48
    ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()),
7✔
UNCOV
49
    ok.
7✔
50

51
sql_schemas() ->
UNCOV
52
    [#sql_schema{
7✔
53
        version = 2,
54
        tables =
55
            [#sql_table{
56
                name = <<"archive">>,
57
                columns =
58
                    [#sql_column{name = <<"username">>, type = text},
59
                     #sql_column{name = <<"server_host">>, type = text},
60
                     #sql_column{name = <<"timestamp">>, type = bigint},
61
                     #sql_column{name = <<"peer">>, type = text},
62
                     #sql_column{name = <<"bare_peer">>, type = text},
63
                     #sql_column{name = <<"xml">>, type = {text, big}},
64
                     #sql_column{name = <<"txt">>, type = {text, big}},
65
                     #sql_column{name = <<"id">>, type = bigserial},
66
                     #sql_column{name = <<"kind">>, type = {text, 10}},
67
                     #sql_column{name = <<"nick">>, type = text},
68
                     #sql_column{name = <<"origin_id">>, type = text},
69
                     #sql_column{name = <<"created_at">>, type = timestamp,
70
                                 default = true}],
71
                indices = [#sql_index{
72
                              columns = [<<"server_host">>, <<"username">>, <<"timestamp">>]},
73
                           #sql_index{
74
                              columns = [<<"server_host">>, <<"username">>, <<"peer">>]},
75
                           #sql_index{
76
                              columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>]},
77
                           #sql_index{
78
                              columns = [<<"server_host">>, <<"timestamp">>]},
79
                           #sql_index{
80
                              columns = [<<"server_host">>, <<"username">>, <<"origin_id">>]}
81
                          ],
82
                post_create =
83
                    fun(#sql_schema_info{db_type = mysql}) ->
UNCOV
84
                            [<<"CREATE FULLTEXT INDEX i_archive_txt ON archive(txt);">>];
2✔
85
                       (_) ->
UNCOV
86
                            []
2✔
87
                    end},
88
             #sql_table{
89
                name = <<"archive_prefs">>,
90
                columns =
91
                    [#sql_column{name = <<"username">>, type = text},
92
                     #sql_column{name = <<"server_host">>, type = text},
93
                     #sql_column{name = <<"def">>, type = text},
94
                     #sql_column{name = <<"always">>, type = text},
95
                     #sql_column{name = <<"never">>, type = text},
96
                     #sql_column{name = <<"created_at">>, type = timestamp,
97
                                 default = true}],
98
                indices = [#sql_index{
99
                              columns = [<<"server_host">>, <<"username">>],
100
                              unique = true}]}],
101
        update =
102
            [{add_column, <<"archive">>, <<"origin_id">>},
103
             {create_index, <<"archive">>,
104
              [<<"server_host">>, <<"username">>, <<"origin_id">>]}
105
            ]},
106
     #sql_schema{
107
        version = 1,
108
        tables =
109
            [#sql_table{
110
                name = <<"archive">>,
111
                columns =
112
                    [#sql_column{name = <<"username">>, type = text},
113
                     #sql_column{name = <<"server_host">>, type = text},
114
                     #sql_column{name = <<"timestamp">>, type = bigint},
115
                     #sql_column{name = <<"peer">>, type = text},
116
                     #sql_column{name = <<"bare_peer">>, type = text},
117
                     #sql_column{name = <<"xml">>, type = {text, big}},
118
                     #sql_column{name = <<"txt">>, type = {text, big}},
119
                     #sql_column{name = <<"id">>, type = bigserial},
120
                     #sql_column{name = <<"kind">>, type = {text, 10}},
121
                     #sql_column{name = <<"nick">>, type = text},
122
                     #sql_column{name = <<"created_at">>, type = timestamp,
123
                                 default = true}],
124
                indices = [#sql_index{
125
                              columns = [<<"server_host">>, <<"username">>, <<"timestamp">>]},
126
                           #sql_index{
127
                              columns = [<<"server_host">>, <<"username">>, <<"peer">>]},
128
                           #sql_index{
129
                              columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>]},
130
                           #sql_index{
131
                              columns = [<<"server_host">>, <<"timestamp">>]}
132
                          ],
133
                post_create =
134
                    fun(#sql_schema_info{db_type = mysql}) ->
135
                            ejabberd_sql:sql_query_t(
×
136
                              <<"CREATE FULLTEXT INDEX i_archive_txt ON archive(txt);">>);
137
                       (_) ->
138
                            ok
×
139
                    end},
140
             #sql_table{
141
                name = <<"archive_prefs">>,
142
                columns =
143
                    [#sql_column{name = <<"username">>, type = text},
144
                     #sql_column{name = <<"server_host">>, type = text},
145
                     #sql_column{name = <<"def">>, type = text},
146
                     #sql_column{name = <<"always">>, type = text},
147
                     #sql_column{name = <<"never">>, type = text},
148
                     #sql_column{name = <<"created_at">>, type = timestamp,
149
                                 default = true}],
150
                indices = [#sql_index{
151
                              columns = [<<"server_host">>, <<"username">>],
152
                              unique = true}]}]}].
153

154
remove_user(LUser, LServer) ->
UNCOV
155
    ejabberd_sql:sql_query(
840✔
156
      LServer,
UNCOV
157
      ?SQL("delete from archive where username=%(LUser)s and %(LServer)H")),
840✔
UNCOV
158
    ejabberd_sql:sql_query(
840✔
159
      LServer,
UNCOV
160
      ?SQL("delete from archive_prefs where username=%(LUser)s and %(LServer)H")).
840✔
161

162
remove_room(LServer, LName, LHost) ->
UNCOV
163
    LUser = jid:encode({LName, LHost, <<>>}),
630✔
UNCOV
164
    remove_user(LUser, LServer).
630✔
165

166
remove_from_archive({LUser, LHost}, LServer, Key) ->
167
    remove_from_archive(jid:encode({LUser, LHost, <<>>}), LServer, Key);
×
168
remove_from_archive(LUser, LServer, none) ->
169
    case ejabberd_sql:sql_query(LServer,
×
170
                                ?SQL("delete from archive where username=%(LUser)s and %(LServer)H")) of
×
171
        {error, Reason} -> {error, Reason};
×
172
        _ -> ok
×
173
    end;
174
remove_from_archive(LUser, LServer, #jid{} = WithJid) ->
175
    Peer = jid:encode(jid:remove_resource(WithJid)),
×
176
    case ejabberd_sql:sql_query(LServer,
×
177
                                ?SQL("delete from archive where username=%(LUser)s and %(LServer)H and bare_peer=%(Peer)s")) of
×
178
        {error, Reason} -> {error, Reason};
×
179
        _ -> ok
×
180
    end;
181
remove_from_archive(LUser, LServer, StanzaId) ->
182
    case ejabberd_sql:sql_query(LServer,
×
183
                                ?SQL("delete from archive where username=%(LUser)s and %(LServer)H and timestamp=%(StanzaId)d")) of
×
184
        {error, Reason} -> {error, Reason};
×
185
        _ -> ok
×
186
    end.
187

188
count_messages_to_delete(ServerHost, TimeStamp, Type) ->
189
    TS = misc:now_to_usec(TimeStamp),
×
190
    Res =
×
191
    case Type of
192
        all ->
193
            ejabberd_sql:sql_query(
×
194
                ServerHost,
195
                ?SQL("select count(*) from archive"
×
196
                     " where timestamp < %(TS)d and %(ServerHost)H"));
197
        _ ->
198
            SType = misc:atom_to_binary(Type),
×
199
            ejabberd_sql:sql_query(
×
200
                ServerHost,
201
                ?SQL("select @(count(*))d from archive"
×
202
                     " where timestamp < %(TS)d"
203
                     " and kind=%(SType)s"
204
                     " and %(ServerHost)H"))
205
    end,
206
    case Res of
×
207
        {selected, [Count]} ->
208
            {ok, Count};
×
209
        _ ->
210
            error
×
211
    end.
212

213
delete_old_messages_batch(ServerHost, TimeStamp, Type, Batch) ->
214
    TS = misc:now_to_usec(TimeStamp),
×
215
    Res =
×
216
        case Type of
217
            all ->
218
                ejabberd_sql:sql_query(
×
219
                    ServerHost,
220
                    fun(sqlite, _) ->
221
                        ejabberd_sql:sql_query_t(
×
222
                            ?SQL("delete from archive where rowid in "
×
223
                                 "(select rowid from archive where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d)"));
224
                       (mssql, _) ->
225
                           ejabberd_sql:sql_query_t(
×
226
                               ?SQL("delete top(%(Batch)d)§ from archive"
×
227
                                    " where timestamp < %(TS)d and %(ServerHost)H"));
228
                       (_, _) ->
229
                           ejabberd_sql:sql_query_t(
×
230
                               ?SQL("delete from archive"
×
231
                                    " where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d"))
232
                    end);
233
            _ ->
234
                SType = misc:atom_to_binary(Type),
×
235
                ejabberd_sql:sql_query(
×
236
                    ServerHost,
237
                    fun(sqlite,_)->
238
                        ejabberd_sql:sql_query_t(
×
239
                            ?SQL("delete from archive where rowid in ("
×
240
                                 " select rowid from archive where timestamp < %(TS)d"
241
                                 " and kind=%(SType)s"
242
                                 " and %(ServerHost)H limit %(Batch)d)"));
243
                       (mssql, _)->
244
                           ejabberd_sql:sql_query_t(
×
245
                               ?SQL("delete top(%(Batch)d) from archive"
×
246
                                    " where timestamp < %(TS)d"
247
                                    " and kind=%(SType)s"
248
                                    " and %(ServerHost)H"));
249
                       (_,_)->
250
                           ejabberd_sql:sql_query_t(
×
251
                               ?SQL("delete from archive"
×
252
                                    " where timestamp < %(TS)d"
253
                                    " and kind=%(SType)s"
254
                                    " and %(ServerHost)H limit %(Batch)d"))
255
                    end)
256
        end,
257
    case Res of
×
258
        {updated, Count} ->
259
            {ok, Count};
×
260
        {error, _} = Error ->
261
            Error
×
262
    end.
263

264
delete_old_messages(ServerHost, TimeStamp, Type) ->
265
    TS = misc:now_to_usec(TimeStamp),
×
266
    case Type of
×
267
        all ->
268
            ejabberd_sql:sql_query(
×
269
              ServerHost,
270
              ?SQL("delete from archive"
×
271
                   " where timestamp < %(TS)d and %(ServerHost)H"));
272
        _ ->
273
            SType = misc:atom_to_binary(Type),
×
274
            ejabberd_sql:sql_query(
×
275
              ServerHost,
276
              ?SQL("delete from archive"
×
277
                   " where timestamp < %(TS)d"
278
                   " and kind=%(SType)s"
279
                   " and %(ServerHost)H"))
280
    end,
281
    ok.
×
282

283
extended_fields(LServer) ->
UNCOV
284
    case ejabberd_option:sql_type(LServer) of
42✔
285
        mysql ->
UNCOV
286
            [{withtext, <<"">>},
12✔
287
             #xdata_field{var = <<"{urn:xmpp:fulltext:0}fulltext">>,
288
                          type = 'text-single',
289
                          label = <<"Search the text">>,
290
                          values = []}];
291
        _ ->
UNCOV
292
            []
30✔
293
    end.
294

295
store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS,
296
      OriginID, Retract) ->
UNCOV
297
    SUser = case Type of
1,918✔
UNCOV
298
                chat -> LUser;
1,764✔
UNCOV
299
                groupchat -> jid:encode({LUser, LHost, <<>>})
154✔
300
            end,
UNCOV
301
    BarePeer = jid:encode(
1,918✔
302
                 jid:tolower(
303
                   jid:remove_resource(Peer))),
UNCOV
304
    LPeer = jid:encode(
1,918✔
305
              jid:tolower(Peer)),
UNCOV
306
    Body = fxml:get_subtag_cdata(Pkt, <<"body">>),
1,918✔
UNCOV
307
    SType = misc:atom_to_binary(Type),
1,918✔
UNCOV
308
    SqlType = ejabberd_option:sql_type(LServer),
1,918✔
UNCOV
309
    XML = case mod_mam_opt:compress_xml(LServer) of
1,918✔
310
              true ->
311
                  J1 = case Type of
×
312
                              chat -> jid:encode({LUser, LHost, <<>>});
×
313
                              groupchat -> SUser
×
314
                          end,
315
                  xml_compress:encode(Pkt, J1, LPeer);
×
316
              _ ->
UNCOV
317
                  fxml:element_to_binary(Pkt)
1,918✔
318
          end,
UNCOV
319
    case Retract of
1,918✔
320
        {true, RID} ->
UNCOV
321
            ejabberd_sql:sql_query(
77✔
322
              LServer,
UNCOV
323
              ?SQL("delete from archive"
77✔
324
                   " where username=%(SUser)s"
325
                   " and %(LServer)H"
326
                   " and bare_peer=%(BarePeer)s"
327
                   " and origin_id=%(RID)s"));
UNCOV
328
        false -> ok
1,841✔
329
    end,
UNCOV
330
    case SqlType of
1,918✔
331
        mssql -> case ejabberd_sql:sql_query(
×
332
                   LServer,
333
                   ?SQL_INSERT(
×
334
                      "archive",
335
                      ["username=%(SUser)s",
336
                       "server_host=%(LServer)s",
337
                       "timestamp=%(TS)d",
338
                       "peer=%(LPeer)s",
339
                       "bare_peer=%(BarePeer)s",
340
                       "xml=N%(XML)s",
341
                       "txt=N%(Body)s",
342
                       "kind=%(SType)s",
343
                       "nick=%(Nick)s",
344
                       "origin_id=%(OriginID)s"])) of
345
                {updated, _} ->
346
                    ok;
×
347
                Err ->
348
                    Err
×
349
            end;
UNCOV
350
        _ -> case ejabberd_sql:sql_query(
1,918✔
351
                   LServer,
UNCOV
352
                   ?SQL_INSERT(
1,918✔
353
                      "archive",
354
                      ["username=%(SUser)s",
355
                       "server_host=%(LServer)s",
356
                       "timestamp=%(TS)d",
357
                       "peer=%(LPeer)s",
358
                       "bare_peer=%(BarePeer)s",
359
                       "xml=%(XML)s",
360
                       "txt=%(Body)s",
361
                       "kind=%(SType)s",
362
                       "nick=%(Nick)s",
363
                       "origin_id=%(OriginID)s"])) of
364
                {updated, _} ->
UNCOV
365
                    ok;
1,918✔
366
                Err ->
367
                    Err
×
368
            end
369
    end.
370

371
write_prefs(LUser, _LServer, #archive_prefs{default = Default,
372
                                           never = Never,
373
                                           always = Always},
374
            ServerHost) ->
UNCOV
375
    SDefault = erlang:atom_to_binary(Default, utf8),
1,127✔
UNCOV
376
    SAlways = misc:term_to_expr(Always),
1,127✔
UNCOV
377
    SNever = misc:term_to_expr(Never),
1,127✔
UNCOV
378
    case ?SQL_UPSERT(
1,127✔
UNCOV
379
            ServerHost,
1,127✔
380
            "archive_prefs",
381
            ["!username=%(LUser)s",
382
             "!server_host=%(ServerHost)s",
383
             "def=%(SDefault)s",
384
             "always=%(SAlways)s",
385
             "never=%(SNever)s"]) of
386
        ok ->
UNCOV
387
            ok;
1,127✔
388
        Err ->
389
            Err
×
390
    end.
391

392
get_prefs(LUser, LServer) ->
UNCOV
393
    case ejabberd_sql:sql_query(
840✔
394
           LServer,
UNCOV
395
           ?SQL("select @(def)s, @(always)s, @(never)s from archive_prefs"
840✔
396
                " where username=%(LUser)s and %(LServer)H")) of
397
        {selected, [{SDefault, SAlways, SNever}]} ->
UNCOV
398
            Default = erlang:binary_to_existing_atom(SDefault, utf8),
791✔
UNCOV
399
            Always = ejabberd_sql:decode_term(SAlways),
791✔
UNCOV
400
            Never = ejabberd_sql:decode_term(SNever),
791✔
UNCOV
401
            {ok, #archive_prefs{us = {LUser, LServer},
791✔
402
                    default = Default,
403
                    always = Always,
404
                    never = Never}};
405
        _ ->
UNCOV
406
            error
49✔
407
    end.
408

409
select(LServer, JidRequestor, #jid{luser = LUser} = JidArchive,
410
       MAMQuery, RSM, MsgType, Flags) ->
UNCOV
411
    User = case MsgType of
1,372✔
UNCOV
412
               chat -> LUser;
1,344✔
UNCOV
413
               _ -> jid:encode(JidArchive)
28✔
414
           end,
UNCOV
415
    {Query, CountQuery} = make_sql_query(User, LServer, MAMQuery, RSM, none),
1,372✔
UNCOV
416
    do_select_query(LServer, JidRequestor, JidArchive, RSM, MsgType, Query, CountQuery, Flags).
1,372✔
417

418
-spec select_with_mucsub(binary(), jid(), jid(), mam_query:result(),
419
                             #rsm_set{} | undefined, all | only_count | only_messages) ->
420
                                {[{binary(), non_neg_integer(), xmlel()}], boolean(), non_neg_integer()} |
421
                                {error, db_failure}.
422
select_with_mucsub(LServer, JidRequestor, #jid{luser = LUser} = JidArchive,
423
                   MAMQuery, RSM, Flags) ->
UNCOV
424
    Extra = case gen_mod:db_mod(LServer, mod_muc) of
21✔
425
                mod_muc_sql ->
UNCOV
426
                    subscribers_table;
21✔
427
                _ ->
428
                    SubRooms = case mod_muc_admin:find_hosts(LServer) of
×
429
                                   [First|_] ->
430
                                       case mod_muc:get_subscribed_rooms(First, JidRequestor) of
×
431
                                           {ok, L} -> L;
×
432
                                           {error, _} -> []
×
433
                                       end;
434
                                   _ ->
435
                                       []
×
436
                               end,
437
                    [jid:encode(Jid) || {Jid, _, _} <- SubRooms]
×
438
            end,
UNCOV
439
    {Query, CountQuery} = make_sql_query(LUser, LServer, MAMQuery, RSM, Extra),
21✔
UNCOV
440
    do_select_query(LServer, JidRequestor, JidArchive, RSM, chat, Query, CountQuery, Flags).
21✔
441

442
do_select_query(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, RSM,
443
                MsgType, Query, CountQuery, Flags) ->
444
    % TODO from XEP-0313 v0.2: "To conserve resources, a server MAY place a
445
    % reasonable limit on how many stanzas may be pushed to a client in one
446
    % request. If a query returns a number of stanzas greater than this limit
447
    % and the client did not specify a limit using RSM then the server should
448
    % return a policy-violation error to the client." We currently don't do this
449
    % for v0.2 requests, but we do limit #rsm_in.max for v0.3 and newer.
UNCOV
450
    QRes = case Flags of
1,393✔
451
                   all ->
UNCOV
452
                       {ejabberd_sql:sql_query(LServer, Query), ejabberd_sql:sql_query(LServer, CountQuery)};
1,337✔
453
                   only_messages ->
UNCOV
454
                       {ejabberd_sql:sql_query(LServer, Query), {selected, ok, [[<<"0">>]]}};
14✔
455
                   only_count ->
UNCOV
456
                       {{selected, ok, []}, ejabberd_sql:sql_query(LServer, CountQuery)}
42✔
457
               end,
UNCOV
458
    case QRes of
1,393✔
459
        {{selected, _, Res}, {selected, _, [[Count]]}} ->
UNCOV
460
            {Max, Direction, _} = get_max_direction_id(RSM),
1,393✔
UNCOV
461
            {Res1, IsComplete} =
1,393✔
462
            if Max >= 0 andalso Max /= undefined andalso length(Res) > Max ->
UNCOV
463
                if Direction == before ->
336✔
UNCOV
464
                    {lists:nthtail(1, Res), false};
56✔
465
                    true ->
UNCOV
466
                        {lists:sublist(Res, Max), false}
280✔
467
                end;
468
                true ->
UNCOV
469
                    {Res, true}
1,057✔
470
            end,
UNCOV
471
            MucState = #state{config = #config{anonymous = true}},
1,393✔
UNCOV
472
            JidArchiveS = jid:encode(jid:remove_resource(JidArchive)),
1,393✔
UNCOV
473
            {lists:flatmap(
1,393✔
474
                fun([TS, XML, PeerBin, Kind, Nick]) ->
UNCOV
475
                    case make_archive_el(JidArchiveS, TS, XML, PeerBin, Kind, Nick,
5,124✔
476
                                         MsgType, JidRequestor, JidArchive) of
477
                        {ok, El} ->
UNCOV
478
                            [{TS, binary_to_integer(TS), El}];
5,124✔
479
                        {error, _} ->
480
                            []
×
481
                    end;
482
                   ([User, TS, XML, PeerBin, Kind, Nick]) when User == LUser ->
483
                       case make_archive_el(JidArchiveS, TS, XML, PeerBin, Kind, Nick,
×
484
                                            MsgType, JidRequestor, JidArchive) of
485
                           {ok, El} ->
486
                               [{TS, binary_to_integer(TS), El}];
×
487
                           {error, _} ->
488
                               []
×
489
                       end;
490
                   ([User, TS, XML, PeerBin, Kind, Nick]) ->
UNCOV
491
                       case make_archive_el(User, TS, XML, PeerBin, Kind, Nick,
77✔
492
                                            {groupchat, member, MucState}, JidRequestor,
493
                                            jid:decode(User)) of
494
                           {ok, El} ->
UNCOV
495
                               mod_mam:wrap_as_mucsub([{TS, binary_to_integer(TS), El}],
77✔
496
                                                      JidRequestor);
497
                           {error, _} ->
498
                               []
×
499
                       end
500
                end, Res1), IsComplete, binary_to_integer(Count)};
501
        _ ->
502
            {[], false, 0}
×
503
    end.
504

505
export(_Server) ->
506
    [{archive_prefs,
×
507
      fun(Host, #archive_prefs{us =
508
                {LUser, LServer},
509
                default = Default,
510
                always = Always,
511
                never = Never})
512
          when LServer == Host ->
513
                SDefault = erlang:atom_to_binary(Default, utf8),
×
514
                SAlways = misc:term_to_expr(Always),
×
515
                SNever = misc:term_to_expr(Never),
×
516
                [?SQL_INSERT(
×
517
                    "archive_prefs",
518
                    ["username=%(LUser)s",
519
                     "server_host=%(LServer)s",
520
                     "def=%(SDefault)s",
521
                     "always=%(SAlways)s",
522
                     "never=%(SNever)s"])];
523
          (_Host, _R) ->
524
              []
×
525
      end},
526
     {archive_msg,
527
      fun([Host | HostTail], #archive_msg{us ={LUser, LServer},
528
                id = _ID, timestamp = TS, peer = Peer,
529
                type = Type, nick = Nick, packet = Pkt, origin_id = OriginID})
530
          when (LServer == Host) or ([LServer] == HostTail)  ->
531
                TStmp = misc:now_to_usec(TS),
×
532
                SUser = case Type of
×
533
                      chat -> LUser;
×
534
                      groupchat -> jid:encode({LUser, LServer, <<>>})
×
535
                    end,
536
                BarePeer = jid:encode(jid:tolower(jid:remove_resource(Peer))),
×
537
                LPeer = jid:encode(jid:tolower(Peer)),
×
538
                XML = fxml:element_to_binary(Pkt),
×
539
                Body = fxml:get_subtag_cdata(Pkt, <<"body">>),
×
540
                SType = misc:atom_to_binary(Type),
×
541
                SqlType = ejabberd_option:sql_type(Host),
×
542
                case SqlType of
×
543
                        mssql -> [?SQL_INSERT(
×
544
                            "archive",
545
                            ["username=%(SUser)s",
546
                             "server_host=%(LServer)s",
547
                             "timestamp=%(TStmp)d",
548
                             "peer=%(LPeer)s",
549
                             "bare_peer=%(BarePeer)s",
550
                             "xml=N%(XML)s",
551
                             "txt=N%(Body)s",
552
                             "kind=%(SType)s",
553
                             "nick=%(Nick)s",
554
                             "origin_id=%(OriginID)s"])];
555
                        _ -> [?SQL_INSERT(
×
556
                            "archive",
557
                            ["username=%(SUser)s",
558
                             "server_host=%(LServer)s",
559
                             "timestamp=%(TStmp)d",
560
                             "peer=%(LPeer)s",
561
                             "bare_peer=%(BarePeer)s",
562
                             "xml=%(XML)s",
563
                             "txt=%(Body)s",
564
                             "kind=%(SType)s",
565
                             "nick=%(Nick)s",
566
                             "origin_id=%(OriginID)s"])]
567
                            end;
568
         (_Host, _R) ->
569
              []
×
570
      end}].
571

572
is_empty_for_user(LUser, LServer) ->
573
    case ejabberd_sql:sql_query(
×
574
           LServer,
575
           ?SQL("select @(1)d from archive"
×
576
                " where username=%(LUser)s and %(LServer)H limit 1")) of
577
        {selected, [{1}]} ->
578
            false;
×
579
        _ ->
580
            true
×
581
    end.
582

583
is_empty_for_room(LServer, LName, LHost) ->
584
    LUser = jid:encode({LName, LHost, <<>>}),
×
585
    is_empty_for_user(LUser, LServer).
×
586

587
%%%===================================================================
588
%%% Internal functions
589
%%%===================================================================
590
make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) ->
UNCOV
591
    Start = proplists:get_value(start, MAMQuery),
1,393✔
UNCOV
592
    End = proplists:get_value('end', MAMQuery),
1,393✔
UNCOV
593
    With = proplists:get_value(with, MAMQuery),
1,393✔
UNCOV
594
    WithText = proplists:get_value(withtext, MAMQuery),
1,393✔
UNCOV
595
    {Max, Direction, ID} = get_max_direction_id(RSM),
1,393✔
UNCOV
596
    ODBCType = ejabberd_option:sql_type(LServer),
1,393✔
UNCOV
597
    ToString = fun(S) -> ejabberd_sql:to_string_literal(ODBCType, S) end,
1,393✔
UNCOV
598
    LimitClause = if is_integer(Max), Max >= 0, ODBCType /= mssql ->
1,393✔
UNCOV
599
                          [<<" limit ">>, integer_to_binary(Max+1)];
1,127✔
600
                     true ->
UNCOV
601
                          []
266✔
602
                  end,
UNCOV
603
    TopClause = if is_integer(Max), Max >= 0, ODBCType == mssql ->
1,393✔
604
                          [<<" TOP ">>, integer_to_binary(Max+1)];
×
605
                     true ->
UNCOV
606
                          []
1,393✔
607
                  end,
UNCOV
608
    SubOrderClause = if LimitClause /= []; TopClause /= [] ->
1,393✔
UNCOV
609
                          <<" ORDER BY timestamp DESC ">>;
1,127✔
610
                     true ->
UNCOV
611
                          []
266✔
612
                  end,
UNCOV
613
    WithTextClause = if is_binary(WithText), WithText /= <<>> ->
1,393✔
614
                             [<<" and match (txt) against (">>,
×
615
                              ToString(WithText), <<")">>];
616
                        true ->
UNCOV
617
                             []
1,393✔
618
                     end,
UNCOV
619
    WithClause = case catch jid:tolower(With) of
1,393✔
620
                     {_, _, <<>>} ->
UNCOV
621
                         [<<" and bare_peer=">>,
56✔
622
                          ToString(jid:encode(With))];
623
                     {_, _, _} ->
UNCOV
624
                         [<<" and peer=">>,
56✔
625
                          ToString(jid:encode(With))];
626
                     _ ->
UNCOV
627
                         []
1,281✔
628
                 end,
UNCOV
629
    PageClause = case catch binary_to_integer(ID) of
1,393✔
630
                     I when is_integer(I), I >= 0 ->
UNCOV
631
                         case Direction of
574✔
632
                             before ->
UNCOV
633
                                 [<<" AND timestamp < ">>, ID];
294✔
634
                             'after' ->
UNCOV
635
                                 [<<" AND timestamp > ">>, ID];
280✔
636
                             _ ->
637
                                 []
×
638
                         end;
639
                     _ ->
UNCOV
640
                         []
819✔
641
                 end,
UNCOV
642
    StartClause = case Start of
1,393✔
643
                      {_, _, _} ->
UNCOV
644
                          [<<" and timestamp >= ">>,
14✔
645
                           integer_to_binary(misc:now_to_usec(Start))];
646
                      _ ->
UNCOV
647
                          []
1,379✔
648
                  end,
UNCOV
649
    EndClause = case End of
1,393✔
650
                    {_, _, _} ->
651
                        [<<" and timestamp <= ">>,
×
652
                         integer_to_binary(misc:now_to_usec(End))];
653
                    _ ->
UNCOV
654
                        []
1,393✔
655
                end,
UNCOV
656
    SUser = ToString(User),
1,393✔
UNCOV
657
    SServer = ToString(LServer),
1,393✔
658

UNCOV
659
    HostMatch = case ejabberd_sql:use_multihost_schema() of
1,393✔
660
                    true ->
UNCOV
661
                        [<<" and server_host=", SServer/binary>>];
597✔
662
                    _ ->
UNCOV
663
                        <<"">>
796✔
664
                end,
665

UNCOV
666
    {UserSel, UserWhere} = case ExtraUsernames of
1,393✔
667
                               Users when is_list(Users) ->
668
                                   EscUsers = [ToString(U) || U <- [User | Users]],
×
669
                                   {<<" username,">>,
×
670
                                    [<<" username in (">>, str:join(EscUsers, <<",">>), <<")">>]};
671
                               subscribers_table ->
UNCOV
672
                                   SJid = ToString(jid:encode({User, LServer, <<>>})),
21✔
UNCOV
673
                                   RoomName = case ODBCType of
21✔
674
                                                  sqlite ->
UNCOV
675
                                                      <<"room || '@' || host">>;
9✔
676
                                                  _ ->
UNCOV
677
                                                      <<"concat(room, '@', host)">>
12✔
678
                                              end,
UNCOV
679
                                   {<<" username,">>,
21✔
680
                                    [<<" (username = ">>, SUser,
681
                                        <<" or username in (select ">>, RoomName,
682
                                          <<" from muc_room_subscribers where jid=">>, SJid, HostMatch, <<"))">>]};
683
                               _ ->
UNCOV
684
                                   {<<>>, [<<" username=">>, SUser]}
1,372✔
685
                           end,
686

UNCOV
687
    Query = [<<"SELECT ">>, TopClause, UserSel,
1,393✔
688
             <<" timestamp, xml, peer, kind, nick"
689
               " FROM archive WHERE">>, UserWhere, HostMatch,
690
             WithClause, WithTextClause,
691
             StartClause, EndClause, PageClause],
692

UNCOV
693
    QueryPage =
1,393✔
694
        case Direction of
695
            before ->
696
                % ID can be empty because of
697
                % XEP-0059: Result Set Management
698
                % 2.5 Requesting the Last Page in a Result Set
UNCOV
699
                [<<"SELECT">>, UserSel, <<" timestamp, xml, peer, kind, nick FROM (">>,
406✔
700
                 Query, SubOrderClause,
701
                 LimitClause, <<") AS t ORDER BY timestamp ASC;">>];
702
            _ ->
UNCOV
703
                [Query, <<" ORDER BY timestamp ASC ">>,
987✔
704
                 LimitClause, <<";">>]
705
        end,
UNCOV
706
    {QueryPage,
1,393✔
707
     [<<"SELECT COUNT(*) FROM archive WHERE ">>, UserWhere,
708
      HostMatch, WithClause, WithTextClause,
709
      StartClause, EndClause, <<";">>]}.
710

711
-spec get_max_direction_id(rsm_set() | undefined) ->
712
                                  {integer() | undefined,
713
                                   before | 'after' | undefined,
714
                                   binary()}.
715
get_max_direction_id(RSM) ->
UNCOV
716
    case RSM of
2,786✔
717
        #rsm_set{max = Max, before = Before} when is_binary(Before) ->
UNCOV
718
            {Max, before, Before};
812✔
719
        #rsm_set{max = Max, 'after' = After} when is_binary(After) ->
UNCOV
720
            {Max, 'after', After};
560✔
721
        #rsm_set{max = Max} ->
UNCOV
722
            {Max, undefined, <<>>};
1,330✔
723
        _ ->
UNCOV
724
            {undefined, undefined, <<>>}
84✔
725
    end.
726

727
-spec make_archive_el(binary(), binary(), binary(), binary(), binary(),
728
                      binary(), _, jid(), jid()) ->
729
                             {ok, xmpp_element()} | {error, invalid_jid |
730
                                                     invalid_timestamp |
731
                                                     invalid_xml}.
732
make_archive_el(User, TS, XML, Peer, Kind, Nick, MsgType, JidRequestor, JidArchive) ->
UNCOV
733
    case xml_compress:decode(XML, User, Peer) of
5,201✔
734
        #xmlel{} = El ->
UNCOV
735
            try binary_to_integer(TS) of
5,201✔
736
                TSInt ->
UNCOV
737
                    try jid:decode(Peer) of
5,201✔
738
                        PeerJID ->
UNCOV
739
                            Now = misc:usec_to_now(TSInt),
5,201✔
UNCOV
740
                            PeerLJID = jid:tolower(PeerJID),
5,201✔
UNCOV
741
                            T = case Kind of
5,201✔
742
                                    <<"">> -> chat;
×
743
                                    null -> chat;
×
UNCOV
744
                                    _ -> misc:binary_to_atom(Kind)
5,201✔
745
                                end,
UNCOV
746
                            mod_mam:msg_to_el(
5,201✔
747
                              #archive_msg{timestamp = Now,
748
                                           id = TS,
749
                                           packet = El,
750
                                           type = T,
751
                                           nick = Nick,
752
                                           peer = PeerLJID},
753
                              MsgType, JidRequestor, JidArchive)
754
                    catch _:{bad_jid, _} ->
755
                            ?ERROR_MSG("Malformed 'peer' field with value "
×
756
                                       "'~ts' detected for user ~ts in table "
757
                                       "'archive': invalid JID",
758
                                       [Peer, jid:encode(JidArchive)]),
×
759
                            {error, invalid_jid}
×
760
                    end
761
            catch _:_ ->
762
                    ?ERROR_MSG("Malformed 'timestamp' field with value '~ts' "
×
763
                               "detected for user ~ts in table 'archive': "
764
                               "not an integer",
765
                               [TS, jid:encode(JidArchive)]),
×
766
                    {error, invalid_timestamp}
×
767
            end;
768
        {error, {_, Reason}} ->
769
            ?ERROR_MSG("Malformed 'xml' field with value '~ts' detected "
×
770
                       "for user ~ts in table 'archive': ~ts",
771
                       [XML, jid:encode(JidArchive), Reason]),
×
772
            {error, invalid_xml}
×
773
    end.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc