diff --git a/doc/guide.tex b/doc/guide.tex index 044fab8..25598ca 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -3864,6 +3864,15 @@ It also supports Roster Versioning (\xepref{0237}). Options: \begin{description} \iqdiscitem{Roster Management (\ns{jabber:iq:roster})} + \titem{\{managers, [Domainname]\}} \ind{options!managers} + List of components (or servers) that can manage users rosters using + \footahref{http://jkaluza.fedorapeople.org/remote-roster.html}{XEP-xxxx: Remote Roster Management}. + The protocol sections implemented are: + \term{2.3. Component requests user's roster}, + \term{2.4. Client sends roster update}, + \term{2.5. Component sends roster update}. + A component only gets or can modify roster items that have the same domain as the component. + Default value is: \term{[]}. \titem{\{versioning, false|true\}} \ind{options!versioning}Enables Roster Versioning. This option is disabled by default. @@ -3877,12 +3886,13 @@ Options: Important: if you use \modsharedroster, you must disable this option. \end{description} -This example configuration enables Roster Versioning with storage of current id: +This example configuration enables Roster Versioning with storage of current id. +The ICQ and MSN transports can get ICQ and MSN contacts, add them, or remove them for any local account: \begin{verbatim} {modules, [ ... - {mod_roster, [{versioning, true}, {store_current_id, true}]}, + {mod_roster, [{versioning, true}, {store_current_id, true}, {["icq.example.org", "msn.example.org"]} ]}, ... ]}. \end{verbatim} diff --git a/src/mod_roster.erl b/src/mod_roster.erl index 90c4601..f837ba4 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -123,6 +123,8 @@ stop(Host) -> ?MODULE, webadmin_user, 50), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_ROSTER). +process_iq(From, To, IQ) when ((From#jid.luser == "") andalso (From#jid.lresource == "")) -> + process_iq_manager(From, To, IQ); process_iq(From, To, IQ) -> #iq{sub_el = SubEl} = IQ, @@ -281,12 +283,13 @@ item_to_xml(Item) -> {xmlelement, "item", Attrs4, SubEls}. -process_iq_set(From, To, #iq{sub_el = SubEl} = IQ) -> +process_iq_set(From, To, #iq{sub_el = SubEl, id = Id} = IQ) -> {xmlelement, _Name, _Attrs, Els} = SubEl, - lists:foreach(fun(El) -> process_item_set(From, To, El) end, Els), + Managed = is_managed_from_id(Id), + lists:foreach(fun(El) -> process_item_set(From, To, El, Managed) end, Els), IQ#iq{type = result, sub_el = []}. -process_item_set(From, To, {xmlelement, _Name, Attrs, Els}) -> +process_item_set(From, To, {xmlelement, _Name, Attrs, Els}, Managed) -> JID1 = jlib:string_to_jid(xml:get_attr_s("jid", Attrs)), #jid{user = User, luser = LUser, lserver = LServer} = From, case JID1 of @@ -308,7 +311,8 @@ process_item_set(From, To, {xmlelement, _Name, Attrs, Els}) -> groups = [], xs = []} end, - Item1 = process_item_attrs(Item, Attrs), +% Item11 = process_item_attrs(Item, Attrs), + Item1 = process_item_attrs_managed(Item, Attrs, Managed), Item2 = process_item_els(Item1, Els), case Item2#roster.subscription of remove -> @@ -316,6 +320,7 @@ process_item_set(From, To, {xmlelement, _Name, Attrs, Els}) -> _ -> mnesia:write(Item2) end, + send_itemset_to_managers(From, Item2, Managed), %% If the item exist in shared roster, take the %% subscription information from there: Item3 = ejabberd_hooks:run_fold(roster_process_item, @@ -341,7 +346,7 @@ process_item_set(From, To, {xmlelement, _Name, Attrs, Els}) -> ok end end; -process_item_set(_From, _To, _) -> +process_item_set(_From, _To, _, _Managed) -> ok. process_item_attrs(Item, [{Attr, Val} | Attrs]) -> @@ -1098,3 +1103,89 @@ us_to_list({User, Server}) -> webadmin_user(Acc, _User, _Server, Lang) -> Acc ++ [?XE("h3", [?ACT("roster/", "Roster")])]. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% Implement http://jkaluza.fedorapeople.org/remote-roster.html#sect-id188012 + +%% Handle 2.3 and 2.5 + +process_iq_manager(From, To, IQ) -> + %% Check what access is allowed for From to To + MatchDomain = From#jid.lserver, + case is_domain_managed(MatchDomain, To#jid.lserver) of + true -> + process_iq_manager2(MatchDomain, To, IQ); + false -> + #iq{sub_el = SubEl} = IQ, + IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} + end. + +process_iq_manager2(MatchDomain, To, IQ) -> + %% If IQ is SET, filter the input IQ + IQFiltered = maybe_filter_request(MatchDomain, IQ), + %% Call the standard function with reversed JIDs + IdInitial = IQFiltered#iq.id, + ResIQ = process_iq(To, To, IQFiltered#iq{id = "roster-remotely-managed"}), + %% Filter the output IQ + filter_stanza(MatchDomain, ResIQ#iq{id = IdInitial}). + +is_domain_managed(ContactHost, UserHost) -> + Managers = gen_mod:get_module_opt(UserHost, ?MODULE, managers, []), + lists:member(ContactHost, Managers). + +maybe_filter_request(MatchDomain, IQ) when IQ#iq.type == set -> + filter_stanza(MatchDomain, IQ); +maybe_filter_request(_MatchDomain, IQ) -> + IQ. + +filter_stanza(_MatchDomain, #iq{sub_el = []} = IQ) -> + IQ; +filter_stanza(MatchDomain, #iq{sub_el = [SubEl | _]} = IQ) -> + #iq{sub_el = SubElFiltered} = IQRes = + filter_stanza(MatchDomain, IQ#iq{sub_el = SubEl}), + IQRes#iq{sub_el = [SubElFiltered]}; +filter_stanza(MatchDomain, #iq{sub_el = SubEl} = IQ) -> + {xmlelement, Type, Attrs, Items} = SubEl, + ItemsFiltered = lists:filter( + fun(Item) -> + is_item_of_domain(MatchDomain, Item) end, Items), + SubElFiltered = {xmlelement, Type, Attrs, ItemsFiltered}, + IQ#iq{sub_el = SubElFiltered}. + +is_item_of_domain(MatchDomain, {xmlelement, _, Attrs, _}) -> + lists:any(fun(Attr) -> is_jid_of_domain(MatchDomain, Attr) end, Attrs); +is_item_of_domain(_MatchDomain, {xmlcdata, _}) -> + false. + +is_jid_of_domain(MatchDomain, {"jid", JIDString}) -> + case jlib:string_to_jid(JIDString) of + JID when JID#jid.lserver == MatchDomain -> true; + _ -> false + end; +is_jid_of_domain(_, _) -> + false. + +%% Handle 2.5 +process_item_attrs_managed(Item, Attrs, true) -> + process_item_attrs_ws(Item, Attrs); +process_item_attrs_managed(Item, _Attrs, false) -> + process_item_attrs(Item, _Attrs). + +%% Handle 2.4 +send_itemset_to_managers(_From, _Item, true) -> + ok; +send_itemset_to_managers(From, Item, false) -> + {_, UserHost} = Item#roster.us, + {_ContactUser, ContactHost, _ContactResource} = Item#roster.jid, + + %% Check if the component is an allowed manager + IsManager = is_domain_managed(ContactHost, UserHost), + case IsManager of + true -> push_item("", ContactHost, "", From, Item); + false -> ok + end. + +is_managed_from_id("roster-remotely-managed") -> + true; +is_managed_from_id(_Id) -> + false. diff --git a/src/mod_roster_odbc.erl b/src/mod_roster_odbc.erl index 4159614..1f5e83f 100644 --- a/src/mod_roster_odbc.erl +++ b/src/mod_roster_odbc.erl @@ -113,6 +113,8 @@ stop(Host) -> ?MODULE, webadmin_user, 50), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_ROSTER). +process_iq(From, To, IQ) when ((From#jid.luser == "") andalso (From#jid.lresource == "")) -> + process_iq_manager(From, To, IQ); process_iq(From, To, IQ) -> #iq{sub_el = SubEl} = IQ, @@ -306,12 +308,13 @@ item_to_xml(Item) -> {xmlelement, "item", Attrs, SubEls}. -process_iq_set(From, To, #iq{sub_el = SubEl} = IQ) -> +process_iq_set(From, To, #iq{sub_el = SubEl, id = Id} = IQ) -> {xmlelement, _Name, _Attrs, Els} = SubEl, - lists:foreach(fun(El) -> process_item_set(From, To, El) end, Els), + Managed = is_managed_from_id(Id), + lists:foreach(fun(El) -> process_item_set(From, To, El, Managed) end, Els), IQ#iq{type = result, sub_el = []}. -process_item_set(From, To, {xmlelement, _Name, Attrs, Els}) -> +process_item_set(From, To, {xmlelement, _Name, Attrs, Els}, Managed) -> JID1 = jlib:string_to_jid(xml:get_attr_s("jid", Attrs)), #jid{user = User, luser = LUser, lserver = LServer} = From, case JID1 of @@ -347,7 +350,8 @@ process_item_set(From, To, {xmlelement, _Name, Attrs, Els}) -> name = ""} end end, - Item1 = process_item_attrs(Item, Attrs), +% Item11 = process_item_attrs(Item, Attrs), + Item1 = process_item_attrs_managed(Item, Attrs, Managed), Item2 = process_item_els(Item1, Els), case Item2#roster.subscription of remove -> @@ -357,6 +361,7 @@ process_item_set(From, To, {xmlelement, _Name, Attrs, Els}) -> ItemGroups = groups_to_string(Item2), odbc_queries:update_roster(LServer, Username, SJID, ItemVals, ItemGroups) end, + send_itemset_to_managers(From, Item2, Managed), %% If the item exist in shared roster, take the %% subscription information from there: Item3 = ejabberd_hooks:run_fold(roster_process_item, @@ -382,7 +387,7 @@ process_item_set(From, To, {xmlelement, _Name, Attrs, Els}) -> ok end end; -process_item_set(_From, _To, _) -> +process_item_set(_From, _To, _, _Managed) -> ok. process_item_attrs(Item, [{Attr, Val} | Attrs]) -> @@ -1197,3 +1202,90 @@ us_to_list({User, Server}) -> webadmin_user(Acc, _User, _Server, Lang) -> Acc ++ [?XE("h3", [?ACT("roster/", "Roster")])]. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% Implement http://jkaluza.fedorapeople.org/remote-roster.html#sect-id188012 + +%% Handle 2.3 and 2.5 + +process_iq_manager(From, To, IQ) -> + %% Check what access is allowed for From to To + MatchDomain = From#jid.lserver, + case is_domain_managed(MatchDomain, To#jid.lserver) of + true -> + process_iq_manager2(MatchDomain, To, IQ); + false -> + #iq{sub_el = SubEl} = IQ, + IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} + end. + +process_iq_manager2(MatchDomain, To, IQ) -> + %% If IQ is SET, filter the input IQ + IQFiltered = maybe_filter_request(MatchDomain, IQ), + %% Call the standard function with reversed JIDs + IdInitial = IQFiltered#iq.id, + ResIQ = process_iq(To, To, IQFiltered#iq{id = "roster-remotely-managed"}), + %% Filter the output IQ + filter_stanza(MatchDomain, ResIQ#iq{id = IdInitial}). + +is_domain_managed(ContactHost, UserHost) -> + Managers = gen_mod:get_module_opt(UserHost, ?MODULE, managers, []), + lists:member(ContactHost, Managers). + +maybe_filter_request(MatchDomain, IQ) when IQ#iq.type == set -> + filter_stanza(MatchDomain, IQ); +maybe_filter_request(_MatchDomain, IQ) -> + IQ. + +filter_stanza(_MatchDomain, #iq{sub_el = []} = IQ) -> + IQ; +filter_stanza(MatchDomain, #iq{sub_el = [SubEl | _]} = IQ) -> + #iq{sub_el = SubElFiltered} = IQRes = + filter_stanza(MatchDomain, IQ#iq{sub_el = SubEl}), + IQRes#iq{sub_el = [SubElFiltered]}; +filter_stanza(MatchDomain, #iq{sub_el = SubEl} = IQ) -> + {xmlelement, Type, Attrs, Items} = SubEl, + ItemsFiltered = lists:filter( + fun(Item) -> + is_item_of_domain(MatchDomain, Item) end, Items), + SubElFiltered = {xmlelement, Type, Attrs, ItemsFiltered}, + IQ#iq{sub_el = SubElFiltered}. + +is_item_of_domain(MatchDomain, {xmlelement, _, Attrs, _}) -> + lists:any(fun(Attr) -> is_jid_of_domain(MatchDomain, Attr) end, Attrs); +is_item_of_domain(_MatchDomain, {xmlcdata, _}) -> + false. + +is_jid_of_domain(MatchDomain, {"jid", JIDString}) -> + case jlib:string_to_jid(JIDString) of + JID when JID#jid.lserver == MatchDomain -> true; + _ -> false + end; +is_jid_of_domain(_, _) -> + false. + +%% Handle 2.5 +process_item_attrs_managed(Item, Attrs, true) -> + process_item_attrs_ws(Item, Attrs); +process_item_attrs_managed(Item, _Attrs, false) -> + process_item_attrs(Item, _Attrs). + +%% Handle 2.4 +send_itemset_to_managers(_From, _Item, true) -> + ok; +send_itemset_to_managers(From, Item, false) -> + {_, UserHost} = Item#roster.us, + {_ContactUser, ContactHost, _ContactResource} = Item#roster.jid, + + %% Check if the component is an allowed manager + IsManager = is_domain_managed(ContactHost, UserHost), + case IsManager of + true -> push_item("", ContactHost, "", From, Item); + false -> ok + end. + +is_managed_from_id("roster-remotely-managed") -> + true; +is_managed_from_id(_Id) -> + false.