diff --git a/include/mod_vcard.hrl b/include/mod_vcard.hrl index 6e7daee6b6..f8e02ee128 100644 --- a/include/mod_vcard.hrl +++ b/include/mod_vcard.hrl @@ -16,51 +16,9 @@ -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}]}). + mongoose_data_forms:form_field(#{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)}]). - + mongoose_data_forms:form_field(#{var => Var, values => [Val]})). diff --git a/src/adhoc.erl b/src/adhoc.erl index ca40cf2962..bd227aee57 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 84d6ba368d..9d607b32c9 100644 --- a/src/event_pusher/mod_event_pusher_push.erl +++ b/src/event_pusher/mod_event_pusher_push.erl @@ -204,7 +204,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; @@ -232,38 +232,24 @@ parse_request(_) -> 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 - end; - _ -> - invalid_form + case mongoose_data_forms:form_to_kvs(Form, <<"submit">>) of + invalid -> + invalid_form; + KVs -> + ParsedKVs = lists:flatmap(fun parse_kv/1, KVs), + case lists:keymember(invalid_field, 1, ParsedKVs) of + false -> ParsedKVs; + true -> invalid_form + end end. -are_form_fields_valid(Fields) -> - lists:all(fun({Key, Value}) -> is_binary(Key) andalso is_binary(Value) end, Fields). +parse_kv({<<"FORM_TYPE">>, [?NS_PUBSUB_PUB_OPTIONS]}) -> []; +parse_kv({K, [V]}) -> [{K, V}]; +parse_kv(Other) -> [{invalid_field, Other}]. -spec enable_node(mongooseim:host_type(), jid:jid(), jid:jid(), pubsub_node(), form()) -> ok | {error, Reason :: term()}. 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 62e40818ce..a63d513f20 100644 --- a/src/event_pusher/mod_event_pusher_push_plugin_defaults.erl +++ b/src/event_pusher/mod_event_pusher_push_plugin_defaults.erl @@ -182,34 +182,31 @@ 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)} ]}. --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(binary(), mod_event_pusher_push:form()) -> exml:element(). +make_form(FormType, Fields) -> + Children = [mongoose_data_forms:form_type_field(FormType) | + [make_form_field(Field) || Field <- Fields]], + mongoose_data_forms:form(Children, <<"submit">>). -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}]}]}. + mongoose_data_forms:form_field(#{var => Name, values => [Value]}). -spec maybe_publish_options(mod_event_pusher_push:form()) -> [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/http_upload/mod_http_upload.erl b/src/http_upload/mod_http_upload.erl index 0baa4eba33..abfdfd7676 100644 --- a/src/http_upload/mod_http_upload.erl +++ b/src/http_upload/mod_http_upload.erl @@ -299,10 +299,10 @@ 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 = [mongoose_data_forms:form_type_field(?NS_HTTP_UPLOAD_030), + mongoose_data_forms:form_field(#{var => <<"max-file-size">>, + values => [MaxFileSizeBin]})], + mongoose_data_forms:form(Fields, <<"result">>). -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 cc1490a4ad..40c6658268 100644 --- a/src/inbox/mod_inbox.erl +++ b/src/inbox/mod_inbox.erl @@ -431,38 +431,25 @@ 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">>}], + FieldSpecs = [#{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">>]}], + Fields = [mongoose_data_forms:form_field(Spec) || Spec <- FieldSpecs], + mongoose_data_forms:form([mongoose_data_forms:form_type_field(?NS_ESL_INBOX) | 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,7 +511,7 @@ expand_limit(Max) -> form_to_params(_, undefined) -> #{ order => desc }; form_to_params(HostType, FormEl) -> - ParsedFields = jlib:parse_xdata_fields(exml_query:subelements(FormEl, <<"field">>)), + ParsedFields = mongoose_data_forms:form_to_kvs(FormEl), ?LOG_DEBUG(#{what => inbox_parsed_form_fields, parsed_fields => ParsedFields}), fields_to_params(HostType, ParsedFields, #{ order => desc }). diff --git a/src/inbox/mod_inbox_entries.erl b/src/inbox/mod_inbox_entries.erl index 74a2a16f93..9610e25d43 100644 --- a/src/inbox/mod_inbox_entries.erl +++ b/src/inbox/mod_inbox_entries.erl @@ -54,14 +54,14 @@ 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">>})]}. + FieldSpecs = + [#{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">>]}], + Fields = lists:map(fun mongoose_data_forms:form_field/1, FieldSpecs), + FormType = mongoose_data_forms:form_type_field(?NS_ESL_INBOX_CONVERSATION), + mongoose_data_forms:form([FormType | 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 b07ed9b2e4..246f1a3bf8 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 4a836cf69c..c991071e99 100644 --- a/src/jlib.erl +++ b/src/jlib.erl @@ -25,7 +25,6 @@ -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"}]). @@ -34,16 +33,12 @@ 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 +194,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 +326,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 99312b5059..a768a00e17 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,18 @@ 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() | invalid. +query_to_map(QueryEl) -> + case mongoose_data_forms:find_form(QueryEl) of + undefined -> + #{}; + Form -> + mongoose_data_forms:form_to_map(Form) + end. + -spec common_lookup_params(exml:element(), non_neg_integer(), non_neg_integer()) -> lookup_params(). common_lookup_params(QueryEl, MaxResultLimit, DefaultResultLimit) -> @@ -177,5 +187,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 31e7494f3d..aedbb80418 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,10 +96,12 @@ 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]). +-import(mongoose_data_forms, [form/1, form_type_field/1, form_field/1]). + %-define(MAM_INLINE_UTILS, true). -ifdef(MAM_INLINE_UTILS). @@ -207,14 +207,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 +613,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 +637,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 maps:get(Name, KVs, undefined) of + [BExtMessID] -> external_binary_to_mess_id(BExtMessID); + undefined -> 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 +677,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). + Children = message_form_fields(Module, HostType, MamNs), + result_query(form(Children), MamNs). message_form_fields(Mod, HostType, MamNs) -> TextSearch = case has_full_text_search(Mod, HostType) of - true -> [form_field(<<"text-single">>, <<"full-text-search">>)]; + true -> [form_field(#{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}]}. + form_field(#{type => <<"jid-single">>, var => <<"with">>}), + form_field(#{type => <<"text-single">>, var => <<"start">>}), + form_field(#{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 670b6afc4e..a5c33decdb 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -595,45 +595,26 @@ 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) -> + case mongoose_data_forms:is_form(El) of + true -> + KVs = mongoose_data_forms:form_to_kvs(El, <<"result">>), + [concat_xdata_fields(KVs)]; + false -> + [] + end 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), +concat_xdata_fields(KVs) -> + {FormType, Res} = lists:foldl(fun({<<"FORM_TYPE">>, [FormType]}, {_, VarFields}) -> + {FormType, VarFields}; + ({Var, Values}, {FormType, VarFields}) -> + NewField = [[V, $<] || V <- [Var | lists:sort(Values)]], + {FormType, [NewField | VarFields]} + end, {<<>>, []}, KVs), [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(_) -> - []. - gb_trees_fold(F, Acc, Tree) -> Iter = gb_trees:iterator(Tree), gb_trees_fold_iter(F, Acc, Iter). diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 2cac489c0d..f3b870498d 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}]}, + EnterNicknameEl = mongoose_data_forms:form_instructions(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)]}]. - + TitleEl = mongoose_data_forms:form_title(TitleText), + NickField = mongoose_data_forms:form_field(#{type => <<"text-single">>, + label => translate:translate(Lang, <<"Nickname">>), + var => <<"nick">>, + values => [Nick]}), + Registered ++ [ClientReqEl, mongoose_data_forms:form([TitleEl, EnterNicknameEl, NickField])]. -spec iq_set_register_info(host_type(), jid:server(), jid:simple_jid() | jid:jid(), nick(), ejabberd:lang()) @@ -1154,40 +1140,39 @@ 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 get_submitted_form(xml:remove_cdata(Els)) of + {ok, XEl} -> + FormType = mongoose_data_forms:form_type(XEl), + process_register(FormType, HostType, MucHost, From, Lang, XEl); + no_form -> {error, mongoose_xmpp_errors:bad_request()} end; _ -> iq_set_unregister_info(HostType, MucHost, From, Lang) end. --spec process_register(XMLNS :: binary(), Type :: binary(), +get_submitted_form([El]) -> + case mongoose_data_forms:is_form(El) andalso mongoose_data_forms:is_submitted(El) of + true -> {ok, El}; + false -> no_form + end; +get_submitted_form(_) -> + no_form. + +-spec process_register(Type :: binary(), HostType :: host_type(), MucHost :: jid:server(), From :: jid:jid(), Lang :: ejabberd:lang(), XEl :: exml:element()) -> {error, exml:element()} | {result, []}. -process_register(?NS_XDATA, <<"cancel">>, _HostType, _Host, _From, _Lang, _XEl) -> +process_register(<<"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()}; +process_register(<<"submit">>, HostType, MucHost, From, Lang, XEl) -> + case mongoose_data_forms:form_to_map(XEl) of + #{<<"nick">> := [Nick]} -> + iq_set_register_info(HostType, MucHost, From, Nick, Lang); _ -> - 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()}. + ErrText = <<"You must fill in field \"Nickname\" in the form">>, + {error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)} + end. -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 2cd59ffa76..c485648e22 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([], <<"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 753e9c54d6..b3e6c7c6aa 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -405,15 +405,20 @@ 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">>)). + has_form(Query) orelse has_destroy(Query). + +has_form(Query) -> + case mongoose_data_forms:find_form(Query) of + undefined -> + undefined; + Form -> + mongoose_data_forms:is_submitted(Form) + end. +has_destroy(Query) -> + exml_query:subelement(Query, <<"destroy">>) =/= undefined. -spec locked_state_process_owner_iq(jid:jid(), exml:element(), ejabberd:lang(), 'error' | 'get' | 'invalid' | 'result', _) @@ -3178,30 +3183,29 @@ 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 {form_type(XEl), StateName} of + {<<"cancel">>, locked_state} -> ?LOG_INFO(ls(#{what => muc_cancel_locked, 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} -> + {<<"cancel">>, normal_state} -> %% received cancel when room was configured - continue without changes {result, [], StateData}; - {?NS_XDATA, <<"submit">>, _} -> - process_authorized_submit_owner(From, XEl, StateData); + {<<"submit">>, _} -> + KVs = mongoose_data_forms:form_to_kvs(XEl), + process_authorized_submit_owner(From, KVs, StateData); _ -> {error, mongoose_xmpp_errors:bad_request()} 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 +3225,32 @@ 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()) -> +form_type(Elem) -> + case mongoose_data_forms:is_form(Elem) of + true -> mongoose_data_forms:form_type(Elem); + false -> undefined + end. + +-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 +3260,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 +3274,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 +3294,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(). @@ -3336,14 +3341,9 @@ get_config(Lang, StateData, From) -> 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}]}]}, + [mongoose_data_forms:form_title(<>), + mongoose_data_forms:form_type_field(?NS_MUC_CONFIG), stringxfield(<<"Room title">>, <<"muc#roomconfig_roomname">>, Config#config.title, Lang), @@ -3420,50 +3420,20 @@ 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(Res)], StateData}. -spec getmemberlist_field(Lang :: ejabberd:lang()) -> exml:element(). 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], + mongoose_data_forms:form_field(#{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 +3444,51 @@ 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], + mongoose_data_forms:form_field(#{type => <<"list-single">>, + label => LabelTxt, + var => <<"muc#roomconfig_maxusers">>, + values => [MaxUsersRoomString], + options => Options}). -spec whois_field(Lang :: ejabberd:lang(), Config :: config()) -> exml:element(). 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">>}], + mongoose_data_forms:form_field(#{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(), @@ -3986,10 +3931,10 @@ 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 +check_voice_approval(From, [XEl], _Lang, StateData) -> + Fields = mongoose_data_forms:form_to_kvs(XEl), + [BRole] = proplists:get_value(<<"muc#role">>, Fields, [undefined]), + case Fields of [_Form, _Role] -> case catch binary_to_role(BRole) of {'EXIT', _} -> {error, mongoose_xmpp_errors:bad_request()}; @@ -3997,31 +3942,15 @@ check_voice_approval(From, [#xmlel{name = <<"x">>, end; _ -> 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}; + proplists:get_value(<<"muc#request_allow">>, Fields), + proplists:get_value(<<"muc#roomnick">>, Fields)} of + {moderator, [<<"true">>], undefined} -> {error, mongoose_xmpp_errors:bad_request()}; + {moderator, [<<"true">>], [RoomNick]} -> {role, BRole, RoomNick}; {moderator, _, _} -> ok; _ -> {error, mongoose_xmpp_errors:not_allowed()} end 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 +4350,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,16 +4565,35 @@ route_nick_iq(#routed_nick_iq{packet = Packet, lang = Lang, nick = ToNick, decode_reason(Elem) -> xml:get_path_s(Elem, [{elem, <<"reason">>}, cdata]). +-spec make_voice_approval_form(From :: jid:simple_jid() | jid:jid(), + Nick :: binary(), Role :: binary()) -> exml:element(). +make_voice_approval_form(From, Nick, Role) -> + FieldSpecs = + [#{var => <<"muc#role">>, type => <<"text-single">>, value => Role, + label => <<"Request role">>}, + #{var => <<"muc#jid">>, type => <<"jid-single">>, value => jid:to_binary(From), + label => <<"User ID">>}, + #{var => <<"muc#roomnick">>, type => <<"text-single">>, value => Nick, + label => <<"Room Nickname">>}, + #{var => <<"muc#request_allow">>, type => <<"boolean">>, value => <<"false">>, + label => <<"Grant voice to this person?">>}], + FormChildren = + [mongoose_data_forms:form_title(<<"Voice request">>), + mongoose_data_forms:form_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.">>), + mongoose_data_forms:form_type_field(?NS_MUC_REQUEST) | + lists:map(fun mongoose_data_forms:form_field/1, FieldSpecs)], + Form = mongoose_data_forms:form(FormChildren), + #xmlel{name = <<"message">>, children = [Form]}. -spec xfield(binary(), any(), binary(), 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}]}]}. - + mongoose_data_forms:form_field(#{type => Type, + label => translate:translate(Lang, Label), + var => Var, + values => [Val]}). -spec boolxfield(any(), binary(), any(), ejabberd:lang()) -> exml:element(). boolxfield(Label, Var, Val, Lang) -> diff --git a/src/mongoose_data_forms.erl b/src/mongoose_data_forms.erl new file mode 100644 index 0000000000..9ffedc0144 --- /dev/null +++ b/src/mongoose_data_forms.erl @@ -0,0 +1,133 @@ +-module(mongoose_data_forms). +-xep([{xep, 4}, {version, "2.13.1"}]). + +%% Form processing +-export([is_form/1, + is_submitted/1, + form_type/1, + find_form/1, find_form/2, + form_to_kvs/1, form_to_kvs/2, + form_to_map/1, form_to_map/2, + form_fields_to_kvs/1]). + +%% Form construction +-export([form/1, form/2, + form_type_field/1, + form_field/1, + form_title/1, + form_instructions/1, + reported_element/1]). + +-include_lib("exml/include/exml.hrl"). +-include("mongoose_ns.hrl"). + +-type field() :: #{var => binary(), type => binary(), label => binary(), + values => [binary()], options => [option()]}. +-type option() :: binary() | {binary(), binary()}. +-type kv_list() :: [{binary(), [binary()]}]. +-type kv_map() :: #{binary() => [binary()]}. + +-export_type([field/0, option/0, kv_list/0, kv_map/0]). + +%% Form processing + +-spec is_form(exml:element()) -> boolean(). +is_form(#xmlel{name = Name} = Elem) -> + Name =:= <<"x">> andalso exml_query:attr(Elem, <<"xmlns">>) =:= ?NS_XDATA. + +-spec is_submitted(exml:element()) -> boolean(). +is_submitted(Form) -> + lists:member(form_type(Form), [<<"submit">>, <<"cancel">>]). + +-spec form_type(exml:element()) -> binary() | undefined. +form_type(Form) -> + exml_query:attr(Form, <<"type">>). + +-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). + +-spec form_to_kvs(exml:element(), binary()) -> kv_list() | invalid. +form_to_kvs(FormEl, Type) -> + case form_type(FormEl) of + Type -> form_to_kvs(FormEl); + _ -> invalid + end. + +-spec form_to_kvs(exml:element()) -> kv_list(). +form_to_kvs(FormEl) -> + form_fields_to_kvs(FormEl#xmlel.children). + +-spec form_to_map(exml:element(), binary()) -> kv_map() | invalid. +form_to_map(FormEl, Type) -> + case form_type(FormEl) of + Type -> form_to_map(FormEl); + _ -> invalid + end. + +-spec form_to_map(exml:element()) -> kv_map(). +form_to_map(FormEl) -> + maps:from_list(form_fields_to_kvs(FormEl#xmlel.children)). + +-spec form_fields_to_kvs([exml:element()]) -> kv_list(). +form_fields_to_kvs(Fields) -> + 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([exml:element()]) -> exml:element(). +form(Children) -> + form(Children, <<"form">>). + +-spec form([exml:element()], binary()) -> exml:element(). +form(Children, Type) -> + #xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, Type}], + children = 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 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 00516289ab..6ea5463dea 100644 --- a/src/mongoose_disco.erl +++ b/src/mongoose_disco.erl @@ -217,26 +217,6 @@ 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}]}]}. + Children = [mongoose_data_forms:form_type_field(NS) | + [mongoose_data_forms:form_field(Field) || Field <- Fields]], + mongoose_data_forms:form(Children, <<"result">>). diff --git a/src/muc_light/mod_muc_light_codec_legacy.erl b/src/muc_light/mod_muc_light_codec_legacy.erl index 156f363f02..40a39749ff 100644 --- a/src/muc_light/mod_muc_light_codec_legacy.erl +++ b/src/muc_light/mod_muc_light_codec_legacy.erl @@ -138,8 +138,7 @@ 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 + try parse_config(mongoose_data_forms:form_to_kvs(mongoose_data_forms:find_form(QueryEl))) of {ok, RawConfig} -> {ok, {set, #config{ id = ID, raw_config = RawConfig }}} catch Class:Reason:Stacktrace -> @@ -191,20 +190,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]) - end. +-spec parse_config([{binary(), [binary()]}]) -> {ok, mod_muc_light_room_config:binary_kv()}. +parse_config(KVs) -> + {ok, lists:flatmap(fun parse_config_kv/1, KVs)}. + +parse_config_kv({<<"FORM_TYPE">>, _}) -> []; +parse_config_kv({K, [V]}) -> [{K, V}]. -spec parse_aff_users(Els :: [jlib:xmlch()]) -> {ok, aff_users()}. parse_aff_users(Els) -> @@ -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}) + ConfigEls = [ mongoose_data_forms:form_field(#{var => K, type => <<"text-single">>, values => [V]}) || {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] }, + FormType = mongoose_data_forms:form_type_field(<<"http://jabber.org/protocol/muc#roomconfig">>), + FormTitle = mongoose_data_forms:form_title(<<"Configuration form for the room">>), + XEl = mongoose_data_forms:form([FormTitle, FormType | ConfigEls]), {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 f2b2c83739..ba2cc80ffe 100644 --- a/src/pubsub/mod_pubsub.erl +++ b/src/pubsub/mod_pubsub.erl @@ -1464,8 +1464,8 @@ 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}]), + Options = exml_query:subelement(QueryEl, <<"options">>), + ConfigXForm = mongoose_data_forms:find_form(Options), JID = xml:get_attr_s(<<"jid">>, SubscribeAttrs), subscribe_node(Host, Node, From, JID, ConfigXForm). @@ -1504,7 +1504,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 +1616,12 @@ 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 = mongoose_data_forms:form_field(#{type => <<"list-single">>, + var => <<"pubsub#node">>, + options => lists:usort(Options)}), + Form = mongoose_data_forms:form([Field]), + adhoc:produce_response(Request, executing, <<"execute">>, [Form]) end. get_pending_nodes(Host, Owner, Plugins) -> @@ -1650,19 +1643,17 @@ 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 +adhoc_get_pending_parse_options(Host, XEl) -> + case mongoose_data_forms:form_to_kvs(XEl, <<"submit">>) of invalid -> + ?LOG_INFO(#{what => pubsub_bad_xform, exml_packet => XEl}), {error, mongoose_xmpp_errors:bad_request()}; - XData2 -> - case set_xoption(Host, XData2, []) of + KVs -> + case set_xoption(Host, 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. %% @doc

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

@@ -1700,88 +1691,49 @@ 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">>}]}]}], + FormChildren = [mongoose_data_forms:form_title( + translate:translate(Lang, <<"PubSub subscriber request">>)), + mongoose_data_forms:form_instructions( + translate:translate(Lang, <<"Choose whether to approve this entity's " + "subscription.">>)), + mongoose_data_forms:form_type_field(?NS_PUBSUB_SUB_AUTH), + mongoose_data_forms:form_field( + #{var => <<"pubsub#node">>, + type => <<"text-single">>, + label => translate:translate(Lang, <<"Node ID">>), + values => [Node]}), + mongoose_data_forms:form_field( + #{var => <<"pubsub#subscriber_jid">>, + type => <<"jid-single">>, + label => translate:translate(Lang, <<"Subscriber Address">>), + values => [jid:to_binary(Subscriber)]}), + mongoose_data_forms:form_field( + #{var => <<"pubsub#allow">>, + type => <<"boolean">>, + label => translate:translate(Lang, <<"Allow this Jabber ID to subscribe to " + "this pubsub node?">>), + values => [<<"false">>]})], Stanza = #xmlel{name = <<"message">>, attrs = [{<<"id">>, mongoose_bin:gen_from_crypto()}], - children = [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = FormChildren}]}, + children = [mongoose_data_forms:form(FormChildren)]}, 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)), + XData = lists:flatmap(fun(El) -> + case mongoose_data_forms:is_form(El) + andalso mongoose_data_forms:form_type(El) =:= <<"submit">> of + true -> + [mongoose_data_forms:form_to_map(El)]; + false -> + [] + end + end, xml:remove_cdata(Els)), case XData of - [] -> - 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 + [] -> none; + [#{<<"FORM_TYPE">> := [?NS_PUBSUB_SUB_AUTH]} = M] -> M; + _ -> invalid end. %% @doc Send a message to JID with the supplied Subscription @@ -1800,12 +1752,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 +1813,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 +1829,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 +1903,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 +1927,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:is_form(XEl) andalso + mongoose_data_forms:form_type(XEl) =:= <<"submit">> of + true -> + KVs = mongoose_data_forms:form_to_kvs(XEl), + case set_xoption(Host, KVs, node_options(Host, Type)) of NewOpts when is_list(NewOpts) -> {result, NewOpts}; Err -> Err - end + end; + false -> + {error, mongoose_xmpp_errors:bad_request()} 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 +3460,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 +3655,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 +3670,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,7 +3773,13 @@ max_items(Host, Options) -> <<"pubsub#", (atom_to_binary(Var, latin1))/binary>>, get_option(Options, Var, []))). -get_configure_xfields(_Type, Options, Lang, Groups) -> +configure_form(Type, Options, Lang, Groups) -> + TypeField = mongoose_data_forms:form_type_field(?NS_PUBSUB_NODE_CONFIG), + Fields = [mongoose_data_forms:form_field(Spec) || + Spec <- get_configure_xfields(Options, Lang, Groups)], + mongoose_data_forms:form([TypeField | Fields], Type). + +get_configure_xfields(Options, Lang, Groups) -> [?XFIELD(<<"hidden">>, <<>>, <<"FORM_TYPE">>, (?NS_PUBSUB_NODE_CONFIG)), ?BOOL_CONFIG_FIELD(<<"Deliver payloads with event notifications">>, deliver_payloads), @@ -3905,17 +3829,26 @@ 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); + case get_form(ActionEl) of + {ok, XEl} -> + case mongoose_data_forms:form_type(XEl) of + <<"cancel">> -> {result, []}; + <<"submit">> -> set_configure_submit(Host, Node, From, XEl, Lang); _ -> {error, mongoose_xmpp_errors:bad_request()} end; - _ -> + no_form -> {error, mongoose_xmpp_errors:bad_request()} end. +get_form(ParentEl) -> + case xml:remove_cdata(ParentEl#xmlel.children) of + [El] -> case mongoose_data_forms:is_form(El) of + true -> {ok, El}; + false -> no_form + end; + _ -> no_form + end. + set_configure_submit(Host, Node, User, XEl, Lang) -> Action = fun(NodeRec) -> set_configure_transaction(Host, User, XEl, NodeRec) @@ -3934,10 +3867,8 @@ set_configure_submit(Host, Node, User, XEl, Lang) -> set_configure_transaction(Host, User, XEl, #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; + KVs = mongoose_data_forms:form_to_kvs(XEl), + set_configure_valid_transaction(Host, NodeRec, KVs); _ -> {error, mongoose_xmpp_errors:forbidden()} end. diff --git a/src/pubsub/node_push.erl b/src/pubsub/node_push.erl index 755ba7e502..e77424be55 100644 --- a/src/pubsub/node_push.erl +++ b/src/pubsub/node_push.erl @@ -111,23 +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 + case mongoose_data_forms:is_form(Form) + andalso mongoose_data_forms:form_type(Form) =:= <<"submit">> of true -> + Fields = mongoose_data_forms:form_to_kvs(Form), + CustomFields = [{K, V} || {K, [V]} <- Fields, K =/= <<"FORM_TYPE">>], maps:from_list(CustomFields); false -> invalid_form diff --git a/src/pubsub/pubsub_form_utils.erl b/src/pubsub/pubsub_form_utils.erl index 938f5bb811..d324940cfa 100644 --- a/src/pubsub/pubsub_form_utils.erl +++ b/src/pubsub/pubsub_form_utils.erl @@ -54,7 +54,7 @@ make_sub_xform(Options) -> parse_sub_xform(undefined) -> {ok, []}; parse_sub_xform(XForm) -> - case jlib:parse_xdata_submit(XForm) of + case mongoose_data_forms:form_to_kvs(XForm, <<"submit">>) of invalid -> {error, invalid_form}; XData -> convert_fields_from_binaries(XData, [], sub_form_options()) 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]}. + mongoose_data_forms:form(XFields, ?NS_PUBSUB_SUB_OPTIONS). -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_field(#{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 %%==================================================================== diff --git a/src/vcard/mod_vcard.erl b/src/vcard/mod_vcard.erl index ce1d429413..77649eb03e 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_res_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,54 @@ 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) -> + %% This is not in the form, but the helper function can be reused + mongoose_data_forms:form_instructions( + translate:translate(Lang, <<"You need an x:data capable client to search">>)). + +search_res_form(JID, SearchFields, Lang) -> + Contents = + [mongoose_data_forms:form_title( + <<(translate:translate(Lang, <<"Search users in ">>))/binary, + (jid:to_binary(JID))/binary>>), + mongoose_data_forms:form_instructions( + 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), + mongoose_data_forms:form(Contents, <<"result">>). + 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 + case mongoose_data_forms:form_to_kvs(XDataEl, <<"submit">>) 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), + 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)) end end. make_search_result_iq(IQ, SearchResult, RSMOutEls) -> + Form = mongoose_data_forms:form(SearchResult, <<"result">>), 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 +600,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]. @@ -723,7 +721,7 @@ apply_rsm_to_search_results([], _, #rsm_out{} = RSMOut1) -> search_result_get_jid(#xmlel{name = <<"item">>, children = Children}) -> - Fields = jlib:parse_xdata_fields(Children), + Fields = mongoose_data_forms:form_fields_to_kvs(Children), case lists:keysearch(<<"jid">>, 1, Fields) of {value, {<<"jid">>, JID}} -> {ok, list_to_binary(JID)}; diff --git a/src/vcard/mod_vcard_ldap.erl b/src/vcard/mod_vcard_ldap.erl index 9070f66a3b..fcd5130249 100644 --- a/src/vcard/mod_vcard_ldap.erl +++ b/src/vcard/mod_vcard_ldap.erl @@ -170,16 +170,9 @@ search_fields(HostType, LServer) -> 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)}. + Fields = [?TLFIELD(<<"text-single">>, Name, Value) || + {Name, Value} <- [{<<"Jabber ID">>, <<"jid">>} | SearchReported]], + mongoose_data_forms:reported_element(Fields). %%-------------------------------------------------------------------- %% API diff --git a/src/vcard/mod_vcard_mnesia.erl b/src/vcard/mod_vcard_mnesia.erl index 8ff8c0fd5d..a1338045de 100644 --- a/src/vcard/mod_vcard_mnesia.erl +++ b/src/vcard/mod_vcard_mnesia.erl @@ -183,7 +183,7 @@ record_to_item(R) -> {User, Server} = R#vcard_search.user, #xmlel{name = <<"item">>, children = [ - ?FIELD(<<"jid">>, [User, <<"@">>, Server]), + ?FIELD(<<"jid">>, <>), ?FIELD(<<"fn">>, (R#vcard_search.fn)), ?FIELD(<<"last">>, (R#vcard_search.family)), ?FIELD(<<"first">>, (R#vcard_search.given)),