diff --git a/big_tests/tests/push_SUITE.erl b/big_tests/tests/push_SUITE.erl index ae81e38ff63..e7cd6bccb9c 100644 --- a/big_tests/tests/push_SUITE.erl +++ b/big_tests/tests/push_SUITE.erl @@ -36,7 +36,7 @@ all() -> ]. groups() -> - G = [ + [ {toggling, [parallel], [ enable_should_fail_with_missing_attributes, enable_should_fail_with_invalid_attributes, @@ -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() -> [ diff --git a/big_tests/tests/push_integration_SUITE.erl b/big_tests/tests/push_integration_SUITE.erl index ef21f28d995..b91108b0d02 100644 --- a/big_tests/tests/push_integration_SUITE.erl +++ b/big_tests/tests/push_integration_SUITE.erl @@ -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). diff --git a/include/mod_vcard.hrl b/include/mod_vcard.hrl index 6e7daee6b6a..eae8f7a4165 100644 --- a/include/mod_vcard.hrl +++ b/include/mod_vcard.hrl @@ -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]}). diff --git a/src/adhoc.erl b/src/adhoc.erl index ca40cf29629..bd227aee57c 100644 --- a/src/adhoc.erl +++ b/src/adhoc.erl @@ -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 -> @@ -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 node to use as response from an adhoc_response %% record, filling in values for language, node and session id from %% the request. diff --git a/src/event_pusher/mod_event_pusher_push.erl b/src/event_pusher/mod_event_pusher_push.erl index 84d6ba368db..85dc10141d4 100644 --- a/src/event_pusher/mod_event_pusher_push.erl +++ b/src/event_pusher/mod_event_pusher_push.erl @@ -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]). %%-------------------------------------------------------------------- @@ -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; @@ -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) -> diff --git a/src/event_pusher/mod_event_pusher_push_plugin.erl b/src/event_pusher/mod_event_pusher_push_plugin.erl index 57b0ad86857..4801098c8de 100644 --- a/src/event_pusher/mod_event_pusher_push_plugin.erl +++ b/src/event_pusher/mod_event_pusher_push_plugin.erl @@ -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 diff --git a/src/event_pusher/mod_event_pusher_push_plugin_defaults.erl b/src/event_pusher/mod_event_pusher_push_plugin_defaults.erl index 62e40818ce6..2f66f4d8d23 100644 --- a/src/event_pusher/mod_event_pusher_push_plugin_defaults.erl +++ b/src/event_pusher/mod_event_pusher_push_plugin_defaults.erl @@ -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); @@ -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}]. diff --git a/src/event_pusher/mod_event_pusher_push_rdbms.erl b/src/event_pusher/mod_event_pusher_push_rdbms.erl index 73f0a6840bc..cdde39a887b 100644 --- a/src/event_pusher/mod_event_pusher_push_rdbms.erl +++ b/src/event_pusher/mod_event_pusher_push_rdbms.erl @@ -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 @@ -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 diff --git a/src/hooks/mongoose_hooks.erl b/src/hooks/mongoose_hooks.erl index 64267902d70..04991ce1e40 100644 --- a/src/hooks/mongoose_hooks.erl +++ b/src/hooks/mongoose_hooks.erl @@ -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}, diff --git a/src/http_upload/mod_http_upload.erl b/src/http_upload/mod_http_upload.erl index 0baa4eba339..5d53a161114 100644 --- a/src/http_upload/mod_http_upload.erl +++ b/src/http_upload/mod_http_upload.erl @@ -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(). diff --git a/src/inbox/mod_inbox.erl b/src/inbox/mod_inbox.erl index cc1490a4adc..be60c04469d 100644 --- a/src/inbox/mod_inbox.erl +++ b/src/inbox/mod_inbox.erl @@ -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). @@ -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()) -> @@ -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}), diff --git a/src/inbox/mod_inbox_entries.erl b/src/inbox/mod_inbox_entries.erl index 74a2a16f936..b545f67329a 100644 --- a/src/inbox/mod_inbox_entries.erl +++ b/src/inbox/mod_inbox_entries.erl @@ -54,14 +54,12 @@ maybe_get_full_entry(SubEl) -> -spec build_inbox_entry_form(mongooseim:host_type()) -> exml:element(). build_inbox_entry_form(HostType) -> AllBoxes = mod_inbox_utils:all_valid_boxes_for_query(HostType), - #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = [jlib:form_field({<<"FORM_TYPE">>, <<"hidden">>, ?NS_ESL_INBOX_CONVERSATION}), - mod_inbox_utils:list_single_form_field(<<"box">>, <<"all">>, AllBoxes), - jlib:form_field({<<"archive">>, <<"boolean">>, <<"false">>}), - jlib:form_field({<<"read">>, <<"boolean">>, <<"false">>}), - jlib:form_field({<<"mute">>, <<"text-single">>, <<"0">>})]}. + Fields = + [#{var => <<"box">>, type => <<"list-single">>, values => [<<"all">>], options => AllBoxes}, + #{var => <<"archive">>, type => <<"boolean">>, values => [<<"false">>]}, + #{var => <<"read">>, type => <<"boolean">>, values => [<<"false">>]}, + #{var => <<"mute">>, type => <<"text-single">>, values => [<<"0">>]}], + mongoose_data_forms:form(#{ns => ?NS_ESL_INBOX_CONVERSATION, fields => Fields}). fetch_right_query(HostType, InboxEntryKey, only_properties) -> mod_inbox_backend:get_entry_properties(HostType, InboxEntryKey); diff --git a/src/inbox/mod_inbox_utils.erl b/src/inbox/mod_inbox_utils.erl index b07ed9b2e4b..246f1a3bf83 100644 --- a/src/inbox/mod_inbox_utils.erl +++ b/src/inbox/mod_inbox_utils.erl @@ -39,7 +39,6 @@ build_inbox_result_elements/2, build_entry_result_elements/2, all_valid_boxes_for_query/1, - list_single_form_field/3, calculate_ts_from/2 ]). @@ -281,33 +280,6 @@ build_delay_el(Timestamp) -> all_valid_boxes_for_query(HostType) -> [<<"all">> | gen_mod:get_module_opt(HostType, mod_inbox, boxes)]. --spec list_single_form_field(Var :: binary(), - Default :: binary(), - Options :: [ Option | {Label, Value}]) -> exml:element() when - Option :: binary(), Label :: binary(), Value :: binary(). -list_single_form_field(Var, Default, Options) -> - Value = form_field_value(Default), - #xmlel{ - name = <<"field">>, - attrs = [{<<"var">>, Var}, {<<"type">>, <<"list-single">>}], - children = [Value | [ form_field_option(Option) || Option <- Options ]] - }. - --spec form_field_option(Option | {Label, Value}) -> exml:element() when - Option :: binary(), Label :: binary(), Value :: binary(). -form_field_option({Label, Value}) -> - #xmlel{ - name = <<"option">>, - attrs = [{<<"label">>, Label}], - children = [form_field_value(Value)] - }; -form_field_option(Option) -> - form_field_option({Option, Option}). - --spec form_field_value(Value :: binary()) -> exml:element(). -form_field_value(Value) -> - #xmlel{name = <<"value">>, children = [#xmlcdata{content = Value}]}. - -spec calculate_ts_from(integer(), non_neg_integer()) -> integer(). calculate_ts_from(Now, Days) -> DaysInMicroSeconds = 86400000000 * Days, % 8.64e+10 microseconds in a day diff --git a/src/jlib.erl b/src/jlib.erl index 4a836cf69cd..7813c64d0fa 100644 --- a/src/jlib.erl +++ b/src/jlib.erl @@ -25,25 +25,19 @@ -module(jlib). -author('alexey@process-one.net'). --xep([{xep, 4}, {version, "2.13.1"}]). -xep([{xep, 59}, {version, "1.0"}]). --xep([{xep, 68}, {version, "1.2"}]). -xep([{xep, 86}, {version, "1.0"}]). -export([make_result_iq_reply/1, make_error_reply/2, make_error_reply/3, make_invitation/3, make_config_change_message/1, - make_voice_approval_form/3, - form_field/1, replace_from_to_attrs/3, replace_from_to/3, remove_attr/2, iq_query_info/1, iq_query_or_response_info/1, iq_to_xml/1, - parse_xdata_submit/1, - parse_xdata_fields/1, timestamp_to_xml/3, decode_base64/1, encode_base64/1, @@ -199,45 +193,6 @@ make_invitation(From, Password, Reason) -> attrs = [{<<"xmlns">>, ?NS_MUC_USER}], children = Elements2}]}. --spec form_field({binary(), binary(), binary()} - | {binary(), binary()} - | {binary(), binary(), binary(), binary()}) -> exml:element(). -form_field({Var, Type, Value, Label}) -> - Field = form_field({Var, Type, Value}), - Field#xmlel{attrs = [{<<"label">>, Label} | Field#xmlel.attrs]}; -form_field({Var, Type, Value}) -> - Field = form_field({Var, Value}), - Field#xmlel{attrs = [{<<"type">>, Type} | Field#xmlel.attrs]}; -form_field({Var, Value}) -> - #xmlel{name = <<"field">>, - attrs = [{<<"var">>, Var}], - children = [#xmlel{name = <<"value">>, children = [#xmlcdata{content = Value}]}]}. - - --spec make_voice_approval_form(From :: jid:simple_jid() | jid:jid(), - Nick :: binary(), Role :: binary()) -> exml:element(). -make_voice_approval_form(From, Nick, Role) -> - Fields = [{<<"FORM_TYPE">>, <<"hidden">>, ?NS_MUC_REQUEST}, - {<<"muc#role">>, <<"text-single">>, Role, <<"Request role">>}, - {<<"muc#jid">>, <<"jid-single">>, jid:to_binary(From), <<"User ID">>}, - {<<"muc#roomnick">>, <<"text-single">>, Nick, <<"Room Nickname">>}, - {<<"muc#request_allow">>, <<"boolean">>, <<"false">>, <<"Grant voice to this person?">>} - ], - #xmlel{name = <<"message">>, - children = [ - #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = [#xmlel{name = <<"title">>, - children = [#xmlcdata{content = <<"Voice request">>}]}, - #xmlel{name = <<"instructions">>, - children = [#xmlcdata{content = <<"To approve this request", - " for voice, select the "Grant voice to this person?" checkbox", - " and click OK. To skip this request, click the cancel button.">>}]} | - [form_field(El) || El <- Fields] - ]} - ]}. - - -spec replace_from_to_attrs(From :: binary(), To :: binary() | undefined, [binary_pair()]) -> [binary_pair()]. @@ -370,38 +325,6 @@ sub_el_to_els(#xmlel{}=E) -> [E]; %% for replies. sub_el_to_els(Es) when is_list(Es) -> Es. - --spec parse_xdata_submit(FormEl :: exml:element()) -> - invalid | [{VarName :: binary(), Values :: [binary()]}]. -parse_xdata_submit(FormEl) -> - case exml_query:attr(FormEl, <<"type">>) of - <<"submit">> -> parse_xdata_fields(FormEl#xmlel.children); - _ -> invalid - end. - --spec parse_xdata_fields(FormChildren :: [xmlcdata() | exml:element()]) -> - [{VarName :: binary(), Values :: [binary()]}]. -parse_xdata_fields([]) -> - []; -parse_xdata_fields([#xmlel{ name = <<"field">> } = FieldEl | REls]) -> - case exml_query:attr(FieldEl, <<"var">>) of - undefined -> - parse_xdata_fields(REls); - Var -> - [ {Var, parse_xdata_values(FieldEl#xmlel.children)} | parse_xdata_fields(REls) ] - end; -parse_xdata_fields([_ | REls]) -> - parse_xdata_fields(REls). - --spec parse_xdata_values(VarChildren :: [xmlcdata() | exml:element()]) -> - Values :: [binary()]. -parse_xdata_values([]) -> - []; -parse_xdata_values([#xmlel{name = <<"value">> } = ValueEl | REls]) -> - [exml_query:cdata(ValueEl) | parse_xdata_values(REls)]; -parse_xdata_values([_ | REls]) -> - parse_xdata_values(REls). - -spec rsm_decode(exml:element() | iq()) -> none | #rsm_in{}. rsm_decode(#iq{sub_el = SubEl})-> rsm_decode(SubEl); diff --git a/src/mam/mam_iq.erl b/src/mam/mam_iq.erl index 99312b5059c..c75961e33e7 100644 --- a/src/mam/mam_iq.erl +++ b/src/mam/mam_iq.erl @@ -7,8 +7,7 @@ -import(mod_mam_utils, [maybe_microseconds/1, - get_one_of_path/2, - form_field_value_s/2]). + get_one_of_path/2]). -include("jlib.hrl"). -include("mongoose_rsm.hrl"). @@ -103,39 +102,41 @@ elem_to_limit(QueryEl) -> ]). --spec form_to_start_microseconds(_) -> 'undefined' | non_neg_integer(). -form_to_start_microseconds(El) -> - maybe_microseconds(form_field_value_s(El, <<"start">>)). +-spec form_to_start_microseconds(mongoose_data_forms:kv_map()) -> 'undefined' | non_neg_integer(). +form_to_start_microseconds(#{<<"start">> := [V]}) -> + maybe_microseconds(V); +form_to_start_microseconds(#{}) -> + undefined. --spec form_to_end_microseconds(_) -> 'undefined' | non_neg_integer(). -form_to_end_microseconds(El) -> - maybe_microseconds(form_field_value_s(El, <<"end">>)). --spec form_to_with_jid(exml:element()) -> 'error' | 'undefined' | jid:jid(). -form_to_with_jid(El) -> - maybe_jid(form_field_value_s(El, <<"with">>)). +-spec form_to_end_microseconds(mongoose_data_forms:kv_map()) -> 'undefined' | non_neg_integer(). +form_to_end_microseconds(#{<<"end">> := [V]}) -> + maybe_microseconds(V); +form_to_end_microseconds(#{}) -> + undefined. --spec maybe_jid(binary()) -> 'error' | 'undefined' | jid:jid(). -maybe_jid(<<>>) -> - undefined; -maybe_jid(JID) when is_binary(JID) -> - jid:from_binary(JID). +-spec form_to_with_jid(mongoose_data_forms:kv_map()) -> 'error' | 'undefined' | jid:jid(). +form_to_with_jid(#{<<"with">> := [JID]}) -> + jid:from_binary(JID); +form_to_with_jid(#{}) -> + undefined. -spec form_to_lookup_params(jlib:iq(), integer(), integer(), undefined | module(), boolean()) -> lookup_params(). form_to_lookup_params(#iq{sub_el = QueryEl} = IQ, MaxResultLimit, DefaultResultLimit, Module, EnforceSimple) -> Params0 = common_lookup_params(QueryEl, MaxResultLimit, DefaultResultLimit), + KVs = query_to_map(QueryEl), Params = Params0#{ %% Filtering by date. %% Start :: integer() | undefined - start_ts => form_to_start_microseconds(QueryEl), - end_ts => form_to_end_microseconds(QueryEl), + start_ts => form_to_start_microseconds(KVs), + end_ts => form_to_end_microseconds(KVs), %% Filtering by contact. - with_jid => form_to_with_jid(QueryEl), + with_jid => form_to_with_jid(KVs), %% Filtering by text - search_text => mod_mam_utils:form_to_text(QueryEl), + search_text => mod_mam_utils:form_to_text(KVs), - borders => mod_mam_utils:form_borders_decode(QueryEl), + borders => mod_mam_utils:form_borders_decode(KVs), %% Whether or not the client query included a element, %% the server MAY simply return its limited results. %% So, disable 'policy-violation'. @@ -144,9 +145,19 @@ form_to_lookup_params(#iq{sub_el = QueryEl} = IQ, MaxResultLimit, DefaultResultL %% - true - do not count records (useful during pagination, when we already %% know how many messages we have from a previous query); %% - false - count messages (slow, according XEP-0313); - is_simple => maybe_enforce_simple(QueryEl, EnforceSimple)}, + is_simple => maybe_enforce_simple(KVs, EnforceSimple)}, maybe_add_extra_lookup_params(Module, Params, IQ). +-spec query_to_map(exml:element()) -> mongoose_data_forms:kv_map(). +query_to_map(QueryEl) -> + case mongoose_data_forms:find_form(QueryEl) of + undefined -> + #{}; + Form -> + #{kvs := KVs} = mongoose_data_forms:parse_form_fields(Form), + KVs + end. + -spec common_lookup_params(exml:element(), non_neg_integer(), non_neg_integer()) -> lookup_params(). common_lookup_params(QueryEl, MaxResultLimit, DefaultResultLimit) -> @@ -177,5 +188,5 @@ maybe_add_extra_lookup_params(Module, Params, IQ) -> maybe_enforce_simple(_, true) -> true; -maybe_enforce_simple(QueryEl, _) -> - mod_mam_utils:form_decode_optimizations(QueryEl). +maybe_enforce_simple(KVs, _) -> + mod_mam_utils:form_decode_optimizations(KVs). diff --git a/src/mam/mod_mam_utils.erl b/src/mam/mod_mam_utils.erl index 31e7494f3dd..35a9b4e5d81 100644 --- a/src/mam/mod_mam_utils.erl +++ b/src/mam/mod_mam_utils.erl @@ -48,8 +48,6 @@ %% Forms -export([ - form_field_value_s/2, - form_field_value/2, message_form/3, form_to_text/1 ]). @@ -98,7 +96,7 @@ mam_iq:lookup_params(), exml:element()) -> exml:element(). --ignore_xref([behaviour_info/1, append_arcid_elem/4, delete_arcid_elem/3, form_field_value/2, +-ignore_xref([behaviour_info/1, append_arcid_elem/4, delete_arcid_elem/3, get_one_of_path/3, is_arcid_elem_for/3, maybe_encode_compact_uuid/2, maybe_last/1, result_query/2, send_message/4, wrap_message/7, wrapper_id/0]). @@ -207,14 +205,6 @@ decode_compact_uuid(Id) -> mess_id_to_external_binary(MessID) when is_integer(MessID) -> integer_to_binary(MessID, 32). - --spec maybe_external_binary_to_mess_id(binary()) -> undefined | integer(). -maybe_external_binary_to_mess_id(<<>>) -> - undefined; -maybe_external_binary_to_mess_id(BExtMessID) -> - external_binary_to_mess_id(BExtMessID). - - %% @doc Decode a message ID received from the user. -spec external_binary_to_mess_id(binary()) -> integer(). external_binary_to_mess_id(BExtMessID) when is_binary(BExtMessID) -> @@ -621,12 +611,12 @@ binary_jid_to_lower(BinJid) when is_binary(BinJid) -> skip_bad_jids(MaybeJids) -> [Jid || Jid <- MaybeJids, is_binary(Jid)]. --spec form_borders_decode(exml:element()) -> 'undefined' | mod_mam:borders(). -form_borders_decode(QueryEl) -> - AfterID = form_field_mess_id(QueryEl, <<"after_id">>), - BeforeID = form_field_mess_id(QueryEl, <<"before_id">>), - FromID = form_field_mess_id(QueryEl, <<"from_id">>), - ToID = form_field_mess_id(QueryEl, <<"to_id">>), +-spec form_borders_decode(mongoose_data_forms:kv_map()) -> 'undefined' | mod_mam:borders(). +form_borders_decode(KVs) -> + AfterID = form_field_mess_id(KVs, <<"after_id">>), + BeforeID = form_field_mess_id(KVs, <<"before_id">>), + FromID = form_field_mess_id(KVs, <<"from_id">>), + ToID = form_field_mess_id(KVs, <<"to_id">>), borders(AfterID, BeforeID, FromID, ToID). @@ -645,14 +635,18 @@ borders(AfterID, BeforeID, FromID, ToID) -> to_id = ToID }. --spec form_field_mess_id(exml:element(), binary()) -> 'undefined' | integer(). -form_field_mess_id(QueryEl, Name) -> - BExtMessID = form_field_value_s(QueryEl, Name), - maybe_external_binary_to_mess_id(BExtMessID). +-spec form_field_mess_id(mongoose_data_forms:kv_map(), binary()) -> 'undefined' | integer(). +form_field_mess_id(KVs, Name) -> + case KVs of + #{Name := [BExtMessID]} -> external_binary_to_mess_id(BExtMessID); + #{} -> undefined + end. --spec form_decode_optimizations(exml:element()) -> boolean(). -form_decode_optimizations(QueryEl) -> - form_field_value(QueryEl, <<"simple">>) =:= <<"true">>. +-spec form_decode_optimizations(mongoose_data_forms:kv_map()) -> boolean(). +form_decode_optimizations(#{<<"simple">> := [<<"true">>]}) -> + true; +form_decode_optimizations(#{}) -> + false. is_mam_result_message(Packet = #xmlel{name = <<"message">>}) -> Ns = maybe_get_result_namespace(Packet), @@ -681,78 +675,29 @@ retraction_features(Module, HostType) -> %% ----------------------------------------------------------------------- %% Forms --spec form_field_value(exml:element(), binary()) -> undefined | binary(). -form_field_value(QueryEl, Name) -> - case exml_query:subelement(QueryEl, <<"x">>) of - undefined -> - undefined; - #xmlel{children = Fields} -> %% - case find_field(Fields, Name) of - undefined -> - undefined; - Field -> - field_to_value(Field) - end - end. - -form_field_value_s(QueryEl, Name) -> - undefined_to_empty(form_field_value(QueryEl, Name)). - -undefined_to_empty(undefined) -> <<>>; -undefined_to_empty(X) -> X. - -%% @doc Return first matched field --spec find_field(list(exml:element()), binary()) -> undefined | exml:element(). -find_field([#xmlel{ name = <<"field">> } = Field | Fields], Name) -> - case exml_query:attr(Field, <<"var">>) of - Name -> Field; - _ -> find_field(Fields, Name) - end; -find_field([_|Fields], Name) -> %% skip whitespaces - find_field(Fields, Name); -find_field([], _Name) -> - undefined. - --spec field_to_value(exml:element()) -> binary(). -field_to_value(FieldEl) -> - exml_query:path(FieldEl, [{element, <<"value">>}, cdata], <<>>). - -spec message_form(Mod :: mod_mam_pm | mod_mam_muc, HostType :: mongooseim:host_type(), binary()) -> exml:element(). message_form(Module, HostType, MamNs) -> - SubEl = #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, <<"jabber:x:data">>}, - {<<"type">>, <<"form">>}], - children = message_form_fields(Module, HostType, MamNs)}, - result_query(SubEl, MamNs). + Fields = message_form_fields(Module, HostType), + Form = mongoose_data_forms:form(#{ns => MamNs, fields => Fields}), + result_query(Form, MamNs). -message_form_fields(Mod, HostType, MamNs) -> +message_form_fields(Mod, HostType) -> TextSearch = case has_full_text_search(Mod, HostType) of - true -> [form_field(<<"text-single">>, <<"full-text-search">>)]; + true -> [#{type => <<"text-single">>, var => <<"full-text-search">>}]; false -> [] end, - [form_type_field(MamNs), - form_field(<<"jid-single">>, <<"with">>), - form_field(<<"text-single">>, <<"start">>), - form_field(<<"text-single">>, <<"end">>) | TextSearch]. - -form_type_field(MamNs) when is_binary(MamNs) -> - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, <<"hidden">>}, - {<<"var">>, <<"FORM_TYPE">>}], - children = [#xmlel{name = <<"value">>, - children = [#xmlcdata{content = MamNs}]}]}. - -form_field(Type, VarName) -> - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, Type}, - {<<"var">>, VarName}]}. + [#{type => <<"jid-single">>, var => <<"with">>}, + #{type => <<"text-single">>, var => <<"start">>}, + #{type => <<"text-single">>, var => <<"end">>} | TextSearch]. -spec form_to_text(_) -> 'undefined' | binary(). -form_to_text(El) -> - form_field_value(El, <<"full-text-search">>). +form_to_text(#{<<"full-text-search">> := [Text]}) -> + Text; +form_to_text(#{}) -> + undefined. %% ----------------------------------------------------------------------- %% Text search tokenization diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 670b6afc4ea..c48f48b666a 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -595,43 +595,18 @@ concat_identities(Els) -> Els)). concat_info(Els) -> - lists:sort(lists:flatmap(fun (#xmlel{name = <<"x">>, - attrs = Attrs, children = Fields}) -> - case {xml:get_attr_s(<<"xmlns">>, Attrs), - xml:get_attr_s(<<"type">>, Attrs)} - of - {?NS_XDATA, <<"result">>} -> - [concat_xdata_fields(Fields)]; - _ -> [] - end; - (_) -> [] + lists:sort(lists:flatmap(fun(El) -> + concat_xdata_fields(mongoose_data_forms:parse_form(El)) end, Els)). -concat_xdata_fields(Fields) -> - {FormType, Res} = - lists:foldl(fun(#xmlel{name = <<"field">>, children = Els} = FieldEl, - {FormType0, VarFields} = Acc) -> - case exml_query:attr(FieldEl, <<"var">>, <<"">>) of - <<"">> -> Acc; - <<"FORM_TYPE">> -> - {exml_query:path(FieldEl, [{element, <<"value">>}, cdata]), - VarFields}; - Var -> - NewField = [[Var, $<], extract_values_sorted_cdatas(Els)], - {FormType0, [NewField | VarFields]} - end; - (_, Acc) -> Acc - end, - {<<"">>, []}, Fields), - [FormType, $<, lists:sort(Res)]. - -extract_values_sorted_cdatas(Els) -> - lists:sort(lists:flatmap(fun extract_value_cdata/1, Els)). - -extract_value_cdata(#xmlel{name = <<"value">>} = ValueEl) -> - [[exml_query:cdata(ValueEl), $<]]; -extract_value_cdata(_) -> +concat_xdata_fields(#{type := <<"result">>, kvs := KVs, ns := NS}) -> + Res = maps:fold(fun(Var, Values, VarFields) -> + NewField = [[V, $<] || V <- [Var | lists:sort(Values)]], + [NewField | VarFields] + end, [], KVs), + [[NS, $<, lists:sort(Res)]]; +concat_xdata_fields(_) -> []. gb_trees_fold(F, Acc, Tree) -> diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 2cac489c0d7..49cdfa26395 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -1058,17 +1058,6 @@ get_room_pos({{NameHost, _}, _}, [{{NameHost, _}, _} | _], HeadPosition) -> get_room_pos(Desired, [_ | Rooms], HeadPosition) -> get_room_pos(Desired, Rooms, HeadPosition + 1). --spec xfield(Type :: binary(), Label :: binary(), Var :: binary(), - Val :: binary(), ejabberd:lang()) -> exml:element(). -xfield(Type, Label, Var, Val, Lang) -> - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = [#xmlel{name = <<"value">>, - children = [#xmlcdata{content = Val}]}]}. - - %% @doc Get a pseudo unique Room Name. The Room Name is generated as a hash of %% the requester JID, the local time and a random salt. %% @@ -1099,18 +1088,15 @@ iq_get_register_info(HostType, MucHost, From, Lang) -> ClientReqEl = #xmlel{name = <<"instructions">>, children = [#xmlcdata{content = ClientReqText}]}, EnterNicknameText = translate:translate(Lang, <<"Enter nickname you want to register">>), - EnterNicknameEl = #xmlel{name = <<"instructions">>, - children = [#xmlcdata{content = EnterNicknameText}]}, TitleText = <<(translate:translate(Lang, <<"Nickname Registration at ">>))/binary, MucHost/binary>>, - TitleEl = #xmlel{name = <<"title">>, children = [#xmlcdata{content = TitleText}]}, - Registered ++ - [ClientReqEl, - #xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_XDATA}], - children = [TitleEl, - EnterNicknameEl, - xfield(<<"text-single">>, <<"Nickname">>, <<"nick">>, Nick, Lang)]}]. - + NickField = #{type => <<"text-single">>, + label => translate:translate(Lang, <<"Nickname">>), + var => <<"nick">>, + values => [Nick]}, + Registered ++ [ClientReqEl, mongoose_data_forms:form(#{title => TitleText, + instructions => EnterNicknameText, + fields => [NickField]})]. -spec iq_set_register_info(host_type(), jid:server(), jid:simple_jid() | jid:jid(), nick(), ejabberd:lang()) @@ -1151,43 +1137,31 @@ iq_set_unregister_info(HostType, MucHost, From, _Lang) -> jid:jid(), exml:element(), ejabberd:lang()) -> {'error', exml:element()} | {'result', []}. process_iq_register_set(HostType, MucHost, From, SubEl, Lang) -> - #xmlel{children = Els} = SubEl, case xml:get_subtag(SubEl, <<"remove">>) of false -> - case xml:remove_cdata(Els) of - [#xmlel{name = <<"x">>} = XEl] -> - process_register(xml:get_tag_attr_s(<<"xmlns">>, XEl), - xml:get_tag_attr_s(<<"type">>, XEl), - HostType, MucHost, From, Lang, XEl); + case mongoose_data_forms:find_and_parse_form(SubEl) of + #{type := <<"cancel">>} -> + {result, []}; + #{type := <<"submit">>, kvs := KVs} -> + process_register(HostType, MucHost, From, Lang, KVs); + {error, Msg} -> + {error, mongoose_xmpp_errors:bad_request(Lang, Msg)}; _ -> - {error, mongoose_xmpp_errors:bad_request()} + {error, mongoose_xmpp_errors:bad_request(Lang, <<"Invalid form type">>)} end; _ -> iq_set_unregister_info(HostType, MucHost, From, Lang) end. --spec process_register(XMLNS :: binary(), Type :: binary(), - HostType :: host_type(), MucHost :: jid:server(), - From :: jid:jid(), Lang :: ejabberd:lang(), XEl :: exml:element()) -> +-spec process_register(HostType :: host_type(), MucHost :: jid:server(), + From :: jid:jid(), Lang :: ejabberd:lang(), + KVs :: mongoose_data_forms:kv_map()) -> {error, exml:element()} | {result, []}. -process_register(?NS_XDATA, <<"cancel">>, _HostType, _Host, _From, _Lang, _XEl) -> - {result, []}; -process_register(?NS_XDATA, <<"submit">>, HostType, MucHost, From, Lang, XEl) -> - XData = jlib:parse_xdata_submit(XEl), - case XData of - invalid -> - {error, mongoose_xmpp_errors:bad_request()}; - _ -> - case lists:keysearch(<<"nick">>, 1, XData) of - {value, {_, [Nick]}} when Nick /= <<>> -> - iq_set_register_info(HostType, MucHost, From, Nick, Lang); - _ -> - ErrText = <<"You must fill in field \"Nickname\" in the form">>, - {error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)} - end - end; -process_register(_, _, _HostType, _MucHost, _From, _Lang, _XEl) -> - {error, mongoose_xmpp_errors:bad_request()}. +process_register(HostType, MucHost, From, Lang, #{<<"nick">> := [Nick]}) -> + iq_set_register_info(HostType, MucHost, From, Nick, Lang); +process_register(_HostType, _MucHost, _From, Lang, #{}) -> + ErrText = <<"You must fill in field \"Nickname\" in the form">>, + {error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)}. -spec iq_get_vcard(ejabberd:lang()) -> [exml:element(), ...]. iq_get_vcard(Lang) -> diff --git a/src/mod_muc_api.erl b/src/mod_muc_api.erl index 2cd59ffa765..ec5b334ba5f 100644 --- a/src/mod_muc_api.erl +++ b/src/mod_muc_api.erl @@ -535,9 +535,7 @@ declination(Sender, Recipient) -> iq(<<"set">>, Sender, Recipient, [data_submission()]). data_submission() -> - query(?NS_MUC_OWNER, [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"submit">>}]}]). + query(?NS_MUC_OWNER, [mongoose_data_forms:form(#{type => <<"submit">>})]). address_attributes(Sender, Recipient) -> [{<<"from">>, jid:to_binary(Sender)}, diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 753e9c54d67..a9b6c7a0921 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -405,15 +405,16 @@ initial_state({route, From, ToNick, _Acc, % TOODOO process_presence(From, ToNick, Presence, StateData) end. - -spec is_query_allowed(exml:element()) -> boolean(). -is_query_allowed(Query) -> - X = xml:get_subtag(Query, <<"x">>), - xml:get_subtag(Query, <<"destroy">>) =/= false orelse - (X =/= false andalso xml:get_tag_attr_s(<<"xmlns">>, X)== ?NS_XDATA andalso - (xml:get_tag_attr_s(<<"type">>, X) == <<"submit">> orelse - xml:get_tag_attr_s(<<"type">>, X)== <<"cancel">>)). - +is_query_allowed(#xmlel{children = Els}) -> + case xml:remove_cdata(Els) of + [#xmlel{name = <<"destroy">>}] -> + true; + [El] -> + mongoose_data_forms:is_form(El, [<<"submit">>, <<"cancel">>]); + _ -> + false + end. -spec locked_state_process_owner_iq(jid:jid(), exml:element(), ejabberd:lang(), 'error' | 'get' | 'invalid' | 'result', _) @@ -3178,30 +3179,31 @@ process_iq_owner(From, Type, Lang, SubEl, StateData, StateName) -> process_authorized_iq_owner(From, set, Lang, SubEl, StateData, StateName) -> #xmlel{children = Els} = SubEl, case xml:remove_cdata(Els) of - [#xmlel{name = <<"x">>} = XEl] -> - case {xml:get_tag_attr_s(<<"xmlns">>, XEl), - xml:get_tag_attr_s(<<"type">>, XEl), - StateName} of - {?NS_XDATA, <<"cancel">>, locked_state} -> + [#xmlel{name = <<"destroy">>} = SubEl1] -> + ?LOG_INFO(ls(#{what => muc_room_destroy, + text => <<"Destroyed MUC room by the owner">>, + from_jid => jid:to_binary(From)}, StateData)), + add_to_log(room_existence, destroyed, StateData), + destroy_room(SubEl1, StateData); + [XEl] -> + case {mongoose_data_forms:parse_form(XEl), StateName} of + {#{type := <<"cancel">>}, locked_state} -> ?LOG_INFO(ls(#{what => muc_cancel_locked, - text => <<"Received cancel before the room was configured - destroy room">>, + text => <<"Received cancel before the room was configured " + "- destroy room">>, from_jid => jid:to_binary(From)}, StateData)), add_to_log(room_existence, destroyed, StateData), destroy_room(XEl, StateData); - {?NS_XDATA, <<"cancel">>, normal_state} -> + {#{type := <<"cancel">>}, normal_state} -> %% received cancel when room was configured - continue without changes {result, [], StateData}; - {?NS_XDATA, <<"submit">>, _} -> - process_authorized_submit_owner(From, XEl, StateData); + {#{type := <<"submit">>, kvs := KVs}, _} -> + process_authorized_submit_owner(From, maps:to_list(KVs), StateData); + {{error, Msg}, _} -> + {error, mongoose_xmpp_errors:bad_request(Lang, Msg)}; _ -> - {error, mongoose_xmpp_errors:bad_request()} + {error, mongoose_xmpp_errors:bad_request(Lang, <<"Invalid form contents">>)} end; - [#xmlel{name = <<"destroy">>} = SubEl1] -> - ?LOG_INFO(ls(#{what => muc_room_destroy, - text => <<"Destroyed MUC room by the owner">>, - from_jid => jid:to_binary(From)}, StateData)), - add_to_log(room_existence, destroyed, StateData), - destroy_room(SubEl1, StateData); Items -> process_admin_items_set(From, Items, Lang, StateData) end; @@ -3221,26 +3223,26 @@ process_authorized_iq_owner(From, get, Lang, SubEl, StateData, _StateName) -> end end. --spec process_authorized_submit_owner(From ::jid:jid(), XEl :: exml:element(), StateData :: state()) -> +-spec process_authorized_submit_owner(From ::jid:jid(), [{binary(), [binary()]}], + StateData :: state()) -> {error, exml:element()} | {result, [exml:element() | jlib:xmlcdata()], state() | stop}. -process_authorized_submit_owner(_From, #xmlel{ children = [] } = _XEl, StateData) -> +process_authorized_submit_owner(_From, [], StateData) -> %confirm an instant room save_persistent_room_state(StateData), {result, [], StateData}; -process_authorized_submit_owner(From, XEl, StateData) -> +process_authorized_submit_owner(From, XData, StateData) -> %attempt to configure - case is_allowed_log_change(XEl, StateData, From) - andalso is_allowed_persistent_change(XEl, StateData, From) - andalso is_allowed_room_name_desc_limits(XEl, StateData) - andalso is_password_settings_correct(XEl, StateData) of - true -> set_config(XEl, StateData); + case is_allowed_log_change(XData, StateData, From) + andalso is_allowed_persistent_change(XData, StateData, From) + andalso is_allowed_room_name_desc_limits(XData, StateData) + andalso is_password_settings_correct(XData, StateData) of + true -> set_config(XData, StateData); false -> {error, mongoose_xmpp_errors:not_acceptable(<<"en">>, <<"not allowed to configure">>)} end. --spec is_allowed_log_change(exml:element(), state(), jid:jid()) -> boolean(). -is_allowed_log_change(XEl, StateData, From) -> - case lists:keymember(<<"muc#roomconfig_enablelogging">>, 1, - jlib:parse_xdata_submit(XEl)) of +-spec is_allowed_log_change([{binary(), [binary()]}], state(), jid:jid()) -> boolean(). +is_allowed_log_change(XData, StateData, From) -> + case lists:keymember(<<"muc#roomconfig_enablelogging">>, 1, XData) of false -> true; true -> @@ -3250,10 +3252,9 @@ is_allowed_log_change(XEl, StateData, From) -> end. --spec is_allowed_persistent_change(exml:element(), state(), jid:jid()) -> boolean(). -is_allowed_persistent_change(XEl, StateData, From) -> - case lists:keymember(<<"muc#roomconfig_persistentroom">>, 1, - jlib:parse_xdata_submit(XEl)) of +-spec is_allowed_persistent_change([{binary(), [binary()]}], state(), jid:jid()) -> boolean(). +is_allowed_persistent_change(XData, StateData, From) -> + case lists:keymember(<<"muc#roomconfig_persistentroom">>, 1, XData) of false -> true; true -> @@ -3265,19 +3266,17 @@ is_allowed_persistent_change(XEl, StateData, From) -> %% @doc Check if the Room Name and Room Description defined in the Data Form %% are conformant to the configured limits --spec is_allowed_room_name_desc_limits(exml:element(), state()) -> boolean(). -is_allowed_room_name_desc_limits(XEl, StateData) -> +-spec is_allowed_room_name_desc_limits([{binary(), [binary()]}], state()) -> boolean(). +is_allowed_room_name_desc_limits(XData, StateData) -> IsNameAccepted = - case lists:keysearch(<<"muc#roomconfig_roomname">>, 1, - jlib:parse_xdata_submit(XEl)) of + case lists:keysearch(<<"muc#roomconfig_roomname">>, 1, XData) of {value, {_, [N]}} -> byte_size(N) =< get_opt(StateData, max_room_name); _ -> true end, IsDescAccepted = - case lists:keysearch(<<"muc#roomconfig_roomdesc">>, 1, - jlib:parse_xdata_submit(XEl)) of + case lists:keysearch(<<"muc#roomconfig_roomdesc">>, 1, XData) of {value, {_, [D]}} -> byte_size(D) =< get_opt(StateData, max_room_desc); _ -> @@ -3287,40 +3286,38 @@ is_allowed_room_name_desc_limits(XEl, StateData) -> %% @doc Return false if: %% `<<"the password for a password-protected room is blank">>' --spec is_password_settings_correct(exml:element(), state()) -> boolean(). -is_password_settings_correct(XEl, StateData) -> +-spec is_password_settings_correct([{binary(), [binary()]}], state()) -> boolean(). +is_password_settings_correct(KVs, StateData) -> Config = StateData#state.config, OldProtected = Config#config.password_protected, OldPassword = Config#config.password, NewProtected = - case lists:keysearch(<<"muc#roomconfig_passwordprotectedroom">>, 1, - jlib:parse_xdata_submit(XEl)) of - {value, {_, [<<"1">>]}} -> - true; - {value, {_, [<<"0">>]}} -> - false; - _ -> - undefined - end, + case lists:keysearch(<<"muc#roomconfig_passwordprotectedroom">>, 1, KVs) of + {value, {_, [<<"1">>]}} -> + true; + {value, {_, [<<"0">>]}} -> + false; + _ -> + undefined + end, NewPassword = - case lists:keysearch(<<"muc#roomconfig_roomsecret">>, 1, - jlib:parse_xdata_submit(XEl)) of - {value, {_, [P]}} -> - P; - _ -> - undefined - end, + case lists:keysearch(<<"muc#roomconfig_roomsecret">>, 1, KVs) of + {value, {_, [P]}} -> + P; + _ -> + undefined + end, case {OldProtected, NewProtected, OldPassword, NewPassword} of - {true, undefined, <<>>, undefined} -> - false; - {true, undefined, _, <<>>} -> - false; - {_, true, <<>>, undefined} -> - false; - {_, true, _, <<>>} -> - false; - _ -> - true + {true, undefined, <<>>, undefined} -> + false; + {true, undefined, _, <<>>} -> + false; + {_, true, <<>>, undefined} -> + false; + {_, true, _, <<>>} -> + false; + _ -> + true end. -spec get_default_room_maxusers(state()) -> any(). @@ -3333,18 +3330,10 @@ get_default_room_maxusers(RoomState) -> get_config(Lang, StateData, From) -> AccessPersistent = access_persistent(StateData), Config = StateData#state.config, - TitleTxt = translate:translate(Lang, <<"Configuration of room ">>), - Res = - [#xmlel{name = <<"title">>, - children = [#xmlcdata{content = <>}]}, - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, <<"hidden">>}, - {<<"var">>, <<"FORM_TYPE">>}], - children = [#xmlel{name = <<"value">>, - children = [#xmlcdata{content = ?NS_MUC_CONFIG}]}]}, - stringxfield(<<"Room title">>, + Title = <>, + Fields = + [stringxfield(<<"Room title">>, <<"muc#roomconfig_roomname">>, Config#config.title, Lang), stringxfield(<<"Room description">>, @@ -3420,50 +3409,17 @@ get_config(Lang, StateData, From) -> InstructionsTxt = translate:translate( Lang, <<"You need an x:data capable client to configure room">>), {result, [#xmlel{name = <<"instructions">>, children = [#xmlcdata{content = InstructionsTxt}]}, - #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = Res}], + mongoose_data_forms:form(#{title => Title, ns => ?NS_MUC_CONFIG, fields => Fields})], StateData}. --spec getmemberlist_field(Lang :: ejabberd:lang()) -> exml:element(). +-spec getmemberlist_field(Lang :: ejabberd:lang()) -> mongoose_data_forms:field(). getmemberlist_field(Lang) -> LabelTxt = translate:translate( Lang, <<"Roles and affiliations that may retrieve member list">>), - OptModerator = #xmlel{name = <<"option">>, - attrs = [{<<"label">>, translate:translate(Lang, <<"moderator">>)}], - children = [ - #xmlel{name = <<"value">>, - children = [#xmlcdata{content = <<"moderator">>}]} - ]}, - OptParticipant = #xmlel{name = <<"option">>, - attrs = [{<<"label">>, translate:translate(Lang, <<"participant">>)}], - children = [ - #xmlel{name = <<"value">>, - children = [#xmlcdata{content = <<"participant">>}]} - ]}, - OptVisitor = #xmlel{name = <<"option">>, - attrs = [{<<"label">>, translate:translate(Lang, <<"visitor">>)}], - children = [ - #xmlel{name = <<"value">>, - children = [#xmlcdata{content = <<"visitor">>}]} - ]}, - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, <<"list-multi">>}, - {<<"label">>, LabelTxt}, - {<<"var">>, <<"muc#roomconfig_getmemberlist">>}], - children = [ - #xmlel{name = <<"value">>, - children = [#xmlcdata{content = <<"moderator">>}]}, - #xmlel{name = <<"value">>, - children = [#xmlcdata{content = <<"participant">>}]}, - #xmlel{name = <<"value">>, - children = [#xmlcdata{content = <<"visitor">>}]}, - OptModerator, - OptParticipant, - OptVisitor - ] - }. + Values = [<<"moderator">>, <<"participant">>, <<"visitor">>], + Options = [{translate:translate(Lang, Opt), Opt} || Opt <- Values], + #{type => <<"list-multi">>, label => LabelTxt, + var => <<"muc#roomconfig_getmemberlist">>, values => Values, options => Options}. maxusers_field(Lang, StateData) -> ServiceMaxUsers = get_service_max_users(StateData), @@ -3474,76 +3430,45 @@ maxusers_field(Lang, StateData) -> {N, integer_to_binary(N)}; _ -> {0, <<"none">>} end, - - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, <<"list-single">>}, - {<<"label">>, translate:translate(Lang, <<"Maximum Number of Occupants">>)}, - {<<"var">>, <<"muc#roomconfig_maxusers">>}], - children = [#xmlel{name = <<"value">>, - children = [#xmlcdata{content = MaxUsersRoomString}]}] ++ - if - is_integer(ServiceMaxUsers) -> []; - true -> - [#xmlel{name = <<"option">>, - attrs = [{<<"label">>, translate:translate(Lang, <<"No limit">>)}], - children = [#xmlel{name = <<"value">>, - children = [#xmlcdata{content = <<"none">>}]}]}] - end ++ - [#xmlel{name = <<"option">>, - attrs = [{<<"label">>, integer_to_binary(N)}], - children = [#xmlel{name = <<"value">>, - children = [#xmlcdata{content = integer_to_binary(N)}]}]} || + LabelTxt = translate:translate(Lang, <<"Maximum Number of Occupants">>), + Options = if + is_integer(ServiceMaxUsers) -> []; + true -> {translate:translate(Lang, <<"No limit">>), <<"none">>} + end ++ + [integer_to_binary(N) || N <- lists:usort([ServiceMaxUsers, DefaultRoomMaxUsers, MaxUsersRoomInteger | - ?MAX_USERS_DEFAULT_LIST]), N =< ServiceMaxUsers]}. + ?MAX_USERS_DEFAULT_LIST]), N =< ServiceMaxUsers], + #{type => <<"list-single">>, label => LabelTxt, + var => <<"muc#roomconfig_maxusers">>, values => [MaxUsersRoomString], options => Options}. --spec whois_field(Lang :: ejabberd:lang(), Config :: config()) -> exml:element(). +-spec whois_field(Lang :: ejabberd:lang(), Config :: config()) -> mongoose_data_forms:field(). whois_field(Lang, Config) -> - OptModerators = #xmlel{name = <<"option">>, - attrs = [{<<"label">>, - translate:translate(Lang, <<"moderators only">>)}], - children = [#xmlel{name = <<"value">>, - children = [#xmlcdata{content = <<"moderators">>}]}]}, - OptAnyone = #xmlel{name = <<"option">>, - attrs = [{<<"label">>, translate:translate(Lang, <<"anyone">>)}], - children = [#xmlel{name = <<"value">>, - children = [#xmlcdata{content = <<"anyone">>}]}]}, - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, <<"list-single">>}, - {<<"label">>, translate:translate(Lang, <<"Present real Jabber IDs to">>)}, - {<<"var">>, <<"muc#roomconfig_whois">>}], - children = [#xmlel{name = <<"value">>, - children = [#xmlcdata{content = if Config#config.anonymous -> - <<"moderators">>; - true -> - <<"anyone">> - end}]}, - OptModerators, - OptAnyone]}. - --spec set_config(exml:element(), state()) -> any(). -set_config(XEl, StateData) -> - XData = jlib:parse_xdata_submit(XEl), - case XData of - invalid -> - {error, mongoose_xmpp_errors:bad_request()}; - _ -> - case set_xoption(XData, StateData#state.config) of - #config{} = Config -> - Res = change_config(Config, StateData), - {result, _, NSD} = Res, - PrevLogging = (StateData#state.config)#config.logging, - NewLogging = Config#config.logging, - PrevAnon = (StateData#state.config)#config.anonymous, - NewAnon = Config#config.anonymous, - Type = notify_config_change_and_get_type(PrevLogging, NewLogging, - PrevAnon, NewAnon, StateData), + Value = if Config#config.anonymous -> <<"moderators">>; + true -> <<"anyone">> + end, + Options = [{translate:translate(Lang, <<"moderators only">>), <<"moderators">>}, + {translate:translate(Lang, <<"anyone">>), <<"anyone">>}], + #{type => <<"list-single">>, label => translate:translate(Lang, <<"moderators only">>), + var => <<"muc#roomconfig_whois">>, values => [Value], options => Options}. + +-spec set_config([{binary(), [binary()]}], state()) -> any(). +set_config(XData, StateData) -> + case set_xoption(XData, StateData#state.config) of + #config{} = Config -> + Res = change_config(Config, StateData), + {result, _, NSD} = Res, + PrevLogging = (StateData#state.config)#config.logging, + NewLogging = Config#config.logging, + PrevAnon = (StateData#state.config)#config.anonymous, + NewAnon = Config#config.anonymous, + Type = notify_config_change_and_get_type(PrevLogging, NewLogging, + PrevAnon, NewAnon, StateData), Users = [{U#user.jid, U#user.nick, U#user.role} || - {_, U} <- maps:to_list(StateData#state.users)], - add_to_log(Type, Users, NSD), - Res; + {_, U} <- maps:to_list(StateData#state.users)], + add_to_log(Type, Users, NSD), + Res; Err -> - Err - end + Err end. -spec notify_config_change_and_get_type(PrevLogging :: boolean(), NewLogging :: boolean(), @@ -3651,9 +3576,6 @@ set_xoption([{<<"muc#roomconfig_getmemberlist">>, Val} | Opts], Config) -> end; set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(logging, Val); -set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config) -> - %% Ignore our FORM_TYPE - set_xoption(Opts, Config); set_xoption([_ | _Opts], _Config) -> {error, mongoose_xmpp_errors:bad_request()}. @@ -3986,42 +3908,32 @@ disco_item(User=#user{nick=Nick}, RoomJID) -> | {role, BRole :: binary(), RoomNick :: mod_muc:nick()} | {error, any()} | ok. -check_voice_approval(From, [#xmlel{name = <<"x">>, - children = Items}], _Lang, StateData) -> - BRole = get_field(<<"muc#role">>, Items), - case Items of - [_Form, _Role] -> - case catch binary_to_role(BRole) of - {'EXIT', _} -> {error, mongoose_xmpp_errors:bad_request()}; - _ -> {form, BRole} +check_voice_approval(From, [XEl], Lang, StateData) -> + case mongoose_data_forms:parse_form(XEl) of + #{kvs := #{<<"muc#role">> := [BRole]} = KVs} -> + case {get_role(From, StateData) =:= moderator, + maps:find(<<"muc#request_allow">>, KVs), + maps:find(<<"muc#roomnick">>, KVs)} of + {_, error, error} -> + case catch binary_to_role(BRole) of + {'EXIT', _} -> {error, mongoose_xmpp_errors:bad_request()}; + _ -> {form, BRole} + end; + {false, _, _} -> + {error, mongoose_xmpp_errors:not_allowed()}; + {true, {ok, [<<"true">>]}, error} -> + {error, mongoose_xmpp_errors:bad_request()}; + {true, {ok, [<<"true">>]}, {ok, [RoomNick]}} -> + {role, BRole, RoomNick}; + {true, _, _} -> + ok end; + {error, Msg} -> + {error, mongoose_xmpp_errors:bad_request(Lang, Msg)}; _ -> - case {get_role(From, StateData), - get_field(<<"muc#request_allow">>, Items), - get_field(<<"muc#roomnick">>, Items)} of - {moderator, <<"true">>, false} -> {error, mongoose_xmpp_errors:bad_request()}; - {moderator, <<"true">>, RoomNick} -> {role, BRole, RoomNick}; - {moderator, _, _} -> ok; - _ -> {error, mongoose_xmpp_errors:not_allowed()} - end + {error, mongoose_xmpp_errors:bad_request(Lang, <<"MUC Role was not provided">>)} end. - --spec get_field(binary(), [jlib:xmlcdata() | exml:element()]) -> any(). -get_field(Var, [#xmlel{name = <<"field">>, attrs = Attrs} = Item|Items]) - when is_binary(Var) -> - case xml:get_attr(<<"var">>, Attrs) of - {value, Var} -> - case xml:get_path_s(Item, [{elem, <<"value">>}, cdata]) of - <<>> -> get_field(Var, Items); - Value -> Value - end; - _ -> - get_field(Var, Items) - end; -get_field(_Var, []) -> - false. - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Invitation support @@ -4421,7 +4333,7 @@ route_voice_approval({error, ErrType}, From, Packet, _Lang, StateData) -> StateData; route_voice_approval({form, RoleName}, From, _Packet, _Lang, StateData) -> {Nick, _} = get_participant_data(From, StateData), - ApprovalForm = jlib:make_voice_approval_form(From, Nick, RoleName), + ApprovalForm = make_voice_approval_form(From, Nick, RoleName), F = fun({_, Info}) -> ejabberd_router:route(StateData#state.jid, Info#user.jid, ApprovalForm) @@ -4636,18 +4548,30 @@ route_nick_iq(#routed_nick_iq{packet = Packet, lang = Lang, nick = ToNick, decode_reason(Elem) -> xml:get_path_s(Elem, [{elem, <<"reason">>}, cdata]). - --spec xfield(binary(), any(), binary(), binary(), ejabberd:lang()) -> exml:element(). +-spec make_voice_approval_form(From :: jid:simple_jid() | jid:jid(), + Nick :: binary(), Role :: binary()) -> exml:element(). +make_voice_approval_form(From, Nick, Role) -> + Title = <<"Voice request">>, + Instructions = <<"To approve this request" + " for voice, select the "Grant voice to this person?" checkbox" + " and click OK. To skip this request, click the cancel button.">>, + Fields = [#{var => <<"muc#role">>, type => <<"text-single">>, + label => <<"Request role">>, values => [Role]}, + #{var => <<"muc#jid">>, type => <<"jid-single">>, + label => <<"User ID">>, values => [jid:to_binary(From)]}, + #{var => <<"muc#roomnick">>, type => <<"text-single">>, + label => <<"Room Nickname">>, values => [Nick]}, + #{var => <<"muc#request_allow">>, type => <<"boolean">>, + label => <<"Grant voice to this person?">>, values => [<<"false">>]}], + Form = mongoose_data_forms:form(#{title => Title, instructions => Instructions, + ns => ?NS_MUC_REQUEST, fields => Fields}), + #xmlel{name = <<"message">>, children = [Form]}. + +-spec xfield(binary(), any(), binary(), binary(), ejabberd:lang()) -> mongoose_data_forms:field(). xfield(Type, Label, Var, Val, Lang) -> - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = [#xmlel{name = <<"value">>, - children = [#xmlcdata{content = Val}]}]}. - + #{type => Type, label => translate:translate(Lang, Label), var => Var, values => [Val]}. --spec boolxfield(any(), binary(), any(), ejabberd:lang()) -> exml:element(). +-spec boolxfield(any(), binary(), any(), ejabberd:lang()) -> mongoose_data_forms:field(). boolxfield(Label, Var, Val, Lang) -> xfield(<<"boolean">>, Label, Var, case Val of diff --git a/src/mongoose_data_forms.erl b/src/mongoose_data_forms.erl new file mode 100644 index 00000000000..db2b58ed6c8 --- /dev/null +++ b/src/mongoose_data_forms.erl @@ -0,0 +1,162 @@ +-module(mongoose_data_forms). +-xep([{xep, 4}, {version, "2.13.1"}]). +-xep([{xep, 68}, {version, "1.3.0"}]). + +%% Form processing +-export([find_and_parse_form/1, find_form/1, find_form/2, + parse_form/1, parse_form_fields/1, + is_form/1, is_form/2]). + +%% Form construction +-export([form/1]). + +-include_lib("exml/include/exml.hrl"). +-include("mongoose_ns.hrl"). + +-type form() :: #{type => binary(), title => binary(), instructions => binary(), ns => binary(), + fields => [field()], reported => [field()], items => [[field()]]}. +-type field() :: #{var => binary(), type => binary(), label => binary(), + values => [binary()], options => [option()]}. +-type option() :: binary() | {binary(), binary()}. + +-type parsed_form() :: #{type => binary(), ns => binary(), kvs := kv_map()}. +-type kv_map() :: #{binary() => [binary()]}. + +-export_type([form/0, field/0, option/0, kv_map/0]). + +-ignore_xref([is_form/1]). % exported for consistency, might be used later + +%% Form processing + +%% @doc Find a form in subelements, and then parse its fields +-spec find_and_parse_form(exml:element()) -> parsed_form() | {error, binary()}. +find_and_parse_form(Parent) -> + case find_form(Parent) of + undefined -> + {error, <<"Form not found">>}; + Form -> + parse_form_fields(Form) + end. + +-spec find_form(exml:element()) -> exml:element() | undefined. +find_form(Parent) -> + exml_query:subelement_with_name_and_ns(Parent, <<"x">>, ?NS_XDATA). + +-spec find_form(exml:element(), Default) -> exml:element() | Default. +find_form(Parent, Default) -> + exml_query:subelement_with_name_and_ns(Parent, <<"x">>, ?NS_XDATA, Default). + +%% @doc Check if the element is a form, and then parse its fields +-spec parse_form(exml:element()) -> parsed_form() | {error, binary()}. +parse_form(Elem) -> + case is_form(Elem) of + true -> + parse_form_fields(Elem); + false -> + {error, <<"Invalid form element">>} + end. + +%% @doc Parse the form fields without checking that it is a form element +-spec parse_form_fields(exml:element()) -> parsed_form(). +parse_form_fields(Elem) -> + M = case form_type(Elem) of + undefined -> #{}; + Type -> #{type => Type} + end, + KVs = form_fields_to_kvs(Elem#xmlel.children), + case maps:take(<<"FORM_TYPE">>, KVs) of + {[NS], FKVs} -> + M#{ns => NS, kvs => FKVs}; + _ -> + % Either zero or more than one value of FORM_TYPE. + % According to XEP-0004 the form is still valid. + M#{kvs => KVs} + end. + +-spec is_form(exml:element()) -> boolean(). +is_form(#xmlel{name = Name} = Elem) -> + Name =:= <<"x">> andalso exml_query:attr(Elem, <<"xmlns">>) =:= ?NS_XDATA. + +-spec is_form(exml:element(), [binary()]) -> boolean(). +is_form(Elem, Types) -> + is_form(Elem) andalso lists:member(form_type(Elem), Types). + +-spec form_type(exml:element()) -> binary() | undefined. +form_type(Form) -> + exml_query:attr(Form, <<"type">>, <<"form">>). + +-spec form_fields_to_kvs([exml:element()]) -> kv_map(). +form_fields_to_kvs(Fields) -> + maps:from_list(lists:flatmap(fun form_field_to_kv/1, Fields)). + +form_field_to_kv(FieldEl = #xmlel{name = <<"field">>}) -> + case exml_query:attr(FieldEl, <<"var">>) of + undefined -> []; + Var -> [{Var, exml_query:paths(FieldEl, [{element, <<"value">>}, cdata])}] + end; +form_field_to_kv(_) -> + []. + +%% Form construction + +-spec form(form()) -> exml:element(). +form(Spec) -> + #xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, maps:get(type, Spec, <<"form">>)}], + children = lists:flatmap(fun(Item) -> form_children(Item, Spec) end, + [title, instructions, ns, fields, reported, items]) + }. + +form_children(title, #{title := Title}) -> + [form_title(Title)]; +form_children(instructions, #{instructions := Instructions}) -> + [form_instructions(Instructions)]; +form_children(ns, #{ns := NS}) -> + [form_type_field(NS)]; +form_children(fields, #{fields := Fields}) -> + [form_field(Field) || Field <- Fields]; +form_children(reported, #{reported := ReportedFields}) -> + [reported_element([form_field(Field) || Field <- ReportedFields])]; +form_children(items, #{items := Items}) -> + [item_element([form_field(Field) || Field <- ItemFields]) || ItemFields <- Items]; +form_children(_, #{}) -> + []. + +-spec form_type_field(binary()) -> exml:element(). +form_type_field(NS) when is_binary(NS) -> + form_field(#{var => <<"FORM_TYPE">>, type => <<"hidden">>, values => [NS]}). + +-spec form_field(field()) -> exml:element(). +form_field(M) when is_map(M) -> + Values = [form_field_value(Value) || Value <- maps:get(values, M, [])], + Options = [form_field_option(Option) || Option <- maps:get(options, M, [])], + Attrs = [{atom_to_binary(K), V} || {K, V} <- maps:to_list(M), K =/= values, K =/= options], + #xmlel{name = <<"field">>, attrs = Attrs, children = Values ++ Options}. + +-spec form_title(binary()) -> exml:element(). +form_title(Title) -> + #xmlel{name = <<"title">>, attrs = [], children = [{xmlcdata, Title}]}. + +-spec form_instructions(binary()) -> exml:element(). +form_instructions(Instructions) -> + #xmlel{name = <<"instructions">>, attrs = [], children = [{xmlcdata, Instructions}]}. + +-spec reported_element([exml:element()]) -> exml:element(). +reported_element(Fields) -> + #xmlel{name = <<"reported">>, attrs = [], children = Fields}. + +-spec item_element([exml:element()]) -> exml:element(). +item_element(Fields) -> + #xmlel{name = <<"item">>, attrs = [], children = Fields}. + +-spec form_field_option(option()) -> exml:element(). +form_field_option({Label, Value}) -> + #xmlel{name = <<"option">>, + attrs = [{<<"label">>, Label}], + children = [form_field_value(Value)]}; +form_field_option(Option) -> + form_field_option({Option, Option}). + +-spec form_field_value(binary()) -> exml:element(). +form_field_value(Value) -> + #xmlel{name = <<"value">>, children = [#xmlcdata{content = Value}]}. diff --git a/src/mongoose_disco.erl b/src/mongoose_disco.erl index 00516289aba..3884873e8b4 100644 --- a/src/mongoose_disco.erl +++ b/src/mongoose_disco.erl @@ -217,26 +217,4 @@ identity_to_xml(Identity) -> -spec info_to_xml(info()) -> exml:element(). info_to_xml(#{xmlns := NS, fields := Fields}) -> - #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}], - children = [form_type_field_xml(NS) | - [info_field_to_xml(Field) || Field <- Fields]]}. - --spec info_field_to_xml(info_field()) -> exml:element(). -info_field_to_xml(InfoField) -> - {Values, Attrs} = maps:take(values, InfoField), - #xmlel{name = <<"field">>, - attrs = lists:map(fun({Key, Value}) -> {atom_to_binary(Key, utf8), Value} end, - maps:to_list(Attrs)), - children = values_to_xml(Values)}. - --spec values_to_xml([binary()]) -> [exml:element()]. -values_to_xml(Values) -> - [#xmlel{name = <<"value">>, children = [#xmlcdata{content = Value}]} || Value <- Values]. - --spec form_type_field_xml(binary()) -> exml:element(). -form_type_field_xml(NS) -> - #xmlel{name = <<"field">>, - attrs = [{<<"var">>, <<"FORM_TYPE">>}, {<<"type">>, <<"hidden">>}], - children = [#xmlel{name = <<"value">>, - children = [#xmlcdata{content = NS}]}]}. + mongoose_data_forms:form(#{type => <<"result">>, ns => NS, fields => Fields}). diff --git a/src/muc_light/mod_muc_light_codec_legacy.erl b/src/muc_light/mod_muc_light_codec_legacy.erl index 156f363f02f..d60b6dc8278 100644 --- a/src/muc_light/mod_muc_light_codec_legacy.erl +++ b/src/muc_light/mod_muc_light_codec_legacy.erl @@ -138,14 +138,12 @@ decode_iq(_From, #iq{ xmlns = ?NS_MUC_OWNER, type = get, sub_el = _QueryEl, id = decode_iq(From, IQ = #iq{ xmlns = ?NS_MUC_OWNER, type = set, sub_el = QueryEl, id = ID }) -> case exml_query:subelement(QueryEl, <<"destroy">>) of undefined -> - try parse_config(exml_query:paths(QueryEl, [{element, <<"x">>}, - {element, <<"field">>}])) of + case parse_config_form(QueryEl) of {ok, RawConfig} -> - {ok, {set, #config{ id = ID, raw_config = RawConfig }}} - catch Class:Reason:Stacktrace -> + {ok, {set, #config{ id = ID, raw_config = RawConfig }}}; + {error, Reason} -> ?LOG_WARNING(#{what => muc_parse_config_failed, - from_jid => jid:to_binary(From), iq => IQ, - class => Class, reason => Reason, stacktrace => Stacktrace}), + from_jid => jid:to_binary(From), iq => IQ, reason => Reason}), {error, bad_request} end; _ -> @@ -191,19 +189,12 @@ decode_iq(_From, #iq{} = IQ) -> %% ------------------ Parsers ------------------ --spec parse_config(Els :: [jlib:xmlch()]) -> {ok, mod_muc_light_room_config:binary_kv()}. -parse_config(Els) -> - parse_config(Els, []). - --spec parse_config(Els :: [jlib:xmlch()], ConfigAcc :: mod_muc_light_room_config:binary_kv()) -> - {ok, mod_muc_light_room_config:binary_kv()}. -parse_config([], ConfigAcc) -> - {ok, ConfigAcc}; -parse_config([Field | REls], ConfigAcc) -> - case {exml_query:attr(Field, <<"var">>), - exml_query:path(Field, [{element, <<"value">>}, cdata])} of - {<<"FORM_TYPE">>, _} -> parse_config(REls, ConfigAcc); - ConfigKV -> parse_config(REls, [ConfigKV | ConfigAcc]) +parse_config_form(QueryEl) -> + case mongoose_data_forms:find_and_parse_form(QueryEl) of + #{kvs := KVs} -> + {ok, [{K, V} || {K, [V]} <- maps:to_list(KVs)]}; + {error, Msg} -> + {error, Msg} end. -spec parse_aff_users(Els :: [jlib:xmlch()]) -> {ok, aff_users()}. @@ -267,15 +258,11 @@ encode_meta({get, #disco_items{ rooms = Rooms, id = ID, rsm = RSMOut }}, || {{RoomU, RoomS}, RoomName, _RoomVersion} <- Rooms ], {iq_reply, ?NS_DISCO_ITEMS, jlib:rsm_encode(RSMOut) ++ DiscoEls, ID}; encode_meta({get, #config{} = Config}, _RoomJID, _SenderJID, _HandleFun, _Acc) -> - ConfigEls = [ jlib:form_field({K, <<"text-single">>, V, K}) - || {K, V} <- Config#config.raw_config ], - XEl = #xmlel{ name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = [ - kv_to_el(<<"title">>, <<"Configuration form for the room">>), - jlib:form_field({<<"FORM_TYPE">>, <<"hidden">>, - <<"http://jabber.org/protocol/muc#roomconfig">>}) - | ConfigEls] }, + Fields = [#{var => K, type => <<"text-single">>, values => [V]} + || {K, V} <- Config#config.raw_config], + XEl = mongoose_data_forms:form(#{title => <<"Configuration form for the room">>, + ns => <<"http://jabber.org/protocol/muc#roomconfig">>, + fields => Fields}), {iq_reply, ?NS_MUC_OWNER, [XEl], Config#config.id}; encode_meta({get, #affiliations{} = Affs}, _RoomJID, _SenderJID, _HandleFun, _Acc) -> AffEls = [ aff_user_to_item(AffUser) || AffUser <- Affs#affiliations.aff_users ], @@ -386,10 +373,6 @@ blocking_to_el({What, Action, {WhoU, WhoS}}, Service) -> {<<"order">>, <<"1">>} ] }. --spec kv_to_el(binary(), binary()) -> exml:element(). -kv_to_el(Key, Value) -> - #xmlel{ name = Key, children = [#xmlcdata{ content = Value }] }. - -spec envelope(XMLNS :: binary(), Children :: [jlib:xmlch()]) -> [jlib:xmlch()]. envelope(XMLNS, Children) -> [ #xmlel{ name = <<"x">>, attrs = [{<<"xmlns">>, XMLNS}], children = Children } ]. diff --git a/src/pubsub/mod_pubsub.erl b/src/pubsub/mod_pubsub.erl index f2b2c837392..11ce6d3f574 100644 --- a/src/pubsub/mod_pubsub.erl +++ b/src/pubsub/mod_pubsub.erl @@ -788,10 +788,12 @@ handle_pep_authorization_response(_, <<"error">>, From, To, Acc, Packet) -> handle_pep_authorization_response(<<"message">>, _, From, To, Acc, Packet) when From#jid.luser == To#jid.luser, From#jid.lserver == To#jid.lserver -> case find_authorization_response(Packet) of - none -> {From, To, Acc, Packet}; - invalid -> {From, To, Acc, Packet}; - XFields -> - handle_authorization_response(Acc, jid:to_lower(To), From, To, Packet, XFields), + none -> + {From, To, Acc, Packet}; + invalid -> + {From, To, Acc, Packet}; + KVs -> + handle_authorization_response(Acc, jid:to_lower(To), From, To, Packet, KVs), drop end; handle_pep_authorization_response(_, _, From, To, Acc, Packet) -> @@ -1464,8 +1466,10 @@ iq_pubsub_set_retract(Host, Node, From, iq_pubsub_set_subscribe(Host, Node, From, #{query_el := QueryEl, action_el := #xmlel{attrs = SubscribeAttrs}}) -> - ConfigXForm = exml_query:path(QueryEl, [{element, <<"options">>}, - {element_with_ns, <<"x">>, ?NS_XDATA}]), + ConfigXForm = case exml_query:subelement(QueryEl, <<"options">>) of + undefined -> undefined; + Options -> mongoose_data_forms:find_form(Options) + end, JID = xml:get_attr_s(<<"jid">>, SubscribeAttrs), subscribe_node(Host, Node, From, JID, ConfigXForm). @@ -1504,7 +1508,7 @@ iq_pubsub_get_options(Host, Node, Lang, #{action_el := #xmlel{attrs = GetOptions get_options(Host, Node, JID, SubId, Lang). iq_pubsub_set_options(Host, Node, #{action_el := #xmlel{attrs = SetOptionsAttrs} = ActionEl}) -> - XForm = exml_query:subelement_with_name_and_ns(ActionEl, <<"x">>, ?NS_XDATA), + XForm = mongoose_data_forms:find_form(ActionEl), SubId = xml:get_attr_s(<<"subid">>, SetOptionsAttrs), JID = xml:get_attr_s(<<"jid">>, SetOptionsAttrs), set_options(Host, Node, JID, SubId, XForm). @@ -1616,19 +1620,11 @@ send_pending_node_form(Request, Host, Owner, Plugins) -> [] -> {error, mongoose_xmpp_errors:feature_not_implemented()}; Ps -> - XOpts = [#xmlel{name = <<"option">>, attrs = [], - children = [#xmlel{name = <<"value">>, - attrs = [], - children = [{xmlcdata, Node}]}]} - || Node <- get_pending_nodes(Host, Owner, Ps)], - XForm = #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = [#xmlel{name = <<"field">>, - attrs = [{<<"type">>, <<"list-single">>}, - {<<"var">>, <<"pubsub#node">>}], - children = lists:usort(XOpts)}]}, - adhoc:produce_response(Request, executing, <<"execute">>, [XForm]) + Options = get_pending_nodes(Host, Owner, Ps), + Field = #{type => <<"list-single">>, var => <<"pubsub#node">>, + options => lists:usort(Options)}, + Form = mongoose_data_forms:form(#{fields => [Field]}), + adhoc:produce_response(Request, executing, <<"execute">>, [Form]) end. get_pending_nodes(Host, Owner, Plugins) -> @@ -1650,19 +1646,18 @@ get_pending_nodes(Host, Owner, Plugins) -> Err -> Err end. -adhoc_get_pending_parse_options(Host, #xmlel{name = <<"x">>} = XEl) -> - case jlib:parse_xdata_submit(XEl) of - invalid -> - {error, mongoose_xmpp_errors:bad_request()}; - XData2 -> - case set_xoption(Host, XData2, []) of +adhoc_get_pending_parse_options(Host, XEl) -> + case mongoose_data_forms:parse_form(XEl) of + #{type := <<"submit">>, kvs := KVs} -> + case set_xoption(Host, maps:to_list(KVs), []) of NewOpts when is_list(NewOpts) -> {result, NewOpts}; Err -> Err - end - end; -adhoc_get_pending_parse_options(_Host, XData) -> - ?LOG_INFO(#{what => pubsub_bad_xform, exml_packet => XData}), - {error, mongoose_xmpp_errors:bad_request()}. + end; + {error, Msg} -> + {error, mongoose_xmpp_errors:bad_request(<<"en">>, Msg)}; + _ -> + {error, mongoose_xmpp_errors:bad_request(<<"en">>, <<"Invalid form type">>)} + end. %% @doc

Send a subscription approval form to Owner for all pending %% subscriptions on Host and Node.

@@ -1700,88 +1695,42 @@ get_node_subscriptions_transaction(Owner, #pubsub_node{id = Nidx, type = Type}) send_authorization_request(#pubsub_node{nodeid = {Host, Node}, owners = Owners}, Subscriber) -> Lang = <<"en">>, - FormChildren = [#xmlel{name = <<"title">>, attrs = [], - children = - [#xmlcdata{content = - translate:translate(Lang, <<"PubSub subscriber request">>)}]}, - #xmlel{name = <<"instructions">>, - attrs = [], - children = - [#xmlcdata{content = translate:translate( - Lang, <<"Choose whether to approve this entity's " - "subscription.">>)}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"FORM_TYPE">>}, - {<<"type">>, <<"hidden">>}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = [#xmlcdata{content = ?NS_PUBSUB_SUB_AUTH}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, <<"pubsub#node">>}, - {<<"type">>, - <<"text-single">>}, - {<<"label">>, translate:translate(Lang, <<"Node ID">>)}], - children = [#xmlel{name = <<"value">>, - attrs = [], - children = [#xmlcdata{content = Node}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, - <<"pubsub#subscriber_jid">>}, - {<<"type">>, <<"jid-single">>}, - {<<"label">>, - translate:translate(Lang, <<"Subscriber Address">>)}], - children = - [#xmlel{name = <<"value">>, - attrs = [], - children = [#xmlcdata{content = jid:to_binary(Subscriber)}]}]}, - #xmlel{name = <<"field">>, - attrs = - [{<<"var">>, - <<"pubsub#allow">>}, - {<<"type">>, <<"boolean">>}, - {<<"label">>, - translate:translate(Lang, - <<"Allow this Jabber ID to subscribe to " - "this pubsub node?">>)}], - children = [#xmlel{name = <<"value">>, - attrs = [], - children = [#xmlcdata{content = <<"false">>}]}]}], + Title = translate:translate(Lang, <<"PubSub subscriber request">>), + Instructions = translate:translate(Lang, <<"Choose whether to approve this entity's " + "subscription.">>), + Fields = [#{var => <<"pubsub#node">>, + type => <<"text-single">>, + label => translate:translate(Lang, <<"Node ID">>), + values => [Node]}, + #{var => <<"pubsub#subscriber_jid">>, + type => <<"jid-single">>, + label => translate:translate(Lang, <<"Subscriber Address">>), + values => [jid:to_binary(Subscriber)]}, + #{var => <<"pubsub#allow">>, + type => <<"boolean">>, + label => translate:translate(Lang, <<"Allow this Jabber ID to subscribe to " + "this pubsub node?">>), + values => [<<"false">>]}], + Form = mongoose_data_forms:form(#{title => Title, instructions => Instructions, + ns => ?NS_PUBSUB_SUB_AUTH, fields => Fields}), Stanza = #xmlel{name = <<"message">>, attrs = [{<<"id">>, mongoose_bin:gen_from_crypto()}], - children = [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = FormChildren}]}, + children = [Form]}, lists:foreach(fun(Owner) -> ejabberd_router:route(service_jid(Host), jid:make(Owner), Stanza) end, Owners). -find_authorization_response(#xmlel{ children = Els }) -> - XData = lists:foldl(fun(#xmlel{name = <<"x">>, attrs = XAttrs} = XEl, Acc) -> - case {xml:get_attr_s(<<"xmlns">>, XAttrs), - xml:get_attr_s(<<"type">>, XAttrs)} of - {?NS_XDATA, <<"submit">>} -> - [jlib:parse_xdata_submit(XEl) | Acc]; - _ -> - Acc - end; - (_, Acc) -> - Acc - end, [], xml:remove_cdata(Els)), - case XData of - [] -> +find_authorization_response(El) -> + case mongoose_data_forms:find_form(El) of + undefined -> none; - [XFields] when is_list(XFields) -> - ?LOG_DEBUG(#{what => pubsub_xfields, xfields => XFields}), - case lists:keysearch(<<"FORM_TYPE">>, 1, XFields) of - {value, {_, [?NS_PUBSUB_SUB_AUTH]}} -> XFields; - _ -> invalid - end; - _ -> - invalid + Form -> + case mongoose_data_forms:parse_form_fields(Form) of + #{type := <<"submit">>, ns := ?NS_PUBSUB_SUB_AUTH, kvs := KVs} -> + KVs; + _ -> + invalid + end end. %% @doc Send a message to JID with the supplied Subscription @@ -1800,12 +1749,10 @@ send_authorization_approval(Host, JID, SNode, Subscription) -> ejabberd_router:route(service_jid(Host), JID, Stanza). handle_authorization_response(Acc, Host, From, To, Packet, XFields) -> - case {lists:keysearch(<<"pubsub#node">>, 1, XFields), - lists:keysearch(<<"pubsub#subscriber_jid">>, 1, XFields), - lists:keysearch(<<"pubsub#allow">>, 1, XFields)} of - {{value, {_, [Node]}}, - {value, {_, [SSubscriber]}}, - {value, {_, [SAllow]}}} -> + case XFields of + #{<<"pubsub#node">> := [Node], + <<"pubsub#subscriber_jid">> := [SSubscriber], + <<"pubsub#allow">> := [SAllow]} -> FromLJID = jid:to_lower(jid:to_bare(From)), Subscriber = jid:from_binary(SSubscriber), Allow = string_allow_to_boolean(SAllow), @@ -1863,12 +1810,10 @@ update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) -> end. -define(XFIELD(Type, Label, Var, Val), - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). + #{type => Type, + label => translate:translate(Lang, Label), + var => Var, + values => [Val]}). -define(BOOLXFIELD(Label, Var, Val), ?XFIELD(<<"boolean">>, Label, Var, @@ -1881,45 +1826,27 @@ update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) -> ?XFIELD(<<"text-single">>, Label, Var, Val)). -define(STRINGMXFIELD(Label, Var, Vals), - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, <<"text-multi">>}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, V}]} - || V <- Vals]}). + #{type => <<"text-multi">>, + label => translate:translate(Lang, Label), + var => Var, + values => Vals}). -define(XFIELDOPT(Type, Label, Var, Val, Opts), - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = [#xmlel{name = <<"option">>, attrs = [], - children = [#xmlel{name = <<"value">>, - attrs = [], - children = [{xmlcdata, Opt}]}]} - || Opt <- Opts] - ++ - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). + #{type => Type, + label => translate:translate(Lang, Label), + var => Var, + options => Opts, + values => [Val]}). -define(LISTXFIELD(Label, Var, Val, Opts), ?XFIELDOPT(<<"list-single">>, Label, Var, Val, Opts)). -define(LISTMXFIELD(Label, Var, Vals, Opts), - #xmlel{name = <<"field">>, - attrs = [{<<"type">>, <<"list-multi">>}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = [#xmlel{name = <<"option">>, attrs = [], - children = [#xmlel{name = <<"value">>, - attrs = [], - children = [{xmlcdata, Opt}]}]} - || Opt <- Opts] - ++ - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]} - || Val <- Vals]}). + #{type => <<"list-multi">>, + label => translate:translate(Lang, Label), + var => Var, + options => Opts, + values => Vals}). %% @doc

Create new pubsub nodes

%%

In addition to method-specific error conditions, there are several general reasons @@ -1973,17 +1900,7 @@ create_node(Host, ServerHost, <<>>, Owner, Type, Access, Configuration) -> end; create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> Type = select_type(ServerHost, Host, Node, GivenType), - ConfigXEl = case xml:remove_cdata(Configuration) of - [] -> - {result, node_options(Host, Type)}; - [#xmlel{name = <<"x">>} = XEl] -> - XEl; - _ -> - ?LOG_INFO(#{what => pubsub_bad_node_configuration, - pubsub_node => Node, configuration => Configuration}), - {error, mongoose_xmpp_errors:bad_request()} - end, - case parse_create_node_options_if_possible(Host, Type, ConfigXEl) of + case parse_create_node_options(Host, Type, xml:remove_cdata(Configuration)) of {result, NodeOptions} -> CreateNode = fun () -> create_node_transaction(Host, ServerHost, Node, Owner, @@ -2007,21 +1924,27 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> Error end; Error -> + ?LOG_INFO(#{what => pubsub_bad_node_configuration, + pubsub_node => Node, configuration => Configuration}), Error end. -parse_create_node_options_if_possible(Host, Type, #xmlel{} = ConfigXEl) -> - case jlib:parse_xdata_submit(ConfigXEl) of - invalid -> - {error, mongoose_xmpp_errors:bad_request()}; - XData -> - case set_xoption(Host, XData, node_options(Host, Type)) of +parse_create_node_options(Host, Type, []) -> + {result, node_options(Host, Type)}; +parse_create_node_options(Host, Type, [XEl]) -> + case mongoose_data_forms:parse_form(XEl) of + #{type := <<"submit">>, kvs := KVs} -> + case set_xoption(Host, maps:to_list(KVs), node_options(Host, Type)) of NewOpts when is_list(NewOpts) -> {result, NewOpts}; Err -> Err - end + end; + {error, Msg} -> + {error, mongoose_xmpp_errors:bad_request(<<"en">>, Msg)}; + _ -> + {error, mongoose_xmpp_errors:bad_request(<<"en">>, <<"Invalid form type">>)} end; -parse_create_node_options_if_possible(_Host, _Type, InvalidConfigXEl) -> - InvalidConfigXEl. +parse_create_node_options(_Host, _Type, _) -> + {error, mongoose_xmpp_errors:bad_request()}. create_node_transaction(Host, ServerHost, Node, Owner, Type, Access, NodeOptions) -> Parent = get_parent(Type, Node), @@ -3534,11 +3457,10 @@ broadcast_config_notification(Host, Node, Nidx, Type, NodeOptions, Lang) -> {result, false} end. -payload_by_option(Type, NodeOptions, Lang) -> +payload_by_option(_Type, NodeOptions, Lang) -> case get_option(NodeOptions, deliver_payloads) of true -> - [#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}], - children = get_configure_xfields(Type, NodeOptions, Lang, [])}]; + [configure_form(<<"result">>, NodeOptions, Lang, [])]; false -> [] end. @@ -3730,10 +3652,7 @@ get_configure_transaction(ServerHost, Node, From, Lang, case node_call(Type, get_affiliation, [Nidx, From]) of {result, owner} -> Groups = mongoose_hooks:roster_groups(ServerHost), - XEl = #xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = get_configure_xfields(Type, Options, Lang, Groups)}, + XEl = configure_form(<<"form">>, Options, Lang, Groups), ConfigureEl = #xmlel{name = <<"configure">>, attrs = node_attr(Node), children = [XEl]}, @@ -3748,12 +3667,8 @@ get_configure_transaction(ServerHost, Node, From, Lang, get_default(Host, Node, _From, #{lang := Lang}) -> Type = select_type(Host, Node), Options = node_options(Host, Type), - DefaultEl = #xmlel{name = <<"default">>, attrs = [], - children = - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"form">>}], - children = get_configure_xfields(Type, Options, Lang, [])}]}, + XEl = configure_form(<<"form">>, Options, Lang, []), + DefaultEl = #xmlel{name = <<"default">>, attrs = [], children = [XEl]}, {result, [#xmlel{name = <<"pubsub">>, attrs = [{<<"xmlns">>, ?NS_PUBSUB_OWNER}], @@ -3855,9 +3770,12 @@ max_items(Host, Options) -> <<"pubsub#", (atom_to_binary(Var, latin1))/binary>>, get_option(Options, Var, []))). -get_configure_xfields(_Type, Options, Lang, Groups) -> - [?XFIELD(<<"hidden">>, <<>>, <<"FORM_TYPE">>, (?NS_PUBSUB_NODE_CONFIG)), - ?BOOL_CONFIG_FIELD(<<"Deliver payloads with event notifications">>, +configure_form(Type, Options, Lang, Groups) -> + Fields = get_configure_xfields(Options, Lang, Groups), + mongoose_data_forms:form(#{type => Type, ns => ?NS_PUBSUB_NODE_CONFIG, fields => Fields}). + +get_configure_xfields(Options, Lang, Groups) -> + [?BOOL_CONFIG_FIELD(<<"Deliver payloads with event notifications">>, deliver_payloads), ?BOOL_CONFIG_FIELD(<<"Deliver event notifications">>, deliver_notifications), @@ -3905,20 +3823,20 @@ get_configure_xfields(_Type, Options, Lang, Groups) -> %%

  • The specified node does not exist.
  • %% set_configure(Host, Node, From, #{action_el := ActionEl, lang := Lang}) -> - case xml:remove_cdata(ActionEl#xmlel.children) of - [#xmlel{name = <<"x">>} = XEl] -> - case {xml:get_tag_attr_s(<<"xmlns">>, XEl), xml:get_tag_attr_s(<<"type">>, XEl)} of - {?NS_XDATA, <<"cancel">>} -> {result, []}; - {?NS_XDATA, <<"submit">>} -> set_configure_submit(Host, Node, From, XEl, Lang); - _ -> {error, mongoose_xmpp_errors:bad_request()} - end; + case mongoose_data_forms:find_and_parse_form(ActionEl) of + #{type := <<"cancel">>} -> + {result, []}; + #{type := <<"submit">>, kvs := KVs} -> + set_configure_submit(Host, Node, From, KVs, Lang); + {error, Msg} -> + {error, mongoose_xmpp_errors:bad_request(Lang, Msg)}; _ -> - {error, mongoose_xmpp_errors:bad_request()} + {error, mongoose_xmpp_errors:bad_request(Lang, <<"Invalid form type">>)} end. -set_configure_submit(Host, Node, User, XEl, Lang) -> +set_configure_submit(Host, Node, User, KVs, Lang) -> Action = fun(NodeRec) -> - set_configure_transaction(Host, User, XEl, NodeRec) + set_configure_transaction(Host, User, KVs, NodeRec) end, case transaction(Host, Node, Action, ?FUNCTION_NAME) of {result, {_OldNode, TNode}} -> @@ -3931,24 +3849,21 @@ set_configure_submit(Host, Node, User, XEl, Lang) -> Other end. -set_configure_transaction(Host, User, XEl, #pubsub_node{ type = Type, id = Nidx } = NodeRec) -> +set_configure_transaction(Host, User, KVs, #pubsub_node{ type = Type, id = Nidx } = NodeRec) -> case node_call(Type, get_affiliation, [Nidx, User]) of {result, owner} -> - case jlib:parse_xdata_submit(XEl) of - invalid -> {error, mongoose_xmpp_errors:bad_request()}; - XData -> set_configure_valid_transaction(Host, NodeRec, XData) - end; + set_configure_valid_transaction(Host, NodeRec, KVs); _ -> {error, mongoose_xmpp_errors:forbidden()} end. set_configure_valid_transaction(Host, #pubsub_node{ type = Type, options = Options } = NodeRec, - XData) -> + KVs) -> OldOpts = case Options of [] -> node_options(Host, Type); _ -> Options end, - case set_xoption(Host, XData, OldOpts) of + case set_xoption(Host, maps:to_list(KVs), OldOpts) of NewOpts when is_list(NewOpts) -> NewNode = NodeRec#pubsub_node{options = NewOpts}, case tree_call(Host, set_node, [NewNode]) of @@ -4002,8 +3917,6 @@ add_opt(Key, Value, Opts) -> set_xoption(Host, Opts, add_opt(Opt, Val, NewOpts))). set_xoption(_Host, [], NewOpts) -> NewOpts; -set_xoption(Host, [{<<"FORM_TYPE">>, _} | Opts], NewOpts) -> - set_xoption(Host, Opts, NewOpts); set_xoption(Host, [{<<"pubsub#roster_groups_allowed">>, Value} | Opts], NewOpts) -> ?SET_LIST_XOPT(roster_groups_allowed, Value); set_xoption(Host, [{<<"pubsub#deliver_payloads">>, [Val]} | Opts], NewOpts) -> diff --git a/src/pubsub/node_push.erl b/src/pubsub/node_push.erl index 755ba7e502e..ab1880525b0 100644 --- a/src/pubsub/node_push.erl +++ b/src/pubsub/node_push.erl @@ -111,24 +111,15 @@ is_allowed_to_publish(PublishModel, Affiliation) -> or (Affiliation == publish_only)). --spec parse_form(undefined | exml:element()) -> invalid_form | #{atom() => binary()}. +-spec parse_form(undefined | exml:element()) -> invalid_form | #{binary() => binary()}. parse_form(undefined) -> #{}; parse_form(Form) -> - IsForm = ?NS_XDATA == exml_query:attr(Form, <<"xmlns">>), - IsSubmit = <<"submit">> == exml_query:attr(Form, <<"type">>, <<"submit">>), - - FieldsXML = exml_query:subelements(Form, <<"field">>), - Fields = [{exml_query:attr(Field, <<"var">>), - exml_query:path(Field, [{element, <<"value">>}, cdata])} || Field <- FieldsXML], - {_, CustomFields} = lists:partition( - fun({Name, _}) -> - Name == <<"FORM_TYPE">> - end, Fields), - - case IsForm andalso IsSubmit of - true -> - maps:from_list(CustomFields); - false -> + case mongoose_data_forms:parse_form(Form) of + #{type := <<"submit">>, kvs := KVs} -> + maps:filtermap(fun(_, [V]) -> {true, V}; + (_, _) -> false + end, KVs); + _ -> invalid_form end. diff --git a/src/pubsub/pubsub_form_utils.erl b/src/pubsub/pubsub_form_utils.erl index 938f5bb8114..ca19f7247de 100644 --- a/src/pubsub/pubsub_form_utils.erl +++ b/src/pubsub/pubsub_form_utils.erl @@ -13,9 +13,7 @@ -export([make_sub_xform/1, parse_sub_xform/1]). --include("mongoose_logger.hrl"). -include("mongoose_ns.hrl"). --include_lib("exml/include/exml.hrl"). -type convert_from_binary_fun() :: fun(([binary()]) -> any()). -type convert_to_binary_fun() :: fun((any()) -> [binary()]). @@ -45,7 +43,7 @@ %% TODO: Right now -spec make_sub_xform(Options :: mod_pubsub:subOptions()) -> {ok, exml:element()}. make_sub_xform(Options) -> - XFields = [make_field_xml(OptDefinition, Options) || OptDefinition <- sub_form_options()], + XFields = [make_field(OptDefinition, Options) || OptDefinition <- sub_form_options()], {ok, make_sub_xform_xml(XFields)}. %% The list of options returned by this function may be a subset of the options schema. @@ -54,9 +52,11 @@ make_sub_xform(Options) -> parse_sub_xform(undefined) -> {ok, []}; parse_sub_xform(XForm) -> - case jlib:parse_xdata_submit(XForm) of - invalid -> {error, invalid_form}; - XData -> convert_fields_from_binaries(XData, [], sub_form_options()) + case mongoose_data_forms:parse_form_fields(XForm) of + #{type := <<"submit">>, kvs := KVs} -> + convert_fields_from_binaries(maps:to_list(KVs), [], sub_form_options()); + _ -> + {error, invalid_form} end. %%==================================================================== @@ -65,41 +65,30 @@ parse_sub_xform(XForm) -> -spec make_sub_xform_xml(XFields :: [exml:element()]) -> exml:element(). make_sub_xform_xml(XFields) -> - FormTypeEl = #xmlel{name = <<"field">>, - attrs = [{<<"var">>, <<"FORM_TYPE">>}, {<<"type">>, <<"hidden">>}], - children = [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, ?NS_PUBSUB_SUB_OPTIONS}]}]}, - #xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_XDATA}], children = [FormTypeEl | XFields]}. - --spec make_field_xml(OptDefinition :: option_definition(), - Options :: mod_pubsub:subOptions()) -> exml:element(). -make_field_xml({VarName, Key, #{ label := Label, form_type := FormType } = VarProps}, Options) -> - ChoicesEls = make_choices_xml(VarProps), - ValEls = make_values_xml(Key, Options, VarProps), - - #xmlel{name = <<"field">>, - attrs = [{<<"var">>, VarName}, {<<"type">>, FormType}, {<<"label">>, Label}], - children = ChoicesEls ++ ValEls}. - -make_choices_xml(#{ possible_choices := PossibleChoices }) -> - [ make_option_xml(Value, Label) || {Value, Label} <- PossibleChoices ]; -make_choices_xml(#{}) -> + mongoose_data_forms:form(#{ns => ?NS_PUBSUB_SUB_OPTIONS, fields => XFields}). + +-spec make_field(OptDefinition :: option_definition(), + Options :: mod_pubsub:subOptions()) -> mongoose_data_forms:field(). +make_field({VarName, Key, #{ label := Label, form_type := FormType } = VarProps}, Options) -> + #{var => VarName, + type => FormType, + label => Label, + options => make_choices(VarProps), + values => make_values(Key, Options, VarProps)}. + +make_choices(#{ possible_choices := PossibleChoices }) -> + [ {Label, Value} || {Value, Label} <- PossibleChoices ]; +make_choices(#{}) -> []. -make_option_xml(Value, Label) -> - #xmlel{name = <<"option">>, attrs = [{<<"label">>, Label}], children = [make_value_xml(Value)]}. - -make_values_xml(Key, Options, #{ data_type := DataType }) -> +make_values(Key, Options, #{ data_type := DataType }) -> case lists:keyfind(Key, 1, Options) of {_, Value} -> - [make_value_xml(BinVal) || BinVal <- convert_value_to_binaries(Value, DataType)]; + convert_value_to_binaries(Value, DataType); false -> [] end. -make_value_xml(Value) -> - #xmlel{name = <<"value">>, attrs = [], children = [#xmlcdata{ content = Value }]}. - %%==================================================================== %% Form definitions & conversions %%==================================================================== @@ -176,8 +165,6 @@ sub_form_options() -> {ok, mod_pubsub:subOptions()} | convert_from_binary_error(). convert_fields_from_binaries([], Result, _Schema) -> {ok, Result}; -convert_fields_from_binaries([{<<"FORM_TYPE">>, _Values} | RData], Acc, Schema) -> - convert_fields_from_binaries(RData, Acc, Schema); convert_fields_from_binaries([{VarBin, Values} | RData], Acc, Schema) -> case lists:keyfind(VarBin, 1, Schema) of {_VBin, _Var, #{ data_type := DataType }} when Values == [] andalso DataType /= list -> diff --git a/src/vcard/mod_vcard.erl b/src/vcard/mod_vcard.erl index ce1d4294138..4c58a27ea63 100644 --- a/src/vcard/mod_vcard.erl +++ b/src/vcard/mod_vcard.erl @@ -502,8 +502,9 @@ do_route(HostType, LServer, From, To, Acc, route_search_iq_set(HostType, LServer, From, To, Acc, Lang, SubEl, IQ); do_route(HostType, LServer, From, To, Acc, #iq{type = get, xmlns = ?NS_SEARCH, lang = Lang} = IQ) -> - Form = ?FORM(To, mod_vcard_backend:search_fields(HostType, LServer), Lang), - ResIQ = make_search_form_result_iq(IQ, Form), + Instr = search_instructions(Lang), + Form = search_form(To, mod_vcard_backend:search_fields(HostType, LServer), Lang), + ResIQ = make_search_form_result_iq(IQ, [Instr, Form]), ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ)); do_route(_HostType, _LServer, From, To, Acc, #iq{type = set, xmlns = ?NS_DISCO_INFO}) -> @@ -542,43 +543,50 @@ do_route(_HostType, _LServer, From, To, Acc, _IQ) -> {Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:service_unavailable()), ejabberd_router:route(To, From, Acc1, Err). -make_search_form_result_iq(IQ, Form) -> +make_search_form_result_iq(IQ, Elements) -> IQ#iq{type = result, sub_el = [#xmlel{name = <<"query">>, attrs = [{<<"xmlns">>, ?NS_SEARCH}], - children = Form + children = Elements }]}. +search_instructions(Lang) -> + Text = translate:translate(Lang, <<"You need an x:data capable client to search">>), + #xmlel{name = <<"instructions">>, attrs = [], children = [#xmlcdata{content = Text}]}. + +search_form(JID, SearchFields, Lang) -> + Title = <<(translate:translate(Lang, <<"Search users in ">>))/binary, + (jid:to_binary(JID))/binary>>, + Instructions = <<"Fill in fields to search for any matching Jabber User">>, + Fields = lists:map(fun ({X, Y}) -> ?TLFIELD(<<"text-single">>, X, Y) end, SearchFields), + mongoose_data_forms:form(#{title => Title, instructions => Instructions, fields => Fields}). + route_search_iq_set(HostType, LServer, From, To, Acc, Lang, SubEl, IQ) -> - XDataEl = find_xdata_el(SubEl), + XDataEl = mongoose_data_forms:find_form(SubEl), RSMIn = jlib:rsm_decode(IQ), case XDataEl of - false -> + undefined -> {Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:bad_request()), ejabberd_router:route(To, From, Acc1, Err); _ -> - XData = jlib:parse_xdata_submit(XDataEl), - case XData of - invalid -> - {Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:bad_request()), - ejabberd_router:route(To, From, Acc1, Err); - _ -> - {SearchResult, RSMOutEls} = search_result(HostType, LServer, Lang, To, XData, RSMIn), + case mongoose_data_forms:parse_form_fields(XDataEl) of + #{type := <<"submit">>, kvs := KVs} -> + {SearchResult, RSMOutEls} = search_result(HostType, LServer, Lang, To, KVs, RSMIn), ResIQ = make_search_result_iq(IQ, SearchResult, RSMOutEls), - ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ)) + ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ)); + _ -> + {Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:bad_request()), + ejabberd_router:route(To, From, Acc1, Err) end end. make_search_result_iq(IQ, SearchResult, RSMOutEls) -> + Form = mongoose_data_forms:form(SearchResult), IQ#iq{ type = result, sub_el = [#xmlel{name = <<"query">>, attrs = [{<<"xmlns">>, ?NS_SEARCH}], - children = [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"result">>}], - children = SearchResult} - ] ++ RSMOutEls} + children = [Form | RSMOutEls]} ]}. iq_get_vcard() -> @@ -588,20 +596,6 @@ iq_get_vcard() -> #xmlel{name = <<"DESC">>, children = [#xmlcdata{content = [<<"MongooseIM vCard module">>, <<"\nCopyright (c) Erlang Solutions Ltd.">>]}]}]. -find_xdata_el(#xmlel{children = SubEls}) -> - find_xdata_el1(SubEls). - -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). features() -> [?NS_DISCO_INFO, ?NS_SEARCH, ?NS_VCARD]. @@ -612,27 +606,25 @@ identity(Lang) -> name => translate:translate(Lang, <<"vCard User Search">>)}. search_result(HostType, LServer, Lang, JID, Data, RSMIn) -> - Text = translate:translate(Lang, <<"Search Results for ">>), - TitleEl = #xmlel{name = <<"title">>, - children = [#xmlcdata{content = [Text, jid:to_binary(JID)]}]}, + Title = translate:translate(Lang, <<"Search Results for ", (jid:to_binary(JID))/binary>>), ReportedFields = mod_vcard_backend:search_reported_fields(HostType, LServer, Lang), - Results1 = mod_vcard_backend:search(HostType, LServer, Data), + Results1 = mod_vcard_backend:search(HostType, LServer, maps:to_list(Data)), Results2 = lists:filtermap( fun(Result) -> case search_result_get_jid(Result) of - {ok, ResultJID} -> + [ResultJID] -> {true, {ResultJID, Result}}; - undefined -> + [] -> false end end, Results1), %% mnesia does not guarantee sorting order Results3 = lists:sort(Results2), - {Results4, RSMOutEls} = - apply_rsm_to_search_results(Results3, RSMIn, none), + {Results4, RSMOutEls} = apply_rsm_to_search_results(Results3, RSMIn, none), Results5 = [Result || {_, Result} <- Results4], - {[TitleEl, ReportedFields | Results5], RSMOutEls}. + Form = #{type => <<"result">>, title => Title, reported => ReportedFields, items => Results5}, + {Form, RSMOutEls}. %% No RSM input, create empty apply_rsm_to_search_results(Results, none, RSMOut) -> @@ -721,15 +713,8 @@ apply_rsm_to_search_results([], _, #rsm_out{} = RSMOut1) -> RSMOut2 = RSMOut1#rsm_out{index = undefined}, {[], jlib:rsm_encode(RSMOut2)}. -search_result_get_jid(#xmlel{name = <<"item">>, - children = Children}) -> - Fields = jlib:parse_xdata_fields(Children), - case lists:keysearch(<<"jid">>, 1, Fields) of - {value, {<<"jid">>, JID}} -> - {ok, list_to_binary(JID)}; - false -> - undefined - end. +search_result_get_jid(Fields) -> + [JID || #{var := <<"jid">>, values := [JID]} <- Fields]. parse_vcard(LUser, VHost, VCARD) -> FN = xml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), @@ -805,23 +790,22 @@ prepare_index_allow_emoji(FieldName, Value) -> prepare_index(FieldName, Sanitized). --spec get_default_reported_fields(binary()) -> exml:element(). +-spec get_default_reported_fields(binary()) -> [mongoose_data_forms:field()]. get_default_reported_fields(Lang) -> - #xmlel{name = <<"reported">>, - children = [ - ?TLFIELD(<<"jid-single">>, <<"Jabber ID">>, <<"jid">>), - ?TLFIELD(<<"text-single">>, <<"Full Name">>, <<"fn">>), - ?TLFIELD(<<"text-single">>, <<"Name">>, <<"first">>), - ?TLFIELD(<<"text-single">>, <<"Middle Name">>, <<"middle">>), - ?TLFIELD(<<"text-single">>, <<"Family Name">>, <<"last">>), - ?TLFIELD(<<"text-single">>, <<"Nickname">>, <<"nick">>), - ?TLFIELD(<<"text-single">>, <<"Birthday">>, <<"bday">>), - ?TLFIELD(<<"text-single">>, <<"Country">>, <<"ctry">>), - ?TLFIELD(<<"text-single">>, <<"City">>, <<"locality">>), - ?TLFIELD(<<"text-single">>, <<"Email">>, <<"email">>), - ?TLFIELD(<<"text-single">>, <<"Organization Name">>, <<"orgname">>), - ?TLFIELD(<<"text-single">>, <<"Organization Unit">>, <<"orgunit">>) - ]}. + [ + ?TLFIELD(<<"jid-single">>, <<"Jabber ID">>, <<"jid">>), + ?TLFIELD(<<"text-single">>, <<"Full Name">>, <<"fn">>), + ?TLFIELD(<<"text-single">>, <<"Name">>, <<"first">>), + ?TLFIELD(<<"text-single">>, <<"Middle Name">>, <<"middle">>), + ?TLFIELD(<<"text-single">>, <<"Family Name">>, <<"last">>), + ?TLFIELD(<<"text-single">>, <<"Nickname">>, <<"nick">>), + ?TLFIELD(<<"text-single">>, <<"Birthday">>, <<"bday">>), + ?TLFIELD(<<"text-single">>, <<"Country">>, <<"ctry">>), + ?TLFIELD(<<"text-single">>, <<"City">>, <<"locality">>), + ?TLFIELD(<<"text-single">>, <<"Email">>, <<"email">>), + ?TLFIELD(<<"text-single">>, <<"Organization Name">>, <<"orgname">>), + ?TLFIELD(<<"text-single">>, <<"Organization Unit">>, <<"orgunit">>) + ]. config_metrics(Host) -> mongoose_module_metrics:opts_for_module(Host, ?MODULE, [backend]). diff --git a/src/vcard/mod_vcard_backend.erl b/src/vcard/mod_vcard_backend.erl index 647a8e6560d..ef8d2ca7a83 100644 --- a/src/vcard/mod_vcard_backend.erl +++ b/src/vcard/mod_vcard_backend.erl @@ -95,7 +95,7 @@ get_vcard(HostType, LUser, LServer) -> mongoose_backend:call_tracked(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args). -spec search(mongooseim:host_type(), jid:lserver(), Data) -> - Res :: term() when + Res :: [[mongoose_data_forms:field()]] when Data :: term(). search(HostType, LServer, Data) -> Args = [HostType, LServer, Data], @@ -107,7 +107,7 @@ search_fields(HostType, LServer) -> mongoose_backend:call(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args). -spec search_reported_fields(mongooseim:host_type(), jid:lserver(), Lang) -> - Res :: term() when + Res :: [mongoose_data_forms:field()] when Lang :: binary(). search_reported_fields(HostType, LServer, Lang) -> Args = [HostType, LServer, Lang], diff --git a/src/vcard/mod_vcard_ldap.erl b/src/vcard/mod_vcard_ldap.erl index 9070f66a3b6..41cc5e9faa1 100644 --- a/src/vcard/mod_vcard_ldap.erl +++ b/src/vcard/mod_vcard_ldap.erl @@ -156,7 +156,8 @@ get_vcard(HostType, LUser, LServer) -> set_vcard(_HostType, _User, _LServer, _VCard, _VCardSearch) -> {error, mongoose_xmpp_errors:not_allowed()}. --spec search(mongooseim:host_type(), jid:lserver(), [{binary(), [binary()]}]) -> [eldap_utils:eldap_entry()]. +-spec search(mongooseim:host_type(), jid:lserver(), [{binary(), [binary()]}]) -> + [[mongoose_data_forms:field()]]. search(HostType, LServer, Data) -> State = get_state(HostType, LServer), search_internal(State, Data). @@ -166,20 +167,13 @@ search_fields(HostType, LServer) -> State = get_state(HostType, LServer), State#state.search_fields. --spec search_reported_fields(mongooseim:host_type(), jid:lserver(), binary()) -> exml:element(). +-spec search_reported_fields(mongooseim:host_type(), jid:lserver(), binary()) -> + [mongoose_data_forms:field()]. search_reported_fields(HostType, LServer, Lang) -> State = get_state(HostType, LServer), SearchReported = State#state.search_reported, - #xmlel{name = <<"reported">>, attrs = [], - children = - [?TLFIELD(<<"text-single">>, <<"Jabber ID">>, - <<"jid">>)] - ++ - lists:map(fun ({Name, Value}) -> - ?TLFIELD(<<"text-single">>, Name, - Value) - end, - SearchReported)}. + [?TLFIELD(<<"text-single">>, Name, Value) || + {Name, Value} <- [{<<"Jabber ID">>, <<"jid">>} | SearchReported]]. %%-------------------------------------------------------------------- %% API @@ -362,10 +356,10 @@ limited_results(E, _) -> E. search_items(Entries, State) -> - lists:flatmap(fun(#eldap_entry{attributes = Attrs}) -> attrs_to_item_xml(Attrs, State) end, + lists:flatmap(fun(#eldap_entry{attributes = Attrs}) -> attrs_to_item(Attrs, State) end, Entries). -attrs_to_item_xml(Attrs, #state{uids = UIDs} = State) -> +attrs_to_item(Attrs, #state{uids = UIDs} = State) -> case eldap_utils:find_ldap_attrs(UIDs, Attrs) of {U, UIDAttrFormat} -> case eldap_utils:get_user_part(U, UIDAttrFormat) of @@ -387,9 +381,9 @@ make_user_item_if_exists(Username, Attrs, {Username, LServer})} end, SearchReported), - Result = [?FIELD(<<"jid">>, <>)] ++ - [?FIELD(Name, search_item_value(Name, Value, BinFields)) || {Name, Value} <- RFields], - [#xmlel{name = <<"item">>, attrs = [], children = Result}]; + [[?FIELD(<<"jid">>, <>)] ++ + [?FIELD(Name, search_item_value(Name, Value, BinFields)) || + {Name, Value} <- RFields]]; _ -> [] end. diff --git a/src/vcard/mod_vcard_mnesia.erl b/src/vcard/mod_vcard_mnesia.erl index 8ff8c0fd5da..1c40c49da40 100644 --- a/src/vcard/mod_vcard_mnesia.erl +++ b/src/vcard/mod_vcard_mnesia.erl @@ -57,6 +57,7 @@ set_vcard(HostType, User, LServer, VCard, VCardSearch) -> mongoose_hooks:vcard_set(HostType, LServer, LUser, VCard), ok. +-spec search(mongooseim:host_type(), jid:lserver(), term()) -> [[mongoose_data_forms:field()]]. search(HostType, LServer, Data) -> MatchHead = make_matchhead(LServer, Data), R = do_search(HostType, LServer, MatchHead), @@ -84,6 +85,8 @@ do_search(HostType, LServer, MatchHeadIn) -> search_fields(_HostType, _LServer) -> mod_vcard:default_search_fields(). +-spec search_reported_fields(mongooseim:host_type(), jid:lserver(), ejabberd:lang()) -> + [mongoose_data_forms:field()]. search_reported_fields(_HostType, _LServer, Lang) -> mod_vcard:get_default_reported_fields(Lang). @@ -181,18 +184,17 @@ make_val(ValBin) -> record_to_item(R) -> {User, Server} = R#vcard_search.user, - #xmlel{name = <<"item">>, - children = [ - ?FIELD(<<"jid">>, [User, <<"@">>, Server]), - ?FIELD(<<"fn">>, (R#vcard_search.fn)), - ?FIELD(<<"last">>, (R#vcard_search.family)), - ?FIELD(<<"first">>, (R#vcard_search.given)), - ?FIELD(<<"middle">>, (R#vcard_search.middle)), - ?FIELD(<<"nick">>, (R#vcard_search.nickname)), - ?FIELD(<<"bday">>, (R#vcard_search.bday)), - ?FIELD(<<"ctry">>, (R#vcard_search.ctry)), - ?FIELD(<<"locality">>, (R#vcard_search.locality)), - ?FIELD(<<"email">>, (R#vcard_search.email)), - ?FIELD(<<"orgname">>, (R#vcard_search.orgname)), - ?FIELD(<<"orgunit">>, (R#vcard_search.orgunit)) - ]}. + [ + ?FIELD(<<"jid">>, <>), + ?FIELD(<<"fn">>, (R#vcard_search.fn)), + ?FIELD(<<"last">>, (R#vcard_search.family)), + ?FIELD(<<"first">>, (R#vcard_search.given)), + ?FIELD(<<"middle">>, (R#vcard_search.middle)), + ?FIELD(<<"nick">>, (R#vcard_search.nickname)), + ?FIELD(<<"bday">>, (R#vcard_search.bday)), + ?FIELD(<<"ctry">>, (R#vcard_search.ctry)), + ?FIELD(<<"locality">>, (R#vcard_search.locality)), + ?FIELD(<<"email">>, (R#vcard_search.email)), + ?FIELD(<<"orgname">>, (R#vcard_search.orgname)), + ?FIELD(<<"orgunit">>, (R#vcard_search.orgunit)) + ]. diff --git a/src/vcard/mod_vcard_rdbms.erl b/src/vcard/mod_vcard_rdbms.erl index ddd56b23b27..033d22913c7 100644 --- a/src/vcard/mod_vcard_rdbms.erl +++ b/src/vcard/mod_vcard_rdbms.erl @@ -163,10 +163,13 @@ search_fields(_HostType, _VHost) -> mod_vcard:default_search_fields(). %% Search vCards reported fields callback +-spec search_reported_fields(mongooseim:host_type(), jid:lserver(), ejabberd:lang()) -> + [mongoose_data_forms:field()]. search_reported_fields(_HostType, _VHost, Lang) -> mod_vcard:get_default_reported_fields(Lang). %% Search vCards callback +-spec search(mongooseim:host_type(), jid:lserver(), term()) -> [[mongoose_data_forms:field()]]. search(HostType, LServer, Data) -> Filters = make_filters(LServer, Data), case Filters of @@ -379,18 +382,17 @@ record_to_items(Records) -> record_to_item({Username, VCardVHost, FN, Family, Given, Middle, Nickname, BDay, CTRY, Locality, EMail, OrgName, OrgUnit}) -> - #xmlel{name = <<"item">>, - children = [ - ?FIELD(<<"jid">>, <>), - ?FIELD(<<"fn">>, FN), - ?FIELD(<<"last">>, Family), - ?FIELD(<<"first">>, Given), - ?FIELD(<<"middle">>, Middle), - ?FIELD(<<"nick">>, Nickname), - ?FIELD(<<"bday">>, BDay), - ?FIELD(<<"ctry">>, CTRY), - ?FIELD(<<"locality">>, Locality), - ?FIELD(<<"email">>, EMail), - ?FIELD(<<"orgname">>, OrgName), - ?FIELD(<<"orgunit">>, OrgUnit) - ]}. + [ + ?FIELD(<<"jid">>, <>), + ?FIELD(<<"fn">>, FN), + ?FIELD(<<"last">>, Family), + ?FIELD(<<"first">>, Given), + ?FIELD(<<"middle">>, Middle), + ?FIELD(<<"nick">>, Nickname), + ?FIELD(<<"bday">>, BDay), + ?FIELD(<<"ctry">>, CTRY), + ?FIELD(<<"locality">>, Locality), + ?FIELD(<<"email">>, EMail), + ?FIELD(<<"orgname">>, OrgName), + ?FIELD(<<"orgunit">>, OrgUnit) + ]. diff --git a/src/vcard/mod_vcard_riak.erl b/src/vcard/mod_vcard_riak.erl index 38648f57d35..0f7568837b0 100644 --- a/src/vcard/mod_vcard_riak.erl +++ b/src/vcard/mod_vcard_riak.erl @@ -62,6 +62,7 @@ get_vcard(HostType, LUser, LServer) -> Other end. +-spec search(mongooseim:host_type(), jid:lserver(), term()) -> [[mongoose_data_forms:field()]]. search(HostType, LServer, Data) -> YZQuery = make_yz_query(Data, []), do_search(YZQuery, HostType, LServer). @@ -85,6 +86,8 @@ do_search(YZQueryIn, HostType, LServer) -> search_fields(_HostType, _LServer) -> mod_vcard:default_search_fields(). +-spec search_reported_fields(mongooseim:host_type(), jid:lserver(), ejabberd:lang()) -> + [mongoose_data_forms:field()]. search_reported_fields(_HostType, _LServer, Lang) -> mod_vcard:get_default_reported_fields(Lang). @@ -116,9 +119,7 @@ make_val(Val) -> end. doc2item(HostType, LServer, Props) -> - Vals = lists:map(pa:bind(fun extract_field/2, Props), search_fields(HostType, LServer)), - #xmlel{name = <<"item">>, - children = Vals}. + lists:map(pa:bind(fun extract_field/2, Props), search_fields(HostType, LServer)). extract_field(Props, {_, <<"user">>}) -> {_, Username} = lists:keyfind(riak_search_mapping(<<"user">>), 1, Props),