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

esl / MongooseIM / 16499779126

24 Jul 2025 02:03PM UTC coverage: 85.593% (-0.02%) from 85.614%
16499779126

Pull #4549

github

jacekwegr
Add TODO comments
Pull Request #4549: Support Erlang 28

50 of 68 new or added lines in 5 files covered. (73.53%)

289 existing lines in 7 files now uncovered.

28945 of 33817 relevant lines covered (85.59%)

51736.46 hits per line

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

77.11
/src/pubsub/mod_pubsub.erl
1
%%% ====================================================================
2
%%% ``The contents of this file are subject to the Erlang Public License,
3
%%% Version 1.1, (the "License"); you may not use this file except in
4
%%% compliance with the License. You should have received a copy of the
5
%%% Erlang Public License along with this software. If not, it can be
6
%%% retrieved via the world wide web at http://www.erlang.org/.
7
%%%
8
%%%
9
%%% Software distributed under the License is distributed on an "AS IS"
10
%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
11
%%% the License for the specific language governing rights and limitations
12
%%% under the License.
13
%%%
14
%%%
15
%%% The Initial Developer of the Original Code is ProcessOne.
16
%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne
17
%%% All Rights Reserved.''
18
%%% This software is copyright 2006-2015, ProcessOne.
19
%%%
20
%%% @copyright 2006-2015 ProcessOne
21
%%% @author Christophe Romain <christophe.romain@process-one.net>
22
%%%   [http://www.process-one.net/]
23
%%% @end
24
%%% ====================================================================
25

26
%%% @doc The module <strong>{@module}</strong> is the core of the PubSub
27
%%% extension. It relies on PubSub plugins for a large part of its functions.
28
%%%
29
%%% @headerfile "pubsub.hrl"
30
%%%
31
%%% @reference See <a href="http://www.xmpp.org/extensions/xep-0060.html">XEP-0060: Pubsub</a> for
32
%%% the latest version of the PubSub specification.
33
%%% This module uses version 1.12 of the specification as a base.
34
%%% Most of the specification is implemented.
35
%%% Functions concerning configuration should be rewritten.
36
%%%
37
%%% Support for subscription-options and multi-subscribe features was
38
%%% added by Brian Cully (bjc AT kublai.com). For information on
39
%%% subscription-options and multi-subscribe see XEP-0060 sections 6.1.6,
40
%%% 6.2.3.1, 6.2.3.5, and 6.3. For information on subscription leases see
41
%%% XEP-0060 section 12.18.
42

43
-module(mod_pubsub).
44
-behaviour(gen_mod).
45
-behaviour(gen_server).
46
-behaviour(mongoose_packet_handler).
47
-behaviour(mongoose_module_metrics).
48
-author('christophe.romain@process-one.net').
49

50
-xep([{xep, 60}, {version, "1.25.0"}]).
51
-xep([{xep, 163}, {version, "1.2.2"}]).
52
-xep([{xep, 248}, {version, "0.3.0"}]).
53
-xep([{xep, 277}, {version, "0.6.5"}]).
54

55
%% https://xmpp.org/extensions/xep-0384.html#server-side
56
-xep([{xep, 384}, {version, "0.8.3"}]).
57

58
-include("mongoose.hrl").
59
-include("adhoc.hrl").
60
-include("jlib.hrl").
61
-include("pubsub.hrl").
62
-include("mongoose_config_spec.hrl").
63
-include("session.hrl").
64

65
-define(STDTREE, nodetree_tree).
66
-define(STDNODE, <<"flat">>).
67
-define(STDNODE_MODULE, node_flat).
68
-define(PEPNODE, <<"pep">>).
69
-define(PUSHNODE, <<"push">>).
70

71
%% exports for hooks
72
-export([presence_probe/3,
73
         caps_recognised/3,
74
         in_subscription/3,
75
         out_subscription/3,
76
         on_user_offline/3,
77
         remove_user/3,
78
         disco_local_features/3,
79
         disco_sm_identity/3,
80
         disco_sm_features/3,
81
         disco_sm_items/3,
82
         handle_pep_authorization_response/3,
83
         foreign_event/3]).
84

85
%% exported iq handlers
86
-export([iq_sm/4]).
87

88
%% exports for console debug manual use
89
-export([create_node/5, create_node/7, delete_node/3,
90
         subscribe_node/5, unsubscribe_node/5, publish_item/6,
91
         delete_item/4, send_items/7, get_items/2, get_item/3,
92
         get_cached_item/2, tree_action/3, node_action/4]).
93

94
%% general helpers for plugins
95
-export([subscription_to_string/1, affiliation_to_string/1,
96
         string_to_subscription/1, string_to_affiliation/1,
97
         extended_error/2, service_jid/1, tree/1, plugin/1,
98
         plugin_call/3, serverhost/1, host_to_host_type/1,
99
         host/2]).
100

101
%% API and gen_server callbacks
102
-export([start_link/2, start/2, stop/1, deps/2, init/1,
103
         handle_call/3, handle_cast/2, handle_info/2,
104
         terminate/2, code_change/3, instrumentation/1]).
105

106
%% Config callbacks
107
-export([config_spec/0, process_pep_mapping/1]).
108

109
-export([default_host/0]).
110

111
-export([get_personal_data/3]).
112

113
%% packet handler export
114
-export([process_packet/5]).
115

116
-export([config_metrics/1]).
117

118
%% Private export for wpool worker callbacks
119
-export([handle_msg/1]).
120

121
-define(MOD_PUBSUB_DB_BACKEND, mod_pubsub_db_backend).
122
-ignore_xref([
123
    {?MOD_PUBSUB_DB_BACKEND, transaction, 2},
124
    {?MOD_PUBSUB_DB_BACKEND, get_user_nodes, 2},
125
    {?MOD_PUBSUB_DB_BACKEND, get_user_payloads, 2},
126
    {?MOD_PUBSUB_DB_BACKEND, get_user_subscriptions, 2},
127
    {?MOD_PUBSUB_DB_BACKEND, start, 0},
128
    {?MOD_PUBSUB_DB_BACKEND, set_subscription_opts, 4},
129
    {?MOD_PUBSUB_DB_BACKEND, stop, 0},
130
    affiliation_to_string/1,
131
    create_node/7,
132
    default_host/0,
133
    delete_item/4,
134
    delete_node/3,
135
    get_cached_item/2,
136
    get_item/3,
137
    get_items/2,
138
    host/2,
139
    iq_sm/4,
140
    node_action/4,
141
    node_call/4,
142
    plugin/2,
143
    plugin/1,
144
    publish_item/6,
145
    send_items/7,
146
    serverhost/1,
147
    start_link/2,
148
    string_to_affiliation/1,
149
    string_to_subscription/1,
150
    subscribe_node/5,
151
    subscription_to_string/1,
152
    tree_action/3,
153
    unsubscribe_node/5,
154
    handle_msg/1
155
]).
156

157
-type plugin_name() :: binary().
158

159
-export_type([
160
              host/0,
161
              hostPubsub/0,
162
              hostPEP/0,
163
              %%
164
              nodeIdx/0,
165
              nodeId/0,
166
              itemId/0,
167
              subId/0,
168
              payload/0,
169
              %%
170
              nodeOption/0,
171
              nodeOptions/0,
172
              subOption/0,
173
              subOptions/0,
174
              %%
175
              affiliation/0,
176
              subscription/0,
177
              accessModel/0,
178
              publishModel/0
179
             ]).
180

181
%% -type payload() defined here because the -type exml:element() is not accessible
182
%% from pubsub.hrl
183
-type(payload() :: [] | [exml:element(), ...]).
184
-type(publishOptions() :: undefined | exml:element()).
185

186
-export_type([
187
              pubsubNode/0,
188
              pubsubState/0,
189
              pubsubItem/0,
190
              pubsubLastItem/0,
191
              publishOptions/0
192
             ]).
193

194
-type(pubsubNode() ::
195
        #pubsub_node{
196
           nodeid  :: {Host::mod_pubsub:host(), Node::mod_pubsub:nodeId()},
197
           id      :: Nidx::mod_pubsub:nodeIdx(),
198
           parents :: [Node::mod_pubsub:nodeId()],
199
           type    :: Type::binary(),
200
           owners  :: [Owner::jid:ljid(), ...],
201
           options :: Opts::mod_pubsub:nodeOptions()
202
          }
203
        ).
204

205
-type(pubsubState() ::
206
        #pubsub_state{
207
           stateid       :: {Entity::jid:ljid(), Nidx::mod_pubsub:nodeIdx()},
208
           items         :: [ItemId::mod_pubsub:itemId()],
209
           affiliation   :: Affs::mod_pubsub:affiliation(),
210
           subscriptions :: [{Sub::mod_pubsub:subscription(), SubId::mod_pubsub:subId()}]
211
          }
212
        ).
213

214
-type(pubsubItem() ::
215
        #pubsub_item{
216
           itemid       :: {ItemId::mod_pubsub:itemId(), Nidx::mod_pubsub:nodeIdx()},
217
           creation     :: {integer(), jid:ljid()},
218
           modification :: {integer(), jid:ljid()},
219
           payload      :: mod_pubsub:payload()
220
          }
221
        ).
222

223
-type(pubsubLastItem() ::
224
        #pubsub_last_item{
225
           nodeid   :: mod_pubsub:nodeIdx(),
226
           itemid   :: mod_pubsub:itemId(),
227
           creation :: {integer(), jid:ljid()},
228
           payload  :: mod_pubsub:payload()
229
          }
230
        ).
231

232
-record(state,
233
        {
234
          server_host,
235
          host,
236
          access,
237
          pep_mapping             = #{},
238
          ignore_pep_from_offline = true,
239
          last_item_cache         = false,
240
          max_items_node          = ?MAXITEMS,
241
          max_subscriptions_node  = undefined,
242
          default_node_config     = [],
243
          nodetree                = ?STDTREE,
244
          plugins                 = [?STDNODE] :: [plugin_name()]
245
        }).
246

247
-type(state() ::
248
        #state{
249
           server_host             :: binary(),
250
           host                    :: mod_pubsub:hostPubsub(),
251
           access                  :: atom(),
252
           pep_mapping             :: map(),
253
           ignore_pep_from_offline :: boolean(),
254
           last_item_cache         :: mnesia | rdbms | false,
255
           max_items_node          :: non_neg_integer(),
256
           max_subscriptions_node  :: non_neg_integer()|undefined,
257
           default_node_config     :: [{atom(), binary()|boolean()|integer()|atom()}],
258
           nodetree                :: module(),
259
           plugins                 :: [binary(), ...]
260
          }
261

262
        ).
263

264
%%====================================================================
265
%% API
266
%%====================================================================
267

268
%% @doc: Starts the server.
269
-spec start_link(mongooseim:domain_name(), gen_mod:module_opts()) -> {ok, pid()} | ignore | {error, any()}.
270
start_link(Host, Opts) ->
271
    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
263✔
272
    gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
263✔
273

274
deps(_Host, _Opts) ->
275
    [{mod_caps, #{cache_size => 1000, cache_life_time => timer:hours(24) div 1000}, optional}].
1,863✔
276

277
start(Host, Opts) ->
278
    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
263✔
279
    ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]},
263✔
280
                 transient, 1000, worker, [?MODULE]},
281
    start_pool(Host, Opts),
263✔
282
    ejabberd_sup:start_child(ChildSpec).
263✔
283

284
start_pool(HostType, #{wpool := WpoolOpts}) ->
285
    {ok, _} = mongoose_wpool:start(generic, HostType, pubsub_notify, maps:to_list(WpoolOpts)).
263✔
286

287
stop(Host) ->
288
    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
263✔
289
    gen_server:call(Proc, stop),
263✔
290
    mongoose_wpool:stop(generic, Host, pubsub_notify),
263✔
291
    ejabberd_sup:stop_child(Proc).
263✔
292

293
-spec config_spec() -> mongoose_config_spec:config_section().
294
config_spec() ->
295
    #section{
15,224✔
296
       items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc(),
297
                 <<"host">> => #option{type = string,
298
                                       validate = subdomain_template,
299
                                       process = fun mongoose_subdomain_utils:make_subdomain_pattern/1},
300
                 <<"backend">> => #option{type = atom,
301
                                          validate = {module, mod_pubsub_db}},
302
                 <<"access_createnode">> => #option{type = atom,
303
                                                    validate = access_rule},
304
                 <<"max_items_node">> => #option{type = integer,
305
                                                 validate = non_negative},
306
                 <<"max_subscriptions_node">> => #option{type = integer,
307
                                                         validate = non_negative},
308
                 <<"nodetree">> => #option{type = binary,
309
                                           validate = {module, nodetree},
310
                                           process = fun tree_mod/1},
311
                 <<"ignore_pep_from_offline">> => #option{type = boolean},
312
                 <<"last_item_cache">> => #option{type = atom,
313
                                                  validate = {enum, [mnesia, rdbms, false]}},
314
                 <<"plugins">> => #list{items = #option{type = binary,
315
                                                        validate = {module, node}}},
316
                 <<"pep_mapping">> => #list{items = pep_mapping_config_spec(),
317
                                            format_items = map},
318
                 <<"default_node_config">> => default_node_config_spec(),
319
                 <<"item_publisher">> => #option{type = boolean},
320
                 <<"sync_broadcast">> => #option{type = boolean},
321
                 <<"wpool">> => wpool_spec()
322
                },
323
       defaults = #{<<"iqdisc">> => one_queue,
324
                    <<"host">> => default_host(),
325
                    <<"backend">> => mnesia,
326
                    <<"access_createnode">> => all,
327
                    <<"max_items_node">> => ?MAXITEMS,
328
                    <<"nodetree">> => ?STDTREE,
329
                    <<"ignore_pep_from_offline">> => true,
330
                    <<"last_item_cache">> => false,
331
                    <<"plugins">> => [?STDNODE],
332
                    <<"pep_mapping">> => #{},
333
                    <<"default_node_config">> => [],
334
                    <<"item_publisher">> => false,
335
                    <<"sync_broadcast">> => false}
336
      }.
337

338
wpool_spec() ->
339
    Wpool = mongoose_config_spec:wpool(#{}),
15,224✔
340
    Wpool#section{include = always}.
15,224✔
341

342
pep_mapping_config_spec() ->
343
    #section{
15,224✔
344
       items = #{<<"namespace">> => #option{type = binary,
345
                                            validate = non_empty},
346
                 <<"node">> => #option{type = binary,
347
                                       validate = non_empty}},
348
       required = all,
349
       process = fun ?MODULE:process_pep_mapping/1
350
      }.
351

352
default_node_config_spec() ->
353
    #section{
15,224✔
354
       items = #{<<"access_model">> => #option{type = atom,
355
                                               validate = non_empty},
356
                 <<"deliver_notifications">> => #option{type = boolean},
357
                 <<"deliver_payloads">> => #option{type = boolean},
358
                 <<"max_items">> => #option{type = integer,
359
                                            validate = non_negative},
360
                 <<"max_payload_size">> => #option{type = integer,
361
                                                   validate = non_negative},
362
                 <<"node_type">> => #option{type = atom,
363
                                            validate = non_empty},
364
                 <<"notification_type">> => #option{type = atom,
365
                                                    validate = non_empty},
366
                 <<"notify_config">> => #option{type = boolean},
367
                 <<"notify_delete">> => #option{type = boolean},
368
                 <<"notify_retract">> => #option{type = boolean},
369
                 <<"persist_items">> => #option{type = boolean},
370
                 <<"presence_based_delivery">> => #option{type = boolean},
371
                 <<"publish_model">> => #option{type = atom,
372
                                                validate = non_empty},
373
                 <<"purge_offline">> => #option{type = boolean},
374
                 <<"roster_groups_allowed">> => #list{items = #option{type = binary,
375
                                                                      validate = non_empty}},
376
                 <<"send_last_published_item">> => #option{type = atom,
377
                                                           validate = non_empty},
378
                 <<"subscribe">> => #option{type = boolean}
379
                },
380
       format_items = list
381
      }.
382

383
process_pep_mapping(#{namespace := NameSpace, node := Node}) ->
384
    {NameSpace, Node}.
6✔
385

386
-spec default_host() -> mongoose_subdomain_utils:subdomain_pattern().
387
default_host() ->
388
    mongoose_subdomain_utils:make_subdomain_pattern(<<"pubsub.@HOST@">>).
15,224✔
389

390
%% State is an extra data, required for processing
391
-spec process_packet(Acc :: mongoose_acc:t(), From ::jid:jid(), To ::jid:jid(), El :: exml:element(),
392
                     #{state := #state{}}) -> mongoose_acc:t().
393
process_packet(Acc, From, To, El, #{state := State}) ->
394
    #state{server_host = ServerHost, access = Access, plugins = Plugins} = State,
5,727✔
395
    do_route(Acc, ServerHost, Access, Plugins, To#jid.lserver, From, To, El).
5,727✔
396

397
%%====================================================================
398
%% GDPR callback
399
%%====================================================================
400

401
-spec get_personal_data(Acc, Params, Extra) -> {ok, Acc} when
402
    Acc :: gdpr:personal_data(),
403
    Params :: #{jid := jid:jid()},
404
    Extra :: gen_hook:extra().
405
get_personal_data(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, _) ->
406
    Payloads = mod_pubsub_db_backend:get_user_payloads(LUser, LServer),
196✔
407
    Nodes = mod_pubsub_db_backend:get_user_nodes(LUser, LServer),
196✔
408
    Subscriptions = mod_pubsub_db_backend:get_user_subscriptions(LUser, LServer),
196✔
409

410
    NewAcc = [{pubsub_payloads, ["node_name", "item_id", "payload"], Payloads},
196✔
411
              {pubsub_nodes, ["node_name", "type"], Nodes},
412
              {pubsub_subscriptions, ["node_name"], Subscriptions} | Acc],
413
    {ok, NewAcc}.
196✔
414

415
%%====================================================================
416
%% gen_server callbacks
417
%%====================================================================
418

419
-spec init([binary() | gen_mod:module_opts(), ...]) -> {'ok', state()}.
420
init([ServerHost, Opts = #{host := SubdomainPattern}]) ->
421
    ?LOG_DEBUG(#{what => pubsub_init, server => ServerHost, opts => Opts}),
263✔
422
    Host = mongoose_subdomain_utils:get_fqdn(SubdomainPattern, ServerHost),
263✔
423

424
    init_backend(ServerHost, Opts),
263✔
425
    Plugins = init_plugins(Host, ServerHost, Opts),
263✔
426

427
    gen_hook:add_handlers(hooks(ServerHost)),
263✔
428
    case lists:member(?PEPNODE, Plugins) of
263✔
429
        true ->
430
            gen_hook:add_handlers(pep_hooks(ServerHost)),
27✔
431
            add_pep_iq_handlers(ServerHost, Opts);
27✔
432
        false ->
433
            ok
236✔
434
    end,
435
    State = init_state(ServerHost, Opts, Plugins),
263✔
436

437
    %% Pass State as extra into ?MODULE:process_packet/5 function
438
    PacketHandler = mongoose_packet_handler:new(?MODULE, #{state => State}),
263✔
439
    %% TODO: Conversion of this module is not done, it doesn't support dynamic
440
    %%       domains yet. Only subdomain registration is done properly.
441
    mongoose_domain_api:register_subdomain(ServerHost, SubdomainPattern, PacketHandler),
263✔
442
    {ok, State}.
263✔
443

444
init_backend(ServerHost, Opts) ->
445
    mod_pubsub_db_backend:init(ServerHost, Opts),
263✔
446
    maybe_start_cache_module(ServerHost, Opts).
263✔
447

448
hooks(ServerHost) ->
449
    [{disco_local_features, ServerHost, fun ?MODULE:disco_local_features/3, #{}, 75},
526✔
450
     {sm_remove_connection, ServerHost, fun ?MODULE:on_user_offline/3, #{}, 75},
451
     {presence_probe, ServerHost, fun ?MODULE:presence_probe/3, #{}, 80},
452
     {roster_in_subscription, ServerHost, fun ?MODULE:in_subscription/3, #{}, 50},
453
     {roster_out_subscription, ServerHost, fun ?MODULE:out_subscription/3, #{}, 50},
454
     {remove_user, ServerHost, fun ?MODULE:remove_user/3, #{}, 50},
455
     {anonymous_purge, ServerHost, fun ?MODULE:remove_user/3, #{}, 50},
456
     {get_personal_data, ServerHost, fun ?MODULE:get_personal_data/3, #{}, 50}].
457

458
pep_hooks(ServerHost) ->
459
    [
54✔
460
     {caps_recognised, ServerHost, fun ?MODULE:caps_recognised/3, #{}, 80},
461
     {disco_sm_identity, ServerHost, fun ?MODULE:disco_sm_identity/3, #{}, 75},
462
     {disco_sm_features, ServerHost, fun ?MODULE:disco_sm_features/3, #{}, 75},
463
     {disco_sm_items, ServerHost, fun ?MODULE:disco_sm_items/3, #{}, 75},
464
     {filter_local_packet, ServerHost, fun ?MODULE:handle_pep_authorization_response/3, #{}, 1},
465
     {foreign_event, ServerHost, fun ?MODULE:foreign_event/3, #{}, 100}
466
    ].
467

468
add_pep_iq_handlers(ServerHost, #{iqdisc := IQDisc}) ->
469
    gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB, ?MODULE, iq_sm, IQDisc),
27✔
470
    gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB_OWNER,
27✔
471
                                  ?MODULE, iq_sm, IQDisc).
472

473
delete_pep_iq_handlers(ServerHost) ->
474
    gen_iq_handler:remove_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB),
27✔
475
    gen_iq_handler:remove_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB_OWNER).
27✔
476

477
%% Plugins is a subset of configured plugins which are able to start
478
%% TODO Evaluate if we should just use plugins from the config instead
479
init_state(ServerHost, #{last_item_cache := LastItemCache, max_items_node := MaxItemsNode,
480
                         pep_mapping := PepMapping, ignore_pep_from_offline := PepOffline,
481
                         access_createnode := Access}, Plugins) ->
482
    HostType = host_to_host_type(ServerHost),
263✔
483
    NodeTree = tree(HostType),
263✔
484
    Host = host(HostType, ServerHost),
263✔
485
    #state{host = Host, server_host = ServerHost,
263✔
486
           access = Access, pep_mapping = PepMapping,
487
           ignore_pep_from_offline = PepOffline,
488
           last_item_cache = LastItemCache,
489
           max_items_node = MaxItemsNode, nodetree = NodeTree,
490
           plugins = Plugins }.
491

492
%% @doc Call the init/1 function for each plugin declared in the config file.
493
%% The default plugin module is implicit.
494
%% <p>The Erlang code for the plugin is located in a module called
495
%% <em>node_plugin</em>. The 'node_' prefix is mandatory.</p>
496
%% <p>The modules are initialized in alphabetical order and the list is checked
497
%% and sorted to ensure that each module is initialized only once.</p>
498
%% <p>See {@link node_hometree:init/1} for an example implementation.</p>
499
init_plugins(Host, ServerHost, Opts = #{nodetree := TreePlugin, plugins := Plugins}) ->
500
    {ok, HostType} = mongoose_domain_api:get_host_type(ServerHost),
263✔
501
    ?LOG_DEBUG(#{what => pubsub_tree_plugin, tree_plugin => TreePlugin}),
263✔
502
    gen_pubsub_nodetree:init(TreePlugin, HostType, Opts),
263✔
503
    PluginsOK = lists:foldl(pa:bind(fun init_plugin/5, Host, ServerHost, Opts), [], Plugins),
263✔
504
    lists:reverse(PluginsOK).
263✔
505

506
init_plugin(Host, ServerHost, Opts, Name, Acc) ->
507
    Plugin = plugin(Name),
385✔
508
    case catch apply(Plugin, init, [Host, ServerHost, Opts]) of
385✔
509
        {'EXIT', Error} ->
UNCOV
510
            ?LOG_ERROR(#{what => pubsub_plugin_init_failed, plugin => Plugin,
×
UNCOV
511
                server => ServerHost, sub_host => Host, opts => Opts, error => Error}),
×
512
            Acc;
×
513
        _ ->
514
            ?LOG_DEBUG(#{what => pubsub_init_plugin, plugin_name => Name}),
385✔
515
            [Name | Acc]
385✔
516
    end.
517

518
terminate_plugins(Host, ServerHost, Plugins, TreePlugin) ->
519
    lists:foreach(
263✔
520
      fun (Name) ->
521
              ?LOG_DEBUG(#{what => pubsub_terminate_plugin, plugin_name => Name}),
385✔
522
              Plugin = plugin(Name),
385✔
523
              gen_pubsub_node:terminate(Plugin, Host, ServerHost)
385✔
524
      end,
525
      Plugins),
526
    gen_pubsub_nodetree:terminate(TreePlugin, Host, ServerHost),
263✔
527
    ok.
263✔
528

529
notify_worker(HostType, HashKey, Request) ->
530
    mongoose_wpool:cast(generic, HostType, pubsub_notify, HashKey,
2,812✔
531
                        {?MODULE, handle_msg, [Request]}).
532

533
handle_msg({send_last_pubsub_items, Host, Recipient, Plugins}) ->
534
    send_last_pubsub_items(Host, Recipient, Plugins);
2,617✔
535
handle_msg({send_last_pep_items, Host, IgnorePepFromOffline, RecipientJID, RecipientPid, Features}) ->
536
    send_last_pep_items(Host, IgnorePepFromOffline, RecipientJID, RecipientPid, Features);
63✔
537
handle_msg({send_last_items_from_owner, Host, NodeOwner, RecipientInfo}) ->
538
    send_last_items_from_owner(Host, NodeOwner, RecipientInfo).
132✔
539

540
send_last_pubsub_items(Host, Recipient, Plugins) ->
541
    F = fun(PluginType) -> send_last_pubsub_items_for_plugin(Host, PluginType, Recipient) end,
2,617✔
542
    lists:foreach(F, Plugins).
2,617✔
543

544
send_last_pubsub_items_for_plugin(Host, PluginType, Recipient) ->
545
    JIDs = [Recipient, jid:to_lower(Recipient), jid:to_bare(Recipient)],
3,871✔
546
    Subs = get_subscriptions_for_send_last(Host, PluginType, JIDs),
3,871✔
547
    lists:foreach(
3,871✔
548
      fun({#pubsub_node{nodeid={_, Node}, id=Nidx, options=Options}, _, _, SubJID}) ->
UNCOV
549
              send_items(Host, Node, Nidx, PluginType, Options, SubJID, last)
×
550
      end,
551
      lists:usort(Subs)).
552

553
send_last_pep_items(Host, IgnorePepFromOffline, RecipientJID, RecipientPid, Features) ->
554
    RecipientLJID = jid:to_lower(RecipientJID),
63✔
555
    [send_last_item_to_jid(NodeOwnerJID, NodeRec, RecipientLJID) ||
63✔
556
        NodeOwnerJID <- get_contacts_for_sending_last_item(RecipientPid, IgnorePepFromOffline),
63✔
557
        NodeRec = #pubsub_node{nodeid = {_, Node}} <- get_nodes_for_sending_last_item(Host, NodeOwnerJID),
72✔
558
        lists:member(<<Node/binary, "+notify">>, Features)],
14✔
559
    ok.
63✔
560

561
get_contacts_for_sending_last_item(RecipientPid, IgnorePepFromOffline) ->
562
    case catch mod_presence:get_subscribed(RecipientPid) of
63✔
563
        Contacts when is_list(Contacts) ->
564
            [Contact || Contact = #jid{luser = U, lserver = S} <- Contacts,
57✔
565
                        user_resources(U, S) /= [] orelse not IgnorePepFromOffline];
72✔
566
        _ ->
567
            []
6✔
568
    end.
569

570
send_last_items_from_owner(Host, NodeOwner, _Recipient = {U, S, Resources}) ->
571
    [send_last_item_to_jid(NodeOwner, NodeRec, {U, S, R}) ||
132✔
572
        NodeRec = #pubsub_node{nodeid = {_, Node}} <- get_nodes_for_sending_last_item(Host, NodeOwner),
132✔
573
        R <- Resources,
49✔
574
        not should_drop_pep_message(Node, jid:make(U, S, R))
49✔
575
    ],
576
    ok.
132✔
577

578
should_drop_pep_message(Node, RecipientJid) ->
579
    case ejabberd_sm:get_session_pid(RecipientJid) of
49✔
UNCOV
580
        none -> true;
×
581
        Pid ->
582
            Event = {filter_pep_recipient, RecipientJid, <<Node/binary, "+notify">>},
49✔
583
            try mongoose_c2s:call(Pid, ?MODULE, Event)
49✔
UNCOV
584
            catch exit:timeout -> true
×
585
            end
586
    end.
587

588
get_nodes_for_sending_last_item(Host, NodeOwnerJID) ->
589
    lists:filter(fun(#pubsub_node{options = Options}) ->
204✔
590
                         match_option(Options, send_last_published_item, on_sub_and_presence)
63✔
591
                 end,
592
                 get_nodes_owned_by(Host, NodeOwnerJID)).
593

594
get_nodes_owned_by(Host, OwnerJID) ->
595
    OwnerBLJID = jid:to_bare(jid:to_lower(OwnerJID)),
204✔
596
    tree_action(Host, get_nodes, [OwnerBLJID, OwnerJID]).
204✔
597

598
send_last_item_to_jid(NodeOwner, #pubsub_node{nodeid = {_, Node}, type = NodeType,
599
                                              id = Nidx, options = NodeOptions}, RecipientJID) ->
600
    NodeOwnerBLJID = jid:to_bare(jid:to_lower(NodeOwner)),
42✔
601
    case is_subscribed(RecipientJID, NodeOwnerBLJID, NodeOptions) of
42✔
602
        true -> send_items(NodeOwnerBLJID, Node, Nidx, NodeType, NodeOptions, RecipientJID, last);
42✔
UNCOV
603
        false -> ok
×
604
    end.
605

606
is_subscribed(Recipient, NodeOwner, NodeOptions) ->
607
    case get_option(NodeOptions, access_model) of
42✔
UNCOV
608
        open -> true;
×
609
        presence -> true;
42✔
610
        whitelist -> false; % subscribers are added manually
×
UNCOV
611
        authorize -> false; % likewise
×
612
        roster ->
613
            Grps = get_option(NodeOptions, roster_groups_allowed, []),
×
UNCOV
614
            {OU, OS, _} = NodeOwner,
×
615
            element(2, get_roster_info(OU, OS, Recipient, Grps))
×
616
    end.
617

618
%% -------
619
%% disco hooks handling functions
620
%%
621

622
-spec identities(jid:lserver(), ejabberd:lang()) -> [mongoose_disco:identity()].
623
identities(Host, Lang) ->
624
    pubsub_identity(Lang) ++ node_identity(Host, ?PEPNODE) ++ node_identity(Host, ?PUSHNODE).
28✔
625

626
-spec pubsub_identity(ejabberd:lang()) -> [mongoose_disco:identity()].
627
pubsub_identity(Lang) ->
628
    [#{category => <<"pubsub">>,
28✔
629
       type => <<"service">>,
630
       name => service_translations:do(Lang, <<"Publish-Subscribe">>)}].
631

632
-spec node_identity(jid:lserver(), binary()) -> [mongoose_disco:identity()].
633
node_identity(Host, Type) ->
634
    case lists:member(Type, plugins(Host)) of
56✔
635
        true -> [#{category => <<"pubsub">>, type => Type}];
14✔
636
        false -> []
42✔
637
    end.
638

639

640
-spec disco_local_features(mongoose_disco:feature_acc(),
641
                           map(),
642
                           map()) -> {ok, mongoose_disco:feature_acc()}.
643
disco_local_features(Acc = #{to_jid := #jid{lserver = LServer}, node := <<>>}, _, _) ->
644
    Features = [?NS_PUBSUB | [feature(F) || F <- features(LServer, <<>>)]],
921✔
645
    {ok, mongoose_disco:add_features(Features, Acc)};
921✔
646
disco_local_features(Acc, _, _) ->
UNCOV
647
    {ok, Acc}.
×
648

649
-spec disco_sm_identity(Acc, Params, Extra) -> {ok, Acc} when
650
    Acc :: mongoose_disco:identity_acc(),
651
    Params :: map(),
652
    Extra :: gen_hook:extra().
653
disco_sm_identity(Acc = #{from_jid := From, to_jid := To}, _, _) ->
654
    Identities = disco_identity(jid:to_lower(jid:to_bare(To)), From),
14✔
655
    {ok, mongoose_disco:add_identities(Identities, Acc)}.
14✔
656

657
disco_identity(error, _From) ->
UNCOV
658
    [];
×
659
disco_identity(_Host, _From) ->
660
    [pep_identity()].
14✔
661

662
pep_identity() ->
663
    #{category => <<"pubsub">>, type => <<"pep">>}.
14✔
664

665
-spec disco_sm_features(Acc, Params, Extra) -> {ok, Acc} when
666
    Acc :: mongoose_disco:feature_acc(),
667
    Params :: map(),
668
    Extra :: gen_hook:extra().
669
disco_sm_features(Acc = #{from_jid := From, to_jid := To}, _, _) ->
670
    Features = disco_features(jid:to_lower(jid:to_bare(To)), From),
14✔
671
    {ok, mongoose_disco:add_features(Features, Acc)}.
14✔
672

673
-spec disco_features(error | jid:simple_jid(), jid:jid()) -> [mongoose_disco:feature()].
674
disco_features(error, _From) ->
UNCOV
675
    [];
×
676
disco_features(_Host, _From) ->
677
    [?NS_PUBSUB | [feature(F) || F <- plugin_features(<<"pep">>)]].
14✔
678

679
-spec disco_sm_items(Acc, Params, Extra) -> {ok, Acc} when
680
    Acc :: mongoose_disco:item_acc(),
681
    Params :: map(),
682
    Extra :: gen_hook:extra().
683
disco_sm_items(Acc = #{from_jid := From, to_jid := To}, _, _) ->
684
    Items = disco_items(jid:to_lower(jid:to_bare(To)), From),
28✔
685
    {ok, mongoose_disco:add_items(Items, Acc)}.
28✔
686

687
-spec disco_items(mod_pubsub:host(), jid:jid()) -> [mongoose_disco:item()].
688
disco_items(Host, From) ->
689
    Action = fun (#pubsub_node{nodeid = {_, Node},
28✔
690
                               options = Options, type = Type, id = Nidx, owners = Owners},
691
                  Acc) ->
692
                     case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) of
14✔
693
                         {result, _} ->
694
                             [disco_item(Node, Host, Options) | Acc];
14✔
695
                         _ ->
UNCOV
696
                             Acc
×
697
                     end
698
             end,
699
    NodeBloc = fun() ->
28✔
700
                       {result,
28✔
701
                        lists:foldl(Action, [], tree_call(Host, get_nodes, [Host, From]))}
702
               end,
703
    ErrorDebug = #{
28✔
704
      action => disco_items,
705
      pubsub_host => Host,
706
      from => From
707
     },
708
    case mod_pubsub_db_backend:dirty(NodeBloc, ErrorDebug) of
28✔
709
        {result, Items} -> Items;
28✔
UNCOV
710
        _ -> []
×
711
    end.
712

713
disco_item(Node, Host, Options) ->
714
    Item = #{node => Node,
14✔
715
             jid => jid:to_binary(Host)},
716
    case get_option(Options, title) of
14✔
717
        false -> Item;
14✔
UNCOV
718
        [Title] -> Item#{name => Title}
×
719
    end.
720

721
%% -------
722
%% callback that prevents routing subscribe authorizations back to the sender
723
%%
724

725
-spec handle_pep_authorization_response(Acc, Params, Extra) -> {ok, Acc} when
726
      Acc :: mongoose_hooks:filter_packet_acc(),
727
      Params :: map(),
728
      Extra :: gen_hook:extra().
729
handle_pep_authorization_response({From, To, Acc, #xmlel{ name = Name } = Packet}, _, _) ->
730
    Type = mongoose_acc:stanza_type(Acc),
4,481✔
731
    {ok, handle_pep_authorization_response(Name, Type, From, To, Acc, Packet)}.
4,481✔
732

733
handle_pep_authorization_response(_, <<"error">>, From, To, Acc, Packet) ->
734
    {From, To, Acc, Packet};
95✔
735
handle_pep_authorization_response(<<"message">>, _, From, To, Acc, Packet)
736
  when From#jid.luser == To#jid.luser, From#jid.lserver == To#jid.lserver ->
737
        case find_authorization_response(Packet) of
14✔
738
            none ->
UNCOV
739
                {From, To, Acc, Packet};
×
740
            invalid ->
741
                {From, To, Acc, Packet};
7✔
742
            KVs ->
743
                handle_authorization_response(Acc, jid:to_lower(To), From, To, Packet, KVs),
7✔
744
                drop
7✔
745
        end;
746
handle_pep_authorization_response(_, _, From, To, Acc, Packet) ->
747
    {From, To, Acc, Packet}.
4,372✔
748

749
%% -------
750
%% callback for foreign_event calls, to distribute pep messages from the node owner c2s process
751
%%
752
-spec foreign_event(mongoose_acc:t(), mongoose_c2s_hooks:params(), gen_hook:extra()) ->
753
      mongoose_c2s_hooks:result().
754
foreign_event(Acc, #{c2s_data := StateData,
755
                     event_type := {call, From},
756
                     event_tag := ?MODULE,
757
                     event_content := {get_pep_recipients, Feature}}, _Extra) ->
758
    Reply = mongoose_hooks:get_pep_recipients(StateData, Feature),
133✔
759
    Acc1 = mongoose_c2s_acc:to_acc(Acc, actions, [{reply, From, Reply}]),
133✔
760
    {stop, Acc1};
133✔
761
foreign_event(Acc, #{c2s_data := StateData,
762
                     event_type := {call, From},
763
                     event_tag := ?MODULE,
764
                     event_content := {filter_pep_recipient, To, Feature}}, _Extra) ->
765
    Reply = mongoose_hooks:filter_pep_recipient(StateData, Feature, To),
49✔
766
    Acc1 = mongoose_c2s_acc:to_acc(Acc, actions, [{reply, From, Reply}]),
49✔
767
    {stop, Acc1};
49✔
768
foreign_event(Acc, _Params, _Extra) ->
769
    {ok, Acc}.
72✔
770

771
%% -------
772
%% presence hooks handling functions
773
%%
774

775
-spec caps_recognised(Acc, Params, Extra) -> {ok, Acc} when
776
    Acc :: mongoose_acc:t(),
777
    Params :: #{from := jid:jid(), pid := pid(), features := [binary()]},
778
    Extra :: gen_hook:extra().
779
caps_recognised(Acc, #{from := #jid{ luser = U, lserver = S } = JID, pid := Pid, features := Features}, _) ->
780
    Host = host(S, S),
63✔
781
    IgnorePepFromOffline = gen_mod:get_module_opt(S, ?MODULE, ignore_pep_from_offline),
63✔
782
    notify_worker(S, U, {send_last_pep_items, Host, IgnorePepFromOffline, JID, Pid, Features}),
63✔
783
    {ok, Acc}.
63✔
784

785
-spec presence_probe(Acc, Params, Extra) -> {ok, Acc} when
786
    Acc :: mongoose_acc:t(),
787
    Params :: #{from := jid:jid(), to := jid:jid()},
788
    Extra :: gen_hook:extra().
789
presence_probe(Acc, #{from := #jid{luser = U, lserver = S, lresource = _R} = JID, to := JID}, _) ->
790
    %% Get subdomain
791
    Host = host(S, S),
2,617✔
792
    Plugins = plugins(S),
2,617✔
793
    notify_worker(S, U, {send_last_pubsub_items, Host, _Recipient = JID, Plugins}),
2,617✔
794
    {ok, Acc};
2,617✔
795
presence_probe(Acc, _, _) ->
796
    {ok, Acc}.
49✔
797

798
%% -------
799
%% subscription hooks handling functions
800
%%
801

802
-spec out_subscription(Acc, Params, Extra) -> {ok, Acc} when
803
    Acc :: mongoose_acc:t(),
804
    Params :: #{to := jid:jid(),
805
                from := jid:jid(),
806
                type := mod_roster:sub_presence()},
807
    Extra :: gen_hook:extra().
808
out_subscription(Acc,
809
                 #{to := #jid{luser = PUser, lserver = PServer, lresource = PResource},
810
                   from := #jid{luser = LUser, lserver = LServer} = FromJID,
811
                   type := subscribed},
812
                 _) ->
813
    PResources = case PResource of
132✔
814
                     <<>> -> user_resources(PUser, PServer);
132✔
UNCOV
815
                     _ -> [PResource]
×
816
                 end,
817
    Host = host(LServer, LServer),
132✔
818
    notify_worker(LServer, LUser, {send_last_items_from_owner, Host, FromJID,
132✔
819
                                   {PUser, PServer, PResources}}),
820
    {ok, Acc};
132✔
821
out_subscription(Acc, _, _) ->
822
    {ok, Acc}.
146✔
823

824
-spec in_subscription(Acc, Params, Extra) -> {ok, Acc} when
825
    Acc :: mongoose_acc:t(),
826
    Params :: #{to := jid:jid(),
827
                from := jid:jid(),
828
                type := mod_roster:sub_presence()},
829
    Extra :: gen_hook:extra().
830
in_subscription(Acc, #{to := ToJID, from := OwnerJID, type := unsubscribed}, _) ->
831
    unsubscribe_user(ToJID, OwnerJID),
17✔
832
    {ok, Acc};
17✔
833
in_subscription(Acc, _, _) ->
834
    {ok, Acc}.
267✔
835

836
unsubscribe_user(Entity, Owner) ->
837
    ServerHosts = lists:usort(lists:foldl(
17✔
838
                                fun(UserHost, Acc) ->
839
                                        case gen_mod:is_loaded(UserHost, mod_pubsub) of
34✔
840
                                            true -> [UserHost|Acc];
34✔
UNCOV
841
                                            false -> Acc
×
842
                                        end
843
                                end, [], [Entity#jid.lserver, Owner#jid.lserver])),
844
    [unsubscribe_user(ServerHost, Entity, Owner) || ServerHost <- ServerHosts],
17✔
845
    ok.
17✔
846

847
unsubscribe_user(Host, Entity, Owner) ->
848
    BJID = jid:to_lower(jid:to_bare(Owner)),
17✔
849
    lists:foreach(fun (PType) ->
17✔
850
                         unsubscribe_user_per_plugin(Host, Entity, BJID, PType)
37✔
851
                  end, plugins(Host)).
852

853
unsubscribe_user_per_plugin(Host, Entity, BJID, PType) ->
854
    {result, Subs} = node_action(Host, PType, get_entity_subscriptions, [Host, Entity]),
37✔
855
    lists:foreach(fun({#pubsub_node{options = Options, owners = Owners, id = Nidx},
37✔
856
                       subscribed, _, JID}) ->
857
                          Unsubscribe = match_option(Options, access_model, presence)
14✔
858
                          andalso lists:member(BJID, Owners),
14✔
859
                          case Unsubscribe of
14✔
860
                              true ->
861
                                  node_action(Host, PType,
14✔
862
                                              unsubscribe_node, [Nidx, Entity, JID, all]);
863
                              false ->
UNCOV
864
                                  ok
×
865
                          end;
866
                     (_) ->
UNCOV
867
                          ok
×
868
                  end, Subs).
869

870
%% -------
871
%% user remove hook handling function
872
%%
873
-spec remove_user(Acc, Params, Extra) -> {ok, Acc} when
874
      Acc :: mongoose_acc:t(),
875
      Params :: #{jid := jid:jid()},
876
      Extra :: #{host_type := mongooseim:host_type()}.
877
remove_user(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, _) ->
878
    lists:foreach(fun(PType) ->
224✔
879
                          remove_user_per_plugin_safe(LUser, LServer, plugin(PType))
672✔
880
                  end, plugins(LServer)),
881
    {ok, Acc}.
224✔
882

883
remove_user_per_plugin_safe(LUser, LServer, Plugin) ->
884
    try
672✔
885
        plugin_call(Plugin, remove_user, [LUser, LServer])
672✔
886
    catch
887
        Class:Reason:StackTrace ->
UNCOV
888
            ?LOG_WARNING(#{what => pubsub_delete_user_failed, user => LUser,
×
889
                server => LServer, class => Class, reason => Reason,
890
                stacktrace => StackTrace})
×
891
    end.
892

893
handle_call(server_host, _From, State) ->
UNCOV
894
    {reply, State#state.server_host, State};
×
895
handle_call(plugins, _From, State) ->
896
    {reply, State#state.plugins, State};
8,592✔
897
handle_call(pep_mapping, _From, State) ->
UNCOV
898
    {reply, State#state.pep_mapping, State};
×
899
handle_call(nodetree, _From, State) ->
900
    {reply, State#state.nodetree, State};
×
901
handle_call(stop, _From, State) ->
902
    {stop, normal, ok, State}.
263✔
903

904
%%--------------------------------------------------------------------
905
%% Function: handle_cast(Msg, State) -> {noreply, State} |
906
%%                                      {noreply, State, Timeout} |
907
%%                                      {stop, Reason, State}
908
%% Description: Handling cast messages
909
%%--------------------------------------------------------------------
910
%% @private
UNCOV
911
handle_cast(_Msg, State) -> {noreply, State}.
×
912

913
%%--------------------------------------------------------------------
914
%% Function: handle_info(Info, State) -> {noreply, State} |
915
%%                                       {noreply, State, Timeout} |
916
%%                                       {stop, Reason, State}
917
%% Description: Handling all non call/cast messages
918
%%--------------------------------------------------------------------
919
%% @private
920
handle_info(_Info, State) ->
UNCOV
921
    {noreply, State}.
×
922

923
%%--------------------------------------------------------------------
924
%% Function: terminate(Reason, State) -> void()
925
%% Description: This function is called by a gen_server when it is about to
926
%% terminate. It should be the opposite of Module:init/1 and do any necessary
927
%% cleaning up. When it returns, the gen_server terminates with Reason.
928
%% The return value is ignored.
929
%%--------------------------------------------------------------------
930
%% @private
931
terminate(_Reason, #state{host = Host, server_host = ServerHost,
932
                          nodetree = TreePlugin, plugins = Plugins}) ->
933
    SubdomainPattern = gen_mod:get_module_opt(ServerHost, ?MODULE, host),
263✔
934
    mongoose_domain_api:unregister_subdomain(ServerHost, SubdomainPattern),
263✔
935
    case lists:member(?PEPNODE, Plugins) of
263✔
936
        true ->
937
            gen_hook:delete_handlers(pep_hooks(ServerHost)),
27✔
938
            delete_pep_iq_handlers(ServerHost);
27✔
939
        false -> ok
236✔
940
    end,
941
    gen_hook:delete_handlers(hooks(ServerHost)),
263✔
942
    case whereis(gen_mod:get_module_proc(ServerHost, ?LOOPNAME)) of
263✔
943
        undefined ->
944
            ?LOG_ERROR(#{what => pubsub_process_is_dead,
263✔
945
                text => <<"process is dead, pubsub was broken">>,
UNCOV
946
                process => ?LOOPNAME});
×
947
        Pid ->
948
            Pid ! stop
×
949
    end,
950
    terminate_plugins(Host, ServerHost, Plugins, TreePlugin),
263✔
951
    mod_pubsub_db_backend:stop().
263✔
952

953
%%--------------------------------------------------------------------
954
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
955
%% Description: Convert process state when code is changed
956
%%--------------------------------------------------------------------
957
%% @private
UNCOV
958
code_change(_OldVsn, State, _Extra) -> {ok, State}.
×
959

960
-spec do_route(
961
        Acc :: mongoose_acc:t(),
962
        ServerHost :: binary(),
963
          Access     :: atom(),
964
          Plugins    :: [binary(), ...],
965
          Host       :: mod_pubsub:hostPubsub(),
966
          From       ::jid:jid(),
967
          To         ::jid:jid(),
968
          Packet     :: exml:element())
969
        -> mongoose_acc:t().
970

971
%%--------------------------------------------------------------------
972
%%% Internal functions
973
%%--------------------------------------------------------------------
974
do_route(Acc, ServerHost, Access, Plugins, Host, From,
975
         #jid{luser = <<>>, lresource = <<>>} = To,
976
         #xmlel{ name = <<"iq">> } = Packet) ->
977
    case jlib:iq_query_info(Packet) of
5,695✔
978
        #iq{type = get, xmlns = ?NS_DISCO_INFO, sub_el = SubEl, lang = Lang} = IQ ->
979
            #xmlel{attrs = QAttrs} = SubEl,
28✔
980
            Node = exml_query:attr(SubEl, <<"node">>, <<>>),
28✔
981
            InfoXML = mongoose_disco:get_info(ServerHost, ?MODULE, <<>>, <<>>),
28✔
982
            Res = case iq_disco_info(ServerHost, Host, Node, From, Lang) of
28✔
983
                      {result, IQRes} ->
984
                          jlib:iq_to_xml(IQ#iq{type = result,
28✔
985
                                               sub_el =
986
                                               [#xmlel{name = <<"query">>,
987
                                                       attrs = QAttrs,
988
                                                       children = IQRes ++ InfoXML}]});
989
                      {error, Error} ->
UNCOV
990
                          make_error_reply(Packet, Error)
×
991
                  end,
992
            ejabberd_router:route(To, From, Acc, Res);
28✔
993
        #iq{type = get, xmlns = ?NS_DISCO_ITEMS, sub_el = SubEl} = IQ ->
994
            #xmlel{attrs = QAttrs} = SubEl,
105✔
995
            Node = exml_query:attr(SubEl, <<"node">>, <<>>),
105✔
996
            Res = case iq_disco_items(Host, Node, From, jlib:rsm_decode(IQ)) of
105✔
997
                      {result, IQRes} ->
998
                          jlib:iq_to_xml(IQ#iq{type = result,
84✔
999
                                               sub_el =
1000
                                               [#xmlel{name = <<"query">>,
1001
                                                       attrs = QAttrs,
1002
                                                       children = IQRes}]});
1003
                      {error, Error} ->
1004
                          make_error_reply(Packet, Error)
21✔
1005
                  end,
1006
            ejabberd_router:route(To, From, Acc, Res);
105✔
1007
        #iq{type = IQType, xmlns = ?NS_PUBSUB, lang = Lang, sub_el = SubEl} = IQ ->
1008
            Res = case iq_pubsub(Host, ServerHost, From, IQType,
3,767✔
1009
                                 SubEl, Lang, Access, Plugins)
1010
                  of
1011
                      {result, IQRes} ->
1012
                          jlib:iq_to_xml(IQ#iq{type = result, sub_el = IQRes});
3,543✔
1013
                      {error, Error} ->
1014
                          make_error_reply(Packet, Error)
224✔
1015
                  end,
1016
            ejabberd_router:route(To, From, Acc, Res);
3,767✔
1017
        #iq{type = IQType, xmlns = ?NS_PUBSUB_OWNER, lang = Lang, sub_el = SubEl} = IQ ->
1018
            Res = case iq_pubsub_owner(Host, ServerHost, From,
1,753✔
1019
                                       IQType, SubEl, Lang)
1020
                  of
1021
                      {result, IQRes} ->
1022
                          jlib:iq_to_xml(IQ#iq{type = result, sub_el = IQRes});
1,599✔
1023
                      {error, {Error, NewPayload}} ->
1024
                          make_error_reply(Packet#xmlel{ children = NewPayload }, Error);
14✔
1025
                      {error, Error} ->
1026
                          make_error_reply(Packet, Error)
140✔
1027
                  end,
1028
            ejabberd_router:route(To, From, Acc, Res);
1,753✔
1029
        #iq{type = get, xmlns = (?NS_VCARD) = XMLNS, lang = Lang, sub_el = _SubEl} = IQ ->
UNCOV
1030
            Res = IQ#iq{type = result,
×
1031
                        sub_el =
1032
                        [#xmlel{name = <<"vCard">>,
1033
                                attrs = #{<<"xmlns">> => XMLNS},
1034
                                children = iq_get_vcard(Lang)}]},
UNCOV
1035
            ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(Res));
×
1036
        #iq{type = set, xmlns = ?NS_COMMANDS} = IQ ->
1037
            Res = case iq_command(Host, ServerHost, From, IQ, Access, Plugins) of
42✔
1038
                      {error, Error} ->
1039
                          make_error_reply(Packet, Error);
14✔
1040
                      {result, IQRes} ->
1041
                          jlib:iq_to_xml(IQ#iq{type = result, sub_el = IQRes})
28✔
1042
                  end,
1043
            ejabberd_router:route(To, From, Acc, Res);
42✔
1044
        #iq{} ->
UNCOV
1045
            Err = make_error_reply(Packet, mongoose_xmpp_errors:feature_not_implemented()),
×
UNCOV
1046
            ejabberd_router:route(To, From, Acc, Err);
×
1047
        _ ->
1048
            Acc
×
1049
    end;
1050
do_route(Acc, _ServerHost, _Access, _Plugins, Host, From,
1051
         #jid{luser = <<>>, lresource = <<>>} = To,
1052
         #xmlel{ name = <<"message">> } = Packet) ->
1053
    case exml_query:attr(Packet, <<"type">>) of
32✔
1054
        <<"error">> ->
1055
            ok;
4✔
1056
        _ ->
1057
            case find_authorization_response(Packet) of
28✔
1058
                none ->
UNCOV
1059
                    Acc;
×
1060
                invalid ->
1061
                    Err = make_error_reply(Packet, mongoose_xmpp_errors:bad_request()),
×
UNCOV
1062
                    ejabberd_router:route(To, From, Acc, Err);
×
1063
                XFields ->
1064
                    handle_authorization_response(Acc, Host, From, To, Packet, XFields)
28✔
1065
            end
1066
    end;
1067
do_route(Acc, _ServerHost, _Access, _Plugins, _Host, _From,
1068
         #jid{luser = <<>>, lresource = <<>>} = _To, _Packet) ->
UNCOV
1069
    Acc;
×
1070
do_route(Acc, _ServerHost, _Access, _Plugins, _Host, From, To, Packet) ->
1071
    case exml_query:attr(Packet, <<"type">>) of
×
1072
        <<"error">> ->
1073
            Acc;
×
1074
        <<"result">> ->
1075
            Acc;
×
1076
        _ ->
1077
            Err = make_error_reply(Packet, mongoose_xmpp_errors:item_not_found()),
×
UNCOV
1078
            ejabberd_router:route(To, From, Acc, Err)
×
1079
    end.
1080

1081
command_disco_info(_Host, ?NS_COMMANDS, _From) ->
UNCOV
1082
    IdentityEl = #xmlel{name = <<"identity">>,
×
1083
                        attrs = #{<<"category">> => <<"automation">>,
1084
                                  <<"type">> => <<"command-list">>}},
UNCOV
1085
    {result, [IdentityEl]};
×
1086
command_disco_info(_Host, ?NS_PUBSUB_GET_PENDING, _From) ->
1087
    IdentityEl = #xmlel{name = <<"identity">>,
×
1088
                        attrs = #{<<"category">> => <<"automation">>,
1089
                                  <<"type">> => <<"command-node">>}},
UNCOV
1090
    FeaturesEl = #xmlel{name = <<"feature">>,
×
1091
                        attrs = #{<<"var">> => ?NS_COMMANDS}},
1092
    {result, [IdentityEl, FeaturesEl]}.
×
1093

1094
node_disco_info(Host, Node, From) ->
UNCOV
1095
    node_disco_info(Host, Node, From, true, true).
×
1096

1097
node_disco_info(Host, Node, _From, _Identity, _Features) ->
UNCOV
1098
    Action = fun (#pubsub_node{type = Type, options = Options}) ->
×
UNCOV
1099
                     NodeType = case get_option(Options, node_type) of
×
1100
                                    collection -> <<"collection">>;
×
1101
                                    _ -> <<"leaf">>
×
1102
                                end,
1103
                     I = #xmlel{name = <<"identity">>,
×
1104
                                attrs = #{<<"category">> => <<"pubsub">>,
1105
                                          <<"type">> => NodeType}},
UNCOV
1106
                     F = [#xmlel{name = <<"feature">>,
×
1107
                                 attrs = #{<<"var">> => ?NS_PUBSUB}}
1108
                          | [#xmlel{name = <<"feature">>,
×
1109
                                    attrs = #{<<"var">> => feature(F)}}
1110
                             || F <- plugin_features(Type)]],
×
UNCOV
1111
                     {result, [I | F]}
×
1112
             end,
1113
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
×
UNCOV
1114
        {result, {_, Result}} -> {result, Result};
×
1115
        Other -> Other
×
1116
    end.
1117

1118
iq_disco_info(ServerHost, Host, SNode, From, Lang) ->
1119
    [Node | _] = case SNode of
28✔
1120
                     <<>> -> [<<>>];
28✔
UNCOV
1121
                     _ -> mongoose_bin:tokens(SNode, <<"!">>)
×
1122
                 end,
1123
    case Node of
28✔
1124
        <<>> ->
1125
            Identities = identities(ServerHost, Lang),
28✔
1126
            Features = [?NS_DISCO_INFO,
28✔
1127
                        ?NS_DISCO_ITEMS,
1128
                        ?NS_PUBSUB,
1129
                        ?NS_COMMANDS,
1130
                        ?NS_VCARD] ++ [feature(F) || F <- features(ServerHost, Node)],
1,057✔
1131
            {result, mongoose_disco:identities_to_xml(Identities) ++
28✔
1132
                 mongoose_disco:features_to_xml(Features)};
1133
        ?NS_COMMANDS ->
UNCOV
1134
            command_disco_info(Host, Node, From);
×
1135
        ?NS_PUBSUB_GET_PENDING ->
1136
            command_disco_info(Host, Node, From);
×
1137
        _ ->
1138
            node_disco_info(Host, Node, From)
×
1139
    end.
1140

1141
-spec iq_disco_items(
1142
        Host   :: mod_pubsub:host(),
1143
          Node   :: <<>> | mod_pubsub:nodeId(),
1144
          From   ::jid:jid(),
1145
          Rsm    :: none | jlib:rsm_in())
1146
        -> {result, [exml:element()]} | {error, term()}.
1147
iq_disco_items(Host, <<>>, From, _RSM) ->
1148
    {result,
56✔
1149
     lists:map(fun (#pubsub_node{nodeid = {_, SubNode}, options = Options}) ->
1150
                       Attrs1 = node_attr(SubNode),
792✔
1151
                       Attrs2 = case get_option(Options, title) of
792✔
1152
                                    false ->
1153
                                        Attrs1#{<<"jid">> => Host};
792✔
1154
                                    [Title] ->
UNCOV
1155
                                        Attrs1#{<<"jid">> => Host,
×
1156
                                                <<"name">> => Title}
1157
                               end,
1158
                       #xmlel{name = <<"item">>, attrs = Attrs2}
792✔
1159
               end,
1160
               tree_action(Host, get_subnodes, [Host, <<>>, From]))};
1161
iq_disco_items(Host, ?NS_COMMANDS, _From, _RSM) ->
UNCOV
1162
    {result, [#xmlel{name = <<"item">>,
×
1163
                     attrs = #{<<"jid">> => Host,
1164
                               <<"node">> => ?NS_PUBSUB_GET_PENDING,
1165
                               <<"name">> => <<"Get Pending">>}}]};
1166
iq_disco_items(_Host, ?NS_PUBSUB_GET_PENDING, _From, _RSM) ->
UNCOV
1167
    {result, []};
×
1168
iq_disco_items(Host, Item, From, RSM) ->
1169
    case mongoose_bin:tokens(Item, <<"!">>) of
49✔
1170
        [_Node, _ItemId] ->
UNCOV
1171
            {result, []};
×
1172
        [Node] ->
1173
            Action = fun (PubSubNode) ->
49✔
1174
                             iq_disco_items_transaction(Host, From, Node, RSM, PubSubNode)
28✔
1175
                     end,
1176
            case dirty(Host, Node, Action, ?FUNCTION_NAME) of
49✔
1177
                {result, {_, Result}} -> {result, Result};
28✔
1178
                Other -> Other
21✔
1179
            end
1180
    end.
1181

1182
iq_disco_items_transaction(Host, From, Node, RSM,
1183
                           #pubsub_node{id = Nidx, type = Type, options = Options, owners = Owners}) ->
1184
    {NodeItems, RsmOut} = case get_allowed_items_call(Host, Nidx,
28✔
1185
                                                      From, Type, Options, Owners, RSM)
1186
                          of
1187
                              {result, R} -> R;
28✔
UNCOV
1188
                              _ -> {[], none}
×
1189
                          end,
1190
    Nodes = lists:map(fun (#pubsub_node{nodeid = {_, SubNode}, options = SubOptions}) ->
28✔
1191
                              Attrs1 = node_attr(SubNode),
21✔
1192
                              Attrs2 = case get_option(SubOptions, title) of
21✔
1193
                                           false ->
1194
                                               Attrs1#{<<"jid">> => Host};
21✔
1195
                                           [Title] ->
UNCOV
1196
                                               Attrs1#{<<"jid">> => Host,
×
1197
                                                       <<"name">> => Title}
1198
                                       end,
1199
                              #xmlel{name = <<"item">>, attrs = Attrs2}
21✔
1200
                      end,
1201
                      tree_call(Host, get_subnodes, [Host, Node, From])),
1202
    Items = lists:map(fun (#pubsub_item{itemid = {RN, _}}) ->
28✔
UNCOV
1203
                              {result, Name} = node_call(Type, get_item_name,
×
1204
                                                         [Host, Node, RN]),
1205
                              #xmlel{name = <<"item">>,
×
1206
                                     attrs = #{<<"jid">> => Host, <<"name">> => Name}}
1207
                      end,
1208
                      NodeItems),
1209
    {result, Nodes ++ Items ++ jlib:rsm_encode(RsmOut)}.
28✔
1210

1211
-spec iq_sm(From ::jid:jid(),
1212
            To   ::jid:jid(),
1213
            Acc :: mongoose_acc:t(),
1214
            IQ   :: jlib:iq())
1215
        -> {mongoose_acc:t(), jlib:iq()}.
1216
iq_sm(From, To, Acc, #iq{type = Type, sub_el = SubEl, xmlns = XMLNS, lang = Lang} = IQ) ->
1217
    ServerHost = To#jid.lserver,
305✔
1218
    LOwner = jid:to_lower(jid:to_bare(To)),
305✔
1219
    Res = case XMLNS of
305✔
1220
              ?NS_PUBSUB ->
1221
                  iq_pubsub(LOwner, ServerHost, From, Type, SubEl, Lang);
269✔
1222
              ?NS_PUBSUB_OWNER ->
1223
                  iq_pubsub_owner(LOwner, ServerHost, From, Type, SubEl, Lang)
36✔
1224
          end,
1225
    case Res of
305✔
1226
        {result, IQRes} -> {Acc, IQ#iq{type = result, sub_el = IQRes}};
270✔
1227
        {error, Error} -> {Acc, make_error_reply(IQ, Error)}
35✔
1228
    end.
1229

1230
iq_get_vcard(Lang) ->
UNCOV
1231
    Desc = <<(service_translations:do(Lang, <<"ejabberd Publish-Subscribe module">>))/binary,
×
1232
             "\nCopyright (c) 2004-2015 ProcessOne">>,
1233
    [#xmlel{name = <<"FN">>,
×
1234
            children = [#xmlcdata{content = <<"ejabberd/mod_pubsub">>}]},
1235
     #xmlel{name = <<"URL">>,
1236
            children = [#xmlcdata{content = ?MONGOOSE_URI}]},
1237
     #xmlel{name = <<"DESC">>,
1238
            children = [#xmlcdata{content = Desc}]}].
1239

1240
-spec iq_pubsub(Host :: mod_pubsub:host(),
1241
                ServerHost :: binary(),
1242
                From ::jid:jid(),
1243
                IQType :: get | set,
1244
                QueryEl :: exml:element(),
1245
                Lang :: binary()) -> {result, [exml:element()]} | {error, exml:element()}.
1246
iq_pubsub(Host, ServerHost, From, IQType, QueryEl, Lang) ->
1247
    iq_pubsub(Host, ServerHost, From, IQType, QueryEl, Lang, all, plugins(ServerHost)).
269✔
1248

1249
-spec iq_pubsub(Host :: mod_pubsub:host(),
1250
                ServerHost :: binary(),
1251
                From ::jid:jid(),
1252
                IQType :: 'get' | 'set',
1253
                QueryEl :: exml:element(),
1254
                Lang :: binary(),
1255
                Access :: atom(),
1256
                Plugins :: [binary(), ...]) -> {result, [exml:element()]} | {error, exml:element()}.
1257
iq_pubsub(Host, ServerHost, From, IQType, #xmlel{children = SubEls} = QueryEl,
1258
          Lang, Access, Plugins) ->
1259
    case jlib:remove_cdata(SubEls) of
4,036✔
1260
        [#xmlel{name = Name} = ActionEl | _] ->
1261
            Node = exml_query:attr(ActionEl, <<"node">>, <<>>),
4,036✔
1262
            ActionExtraArgs = #{server_host => ServerHost,
4,036✔
1263
                                plugins => Plugins,
1264
                                access => Access,
1265
                                action_el => ActionEl,
1266
                                query_el => QueryEl,
1267
                                lang => Lang},
1268
            mongoose_instrument:span(event_name(IQType, Name), #{host_type => ServerHost}, fun iq_pubsub_action/6,
4,036✔
1269
                                     [IQType, Name, Host, Node, From, ActionExtraArgs],
1270
                                     fun(Time, Result) -> iq_action_result_to_measurements(Time, Result, From) end);
4,036✔
1271
        Other ->
UNCOV
1272
            ?LOG_INFO(#{what => pubsub_bad_request, exml_packet => Other}),
×
UNCOV
1273
            {error, mongoose_xmpp_errors:bad_request()}
×
1274
    end.
1275

1276
iq_pubsub_action(IQType, Name, Host, Node, From, ExtraArgs) ->
1277
    case {IQType, Name} of
4,036✔
1278
        {set, <<"create">>} ->
1279
            iq_pubsub_set_create(Host, Node, From, ExtraArgs);
1,501✔
1280
        {set, <<"publish">>} ->
1281
            iq_pubsub_set_publish(Host, Node, From, ExtraArgs);
1,368✔
1282
        {set, <<"retract">>} ->
1283
            iq_pubsub_set_retract(Host, Node, From, ExtraArgs);
56✔
1284
        {set, <<"subscribe">>} ->
1285
            iq_pubsub_set_subscribe(Host, Node, From, ExtraArgs);
649✔
1286
        {set, <<"unsubscribe">>} ->
1287
            iq_pubsub_set_unsubscribe(Host, Node, From, ExtraArgs);
35✔
1288
        {get, <<"items">>} ->
1289
            iq_pubsub_get_items(Host, Node, From, ExtraArgs);
231✔
1290
        {get, <<"subscriptions">>} ->
1291
            get_subscriptions(Host, Node, From, ExtraArgs);
70✔
1292
        {get, <<"affiliations">>} ->
UNCOV
1293
            get_affiliations(Host, Node, From, ExtraArgs);
×
1294
        {get, <<"options">>} ->
1295
            iq_pubsub_get_options(Host, Node, From, ExtraArgs);
98✔
1296
        {set, <<"options">>} ->
1297
            iq_pubsub_set_options(Host, Node, ExtraArgs);
28✔
1298
          _ ->
UNCOV
1299
            {error, mongoose_xmpp_errors:feature_not_implemented()}
×
1300
    end.
1301

1302
-spec instrumentation(mongooseim:host_type()) -> [mongoose_instrument:spec()].
1303
instrumentation(HostType) ->
1304
    Measurements = #{count => spiral, errors => spiral, time => histogram},
533✔
1305
    [{event_name(IQType, Name), #{host_type => HostType}, #{metrics => Measurements}} ||
533✔
1306
        {IQType, Name} <- all_metrics()].
533✔
1307

1308
all_metrics() ->
1309
    [{set, create},
533✔
1310
     {set, publish},
1311
     {set, retract},
1312
     {set, subscribe},
1313
     {set, unsubscribe},
1314
     {get, items},
1315
     {get, options},
1316
     {set, options},
1317
     {get, configure},
1318
     {set, configure},
1319
     {get, default},
1320
     {set, delete},
1321
     {set, purge},
1322
     {get, subscriptions},
1323
     {set, subscriptions},
1324
     {get, affiliations},
1325
     {set, affiliations}].
1326

1327
iq_action_to_metric_name(<<"create">>) -> create;
1,501✔
1328
iq_action_to_metric_name(<<"publish">>) -> publish;
1,368✔
1329
iq_action_to_metric_name(<<"retract">>) -> retract;
56✔
1330
iq_action_to_metric_name(<<"subscribe">>) -> subscribe;
649✔
1331
iq_action_to_metric_name(<<"unsubscribe">>) -> unsubscribe;
35✔
1332
iq_action_to_metric_name(<<"items">>) -> items;
231✔
1333
iq_action_to_metric_name(<<"options">>) -> options;
126✔
1334
iq_action_to_metric_name(<<"configure">>) -> configure;
147✔
1335
iq_action_to_metric_name(<<"default">>) -> default;
14✔
1336
iq_action_to_metric_name(<<"delete">>) -> delete;
1,052✔
1337
iq_action_to_metric_name(<<"purge">>) -> purge;
56✔
1338
iq_action_to_metric_name(<<"subscriptions">>) -> subscriptions;
182✔
1339
iq_action_to_metric_name(<<"affiliations">>) -> affiliations.
394✔
1340

1341
event_name(IQType, Name) when is_binary(Name) ->
1342
    NameAtom = iq_action_to_metric_name(Name),
5,811✔
1343
    event_name(IQType, NameAtom);
5,811✔
1344
event_name(set, create) ->
1345
    mod_pubsub_set_create;
2,034✔
1346
event_name(set, publish) ->
1347
    mod_pubsub_set_publish;
1,901✔
1348
event_name(set, retract) ->
1349
    mod_pubsub_set_retract;
589✔
1350
event_name(set, subscribe) ->
1351
    mod_pubsub_set_subscribe;
1,182✔
1352
event_name(set, unsubscribe) ->
1353
    mod_pubsub_set_unsubscribe;
568✔
1354
event_name(get, items) ->
1355
    mod_pubsub_get_items;
764✔
1356
event_name(get, options) ->
1357
    mod_pubsub_get_options;
631✔
1358
event_name(set, options) ->
1359
    mod_pubsub_set_options;
561✔
1360
event_name(get, configure) ->
1361
    mod_pubsub_get_configure;
582✔
1362
event_name(set, configure) ->
1363
    mod_pubsub_set_configure;
631✔
1364
event_name(get, default) ->
1365
    mod_pubsub_get_default;
547✔
1366
event_name(set, delete) ->
1367
    mod_pubsub_set_delete;
1,585✔
1368
event_name(set, purge) ->
1369
    mod_pubsub_set_purge;
589✔
1370
event_name(get, subscriptions) ->
1371
    mod_pubsub_get_subscriptions;
673✔
1372
event_name(set, subscriptions) ->
1373
    mod_pubsub_set_subscriptions;
575✔
1374
event_name(get, affiliations) ->
1375
    mod_pubsub_get_affiliations;
589✔
1376
event_name(set, affiliations) ->
1377
    mod_pubsub_set_affiliations.
871✔
1378

1379
iq_action_result_to_measurements(_Time, {error, _}, From) ->
1380
    #{errors => 1, jid => From};
399✔
1381
iq_action_result_to_measurements(Time, _Result, From) ->
1382
    #{time => Time, count => 1, jid => From}.
5,412✔
1383

1384
iq_pubsub_set_create(Host, Node, From,
1385
                     #{server_host := ServerHost, access := Access, plugins := Plugins,
1386
                       action_el := CreateEl, query_el := QueryEl}) ->
1387
    Config = exml_query:subelement(QueryEl, <<"configure">>),
1,501✔
1388
    Type = exml_query:attr(CreateEl, <<"type">>, hd(Plugins)),
1,501✔
1389
    case lists:member(Type, Plugins) of
1,501✔
1390
        false ->
UNCOV
1391
            {error,
×
1392
             unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
1393
                               <<"create-nodes">>)};
1394
        true ->
1395
            create_node(Host, ServerHost, Node, From, Type, Access, Config)
1,501✔
1396
    end.
1397

1398
iq_pubsub_set_publish(_Host, <<>>, _From, _ExtraArgs) ->
1399
    {error, extended_error(mongoose_xmpp_errors:bad_request(), <<"nodeid-required">>)};
14✔
1400
iq_pubsub_set_publish(Host, Node, From, #{server_host := ServerHost, access := Access,
1401
                                          action_el := ActionEl, query_el := QueryEl}) ->
1402
    case jlib:remove_cdata(ActionEl#xmlel.children) of
1,354✔
1403
        [#xmlel{name = <<"item">>, children = Payload} = Element] ->
1404
            ItemId = exml_query:attr(Element, <<"id">>, <<>>),
1,354✔
1405
            PublishOptions = exml_query:subelement(QueryEl, <<"publish-options">>),
1,354✔
1406
            publish_item(Host, ServerHost, Node, From, ItemId,
1,354✔
1407
                         Payload, Access, PublishOptions);
1408
        [] ->
UNCOV
1409
            {error, extended_error(mongoose_xmpp_errors:bad_request(), <<"item-required">>)};
×
1410
        _ ->
1411
            {error, extended_error(mongoose_xmpp_errors:bad_request(), <<"invalid-payload">>)}
×
1412
    end.
1413

1414
iq_pubsub_set_retract(Host, Node, From,
1415
                      #{action_el := #xmlel{children = RetractSubEls} = Element}) ->
1416
    ForceNotify = case exml_query:attr(Element, <<"notify">>) of
56✔
UNCOV
1417
                      <<"1">> -> true;
×
UNCOV
1418
                      <<"true">> -> true;
×
1419
                      _ -> false
56✔
1420
                  end,
1421
    case jlib:remove_cdata(RetractSubEls) of
56✔
1422
        [#xmlel{name = <<"item">>} = Element1] ->
1423
            ItemId = exml_query:attr(Element1, <<"id">>, <<>>),
56✔
1424
            delete_item(Host, Node, From, ItemId, ForceNotify);
56✔
1425
        _ ->
UNCOV
1426
            {error,
×
1427
             extended_error(mongoose_xmpp_errors:bad_request(), <<"item-required">>)}
1428
    end.
1429

1430
iq_pubsub_set_subscribe(Host, Node, From, #{query_el := QueryEl,
1431
                                            action_el := #xmlel{} = El}) ->
1432
    ConfigXForm = case exml_query:subelement(QueryEl, <<"options">>) of
649✔
1433
                      undefined -> undefined;
579✔
1434
                      Options -> mongoose_data_forms:find_form(Options)
70✔
1435
                  end,
1436
    JID = exml_query:attr(El, <<"jid">>, <<>>),
649✔
1437
    subscribe_node(Host, Node, From, JID, ConfigXForm).
649✔
1438

1439
iq_pubsub_set_unsubscribe(Host, Node, From, #{action_el := #xmlel{} = El}) ->
1440
    JID = exml_query:attr(El, <<"jid">>, <<>>),
35✔
1441
    SubId = exml_query:attr(El, <<"subid">>, <<>>),
35✔
1442
    unsubscribe_node(Host, Node, From, JID, SubId).
35✔
1443

1444
iq_pubsub_get_items(Host, Node, From,
1445
                    #{query_el := QueryEl,
1446
                      action_el := #xmlel{children = GetItemsSubEls}= El}) ->
1447
    MaxItems = exml_query:attr(El, <<"max_items">>, <<>>),
231✔
1448
    SubId = exml_query:attr(El, <<"subid">>, <<>>),
231✔
1449
    ItemIds = extract_item_ids(GetItemsSubEls),
231✔
1450
    get_items(Host, Node, From, SubId, MaxItems, ItemIds, jlib:rsm_decode(QueryEl)).
231✔
1451

1452
extract_item_ids(GetItemsSubEls) ->
1453
    case lists:foldl(fun extract_item_id/2, [], GetItemsSubEls) of
231✔
1454
        [] ->
1455
            undefined;
175✔
1456
        List ->
1457
            List
56✔
1458
    end.
1459

1460
extract_item_id(#xmlel{name = <<"item">>} = Item, Acc) ->
1461
  case exml_query:attr(Item, <<"id">>) of
56✔
UNCOV
1462
      undefined -> Acc;
×
1463
      ItemId -> [ItemId | Acc]
56✔
1464
    end;
UNCOV
1465
extract_item_id(_, Acc) -> Acc.
×
1466

1467

1468
iq_pubsub_get_options(Host, Node, Lang, #{action_el := #xmlel{} = El}) ->
1469
    SubId = exml_query:attr(El, <<"subid">>, <<>>),
98✔
1470
    JID = exml_query:attr(El, <<"jid">>, <<>>),
98✔
1471
    get_options(Host, Node, JID, SubId, Lang).
98✔
1472

1473
iq_pubsub_set_options(Host, Node, #{action_el := #xmlel{} = ActionEl}) ->
1474
    XForm = mongoose_data_forms:find_form(ActionEl),
28✔
1475
    SubId = exml_query:attr(ActionEl, <<"subid">>, <<>>),
28✔
1476
    JID = exml_query:attr(ActionEl, <<"jid">>, <<>>),
28✔
1477
    set_options(Host, Node, JID, SubId, XForm).
28✔
1478

1479
-spec iq_pubsub_owner(
1480
        Host       :: mod_pubsub:host(),
1481
          ServerHost :: binary(),
1482
          From       ::jid:jid(),
1483
          IQType     :: 'get' | 'set',
1484
          SubEl      :: exml:element(),
1485
          Lang       :: binary())
1486
        -> {result, [exml:element()]}
1487
           | {error, exml:element() | [exml:element()] | {exml:element(), [exml:element()]}}.
1488
iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) ->
1489
    #xmlel{children = SubEls} = SubEl,
1,789✔
1490
    Action = jlib:remove_cdata(SubEls),
1,789✔
1491
    case Action of
1,789✔
1492
        [#xmlel{name = Name} = ActionEl] ->
1493
            Node = exml_query:attr(ActionEl, <<"node">>, <<>>),
1,775✔
1494
            ActionExtraArgs = #{server_host => ServerHost,
1,775✔
1495
                                action_el => ActionEl,
1496
                                lang => Lang},
1497
            mongoose_instrument:span(event_name(IQType, Name), #{host_type => ServerHost}, fun iq_pubsub_owner_action/6,
1,775✔
1498
                                     [IQType, Name, Host, From, Node, ActionExtraArgs],
1499
                                     fun(Time, Result) -> iq_action_result_to_measurements(Time, Result, From) end);
1,775✔
1500
        _ ->
1501
            ?LOG_INFO(#{what => pubsub_too_many_actions, exml_packet => Action}),
14✔
1502
            {error, mongoose_xmpp_errors:bad_request()}
14✔
1503
    end.
1504

1505
iq_pubsub_owner_action(IQType, Name, Host, From, Node, ExtraParams) ->
1506
    case {IQType, Name} of
1,775✔
1507
        {get, <<"configure">>} ->
1508
            get_configure(Host, Node, From, ExtraParams);
49✔
1509
        {set, <<"configure">>} ->
1510
            set_configure(Host, Node, From, ExtraParams);
98✔
1511
        {get, <<"default">>} ->
1512
            get_default(Host, Node, From, ExtraParams);
14✔
1513
        {set, <<"delete">>} ->
1514
            delete_node(Host, Node, From);
1,052✔
1515
        {set, <<"purge">>} ->
1516
            purge_node(Host, Node, From);
56✔
1517
        {get, <<"subscriptions">>} ->
1518
            get_subscriptions(Host, Node, From);
70✔
1519
        {set, <<"subscriptions">>} ->
1520
            set_subscriptions(Host, Node, From, ExtraParams);
42✔
1521
        {get, <<"affiliations">>} ->
1522
            get_affiliations(Host, Node, From);
56✔
1523
        {set, <<"affiliations">>} ->
1524
            set_affiliations(Host, Node, From, ExtraParams);
338✔
1525
        _ ->
UNCOV
1526
            {error, mongoose_xmpp_errors:feature_not_implemented()}
×
1527
    end.
1528

1529
iq_command(Host, ServerHost, From, IQ, Access, Plugins) ->
1530
    case adhoc:parse_request(IQ) of
42✔
1531
        Req when is_record(Req, adhoc_request) ->
1532
            case adhoc_request(Host, ServerHost, From, Req, Access, Plugins) of
42✔
1533
                Resp when is_record(Resp, xmlel) ->
1534
                    {result, [Resp]};
28✔
1535
                Error ->
1536
                    Error
14✔
1537
            end;
UNCOV
1538
        Err -> Err
×
1539
    end.
1540

1541
%% @doc <p>Processes an Ad Hoc Command.</p>
1542
adhoc_request(Host, _ServerHost, Owner,
1543
              Request = #adhoc_request{node = ?NS_PUBSUB_GET_PENDING,
1544
                                       action = <<"execute">>,
1545
                                       xdata = false},
1546
              _Access, Plugins) ->
1547
    send_pending_node_form(Request, Host, Owner, Plugins);
14✔
1548
adhoc_request(Host, _ServerHost, Owner,
1549
              Request = #adhoc_request{node = ?NS_PUBSUB_GET_PENDING,
1550
                                       action = <<"execute">>, xdata = XData},
1551
              _Access, _Plugins) ->
1552
    ParseOptions = adhoc_get_pending_parse_options(Host, XData),
28✔
1553
    case ParseOptions of
28✔
1554
        {result, XForm} ->
1555
            case lists:keysearch(node, 1, XForm) of
14✔
1556
                {value, {_, Node}} -> send_pending_auth_events(Request, Host, Node, Owner);
14✔
UNCOV
1557
                false -> {error, extended_error(mongoose_xmpp_errors:bad_request(), <<"bad-payload">>)}
×
1558
            end;
1559
        Error -> Error
14✔
1560
    end;
1561
adhoc_request(_Host, _ServerHost, _Owner,
1562
              #adhoc_request{action = <<"cancel">>} = Request, _Access,
1563
              _Plugins) ->
UNCOV
1564
    adhoc:produce_response(Request, canceled);
×
1565
adhoc_request(Host, ServerHost, Owner,
1566
              #adhoc_request{action = <<>>} = R, Access, Plugins) ->
UNCOV
1567
    adhoc_request(Host, ServerHost, Owner,
×
1568
                  R#adhoc_request{action = <<"execute">>}, Access,
1569
                  Plugins);
1570
adhoc_request(_Host, _ServerHost, _Owner, Other, _Access, _Plugins) ->
UNCOV
1571
    ?LOG_DEBUG(#{what => pubsub_adhoc_request_error,
×
UNCOV
1572
        text => <<"Couldn't process ad hoc command">>, command => Other}),
×
1573
    {error, mongoose_xmpp_errors:item_not_found()}.
×
1574

1575
%% @doc <p>Sends the process pending subscriptions XForm for Host to Owner.</p>
1576
send_pending_node_form(Request, Host, Owner, Plugins) ->
1577
    Filter = fun (Type) ->
14✔
1578
                     lists:member(<<"get-pending">>, plugin_features(Type))
14✔
1579
             end,
1580
    case lists:filter(Filter, Plugins) of
14✔
1581
        [] ->
UNCOV
1582
            {error, mongoose_xmpp_errors:feature_not_implemented()};
×
1583
        Ps ->
1584
            Options = get_pending_nodes(Host, Owner, Ps),
14✔
1585
            Field = #{type => <<"list-single">>, var => <<"pubsub#node">>,
14✔
1586
                      options => lists:usort(Options)},
1587
            Form = mongoose_data_forms:form(#{fields => [Field]}),
14✔
1588
            adhoc:produce_response(Request, executing, <<"execute">>, [Form])
14✔
1589
    end.
1590

1591
get_pending_nodes(Host, Owner, Plugins) ->
1592
    Tr = fun (Type) ->
14✔
1593
                 case node_call(Type, get_pending_nodes, [Host, Owner]) of
14✔
1594
                     {result, Nodes} -> Nodes;
14✔
UNCOV
1595
                     _ -> []
×
1596
                 end
1597
         end,
1598
    Action = fun() -> {result, lists:flatmap(Tr, Plugins)} end,
14✔
1599
    ErrorDebug = #{
14✔
1600
      action => get_pending_nodes,
1601
      pubsub_host => Host,
1602
      owner => Owner,
1603
      plugins => Plugins
1604
     },
1605
    case mod_pubsub_db_backend:dirty(Action, ErrorDebug) of
14✔
1606
        {result, Res} -> Res;
14✔
UNCOV
1607
        Err -> Err
×
1608
    end.
1609

1610
adhoc_get_pending_parse_options(Host, XEl) ->
1611
    case mongoose_data_forms:parse_form_fields(XEl) of
28✔
1612
        #{type := <<"submit">>, kvs := KVs} ->
1613
            case set_xoption(Host, maps:to_list(KVs), []) of
14✔
1614
                NewOpts when is_list(NewOpts) -> {result, NewOpts};
14✔
UNCOV
1615
                Err -> Err
×
1616
            end;
1617
        #{} ->
1618
            {error, mongoose_xmpp_errors:bad_request(<<"en">>, <<"Invalid form type">>)}
14✔
1619
    end.
1620

1621
%% @doc <p>Send a subscription approval form to Owner for all pending
1622
%% subscriptions on Host and Node.</p>
1623
send_pending_auth_events(Request, Host, Node, Owner) ->
1624
    ?LOG_DEBUG(#{what => pubsub_sending_pending_auth_events,
14✔
1625
        owner => jid:to_binary(Owner), sub_host => Host, pubsub_node => Node}),
14✔
1626
    Action = fun(PubSubNode) ->
14✔
1627
                     get_node_subscriptions_transaction(Owner, PubSubNode)
14✔
1628
             end,
1629
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
14✔
1630
        {result, {N, Subs}} ->
1631
            lists:foreach(fun
14✔
1632
                              ({J, pending, _SubId, _}) -> send_authorization_request(N, jid:make(J));
28✔
UNCOV
1633
                              (_) -> ok
×
1634
                         end,
1635
                          Subs),
1636
            adhoc:produce_response(Request, undefined);
14✔
1637
        Err ->
UNCOV
1638
            Err
×
1639
    end.
1640

1641
get_node_subscriptions_transaction(Owner, #pubsub_node{id = Nidx, type = Type}) ->
1642
    case lists:member(<<"get-pending">>, plugin_features(Type)) of
14✔
1643
        true ->
1644
            case node_call(Type, get_affiliation, [Nidx, Owner]) of
14✔
1645
                {result, owner} -> node_call(Type, get_node_subscriptions, [Nidx]);
14✔
UNCOV
1646
                _ -> {error, mongoose_xmpp_errors:forbidden()}
×
1647
            end;
1648
        false ->
UNCOV
1649
            {error, mongoose_xmpp_errors:feature_not_implemented()}
×
1650
    end.
1651

1652
%%% authorization handling
1653

1654
send_authorization_request(#pubsub_node{nodeid = {Host, Node}, owners = Owners},
1655
                           Subscriber) ->
1656
    Lang = <<"en">>,
91✔
1657
    Title = service_translations:do(Lang, <<"PubSub subscriber request">>),
91✔
1658
    Instructions = service_translations:do(Lang, <<"Choose whether to approve this entity's "
91✔
1659
                                               "subscription.">>),
1660
    Fields = [#{var => <<"pubsub#node">>,
91✔
1661
                type => <<"text-single">>,
1662
                label => service_translations:do(Lang, <<"Node ID">>),
1663
                values => [Node]},
1664
              #{var => <<"pubsub#subscriber_jid">>,
1665
                type => <<"jid-single">>,
1666
                label => service_translations:do(Lang, <<"Subscriber Address">>),
1667
                values => [jid:to_binary(Subscriber)]},
1668
              #{var => <<"pubsub#allow">>,
1669
                type => <<"boolean">>,
1670
                label => service_translations:do(Lang, <<"Allow this Jabber ID to subscribe to "
1671
                                                     "this pubsub node?">>),
1672
                values => [<<"false">>]}],
1673
    Form = mongoose_data_forms:form(#{title => Title, instructions => Instructions,
91✔
1674
                                      ns => ?NS_PUBSUB_SUB_AUTH, fields => Fields}),
1675
    Stanza = #xmlel{name = <<"message">>,
91✔
1676
                    attrs = #{<<"id">> => mongoose_bin:gen_from_crypto()},
1677
                    children = [Form]},
1678
    lists:foreach(fun(Owner) ->
91✔
1679
                          ejabberd_router:route(service_jid(Host), jid:make(Owner), Stanza)
91✔
1680
                  end, Owners).
1681

1682
find_authorization_response(El) ->
1683
    case mongoose_data_forms:find_form(El) of
42✔
1684
        undefined ->
UNCOV
1685
            none;
×
1686
        Form ->
1687
            case mongoose_data_forms:parse_form_fields(Form) of
42✔
1688
                #{type := <<"submit">>, ns := ?NS_PUBSUB_SUB_AUTH, kvs := KVs} ->
1689
                    KVs;
35✔
1690
                _ ->
1691
                    invalid
7✔
1692
            end
1693
    end.
1694

1695
%% @doc Send a message to JID with the supplied Subscription
1696
send_authorization_approval(Host, JID, SNode, Subscription) ->
1697
    Attrs1 = node_attr(SNode),
35✔
1698
    Attrs2 = case Subscription of
35✔
1699
                %  {S, SID} ->
1700
                %      Attrs1#{<<"subscription">> => subscription_to_string(S),
1701
                %              <<"subid">> => SID};
1702
                 S ->
1703
                     Attrs1#{<<"subscription">> => subscription_to_string(S)}
35✔
1704
             end,
1705
    Stanza = event_stanza(<<"subscription">>,
35✔
1706
                          Attrs2#{<<"jid">> => jid:to_binary(JID)}),
1707
    ejabberd_router:route(service_jid(Host), JID, Stanza).
35✔
1708

1709
handle_authorization_response(Acc, Host, From, To, Packet, XFields) ->
1710
    case XFields of
35✔
1711
        #{<<"pubsub#node">> := [Node],
1712
          <<"pubsub#subscriber_jid">> := [SSubscriber],
1713
          <<"pubsub#allow">> := [SAllow]} ->
1714
            FromLJID = jid:to_lower(jid:to_bare(From)),
35✔
1715
            Subscriber = jid:from_binary(SSubscriber),
35✔
1716
            Allow = string_allow_to_boolean(SAllow),
35✔
1717
            Action = fun (PubSubNode) ->
35✔
1718
                             handle_authorization_response_transaction(Host, FromLJID, Subscriber,
35✔
1719
                                                                       Allow, Node, PubSubNode)
1720
                     end,
1721
            case dirty(Host, Node, Action, ?FUNCTION_NAME) of
35✔
1722
                {error, Error} ->
UNCOV
1723
                    Err = make_error_reply(Packet, Error),
×
UNCOV
1724
                    ejabberd_router:route(To, From, Acc, Err);
×
1725
                {result, {_, _NewSubscription}} ->
1726
                    %% XXX: notify about subscription state change, section 12.11
1727
                    Acc;
35✔
1728
                _ ->
UNCOV
1729
                    Err = make_error_reply(Packet, mongoose_xmpp_errors:internal_server_error()),
×
UNCOV
1730
                    ejabberd_router:route(To, From, Acc, Err)
×
1731
            end;
1732
        _ ->
UNCOV
1733
            Err = make_error_reply(Packet, mongoose_xmpp_errors:not_acceptable()),
×
UNCOV
1734
            ejabberd_router:route(To, From, Acc, Err)
×
1735
    end.
1736

UNCOV
1737
string_allow_to_boolean(<<"1">>) -> true;
×
1738
string_allow_to_boolean(<<"true">>) -> true;
21✔
1739
string_allow_to_boolean(_) -> false.
14✔
1740

1741
handle_authorization_response_transaction(Host, FromLJID, Subscriber, Allow, Node,
1742
                                          #pubsub_node{type = Type, id = Nidx, owners = Owners}) ->
1743
    case lists:member(FromLJID, Owners) of
35✔
1744
        true ->
1745
            {result, Subs} = node_call(Type, get_subscriptions, [Nidx, Subscriber]),
35✔
1746
            update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs);
35✔
1747
        false ->
UNCOV
1748
            {error, mongoose_xmpp_errors:forbidden()}
×
1749
    end.
1750

1751
update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) ->
1752
    Sub = lists:filter(fun
35✔
1753
                          ({pending, _, _}) -> true;
35✔
UNCOV
1754
                          (_) -> false
×
1755
                     end,
1756
                      Subs),
1757
    case Sub of
35✔
1758
        [{pending, SubId, _}] ->
1759
            NewSub = case Allow of
35✔
1760
                         true -> subscribed;
21✔
1761
                         false -> none
14✔
1762
                     end,
1763
            node_call(Type, set_subscriptions, [Nidx, Subscriber, NewSub, SubId]),
35✔
1764
            send_authorization_approval(Host, Subscriber, Node, NewSub),
35✔
1765
            {result, ok};
35✔
1766
        _ ->
UNCOV
1767
            {error, mongoose_xmpp_errors:unexpected_request()}
×
1768
    end.
1769

1770
-define(XFIELD(Type, Label, Var, Val),
1771
        #{type => Type,
1772
          label => service_translations:do(Lang, Label),
1773
          var => Var,
1774
          values => [Val]}).
1775

1776
-define(BOOLXFIELD(Label, Var, Val),
1777
        ?XFIELD(<<"boolean">>, Label, Var,
1778
                case Val of
1779
                    true -> <<"1">>;
1780
                    _ -> <<"0">>
1781
                end)).
1782

1783
-define(STRINGXFIELD(Label, Var, Val),
1784
        ?XFIELD(<<"text-single">>, Label, Var, Val)).
1785

1786
-define(STRINGMXFIELD(Label, Var, Vals),
1787
        #{type => <<"text-multi">>,
1788
          label => service_translations:do(Lang, Label),
1789
          var => Var,
1790
          values => Vals}).
1791

1792
-define(XFIELDOPT(Type, Label, Var, Val, Opts),
1793
        #{type => Type,
1794
          label => service_translations:do(Lang, Label),
1795
          var => Var,
1796
          options => Opts,
1797
          values => [Val]}).
1798

1799
-define(LISTXFIELD(Label, Var, Val, Opts),
1800
        ?XFIELDOPT(<<"list-single">>, Label, Var, Val, Opts)).
1801

1802
-define(LISTMXFIELD(Label, Var, Vals, Opts),
1803
        #{type => <<"list-multi">>,
1804
          label => service_translations:do(Lang, Label),
1805
          var => Var,
1806
          options => Opts,
1807
          values => Vals}).
1808

1809
%% @doc <p>Create new pubsub nodes</p>
1810
%%<p>In addition to method-specific error conditions, there are several general reasons
1811
%%   why the node creation request might fail:</p>
1812
%%<ul>
1813
%%<li>The service does not support node creation.</li>
1814
%%<li>Only entities that are registered with the service are allowed to create nodes
1815
%%    but the requesting entity is not registered.</li>
1816
%%<li>The requesting entity does not have sufficient privileges to create nodes.</li>
1817
%%<li>The requested Node already exists.</li>
1818
%%<li>The request did not include a Node and "instant nodes" are not supported.</li>
1819
%%</ul>
1820
%%<p>Note: node creation is a particular case, error return code is evaluated at many places:</p>
1821
%%<ul>
1822
%%<li>iq_pubsub checks if service supports node creation (type exists)</li>
1823
%%<li>create_node checks if instant nodes are supported</li>
1824
%%<li>create_node asks node plugin if entity have sufficient privilege</li>
1825
%%<li>nodetree create_node checks if nodeid already exists</li>
1826
%%<li>node plugin create_node just sets default affiliation/subscription</li>
1827
%%</ul>
1828
%% @end
1829

1830
create_node(Host, ServerHost, Node, Owner, Type) ->
1831
    create_node(Host, ServerHost, Node, Owner, Type, all, undefined).
14✔
1832

1833
-spec create_node(Host, ServerHost, Node, Owner, Type, Access, Configuration) -> R when
1834
      Host          :: mod_pubsub:host(),
1835
      ServerHost    :: binary(),
1836
      Node          :: <<>> | mod_pubsub:nodeId(),
1837
      Owner         :: jid:jid(),
1838
      Type          :: binary(),
1839
      Access        :: atom(),
1840
      Configuration :: exml:element() | undefined,
1841
      R             :: {result, [exml:element(), ...]}
1842
                     | {error, exml:element()}.
1843
create_node(Host, ServerHost, <<>>, Owner, Type, Access, Configuration) ->
UNCOV
1844
    case lists:member(<<"instant-nodes">>, plugin_features(Type)) of
×
1845
        true ->
1846
            Node = mongoose_bin:gen_from_crypto(),
×
UNCOV
1847
            case create_node(Host, ServerHost, Node, Owner, Type, Access, Configuration) of
×
1848
                {result, _} ->
1849
                    {result, [#xmlel{name = <<"pubsub">>,
×
1850
                                     attrs = #{<<"xmlns">> => ?NS_PUBSUB},
1851
                                     children = [#xmlel{name = <<"create">>,
1852
                                                        attrs = node_attr(Node)}]}]};
1853
                Error ->
UNCOV
1854
                    Error
×
1855
            end;
1856
        false ->
UNCOV
1857
            {error, extended_error(mongoose_xmpp_errors:not_acceptable(), <<"nodeid-required">>)}
×
1858
    end;
1859
create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) ->
1860
    Type = select_type(ServerHost, Host, Node, GivenType),
1,636✔
1861
    case parse_create_node_options(Host, Type, Configuration) of
1,636✔
1862
        {result, NodeOptions} ->
1863
            CreateNode = fun () ->
1,608✔
1864
                                 create_node_transaction(Host, ServerHost, Node, Owner,
1,628✔
1865
                                                         Type, Access, NodeOptions)
1866
                         end,
1867
            ErrorDebug = #{
1,608✔
1868
              action => create_node,
1869
              pubsub_host => Host,
1870
              owner => Owner,
1871
              node_name => Node },
1872
            case mod_pubsub_db_backend:transaction(CreateNode, ErrorDebug) of
1,608✔
1873
                {result, {Nidx, SubsByDepth, {Result, broadcast}}} ->
1874
                    broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth),
1,601✔
1875
                    create_node_reply(Node, Result);
1,601✔
1876
                {result, {_Nidx, _SubsByDepth, Result}} ->
UNCOV
1877
                    create_node_reply(Node, Result);
×
1878
                Error ->
1879
                    %% in case we change transaction to sync_dirty...
1880
                    %%  node_call(Type, delete_node, [Host, Node]),
1881
                    %%  tree_call(Host, delete_node, [Host, Node]),
1882
                    Error
7✔
1883
            end;
1884
        Error ->
1885
            ?LOG_INFO(#{what => pubsub_bad_node_configuration,
28✔
1886
                        pubsub_node => Node, configuration => Configuration}),
28✔
1887
            Error
28✔
1888
    end.
1889

1890
parse_create_node_options(Host, Type, undefined) ->
1891
    {result, node_options(Host, Type)};
825✔
1892
parse_create_node_options(Host, Type, Configuration) ->
1893
    case mongoose_data_forms:find_and_parse_form(Configuration) of
811✔
1894
        #{type := <<"submit">>, kvs := KVs} ->
1895
            case set_xoption(Host, maps:to_list(KVs), node_options(Host, Type)) of
783✔
1896
                NewOpts when is_list(NewOpts) -> {result, NewOpts};
769✔
1897
                Err -> Err
14✔
1898
            end;
1899
        #{} ->
1900
            {error, mongoose_xmpp_errors:bad_request(<<"en">>, <<"Invalid form type">>)};
14✔
1901
        {error, _} ->
1902
            {result, node_options(Host, Type)}
14✔
1903
    end.
1904

1905
create_node_transaction(Host, ServerHost, Node, Owner, Type, Access, NodeOptions) ->
1906
    Parent = get_parent(Type, Node),
1,628✔
1907
    case node_call(Type, create_node_permission,
1,628✔
1908
                   [Host, ServerHost, Node, Parent, Owner, Access]) of
1909
        {result, true} ->
1910
            create_node_authorized_transaction(Host, Node, Parent, Owner, Type, NodeOptions);
1,628✔
1911
        _ ->
UNCOV
1912
            {error, mongoose_xmpp_errors:forbidden()}
×
1913
    end.
1914

1915
get_parent(Type, Node) ->
1916
    case node_call(Type, node_to_path, [Node]) of
1,628✔
1917
        {result, [Node]} ->
1918
            <<>>;
1,543✔
1919
        {result, Path} ->
1920
            element(2, node_call(Type, path_to_node, [lists:sublist(Path, length(Path)-1)]))
85✔
1921
    end.
1922

1923
create_node_authorized_transaction(Host, Node, Parent, Owner, Type, NodeOptions) ->
1924
    Parents = case Parent of
1,628✔
1925
                  <<>> -> [];
1,550✔
1926
                  _ -> [Parent]
78✔
1927
              end,
1928
    case tree_call(Host, create_node, [Host, Node, Type, Owner, NodeOptions, Parents]) of
1,628✔
1929
        {ok, Nidx} ->
1930
            SubsByDepth = get_node_subs_by_depth(Host, Node, Owner),
1,621✔
1931
            case node_call(Type, create_node, [Nidx, Owner]) of
1,608✔
1932
                {result, Result} -> {result, {Nidx, SubsByDepth, Result}};
1,601✔
UNCOV
1933
                Error -> Error
×
1934
            end;
1935
        {error, {virtual, Nidx}} ->
UNCOV
1936
            case node_call(Type, create_node, [Nidx, Owner]) of
×
UNCOV
1937
                {result, Result} -> {result, {Nidx, [], Result}};
×
1938
                Error -> Error
×
1939
            end;
1940
        Error ->
1941
            Error
7✔
1942
    end.
1943

1944
create_node_reply(Node, default) ->
1945
    {result, create_node_make_reply(Node)};
1,601✔
1946
create_node_reply(_Node, Result) ->
UNCOV
1947
    {result, Result}.
×
1948

1949
create_node_make_reply(Node) ->
1950
    [#xmlel{name = <<"pubsub">>,
1,601✔
1951
            attrs = #{<<"xmlns">> => ?NS_PUBSUB},
1952
            children = [#xmlel{name = <<"create">>,
1953
                               attrs = node_attr(Node)}]}].
1954

1955
%% @doc <p>Delete specified node and all childs.</p>
1956
%%<p>There are several reasons why the node deletion request might fail:</p>
1957
%%<ul>
1958
%%<li>The requesting entity does not have sufficient privileges to delete the node.</li>
1959
%%<li>The node is the root collection node, which cannot be deleted.</li>
1960
%%<li>The specified node does not exist.</li>
1961
%%</ul>
1962
-spec delete_node(
1963
        Host  :: mod_pubsub:host(),
1964
          Node  :: mod_pubsub:nodeId(),
1965
          Owner :: jid:jid())
1966
        -> {result, [exml:element(), ...]} | {error, exml:element()}.
1967
delete_node(_Host, <<>>, _Owner) ->
UNCOV
1968
    {error, mongoose_xmpp_errors:not_allowed()};
×
1969
delete_node(Host, Node, Owner) ->
1970
    Action = fun (PubSubNode) -> delete_node_transaction(Host, Owner, Node, PubSubNode) end,
1,052✔
1971
    case transaction(Host, Node, Action, ?FUNCTION_NAME) of
1,052✔
1972
        {result, {_, {SubsByDepth, {Result, broadcast, Removed}}}} ->
1973
            lists:foreach(fun ({RNode, _RSubs}) ->
1,023✔
1974
                                  {RH, RN} = RNode#pubsub_node.nodeid,
1,030✔
1975
                                  RNidx = RNode#pubsub_node.id,
1,030✔
1976
                                  RType = RNode#pubsub_node.type,
1,030✔
1977
                                  ROptions = RNode#pubsub_node.options,
1,030✔
1978
                                  broadcast_removed_node(RH, RN, RNidx,
1,030✔
1979
                                                         RType, ROptions, SubsByDepth)
1980
                          end,
1981
                          Removed),
1982
            case Result of
1,023✔
1983
                default -> {result, []};
1,023✔
UNCOV
1984
                _ -> {result, Result}
×
1985
            end;
1986
        {result, {_, {_, {Result, _}}}} ->
1987
            case Result of
29✔
UNCOV
1988
                default -> {result, []};
×
1989
                _ -> {result, Result}
29✔
1990
            end;
1991
        Error ->
UNCOV
1992
            Error
×
1993
    end.
1994

1995
delete_node_transaction(Host, Owner, Node, #pubsub_node{type = Type, id = Nidx}) ->
1996
    case node_call(Type, get_affiliation, [Nidx, Owner]) of
1,090✔
1997
        {result, owner} ->
1998
            SubsByDepth = get_node_subs_by_depth(Host, Node, service_jid(Host)),
1,090✔
1999
            Removed = tree_call(Host, delete_node, [Host, Node]),
1,076✔
2000
            case node_call(Type, delete_node, [Removed]) of
1,076✔
2001
                {result, Res} -> {result, {SubsByDepth, Res}};
1,052✔
UNCOV
2002
                Error -> Error
×
2003
            end;
2004
        _ ->
UNCOV
2005
            {error, mongoose_xmpp_errors:forbidden()}
×
2006
    end.
2007

2008
%% @see node_hometree:subscribe_node/5
2009
%% @doc <p>Accepts or rejects subcription requests on a PubSub node.</p>
2010
%%<p>There are several reasons why the subscription request might fail:</p>
2011
%%<ul>
2012
%%<li>The bare JID portions of the JIDs do not match.</li>
2013
%%<li>The node has an access model of "presence" and the requesting entity
2014
%%    is not subscribed to the owner's presence.</li>
2015
%%<li>The node has an access model of "roster" and the requesting entity
2016
%%    is not in one of the authorized roster groups.</li>
2017
%%<li>The node has an access model of "whitelist"
2018
%%    and the requesting entity is not on the whitelist.</li>
2019
%%<li>The service requires payment for subscriptions to the node.</li>
2020
%%<li>The requesting entity is anonymous and the service
2021
%%    does not allow anonymous entities to subscribe.</li>
2022
%%<li>The requesting entity has a pending subscription.</li>
2023
%%<li>The requesting entity is blocked from subscribing
2024
%%    (e.g., because having an affiliation of outcast).</li>
2025
%%<li>The node does not support subscriptions.</li>
2026
%%<li>The node does not exist.</li>
2027
%%</ul>
2028
-spec subscribe_node(
2029
        Host          :: mod_pubsub:host(),
2030
        Node          :: mod_pubsub:nodeId(),
2031
        From          ::jid:jid(),
2032
        JID           :: binary(),
2033
        ConfigurationXForm :: exml:element() | undefined)
2034
        -> {result, [exml:element(), ...]} | {error, exml:element()}.
2035
subscribe_node(Host, Node, From, JID, ConfigurationXForm) ->
2036
    SubOpts = case pubsub_form_utils:parse_sub_xform(ConfigurationXForm) of
649✔
2037
                  {ok, GoodSubOpts} -> GoodSubOpts;
649✔
UNCOV
2038
                  _ -> invalid
×
2039
              end,
2040
    Subscriber = string_to_ljid(JID),
649✔
2041
    Action = fun (PubSubNode) ->
649✔
2042
                     subscribe_node_transaction(Host, SubOpts, From, Subscriber, PubSubNode)
649✔
2043
             end,
2044
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
649✔
2045
        {result, {TNode, {Result, subscribed, SubId, send_last}}} ->
2046
            Nidx = TNode#pubsub_node.id,
56✔
2047
            Type = TNode#pubsub_node.type,
56✔
2048
            Options = TNode#pubsub_node.options,
56✔
2049
            send_items(Host, Node, Nidx, Type, Options, Subscriber, last),
56✔
2050
            case Result of
56✔
2051
                default -> {result, subscribe_node_reply(Subscriber, Node, {subscribed, SubId})};
56✔
UNCOV
2052
                _ -> {result, Result}
×
2053
            end;
2054
        {result, {_TNode, {default, subscribed, SubId}}} ->
2055
            {result, subscribe_node_reply(Subscriber, Node, {subscribed, SubId})};
502✔
2056
        {result, {_TNode, {Result, subscribed, _SubId}}} ->
UNCOV
2057
            {result, Result};
×
2058
        {result, {TNode, {default, pending, _SubId}}} ->
2059
            send_authorization_request(TNode, Subscriber),
63✔
2060
            {result, subscribe_node_reply(Subscriber, Node, pending)};
63✔
2061
        {result, {TNode, {Result, pending}}} ->
UNCOV
2062
            send_authorization_request(TNode, Subscriber),
×
UNCOV
2063
            {result, Result};
×
2064
        {result, {_, Result}} ->
2065
            {result, Result};
×
2066
        Error -> Error
28✔
2067
    end.
2068

2069
subscribe_node_transaction(Host, SubOpts, From, Subscriber, PubSubNode) ->
2070
    Features = plugin_features(PubSubNode#pubsub_node.type),
649✔
2071
    subscribe_node_transaction_step1(Host, SubOpts, From, Subscriber, PubSubNode, Features).
649✔
2072

2073
subscribe_node_transaction_step1(Host, SubOpts, From, Subscriber, PubSubNode, Features) ->
2074
    case lists:member(<<"subscribe">>, Features) of
649✔
2075
        false ->
UNCOV
2076
            {error, unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
×
2077
                                      <<"subscribe">>)};
2078
        true ->
2079
            subscribe_node_transaction_step2(Host, SubOpts, From, Subscriber, PubSubNode, Features)
649✔
2080
    end.
2081

2082
subscribe_node_transaction_step2(Host, SubOpts, From, Subscriber, PubSubNode, Features) ->
2083
    case get_option(PubSubNode#pubsub_node.options, subscribe) of
649✔
2084
        false ->
UNCOV
2085
            {error, unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
×
2086
                                      <<"subscribe">>)};
2087
        true ->
2088
            subscribe_node_transaction_step3(Host, SubOpts, From, Subscriber, PubSubNode, Features)
649✔
2089
    end.
2090

2091
subscribe_node_transaction_step3(Host, SubOpts, From, Subscriber, PubSubNode, Features) ->
2092
    case {SubOpts /= [], lists:member(<<"subscription-options">>, Features)} of
649✔
2093
        {true, false} ->
UNCOV
2094
           {error,
×
2095
            unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2096
                              <<"subscription-options">>)};
2097
        _ ->
2098
            subscribe_node_transaction_step4(Host, SubOpts, From, Subscriber, PubSubNode)
649✔
2099
    end.
2100

2101
subscribe_node_transaction_step4(_Host, invalid, _From, _Subscriber, _PubSubNode) ->
UNCOV
2102
    {error, extended_error(mongoose_xmpp_errors:bad_request(), <<"invalid-options">>)};
×
2103
subscribe_node_transaction_step4(Host, SubOpts, From, Subscriber,
2104
                                 #pubsub_node{options = Options, type = Type,
2105
                                              id = Nidx, owners = Owners}) ->
2106
    case check_subs_limit(Host, Type, Nidx) of
649✔
2107
        true ->
2108
           {error, extended_error(mongoose_xmpp_errors:not_allowed(), <<"closed-node">>)};
14✔
2109
        false ->
2110
            AccessModel = get_option(Options, access_model),
635✔
2111
            SendLast = get_option(Options, send_last_published_item),
635✔
2112
            AllowedGroups = get_option(Options, roster_groups_allowed, []),
635✔
2113

2114
            {PS, RG} = get_presence_and_roster_permissions(Host, Subscriber,
635✔
2115
                                                           Owners, AccessModel, AllowedGroups),
2116
            node_call(Type, subscribe_node,
635✔
2117
                      [Nidx, From, Subscriber, AccessModel,
2118
                       SendLast, PS, RG, SubOpts])
2119
    end.
2120

2121
check_subs_limit(Host, Type, Nidx) ->
2122
    case get_max_subscriptions_node(Host) of
649✔
2123
        Max when is_integer(Max) ->
2124
            case node_call(Type, get_node_subscriptions, [Nidx]) of
28✔
2125
                {result, NodeSubs} -> count_subscribed(NodeSubs) >= Max;
28✔
UNCOV
2126
                _ -> false
×
2127
            end;
2128
        _ ->
2129
            false
621✔
2130
    end.
2131

2132
count_subscribed(NodeSubs) ->
2133
    lists:foldl(
28✔
2134
      fun ({_JID, subscribed, _SubId, _Opts}, Acc) -> Acc+1;
14✔
UNCOV
2135
          (_, Acc) -> Acc
×
2136
      end, 0, NodeSubs).
2137

2138
subscribe_node_reply(Subscriber, Node, {subscribed, SubId}) ->
2139
    SubAttrs = #{<<"subscription">> => subscription_to_string(subscribed),
558✔
2140
                 <<"subid">> => SubId, <<"node">> => Node},
2141
    subscribe_node_reply(Subscriber, SubAttrs);
558✔
2142
subscribe_node_reply(Subscriber, Node, Subscription) ->
2143
    SubAttrs = #{<<"subscription">> => subscription_to_string(Subscription),
63✔
2144
                 <<"node">> => Node},
2145
    subscribe_node_reply(Subscriber, SubAttrs).
63✔
2146

2147
subscribe_node_reply(Subscriber, SubAttrs) ->
2148
    [#xmlel{name = <<"pubsub">>,
621✔
2149
            attrs = #{<<"xmlns">> => ?NS_PUBSUB},
2150
            children = [#xmlel{name = <<"subscription">>,
2151
                               attrs = SubAttrs#{<<"jid">> => jid:to_binary(Subscriber)}}]}].
2152

2153
%% @doc <p>Unsubscribe <tt>JID</tt> from the <tt>Node</tt>.</p>
2154
%%<p>There are several reasons why the unsubscribe request might fail:</p>
2155
%%<ul>
2156
%%<li>The requesting entity has multiple subscriptions to the node
2157
%%    but does not specify a subscription ID.</li>
2158
%%<li>The request does not specify an existing subscriber.</li>
2159
%%<li>The requesting entity does not have sufficient privileges
2160
%%    to unsubscribe the specified JID.</li>
2161
%%<li>The node does not exist.</li>
2162
%%<li>The request specifies a subscription ID that is not valid or current.</li>
2163
%%</ul>
2164
-spec unsubscribe_node(
2165
        Host  :: mod_pubsub:host(),
2166
          Node  :: mod_pubsub:nodeId(),
2167
          From  ::jid:jid(),
2168
          JID   :: binary() | jid:ljid(),
2169
          SubId :: mod_pubsub:subId())
2170
        -> {result, []} | {error, exml:element()}.
2171
unsubscribe_node(Host, Node, From, JID, SubId) when is_binary(JID) ->
2172
    unsubscribe_node(Host, Node, From, string_to_ljid(JID), SubId);
35✔
2173
unsubscribe_node(Host, Node, From, Subscriber, SubId) ->
2174
    Action = fun (#pubsub_node{type = Type, id = Nidx}) ->
35✔
2175
                     node_call(Type, unsubscribe_node, [Nidx, From, Subscriber, SubId])
35✔
2176
             end,
2177
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
35✔
2178
        {result, {_, default}} -> {result, []};
35✔
2179
 %      {result, {_, Result}} -> {result, Result};
UNCOV
2180
        Error -> Error
×
2181
    end.
2182

2183
%% @doc <p>Publish item to a PubSub node.</p>
2184
%% <p>The permission to publish an item must be verified by the plugin implementation.</p>
2185
%%<p>There are several reasons why the publish request might fail:</p>
2186
%%<ul>
2187
%%<li>The requesting entity does not have sufficient privileges to publish.</li>
2188
%%<li>The node does not support item publication.</li>
2189
%%<li>The node does not exist.</li>
2190
%%<li>The payload size exceeds a service-defined limit.</li>
2191
%%<li>The item contains more than one payload element or the namespace of the root payload element
2192
%%    does not match the configured namespace for the node.</li>
2193
%%<li>The request does not match the node configuration.</li>
2194
%%</ul>
2195
-spec publish_item(
2196
          Host       :: mod_pubsub:host(),
2197
          ServerHost :: binary(),
2198
          Node       :: mod_pubsub:nodeId(),
2199
          Publisher  ::jid:jid(),
2200
          ItemId     :: <<>> | mod_pubsub:itemId(),
2201
          Payload    :: mod_pubsub:payload())
2202
        -> {result, [exml:element(), ...]} | {error, exml:element()}.
2203
publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) ->
2204
    publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, all).
121✔
2205
publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, Access) ->
2206
    publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, Access, undefined).
121✔
2207
publish_item(Host, ServerHost, Node, Publisher, <<>>, Payload, Access, PublishOptions) ->
2208
    publish_item(Host, ServerHost, Node, Publisher, uniqid(), Payload, Access, PublishOptions);
346✔
2209
publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, Access, PublishOptions) ->
2210
    ItemPublisher = config(serverhost(Host), item_publisher),
1,475✔
2211
    Action =
1,475✔
2212
        fun (#pubsub_node{options = Options, type = Type, id = Nidx}) ->
2213
                Features = plugin_features(Type),
1,354✔
2214
                PublishFeature = lists:member(<<"publish">>, Features),
1,354✔
2215
                PubOptsFeature = lists:member(<<"publish-options">>, Features),
1,354✔
2216
                PublishModel = get_option(Options, publish_model),
1,354✔
2217
                DeliverPayloads = get_option(Options, deliver_payloads),
1,354✔
2218
                PersistItems = get_option(Options, persist_items),
1,354✔
2219
                MaxItems = max_items(Host, Options),
1,354✔
2220
                PayloadCount = payload_xmlelements(Payload),
1,354✔
2221
                PayloadSize = byte_size(term_to_binary(Payload)) - 2,
1,354✔
2222
                PayloadMaxSize = get_option(Options, max_payload_size),
1,354✔
2223

2224
                Errors = [ %% [{Condition :: boolean(), Reason :: term()}]
1,354✔
2225
                    {not PublishFeature,
2226
                     unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2227
                                       <<"publish">>)},
2228
                    {not PubOptsFeature andalso PublishOptions /= undefined,
1,354✔
2229
                     unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2230
                                       <<"publish-options">>)},
2231
                    {PayloadSize > PayloadMaxSize,
2232
                     extended_error(mongoose_xmpp_errors:not_acceptable(), <<"payload-too-big">>)},
2233
                    {(PayloadCount == 0) and (Payload == []),
2234
                     extended_error(mongoose_xmpp_errors:bad_request(), <<"payload-required">>)},
2235
                    {(PayloadCount > 1) or (PayloadCount == 0),
2236
                     extended_error(mongoose_xmpp_errors:bad_request(), <<"invalid-payload">>)},
2237
                    {(DeliverPayloads == false) and (PersistItems == false) and (PayloadSize > 0),
2238
                     extended_error(mongoose_xmpp_errors:bad_request(), <<"item-forbidden">>)},
2239
                    {((DeliverPayloads == true) or (PersistItems == true)) and (PayloadSize == 0),
2240
                     extended_error(mongoose_xmpp_errors:bad_request(), <<"item-required">>)},
2241
                    {PubOptsFeature andalso check_publish_options(Type, PublishOptions, Options),
1,354✔
2242
                     extended_error(mongoose_xmpp_errors:conflict(), <<"precondition-not-met">>)}
2243
                ],
2244

2245
                case lists:keyfind(true, 1, Errors) of
1,354✔
2246
                    {true, Reason} ->
2247
                        {error, Reason};
42✔
2248
                    false ->
2249
                        node_call(Type, publish_item,
1,312✔
2250
                                  [ServerHost, Nidx, Publisher, PublishModel, MaxItems, ItemId,
2251
                                   ItemPublisher, Payload, PublishOptions])
2252
                end
2253
        end,
2254
    Reply = [#xmlel{name = <<"pubsub">>,
1,475✔
2255
                    attrs = #{<<"xmlns">> => ?NS_PUBSUB},
2256
                    children = [#xmlel{name = <<"publish">>, attrs = node_attr(Node),
2257
                                       children = [#xmlel{name = <<"item">>,
2258
                                                          attrs = item_attr(ItemId)}]}]}],
2259
    ErrorItemNotFound = mongoose_xmpp_errors:item_not_found(),
1,475✔
2260
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
1,475✔
2261
        {result, {TNode, {Result, Broadcast, Removed}}} ->
2262
            Nidx = TNode#pubsub_node.id,
874✔
2263
            Type = TNode#pubsub_node.type,
874✔
2264
            Options = TNode#pubsub_node.options,
874✔
2265
            BrPayload = case Broadcast of
874✔
2266
                            broadcast -> Payload;
874✔
UNCOV
2267
                            PluginPayload -> PluginPayload
×
2268
                        end,
2269
            set_cached_item(Host, Nidx, ItemId, Publisher, BrPayload),
874✔
2270
            case get_option(Options, deliver_notifications) of
874✔
2271
                true ->
2272
                    broadcast_publish_item(Host, Node, Nidx, Type, Options, ItemId,
853✔
2273
                                           Publisher, BrPayload, Removed, ItemPublisher);
2274
                false ->
2275
                    ok
21✔
2276
            end,
2277
            case Result of
874✔
2278
                default -> {result, Reply};
874✔
UNCOV
2279
                _ -> {result, Result}
×
2280
            end;
2281
        {result, {TNode, {default, Removed}}} ->
UNCOV
2282
            Nidx = TNode#pubsub_node.id,
×
UNCOV
2283
            Type = TNode#pubsub_node.type,
×
2284
            Options = TNode#pubsub_node.options,
×
2285
            broadcast_retract_items(Host, Node, Nidx, Type, Options, Removed),
×
2286
            set_cached_item(Host, Nidx, ItemId, Publisher, Payload),
×
2287
            {result, Reply};
×
2288
        {result, {TNode, {Result, Removed}}} ->
2289
            Nidx = TNode#pubsub_node.id,
×
UNCOV
2290
            Type = TNode#pubsub_node.type,
×
2291
            Options = TNode#pubsub_node.options,
×
2292
            broadcast_retract_items(Host, Node, Nidx, Type, Options, Removed),
×
2293
            set_cached_item(Host, Nidx, ItemId, Publisher, Payload),
×
2294
            {result, Result};
×
2295
        {result, {_, default}} ->
2296
            {result, Reply};
354✔
2297
        {result, {_, Result}} ->
UNCOV
2298
            {result, Result};
×
2299
        {error, ErrorItemNotFound} ->
2300
            Type = select_type(ServerHost, Host, Node),
121✔
2301
            autocreate_if_supported_and_publish(Host, ServerHost, Node, Publisher,
121✔
2302
                                                Type, Access, ItemId, Payload, PublishOptions);
2303
        Error ->
2304
            Error
126✔
2305
    end.
2306

2307
autocreate_if_supported_and_publish(Host, ServerHost, Node, Publisher,
2308
                                    Type, Access, ItemId, Payload, PublishOptions) ->
2309
    ErrorItemNotFound = mongoose_xmpp_errors:item_not_found(),
121✔
2310
    case lists:member(<<"auto-create">>, plugin_features(Type)) of
121✔
2311
        true ->
2312
            case create_node(Host, ServerHost, Node, Publisher, Type, Access, PublishOptions) of
121✔
2313
                {result,
2314
                 [#xmlel{name = <<"pubsub">>,
2315
                         attrs = #{<<"xmlns">> := ?NS_PUBSUB},
2316
                         children = [#xmlel{name = <<"create">>,
2317
                                            attrs = #{<<"node">> := NewNode}}]}]} ->
2318
                    publish_item(Host, ServerHost, NewNode, Publisher, ItemId, Payload);
121✔
2319
                _ ->
UNCOV
2320
                    {error, ErrorItemNotFound}
×
2321
            end;
2322
        false ->
UNCOV
2323
            {error, ErrorItemNotFound}
×
2324
    end.
2325

2326
%% @doc <p>Delete item from a PubSub node.</p>
2327
%% <p>The permission to delete an item must be verified by the plugin implementation.</p>
2328
%%<p>There are several reasons why the item retraction request might fail:</p>
2329
%%<ul>
2330
%%<li>The publisher does not have sufficient privileges to delete the requested item.</li>
2331
%%<li>The node or item does not exist.</li>
2332
%%<li>The request does not specify a node.</li>
2333
%%<li>The request does not include an <item/> element
2334
%%    or the <item/> element does not specify an ItemId.</li>
2335
%%<li>The node does not support persistent items.</li>
2336
%%<li>The service does not support the deletion of items.</li>
2337
%%</ul>
2338
-spec delete_item(
2339
        Host      :: mod_pubsub:host(),
2340
          Node      :: mod_pubsub:nodeId(),
2341
          Publisher ::jid:jid(),
2342
          ItemId    :: mod_pubsub:itemId())
2343
        -> {result, []} | {error, exml:element()}.
2344
delete_item(Host, Node, Publisher, ItemId) ->
UNCOV
2345
    delete_item(Host, Node, Publisher, ItemId, false).
×
2346
delete_item(_, <<>>, _, _, _) ->
2347
    {error, extended_error(mongoose_xmpp_errors:bad_request(), <<"node-required">>)};
×
2348
delete_item(Host, Node, Publisher, ItemId, ForceNotify) ->
2349
    Action = fun(PubSubNode) -> delete_item_transaction(Publisher, ItemId, PubSubNode) end,
56✔
2350
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
56✔
2351
        {result, {TNode, {Result, broadcast}}} ->
2352
            Nidx = TNode#pubsub_node.id,
42✔
2353
            Type = TNode#pubsub_node.type,
42✔
2354
            Options = TNode#pubsub_node.options,
42✔
2355
            broadcast_retract_items(Host, Node, Nidx, Type, Options, [ItemId], ForceNotify),
42✔
2356
            case get_cached_item(Host, Nidx) of
42✔
UNCOV
2357
                #pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx);
×
2358
                _ -> ok
42✔
2359
            end,
2360
            case Result of
42✔
2361
                default -> {result, []};
42✔
UNCOV
2362
                _ -> {result, Result}
×
2363
            end;
2364
        {result, {_, default}} ->
UNCOV
2365
            {result, []};
×
2366
        {result, {_, Result}} ->
2367
            {result, Result};
×
2368
        Error ->
2369
            Error
14✔
2370
    end.
2371

2372
delete_item_transaction(Publisher, ItemId,
2373
                        #pubsub_node{options = Options, type = Type, id = Nidx}) ->
2374
    Features = plugin_features(Type),
56✔
2375
    case lists:member(<<"persistent-items">>, Features) of
56✔
2376
        true ->
2377
            case lists:member(<<"delete-items">>, Features) of
56✔
2378
                true ->
2379
                    PublishModel = get_option(Options, publish_model),
56✔
2380
                    node_call(Type, delete_item, [Nidx, Publisher, PublishModel, ItemId]);
56✔
2381
                false ->
UNCOV
2382
                    {error,
×
2383
                     unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2384
                                       <<"delete-items">>)}
2385
            end;
2386
        false ->
UNCOV
2387
            {error,
×
2388
             unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2389
                               <<"persistent-items">>)}
2390
    end.
2391

2392
%% @doc <p>Delete all items of specified node owned by JID.</p>
2393
%%<p>There are several reasons why the node purge request might fail:</p>
2394
%%<ul>
2395
%%<li>The node or service does not support node purging.</li>
2396
%%<li>The requesting entity does not have sufficient privileges to purge the node.</li>
2397
%%<li>The node is not configured to persist items.</li>
2398
%%<li>The specified node does not exist.</li>
2399
%%</ul>
2400
-spec purge_node(
2401
        Host  :: mod_pubsub:host(),
2402
          Node  :: mod_pubsub:nodeId(),
2403
          Owner :: jid:jid())
2404
        -> {result, []} | {error, exml:element()}.
2405
purge_node(Host, Node, Owner) ->
2406
    Action = fun (PubSubNode) -> purge_node_transaction(Owner, PubSubNode) end,
56✔
2407
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
56✔
2408
        {result, {TNode, {Result, broadcast}}} ->
2409
            Nidx = TNode#pubsub_node.id,
28✔
2410
            Type = TNode#pubsub_node.type,
28✔
2411
            Options = TNode#pubsub_node.options,
28✔
2412
            broadcast_purge_node(Host, Node, Nidx, Type, Options),
28✔
2413
            unset_cached_item(Host, Nidx),
28✔
2414
            case Result of
28✔
2415
                default -> {result, []};
28✔
UNCOV
2416
                _ -> {result, Result}
×
2417
            end;
2418
        {result, {_, default}} ->
UNCOV
2419
            {result, []};
×
2420
        {result, {_, Result}} ->
2421
            {result, Result};
×
2422
        Error ->
2423
            Error
28✔
2424
    end.
2425

2426
purge_node_transaction(Owner, #pubsub_node{options = Options, type = Type, id = Nidx}) ->
2427
    Features = plugin_features(Type),
56✔
2428
    case {lists:member(<<"purge-nodes">>, Features),
56✔
2429
          lists:member(<<"persistent-items">>, Features),
2430
          get_option(Options, persist_items)} of
2431
        {false, _, _} ->
UNCOV
2432
            {error,
×
2433
             unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2434
                               <<"purge-nodes">>)};
2435
        {_, false, _} ->
UNCOV
2436
            {error,
×
2437
             unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2438
                               <<"persistent-items">>)};
2439
        {_, _, false} ->
UNCOV
2440
            {error,
×
2441
             unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2442
                               <<"persistent-items">>)};
2443
        _ -> node_call(Type, purge_node, [Nidx, Owner])
56✔
2444
    end.
2445

2446
%% @doc <p>Return the items of a given node.</p>
2447
%% <p>The number of items to return is limited by MaxItems.</p>
2448
%% <p>The permission are not checked in this function.</p>
2449
%% @todo We probably need to check that the user doing the query has the right
2450
%% to read the items.
2451
-spec get_items(Host :: mod_pubsub:host(),
2452
                Node :: mod_pubsub:nodeId(),
2453
                From ::jid:jid(),
2454
                SubId :: mod_pubsub:subId(),
2455
                SMaxItems :: binary(),
2456
                ItemIds :: [mod_pubsub:itemId()],
2457
                Rsm :: none | jlib:rsm_in()) -> {result, [exml:element(), ...]} | {error, exml:element()}.
2458
get_items(Host, Node, From, SubId, <<>>, ItemIds, RSM) ->
2459
    MaxItems = case get_max_items_node(Host) of
231✔
UNCOV
2460
                   undefined -> ?MAXITEMS;
×
2461
                   Max -> Max
231✔
2462
               end,
2463
    get_items_with_limit(Host, Node, From, SubId, ItemIds, RSM, MaxItems);
231✔
2464
get_items(Host, Node, From, SubId, SMaxItems, ItemIds, RSM) ->
UNCOV
2465
    MaxItems = case catch binary_to_integer(SMaxItems) of
×
UNCOV
2466
                   {'EXIT', _} -> {error, mongoose_xmpp_errors:bad_request()};
×
2467
                   Val -> Val
×
2468
               end,
2469
    get_items_with_limit(Host, Node, From, SubId, ItemIds, RSM, MaxItems).
×
2470

2471
get_items_with_limit(_Host, _Node, _From, _SubId, _ItemIds, _RSM, {error, _} = Err) ->
UNCOV
2472
    Err;
×
2473
get_items_with_limit(Host, Node, From, SubId, ItemIds, RSM, MaxItems) ->
2474
    Action = fun (PubSubNode) ->
231✔
2475
                     get_items_transaction(Host, From, RSM, SubId, PubSubNode, MaxItems, ItemIds)
231✔
2476
             end,
2477
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
231✔
2478
        {result, {_, {Items, RsmOut}}} ->
2479
            {result,
231✔
2480
             [#xmlel{name = <<"pubsub">>,
2481
                     attrs = #{<<"xmlns">> => ?NS_PUBSUB},
2482
                     children =
2483
                     [#xmlel{name = <<"items">>, attrs = node_attr(Node),
2484
                             children = items_els(Items)}
2485
                      | jlib:rsm_encode(RsmOut)]}]};
2486
        Error ->
UNCOV
2487
            Error
×
2488
    end.
2489

2490
get_items_transaction(Host, From, RSM, SubId,
2491
                      #pubsub_node{options = Options, type = Type, id = Nidx, owners = Owners},
2492
                      MaxItems, ItemIds) ->
2493
    Features = plugin_features(Type),
231✔
2494
    case {lists:member(<<"retrieve-items">>, Features),
231✔
2495
          lists:member(<<"persistent-items">>, Features)} of
2496
        {false, _} ->
UNCOV
2497
            {error,
×
2498
             unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2499
                               <<"retrieve-items">>)};
2500
        {_, false} ->
UNCOV
2501
           {error,
×
2502
            unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2503
                              <<"persistent-items">>)};
2504
        _ ->
2505
            AccessModel = get_option(Options, access_model),
231✔
2506
            AllowedGroups = get_option(Options, roster_groups_allowed, []),
231✔
2507
            {PS, RG} = get_presence_and_roster_permissions(Host, From, Owners,
231✔
2508
                                                           AccessModel, AllowedGroups),
2509
            Opts = #{access_model => AccessModel,
231✔
2510
                     presence_permission => PS,
2511
                     roster_permission => RG,
2512
                     rsm => RSM,
2513
                     max_items => MaxItems,
2514
                     item_ids => ItemIds,
2515
                     subscription_id => SubId},
2516

2517
            node_call(Type, get_items_if_authorised, [Nidx, From, Opts])
231✔
2518
    end.
2519

2520
get_items(Host, Node) ->
2521
    Action = fun (#pubsub_node{type = Type, id = Nidx}) ->
28✔
2522
                     node_call(Type, get_items, [Nidx, service_jid(Host), #{}])
14✔
2523
             end,
2524
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
28✔
2525
        {result, {_, {Items, _}}} -> Items;
14✔
2526
        Error -> Error
14✔
2527
    end.
2528

2529
get_item(Host, Node, ItemId) ->
2530
    Action = fun (#pubsub_node{type = Type, id = Nidx}) ->
28✔
2531
                     node_call(Type, get_item, [Nidx, ItemId])
28✔
2532
             end,
2533
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
28✔
2534
        {result, {_, Items}} -> Items;
14✔
2535
        Error -> Error
14✔
2536
    end.
2537

2538
get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) ->
2539
    case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, none) of
14✔
2540
        {result, {Items, _RSM}} -> {result, Items};
14✔
UNCOV
2541
        Error -> Error
×
2542
    end.
2543
get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, RSM) ->
2544
    AccessModel = get_option(Options, access_model),
42✔
2545
    AllowedGroups = get_option(Options, roster_groups_allowed, []),
42✔
2546
    {PS, RG} = get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups),
42✔
2547
    Opts = #{access_model => AccessModel,
42✔
2548
             presence_permission => PS,
2549
             roster_permission => RG,
2550
             rsm => RSM},
2551
    node_call(Type, get_items_if_authorised, [Nidx, From, Opts]).
42✔
2552

2553
get_last_item(Host, Type, Nidx, LJID) ->
2554
    case get_cached_item(Host, Nidx) of
98✔
2555
        false ->
2556
            case node_action(Host, Type, get_items, [Nidx, LJID, #{}]) of
63✔
2557
                {result, {[LastItem|_], _}} -> LastItem;
35✔
2558
                _ -> undefined
28✔
2559
            end;
2560
        LastItem ->
2561
            LastItem
35✔
2562
    end.
2563

2564
get_last_items(Host, Type, Nidx, LJID, Number) ->
UNCOV
2565
    case node_action(Host, Type, get_items, [Nidx, LJID, #{}]) of
×
UNCOV
2566
        {result, {Items, _}} -> lists:sublist(Items, Number);
×
2567
        _ -> []
×
2568
    end.
2569

2570
%% @doc <p>Resend the items of a node to the user.</p>
2571
%% @todo use cache-last-item feature
2572
send_items(Host, Node, Nidx, Type, Options, LJID, last) ->
2573
    case get_last_item(Host, Type, Nidx, LJID) of
98✔
2574
        undefined ->
2575
            ok;
28✔
2576
        LastItem ->
2577
            Stanza = items_event_stanza(Node, [LastItem]),
70✔
2578
            dispatch_items(Host, LJID, Options, Stanza)
70✔
2579
    end;
2580
send_items(Host, Node, Nidx, Type, Options, LJID, Number) when Number > 0 ->
UNCOV
2581
    Stanza = items_event_stanza(Node, get_last_items(Host, Type, Nidx, Number, LJID)),
×
UNCOV
2582
    dispatch_items(Host, LJID, Options, Stanza);
×
2583
send_items(Host, Node, _Nidx, _Type, Options, LJID, _) ->
2584
    Stanza = items_event_stanza(Node, []),
×
UNCOV
2585
    dispatch_items(Host, LJID, Options, Stanza).
×
2586

2587
dispatch_items({_, FromS, _} = From, To, Options, Stanza) ->
2588
    NotificationType = get_option(Options, notification_type, headline),
42✔
2589
    Message = add_message_type(Stanza, NotificationType),
42✔
2590
    {ok, HostType} = mongoose_domain_api:get_domain_host_type(FromS),
42✔
2591
    FromJid = jid:make(From),
42✔
2592
    ToJid = jid:make(To),
42✔
2593
    AccParams = #{host_type => HostType, lserver => FromS, location => ?LOCATION,
42✔
2594
                  element => Message, from_jid => FromJid, to_jid => ToJid},
2595
    Acc = mongoose_acc:new(AccParams),
42✔
2596
    ejabberd_router:route(FromJid, ToJid, Acc);
42✔
2597
dispatch_items(From, To, Options, Stanza) ->
2598
    NotificationType = get_option(Options, notification_type, headline),
28✔
2599
    Message = add_message_type(Stanza, NotificationType),
28✔
2600
    ejabberd_router:route(service_jid(From), jid:make(To), Message).
28✔
2601

2602
%% @doc <p>Return the list of affiliations as an XMPP response.</p>
2603
-spec get_affiliations(
2604
        Host    :: mod_pubsub:host(),
2605
        Node    :: mod_pubsub:nodeId(),
2606
        JID     :: jid:jid(),
2607
        Plugins :: #{plugins := [binary()]})
2608
        -> {result, [exml:element()]} | {error, exml:element()}.
2609
get_affiliations(Host, Node, JID, #{plugins := Plugins}) when is_list(Plugins) ->
UNCOV
2610
    Result = lists:foldl(
×
2611
               fun(Type, {Status, Acc}) ->
2612
                       Features = plugin_features(Type),
×
UNCOV
2613
                       case lists:member(<<"retrieve-affiliations">>, Features) of
×
2614
                           true ->
2615
                               {result, Affs} = node_action(Host, Type, get_entity_affiliations,
×
2616
                                                            [Host, JID]),
2617
                               {Status, [Affs | Acc]};
×
2618
                           false ->
2619
                               {{error,
×
2620
                                 unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2621
                                                   <<"retrieve-affiliations">>)},
2622
                                Acc}
2623
                       end
2624
               end, {ok, []}, Plugins),
UNCOV
2625
    case Result of
×
2626
        {ok, Affs} ->
2627
            Entities = lists:flatmap(
×
2628
                         fun ({_, none}) ->
2629
                                 [];
×
2630
                             ({#pubsub_node{nodeid = {_, NodeId}}, Aff}) when
2631
                                   Node == <<>> orelse Node == NodeId ->
UNCOV
2632
                                 Attrs = node_attr(NodeId),
×
UNCOV
2633
                                 [#xmlel{name = <<"affiliation">>,
×
2634
                                         attrs = Attrs#{<<"affiliation">> => affiliation_to_string(Aff)}}];
2635
                             (_) ->
UNCOV
2636
                                 []
×
2637
                         end,
2638
                         lists:usort(lists:flatten(Affs))),
UNCOV
2639
            {result,
×
2640
             [#xmlel{name = <<"pubsub">>,
2641
                     attrs = #{<<"xmlns">> => ?NS_PUBSUB},
2642
                     children = [#xmlel{name = <<"affiliations">>,
2643
                                        children = Entities}]}]};
2644
        {Error, _} ->
UNCOV
2645
            Error
×
2646
    end.
2647

2648
-spec get_affiliations(Host :: mod_pubsub:host(), Node :: mod_pubsub:nodeId(), JID :: jid:jid()) ->
2649
    {result, [exml:element(), ...]} | {error, exml:element()}.
2650
get_affiliations(Host, Node, JID) ->
2651
    Action = fun (PubSubNode) -> get_affiliations_transaction(JID, PubSubNode) end,
56✔
2652
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
56✔
2653
        {result, {_, []}} ->
UNCOV
2654
            {error, mongoose_xmpp_errors:item_not_found()};
×
2655
        {result, {_, Affs}} ->
2656
            Entities =
42✔
2657
            lists:flatmap(fun({_, none}) ->
UNCOV
2658
                                  [];
×
2659
                             ({AJID, Aff}) ->
2660
                                  [#xmlel{
42✔
2661
                                      name = <<"affiliation">>,
2662
                                      attrs = #{<<"jid">> => jid:to_binary(AJID),
2663
                                                <<"affiliation">> => affiliation_to_string(Aff)}}]
2664
                          end, Affs),
2665
            {result,
42✔
2666
             [#xmlel{name = <<"pubsub">>,
2667
                     attrs = #{<<"xmlns">> => ?NS_PUBSUB_OWNER},
2668
                     children = [#xmlel{name = <<"affiliations">>,
2669
                                        attrs = node_attr(Node), children = Entities}]}]};
2670
        Error ->
2671
            Error
14✔
2672
    end.
2673

2674
get_affiliations_transaction(JID, #pubsub_node{type = Type, id = Nidx}) ->
2675
    Features = plugin_features(Type),
56✔
2676
    case lists:member(<<"modify-affiliations">>, Features) of
56✔
2677
        true ->
2678
            case node_call(Type, get_affiliation, [Nidx, JID]) of
56✔
2679
                {result, owner} ->
2680
                    node_call(Type, get_node_affiliations, [Nidx]);
42✔
2681
                _ ->
2682
                    {error, mongoose_xmpp_errors:forbidden()}
14✔
2683
            end;
2684
        false ->
UNCOV
2685
            {error,
×
2686
             unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2687
                               <<"modify-affiliations">>)}
2688
    end.
2689

2690
-spec set_affiliations(
2691
        Host        :: mod_pubsub:host(),
2692
        Node        :: mod_pubsub:nodeId(),
2693
        From        ::jid:jid(),
2694
        EntitiesEls :: #{action_el := exml:element()})
2695
        -> {result, []} | {error, exml:element() | {exml:element(), [exml:element()]}} | {error, exml:element()}.
2696
set_affiliations(Host, Node, From, #{action_el := ActionEl} ) ->
2697
    EntitiesEls = jlib:remove_cdata(ActionEl#xmlel.children),
338✔
2698
    Owner = jid:to_lower(jid:to_bare(From)),
338✔
2699
    Entities = lists:foldl(fun
338✔
2700
                               (_, error) ->
UNCOV
2701
                                  error;
×
2702
                               (#xmlel{name = <<"affiliation">>} = El, Acc) ->
2703
                                   JID = jid:from_binary(exml_query:attr(El, <<"jid">>, <<>>)),
366✔
2704
                                   Affiliation = string_to_affiliation(
366✔
2705
                                                   exml_query:attr(El, <<"affiliation">>, <<>>)),
2706
                                   case (JID == error) or (Affiliation == false) of
366✔
UNCOV
2707
                                       true -> error;
×
2708
                                       false -> [{jid:to_lower(JID), Affiliation} | Acc]
366✔
2709
                                   end;
2710
                               (_, _) ->
UNCOV
2711
                                   error
×
2712
                          end,
2713
                           [], EntitiesEls),
2714
    case Entities of
338✔
2715
        error ->
UNCOV
2716
            {error, mongoose_xmpp_errors:bad_request()};
×
2717
        _ ->
2718
            Action = fun (PubSubNode) ->
338✔
2719
                             set_affiliations_transaction(Host, Owner, PubSubNode, Entities)
338✔
2720
                     end,
2721
            case dirty(Host, Node, Action, ?FUNCTION_NAME) of
338✔
2722
                {result, {_, Result}} -> {result, Result};
324✔
2723
                Other -> Other
14✔
2724
            end
2725
    end.
2726

2727
set_affiliations_transaction(Host, Owner,
2728
                             #pubsub_node{owners = Owners, nodeid = {_, NodeId}} = N, Entities) ->
2729
    case lists:member(Owner, Owners) of
338✔
2730
        true ->
2731
            % It is a very simple check, as XEP doesn't state any
2732
            % other invalid affiliation transitions
2733
            OwnersDryRun = lists:foldl(
338✔
2734
                             fun({JID, owner}, Acc) ->
2735
                                     sets:add_element(jid:to_bare(JID), Acc);
14✔
2736
                                ({JID, _}, Acc) ->
2737
                                     sets:del_element(jid:to_bare(JID), Acc)
352✔
2738
                             end, sets:from_list(Owners), Entities),
2739
            case sets:size(OwnersDryRun) of
338✔
2740
                0 ->
2741
                    OwnersPayload = [ #xmlel{ name = <<"affiliation">>,
14✔
2742
                                              attrs = #{<<"jid">> => jid:to_binary(Unchanged),
2743
                                                        <<"affiliation">> => <<"owner">>} }
2744
                                      || Unchanged <- Owners ],
14✔
2745
                    AffiliationsPayload = #xmlel{ name = <<"affiliations">>,
14✔
2746
                                                  attrs = #{<<"node">> => NodeId},
2747
                                                  children = OwnersPayload },
2748
                    NewPubSubPayload = #xmlel{ name = <<"pubsub">>,
14✔
2749
                                               attrs = #{<<"xmlns">> => ?NS_PUBSUB_OWNER},
2750
                                               children = [AffiliationsPayload] },
2751
                    {error, {mongoose_xmpp_errors:not_acceptable(), [NewPubSubPayload]}};
14✔
2752
                _ ->
2753
                    set_validated_affiliations_transaction(Host, N, Owners, Entities),
324✔
2754
                    {result, []}
324✔
2755
            end;
2756
        _ ->
UNCOV
2757
            {error, mongoose_xmpp_errors:forbidden()}
×
2758
    end.
2759

2760
set_validated_affiliations_transaction(Host, #pubsub_node{ type = Type, id = Nidx } = N,
2761
                                       Owners, Entities) ->
2762
    lists:foreach(fun ({JID, owner}) ->
324✔
2763
                          node_call(Type, set_affiliation, [Nidx, JID, owner]),
14✔
2764
                          NewOwner = jid:to_bare(JID),
14✔
2765
                          NewOwners = [NewOwner | Owners],
14✔
2766
                          tree_call(Host,
14✔
2767
                                    set_node,
2768
                                    [N#pubsub_node{owners = NewOwners}]);
2769
                      ({JID, none}) ->
2770
                          node_call(Type, set_affiliation, [Nidx, JID, none]),
14✔
2771
                          OldOwner = jid:to_bare(JID),
14✔
2772
                          case lists:member(OldOwner, Owners) of
14✔
2773
                              true ->
2774
                                  NewOwners = Owners -- [OldOwner],
14✔
2775
                                  tree_call(Host,
14✔
2776
                                            set_node,
2777
                                            [N#pubsub_node{owners = NewOwners}]);
2778
                              _ ->
UNCOV
2779
                                  ok
×
2780
                          end;
2781
                      ({JID, Affiliation}) ->
2782
                          node_call(Type, set_affiliation, [Nidx, JID, Affiliation])
324✔
2783
                  end,
2784
                  Entities).
2785

2786
get_options(Host, Node, JID, SubId, Lang) ->
2787
    Action = fun(PubSubNode) ->
98✔
2788
                     get_options_transaction(Node, JID, SubId, Lang, PubSubNode)
98✔
2789
             end,
2790
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
98✔
2791
        {result, {_Node, XForm}} -> {result, [XForm]};
56✔
2792
        Error -> Error
42✔
2793
    end.
2794

2795
get_options_transaction(Node, JID, SubId, Lang, #pubsub_node{type = Type, id = Nidx}) ->
2796
    case lists:member(<<"subscription-options">>, plugin_features(Type)) of
98✔
2797
        true ->
2798
            get_sub_options_xml(JID, Lang, Node, Nidx, SubId, Type);
98✔
2799
        false ->
UNCOV
2800
            {error,
×
2801
             unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2802
                               <<"subscription-options">>)}
2803
    end.
2804

2805
% TODO: Support Lang at some point again
2806
get_sub_options_xml(JID, _Lang, Node, Nidx, RequestedSubId, Type) ->
2807
    Subscriber = string_to_ljid(JID),
98✔
2808
    {result, Subs} = node_call(Type, get_subscriptions, [Nidx, Subscriber]),
98✔
2809
    SubscribedSubs = [{Id, Opts} || {Sub, Id, Opts} <- Subs, Sub == subscribed],
98✔
2810

2811
    case {RequestedSubId, SubscribedSubs} of
98✔
2812
        {_, []} ->
2813
            {error, extended_error(mongoose_xmpp_errors:not_acceptable(), <<"not-subscribed">>)};
42✔
2814
        {<<>>, [{TheOnlySID, Opts}]} ->
2815
            make_and_wrap_sub_xform(Opts, Node, Subscriber, TheOnlySID);
56✔
2816
        {<<>>, _} ->
UNCOV
2817
            {error, extended_error(mongoose_xmpp_errors:not_acceptable(), <<"subid-required">>)};
×
2818
        {_, _} ->
2819
            case lists:keyfind(RequestedSubId, 1, SubscribedSubs) of
×
2820
                {_, Opts} ->
2821
                    make_and_wrap_sub_xform(Opts, Node, Subscriber, RequestedSubId);
×
2822
                _ ->
2823
                    {error, extended_error(mongoose_xmpp_errors:not_acceptable(),
×
2824
                                           <<"invalid-subid">>)}
2825
            end
2826
    end.
2827

2828
make_and_wrap_sub_xform(Options, Node, Subscriber, SubId) ->
2829
    {ok, XForm} = pubsub_form_utils:make_sub_xform(Options),
56✔
2830
    Attrs = node_attr(Node),
56✔
2831
    OptionsEl = #xmlel{name = <<"options">>,
56✔
2832
                       attrs = Attrs#{<<"jid">> => jid:to_binary(Subscriber),
2833
                                      <<"subid">> => SubId},
2834
                       children = [XForm]},
2835
    PubSubEl = #xmlel{name = <<"pubsub">>,
56✔
2836
                      attrs = #{<<"xmlns">> => ?NS_PUBSUB},
2837
                      children = [OptionsEl]},
2838
    {result, PubSubEl}.
56✔
2839

2840
set_options(Host, Node, JID, SubId, ConfigXForm) ->
2841
    Action = fun(PubSubNode) ->
28✔
2842
                     ok = set_options_transaction(JID, SubId, ConfigXForm, PubSubNode),
28✔
2843
                     {result, []}
28✔
2844
             end,
2845
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
28✔
2846
        {result, {_Node, Result}} -> {result, Result};
28✔
UNCOV
2847
        Error -> Error
×
2848
    end.
2849

2850
set_options_transaction(JID, SubId, ConfigXForm, #pubsub_node{type = Type, id = Nidx}) ->
2851
    case lists:member(<<"subscription-options">>, plugin_features(Type)) of
28✔
2852
        true ->
2853
            validate_and_set_options_helper(ConfigXForm, JID, Nidx, SubId, Type);
28✔
2854
        false ->
UNCOV
2855
            {error,
×
2856
             unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2857
                               <<"subscription-options">>)}
2858
    end.
2859

2860
validate_and_set_options_helper(ConfigXForm, JID, Nidx, SubId, Type) ->
2861
    SubOpts = pubsub_form_utils:parse_sub_xform(ConfigXForm),
28✔
2862
    set_options_helper(SubOpts, JID, Nidx, SubId, Type).
28✔
2863

2864
set_options_helper({error, Reason}, JID, Nidx, RequestedSubId, _Type) ->
2865
    % TODO: Make smarter logging (better details formatting)
UNCOV
2866
    ?LOG_DEBUG(#{what => pubsub_invalid_subscription_options, jid => JID,
×
UNCOV
2867
        nidx => Nidx, sub_id => RequestedSubId, reason => Reason}),
×
2868
    {error, extended_error(mongoose_xmpp_errors:bad_request(), <<"invalid-options">>)};
×
2869
set_options_helper({ok, []}, _JID, _Nidx, _RequestedSubId, _Type) ->
2870
    {result, []};
×
2871
set_options_helper({ok, SubOpts}, JID, Nidx, RequestedSubId, Type) ->
2872
    Subscriber = string_to_ljid(JID),
28✔
2873
    {result, Subs} = node_call(Type, get_subscriptions, [Nidx, Subscriber]),
28✔
2874
    SubIds = [Id || {Sub, Id, _Opts} <- Subs, Sub == subscribed],
28✔
2875
    case {RequestedSubId, SubIds} of
28✔
2876
        {_, []} ->
UNCOV
2877
            {error, extended_error(mongoose_xmpp_errors:not_acceptable(), <<"not-subscribed">>)};
×
2878
        {<<>>, [TheOnlySID]} ->
2879
            mod_pubsub_db_backend:set_subscription_opts(Nidx, Subscriber, TheOnlySID, SubOpts);
28✔
2880
        {<<>>, _} ->
UNCOV
2881
            {error, extended_error(mongoose_xmpp_errors:not_acceptable(), <<"subid-required">>)};
×
2882
        {_, _} ->
2883
            case lists:member(RequestedSubId, SubIds) of
×
2884
                true ->
2885
                    mod_pubsub_db_backend:set_subscription_opts(Nidx, Subscriber, RequestedSubId,
×
2886
                                                                SubOpts);
2887
                false ->
UNCOV
2888
                    {error, extended_error(mongoose_xmpp_errors:not_acceptable(),
×
2889
                                           <<"invalid-subid">>)}
2890
            end
2891
    end.
2892

2893
%% @doc <p>Return the list of subscriptions as an XMPP response.</p>
2894
-spec get_subscriptions(Host, Node, JID, Plugins) -> {error, Reason} | {result, Response} when
2895
    Host :: host(),
2896
    Node :: pubsubNode(),
2897
    JID :: jid:jid(),
2898
    Plugins :: map(),
2899
    Reason :: any(),
2900
    Response :: [exml:element()].
2901
get_subscriptions(Host, Node, JID, #{plugins := Plugins}) when is_list(Plugins) ->
2902
    Result = lists:foldl(fun (Type, {Status, Acc}) ->
70✔
2903
                                 Features = plugin_features(Type),
77✔
2904
                                 case lists:member(<<"retrieve-subscriptions">>, Features) of
77✔
2905
                                     true ->
2906
                                         Subscriber = jid:to_bare(JID),
77✔
2907
                                         {result, Subs} = node_action(Host, Type,
77✔
2908
                                                                      get_entity_subscriptions,
2909
                                                                      [Host, Subscriber]),
2910
                                         {Status, [Subs | Acc]};
77✔
2911
                                     false ->
UNCOV
2912
                                         {{error,
×
2913
                                           unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
2914
                                                             <<"retrieve-subscriptions">>)},
2915
                                          Acc}
2916
                                 end
2917
                         end,
2918
                         {ok, []}, Plugins),
2919
    case Result of
70✔
2920
        {ok, Subs} ->
2921
            Entities = lists:flatmap(fun(Sub) -> subscription_to_xmlel(Sub, Node) end,
70✔
2922
                                     lists:usort(lists:flatten(Subs))),
2923
            {result,
70✔
2924
             [#xmlel{name = <<"pubsub">>,
2925
                     attrs = #{<<"xmlns">> => ?NS_PUBSUB},
2926
                     children = [#xmlel{name = <<"subscriptions">>,
2927
                                        children = Entities}]}]};
2928
        {Error, _} ->
UNCOV
2929
            Error
×
2930
    end.
2931

2932
%% 2-element tuples are not used by any node type probably
2933
subscription_to_xmlel({_, none}, _Node) ->
UNCOV
2934
    [];
×
2935
subscription_to_xmlel({#pubsub_node{nodeid = {_, SubsNode}}, Sub}, <<>>) ->
2936
    Attrs = node_attr(SubsNode),
×
UNCOV
2937
    [#xmlel{name = <<"subscription">>,
×
2938
            attrs = Attrs#{<<"subscription">> => subscription_to_string(Sub)}}];
2939
subscription_to_xmlel({#pubsub_node{nodeid = {_, SubsNode}}, Sub}, SubsNode) ->
UNCOV
2940
    [#xmlel{name = <<"subscription">>,
×
2941
            attrs = #{<<"subscription">> => subscription_to_string(Sub)}}];
2942
subscription_to_xmlel({#pubsub_node{nodeid = {_, _}}, _}, _) ->
UNCOV
2943
    [];
×
2944
%% no idea how to trigger this one
2945
subscription_to_xmlel({_, none, _}, _Node) ->
UNCOV
2946
    [];
×
2947
%% sometimes used by node_pep
2948
subscription_to_xmlel({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubId, SubJID}, <<>>) ->
2949
    Attrs = node_attr(SubsNode),
63✔
2950
    [#xmlel{name = <<"subscription">>,
63✔
2951
            attrs = Attrs#{<<"jid">> => jid:to_binary(SubJID),
2952
                           <<"subid">> => SubId,
2953
                           <<"subscription">> => subscription_to_string(Sub)}}];
2954
subscription_to_xmlel({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubId, SubJID}, SubsNode) ->
UNCOV
2955
    [#xmlel{name = <<"subscription">>,
×
2956
            attrs =#{<<"jid">> => jid:to_binary(SubJID),
2957
                     <<"subid">> => SubId,
2958
                     <<"subscription">> => subscription_to_string(Sub)}}];
2959
subscription_to_xmlel({#pubsub_node{nodeid = {_, _}}, _, _, _}, _Node) ->
UNCOV
2960
    [];
×
2961
%% used by node_flat (therefore by dag, hometree and push as well)
2962
subscription_to_xmlel({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubJID}, <<>>) ->
2963
    Attrs = node_attr(SubsNode),
7✔
2964
    [#xmlel{name = <<"subscription">>,
7✔
2965
            attrs = Attrs#{<<"jid">> => jid:to_binary(SubJID),
2966
                           <<"subscription">> => subscription_to_string(Sub)}}];
2967
subscription_to_xmlel({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubJID}, SubsNode) ->
UNCOV
2968
    [#xmlel{name = <<"subscription">>,
×
2969
            attrs = #{<<"jid">> => jid:to_binary(SubJID),
2970
                      <<"subscription">> => subscription_to_string(Sub)}}];
2971
subscription_to_xmlel({#pubsub_node{nodeid = {_, _}}, _, _}, _Node) ->
UNCOV
2972
    [].
×
2973

2974
get_subscriptions(Host, Node, JID) ->
2975
    Action = fun (PubSubNode) -> get_subscriptions_transaction(JID, PubSubNode) end,
70✔
2976
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
70✔
2977
        {result, {_, Subs}} ->
2978
            Entities =
56✔
2979
            lists:flatmap(fun({_, pending, _, _}) ->
UNCOV
2980
                                  [];
×
2981
                             ({AJID, Sub, SubId, _}) ->
2982
                                  [#xmlel{name = <<"subscription">>,
84✔
2983
                                          attrs =#{
2984
                                            <<"jid">> => jid:to_binary(AJID),
2985
                                            <<"subscription">> => subscription_to_string(Sub),
2986
                                            <<"subid">> => SubId}}]
2987
                          end, Subs),
2988
            {result,
56✔
2989
             [#xmlel{name = <<"pubsub">>,
2990
                     attrs = #{<<"xmlns">> => ?NS_PUBSUB_OWNER},
2991
                     children = [#xmlel{name = <<"subscriptions">>,
2992
                                        attrs = node_attr(Node),
2993
                                        children = Entities}]}]};
2994
        Error ->
2995
            Error
14✔
2996
    end.
2997

2998
get_subscriptions_transaction(JID, #pubsub_node{type = Type, id = Nidx}) ->
2999
    Features = plugin_features(Type),
70✔
3000
    case lists:member(<<"manage-subscriptions">>, Features) of
70✔
3001
        true ->
3002
            case node_call(Type, get_affiliation, [Nidx, JID]) of
70✔
3003
                {result, owner} ->
3004
                    node_call(Type, get_node_subscriptions, [Nidx]);
56✔
3005
                _ ->
3006
                    {error, mongoose_xmpp_errors:forbidden()}
14✔
3007
            end;
3008
        false ->
UNCOV
3009
            {error,
×
3010
             unsupported_error(mongoose_xmpp_errors:feature_not_implemented(),
3011
                               <<"manage-subscriptions">>)}
3012
    end.
3013

3014
get_subscriptions_for_send_last(Host, PType, [JID, LJID, BJID]) ->
3015
    {result, Subs} = node_action(Host, PType,
3,871✔
3016
                                 get_entity_subscriptions,
3017
                                 [Host, JID]),
3018
    [{Node, Sub, SubId, SubJID}
3,871✔
3019
     || {Node, Sub, SubId, SubJID} <- Subs,
3,871✔
UNCOV
3020
        Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID),
×
UNCOV
3021
        match_option(Node, send_last_published_item, on_sub_and_presence)];
×
3022
get_subscriptions_for_send_last(_Host, _PType, _JIDs) ->
UNCOV
3023
    [].
×
3024

3025
set_subscriptions(Host, Node, From, #{action_el := ActionEl} ) ->
3026
    EntitiesEls = jlib:remove_cdata(ActionEl#xmlel.children),
42✔
3027
    Owner = jid:to_lower(jid:to_bare(From)),
42✔
3028
    Entities = lists:foldl(fun(_, error) ->
42✔
UNCOV
3029
                                   error;
×
3030
                              (#xmlel{name = <<"subscription">>} = El, Acc) ->
3031
                                   JID = jid:from_binary(exml_query:attr(El, <<"jid">>, <<>>)),
70✔
3032
                                   Sub = string_to_subscription(
70✔
3033
                                           exml_query:attr(El, <<"subscription">>, <<>>)),
3034
                                   SubId = exml_query:attr(El, <<"subid">>, <<>>),
70✔
3035
                                   case (JID == error) or (Sub == false) of
70✔
UNCOV
3036
                                       true -> error;
×
3037
                                       false -> [{jid:to_lower(JID), Sub, SubId} | Acc]
70✔
3038
                                   end;
3039
                              (_, _) ->
UNCOV
3040
                                   error
×
3041
                           end, [], EntitiesEls),
3042
    case Entities of
42✔
3043
        error ->
UNCOV
3044
            {error, mongoose_xmpp_errors:bad_request()};
×
3045
        _ ->
3046
            Action = fun (PubSubNode) ->
42✔
3047
                             set_subscriptions_transaction(Host, Owner, Node, PubSubNode, Entities)
42✔
3048
                     end,
3049
            case dirty(Host, Node, Action, ?FUNCTION_NAME) of
42✔
3050
                {result, {_, Result}} -> {result, Result};
28✔
3051
                Other -> Other
14✔
3052
            end
3053
    end.
3054

3055
set_subscriptions_transaction(Host, Owner, Node,
3056
                              #pubsub_node{type = Type, id = Nidx, owners = Owners}, Entities) ->
3057
    case lists:member(Owner, Owners) of
42✔
3058
        true ->
3059
            Result =
28✔
3060
            lists:foldl(fun(Entity, Acc) ->
3061
                                set_subscription_transaction(Host, Node, Nidx, Type, Entity, Acc)
56✔
3062
                        end,
3063
                        [], Entities),
3064
            case Result of
28✔
3065
                [] -> {result, []};
28✔
UNCOV
3066
                _ -> {error, mongoose_xmpp_errors:not_acceptable()}
×
3067
            end;
3068
        _ ->
3069
            {error, mongoose_xmpp_errors:forbidden()}
14✔
3070
    end.
3071

3072
set_subscription_transaction(Host, Node, Nidx, Type, {JID, Sub, SubId}, Acc) ->
3073
    case node_call(Type, set_subscriptions, [Nidx, JID, Sub, SubId]) of
56✔
UNCOV
3074
        {error, Err} -> [{error, Err} | Acc];
×
3075
        _ -> notify_subscription_change(Host, Node, JID, Sub), Acc
56✔
3076
    end.
3077

3078
notify_subscription_change(Host, Node, JID, Sub) ->
3079
    Attrs = node_attr(Node),
56✔
3080
    SubscriptionEl = #xmlel{name = <<"subscription">>,
56✔
3081
                            attrs = Attrs#{<<"jid">> => jid:to_binary(JID),
3082
                                           <<"subscription">> => subscription_to_string(Sub)}},
3083
    PubSubEl = #xmlel{name = <<"pubsub">>,
56✔
3084
                      attrs = #{<<"xmlns">> => ?NS_PUBSUB},
3085
                      children = [SubscriptionEl]},
3086
    Stanza = #xmlel{name = <<"message">>, children = [PubSubEl]},
56✔
3087
    ejabberd_router:route(service_jid(Host), jid:make(JID), Stanza).
56✔
3088

3089
-spec get_presence_and_roster_permissions(Host :: mod_pubsub:host(),
3090
                                          From :: jid:jid() | jid:ljid(),
3091
                                          Owners :: [jid:ljid(), ...],
3092
                                          AccessModel :: mod_pubsub:accessModel(),
3093
                                          AllowedGroups :: [binary()]) ->
3094
    {PresenceSubscription :: boolean(), RosterGroup :: boolean()}.
3095
get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups)
3096
      when (AccessModel == presence) orelse (AccessModel == roster) ->
3097
    case Host of
28✔
3098
        {User, Server, _} ->
3099
            get_roster_info(User, Server, From, AllowedGroups);
28✔
3100
        _ ->
UNCOV
3101
            [{OUser, OServer, _} | _] = Owners,
×
UNCOV
3102
            get_roster_info(OUser, OServer, From, AllowedGroups)
×
3103
    end;
3104
get_presence_and_roster_permissions(_Host, _From, _Owners, _AccessModel, _AllowedGroups) ->
3105
    {true, true}.
880✔
3106

3107
get_roster_info(_, _, {<<>>, <<>>, _}, _) ->
UNCOV
3108
    {false, false};
×
3109
get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, AllowedGroups) ->
3110
    LJID = {SubscriberUser, SubscriberServer, <<>>},
28✔
3111
    {Subscription, Groups} = mongoose_hooks:roster_get_jid_info(
28✔
3112
                               OwnerServer,
3113
                               jid:make_bare(OwnerUser, OwnerServer), LJID),
3114
    PresenceSubscription = Subscription == both orelse
28✔
3115
        Subscription == from orelse
14✔
3116
        {OwnerUser, OwnerServer} == {SubscriberUser, SubscriberServer},
14✔
3117
    RosterGroup = lists:any(fun (Group) ->
28✔
UNCOV
3118
                                    lists:member(Group, AllowedGroups)
×
3119
                            end,
3120
                            Groups),
3121
    {PresenceSubscription, RosterGroup};
28✔
3122
get_roster_info(OwnerUser, OwnerServer, JID, AllowedGroups) ->
3123
    get_roster_info(OwnerUser, OwnerServer, jid:to_lower(JID), AllowedGroups).
14✔
3124

3125
string_to_affiliation(<<"owner">>) -> owner;
14✔
3126
string_to_affiliation(<<"publisher">>) -> publisher;
14✔
3127
string_to_affiliation(<<"publish-only">>) -> publish_only;
296✔
3128
string_to_affiliation(<<"member">>) -> member;
28✔
UNCOV
3129
string_to_affiliation(<<"outcast">>) -> outcast;
×
3130
string_to_affiliation(<<"none">>) -> none;
14✔
3131
string_to_affiliation(_) -> false.
×
3132

3133
string_to_subscription(<<"subscribed">>) -> subscribed;
56✔
UNCOV
3134
string_to_subscription(<<"pending">>) -> pending;
×
3135
string_to_subscription(<<"none">>) -> none;
14✔
3136
string_to_subscription(_) -> false.
×
3137

3138
affiliation_to_string(owner) -> <<"owner">>;
42✔
UNCOV
3139
affiliation_to_string(publisher) -> <<"publisher">>;
×
UNCOV
3140
affiliation_to_string(publish_only) -> <<"publish-only">>;
×
3141
affiliation_to_string(member) -> <<"member">>;
×
3142
affiliation_to_string(outcast) -> <<"outcast">>;
×
3143
affiliation_to_string(_) -> <<"none">>.
×
3144

3145
subscription_to_string(subscribed) -> <<"subscribed">>;
761✔
3146
subscription_to_string(pending) -> <<"pending">>;
77✔
3147
subscription_to_string(_) -> <<"none">>.
28✔
3148

3149
-spec service_jid(mod_pubsub:host()) -> jid:jid().
3150
service_jid({U, S, _}) ->
3151
    jid:make_bare(U, S);
460✔
3152
service_jid(Host) ->
3153
    jid:make_bare(<<>>, Host).
4,262✔
3154

3155
%% @doc <p>Check if a notification must be delivered or not based on
3156
%% node and subscription options.</p>
3157
-spec is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) -> boolean() when
3158
    LJID :: jid:ljid(),
3159
    NotifyType :: items | nodes,
3160
    Depth :: integer(),
3161
    NodeOptions :: [{atom(), term()}],
3162
    SubOptions :: [{atom(), term()}].
3163
is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) ->
3164
    sub_to_deliver(LJID, NotifyType, Depth, SubOptions)
353✔
3165
        andalso node_to_deliver(LJID, NodeOptions).
339✔
3166

3167
sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) ->
3168
    lists:all(fun (Option) ->
353✔
3169
                      sub_option_can_deliver(NotifyType, Depth, Option)
28✔
3170
              end,
3171
              SubOptions).
3172

3173
node_to_deliver(LJID, NodeOptions) ->
3174
    presence_can_deliver(LJID, get_option(NodeOptions, presence_based_delivery)).
339✔
3175

UNCOV
3176
sub_option_can_deliver(items, _, {subscription_type, nodes}) -> false;
×
UNCOV
3177
sub_option_can_deliver(nodes, _, {subscription_type, items}) -> false;
×
3178
sub_option_can_deliver(_, _, {subscription_depth, all}) -> true;
×
3179
sub_option_can_deliver(_, Depth, {subscription_depth, D}) -> Depth =< D;
×
3180
sub_option_can_deliver(_, _, {deliver, false}) -> false;
14✔
3181
sub_option_can_deliver(_, _, {expire, When}) -> timestamp() < When;
×
3182
sub_option_can_deliver(_, _, _) -> true.
14✔
3183

3184
-spec presence_can_deliver(Entity :: jid:ljid(), PresenceBasedDelivery :: boolean()) -> boolean().
3185
presence_can_deliver(_, false) ->
3186
    true;
300✔
3187
presence_can_deliver({User, Server, <<>>}, true) ->
3188
    ejabberd_sm:get_user_present_resources(jid:make_noprep(User, Server, <<>>)) =/= [];
20✔
3189
presence_can_deliver({User, Server, Resource}, true) ->
3190
    JID = jid:make_noprep(User, Server, Resource),
19✔
3191
    case ejabberd_sm:get_session(JID) of
19✔
3192
        #session{priority = SPriority} when SPriority /= undefined -> true;
19✔
UNCOV
3193
        _ -> false
×
3194
    end.
3195

3196
-spec state_can_deliver(
3197
        Entity::jid:ljid(),
3198
          SubOptions :: mod_pubsub:subOptions() | [])
3199
        -> [jid:ljid()].
3200
state_can_deliver({U, S, R}, []) -> [{U, S, R}];
315✔
3201
state_can_deliver({U, S, R}, SubOptions) ->
3202
    case lists:keysearch(show_values, 1, SubOptions) of
14✔
3203
        %% If not in suboptions, item can be delivered, case doesn't apply
3204
        false -> [{U, S, R}];
14✔
3205
        %% If in a suboptions ...
3206
        {_, {_, ShowValues}} ->
UNCOV
3207
            Resources = case R of
×
3208
                            %% If the subscriber JID is a bare one, get all its resources
3209
                            <<>> -> user_resources(U, S);
×
3210
                            %% If the subscriber JID is a full one, use its resource
3211
                            R -> [R]
×
3212
                        end,
3213
            lists:foldl(fun (Resource, Acc) ->
×
UNCOV
3214
                                get_resource_state({U, S, Resource}, ShowValues, Acc)
×
3215
                        end,
3216
                        [], Resources)
3217
    end.
3218

3219
-spec get_resource_state(
3220
        Entity     :: jid:ljid(),
3221
          ShowValues :: [binary()],
3222
          JIDs       :: [jid:ljid()])
3223
        -> [jid:ljid()].
3224
get_resource_state({U, S, R}, ShowValues, JIDs) ->
UNCOV
3225
    case ejabberd_sm:get_session_pid(jid:make_noprep(U, S, R)) of
×
3226
        none ->
3227
            %% If no PID, item can be delivered
UNCOV
3228
            lists:append([{U, S, R}], JIDs);
×
3229
        Pid ->
3230
            Show = case mod_presence:get_presence(Pid) of
×
UNCOV
3231
                       {_, _, <<"available">>, _} -> <<"online">>;
×
3232
                       {_, _, State, _} -> State
×
3233
                   end,
3234
            case lists:member(Show, ShowValues) of
×
3235
                %% If yes, item can be delivered
3236
                true -> lists:append([{U, S, R}], JIDs);
×
3237
                %% If no, item can't be delivered
3238
                false -> JIDs
×
3239
            end
3240
    end.
3241

3242
-spec payload_xmlelements(
3243
        Payload :: mod_pubsub:payload())
3244
        -> Count :: non_neg_integer().
3245
payload_xmlelements(Payload) ->
3246
    payload_xmlelements(Payload, 0).
1,354✔
3247

3248
payload_xmlelements([], Count) -> Count;
1,354✔
3249
payload_xmlelements([#xmlel{} | Tail], Count) ->
3250
    payload_xmlelements(Tail, Count + 1);
1,354✔
3251
payload_xmlelements([_ | Tail], Count) ->
UNCOV
3252
    payload_xmlelements(Tail, Count).
×
3253

3254
items_event_stanza(Node, Items) ->
3255
    MoreEls =
70✔
3256
    case Items of
3257
        [LastItem] ->
3258
            {ModifNow, ModifUSR} = LastItem#pubsub_item.modification,
70✔
3259
            Sec = erlang:convert_time_unit(ModifNow, microsecond, second),
70✔
3260
            TString = calendar:system_time_to_rfc3339(Sec, [{offset, "Z"}]),
70✔
3261
            [#xmlel{name = <<"delay">>,
70✔
3262
                    attrs = #{<<"xmlns">> => ?NS_DELAY,
3263
                              <<"from">> => jid:to_binary(ModifUSR),
3264
                              <<"stamp">> => list_to_binary(TString)},
3265
                    children = [#xmlcdata{content =  <<>>}]}];
3266
        _ ->
UNCOV
3267
            []
×
3268
    end,
3269
    Attrs = node_attr(Node),
70✔
3270
    event_stanza_with_els([#xmlel{name = <<"items">>,
70✔
3271
                                  attrs = Attrs#{<<"type">> => <<"headline">>},
3272
                                  children = items_els(Items)}],
3273
                          MoreEls).
3274

3275
event_stanza(Els) ->
3276
    event_stanza_with_els(Els, []).
2,506✔
3277
event_stanza_with_els(Els, MoreEls) ->
3278
    #xmlel{name = <<"message">>,
2,611✔
3279
           children = [#xmlel{name = <<"event">>,
3280
                              attrs = #{<<"xmlns">> => ?NS_PUBSUB_EVENT},
3281
                              children = Els}
3282
                       | MoreEls]}.
3283

3284
event_stanza(Event, EvAttr) ->
3285
    event_stanza_with_els([#xmlel{name = Event, attrs = EvAttr}], []).
35✔
3286

3287
%%%%%% broadcast functions
3288

3289
broadcast_publish_item(Host, Node, Nidx, Type, NodeOptions,
3290
                       ItemId, From, Payload, Removed, ItemPublisher) ->
3291
    case get_collection_subscriptions(Host, Node) of
853✔
3292
        SubsByDepth when is_list(SubsByDepth) ->
3293
            Content = case get_option(NodeOptions, deliver_payloads) of
853✔
3294
                          true -> Payload;
832✔
3295
                          false -> []
21✔
3296
                      end,
3297
            ItemAttr = case ItemPublisher of
853✔
3298
                           true  -> item_attr(ItemId, From);
28✔
3299
                           false -> item_attr(ItemId)
825✔
3300
                       end,
3301
            Stanza = event_stanza(
853✔
3302
                       [#xmlel{name = <<"items">>, attrs = node_attr(Node),
3303
                               children = [#xmlel{name = <<"item">>, attrs = ItemAttr,
3304
                                                  children = Content}]}]),
3305
            broadcast_step(Host, fun() ->
853✔
3306
                broadcast_stanza(Host, From, Node, Nidx, Type,
853✔
3307
                                 NodeOptions, SubsByDepth, items, Stanza, true),
3308
                broadcast_auto_retract_notification(Host, Node, Nidx, Type,
853✔
3309
                                                    NodeOptions, SubsByDepth, Removed)
3310
                end),
3311
            {result, true};
853✔
3312
        _ ->
UNCOV
3313
            {result, false}
×
3314
    end.
3315

3316
broadcast_auto_retract_notification(_Host, _Node, _Nidx, _Type, _NodeOptions, _SubsByDepth, []) ->
3317
    ok;
811✔
3318
broadcast_auto_retract_notification(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, Removed) ->
3319
    case get_option(NodeOptions, notify_retract) of
42✔
3320
        true ->
3321
            RetractEls = [#xmlel{name = <<"retract">>, attrs = item_attr(RId)} || RId <- Removed],
14✔
3322
            RetractStanza = event_stanza([#xmlel{name = <<"items">>, attrs = node_attr(Node),
14✔
3323
                                                 children = RetractEls}]),
3324
            broadcast_stanza(Host, Node, Nidx, Type,
14✔
3325
                             NodeOptions, SubsByDepth,
3326
                             items, RetractStanza, true);
3327
        _ ->
3328
            ok
28✔
3329
    end.
3330

3331
broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds) ->
UNCOV
3332
    broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds, false).
×
3333
broadcast_retract_items(_Host, _Node, _Nidx, _Type, _NodeOptions, [], _ForceNotify) ->
UNCOV
3334
    {result, false};
×
3335
broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds, ForceNotify) ->
3336
    case (get_option(NodeOptions, notify_retract) or ForceNotify) of
56✔
3337
        true ->
3338
            case get_collection_subscriptions(Host, Node) of
14✔
3339
                SubsByDepth when is_list(SubsByDepth) ->
3340
                    Stanza = event_stanza(
14✔
3341
                               [#xmlel{name = <<"items">>, attrs = node_attr(Node),
3342
                                       children = [#xmlel{name = <<"retract">>,
14✔
3343
                                                          attrs = item_attr(ItemId)}
3344
                                                   || ItemId <- ItemIds]}]),
14✔
3345
                    broadcast_step(Host, fun() ->
14✔
3346
                        broadcast_stanza(Host, Node, Nidx, Type,
14✔
3347
                                         NodeOptions, SubsByDepth, items, Stanza, true)
3348
                        end),
3349
                    {result, true};
14✔
3350
                _ ->
UNCOV
3351
                    {result, false}
×
3352
            end;
3353
        _ ->
3354
            {result, false}
42✔
3355
    end.
3356

3357
broadcast_purge_node(Host, Node, Nidx, Type, NodeOptions) ->
3358
    case get_option(NodeOptions, notify_retract) of
28✔
3359
        true ->
UNCOV
3360
            case get_collection_subscriptions(Host, Node) of
×
3361
                SubsByDepth when is_list(SubsByDepth) ->
UNCOV
3362
                    Stanza = event_stanza(
×
3363
                               [#xmlel{name = <<"purge">>, attrs = node_attr(Node)}]),
UNCOV
3364
                    broadcast_step(Host, fun() ->
×
3365
                        broadcast_stanza(Host, Node, Nidx, Type,
×
3366
                                         NodeOptions, SubsByDepth, nodes, Stanza, false)
3367
                        end),
3368
                    {result, true};
×
3369
                _ ->
UNCOV
3370
                    {result, false}
×
3371
            end;
3372
        _ ->
3373
            {result, false}
28✔
3374
    end.
3375

3376
broadcast_removed_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth) ->
3377
    case get_option(NodeOptions, notify_delete) of
1,030✔
3378
        true ->
3379
            case SubsByDepth of
10✔
3380
                [] ->
UNCOV
3381
                    {result, false};
×
3382
                _ ->
3383
                    Stanza = event_stanza(
10✔
3384
                               [#xmlel{name = <<"delete">>, attrs = node_attr(Node)}]),
3385
                    broadcast_step(Host, fun() ->
10✔
3386
                        broadcast_stanza(Host, Node, Nidx, Type,
10✔
3387
                                         NodeOptions, SubsByDepth, nodes, Stanza, false)
3388
                        end),
3389
                    {result, true}
10✔
3390
            end;
3391
        _ ->
3392
            {result, false}
1,020✔
3393
    end.
3394

3395
broadcast_created_node(_, _, _, _, _, []) ->
UNCOV
3396
    {result, false};
×
3397
broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth) ->
3398
    Stanza = event_stanza([#xmlel{name = <<"create">>, attrs = node_attr(Node)}]),
1,601✔
3399
    broadcast_step(Host, fun() ->
1,601✔
3400
        broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, nodes, Stanza, true)
1,601✔
3401
        end),
3402
    {result, true}.
1,601✔
3403

3404
broadcast_config_notification(Host, Node, Nidx, Type, NodeOptions, Lang) ->
3405
    case get_option(NodeOptions, notify_config) of
42✔
3406
        true ->
3407
            case get_collection_subscriptions(Host, Node) of
14✔
3408
                SubsByDepth when is_list(SubsByDepth) ->
3409
                    Content = payload_by_option(Type, NodeOptions, Lang),
14✔
3410
                    Stanza = event_stanza([#xmlel{name = <<"configuration">>,
14✔
3411
                                                  attrs = node_attr(Node), children = Content}]),
3412
                    broadcast_step(Host, fun() ->
14✔
3413
                        broadcast_stanza(Host, Node, Nidx, Type,
14✔
3414
                                         NodeOptions, SubsByDepth, nodes, Stanza, false)
3415
                        end),
3416
                    {result, true};
14✔
3417
                _ ->
UNCOV
3418
                    {result, false}
×
3419
            end;
3420
        _ ->
3421
            {result, false}
28✔
3422
    end.
3423

3424
payload_by_option(_Type, NodeOptions, Lang) ->
3425
    case get_option(NodeOptions, deliver_payloads) of
14✔
3426
        true ->
3427
            [configure_form(<<"result">>, NodeOptions, Lang, [])];
14✔
3428
        false ->
UNCOV
3429
            []
×
3430
    end.
3431

3432
get_collection_subscriptions(Host, Node) ->
3433
    Action = fun() ->
881✔
3434
                     {result, get_node_subs_by_depth(Host, Node, service_jid(Host))}
881✔
3435
             end,
3436
    ErrorDebug = #{
881✔
3437
      pubsub_host => Host,
3438
      action => get_collection_subscriptions,
3439
      node_name => Node
3440
     },
3441
    case mod_pubsub_db_backend:dirty(Action, ErrorDebug) of
881✔
3442
        {result, CollSubs} -> CollSubs;
881✔
UNCOV
3443
        _ -> []
×
3444
    end.
3445

3446
get_node_subs_by_depth(Host, Node, From) ->
3447
    ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, From]),
3,592✔
3448
    [{Depth, [{N, get_node_subs(N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree].
3,592✔
3449

3450
get_node_subs(#pubsub_node{type = Type, id = Nidx}) ->
3451
    case node_call(Type, get_node_subscriptions, [Nidx]) of
3,992✔
3452
        {result, Subs} ->
3453
            % TODO: Replace with proper DB/plugin call with sub type filter
3454
            [{JID, SubID, Opts} || {JID, SubType, SubID, Opts} <- Subs, SubType == subscribed];
3,965✔
3455
        Other ->
UNCOV
3456
            Other
×
3457
    end.
3458

3459
%% Execute broadcasting step in a new process
3460
%% F contains one or more broadcast_stanza calls, executed sequentially
3461
broadcast_step(Host, F) ->
3462
    case gen_mod:get_module_opt(serverhost(Host), ?MODULE, sync_broadcast) of
2,492✔
3463
        true ->
UNCOV
3464
            F();
×
3465
        false ->
3466
            proc_lib:spawn(F)
2,492✔
3467
    end.
3468

3469
broadcast_stanza(Host, Node, _Nidx, _Type, NodeOptions,
3470
                 SubsByDepth, NotifyType, BaseStanza, SHIM) ->
3471
    NotificationType = get_option(NodeOptions, notification_type, headline),
2,506✔
3472
    %% Option below is not standard, but useful
3473
    BroadcastAll = get_option(NodeOptions, broadcast_all_resources),
2,506✔
3474
    From = service_jid(Host),
2,506✔
3475
    Stanza = add_message_type(BaseStanza, NotificationType),
2,506✔
3476
    %% Handles explicit subscriptions
3477
    SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth),
2,506✔
3478
    lists:foreach(fun ({LJID, SubNodeName, SubIDs}) ->
2,506✔
3479
                          LJIDs = case BroadcastAll of
322✔
3480
                                      true ->
UNCOV
3481
                                          {U, S, _} = LJID,
×
UNCOV
3482
                                          [{U, S, R} || R <- user_resources(U, S)];
×
3483
                                      false ->
3484
                                          [LJID]
322✔
3485
                                  end,
3486
                          StanzaToSend = maybe_add_shim_headers(Stanza, SHIM, SubIDs,
322✔
3487
                                                                Node, SubNodeName),
3488

3489
                          lists:foreach(fun(To) ->
322✔
3490
                                                ejabberd_router:route(From, jid:make(To),
322✔
3491
                                                                      StanzaToSend)
3492
                                        end, LJIDs)
3493
                  end, SubIDsByJID).
3494

3495
broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, Nidx, Type, NodeOptions,
3496
                 SubsByDepth, NotifyType, BaseStanza, SHIM) ->
3497
    broadcast_stanza({LUser, LServer, LResource}, Node, Nidx, Type, NodeOptions,
133✔
3498
                     SubsByDepth, NotifyType, BaseStanza, SHIM),
3499
    %% Handles implicit presence subscriptions
3500
    SenderResource = user_resource(LUser, LServer, LResource),
133✔
3501
    case ejabberd_sm:get_session_pid(jid:make(LUser, LServer, SenderResource)) of
133✔
3502
        C2SPid when is_pid(C2SPid) ->
3503
            NotificationType = get_option(NodeOptions, notification_type, headline),
133✔
3504
            Stanza = add_message_type(BaseStanza, NotificationType),
133✔
3505
            %% set the from address on the notification to the bare JID of the account owner
3506
            %% Also, add "replyto" if entity has presence subscription to the account owner
3507
            %% See XEP-0163 1.1 section 4.3.1
3508
            ReplyTo = extended_headers([jid:to_binary(Publisher)]),
133✔
3509
            Feature = <<((Node))/binary, "+notify">>,
133✔
3510
            Recipients = mongoose_c2s:call(C2SPid, ?MODULE, {get_pep_recipients, Feature}),
133✔
3511
            Packet = add_extended_headers(Stanza, ReplyTo),
133✔
3512
            From = jid:make_bare(LUser, LServer),
133✔
3513
            lists:foreach(fun(USR) -> ejabberd_router:route(From, jid:make(USR), Packet) end,
133✔
3514
                          lists:usort(Recipients));
3515
        _ ->
UNCOV
3516
            ?LOG_DEBUG(#{what => pubsub_no_session,
×
3517
                text => <<"User has no session; cannot deliver stanza to contacts">>,
UNCOV
3518
                user => LUser, server => LServer, exml_packet => BaseStanza})
×
3519
    end;
3520
broadcast_stanza(Host, _Publisher, Node, Nidx, Type, NodeOptions,
3521
                 SubsByDepth, NotifyType, BaseStanza, SHIM) ->
3522
    broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth,
720✔
3523
                     NotifyType, BaseStanza, SHIM).
3524

3525
subscribed_nodes_by_jid(NotifyType, SubsByDepth) ->
3526
    DepthsToDeliver =
2,506✔
3527
    fun({Depth, SubsByNode}, Acc1) ->
3528
            lists:foldl(fun({Node, Subs}, Acc2) ->
2,744✔
3529
                                nodes_to_deliver(NotifyType, Depth, Node, Subs, Acc2)
2,758✔
3530
                        end, Acc1, SubsByNode)
3531
    end,
3532
    {_, JIDSubs} = lists:foldl(DepthsToDeliver, {[], []}, SubsByDepth),
2,506✔
3533
    JIDSubs.
2,506✔
3534

3535
nodes_to_deliver(NotifyType, Depth, Node, Subs, Acc) ->
3536
    NodeName = case Node#pubsub_node.nodeid of
2,758✔
3537
                   {_, N} -> N;
2,758✔
UNCOV
3538
                   Other -> Other
×
3539
               end,
3540
    NodeOptions = Node#pubsub_node.options,
2,758✔
3541
    lists:foldl(fun({LJID, SubID, SubOptions}, InnerAcc) ->
2,758✔
3542
                        case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of
353✔
3543
                            true ->
3544
                                JIDsToDeliver = state_can_deliver(LJID, SubOptions),
329✔
3545
                                process_jids_to_deliver(NodeName, SubID, JIDsToDeliver, InnerAcc);
329✔
3546
                            false ->
3547
                                InnerAcc
24✔
3548
                        end
3549
                end, Acc, Subs).
3550

3551
process_jids_to_deliver(NodeName, SubID, JIDsToDeliver, {JIDs, Recipients}) ->
3552
    lists:foldl(
329✔
3553
      fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) ->
3554
              process_jid_to_deliver(JIDs, SubID, NodeName,
329✔
3555
                                     JIDToDeliver, {JIDsAcc, RecipientsAcc})
3556
      end, {JIDs, Recipients}, JIDsToDeliver).
3557

3558
process_jid_to_deliver(JIDs, SubID, NodeName, JIDToDeliver, {JIDsAcc, RecipientsAcc}) ->
3559
    case lists:member(JIDToDeliver, JIDs) of
329✔
3560
        %% check if the JIDs co-accumulator contains the Subscription Jid,
3561
        false ->
3562
            %%  - if not,
3563
            %%  - add the Jid to JIDs list co-accumulator ;
3564
            %%  - create a tuple of the Jid, Nidx, and SubID (as list),
3565
            %%    and add the tuple to the Recipients list co-accumulator
3566
            {[JIDToDeliver | JIDsAcc],
322✔
3567
             [{JIDToDeliver, NodeName, [SubID]}
3568
              | RecipientsAcc]};
3569
        true ->
3570
            %% - if the JIDs co-accumulator contains the Jid
3571
            %%   get the tuple containing the Jid from the Recipient list co-accumulator
3572
            {_, {JIDToDeliver, NodeName1, SubIDs}} =
7✔
3573
                lists:keysearch(JIDToDeliver, 1, RecipientsAcc),
3574
            %%   delete the tuple from the Recipients list
3575
            % v1 : Recipients1 = lists:keydelete(LJID, 1, Recipients),
3576
            % v2 : Recipients1 = lists:keyreplace(LJID, 1, Recipients,
3577
            %                                     {LJID, Nidx1, [SubID | SubIDs]}),
3578
            %%   add the SubID to the SubIDs list in the tuple,
3579
            %%   and add the tuple back to the Recipients list co-accumulator
3580
            % v1.1 : {JIDs, lists:append(Recipients1,
3581
            %                            [{LJID, Nidx1, lists:append(SubIDs, [SubID])}])}
3582
            % v1.2 : {JIDs, [{LJID, Nidx1, [SubID | SubIDs]} | Recipients1]}
3583
            % v2: {JIDs, Recipients1}
3584
            {JIDsAcc,
7✔
3585
             lists:keyreplace(JIDToDeliver, 1,
3586
                              RecipientsAcc,
3587
                              {JIDToDeliver, NodeName1,
3588
                               [SubID | SubIDs]})}
3589
    end.
3590

3591
user_resources(User, Server) ->
3592
    JID = jid:make_bare(User, Server),
2,938✔
3593
    ejabberd_sm:get_user_resources(JID).
2,938✔
3594

3595
user_resource(User, Server, <<>>) ->
3596
    case user_resources(User, Server) of
133✔
3597
        [R | _] -> R;
133✔
UNCOV
3598
        _ -> <<>>
×
3599
    end;
3600
user_resource(_, _, Resource) ->
3601
    Resource.
×
3602

3603
%%%%%%% Configuration handling
3604

3605
get_configure(Host, Node, From, #{server_host := ServerHost, lang := Lang}) ->
3606
    Action = fun(PubSubNode) ->
49✔
3607
                     get_configure_transaction(ServerHost, Node, From, Lang, PubSubNode)
49✔
3608
             end,
3609
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
49✔
3610
        {result, {_, Result}} -> {result, Result};
49✔
UNCOV
3611
        Other -> Other
×
3612
    end.
3613

3614
get_configure_transaction(ServerHost, Node, From, Lang,
3615
                          #pubsub_node{options = Options, type = Type, id = Nidx}) ->
3616
    case node_call(Type, get_affiliation, [Nidx, From]) of
49✔
3617
        {result, owner} ->
3618
            Groups = mongoose_hooks:roster_groups(ServerHost),
49✔
3619
            XEl = configure_form(<<"form">>, Options, Lang, Groups),
49✔
3620
            ConfigureEl = #xmlel{name = <<"configure">>,
49✔
3621
                                 attrs = node_attr(Node),
3622
                                 children = [XEl]},
3623
            {result,
49✔
3624
             [#xmlel{name = <<"pubsub">>,
3625
                     attrs = #{<<"xmlns">> => ?NS_PUBSUB_OWNER},
3626
                     children = [ConfigureEl]}]};
3627
        _ ->
UNCOV
3628
            {error, mongoose_xmpp_errors:forbidden()}
×
3629
    end.
3630

3631
get_default(Host, Node, _From, #{lang := Lang}) ->
3632
    Type = select_type(Host, Node),
14✔
3633
    Options = node_options(Host, Type),
14✔
3634
    XEl = configure_form(<<"form">>, Options, Lang, []),
14✔
3635
    DefaultEl = #xmlel{name = <<"default">>, children = [XEl]},
14✔
3636
    {result,
14✔
3637
     [#xmlel{name = <<"pubsub">>,
3638
             attrs = #{<<"xmlns">> => ?NS_PUBSUB_OWNER},
3639
             children = [DefaultEl]}]}.
3640

3641
match_option(Node, Var, Val) when is_record(Node, pubsub_node) ->
UNCOV
3642
    match_option(Node#pubsub_node.options, Var, Val);
×
3643
match_option(Options, Var, Val) when is_list(Options) ->
3644
    get_option(Options, Var) == Val;
77✔
3645
match_option(_, _, _) ->
UNCOV
3646
    false.
×
3647

3648
get_option([], _) -> false;
376✔
3649
get_option(Options, Var) -> get_option(Options, Var, false).
19,476✔
3650

3651
get_option(Options, Var, Def) ->
3652
    case lists:keysearch(Var, 1, Options) of
23,324✔
3653
        {value, {_Val, Ret}} -> Ret;
19,809✔
3654
        _ -> Def
3,515✔
3655
    end.
3656

3657
-spec check_publish_options(binary(), undefined | exml:element(), mod_pubsub:nodeOptions()) ->
3658
    boolean().
3659
check_publish_options(Type, PublishOptions, Options) ->
3660
    ParsedPublishOptions = parse_publish_options(PublishOptions),
571✔
3661
    ConvertedOptions = convert_options(Options),
571✔
3662
    case node_call(Type, check_publish_options, [ParsedPublishOptions, ConvertedOptions]) of
571✔
3663
        {error, _} ->
UNCOV
3664
            true;
×
3665
        {result, Result} ->
3666
            Result
571✔
3667
    end.
3668

3669
-spec parse_publish_options(undefined | exml:element()) -> invalid_form | #{binary() => [binary()]}.
3670
parse_publish_options(undefined) ->
3671
    #{};
133✔
3672
parse_publish_options(PublishOptions) ->
3673
    case mongoose_data_forms:find_and_parse_form(PublishOptions) of
438✔
3674
        #{type := <<"submit">>, kvs := KVs, ns := ?NS_PUBSUB_PUB_OPTIONS} ->
3675
            KVs;
431✔
3676
        _ ->
3677
            invalid_form
7✔
3678
    end.
3679

3680
-spec convert_options(mod_pubsub:nodeOptions()) -> #{binary() => [binary()]}.
3681
convert_options(Options) ->
3682
    ConvertedOptions = lists:map(fun({Key, Value}) ->
571✔
3683
                                     {atom_to_binary(Key), convert_option_value(Value)}
9,136✔
3684
                                 end, Options),
3685
    maps:from_list(ConvertedOptions).
571✔
3686

3687
-spec convert_option_value(binary() | [binary()] | atom() | non_neg_integer()) -> [binary()].
3688
convert_option_value(true) ->
3689
    [<<"1">>];
2,452✔
3690
convert_option_value(false) ->
3691
    [<<"0">>];
2,687✔
3692
convert_option_value(Element) when is_atom(Element) ->
3693
    [atom_to_binary(Element)];
2,284✔
3694
convert_option_value(Element) when is_integer(Element) ->
3695
    [integer_to_binary(Element)];
1,142✔
3696
convert_option_value(List) when is_list(List) ->
3697
    List.
571✔
3698

3699
node_options(Host, Type) ->
3700
    ConfiguredOpts = lists:keysort(1, config(serverhost(Host), default_node_config)),
1,636✔
3701
    DefaultOpts = lists:keysort(1, node_plugin_options(Type)),
1,636✔
3702
    lists:keymerge(1, ConfiguredOpts, DefaultOpts).
1,636✔
3703

3704
node_plugin_options(Type) ->
3705
    Module = plugin(Type),
1,636✔
3706
    case catch gen_pubsub_node:options(Module) of
1,636✔
3707
        {'EXIT', {undef, _}} ->
UNCOV
3708
            DefaultModule = plugin(?STDNODE),
×
UNCOV
3709
            gen_pubsub_node:options(DefaultModule);
×
3710
        Result ->
3711
            Result
1,636✔
3712
    end.
3713

3714
%% @doc <p>Return the maximum number of items for a given node.</p>
3715
%% <p>Unlimited means that there is no limit in the number of items that can
3716
%% be stored.</p>
3717
%% @todo In practice, the current data structure means that we cannot manage
3718
%% millions of items on a given node. This should be addressed in a new
3719
%% version.
3720
-spec max_items(Host, Options) -> MaxItems when
3721
    Host :: host(),
3722
    Options :: [Option],
3723
    Option :: {Key :: atom(), Value :: term()},
3724
    MaxItems :: integer() | unlimited.
3725
max_items(Host, Options) ->
3726
    case get_option(Options, persist_items) of
1,354✔
3727
        true ->
3728
            case get_option(Options, max_items) of
930✔
UNCOV
3729
                I when is_integer(I), I < 0 -> 0;
×
3730
                I when is_integer(I) -> I;
930✔
UNCOV
3731
                _ -> ?MAXITEMS
×
3732
            end;
3733
        false ->
3734
            %% Don't publish if it means sending the item without a way to retrieve it
3735
            case get_option(Options, send_last_published_item) == never
424✔
3736
                 orelse is_last_item_cache_enabled(Host) of
403✔
3737
                true -> 0;
21✔
3738
                false -> 1
403✔
3739
            end
3740
    end.
3741

3742
-define(BOOL_CONFIG_FIELD(Label, Var),
3743
        ?BOOLXFIELD(Label,
3744
                    <<"pubsub#", (atom_to_binary(Var, latin1))/binary>>,
3745
                    (get_option(Options, Var)))).
3746

3747
-define(STRING_CONFIG_FIELD(Label, Var),
3748
        ?STRINGXFIELD(Label,
3749
                      <<"pubsub#", (atom_to_binary(Var, latin1))/binary>>,
3750
                      (get_option(Options, Var, <<>>)))).
3751

3752
-define(INTEGER_CONFIG_FIELD(Label, Var),
3753
        ?STRINGXFIELD(Label,
3754
                      <<"pubsub#", (atom_to_binary(Var, latin1))/binary>>,
3755
                      (integer_to_binary(get_option(Options, Var))))).
3756

3757
-define(JLIST_CONFIG_FIELD(Label, Var, Opts),
3758
        ?LISTXFIELD(Label,
3759
                    <<"pubsub#", (atom_to_binary(Var, latin1))/binary>>,
3760
                    (jid:to_binary(get_option(Options, Var))),
3761
                    [jid:to_binary(O) || O <- Opts])).
3762

3763
-define(ALIST_CONFIG_FIELD(Label, Var, Opts),
3764
        ?LISTXFIELD(Label,
3765
                    <<"pubsub#", (atom_to_binary(Var, latin1))/binary>>,
3766
                    (atom_to_binary(get_option(Options, Var), latin1)),
3767
                    [atom_to_binary(O, latin1) || O <- Opts])).
3768

3769
-define(LISTM_CONFIG_FIELD(Label, Var, Opts),
3770
        ?LISTMXFIELD(Label,
3771
                     <<"pubsub#", (atom_to_binary(Var, latin1))/binary>>,
3772
                     (get_option(Options, Var)), Opts)).
3773

3774
-define(NLIST_CONFIG_FIELD(Label, Var),
3775
        ?STRINGMXFIELD(Label,
3776
                       <<"pubsub#", (atom_to_binary(Var, latin1))/binary>>,
3777
                       get_option(Options, Var, []))).
3778

3779
configure_form(Type, Options, Lang, Groups) ->
3780
    Fields = get_configure_xfields(Options, Lang, Groups),
77✔
3781
    mongoose_data_forms:form(#{type => Type, ns => ?NS_PUBSUB_NODE_CONFIG, fields => Fields}).
77✔
3782

3783
get_configure_xfields(Options, Lang, Groups) ->
3784
    [?BOOL_CONFIG_FIELD(<<"Deliver payloads with event notifications">>,
77✔
3785
                        deliver_payloads),
77✔
3786
     ?BOOL_CONFIG_FIELD(<<"Deliver event notifications">>,
3787
                        deliver_notifications),
77✔
3788
     ?BOOL_CONFIG_FIELD(<<"Notify subscribers when the node configuration changes">>,
3789
                        notify_config),
77✔
3790
     ?BOOL_CONFIG_FIELD(<<"Notify subscribers when the node is deleted">>,
3791
                        notify_delete),
77✔
3792
     ?BOOL_CONFIG_FIELD(<<"Notify subscribers when items are removed from the node">>,
3793
                        notify_retract),
77✔
3794
     ?BOOL_CONFIG_FIELD(<<"Persist items to storage">>,
3795
                        persist_items),
77✔
3796
     ?STRING_CONFIG_FIELD(<<"A friendly name for the node">>,
3797
                          title),
3798
     ?INTEGER_CONFIG_FIELD(<<"Max # of items to persist">>,
3799
                           max_items),
3800
     ?BOOL_CONFIG_FIELD(<<"Whether to allow subscriptions">>,
3801
                        subscribe),
77✔
3802
     ?ALIST_CONFIG_FIELD(<<"Specify the access model">>,
3803
                         access_model, [open, authorize, presence, roster, whitelist]),
385✔
3804
     ?LISTM_CONFIG_FIELD(<<"Roster groups allowed to subscribe">>,
3805
                         roster_groups_allowed, Groups),
3806
     ?ALIST_CONFIG_FIELD(<<"Specify the publisher model">>,
3807
                         publish_model, [publishers, subscribers, open]),
231✔
3808
     ?BOOL_CONFIG_FIELD(<<"Purge all items when the relevant publisher goes offline">>,
3809
                        purge_offline),
77✔
3810
     ?ALIST_CONFIG_FIELD(<<"Specify the event message type">>,
3811
                         notification_type, [headline, normal]),
154✔
3812
     ?INTEGER_CONFIG_FIELD(<<"Max payload size in bytes">>,
3813
                           max_payload_size),
3814
     ?ALIST_CONFIG_FIELD(<<"When to send the last published item">>,
3815
                         send_last_published_item, [never, on_sub, on_sub_and_presence]),
231✔
3816
     ?BOOL_CONFIG_FIELD(<<"Only deliver notifications to available users">>,
3817
                        presence_based_delivery),
77✔
3818
     ?STRING_CONFIG_FIELD(<<"Specify the type of payload data to be provided at this node">>,
3819
                          type),
3820
     ?NLIST_CONFIG_FIELD(<<"The collections with which a node is affiliated">>,
3821
                         collection)].
3822

3823
%%<p>There are several reasons why the node configuration request might fail:</p>
3824
%%<ul>
3825
%%<li>The service does not support node configuration.</li>
3826
%%<li>The requesting entity does not have sufficient privileges to configure the node.</li>
3827
%%<li>The request did not specify a node.</li>
3828
%%<li>The node has no configuration options.</li>
3829
%%<li>The specified node does not exist.</li>
3830
%%</ul>
3831
set_configure(Host, Node, From, #{action_el := ActionEl, lang := Lang}) ->
3832
    case mongoose_data_forms:find_and_parse_form(ActionEl) of
98✔
3833
        #{type := <<"cancel">>} ->
UNCOV
3834
            {result, []};
×
3835
        #{type := <<"submit">>, kvs := KVs} ->
3836
            set_configure_submit(Host, Node, From, KVs, Lang);
56✔
3837
        {error, Msg} ->
3838
            {error, mongoose_xmpp_errors:bad_request(Lang, Msg)};
28✔
3839
        _ ->
3840
            {error, mongoose_xmpp_errors:bad_request(Lang, <<"Invalid form type">>)}
14✔
3841
    end.
3842

3843
set_configure_submit(Host, Node, User, KVs, Lang) ->
3844
    Action = fun(NodeRec) ->
56✔
3845
                     set_configure_transaction(Host, User, KVs, NodeRec)
56✔
3846
             end,
3847
    case transaction(Host, Node, Action, ?FUNCTION_NAME) of
56✔
3848
        {result, {_OldNode, TNode}} ->
3849
            Nidx = TNode#pubsub_node.id,
42✔
3850
            Type = TNode#pubsub_node.type,
42✔
3851
            Options = TNode#pubsub_node.options,
42✔
3852
            broadcast_config_notification(Host, Node, Nidx, Type, Options, Lang),
42✔
3853
            {result, []};
42✔
3854
        Other ->
3855
            Other
14✔
3856
    end.
3857

3858
set_configure_transaction(Host, User, KVs, #pubsub_node{ type = Type, id = Nidx } = NodeRec) ->
3859
    case node_call(Type, get_affiliation, [Nidx, User]) of
56✔
3860
        {result, owner} ->
3861
            set_configure_valid_transaction(Host, NodeRec, KVs);
56✔
3862
        _ ->
UNCOV
3863
            {error, mongoose_xmpp_errors:forbidden()}
×
3864
    end.
3865

3866
set_configure_valid_transaction(Host, #pubsub_node{ type = Type, options = Options } = NodeRec,
3867
                                KVs) ->
3868
    OldOpts = case Options of
56✔
UNCOV
3869
                  [] -> node_options(Host, Type);
×
3870
                  _ -> Options
56✔
3871
              end,
3872
    case set_xoption(Host, maps:to_list(KVs), OldOpts) of
56✔
3873
        NewOpts when is_list(NewOpts) ->
3874
            NewNode = NodeRec#pubsub_node{options = NewOpts},
42✔
3875
            case tree_call(Host, set_node, [NewNode]) of
42✔
3876
                {ok, _} -> {result, NewNode};
42✔
UNCOV
3877
                Err -> Err
×
3878
            end;
3879
        Error ->
3880
            Error
14✔
3881
    end.
3882

3883
add_opt(Key, Value, Opts) ->
3884
    [{Key, Value} | lists:keydelete(Key, 1, Opts)].
1,376✔
3885

3886
-define(SET_BOOL_XOPT(Opt, Val),
3887
        BoolVal = case Val of
3888
                      <<"0">> -> false;
3889
                      <<"1">> -> true;
3890
                      <<"false">> -> false;
3891
                      <<"true">> -> true;
3892
                      _ -> error
3893
                  end,
3894
        case BoolVal of
3895
            error -> {error, mongoose_xmpp_errors:not_acceptable()};
3896
            _ -> set_xoption(Host, Opts, add_opt(Opt, BoolVal, NewOpts))
3897
        end).
3898

3899
-define(SET_STRING_XOPT(Opt, Val),
3900
        set_xoption(Host, Opts, add_opt(Opt, Val, NewOpts))).
3901

3902
-define(SET_INTEGER_XOPT(Opt, Val, Min, Max),
3903
        case catch binary_to_integer(Val) of
3904
            IVal when is_integer(IVal), IVal >= Min ->
3905
                if (Max =:= undefined) orelse (IVal =< Max) ->
3906
                        set_xoption(Host, Opts, add_opt(Opt, IVal, NewOpts));
3907
                   true ->
3908
                        {error, mongoose_xmpp_errors:not_acceptable()}
3909
                end;
3910
            _ ->
3911
                {error, mongoose_xmpp_errors:not_acceptable()}
3912
        end).
3913

3914
-define(SET_ALIST_XOPT(Opt, Val, Vals),
3915
        case lists:member(Val, [atom_to_binary(V, latin1) || V <- Vals]) of
3916
            true ->
3917
                set_xoption(Host, Opts, add_opt(Opt, binary_to_atom(Val, utf8), NewOpts));
3918
            false ->
3919
                {error, mongoose_xmpp_errors:not_acceptable()}
3920
        end).
3921

3922
-define(SET_LIST_XOPT(Opt, Val),
3923
        set_xoption(Host, Opts, add_opt(Opt, Val, NewOpts))).
3924

3925
set_xoption(_Host, [], NewOpts) -> NewOpts;
825✔
3926
set_xoption(Host, [{<<"pubsub#roster_groups_allowed">>, Value} | Opts], NewOpts) ->
3927
    ?SET_LIST_XOPT(roster_groups_allowed, Value);
14✔
3928
set_xoption(Host, [{<<"pubsub#deliver_payloads">>, [Val]} | Opts], NewOpts) ->
3929
    ?SET_BOOL_XOPT(deliver_payloads, Val);
35✔
3930
set_xoption(Host, [{<<"pubsub#deliver_notifications">>, [Val]} | Opts], NewOpts) ->
3931
    ?SET_BOOL_XOPT(deliver_notifications, Val);
35✔
3932
set_xoption(Host, [{<<"pubsub#notify_config">>, [Val]} | Opts], NewOpts) ->
3933
    ?SET_BOOL_XOPT(notify_config, Val);
56✔
3934
set_xoption(Host, [{<<"pubsub#notify_delete">>, [Val]} | Opts], NewOpts) ->
3935
    ?SET_BOOL_XOPT(notify_delete, Val);
14✔
3936
set_xoption(Host, [{<<"pubsub#notify_retract">>, [Val]} | Opts], NewOpts) ->
3937
    ?SET_BOOL_XOPT(notify_retract, Val);
42✔
3938
set_xoption(Host, [{<<"pubsub#persist_items">>, [Val]} | Opts], NewOpts) ->
3939
    ?SET_BOOL_XOPT(persist_items, Val);
35✔
3940
set_xoption(Host, [{<<"pubsub#max_items">>, [Val]} | Opts], NewOpts) ->
3941
    MaxItems = get_max_items_node(Host),
28✔
3942
    ?SET_INTEGER_XOPT(max_items, Val, 0, MaxItems);
28✔
3943
set_xoption(Host, [{<<"pubsub#subscribe">>, [Val]} | Opts], NewOpts) ->
3944
    ?SET_BOOL_XOPT(subscribe, Val);
14✔
3945
set_xoption(Host, [{<<"pubsub#access_model">>, [Val]} | Opts], NewOpts) ->
3946
    ?SET_ALIST_XOPT(access_model, Val, [open, authorize, presence, roster, whitelist]);
349✔
3947
set_xoption(Host, [{<<"pubsub#publish_model">>, [Val]} | Opts], NewOpts) ->
3948
    ?SET_ALIST_XOPT(publish_model, Val, [publishers, subscribers, open]);
299✔
3949
set_xoption(Host, [{<<"pubsub#notification_type">>, [Val]} | Opts], NewOpts) ->
3950
    ?SET_ALIST_XOPT(notification_type, Val, [headline, normal]);
14✔
3951
set_xoption(Host, [{<<"pubsub#node_type">>, [Val]} | Opts], NewOpts) ->
3952
    ?SET_ALIST_XOPT(node_type, Val, [leaf, collection]);
133✔
3953
set_xoption(Host, [{<<"pubsub#max_payload_size">>, [Val]} | Opts], NewOpts) ->
3954
    ?SET_INTEGER_XOPT(max_payload_size, Val, 0, (?MAX_PAYLOAD_SIZE));
14✔
3955
set_xoption(Host, [{<<"pubsub#send_last_published_item">>, [Val]} | Opts], NewOpts) ->
3956
    ?SET_ALIST_XOPT(send_last_published_item, Val, [never, on_sub, on_sub_and_presence]);
56✔
3957
set_xoption(Host, [{<<"pubsub#presence_based_delivery">>, [Val]} | Opts], NewOpts) ->
3958
    ?SET_BOOL_XOPT(presence_based_delivery, Val);
42✔
3959
set_xoption(Host, [{<<"pubsub#purge_offline">>, [Val]} | Opts], NewOpts) ->
3960
    ?SET_BOOL_XOPT(purge_offline, Val);
28✔
3961
set_xoption(Host, [{<<"pubsub#title">>, Value} | Opts], NewOpts) ->
3962
    ?SET_STRING_XOPT(title, Value);
28✔
3963
set_xoption(Host, [{<<"pubsub#type">>, Value} | Opts], NewOpts) ->
3964
    ?SET_STRING_XOPT(type, Value);
14✔
3965
set_xoption(Host, [{<<"pubsub#body_xslt">>, Value} | Opts], NewOpts) ->
UNCOV
3966
    ?SET_STRING_XOPT(body_xslt, Value);
×
3967
set_xoption(Host, [{<<"pubsub#collection">>, Value} | Opts], NewOpts) ->
3968
    ?SET_LIST_XOPT(collection, Value);
140✔
3969
set_xoption(Host, [{<<"pubsub#node">>, [Value]} | Opts], NewOpts) ->
3970
    ?SET_LIST_XOPT(node, Value);
14✔
3971
set_xoption(Host, [_ | Opts], NewOpts) ->
UNCOV
3972
    set_xoption(Host, Opts, NewOpts).
×
3973

3974
get_max_items_node({_, ServerHost, _}) ->
UNCOV
3975
    get_max_items_node(ServerHost);
×
3976
get_max_items_node(Host) ->
3977
    config(serverhost(Host), max_items_node).
259✔
3978

3979
get_max_subscriptions_node({_, ServerHost, _}) ->
3980
    get_max_subscriptions_node(ServerHost);
21✔
3981
get_max_subscriptions_node(Host) ->
3982
    config(serverhost(Host), max_subscriptions_node).
649✔
3983

3984
%%%% last item cache handling
3985
maybe_start_cache_module(ServerHost, #{last_item_cache := Cache} = Opts) ->
3986
    case Cache of
263✔
3987
        false -> ok;
242✔
3988
        _Backend -> mod_pubsub_cache_backend:start(ServerHost, Opts)
21✔
3989
    end.
3990

3991
is_last_item_cache_enabled(Host) ->
3992
    cache_backend(Host) =/= false.
1,459✔
3993

3994
cache_backend(Host) ->
3995
    gen_mod:get_module_opt(serverhost(Host), mod_pubsub, last_item_cache).
1,459✔
3996

3997
set_cached_item({_, ServerHost, _}, Nidx, ItemId, Publisher, Payload) ->
3998
    set_cached_item(ServerHost, Nidx, ItemId, Publisher, Payload);
133✔
3999
set_cached_item(Host, Nidx, ItemId, Publisher, Payload) ->
4000
    is_last_item_cache_enabled(Host) andalso
874✔
4001
        mod_pubsub_cache_backend:upsert_last_item(serverhost(Host), Nidx, ItemId, Publisher, Payload).
98✔
4002

4003
unset_cached_item({_, ServerHost, _}, Nidx) ->
UNCOV
4004
    unset_cached_item(ServerHost, Nidx);
×
4005
unset_cached_item(Host, Nidx) ->
4006
    is_last_item_cache_enabled(Host) andalso
28✔
4007
        mod_pubsub_cache_backend:delete_last_item(serverhost(Host), Nidx).
14✔
4008

4009
-spec get_cached_item(ServerHost :: mod_pubsub:host(),
4010
                      Nidx :: mod_pubsub:nodeIdx()) -> false | mod_pubsub:pubsubItem().
4011
get_cached_item({_, ServerHost, _}, Nidx) ->
4012
    get_cached_item(ServerHost, Nidx);
56✔
4013
get_cached_item(Host, Nidx) ->
4014
    is_last_item_cache_enabled(Host) andalso
154✔
4015
        case mod_pubsub_cache_backend:get_last_item(serverhost(Host), Nidx) of
56✔
4016
            {ok, #pubsub_last_item{itemid = ItemId, creation = Creation, payload = Payload}} ->
4017
                    #pubsub_item{itemid = {ItemId, Nidx},
35✔
4018
                                 payload = Payload, creation = Creation,
4019
                                 modification = Creation};
4020
                _ ->
4021
                   false
21✔
4022
        end.
4023

4024
%%%% plugin handling
4025

4026
-spec host(HostType :: mongooseim:host_type(), ServerHost :: mongooseim:domain_name()) -> host().
4027
host(HostType, ServerHost) ->
4028
    SubdomainPattern = config(HostType, host),
5,629✔
4029
    mongoose_subdomain_utils:get_fqdn(SubdomainPattern, ServerHost).
5,629✔
4030

4031
-spec serverhost(host()) -> host().
4032
serverhost({_U, Server, _R})->
4033
    Server;
1,522✔
4034
serverhost(Host) ->
4035
    case config(Host, host) of
25,061✔
4036
        undefined ->
4037
            [_, ServerHost] = binary:split(Host, <<".">>),
24,481✔
4038
            ServerHost;
24,481✔
4039
        _ ->
4040
            Host
580✔
4041
    end.
4042

4043
-spec host_to_host_type(mod_pubsub:host()) -> mongooseim:host_type().
4044
host_to_host_type(Host) ->
4045
    SH = serverhost(Host),
18,431✔
4046
    {ok, HT} = mongoose_domain_api:get_host_type(SH),
18,431✔
4047
    HT.
18,431✔
4048

4049
-spec tree(HostType :: mongooseim:host_type() | host()) -> module() | nodetree_virtual.
4050
tree(HostType) ->
4051
    try gen_mod:get_module_opt(HostType, ?MODULE, nodetree)
18,431✔
4052
    catch error:{badkey, _} ->
4053
        %todo remove when pubsub supports dynamic domains
4054
        HT = host_to_host_type(HostType),
11,165✔
4055
        gen_mod:get_module_opt(HT, ?MODULE, nodetree)
11,165✔
4056
    end.
4057

4058
tree_mod(<<"virtual">>) ->
UNCOV
4059
    nodetree_virtual;   % special case, virtual does not use any backend
×
4060
tree_mod(Name) ->
4061
    binary_to_atom(<<"nodetree_", Name/binary>>, utf8).
4✔
4062

4063
-spec plugin(Name :: plugin_name()) -> module().
4064
plugin(Name) ->
4065
    binary_to_atom(<<"node_", Name/binary>>, utf8).
34,275✔
4066

4067
-spec plugins(ServerHost :: mongooseim:domain_name()) -> [plugin_name()].
4068
plugins(ServerHost) ->
4069
    Proc = gen_mod:get_module_proc(ServerHost, ?PROCNAME),
8,592✔
4070
    %% TODO This call could be replaced with persistent terms
4071
    gen_server:call(Proc, plugins).
8,592✔
4072

4073
config(ServerHost, Key) ->
4074
    config(ServerHost, Key, undefined).
34,936✔
4075
config(ServerHost, Key, Default) ->
4076
    gen_mod:get_module_opt(ServerHost, ?MODULE, Key, Default).
34,936✔
4077

4078
select_type(Host, Node) ->
4079
    select_type(serverhost(Host), Host, Node).
14✔
4080

4081
select_type(ServerHost, Host, Node) ->
4082
    select_type(ServerHost, Host, Node, hd(plugins(ServerHost))).
135✔
4083

4084
select_type(ServerHost, Host, Node, Type) ->
4085
    SelectedType = case Host of
1,771✔
4086
                       {_User, _Server, _Resource} ->
4087
                           case config(ServerHost, pep_mapping) of
227✔
UNCOV
4088
                               undefined -> ?PEPNODE;
×
4089
                               Mapping -> maps:get(Node, Mapping, ?PEPNODE)
227✔
4090
                           end;
4091
                       _ ->
4092
                           Type
1,544✔
4093
                   end,
4094
    ConfiguredTypes = plugins(ServerHost),
1,771✔
4095
    case lists:member(SelectedType, ConfiguredTypes) of
1,771✔
4096
        true -> SelectedType;
1,771✔
UNCOV
4097
        false -> hd(ConfiguredTypes)
×
4098
    end.
4099

4100
feature(Feature) -> <<(?NS_PUBSUB)/binary, "#", Feature/binary>>.
37,648✔
4101

4102
features() ->
4103
    [% see plugin "access-authorize",   % OPTIONAL
949✔
4104
     <<"access-open">>,   % OPTIONAL this relates to access_model option in node_hometree
4105
     <<"access-presence">>,   % OPTIONAL this relates to access_model option in node_pep
4106
     <<"access-whitelist">>,   % OPTIONAL
4107
     <<"collections">>,   % RECOMMENDED
4108
     <<"config-node">>,   % RECOMMENDED
4109
     <<"create-and-configure">>,   % RECOMMENDED
4110
     <<"item-ids">>,   % RECOMMENDED
4111
     <<"last-published">>,   % RECOMMENDED
4112
     <<"member-affiliation">>,   % RECOMMENDED
4113
     <<"presence-notifications">>,   % OPTIONAL
4114
     <<"presence-subscribe">>,   % RECOMMENDED
4115
     <<"publisher-affiliation">>,   % RECOMMENDED
4116
     <<"publish-only-affiliation">>,   % OPTIONAL
4117
     <<"retrieve-default">>,
4118
     <<"rsm">>,   % RECOMMENDED
4119
     <<"shim">>].   % RECOMMENDED
4120
% see plugin "retrieve-items",   % RECOMMENDED
4121
% see plugin "retrieve-subscriptions",   % RECOMMENDED
4122
% see plugin "subscribe",   % REQUIRED
4123
% see plugin "subscription-options",   % OPTIONAL
4124
% see plugin "subscription-notifications"   % OPTIONAL
4125

4126
plugin_features(Type) ->
4127
    Module = plugin(Type),
8,917✔
4128
    case catch gen_pubsub_node:features(Module) of
8,917✔
UNCOV
4129
        {'EXIT', {undef, _}} -> [];
×
4130
        Result -> Result
8,917✔
4131
    end.
4132

4133
features(Host, <<>>) ->
4134
    lists:usort(lists:foldl(fun (Plugin, Acc) ->
949✔
4135
                                    Acc ++ plugin_features(Plugin)
2,294✔
4136
                            end,
4137
                            features(), plugins(Host)));
4138
features(Host, Node) when is_binary(Node) ->
UNCOV
4139
    Action = fun (#pubsub_node{type = Type}) ->
×
4140
                     {result, plugin_features(Type)}
×
4141
             end,
UNCOV
4142
    case dirty(Host, Node, Action, ?FUNCTION_NAME) of
×
UNCOV
4143
        {result, Features} -> lists:usort(features() ++ Features);
×
UNCOV
4144
        _ -> features()
×
4145
    end.
4146

4147
%% @doc <p>node tree plugin call.</p>
4148
tree_call(HostType, Function, Args) ->
4149
    ?LOG_DEBUG(#{what => pubsub_tree_call, action_function => Function,
11,165✔
4150
                 args => Args, host_type => HostType}),
11,165✔
4151
    apply(tree(HostType), Function, Args).
11,165✔
4152

4153
tree_action(HostType, Function, Args) ->
4154
    ?LOG_DEBUG(#{what => pubsub_tree_action,
260✔
4155
        action_function => Function, args => Args}),
260✔
4156
    Fun = fun () -> tree_call(HostType, Function, Args) end,
260✔
4157
    ErrorDebug = #{
260✔
4158
      action => tree_action,
4159
      host_type => HostType,
4160
      function => Function,
4161
      args => Args
4162
     },
4163
    catch mod_pubsub_db_backend:dirty(Fun, ErrorDebug).
260✔
4164

4165
%% @doc <p>node plugin call.</p>
4166
node_call(Type, Function, Args) ->
4167
    ?LOG_DEBUG(#{what => pubsub_node_call, node_type => Type,
22,184✔
4168
        action_function => Function, args => Args}),
22,184✔
4169
    PluginModule = plugin(Type),
22,184✔
4170
    plugin_call(PluginModule, Function, Args).
22,184✔
4171

4172
-spec plugin_call(module(), atom(), [term()]) -> {result, any()} | {error, any()}.
4173
plugin_call(PluginModule, Function, Args) ->
4174
    CallModule = maybe_default_node(PluginModule, Function, Args),
22,952✔
4175
    case apply(CallModule, Function, Args) of
22,952✔
4176
        {result, Result} ->
4177
            {result, Result};
19,245✔
4178
        {error, Error} ->
4179
            {error, Error};
154✔
4180
        {'EXIT', Reason} ->
UNCOV
4181
            {error, Reason};
×
4182
        Result ->
4183
            {result, Result} %% any other return value is forced as result
3,495✔
4184
    end.
4185

4186
maybe_default_node(PluginModule, Function, Args) ->
4187
    case erlang:function_exported(PluginModule, Function, length(Args)) of
40,324✔
4188
        true ->
4189
            PluginModule;
22,952✔
4190
        _ ->
4191
           case gen_pubsub_node:based_on(PluginModule) of
17,372✔
4192
               none ->
UNCOV
4193
                   ?LOG_ERROR(#{what => pubsub_undefined_function,
×
UNCOV
4194
                       node_plugin => PluginModule, action_function => Function}),
×
UNCOV
4195
                   exit(udefined_node_plugin_function);
×
4196
               BaseModule ->
4197
                   maybe_default_node(BaseModule, Function, Args)
17,372✔
4198
           end
4199
    end.
4200

4201
node_action(Host, Type, Function, Args) ->
4202
    ?LOG_DEBUG(#{what => pubsub_node_action, sub_host => Host,
7,094✔
4203
        node_type => Type, action_function => Function, args => Args}),
7,094✔
4204
    ErrorDebug = #{
7,094✔
4205
        action => {node_action, Function},
4206
        pubsub_host => Host,
4207
        node_type => Type,
4208
        args => Args
4209
     },
4210
    mod_pubsub_db_backend:dirty(fun() ->
7,094✔
4211
                                        node_call(Type, Function, Args)
7,094✔
4212
                                end, ErrorDebug).
4213

4214
dirty(Host, Node, Action, ActionName) ->
4215
    ErrorDebug = #{
3,337✔
4216
      pubsub_host => Host,
4217
      node_name => Node,
4218
      action => ActionName },
4219
    mod_pubsub_db_backend:dirty(db_call_fun(Host, Node, Action), ErrorDebug).
3,337✔
4220

4221
transaction(Host, Node, Action, ActionName) ->
4222
    ErrorDebug = #{
1,108✔
4223
      pubsub_host => Host,
4224
      node_name => Node,
4225
      action => ActionName
4226
     },
4227
    mod_pubsub_db_backend:transaction(db_call_fun(Host, Node, Action), ErrorDebug).
1,108✔
4228

4229
db_call_fun(Host, Node, Action) ->
4230
    fun () ->
4,445✔
4231
            case tree_call(Host, get_node, [Host, Node]) of
4,483✔
4232
                #pubsub_node{} = N ->
4233
                    case Action(N) of
4,327✔
4234
                        {result, Result} -> {result, {N, Result}};
3,967✔
UNCOV
4235
                        {atomic, {result, Result}} -> {result, {N, Result}};
×
4236
                        Other -> Other
322✔
4237
                    end;
4238
                Error -> Error
156✔
4239
            end
4240
    end.
4241

4242
%%%% helpers
4243

4244
%% Add pubsub-specific error element
4245
extended_error(#xmlel{} = Error, Ext) when is_binary(Ext) ->
4246
    extend_error(Error, Ext, #{<<"xmlns">> => ?NS_PUBSUB_ERRORS}).
8,236✔
4247

4248
unsupported_error(Error, Feature) ->
4249
    %% Give a uniq identifier
4250
    extend_error(Error, <<"unsupported">>,
2,708✔
4251
                 #{<<"xmlns">> => ?NS_PUBSUB_ERRORS, <<"feature">> => Feature}).
4252

4253
extend_error(#xmlel{children = SubEls} = El, Ext, ExtAttrs) ->
4254
    El#xmlel{children = lists:reverse([#xmlel{name = Ext, attrs = ExtAttrs} | SubEls])}.
10,944✔
4255

4256
string_to_ljid(JID) ->
4257
    case jid:from_binary(JID) of
810✔
4258
        error ->
UNCOV
4259
            {<<>>, <<>>, <<>>};
×
4260
        J ->
4261
            case jid:to_lower(J) of
810✔
UNCOV
4262
                error -> {<<>>, <<>>, <<>>};
×
4263
                J1 -> J1
810✔
4264
            end
4265
    end.
4266

4267
-spec uniqid() -> mod_pubsub:itemId().
4268
uniqid() ->
4269
    uuid:uuid_to_string(uuid:get_v4(), binary_standard).
346✔
4270

4271
node_attr(Node) -> #{<<"node">> => Node}.
7,060✔
4272

4273
item_attr([])     -> #{};
×
4274
item_attr(ItemId) -> #{<<"id">> => ItemId}.
2,608✔
4275

4276
item_attr(ItemId, undefined) -> item_attr(ItemId);
280✔
UNCOV
4277
item_attr([], Publisher)     -> #{<<"publisher">> => jid:to_binary(jid:to_lower(Publisher))};
×
4278
item_attr(ItemId, Publisher) -> #{<<"id">> => ItemId,
42✔
4279
                                  <<"publisher">> => jid:to_binary(jid:to_lower(Publisher))}.
4280

4281
items_els(Items) ->
4282
    [#xmlel{name = <<"item">>, attrs = item_attr(ItemId, Publisher), children = Payload}
301✔
4283
     || #pubsub_item{itemid = {ItemId, _}, publisher = Publisher, payload = Payload } <- Items].
301✔
4284

4285
-spec add_message_type(Message :: exml:element(), Type :: atom()) -> exml:element().
4286
add_message_type(Message, normal) -> Message;
10✔
4287
add_message_type(#xmlel{name = <<"message">>, attrs = Attrs, children = Els}, Type) ->
4288
    #xmlel{name = <<"message">>,
2,699✔
4289
           attrs = Attrs#{<<"type">> => atom_to_binary(Type, utf8)},
4290
           children = Els};
4291
add_message_type(XmlEl, _Type) ->
UNCOV
4292
    XmlEl.
×
4293

4294
maybe_add_shim_headers(Stanza, false, _SubIDs, _OriginNode, _SubNode) ->
4295
    Stanza;
14✔
4296
maybe_add_shim_headers(Stanza, true, SubIDs, OriginNode, SubNode) ->
4297
    Headers1 = case SubIDs of
308✔
4298
                   [_OnlyOneSubID] ->
4299
                       [];
301✔
4300
                   _ ->
4301
                       subid_shim(SubIDs)
7✔
4302
               end,
4303
    Headers2 = case SubNode of
308✔
4304
                   OriginNode ->
4305
                       Headers1;
210✔
4306
                   _ ->
4307
                       [collection_shim(SubNode) | Headers1]
98✔
4308
               end,
4309
    add_headers(Stanza, <<"headers">>, ?NS_SHIM, Headers2).
308✔
4310

4311
add_extended_headers(Stanza, HeaderEls) ->
4312
    add_headers(Stanza, <<"addresses">>, ?NS_ADDRESS, HeaderEls).
133✔
4313

4314
add_headers(#xmlel{name = Name, attrs = Attrs, children = Els}, HeaderName, HeaderNS, HeaderEls) ->
4315
    HeaderEl = #xmlel{name = HeaderName,
441✔
4316
                      attrs = #{<<"xmlns">> => HeaderNS},
4317
                      children = HeaderEls},
4318
    #xmlel{name = Name, attrs = Attrs,
441✔
4319
           children = lists:append(Els, [HeaderEl])}.
4320

4321
subid_shim(SubIds) ->
4322
    [#xmlel{ name = <<"header">>,
7✔
4323
             attrs = #{<<"name">> => <<"SubId">>},
4324
             children = [#xmlcdata{ content = SubId }]}
4325
     || SubId <- SubIds].
7✔
4326

4327
collection_shim(CollectionNode) ->
4328
    #xmlel{ name = <<"header">>,
98✔
4329
            attrs = #{<<"name">> => <<"Collection">>},
4330
            children = [#xmlcdata{ content = CollectionNode }] }.
4331

4332
%% The argument is a list of Jids because this function could be used
4333
%% with the 'pubsub#replyto' (type=jid-multi) node configuration.
4334
extended_headers(Jids) ->
4335
    [#xmlel{name = <<"address">>,
133✔
4336
            attrs = #{<<"type">> => <<"replyto">>, <<"jid">> => Jid}}
4337
     || Jid <- Jids].
133✔
4338

4339
-spec on_user_offline(Acc, Params, Extra) -> {ok, Acc} when
4340
    Acc :: mongoose_acc:t(),
4341
    Params :: #{jid := jid:jid()},
4342
    Extra :: gen_hook:extra().
4343
on_user_offline(Acc,
4344
                #{jid := #jid{luser = User, lserver = Server, lresource = Resource}},
4345
                #{host_type := HostType}) ->
4346
    case user_resources(User, Server) of
2,601✔
4347
        [] -> purge_offline(HostType, {User, Server, Resource});
2,554✔
4348
        _ -> true
47✔
4349
    end,
4350
    {ok, Acc}.
2,601✔
4351

4352
purge_offline(HT, {_, LServer, _} = LJID) ->
4353
    Host = host(HT, LServer),
2,554✔
4354
    Plugins = plugins(LServer),
2,554✔
4355
    Affs = lists:foldl(
2,554✔
4356
             fun (PluginType, Acc) ->
4357
                     check_plugin_features_and_acc_affs(Host, PluginType, LJID, Acc)
3,785✔
4358
             end, [], Plugins),
4359
    lists:foreach(
2,554✔
4360
      fun ({Node, Affiliation}) ->
4361
              Options = Node#pubsub_node.options,
710✔
4362
              IsPublisherOrOwner = lists:member(Affiliation, [owner, publisher, publish_only]),
710✔
4363
              OpenNode = get_option(Options, publish_model) == open,
710✔
4364
              ShouldPurge = get_option(Options, purge_offline)
710✔
4365
              andalso get_option(Options, persist_items),
14✔
4366
              case (IsPublisherOrOwner or OpenNode) and ShouldPurge of
710✔
4367
                  true -> purge_offline(Host, LJID, Node);
14✔
4368
                  false -> ok
696✔
4369
              end
4370
      end, lists:usort(lists:flatten(Affs))).
4371

4372
check_plugin_features_and_acc_affs(Host, PluginType, LJID, AffsAcc) ->
4373
    Features = plugin_features(PluginType),
3,785✔
4374
    case lists:member(<<"retract-items">>, Features)
3,785✔
4375
         andalso lists:member(<<"persistent-items">>, Features)
3,004✔
4376
         andalso lists:member(<<"retrieve-affiliations">>, Features) of
3,004✔
4377
        true ->
4378
            {result, Affs} = node_action(Host, PluginType, get_entity_affiliations, [Host, LJID]),
3,004✔
4379
            [Affs | AffsAcc];
3,004✔
4380
        false ->
4381
            ?LOG_DEBUG(#{what => pubsub_plugin_features_check_error,
781✔
4382
                text => <<"Cannot purge items on offline">>,
4383
                plugin => PluginType, user => jid:to_binary(LJID)}),
781✔
4384
            AffsAcc
781✔
4385
    end.
4386

4387
purge_offline(Host, {User, Server, _} = _LJID, #pubsub_node{ id = Nidx, type = Type } = Node) ->
4388
    case node_action(Host, Type, get_items, [Nidx, service_jid(Host), #{}]) of
14✔
4389
        {result, {[], _}} ->
UNCOV
4390
            ok;
×
4391
        {result, {Items, _}} ->
4392
            lists:foreach(fun(#pubsub_item{itemid = {ItemId, _}, modification = {_, {U, S, _}}})
14✔
4393
                                when (U == User) and (S == Server) ->
4394
                                  purge_item_of_offline_user(Host, Node, ItemId, U, S);
14✔
4395
                             (_) ->
4396
                                  true
14✔
4397
                          end, Items);
4398
        Error ->
UNCOV
4399
            Error
×
4400
    end.
4401

4402
purge_item_of_offline_user(Host, #pubsub_node{ id = Nidx, nodeid = {_, NodeId},
4403
                                               options = Options, type = Type }, ItemId, U, S) ->
4404
    PublishModel = get_option(Options, publish_model),
14✔
4405
    ForceNotify = get_option(Options, notify_retract),
14✔
4406
    case node_action(Host, Type, delete_item, [Nidx, {U, S, <<>>}, PublishModel, ItemId]) of
14✔
4407
        {result, {_, broadcast}} ->
4408
            broadcast_retract_items(Host, NodeId, Nidx, Type, Options, [ItemId], ForceNotify),
14✔
4409
            case get_cached_item(Host, Nidx) of
14✔
4410
                #pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx);
×
4411
                _ -> ok
14✔
4412
            end;
4413
        {result, _} ->
UNCOV
4414
            ok;
×
4415
        Error ->
UNCOV
4416
            Error
×
4417
    end.
4418

4419
timestamp() ->
UNCOV
4420
    os:system_time(microsecond).
×
4421

4422
make_error_reply(#iq{ sub_el = SubEl } = IQ, #xmlel{} = ErrorEl) ->
4423
    IQ#iq{type = error, sub_el = [ErrorEl, SubEl]};
35✔
4424
make_error_reply(#iq{ sub_el = SubEl } = IQ, Error) ->
UNCOV
4425
    ?LOG_ERROR(#{what => pubsub_crash, reason => Error}),
×
4426
    IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:internal_server_error(), SubEl]};
×
4427
make_error_reply(Packet, #xmlel{} = ErrorEl) ->
4428
    jlib:make_error_reply(Packet, ErrorEl);
413✔
4429
make_error_reply(Packet, Error) ->
UNCOV
4430
    ?LOG_ERROR(#{what => pubsub_crash, reason => Error}),
×
UNCOV
4431
    jlib:make_error_reply(Packet, mongoose_xmpp_errors:internal_server_error()).
×
4432

4433
config_metrics(Host) ->
4434
    mongoose_module_metrics:opts_for_module(Host, ?MODULE, [backend]).
132✔
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