Skip to content

Commit

Permalink
Merge pull request #4028 from esl/mongoose-data-forms
Browse files Browse the repository at this point in the history
Implement XEP-0004: Data Forms in a separate module
  • Loading branch information
JanuszJakubiec committed May 31, 2023
2 parents 2f1710a + 4ff9d5a commit 0bd9702
Show file tree
Hide file tree
Showing 32 changed files with 744 additions and 1,134 deletions.
5 changes: 2 additions & 3 deletions big_tests/tests/push_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ all() ->
].

groups() ->
G = [
[
{toggling, [parallel], [
enable_should_fail_with_missing_attributes,
enable_should_fail_with_invalid_attributes,
Expand Down Expand Up @@ -68,8 +68,7 @@ groups() ->
muclight_msg_notify_if_user_offline_with_publish_options,
muclight_msg_notify_stops_after_disabling
]}
],
ct_helper:repeat_all_until_all_ok(G).
].

notification_groups() ->
[
Expand Down
4 changes: 2 additions & 2 deletions big_tests/tests/push_integration_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -954,8 +954,8 @@ add_user_server_to_whitelist(User, {NodeAddr, NodeName}) ->
assert_push_notification_in_session(User, NodeName, Service, DeviceToken) ->
Info = mongoose_helper:get_session_info(?RPC_SPEC, User),
{_JID, NodeName, Details} = maps:get(?SESSION_KEY, Info),
?assertMatch({<<"service">>, Service}, lists:keyfind(<<"service">>, 1, Details)),
?assertMatch({<<"device_id">>, DeviceToken}, lists:keyfind(<<"device_id">>, 1, Details)).
?assertMatch(#{<<"service">> := Service}, Details),
?assertMatch(#{<<"device_id">> := DeviceToken}, Details).

wait_for_push_request(DeviceToken) ->
mongoose_push_mock:wait_for_push_request(DeviceToken, 10000).
Expand Down
48 changes: 2 additions & 46 deletions include/mod_vcard.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -16,51 +16,7 @@
-export_type([vcard_search/0]).

-define(TLFIELD(Type, Label, Var),
#xmlel{name = <<"field">>,
attrs =
[{<<"type">>, Type},
{<<"label">>, translate:translate(Lang, Label)},
{<<"var">>, Var}],
children = []}).

-define(LFIELD(Label, Var),
#xmlel{name = <<"field">>,
attrs = [{<<"label">>, translate:translate(Lang, Label)},
{<<"var">>, Var}]}).
#{var => Var, type => Type, label => translate:translate(Lang, Label)}).

-define(FIELD(Var, Val),
#xmlel{name = <<"field">>, attrs = [{<<"var">>, Var}],
children = [#xmlel{name = <<"value">>,
children = [#xmlcdata{content = Val}]}]}).

-define(FORM(JID, SearchFields,Lang),
[#xmlel{name = <<"instructions">>, attrs = [],
children =
[{xmlcdata,
translate:translate(Lang,
<<"You need an x:data capable client to "
"search">>)}]},
#xmlel{name = <<"x">>,
attrs =
[{<<"xmlns">>, ?NS_XDATA},
{<<"type">>, <<"form">>}],
children =
[#xmlel{name = <<"title">>, attrs = [],
children =
[{xmlcdata,
<<(translate:translate(Lang,
<<"Search users in ">>))/binary,
(jid:to_binary(JID))/binary>>}]},
#xmlel{name = <<"instructions">>, attrs = [],
children =
[{xmlcdata,
translate:translate(Lang,
<<"Fill in fields to search for any matching "
"Jabber User">>)}]}]
++
lists:map(fun ({X, Y}) ->
?TLFIELD(<<"text-single">>, X, Y)
end,
SearchFields)}]).


#{var => Var, values => [Val]}).
20 changes: 1 addition & 19 deletions src/adhoc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ parse_request(#iq{type = set, lang = Lang, sub_el = SubEl, xmlns = ?NS_COMMANDS}
Node = xml:get_tag_attr_s(<<"node">>, SubEl),
SessionID = xml:get_tag_attr_s(<<"sessionid">>, SubEl),
Action = xml:get_tag_attr_s(<<"action">>, SubEl),
XData = find_xdata_el(SubEl),
XData = mongoose_data_forms:find_form(SubEl, false),
#xmlel{children = AllEls} = SubEl,
Others = case XData of
false ->
Expand All @@ -68,24 +68,6 @@ parse_request(#iq{type = set, lang = Lang, sub_el = SubEl, xmlns = ?NS_COMMANDS}
parse_request(_) ->
{error, mongoose_xmpp_errors:bad_request()}.

%% @doc Borrowed from mod_vcard.erl
-spec find_xdata_el(exml:element()) -> false | exml:element().
find_xdata_el(#xmlel{children = SubEls}) ->
find_xdata_el1(SubEls).

%% @private
find_xdata_el1([]) ->
false;
find_xdata_el1([XE = #xmlel{attrs = Attrs} | Els]) ->
case xml:get_attr_s(<<"xmlns">>, Attrs) of
?NS_XDATA ->
XE;
_ ->
find_xdata_el1(Els)
end;
find_xdata_el1([_ | Els]) ->
find_xdata_el1(Els).

%% @doc Produce a <command/> node to use as response from an adhoc_response
%% record, filling in values for language, node and session id from
%% the request.
Expand Down
44 changes: 14 additions & 30 deletions src/event_pusher/mod_event_pusher_push.erl
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,9 @@
%% Types
-type publish_service() :: {PubSub :: jid:jid(), Node :: pubsub_node(), Form :: form()}.
-type pubsub_node() :: binary().
-type form_field() :: {Name :: binary(), Value :: binary()}.
-type form() :: [form_field()].
-type form() :: #{binary() => binary()}.

-export_type([pubsub_node/0, form_field/0, form/0]).
-export_type([pubsub_node/0, form/0]).
-export_type([publish_service/0]).

%%--------------------------------------------------------------------
Expand Down Expand Up @@ -204,7 +203,7 @@ do_push_event(Acc, Event, BareRecipient) ->
parse_request(#xmlel{name = <<"enable">>} = Request) ->
JID = jid:from_binary(exml_query:attr(Request, <<"jid">>, <<>>)),
Node = exml_query:attr(Request, <<"node">>, <<>>), %% Treat unset node as empty - both forbidden
Form = exml_query:subelement(Request, <<"x">>),
Form = mongoose_data_forms:find_form(Request),

case {JID, Node, parse_form(Form)} of
{_, _, invalid_form} -> bad_request;
Expand All @@ -230,41 +229,26 @@ parse_request(_) ->

-spec parse_form(undefined | exml:element()) -> invalid_form | form().
parse_form(undefined) ->
[];
#{};
parse_form(Form) ->
case is_valid_form(Form) of
true ->
parse_form_fields(Form);
false ->
invalid_form
end.

-spec is_valid_form(exml:element()) -> boolean().
is_valid_form(Form) ->
IsForm = ?NS_XDATA == exml_query:attr(Form, <<"xmlns">>),
IsSubmit = <<"submit">> == exml_query:attr(Form, <<"type">>, <<"submit">>),
IsForm andalso IsSubmit.
parse_form_fields(Form).

-spec parse_form_fields(exml:element()) -> invalid_form | form().
parse_form_fields(Form) ->
FieldsXML = exml_query:subelements(Form, <<"field">>),
Fields = [{exml_query:attr(Field, <<"var">>),
exml_query:path(Field, [{element, <<"value">>}, cdata])} || Field <- FieldsXML],
case lists:keytake(<<"FORM_TYPE">>, 1, Fields) of
{value, {_, ?NS_PUBSUB_PUB_OPTIONS}, CustomFields} ->
case are_form_fields_valid(CustomFields) of
true ->
CustomFields;
false ->
invalid_form
case mongoose_data_forms:parse_form_fields(Form) of
#{type := <<"submit">>, ns := ?NS_PUBSUB_PUB_OPTIONS, kvs := KVs} ->
case maps:filtermap(fun(_, [V]) -> {true, V};
(_, _) -> false
end, KVs) of
ParsedKVs when map_size(ParsedKVs) < map_size(KVs) ->
invalid_form;
ParsedKVs ->
ParsedKVs
end;
_ ->
invalid_form
end.

are_form_fields_valid(Fields) ->
lists:all(fun({Key, Value}) -> is_binary(Key) andalso is_binary(Value) end, Fields).

-spec enable_node(mongooseim:host_type(), jid:jid(), jid:jid(), pubsub_node(), form()) ->
ok | {error, Reason :: term()}.
enable_node(HostType, From, BarePubSubJID, Node, FormFields) ->
Expand Down
2 changes: 1 addition & 1 deletion src/event_pusher/mod_event_pusher_push_plugin.erl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

-optional_callbacks([should_publish/3, prepare_notification/2, publish_notification/4]).

-type push_payload() :: mod_event_pusher_push:form().
-type push_payload() :: [{binary(), binary()}].
-export_type([push_payload/0]).
%%--------------------------------------------------------------------
%% API
Expand Down
27 changes: 9 additions & 18 deletions src/event_pusher/mod_event_pusher_push_plugin_defaults.erl
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,10 @@ push_content_fields(SenderId, BodyCData, MessageCount) ->
PushPayload :: mod_event_pusher_push_plugin:push_payload()) ->
any().
publish_via_hook(Acc0, HostType, To, {PubsubJID, Node, Form}, PushPayload) ->
OptionMap = maps:from_list(Form),
%% Acc is ignored by mod_push_service_mongoosepush, added here only for
%% traceability purposes and push_SUITE code unification
Acc = mongoose_acc:set(push_notifications, pubsub_jid, PubsubJID, Acc0),
case mongoose_hooks:push_notifications(HostType, Acc, [maps:from_list(PushPayload)], OptionMap) of
case mongoose_hooks:push_notifications(HostType, Acc, [maps:from_list(PushPayload)], Form) of
{error, device_not_registered} ->
%% We disable the push node in case the error type is device_not_registered
mod_event_pusher_push:disable_node(HostType, To, PubsubJID, Node);
Expand Down Expand Up @@ -182,34 +181,26 @@ handle_publish_response(HostType, Recipient, PubsubJID, Node, #iq{type = error,
PushPayload :: mod_event_pusher_push_plugin:push_payload()) ->
jlib:iq().
push_notification_iq(Node, Form, PushPayload) ->
NotificationFields = [{<<"FORM_TYPE">>, ?PUSH_FORM_TYPE} | PushPayload ],

#iq{type = set, sub_el = [
#xmlel{name = <<"pubsub">>, attrs = [{<<"xmlns">>, ?NS_PUBSUB}], children = [
#xmlel{name = <<"publish">>, attrs = [{<<"node">>, Node}], children = [
#xmlel{name = <<"item">>, children = [
#xmlel{name = <<"notification">>,
attrs = [{<<"xmlns">>, ?NS_PUSH}],
children = [make_form(NotificationFields)]}
children = [make_form(?PUSH_FORM_TYPE, PushPayload)]}
]}
]}
] ++ maybe_publish_options(Form)}
] ++ maybe_publish_options(maps:to_list(Form))}
]}.

-spec make_form(mod_event_pusher_push:form()) -> exml:element().
make_form(Fields) ->
#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"submit">>}],
children = [make_form_field(Field) || Field <- Fields]}.

-spec make_form_field(mod_event_pusher_push:form_field()) -> exml:element().
make_form_field({Name, Value}) ->
#xmlel{name = <<"field">>,
attrs = [{<<"var">>, Name}],
children = [#xmlel{name = <<"value">>, children = [#xmlcdata{content = Value}]}]}.
-spec make_form(binary(), mod_event_pusher_push_plugin:push_payload()) -> exml:element().
make_form(FormType, FieldKVs) ->
Fields = [#{var => Name, values => [Value]} || {Name, Value} <- FieldKVs],
mongoose_data_forms:form(#{ns => FormType, type => <<"submit">>, fields => Fields}).

-spec maybe_publish_options(mod_event_pusher_push:form()) -> [exml:element()].
-spec maybe_publish_options([{binary(), binary()}]) -> [exml:element()].
maybe_publish_options([]) ->
[];
maybe_publish_options(FormFields) ->
Children = [make_form([{<<"FORM_TYPE">>, ?NS_PUBSUB_PUB_OPTIONS}] ++ FormFields)],
Children = [make_form(?NS_PUBSUB_PUB_OPTIONS, FormFields)],
[#xmlel{name = <<"publish-options">>, children = Children}].
11 changes: 5 additions & 6 deletions src/event_pusher/mod_event_pusher_push_rdbms.erl
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ init(_HostType, _Opts) ->
Node :: mod_event_pusher_push:pubsub_node(),
Form :: mod_event_pusher_push:form(),
Result :: ok | {error, term()}.
enable(HostType, User, PubSub, Node, Forms) ->
enable(HostType, User, PubSub, Node, Form) ->
ExtUser = jid:to_bare_binary(User),
ExtPubSub = jid:to_binary(PubSub),
ExtForms = encode_form(Forms),
ExtForms = encode_form(Form),
execute_delete(HostType, ExtUser, Node, ExtPubSub),
CreatedAt = os:system_time(microsecond),
case execute_insert(HostType, ExtUser, Node, ExtPubSub, ExtForms, CreatedAt) of
Expand Down Expand Up @@ -81,12 +81,11 @@ decode_row({NodeID, PubSubBin, FormJSON}) ->
NodeID,
decode_form(FormJSON)}.

encode_form(Forms) ->
jiffy:encode({Forms}).
encode_form(Form) ->
jiffy:encode(Form).

decode_form(FormJSON) ->
{Items} = jiffy:decode(FormJSON),
Items.
jiffy:decode(FormJSON, [return_maps]).

%% Prepared queries

Expand Down
4 changes: 2 additions & 2 deletions src/hooks/mongoose_hooks.erl
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ presence_probe_hook(HostType, Acc, From, To, Pid) ->
-spec push_notifications(HostType, Acc, NotificationForms, Options) -> Result when
HostType :: mongooseim:host_type(),
Acc :: ok | mongoose_acc:t(),
NotificationForms :: [#{atom() => binary()}],
Options :: #{atom() => binary()},
NotificationForms :: [#{binary() => binary()}],
Options :: #{binary() => binary()},
Result :: ok | {error, any()}.
push_notifications(HostType, Acc, NotificationForms, Options) ->
Params = #{options => Options, notification_forms => NotificationForms},
Expand Down
6 changes: 2 additions & 4 deletions src/http_upload/mod_http_upload.erl
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,8 @@ parse_request(Request) ->

-spec get_disco_info_form(MaxFileSizeBin :: binary()) -> exml:element().
get_disco_info_form(MaxFileSizeBin) ->
#xmlel{name = <<"x">>,
attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}],
children = [jlib:form_field({<<"FORM_TYPE">>, <<"hidden">>, ?NS_HTTP_UPLOAD_030}),
jlib:form_field({<<"max-file-size">>, MaxFileSizeBin})]}.
Fields = [#{var => <<"max-file-size">>, values => [MaxFileSizeBin]}],
mongoose_data_forms:form(#{type => <<"result">>, ns => ?NS_HTTP_UPLOAD_030, fields => Fields}).


-spec header_to_xmlel({Key :: binary(), Value :: binary()}) -> exml:element().
Expand Down
44 changes: 14 additions & 30 deletions src/inbox/mod_inbox.erl
Original file line number Diff line number Diff line change
Expand Up @@ -431,38 +431,24 @@ result_set([#{remote_jid := FirstBinJid, timestamp := FirstTS} | _] = List) ->
-spec build_inbox_form(mongooseim:host_type()) -> exml:element().
build_inbox_form(HostType) ->
AllBoxes = mod_inbox_utils:all_valid_boxes_for_query(HostType),
OrderOptions = [
{<<"Ascending by timestamp">>, <<"asc">>},
{<<"Descending by timestamp">>, <<"desc">>}
],
FormFields = [
jlib:form_field({<<"FORM_TYPE">>, <<"hidden">>, ?NS_ESL_INBOX}),
text_single_form_field(<<"start">>),
text_single_form_field(<<"end">>),
text_single_form_field(<<"hidden_read">>, <<"false">>),
mod_inbox_utils:list_single_form_field(<<"order">>, <<"desc">>, OrderOptions),
mod_inbox_utils:list_single_form_field(<<"box">>, <<"all">>, AllBoxes),
jlib:form_field({<<"archive">>, <<"boolean">>, <<"false">>})
],
#xmlel{name = <<"x">>,
attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}],
children = FormFields}.

-spec text_single_form_field(Var :: binary()) -> exml:element().
text_single_form_field(Var) ->
#xmlel{name = <<"field">>, attrs = [{<<"var">>, Var}, {<<"type">>, <<"text-single">>}]}.

-spec text_single_form_field(Var :: binary(), DefaultValue :: binary()) -> exml:element().
text_single_form_field(Var, DefaultValue) ->
#xmlel{name = <<"field">>,
attrs = [{<<"var">>, Var}, {<<"type">>, <<"text-single">>}, {<<"value">>, DefaultValue}]}.
OrderOptions = [{<<"Ascending by timestamp">>, <<"asc">>},
{<<"Descending by timestamp">>, <<"desc">>}],
Fields = [#{var => <<"start">>, type => <<"text-single">>},
#{var => <<"end">>, type => <<"text-single">>},
#{var => <<"hidden_read">>, type => <<"text-single">>, values => [<<"false">>]},
#{var => <<"order">>, type => <<"list-single">>, values => [<<"desc">>],
options => OrderOptions},
#{var => <<"box">>, type => <<"list-single">>, values => [<<"all">>],
options => AllBoxes},
#{var => <<"archive">>, type => <<"boolean">>, values => [<<"false">>]}],
mongoose_data_forms:form(#{ns => ?NS_ESL_INBOX, fields => Fields}).

%%%%%%%%%%%%%%%%%%%
%% iq-set
-spec query_to_params(mongooseim:host_type(), QueryEl :: exml:element()) ->
get_inbox_params() | {error, atom(), binary()}.
query_to_params(HostType, QueryEl) ->
Form = form_to_params(HostType, exml_query:subelement_with_ns(QueryEl, ?NS_XDATA)),
Form = form_to_params(HostType, mongoose_data_forms:find_form(QueryEl)),
Rsm = create_rsm(HostType, QueryEl),
build_params(Form, Rsm).

Expand Down Expand Up @@ -524,9 +510,9 @@ expand_limit(Max) ->
form_to_params(_, undefined) ->
#{ order => desc };
form_to_params(HostType, FormEl) ->
ParsedFields = jlib:parse_xdata_fields(exml_query:subelements(FormEl, <<"field">>)),
#{kvs := ParsedFields} = mongoose_data_forms:parse_form_fields(FormEl),
?LOG_DEBUG(#{what => inbox_parsed_form_fields, parsed_fields => ParsedFields}),
fields_to_params(HostType, ParsedFields, #{ order => desc }).
fields_to_params(HostType, maps:to_list(ParsedFields), #{ order => desc }).

-spec fields_to_params(mongooseim:host_type(),
[{Var :: binary(), Values :: [binary()]}], Acc :: get_inbox_params()) ->
Expand Down Expand Up @@ -593,8 +579,6 @@ fields_to_params(HostType, [{<<"box">>, [Value]} | RFields], Acc) ->
fields_to_params(HostType, RFields, Acc#{ box => Value })
end;

fields_to_params(HostType, [{<<"FORM_TYPE">>, _} | RFields], Acc) ->
fields_to_params(HostType, RFields, Acc);
fields_to_params(_, [{Invalid, [InvalidFieldVal]} | _], _) ->
?LOG_WARNING(#{what => inbox_invalid_form_field, reason => unknown_field,
field => Invalid, value => InvalidFieldVal}),
Expand Down
Loading

0 comments on commit 0bd9702

Please sign in to comment.