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