Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement XEP-0004: Data Forms in a separate module #4028

Merged
merged 16 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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