From bc6e56dce5014d6af24b99d0366c44b0b9a55dd9 Mon Sep 17 00:00:00 2001 From: Piotr Nosek Date: Wed, 26 Jun 2019 01:07:43 +0200 Subject: [PATCH 1/2] Add GDPR removal for pubsub Tests by @NelsonVides and @ludwikbukowski --- big_tests/tests/gdpr_SUITE.erl | 283 ++++++++++++++++++++++--- big_tests/tests/pubsub_tools.erl | 22 +- src/pubsub/gen_pubsub_node.erl | 5 +- src/pubsub/mod_pubsub.erl | 69 +++--- src/pubsub/mod_pubsub_db.erl | 13 +- src/pubsub/mod_pubsub_db_mnesia.erl | 32 ++- src/pubsub/mod_pubsub_db_rdbms.erl | 20 +- src/pubsub/mod_pubsub_db_rdbms_sql.erl | 16 ++ src/pubsub/node_flat.erl | 5 +- src/pubsub/node_pep.erl | 5 +- src/pubsub/node_push.erl | 4 +- src/pubsub/pubsub_index.erl | 3 +- 12 files changed, 407 insertions(+), 70 deletions(-) diff --git a/big_tests/tests/gdpr_SUITE.erl b/big_tests/tests/gdpr_SUITE.erl index 4f08f65a800..a9b6ef60ee9 100644 --- a/big_tests/tests/gdpr_SUITE.erl +++ b/big_tests/tests/gdpr_SUITE.erl @@ -46,7 +46,13 @@ retrieve_inbox_muc/1, remove_inbox_muclight/1, remove_inbox_muc/1, - retrieve_logs/1 + retrieve_logs/1, + remove_pubsub_all_data/1, + remove_pubsub_dont_remove_node_when_only_publisher/1, + remove_pubsub_subscriptions/1, + remove_pubsub_dont_remove_flat_pubsub_node/1, + remove_pubsub_push_node/1, + remove_pubsub_pep_node/1 ]). -export([ data_is_not_retrieved_for_missing_user/1 @@ -114,7 +120,7 @@ groups() -> retrieve_offline, retrieve_logs, retrieve_roster, - retrieve_all_pubsub_data, + {group, retrieve_personal_data_pubsub}, retrieve_multiple_private_xmls, {group, retrieve_personal_data_mam} ]}, @@ -148,8 +154,16 @@ groups() -> {remove_personal_data_mam_rdbms, [], mam_removal_testcases()}, {remove_personal_data_mam_riak, [], mam_removal_testcases()}, {remove_personal_data_mam_cassandra, [], mam_removal_testcases()}, - {remove_personal_data_mam_elasticsearch, [], mam_removal_testcases()}]. - + {remove_personal_data_mam_elasticsearch, [], mam_removal_testcases()}, + {remove_personal_data_pubsub, [], [ + remove_pubsub_subscriptions, + remove_pubsub_dont_remove_node_when_only_publisher, + remove_pubsub_dont_remove_flat_pubsub_node, + remove_pubsub_push_node, + remove_pubsub_pep_node, + remove_pubsub_all_data + ]} + ]. removal_testcases() -> [ @@ -160,6 +174,7 @@ removal_testcases() -> remove_multiple_private_xmls, dont_remove_other_user_private_xml, {group, remove_personal_data_inbox}, + {group, remove_personal_data_pubsub}, {group, remove_personal_data_mam} ]. @@ -201,16 +216,18 @@ end_per_suite(Config) -> gdpr_removal_for_disabled_modules(Flag) -> mongoose_helper:successful_rpc(ejabberd_config, add_global_option, [gdpr_removal_for_disabled_modules, Flag]). +init_per_group(GN, Config) when GN =:= retrieve_personal_data; + GN =:= remove_personal_data -> + lists:keystore(disable_module, 1, Config, {disable_module, false}); init_per_group(GN, Config) when GN =:= retrieve_personal_data_with_mods_disabled; GN =:= remove_personal_data_with_mods_disabled-> - dynamic_modules:ensure_modules(domain(), pubsub_required_modules()), - [{disable_module, true} | Config]; -init_per_group(retrieve_personal_data_pubsub, Config) -> - dynamic_modules:ensure_modules(domain(), pubsub_required_modules()), - Config; + lists:keystore(disable_module, 1, Config, {disable_module, true}); init_per_group(GN, Config) when GN =:= remove_personal_data_mam_rdbms; GN =:= retrieve_personal_data_mam_rdbms -> try_backend_for_mam(Config, rdbms); +init_per_group(GN, Config) when GN =:= retrieve_personal_data_pubsub; + GN =:= remove_personal_data_pubsub -> + [{group, GN} | Config]; init_per_group(GN, Config) when GN =:= retrieve_personal_data_mam_riak; GN =:= remove_personal_data_mam_riak -> try_backend_for_mam(Config, riak); @@ -243,6 +260,11 @@ is_backend_enabled(cassandra) -> mam_helper:is_cassandra_enabled(domain()); is_backend_enabled(elasticsearch) -> mam_helper:is_elasticsearch_enabled(domain()). +init_per_testcase(retrieve_logs = CN, Config) -> + case is_mim2_started() of + false -> {skip, not_running_in_distributed}; + _ -> escalus:init_per_testcase(CN, Config) + end; init_per_testcase(remove_offline = CN, Config) -> offline_started(), escalus:init_per_testcase(CN, Config); @@ -303,6 +325,14 @@ init_per_testcase(remove_roster = CN, Config) -> dynamic_modules:ensure_modules(domain(), [{mod_roster, [{backend, Backend}]}]), escalus:init_per_testcase(CN, Config); init_per_testcase(CN, Config) -> + GN = proplists:get_value(group, Config), + IsPubSub = lists:member(GN, [retrieve_personal_data_pubsub, remove_personal_data_pubsub]), + case IsPubSub of + true -> + dynamic_modules:ensure_modules(domain(), pubsub_required_modules()); + _ -> + ok + end, escalus:init_per_testcase(CN, Config). @@ -400,14 +430,23 @@ offline_required_modules() -> [{mod_offline, [{backend, pick_enabled_backend()}]}]. pubsub_required_modules() -> + pubsub_required_modules([<<"flat">>, <<"pep">>, <<"push">>]). +pubsub_required_modules(Plugins) -> [{mod_caps, []}, {mod_pubsub, [ {backend, mongoose_helper:mnesia_or_rdbms_backend()}, {host, "pubsub.@HOST@"}, {nodetree, <<"tree">>}, - {plugins, [<<"flat">>, <<"pep">>, <<"push">>]} + {plugins, Plugins} ] }]. +is_mim2_started() -> + Node = distributed_helper:mim2(), + case net_adm:ping(Node) of + pong -> true; + _ -> false + end. + vcard_started() -> dynamic_modules:ensure_modules(domain(), vcard_required_modules()). @@ -1066,8 +1105,7 @@ remove_offline(Config) -> retrieve_pubsub_payloads(Config) -> escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> - Node1 = {_Domain, NodeName1} = pubsub_tools:pubsub_node(), - Node2 = {_Domain, NodeName2} = pubsub_tools:pubsub_node(), + [Node1={_,NodeName1}, Node2={_,NodeName2}] = pubsub_tools:create_node_names(2), {BinItem1, StringItem1} = item_content(<<"Item1Data">>), {BinItem2, StringItem2} = item_content(<<"Item2Data">>), {BinItem3, StringItem3} = item_content(<<"Item3Data">>), @@ -1091,13 +1129,13 @@ retrieve_pubsub_payloads(Config) -> dont_retrieve_other_user_pubsub_payload(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> - Node1 = {_Domain, NodeName1} = pubsub_tools:pubsub_node(), - pubsub_tools:create_node(Alice, Node1, []), - AffChange = [{Bob, <<"publish-only">>}], + [Node1={_,NodeName1}] = pubsub_tools:create_node_names(1), + pubsub_tools:create_nodes([{Alice, Node1, []}]), {BinItem1, StringItem1} = item_content(<<"Item1Data">>), {BinItem2, StringItem2} = item_content(<<"Item2Data">>), + AffChange = [{Bob, <<"publish-only">>}], pubsub_tools:set_affiliations(Alice, Node1, AffChange, []), pubsub_tools:publish(Alice, <<"Item1">>, Node1, [{with_payload, {true, BinItem1}}]), pubsub_tools:publish(Bob, <<"Item2">>, Node1, [{with_payload, {true, BinItem2}}]), @@ -1115,18 +1153,19 @@ dont_retrieve_other_user_pubsub_payload(Config) -> retrieve_created_pubsub_nodes(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> - Node1 = {_Domain, NodeName1} = {pubsub_tools:node_addr(), <<"node1">>}, - Node2 = {_Domain, NodeName2} = {pubsub_tools:node_addr(), <<"node2">>}, - Node3 = {_Domain, NodeName3} = {pubsub_tools:node_addr(), <<"node3">>}, + [Node1={_,NodeName1}, Node2={_,NodeName2}, Node3={_,NodeName3}] = + pubsub_tools:create_node_names(3), NodeNS = <<"myns">>, PepNode = make_pep_node_info(Alice, NodeNS), AccessModel = {<<"pubsub#access_model">>, <<"authorize">>}, - pubsub_tools:create_node(Alice, Node1, []), - pubsub_tools:create_node(Alice, Node2, []), - pubsub_tools:create_node(Alice, PepNode, [{config, [AccessModel]}]), - pubsub_tools:create_node(Bob, Node3, [{type, <<"push">>}]), + pubsub_tools:create_nodes([ + {Alice, Node1, []}, + {Alice, Node2, []}, + {Alice, PepNode, [{config, [AccessModel]}]}, + {Bob, Node3, [{type, <<"push">>}]} + ]), ExpectedHeader = ["node_name", "type"], @@ -1145,6 +1184,28 @@ retrieve_created_pubsub_nodes(Config) -> [pubsub_tools:delete_node(User, Node, []) || {User, Node} <- Nodes] end). +remove_pubsub_subscriptions(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + Node = pubsub_tools:pubsub_node(), + pubsub_tools:create_node(Alice, Node, []), + pubsub_tools:subscribe(Bob, Node, []), + + BobU = escalus_utils:jid_to_lower(escalus_client:username(Bob)), + BobS = escalus_utils:jid_to_lower(escalus_client:server(Bob)), + maybe_stop_and_unload_module(mod_pubsub, mod_pubsub_db_backend, Config), + {0, _} = unregister(Bob, Config), + + mongoose_helper:wait_until( + fun() -> + mongoose_helper:successful_rpc(mod_pubsub, get_personal_data, + [BobU, BobS]) + end, + [{pubsub_payloads,["node_name","item_id","payload"],[]}, + {pubsub_nodes,["node_name","type"],[]}, + {pubsub_subscriptions,["node_name"],[]}] + ) + end). + retrieve_pubsub_subscriptions(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> Node = {_Domain, NodeName} = pubsub_tools:pubsub_node(), @@ -1156,14 +1217,180 @@ retrieve_pubsub_subscriptions(Config) -> pubsub_tools:delete_node(Alice, Node, []) end). +remove_pubsub_dont_remove_flat_pubsub_node(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + Node1 = {_,NodeName} = pubsub_tools:pubsub_node(1), + pubsub_tools:create_nodes([{Alice, Node1, []}]), + + maybe_stop_and_unload_module(mod_pubsub, mod_pubsub_db_backend, Config), + {0, _} = unregister(Alice, Config), + + AliceU = escalus_utils:jid_to_lower(escalus_client:username(Alice)), + AliceS = escalus_utils:jid_to_lower(escalus_client:server(Alice)), + mongoose_helper:wait_until( + fun() -> + mongoose_helper:successful_rpc(mod_pubsub, get_personal_data, + [AliceU, AliceS]) + end, + [{pubsub_payloads,["node_name","item_id","payload"],[]}, + {pubsub_nodes,["node_name","type"],[[NodeName, <<"flat">>]]}, + {pubsub_subscriptions,["node_name"],[]}] + ) + end). + +remove_pubsub_push_node(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + [Node] = pubsub_tools:create_node_names(1), + pubsub_tools:create_nodes([{Alice, Node, [{type, <<"push">>}]}]), + + Content = [ + {<<"message-count">>, <<"1">>}, + {<<"last-message-sender">>, <<"senderId">>}, + {<<"last-message-body">>, <<"message body">>} + ], + Options = [ + {<<"device_id">>, <<"sometoken">>}, + {<<"service">>, <<"apns">>} + ], + + PublishIQ = push_pubsub_SUITE:publish_iq(Bob, Node, Content, Options), + escalus:send(Bob, PublishIQ), + escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)), + + maybe_stop_and_unload_module(mod_pubsub, mod_pubsub_db_backend, Config), + {0, _} = unregister(Alice, Config), + + AliceU = escalus_utils:jid_to_lower(escalus_client:username(Alice)), + AliceS = escalus_utils:jid_to_lower(escalus_client:server(Alice)), + mongoose_helper:wait_until( + fun() -> + mongoose_helper:successful_rpc(mod_pubsub, get_personal_data, + [AliceU, AliceS]) + end, + [{pubsub_payloads,["node_name","item_id","payload"],[]}, + {pubsub_nodes,["node_name","type"],[]}, + {pubsub_subscriptions,["node_name"],[]}]) + end). + +remove_pubsub_pep_node(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + NodeName = <<"myns">>, + PepNode = make_pep_node_info(Alice, NodeName), + + pubsub_tools:create_nodes([ + {Alice, PepNode, []} + ]), + + maybe_stop_and_unload_module(mod_pubsub, mod_pubsub_db_backend, Config), + {0, _} = unregister(Alice, Config), + + AliceU = escalus_utils:jid_to_lower(escalus_client:username(Alice)), + AliceS = escalus_utils:jid_to_lower(escalus_client:server(Alice)), + mongoose_helper:wait_until( + fun() -> + mongoose_helper:successful_rpc(mod_pubsub, get_personal_data, + [AliceU, AliceS]) + end, + [{pubsub_payloads,["node_name","item_id","payload"],[]}, + {pubsub_nodes,["node_name","type"],[]}, + {pubsub_subscriptions,["node_name"],[]}] + ) + end). + +remove_pubsub_dont_remove_node_when_only_publisher(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + Node1 = {_,NodeName} = pubsub_tools:pubsub_node(1), + pubsub_tools:create_nodes([{Alice, Node1, []}]), + + AffChange = [{Bob, <<"publish-only">>}], + pubsub_tools:set_affiliations(Alice, Node1, AffChange, []), + + maybe_stop_and_unload_module(mod_pubsub, mod_pubsub_db_backend, Config), + {0, _} = unregister(Bob, Config), + + AliceU = escalus_utils:jid_to_lower(escalus_client:username(Alice)), + AliceS = escalus_utils:jid_to_lower(escalus_client:server(Alice)), + mongoose_helper:wait_until( + fun() -> + mongoose_helper:successful_rpc(mod_pubsub, get_personal_data, + [AliceU, AliceS]) + end, + [{pubsub_payloads,["node_name","item_id","payload"],[]}, + {pubsub_nodes,["node_name","type"],[[NodeName, <<"flat">>]]}, + {pubsub_subscriptions,["node_name"],[]}] + ) + end). + +remove_pubsub_all_data(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + [Node1={_,Name1}, Node2={_,Name2}, Node3={_,Name3}, Node4={_,Name4}] + = pubsub_tools:create_node_names(4), + PepNode = make_pep_node_info(Alice, <<"myns">>), + pubsub_tools:create_nodes([ + {Alice, Node1, []}, + {Alice, Node2, []}, + {Alice, PepNode, []}, + {Bob, Node3, []}, + {Bob, Node4, [{type, <<"push">>}]} + ]), + + AffChange = [{Bob, <<"publish-only">>}], + pubsub_tools:set_affiliations(Alice, Node1, AffChange, []), + pubsub_tools:subscribe(Bob, Node2, []), + pubsub_tools:subscribe(Alice, Node3, []), + + {BinItem1, _} = item_content(<<"Item1Data">>), + {BinItem2, _} = item_content(<<"Item2Data">>), + {BinItem3, _} = item_content(<<"Item3Data">>), + {BinItem4, _} = item_content(<<"Item4Data">>), + AliceToNode1 = <<"Alice publishes to Node1, but nobody is subscribed">>, + AliceToNode2 = <<"Alice published to Node2, so Bob receives it">>, + BobToNode1 = <<"Bob publishes to Node1, but nobody is subscribed">>, + BobToNode3 = <<"Bob publishes to Node3, so Alice receives it">>, + + pubsub_tools:publish(Alice, AliceToNode1, Node1, [{with_payload, {true, BinItem1}}]), + + pubsub_tools:publish(Alice, AliceToNode2, Node2, [{with_payload, {true, BinItem2}}]), + pubsub_tools:receive_item_notification(Bob, AliceToNode2, Node2, []), + + pubsub_tools:publish(Bob, BobToNode1, Node1, [{with_payload, {true, BinItem3}}]), + + pubsub_tools:publish(Bob, BobToNode3, Node3, [{with_payload, {true, BinItem4}}]), + pubsub_tools:receive_item_notification(Alice, BobToNode3, Node3, []), + + maybe_stop_and_unload_module(mod_pubsub, mod_pubsub_db_backend, Config), + {0, _} = unregister(Alice, Config), + + AliceU = escalus_utils:jid_to_lower(escalus_client:username(Alice)), + AliceS = escalus_utils:jid_to_lower(escalus_client:server(Alice)), + [{pubsub_payloads,["node_name","item_id","payload"], AlicePayloads}, + {pubsub_nodes,["node_name","type"], AliceNodes}, + {pubsub_subscriptions, ["node_name"], []}] = mongoose_helper:successful_rpc(mod_pubsub, get_personal_data, [AliceU, AliceS]), + XmlBinItem1 = exml:to_binary(BinItem1), + XmlBinItem2 = exml:to_binary(BinItem2), + [[Name1, AliceToNode1, XmlBinItem1], + [Name2, AliceToNode2, XmlBinItem2]] = lists:sort(AlicePayloads), + [[Name1, <<"flat">>], [Name2, <<"flat">>]] = lists:sort(AliceNodes), + + BobU = escalus_utils:jid_to_lower(escalus_client:username(Bob)), + BobS = escalus_utils:jid_to_lower(escalus_client:server(Bob)), + [{pubsub_payloads,["node_name","item_id","payload"], Payloads}, + {pubsub_nodes,["node_name","type"], Nodes}, + {pubsub_subscriptions, ["node_name"], Subs}] = mongoose_helper:successful_rpc(mod_pubsub, get_personal_data, [BobU, BobS]), + XmlBinItem3 = exml:to_binary(BinItem3), + XmlBinItem4 = exml:to_binary(BinItem4), + [[Name1, BobToNode1, XmlBinItem3], + [Name3, BobToNode3, XmlBinItem4]] = lists:sort(Payloads), + [[Name3, <<"flat">>], [Name4, <<"push">>]] = lists:sort(Nodes), + [[Name2]] = Subs + end). + retrieve_all_pubsub_data(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> - Node1 = {_Domain, NodeName1} = {pubsub_tools:node_addr(), <<"node1xx">>}, - Node2 = {_Domain, NodeName2} = {pubsub_tools:node_addr(), <<"node2xx">>}, - Node3 = {_Domain, NodeName3} = {pubsub_tools:node_addr(), <<"node3xx">>}, - pubsub_tools:create_node(Alice, Node1, []), - pubsub_tools:create_node(Alice, Node2, []), - pubsub_tools:create_node(Bob, Node3, []), + [Node1={_,NodeName1}, Node2={_,NodeName2}, Node3={_,NodeName3}] = + pubsub_tools:create_node_names(3), + pubsub_tools:create_nodes([{Alice, Node1, []}, {Alice, Node2, []}, {Bob, Node3, []}]), + AffChange = [{Bob, <<"publish-only">>}], pubsub_tools:set_affiliations(Alice, Node1, AffChange, []), pubsub_tools:subscribe(Bob, Node2, []), diff --git a/big_tests/tests/pubsub_tools.erl b/big_tests/tests/pubsub_tools.erl index 4959169cc2c..b51cf1777ca 100644 --- a/big_tests/tests/pubsub_tools.erl +++ b/big_tests/tests/pubsub_tools.erl @@ -16,6 +16,7 @@ -include_lib("eunit/include/eunit.hrl"). %% Send request, receive (optional) response -export([pubsub_node/0, + pubsub_node/1, domain/0, node_addr/0, rand_name/1, @@ -50,7 +51,10 @@ submit_subscription_response/5, get_pending_subscriptions/3, get_pending_subscriptions/4, - modify_node_subscriptions/4 + modify_node_subscriptions/4, + + create_node_names/1, + create_nodes/1 ]). %% Receive notification or response @@ -636,8 +640,11 @@ decode_affiliations(IQResult) -> [ {exml_query:attr(F, <<"jid">>), exml_query:attr(F, <<"affiliation">>)} || F <- Fields ]. -pubsub_node() -> - {node_addr(), pubsub_node_name()}. +pubsub_node() -> pubsub_node(1). +pubsub_node(Num) -> + {pubsub_tools:node_addr(), <<"node", + (integer_to_binary(Num))/binary, "_", + (base64:encode(crypto:strong_rand_bytes(6)))/binary>>}. domain() -> ct:get_config({hosts, mim, domain}). @@ -661,3 +668,12 @@ encode_group_name(BaseName, NodeTree) -> decode_group_name(ComplexName) -> [NodeTree, BaseName] = binary:split(atom_to_binary(ComplexName, utf8), <<"+">>), #{node_tree => NodeTree, base_name => binary_to_atom(BaseName, utf8)}. + +create_node_names(Count) -> + [pubsub_node(N) || N <- lists:seq(1, Count)]. + +create_nodes(List) -> + lists:map(fun({User, Node, Opts}) -> + pubsub_tools:create_node(User, Node, Opts) + end, List). + diff --git a/src/pubsub/gen_pubsub_node.erl b/src/pubsub/gen_pubsub_node.erl index 342c869db9b..cc615373130 100644 --- a/src/pubsub/gen_pubsub_node.erl +++ b/src/pubsub/gen_pubsub_node.erl @@ -199,6 +199,8 @@ -callback path_to_node(Path :: [nodeId()]) -> nodeId(). +-callback should_delete_when_owner_removed() -> boolean(). + -optional_callbacks([create_node_permission/6, create_node/2, delete_node/1, @@ -222,7 +224,8 @@ get_item/2, set_item/1, get_item_name/3, - path_to_node/1]). + path_to_node/1, + should_delete_when_owner_removed/0]). %% -------------------------------------------------------- %% API diff --git a/src/pubsub/mod_pubsub.erl b/src/pubsub/mod_pubsub.erl index f16cd6b5760..e34139ee8a6 100644 --- a/src/pubsub/mod_pubsub.erl +++ b/src/pubsub/mod_pubsub.erl @@ -879,35 +879,45 @@ remove_user(Acc, User, Server) -> remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - Entity = jid:make(LUser, LServer, <<>>), - Host = host(LServer), - HomeTreeBase = <<"/home/", LServer/binary, "/", LUser/binary>>, - spawn(fun() -> lists:foreach( - fun(PType) -> - catch remove_user_per_plugin(PType, Host, Entity, HomeTreeBase) - end, plugins(Host)) end), - ok. -remove_user_per_plugin(PType, Host, Entity, HomeTreeBase) -> - {result, Subs} = node_action(Host, PType, get_entity_subscriptions, [Host, Entity]), - lists:foreach(fun({#pubsub_node{id = Nidx}, _, _, JID}) -> - node_action(Host, PType, unsubscribe_node, [Nidx, Entity, JID, all]); - (_) -> - ok - end, Subs), - {result, Affs} = node_action(Host, PType, get_entity_affiliations, [Host, Entity]), - lists:foreach(fun({#pubsub_node{nodeid = {H, N}, parents = []}, owner}) -> - delete_node(H, N, Entity); - ({#pubsub_node{nodeid = {H, N}, type = Type}, owner}) - when N == HomeTreeBase, Type == <<"hometree">> -> - delete_node(H, N, Entity); - ({#pubsub_node{id = Nidx}, publisher}) -> - node_action(Host, PType, - set_affiliation, - [Nidx, Entity, none]); - (_) -> - ok - end, Affs). + BackendModules = mongoose_lib:find_behaviour_implementations(mod_pubsub_db), + lists:foreach(fun(Backend) -> + remove_user_per_backend_safe(LUser, LServer, Backend) + end, BackendModules). + +remove_user_per_backend_safe(LUser, LServer, Backend) -> + try + ok = Backend:dirty(fun() -> remove_user_per_backend(LUser, LServer, Backend) end, #{}) + catch + Class:Reason -> + StackTrace = erlang:get_stacktrace(), + ?WARNING_MSG("event=cannot_delete_pubsub_user," + "luser=~s,lserver=~s,backend=~p,class=~p,reason=~p,stacktrace=~p", + [LUser, LServer, Backend, Class, Reason, StackTrace]) + end. + +remove_user_per_backend(LUser, LServer, Backend) -> + LJID = {LUser, LServer, <<>>}, + Backend:delete_user_subscriptions(LJID), + Nodes = Backend:find_nodes_by_affiliated_user(LJID), + %% We don't broadcast any node deletion notifications to subscribers because: + %% * PEP nodes do not broadcast anything upon deletion + %% * Push nodes do not have (should not have) any subscribers + %% * Remaining nodes are not deleted when the owner leaves + lists:foreach(fun({#pubsub_node{ id = Nidx, type = Type } = Node, owner}) -> + Plugin = plugin(Type), + MaybeBasePlugin + = maybe_default_node(Plugin, should_delete_when_owner_removed, []), + case MaybeBasePlugin:should_delete_when_owner_removed() of + true -> + % Oh my, we do have a mess in the API, don't we? + Backend:delete_node(Node), + Backend:del_node(Nidx); + _ -> Backend:set_affiliation(Nidx, LJID, none) + end; + ({#pubsub_node{ id = Nidx }, _}) -> + Backend:set_affiliation(Nidx, LJID, none) + end, Nodes). handle_call(server_host, _From, State) -> {reply, State#state.server_host, State}; @@ -4174,6 +4184,9 @@ tree(_Host, Name) -> binary_to_atom(<<"nodetree_", Name/binary>>, utf8). plugin(_Host, Name) -> + plugin(Name). + +plugin(Name) -> binary_to_atom(<<"node_", Name/binary>>, utf8). plugins(Host) -> diff --git a/src/pubsub/mod_pubsub_db.erl b/src/pubsub/mod_pubsub_db.erl index be6535269d0..f5603a3f92e 100644 --- a/src/pubsub/mod_pubsub_db.erl +++ b/src/pubsub/mod_pubsub_db.erl @@ -185,6 +185,12 @@ -callback get_user_subscriptions(LUser :: jid:luser(), LServer :: jid:lserver()) -> [NodeName :: [binary()]]. +-callback find_nodes_by_affiliated_user(JID :: jid:ljid()) -> + [{mod_pubsub:pubsubNode(), mod_pubsub:affiliation()}]. + +-callback delete_user_subscriptions(JID :: jid:ljid()) -> + ok. + %%==================================================================== %% API %%==================================================================== @@ -192,7 +198,7 @@ -spec db_error(ReasonData :: map(), ErrorDebug :: map(), Event :: any()) -> {error, Details :: map()}. db_error(ReasonData, ErrorDebug, Event) -> - {error, maps:merge(ErrorDebug#{ event => Event }, ReasonData)}. + {error, maps:merge(ErrorDebug#{ event => Event }, sanitize_reason(ReasonData))}. %% transaction and sync_dirty return very truncated error data so we add extra %% try to gather stack trace etc. @@ -214,3 +220,8 @@ extra_debug_fun(Fun) -> %% Internal functions %%==================================================================== +sanitize_reason(Map) when is_map(Map) -> + Map; +sanitize_reason(Other) -> + #{ unexpected_reason => Other }. + diff --git a/src/pubsub/mod_pubsub_db_mnesia.erl b/src/pubsub/mod_pubsub_db_mnesia.erl index 0f9bf185593..e3393ca4b96 100644 --- a/src/pubsub/mod_pubsub_db_mnesia.erl +++ b/src/pubsub/mod_pubsub_db_mnesia.erl @@ -69,7 +69,9 @@ -export([ get_user_payloads/2, get_user_nodes/2, - get_user_subscriptions/2 + get_user_subscriptions/2, + delete_user_subscriptions/1, + find_nodes_by_affiliated_user/1 ]). %%==================================================================== @@ -208,6 +210,15 @@ node_name(Nidx) -> _ -> <<>> end. +-spec find_nodes_by_affiliated_user(JID :: jid:ljid()) -> + [{mod_pubsub:pubsubNode(), mod_pubsub:affiliation()}]. +find_nodes_by_affiliated_user(LJID) -> + {ok, States} = get_states_by_lus(LJID), + lists:map(fun(#pubsub_state{ stateid = {_, Nidx}, affiliation = Aff }) -> + {ok, Node} = find_node_by_id(Nidx), + {Node, Aff} + end, States). + %% ------------------------ Direct #pubsub_state access ------------------------ -spec get_state(Nidx :: mod_pubsub:nodeIdx(), @@ -527,10 +538,19 @@ delete_subscription(Nidx, LJID, SubId) -> ok. delete_all_subscriptions(Nidx, LJID) -> {ok, State} = get_state(Nidx, LJID, write), + delete_all_subscriptions_by_state(State). + +-spec delete_user_subscriptions(jid:ljid()) -> ok. +delete_user_subscriptions(LJID) -> + {ok, States} = get_states_by_lus(LJID), + lists:foreach(fun delete_all_subscriptions_by_state/1, States). + +-spec delete_all_subscriptions_by_state(mod_pubsub:pubsubState()) -> ok. +delete_all_subscriptions_by_state(State) -> lists:foreach(fun({_, SubId}) -> mnesia:delete({pubsub_subscription, SubId}) end, State#pubsub_state.subscriptions), case State#pubsub_state.affiliation of - none -> del_state(Nidx, LJID); + none -> del_state(State); _ -> mnesia:write(State#pubsub_state{subscriptions = []}) end. @@ -624,9 +644,13 @@ del_items(Nidx, ItemIds) -> %%==================================================================== -spec del_state(Nidx :: mod_pubsub:nodeIdx(), - LJID :: jid:ljid()) -> ok. + LJID :: jid:ljid()) -> ok. del_state(Nidx, LJID) -> - {ok, #pubsub_state{ subscriptions = Subs }} = get_state(Nidx, LJID, write), + {ok, State} = get_state(Nidx, LJID, write), + del_state(State). + +-spec del_state(mod_pubsub:pubsubState()) -> ok. +del_state(#pubsub_state{ stateid = {LJID, Nidx}, subscriptions = Subs }) -> lists:foreach(fun({_, SubId}) -> mnesia:delete({pubsub_subscription, SubId}) end, Subs), mnesia:delete({pubsub_state, {LJID, Nidx}}). diff --git a/src/pubsub/mod_pubsub_db_rdbms.erl b/src/pubsub/mod_pubsub_db_rdbms.erl index ff3092f9d95..8e3390b57c9 100644 --- a/src/pubsub/mod_pubsub_db_rdbms.erl +++ b/src/pubsub/mod_pubsub_db_rdbms.erl @@ -69,7 +69,9 @@ -export([ get_user_payloads/2, get_user_nodes/2, - get_user_subscriptions/2 + get_user_subscriptions/2, + delete_user_subscriptions/1, + find_nodes_by_affiliated_user/1 ]). % For SQL queries @@ -437,6 +439,7 @@ find_subnodes(Key, Nodes, Depth, Acc) -> {Subnodes, NewNodes} = maps:fold(MapTransformer, {[], []}, Map), NewAcc = [{Depth, NewNodes} | Acc], find_subnodes(Key, lists:flatten(Subnodes), Depth + 1, NewAcc). + % ------------------- Affiliations -------------------------------- -spec set_affiliation(Nidx :: mod_pubsub:nodeIdx(), @@ -610,6 +613,21 @@ strip_payload(PayloadDB) -> {ok, #xmlel{children = Payload}} = exml:parse(PayloadXML), exml:to_binary(Payload). +-spec delete_user_subscriptions(jid:ljid()) -> ok. +delete_user_subscriptions({ LU, LS, _ }) -> + SQL = mod_pubsub_db_rdbms_sql:delete_user_subscriptions(LU, LS), + {updated, _} = mongoose_rdbms:sql_query_t(SQL), + ok. + +find_nodes_by_affiliated_user({ LU, LS, _ }) -> + SQL = mod_pubsub_db_rdbms_sql:select_nodes_by_affiliated_user(LU, LS), + {selected, NodesWithAffs} = mongoose_rdbms:sql_query(global, SQL), + lists:map(fun decode_pubsub_node_with_aff_row/1, NodesWithAffs). + +decode_pubsub_node_with_aff_row(Row) -> + [Aff | NodeRow] = tuple_to_list(Row), + {decode_pubsub_node_row(list_to_tuple(NodeRow)), sql2aff(Aff)}. + %%==================================================================== %% Helpers %%==================================================================== diff --git a/src/pubsub/mod_pubsub_db_rdbms_sql.erl b/src/pubsub/mod_pubsub_db_rdbms_sql.erl index 5508ad9a730..0a43842f6b9 100644 --- a/src/pubsub/mod_pubsub_db_rdbms_sql.erl +++ b/src/pubsub/mod_pubsub_db_rdbms_sql.erl @@ -37,6 +37,7 @@ delete_subscription/5, delete_all_subscriptions/4, delete_all_subscriptions/1, + delete_user_subscriptions/2, update_subscription/6 ]). @@ -61,6 +62,7 @@ select_nodes_in_list_with_key/2, select_nodes_by_key_and_names_in_list_with_parents/2, select_nodes_by_key_and_names_in_list_with_children/2, + select_nodes_by_affiliated_user/2, select_subnodes/2, delete_node/2, set_parents/2, @@ -247,6 +249,12 @@ delete_all_subscriptions(Nidx) -> ["DELETE FROM pubsub_subscriptions" " WHERE nidx = ", esc_int(Nidx)]. +-spec delete_user_subscriptions(LU :: jid:luser(), LS :: jid:lserver()) -> iolist(). +delete_user_subscriptions(LU, LS) -> + ["DELETE FROM pubsub_subscriptions" + " WHERE luser = ", esc_string(LU), + " AND lserver = ", esc_string(LS)]. + -spec update_subscription(Nidx :: mod_pubsub:nodeIdx(), LU :: jid:luser(), LS :: jid:lserver(), @@ -470,6 +478,14 @@ select_nodes_by_owner(LJID) -> ] end. +-spec select_nodes_by_affiliated_user(LU :: jid:luser(), LS :: jid:lserver()) -> iolist(). +select_nodes_by_affiliated_user(LU, LS) -> + ["SELECT aff, ", pubsub_node_fields("pn"), + " FROM pubsub_affiliations AS pa" + " INNER JOIN pubsub_nodes AS pn ON pa.nidx = pn.nidx" + " WHERE luser = ", esc_string(LU), + " AND lserver = ", esc_string(LS)]. + -spec select_nodes_in_list_with_key(Key :: binary(), Nodes :: [binary()]) -> iolist(). select_nodes_in_list_with_key(Key, Nodes) -> EscapedNames = [esc_string(Node) || Node <- Nodes], diff --git a/src/pubsub/node_flat.erl b/src/pubsub/node_flat.erl index b795e07b887..acfbbf45e2e 100644 --- a/src/pubsub/node_flat.erl +++ b/src/pubsub/node_flat.erl @@ -49,7 +49,8 @@ get_pending_nodes/2, get_items_if_authorised/3, get_items/3, get_item/7, get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1, can_fetch_item/2, is_subscribed/1]). + path_to_node/1, can_fetch_item/2, is_subscribed/1, + should_delete_when_owner_removed/0]). based_on() -> none. @@ -665,6 +666,8 @@ path_to_node(Path) -> _ -> iolist_to_binary(Path) end. +should_delete_when_owner_removed() -> false. + can_fetch_item(owner, _) -> true; can_fetch_item(member, _) -> true; can_fetch_item(publisher, _) -> true; diff --git a/src/pubsub/node_pep.erl b/src/pubsub/node_pep.erl index b77d9ed86c3..a6193fe4aca 100644 --- a/src/pubsub/node_pep.erl +++ b/src/pubsub/node_pep.erl @@ -39,7 +39,8 @@ create_node_permission/6, delete_node/1, unsubscribe_node/4, node_to_path/1, get_entity_affiliations/2, get_entity_affiliations/3, - get_entity_subscriptions/2, get_entity_subscriptions/4 + get_entity_subscriptions/2, get_entity_subscriptions/4, + should_delete_when_owner_removed/0 ]). based_on() -> node_flat. @@ -171,6 +172,8 @@ accumulate_entity_subscriptions(J, Node, Ss, Acc) -> node_to_path(Node) -> node_flat:node_to_path(Node). +should_delete_when_owner_removed() -> true. + %%% %%% Internal %%% diff --git a/src/pubsub/node_push.erl b/src/pubsub/node_push.erl index 53faf7e4841..1a3caf3a07f 100644 --- a/src/pubsub/node_push.erl +++ b/src/pubsub/node_push.erl @@ -18,7 +18,7 @@ -include("pubsub.hrl"). -export([based_on/0, init/3, terminate/2, options/0, features/0, - publish_item/9, node_to_path/1]). + publish_item/9, node_to_path/1, should_delete_when_owner_removed/0]). based_on() -> node_flat. @@ -90,6 +90,8 @@ do_publish_item(_ServerHost, _PublishOptions, _Payload) -> node_to_path(Node) -> node_flat:node_to_path(Node). +should_delete_when_owner_removed() -> true. + %%% %%% Internal %%% diff --git a/src/pubsub/pubsub_index.erl b/src/pubsub/pubsub_index.erl index 46d469d2d6c..5a0c6985116 100644 --- a/src/pubsub/pubsub_index.erl +++ b/src/pubsub/pubsub_index.erl @@ -62,8 +62,9 @@ spawn_and_call(F) -> Pid = spawn_monitor(FF), receive {call_result, Ref, Result} -> + {atomic, NewId} = Result, erlang:demonitor(Ref, [flush]), - Result; + NewId; {'DOWN', Ref, process, Pid, Reason} -> erlang:error({spawn_and_call_failed, Reason}) after 5000 -> From dcc0c320d6494e634ce0ed057457fda8af9b6dbd Mon Sep 17 00:00:00 2001 From: Piotr Nosek Date: Wed, 26 Jun 2019 12:45:59 +0200 Subject: [PATCH 2/2] Apply reviews comments --- big_tests/tests/pubsub_tools.erl | 2 +- src/pubsub/gen_pubsub_node.erl | 5 +++- src/pubsub/mod_pubsub.erl | 42 +++++++++-------------------- src/pubsub/mod_pubsub_db.erl | 4 ++- src/pubsub/mod_pubsub_db_mnesia.erl | 12 ++++----- src/pubsub/node_flat.erl | 37 ++++++++++++++++++++++++- 6 files changed, 63 insertions(+), 39 deletions(-) diff --git a/big_tests/tests/pubsub_tools.erl b/big_tests/tests/pubsub_tools.erl index b51cf1777ca..7e028987166 100644 --- a/big_tests/tests/pubsub_tools.erl +++ b/big_tests/tests/pubsub_tools.erl @@ -642,7 +642,7 @@ decode_affiliations(IQResult) -> pubsub_node() -> pubsub_node(1). pubsub_node(Num) -> - {pubsub_tools:node_addr(), <<"node", + {pubsub_tools:node_addr(), <<"node_", (integer_to_binary(Num))/binary, "_", (base64:encode(crypto:strong_rand_bytes(6)))/binary>>}. diff --git a/src/pubsub/gen_pubsub_node.erl b/src/pubsub/gen_pubsub_node.erl index cc615373130..6465e13bed3 100644 --- a/src/pubsub/gen_pubsub_node.erl +++ b/src/pubsub/gen_pubsub_node.erl @@ -201,6 +201,8 @@ -callback should_delete_when_owner_removed() -> boolean(). +-callback remove_user(LUser :: jid:luser(), LServer :: jid:lserver(), Backend :: module()) -> any(). + -optional_callbacks([create_node_permission/6, create_node/2, delete_node/1, @@ -225,7 +227,8 @@ set_item/1, get_item_name/3, path_to_node/1, - should_delete_when_owner_removed/0]). + should_delete_when_owner_removed/0, + remove_user/3]). %% -------------------------------------------------------- %% API diff --git a/src/pubsub/mod_pubsub.erl b/src/pubsub/mod_pubsub.erl index e34139ee8a6..40fb75ae6fb 100644 --- a/src/pubsub/mod_pubsub.erl +++ b/src/pubsub/mod_pubsub.erl @@ -86,7 +86,7 @@ -export([subscription_to_string/1, affiliation_to_string/1, string_to_subscription/1, string_to_affiliation/1, extended_error/2, extended_error/3, service_jid/1, - tree/1, tree/2, plugin/2, plugins/1, config/3, + tree/1, tree/2, plugin/2, plugin/1, plugins/1, plugin_call/3, config/3, host/1, serverhost/1]). %% API and gen_server callbacks @@ -881,13 +881,17 @@ remove_user(User, Server) -> LServer = jid:nameprep(Server), BackendModules = mongoose_lib:find_behaviour_implementations(mod_pubsub_db), - lists:foreach(fun(Backend) -> - remove_user_per_backend_safe(LUser, LServer, Backend) - end, BackendModules). + PluginModules = mongoose_lib:find_behaviour_implementations(gen_pubsub_node), + % The line below will currently lead to unnecessary DB selects, + % because plugins don't really filter DB data by type. + % TODO: This should be optimised during GDPR load tests. + [ remove_user_per_backend_and_plugin_safe(LUser, LServer, Plugin, Backend) + || Plugin <- PluginModules, Backend <- BackendModules ], + ok. -remove_user_per_backend_safe(LUser, LServer, Backend) -> +remove_user_per_backend_and_plugin_safe(LUser, LServer, Plugin, Backend) -> try - ok = Backend:dirty(fun() -> remove_user_per_backend(LUser, LServer, Backend) end, #{}) + plugin_call(Plugin, remove_user, [LUser, LServer, Backend]) catch Class:Reason -> StackTrace = erlang:get_stacktrace(), @@ -896,29 +900,6 @@ remove_user_per_backend_safe(LUser, LServer, Backend) -> [LUser, LServer, Backend, Class, Reason, StackTrace]) end. -remove_user_per_backend(LUser, LServer, Backend) -> - LJID = {LUser, LServer, <<>>}, - Backend:delete_user_subscriptions(LJID), - Nodes = Backend:find_nodes_by_affiliated_user(LJID), - %% We don't broadcast any node deletion notifications to subscribers because: - %% * PEP nodes do not broadcast anything upon deletion - %% * Push nodes do not have (should not have) any subscribers - %% * Remaining nodes are not deleted when the owner leaves - lists:foreach(fun({#pubsub_node{ id = Nidx, type = Type } = Node, owner}) -> - Plugin = plugin(Type), - MaybeBasePlugin - = maybe_default_node(Plugin, should_delete_when_owner_removed, []), - case MaybeBasePlugin:should_delete_when_owner_removed() of - true -> - % Oh my, we do have a mess in the API, don't we? - Backend:delete_node(Node), - Backend:del_node(Nidx); - _ -> Backend:set_affiliation(Nidx, LJID, none) - end; - ({#pubsub_node{ id = Nidx }, _}) -> - Backend:set_affiliation(Nidx, LJID, none) - end, Nodes). - handle_call(server_host, _From, State) -> {reply, State#state.server_host, State}; handle_call(plugins, _From, State) -> @@ -4295,6 +4276,9 @@ tree_action(Host, Function, Args) -> node_call(Host, Type, Function, Args) -> ?DEBUG("node_call ~p ~p ~p", [Type, Function, Args]), PluginModule = plugin(Host, Type), + plugin_call(PluginModule, Function, Args). + +plugin_call(PluginModule, Function, Args) -> CallModule = maybe_default_node(PluginModule, Function, Args), case apply(CallModule, Function, Args) of {result, Result} -> diff --git a/src/pubsub/mod_pubsub_db.erl b/src/pubsub/mod_pubsub_db.erl index f5603a3f92e..0e7bb0d56d1 100644 --- a/src/pubsub/mod_pubsub_db.erl +++ b/src/pubsub/mod_pubsub_db.erl @@ -195,7 +195,9 @@ %% API %%==================================================================== --spec db_error(ReasonData :: map(), ErrorDebug :: map(), Event :: any()) -> +% ReasonData may either be a debug map provided by mod_pubsub +% or some other term if the crash is serious enough to lose the debug map somewhere. +-spec db_error(ReasonData :: map() | any(), ErrorDebug :: map(), Event :: any()) -> {error, Details :: map()}. db_error(ReasonData, ErrorDebug, Event) -> {error, maps:merge(ErrorDebug#{ event => Event }, sanitize_reason(ReasonData))}. diff --git a/src/pubsub/mod_pubsub_db_mnesia.erl b/src/pubsub/mod_pubsub_db_mnesia.erl index e3393ca4b96..3843afe9761 100644 --- a/src/pubsub/mod_pubsub_db_mnesia.erl +++ b/src/pubsub/mod_pubsub_db_mnesia.erl @@ -280,7 +280,7 @@ del_node(Nidx) -> {ok, States} = get_states(Nidx), lists:foreach(fun (#pubsub_state{stateid = {LJID, _}, items = Items}) -> del_items(Nidx, Items), - del_state(Nidx, LJID) + del_state_by_idx_and_ljid(Nidx, LJID) end, States), {ok, States}. @@ -469,7 +469,7 @@ set_affiliation(Nidx, LJID, Affiliation) -> BareLJID = jid:to_bare(LJID), {ok, State} = get_state(Nidx, BareLJID, write), case {Affiliation, State#pubsub_state.subscriptions} of - {none, []} -> del_state(Nidx, BareLJID); + {none, []} -> del_state_by_idx_and_ljid(Nidx, BareLJID); _ -> mnesia:write(State#pubsub_state{ affiliation = Affiliation }) end. @@ -528,7 +528,7 @@ delete_subscription(Nidx, LJID, SubId) -> NewSubs = lists:keydelete(SubId, 2, State#pubsub_state.subscriptions), mnesia:delete({pubsub_subscription, SubId}), case {State#pubsub_state.affiliation, NewSubs} of - {none, []} -> del_state(Nidx, LJID); + {none, []} -> del_state_by_idx_and_ljid(Nidx, LJID); _ -> mnesia:write(State#pubsub_state{subscriptions = NewSubs}) end. @@ -643,9 +643,9 @@ del_items(Nidx, ItemIds) -> %% Internal functions %%==================================================================== --spec del_state(Nidx :: mod_pubsub:nodeIdx(), - LJID :: jid:ljid()) -> ok. -del_state(Nidx, LJID) -> +-spec del_state_by_idx_and_ljid(Nidx :: mod_pubsub:nodeIdx(), + LJID :: jid:ljid()) -> ok. +del_state_by_idx_and_ljid(Nidx, LJID) -> {ok, State} = get_state(Nidx, LJID, write), del_state(State). diff --git a/src/pubsub/node_flat.erl b/src/pubsub/node_flat.erl index acfbbf45e2e..f226b421626 100644 --- a/src/pubsub/node_flat.erl +++ b/src/pubsub/node_flat.erl @@ -50,7 +50,7 @@ get_items_if_authorised/3, get_items/3, get_item/7, get_item/2, set_item/1, get_item_name/3, node_to_path/1, path_to_node/1, can_fetch_item/2, is_subscribed/1, - should_delete_when_owner_removed/0]). + should_delete_when_owner_removed/0, remove_user/3]). based_on() -> none. @@ -686,3 +686,38 @@ is_subscribed(Subscriptions) -> make_subid() -> mongoose_bin:gen_from_timestamp(). +remove_user(LUser, LServer, Backend) -> + Backend:dirty(fun() -> + LJID = {LUser, LServer, <<>>}, + Backend:delete_user_subscriptions(LJID), + NodesAndAffs = Backend:find_nodes_by_affiliated_user(LJID), + %% We don't broadcast node deletion notifications to subscribers because: + %% * PEP nodes do not broadcast anything upon deletion + %% * Push nodes do not have (should not have) any subscribers + %% * Remaining nodes are not deleted when the owner leaves + lists:foreach( + fun(NodeAffPair) -> + remove_user_by_affiliation_and_node(NodeAffPair, LJID, Backend) + end, NodesAndAffs) + end, #{}). + +-spec remove_user_by_affiliation_and_node(NodeAffPair :: {mod_pubsub:pubsubNode(), + mod_pubsub:affiliation()}, + LJID :: jid:ljid(), + Backend :: module()) -> + any(). +remove_user_by_affiliation_and_node({#pubsub_node{ id = Nidx } = Node, owner}, LJID, Backend) -> + case mod_pubsub:plugin_call(mod_pubsub:plugin(Node#pubsub_node.type), + should_delete_when_owner_removed, []) of + {result, true} -> + % Oh my, we do have a mess in the API, don't we? + % delete_node removes actual node structure + % del_node removes node's items and states (affs and subs) + Backend:delete_node(Node), + Backend:del_node(Nidx); + _ -> + Backend:set_affiliation(Nidx, LJID, none) + end; +remove_user_by_affiliation_and_node({#pubsub_node{ id = Nidx }, _}, LJID, Backend) -> + Backend:set_affiliation(Nidx, LJID, none). +