./ct_report/coverage/mod_event_pusher_push.COVER.html

1 %%%-------------------------------------------------------------------
2 %%% @author Rafal Slota
3 %%% @copyright (C) 2017 Erlang Solutions Ltd.
4 %%% This software is released under the Apache License, Version 2.0
5 %%% cited in 'LICENSE.txt'.
6 %%% @end
7 %%%-------------------------------------------------------------------
8 %%% @doc
9 %%% Implementation of XEP-0357
10 %%% @end
11 %%%-------------------------------------------------------------------
12 -module(mod_event_pusher_push).
13 -author('rafal.slota@erlang-solutions.com').
14 -behavior(gen_mod).
15 -behavior(mod_event_pusher).
16 -behaviour(mongoose_module_metrics).
17 -xep([{xep, 357}, {version, "0.4.1"}]).
18
19 -include("mod_event_pusher_events.hrl").
20 -include("mongoose.hrl").
21 -include("session.hrl").
22 -include("jlib.hrl").
23 -include("mongoose_config_spec.hrl").
24
25 -define(SESSION_KEY, publish_service).
26
27 %%--------------------------------------------------------------------
28 %% Exports
29 %%--------------------------------------------------------------------
30
31 %% gen_mod behaviour
32 -export([start/2, stop/1, config_spec/0]).
33
34 %% mongoose_module_metrics behaviour
35 -export([config_metrics/1]).
36
37 %% mod_event_pusher behaviour
38 -export([push_event/2]).
39
40 %% Hooks and IQ handlers
41 -export([iq_handler/4,
42 remove_user/3]).
43
44 %% Plugin utils
45 -export([cast/3]).
46 -export([is_virtual_pubsub_host/3]).
47 -export([disable_node/4]).
48
49 -ignore_xref([iq_handler/4]).
50
51 %% Types
52 -type publish_service() :: {PubSub :: jid:jid(), Node :: pubsub_node(), Form :: form()}.
53 -type pubsub_node() :: binary().
54 -type form() :: #{binary() => binary()}.
55
56 -export_type([pubsub_node/0, form/0]).
57 -export_type([publish_service/0]).
58
59 %%--------------------------------------------------------------------
60 %% gen_mod callbacks
61 %%--------------------------------------------------------------------
62 -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> any().
63 start(HostType, Opts) ->
64 24 ?LOG_INFO(#{what => event_pusher_starting, host_type => HostType}),
65 24 start_pool(HostType, Opts),
66 24 mod_event_pusher_push_backend:init(HostType, Opts),
67 24 mod_event_pusher_push_plugin:init(HostType, Opts),
68 24 init_iq_handlers(HostType, Opts),
69 24 gen_hook:add_handler(remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 90),
70 24 ok.
71
72 start_pool(HostType, #{wpool := WpoolOpts}) ->
73 24 {ok, _} = mongoose_wpool:start(generic, HostType, pusher_push, maps:to_list(WpoolOpts)).
74
75 init_iq_handlers(HostType, #{iqdisc := IQDisc}) ->
76 24 gen_iq_handler:add_iq_handler(ejabberd_local, HostType, ?NS_PUSH, ?MODULE,
77 iq_handler, IQDisc),
78 24 gen_iq_handler:add_iq_handler(ejabberd_sm, HostType, ?NS_PUSH, ?MODULE,
79 iq_handler, IQDisc).
80
81 -spec stop(mongooseim:host_type()) -> ok.
82 stop(HostType) ->
83 24 gen_hook:delete_handler(remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 90),
84
85 24 gen_iq_handler:remove_iq_handler(ejabberd_sm, HostType, ?NS_PUSH),
86 24 gen_iq_handler:remove_iq_handler(ejabberd_local, HostType, ?NS_PUSH),
87
88 24 mongoose_wpool:stop(generic, HostType, pusher_push),
89 24 ok.
90
91 -spec config_spec() -> mongoose_config_spec:config_section().
92 config_spec() ->
93 66 VirtPubSubHost = #option{type = string, validate = subdomain_template,
94 process = fun mongoose_subdomain_utils:make_subdomain_pattern/1},
95 66 #section{
96 items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc(),
97 <<"backend">> => #option{type = atom, validate = {module, ?MODULE}},
98 <<"wpool">> => wpool_spec(),
99 <<"plugin_module">> => #option{type = atom, validate = module},
100 <<"virtual_pubsub_hosts">> => #list{items = VirtPubSubHost}},
101 defaults = #{<<"iqdisc">> => one_queue,
102 <<"backend">> => mnesia,
103 <<"plugin_module">> => mod_event_pusher_push_plugin:default_plugin_module(),
104 <<"virtual_pubsub_hosts">> => []}
105 }.
106
107 wpool_spec() ->
108 66 Wpool = mongoose_config_spec:wpool(#{<<"strategy">> => available_worker}),
109 66 Wpool#section{include = always}.
110
111 %%--------------------------------------------------------------------
112 %% mod_event_pusher callbacks
113 %%--------------------------------------------------------------------
114 -spec push_event(mongoose_acc:t(), mod_event_pusher:event()) -> mongoose_acc:t().
115 push_event(Acc, Event = #chat_event{direction = out, to = To, type = Type})
116 when Type =:= groupchat;
117 Type =:= chat ->
118 376 BareRecipient = jid:to_bare(To),
119 376 do_push_event(Acc, Event, BareRecipient);
120 push_event(Acc, Event = #unack_msg_event{to = To}) ->
121 10 BareRecipient = jid:to_bare(To),
122 10 do_push_event(Acc, Event, BareRecipient);
123 push_event(Acc, _) ->
124 890 Acc.
125
126 %%--------------------------------------------------------------------
127 %% Hooks and IQ handlers
128 %%--------------------------------------------------------------------
129 -spec remove_user(Acc, Params, Extra) -> {ok, Acc} when
130 Acc :: mongoose_acc:t(),
131 Params :: #{jid := jid:jid()},
132 Extra :: map().
133 remove_user(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, _) ->
134 2 R = mod_event_pusher_push_backend:disable(LServer, jid:make_noprep(LUser, LServer, <<>>)),
135 2 mongoose_lib:log_if_backend_error(R, ?MODULE, ?LINE, {Acc, LUser, LServer}),
136 2 {ok, Acc}.
137
138 -spec iq_handler(From :: jid:jid(), To :: jid:jid(), Acc :: mongoose_acc:t(),
139 IQ :: jlib:iq()) ->
140 {mongoose_acc:t(), jlib:iq() | ignore}.
141 iq_handler(_From, _To, Acc, IQ = #iq{type = get, sub_el = SubEl}) ->
142
:-(
{Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:not_allowed()]}};
143 iq_handler(From, _To, Acc, IQ = #iq{type = set, sub_el = Request}) ->
144 125 HostType = mongoose_acc:host_type(Acc),
145 125 Res = case parse_request(Request) of
146 {enable, BarePubSubJID, Node, FormFields} ->
147 106 ok = enable_node(HostType, From, BarePubSubJID, Node, FormFields),
148 106 store_session_info(From, {BarePubSubJID, Node, FormFields}, Acc),
149 106 IQ#iq{type = result, sub_el = []};
150 {disable, BarePubsubJID, Node} ->
151 9 ok = disable_node(HostType, From, BarePubsubJID, Node),
152 9 IQ#iq{type = result, sub_el = []};
153 bad_request ->
154 10 IQ#iq{type = error, sub_el = [Request, mongoose_xmpp_errors:bad_request()]}
155 end,
156 125 {Acc, Res}.
157
158 %%--------------------------------------------------------------------
159 %% Plugin utils API
160 %%--------------------------------------------------------------------
161 -spec disable_node(mongooseim:host_type(), UserJID :: jid:jid(), BarePubSubJID :: jid:jid(),
162 Node :: pubsub_node()) -> ok | {error, Reason :: term()}.
163 disable_node(HostType, UserJID, BarePubSubJID, Node) ->
164 11 BareUserJID = jid:to_bare(UserJID),
165 11 maybe_remove_push_node_from_sessions_info(BareUserJID, BarePubSubJID, Node),
166 11 mod_event_pusher_push_backend:disable(HostType, BareUserJID, BarePubSubJID, Node).
167
168 -spec cast(mongooseim:host_type(), F :: function(), A :: [any()]) -> any().
169 cast(HostType, F, A) ->
170 130 mongoose_wpool:cast(generic, HostType, pusher_push, {erlang, apply, [F, A]}).
171
172 -spec is_virtual_pubsub_host(HostType :: mongooseim:host_type(), %% recipient host type
173 RecipientDomain :: mongooseim:domain_name(),
174 VirtPubsubDomain :: mongooseim:domain_name()) -> boolean().
175 is_virtual_pubsub_host(HostType, RecipientDomain, VirtPubsubDomain) ->
176 130 Templates = gen_mod:get_module_opt(HostType, ?MODULE, virtual_pubsub_hosts),
177 130 PredFn = fun(Template) ->
178 69 mongoose_subdomain_utils:is_subdomain(Template,
179 RecipientDomain,
180 VirtPubsubDomain)
181 end,
182 130 lists:any(PredFn, Templates).
183
184 %%--------------------------------------------------------------------
185 %% local functions
186 %%--------------------------------------------------------------------
187 -spec do_push_event(mongoose_acc:t(), mod_event_pusher:event(), jid:jid()) -> mongoose_acc:t().
188 do_push_event(Acc, Event, BareRecipient) ->
189 386 case mod_event_pusher_push_plugin:prepare_notification(Acc, Event) of
190 84 skip -> Acc;
191 Payload ->
192 302 HostType = mongoose_acc:host_type(Acc),
193 302 {ok, Services} = mod_event_pusher_push_backend:get_publish_services(HostType,
194 BareRecipient),
195 302 FilteredService = mod_event_pusher_push_plugin:should_publish(Acc, Event, Services),
196 302 mod_event_pusher_push_plugin:publish_notification(Acc, Event, Payload, FilteredService)
197 end.
198
199 -spec parse_request(Request :: exml:element()) ->
200 {enable, jid:jid(), pubsub_node(), form()} |
201 {disable, jid:jid(), pubsub_node() | undefined} |
202 bad_request.
203 parse_request(#xmlel{name = <<"enable">>} = Request) ->
204 113 JID = jid:from_binary(exml_query:attr(Request, <<"jid">>, <<>>)),
205 113 Node = exml_query:attr(Request, <<"node">>, <<>>), %% Treat unset node as empty - both forbidden
206 113 Form = mongoose_data_forms:find_form(Request),
207
208 113 case {JID, Node, parse_form(Form)} of
209 2 {_, _, invalid_form} -> bad_request;
210 3 {_, <<>>, _} -> bad_request;
211 2 {error, _, _} -> bad_request;
212
:-(
{#jid{lserver = <<>>}, _, _} -> bad_request;
213 {JID, Node, FormFields} ->
214 106 {enable, jid:to_bare(JID), Node, FormFields}
215 end;
216 parse_request(#xmlel{name = <<"disable">>} = Request) ->
217 12 JID = jid:from_binary(exml_query:attr(Request, <<"jid">>, <<>>)),
218 12 Node = exml_query:attr(Request, <<"node">>, undefined),
219
220 12 case {JID, Node} of
221 3 {error, _} -> bad_request;
222
:-(
{_, <<>>} -> bad_request; %% Node may not be set, but shouldn't be empty
223
:-(
{#jid{lserver = <<>>}, _} -> bad_request;
224 {JID, Node} ->
225 9 {disable, jid:to_bare(JID), Node}
226 end;
227 parse_request(_) ->
228
:-(
bad_request.
229
230 -spec parse_form(undefined | exml:element()) -> invalid_form | form().
231 parse_form(undefined) ->
232 16 #{};
233 parse_form(Form) ->
234 97 parse_form_fields(Form).
235
236 -spec parse_form_fields(exml:element()) -> invalid_form | form().
237 parse_form_fields(Form) ->
238 97 case mongoose_data_forms:parse_form_fields(Form) of
239 #{type := <<"submit">>, ns := ?NS_PUBSUB_PUB_OPTIONS, kvs := KVs} ->
240 96 case maps:filtermap(fun(_, [V]) -> {true, V};
241 1 (_, _) -> false
242 end, KVs) of
243 ParsedKVs when map_size(ParsedKVs) < map_size(KVs) ->
244 1 invalid_form;
245 ParsedKVs ->
246 95 ParsedKVs
247 end;
248 _ ->
249 1 invalid_form
250 end.
251
252 -spec enable_node(mongooseim:host_type(), jid:jid(), jid:jid(), pubsub_node(), form()) ->
253 ok | {error, Reason :: term()}.
254 enable_node(HostType, From, BarePubSubJID, Node, FormFields) ->
255 106 mod_event_pusher_push_backend:enable(HostType, jid:to_bare(From), BarePubSubJID, Node,
256 FormFields).
257
258 -spec store_session_info(jid:jid(), publish_service(), mongoose_acc:t()) -> any().
259 store_session_info(Jid, Service, Acc) ->
260 106 OriginSid = mongoose_acc:get(c2s, origin_sid, undefined, Acc),
261 106 ejabberd_sm:store_info(Jid, OriginSid, ?SESSION_KEY, Service).
262
263 -spec maybe_remove_push_node_from_sessions_info(jid:jid(), jid:jid(), pubsub_node() | undefined) ->
264 ok.
265 maybe_remove_push_node_from_sessions_info(From, PubSubJid, Node) ->
266 11 AllSessions = ejabberd_sm:get_raw_sessions(From),
267 11 find_and_remove_push_node(From, AllSessions, PubSubJid, Node).
268
269 -spec find_and_remove_push_node(jid:jid(), [ejabberd_sm:session()],
270 jid:jid(), pubsub_node() | undefined) -> ok.
271 find_and_remove_push_node(_From, [], _,_) ->
272 11 ok;
273 find_and_remove_push_node(From, [RawSession | Rest], PubSubJid, Node) ->
274 13 case my_push_node(RawSession, PubSubJid, Node) of
275 true ->
276 10 LResource = mongoose_session:get_resource(RawSession),
277 10 JID = jid:replace_resource(From, LResource),
278 10 Sid = RawSession#session.sid,
279 10 ejabberd_sm:remove_info(JID, Sid, ?SESSION_KEY),
280 10 find_and_remove_push_node(From, Rest, PubSubJid, Node);
281 false ->
282 3 find_and_remove_push_node(From, Rest, PubSubJid, Node)
283 end.
284
285 -spec my_push_node(ejabberd_sm:session(), jid:jid(), pubsub_node() | undfined) -> boolean().
286 my_push_node(RawSession, PubSubJid, Node) ->
287 13 case mongoose_session:get_info(RawSession, ?SESSION_KEY, undefined) of
288 {?SESSION_KEY, {PubSubJid, Node, _}} ->
289 8 true;
290 {?SESSION_KEY, {PubSubJid, _, _}} when Node =:= undefined ->
291 %% The node is undefined which means that a user wants to
292 %% disable all the push nodes for the specified service
293 2 true;
294 3 _ -> false
295 end.
296
297 -spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}].
298 config_metrics(HostType) ->
299 24 mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]).
Line Hits Source