diff options
Diffstat (limited to 'src/mod_pubsub')
32 files changed, 12532 insertions, 2600 deletions
diff --git a/src/mod_pubsub/Makefile.in b/src/mod_pubsub/Makefile.in index 39f6a16e8..88bf2ba0c 100644 --- a/src/mod_pubsub/Makefile.in +++ b/src/mod_pubsub/Makefile.in @@ -9,7 +9,9 @@ LIBS = @LIBS@ ERLANG_CFLAGS = @ERLANG_CFLAGS@ ERLANG_LIBS = @ERLANG_LIBS@ -EFLAGS = -I .. -pz .. +EFLAGS += -I .. +EFLAGS += -pz .. + # make debug=true to compile Erlang module with debug informations. ifdef debug EFLAGS+=+debug_info @@ -23,7 +25,7 @@ ERLBEHAVBEAMS = $(addprefix $(OUTDIR)/,$(ERLBEHAVS:.erl=.beam)) BEAMS = $(addprefix $(OUTDIR)/,$(SOURCES:.erl=.beam)) -all: $(ERLBEHAVBEAMS) $(BEAMS) +all: mod_pubsub_odbc.erl $(ERLBEHAVBEAMS) $(BEAMS) $(BEAMS): $(ERLBEHAVBEAMS) @@ -36,6 +38,9 @@ clean: distclean: clean rm -f Makefile +mod_pubsub_odbc.erl: + patch -o mod_pubsub_odbc.erl mod_pubsub.erl pubsub_odbc.patch + TAGS: etags *.erl diff --git a/src/mod_pubsub/Makefile.win32 b/src/mod_pubsub/Makefile.win32 index f68375ccd..8308f242a 100644 --- a/src/mod_pubsub/Makefile.win32 +++ b/src/mod_pubsub/Makefile.win32 @@ -4,8 +4,7 @@ include ..\Makefile.inc EFLAGS = -I .. -pz .. OUTDIR = .. -SOURCES = $(wildcard *.erl) -BEAMS = $(addprefix $(OUTDIR)/,$(SOURCES:.erl=.beam)) +BEAMS = ..\gen_pubsub_node.beam ..\gen_pubsub_nodetree.beam ..\mod_pubsub.beam ..\mod_pubsub_odbc.beam ..\node_buddy.beam ..\node_club.beam ..\node_dag.beam ..\node_dispatch.beam ..\node_flat.beam ..\node_flat_odbc.beam ..\node_hometree.beam ..\node_hometree_odbc.beam ..\node_mb.beam ..\node_pep.beam ..\node_pep_odbc.beam ..\node_private.beam ..\node_public.beam ..\nodetree_dag.beam ..\nodetree_tree.beam ..\nodetree_tree_odbc.beam ..\nodetree_virtual.beam ..\pubsub_db_odbc.beam ..\pubsub_index.beam ..\pubsub_subscription.beam ..\pubsub_subscription_odbc.beam ALL : $(BEAMS) @@ -21,15 +20,68 @@ $(OUTDIR)\gen_pubsub_nodetree.beam : gen_pubsub_nodetree.erl $(OUTDIR)\mod_pubsub.beam : mod_pubsub.erl erlc -W $(EFLAGS) -o $(OUTDIR) mod_pubsub.erl -$(OUTDIR)\nodetree_default.beam : nodetree_default.erl - erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_default.erl +$(OUTDIR)\mod_pubsub_odbc.beam : mod_pubsub_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) mod_pubsub_odbc.erl -$(OUTDIR)\nodetree_virtual.beam : nodetree_virtual.erl - erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_virtual.erl +$(OUTDIR)\node_buddy.beam : node_buddy.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_buddy.erl + +$(OUTDIR)\node_club.beam : node_club.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_club.erl + +$(OUTDIR)\node_dag.beam : node_dag.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_dag.erl + +$(OUTDIR)\node_dispatch.beam : node_dispatch.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_dispatch.erl + +$(OUTDIR)\node_flat.beam : node_flat.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_flat.erl -$(OUTDIR)\node_default.beam : node_default.erl - erlc -W $(EFLAGS) -o $(OUTDIR) node_default.erl +$(OUTDIR)\node_flat_odbc.beam : node_flat_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_flat_odbc.erl + +$(OUTDIR)\node_hometree.beam : node_hometree.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_hometree.erl + +$(OUTDIR)\node_hometree_odbc.beam : node_hometree_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_hometree_odbc.erl + +$(OUTDIR)\node_mb.beam : node_mb.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_mb.erl $(OUTDIR)\node_pep.beam : node_pep.erl erlc -W $(EFLAGS) -o $(OUTDIR) node_pep.erl +$(OUTDIR)\node_pep_odbc.beam : node_pep_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_pep_odbc.erl + +$(OUTDIR)\node_private.beam : node_private.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_private.erl + +$(OUTDIR)\node_public.beam : node_public.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_public.erl + +$(OUTDIR)\nodetree_dag.beam : nodetree_dag.erl + erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_dag.erl + +$(OUTDIR)\nodetree_tree.beam : nodetree_tree.erl + erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_tree.erl + +$(OUTDIR)\nodetree_tree_odbc.beam : nodetree_tree_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_tree_odbc.erl + +$(OUTDIR)\nodetree_virtual.beam : nodetree_virtual.erl + erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_virtual.erl + +$(OUTDIR)\pubsub_db_odbc.beam : pubsub_db_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) pubsub_db_odbc.erl + +$(OUTDIR)\pubsub_index.beam : pubsub_index.erl + erlc -W $(EFLAGS) -o $(OUTDIR) pubsub_index.erl + +$(OUTDIR)\pubsub_subscription.beam : pubsub_subscription.erl + erlc -W $(EFLAGS) -o $(OUTDIR) pubsub_subscription.erl + +$(OUTDIR)\pubsub_subscription_odbc.beam : pubsub_subscription_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) pubsub_subscription_odbc.erl diff --git a/src/mod_pubsub/gen_pubsub_node.erl b/src/mod_pubsub/gen_pubsub_node.erl index 195ed3934..64464bcac 100644 --- a/src/mod_pubsub/gen_pubsub_node.erl +++ b/src/mod_pubsub/gen_pubsub_node.erl @@ -10,13 +10,13 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% %%% -%%% @copyright 2006-2008 Process-one +%%% @copyright 2006-2009 ProcessOne %%% @author Christophe Romain <christophe.romain@process-one.net> %%% [http://www.process-one.net/] %%% @version {@vsn}, {@date} {@time} @@ -43,29 +43,34 @@ behaviour_info(callbacks) -> {options, 0}, {features, 0}, {create_node_permission, 6}, - {create_node, 3}, - {delete_node, 2}, - {purge_node, 3}, + {create_node, 2}, + {delete_node, 1}, + {purge_node, 2}, {subscribe_node, 8}, - {unsubscribe_node, 5}, - {publish_item, 7}, + {unsubscribe_node, 4}, + {publish_item, 6}, {delete_item, 4}, - {remove_extra_items, 4}, - {get_node_affiliations, 2}, + {remove_extra_items, 3}, + {get_node_affiliations, 1}, {get_entity_affiliations, 2}, - {get_affiliation, 3}, - {set_affiliation, 4}, - {get_node_subscriptions, 2}, + {get_affiliation, 2}, + {set_affiliation, 3}, + {get_node_subscriptions, 1}, {get_entity_subscriptions, 2}, - {get_subscription, 3}, - {set_subscription, 4}, - {get_states, 2}, - {get_state, 3}, + {get_subscriptions, 2}, + {set_subscriptions, 4}, + {get_pending_nodes, 2}, + {get_states, 1}, + {get_state, 2}, {set_state, 1}, + {get_items, 6}, {get_items, 2}, - {get_item, 3}, + {get_item, 7}, + {get_item, 2}, {set_item, 1}, - {get_item_name, 3} + {get_item_name, 3}, + {node_to_path, 1}, + {path_to_node, 1} ]; behaviour_info(_Other) -> undefined. diff --git a/src/mod_pubsub/gen_pubsub_nodetree.erl b/src/mod_pubsub/gen_pubsub_nodetree.erl index 668b71a8e..84cf8f61b 100644 --- a/src/mod_pubsub/gen_pubsub_nodetree.erl +++ b/src/mod_pubsub/gen_pubsub_nodetree.erl @@ -10,13 +10,13 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% %%% -%%% @copyright 2006-2008 Process-one +%%% @copyright 2006-2009 ProcessOne %%% @author Christophe Romain <christophe.romain@process-one.net> %%% [http://www.process-one.net/] %%% @version {@vsn}, {@date} {@time} @@ -42,11 +42,16 @@ behaviour_info(callbacks) -> {terminate, 2}, {options, 0}, {set_node, 1}, + {get_node, 3}, {get_node, 2}, + {get_node, 1}, + {get_nodes, 2}, {get_nodes, 1}, + {get_parentnodes, 3}, + {get_parentnodes_tree, 3}, {get_subnodes, 3}, - {get_subnodes_tree, 2}, - {create_node, 5}, + {get_subnodes_tree, 3}, + {create_node, 6}, {delete_node, 2} ]; behaviour_info(_Other) -> diff --git a/src/mod_pubsub/mod_pubsub.erl b/src/mod_pubsub/mod_pubsub.erl index 43bd7d9ac..d7d1fffe3 100644 --- a/src/mod_pubsub/mod_pubsub.erl +++ b/src/mod_pubsub/mod_pubsub.erl @@ -10,12 +10,12 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% -%%% @copyright 2006-2008 Process-one +%%% @copyright 2006-2009 ProcessOne %%% @author Christophe Romain <christophe.romain@process-one.net> %%% [http://www.process-one.net/] %%% @version {@vsn}, {@date} {@time} @@ -30,30 +30,38 @@ %%% %%% @reference See <a href="http://www.xmpp.org/extensions/xep-0060.html">XEP-0060: Pubsub</a> for %%% the latest version of the PubSub specification. -%%% This module uses version 1.10 of the specification as a base. +%%% This module uses version 1.12 of the specification as a base. %%% Most of the specification is implemented. -%%% Code is derivated from the original pubsub v1.7, functions concerning config may be rewritten. -%%% Code also inspired from the original PEP patch by Magnus Henoch (mangeATfreemail.hu) - -%%% TODO -%%% plugin: generate Reply (do not use broadcast atom anymore) +%%% Functions concerning configuration should be rewritten. +%%% +%%% Support for subscription-options and multi-subscribe features was +%%% added by Brian Cully (bjc AT kublai.com). Subscriptions and options are +%%% stored in the pubsub_subscription table, with a link to them provided +%%% by the subscriptions field of pubsub_state. For information on +%%% subscription-options and mulit-subscribe see XEP-0060 sections 6.1.6, +%%% 6.2.3.1, 6.2.3.5, and 6.3. For information on subscription leases see +%%% XEP-0060 section 12.18. -module(mod_pubsub). --version('1.10-01'). +-author('christophe.romain@process-one.net'). +-version('1.13-0'). -behaviour(gen_server). -behaviour(gen_mod). -include("ejabberd.hrl"). +-include("adhoc.hrl"). -include("jlib.hrl"). -include("pubsub.hrl"). --define(STDTREE, "default"). --define(STDNODE, "default"). +-define(STDTREE, "tree"). +-define(STDNODE, "flat"). -define(PEPNODE, "pep"). %% exports for hooks -export([presence_probe/3, + in_subscription/6, + out_subscription/4, remove_user/2, disco_local_identity/5, disco_local_features/5, @@ -70,14 +78,18 @@ %% exports for console debug manual use -export([create_node/5, delete_node/3, - subscribe_node/4, + subscribe_node/5, unsubscribe_node/5, publish_item/6, delete_item/4, - get_configure/4, - set_configure/5, + send_items/6, get_items/2, - node_action/3, + get_item/3, + get_cached_item/2, + broadcast_stanza/8, + get_configure/5, + set_configure/5, + tree_action/3, node_action/4 ]). @@ -90,8 +102,7 @@ string_to_affiliation/1, extended_error/2, extended_error/3, - make_stanza/3, - route_stanza/3 + rename_default_nodeplugin/0 ]). %% API and gen_server callbacks @@ -106,6 +117,10 @@ code_change/3 ]). +%% calls for parallel sending of last items +-export([send_loop/1 + ]). + -define(PROCNAME, ejabberd_mod_pubsub). -define(PLUGIN_PREFIX, "node_"). -define(TREE_PREFIX, "nodetree_"). @@ -113,8 +128,13 @@ -record(state, {server_host, host, access, + pep_mapping = [], + ignore_pep_from_offline = true, + last_item_cache = false, + max_items_node = ?MAXITEMS, nodetree = ?STDTREE, - plugins = [?STDNODE]}). + plugins = [?STDNODE], + send_loop}). %%==================================================================== %% API @@ -145,50 +165,70 @@ stop(Host) -> %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} %% Description: Initiates the server %%-------------------------------------------------------------------- init([ServerHost, Opts]) -> - ?INFO_MSG("pubsub init ~p ~p",[ServerHost,Opts]), + ?DEBUG("pubsub init ~p ~p",[ServerHost,Opts]), Host = gen_mod:get_opt_host(ServerHost, Opts, "pubsub.@HOST@"), - ServedHosts = gen_mod:get_opt(served_hosts, Opts, []), Access = gen_mod:get_opt(access_createnode, Opts, all), + PepOffline = gen_mod:get_opt(ignore_pep_from_offline, Opts, true), + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), + LastItemCache = gen_mod:get_opt(last_item_cache, Opts, false), + MaxItemsNode = gen_mod:get_opt(max_items_node, Opts, ?MAXITEMS), + pubsub_index:init(Host, ServerHost, Opts), + ets:new(gen_mod:get_module_proc(Host, config), [set, named_table]), + ets:new(gen_mod:get_module_proc(ServerHost, config), [set, named_table]), + ets:new(gen_mod:get_module_proc(Host, last_items), [set, named_table]), + ets:new(gen_mod:get_module_proc(ServerHost, last_items), [set, named_table]), + {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), mod_disco:register_feature(ServerHost, ?NS_PUBSUB), - ejabberd_hooks:add(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 75), - ejabberd_hooks:add(disco_local_features, ServerHost, ?MODULE, disco_local_features, 75), - ejabberd_hooks:add(disco_local_items, ServerHost, ?MODULE, disco_local_items, 75), + ets:insert(gen_mod:get_module_proc(Host, config), {nodetree, NodeTree}), + ets:insert(gen_mod:get_module_proc(Host, config), {plugins, Plugins}), + ets:insert(gen_mod:get_module_proc(Host, config), {last_item_cache, LastItemCache}), + ets:insert(gen_mod:get_module_proc(Host, config), {max_items_node, MaxItemsNode}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {nodetree, NodeTree}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {plugins, Plugins}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {last_item_cache, Plugins}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {max_items_node, LastItemCache}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {pep_mapping, PepMapping}), ejabberd_hooks:add(disco_sm_identity, ServerHost, ?MODULE, disco_sm_identity, 75), ejabberd_hooks:add(disco_sm_features, ServerHost, ?MODULE, disco_sm_features, 75), ejabberd_hooks:add(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75), - ejabberd_hooks:add(presence_probe_hook, ServerHost, ?MODULE, presence_probe, 50), + ejabberd_hooks:add(presence_probe_hook, ServerHost, ?MODULE, presence_probe, 80), + ejabberd_hooks:add(roster_in_subscription, ServerHost, ?MODULE, in_subscription, 50), + ejabberd_hooks:add(roster_out_subscription, ServerHost, ?MODULE, out_subscription, 50), ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, remove_user, 50), - IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), - lists:foreach( - fun({NS,Mod,Fun}) -> - gen_iq_handler:add_iq_handler( - Mod, ServerHost, NS, ?MODULE, Fun, IQDisc) - end, - [{?NS_PUBSUB, ejabberd_local, iq_local}, - {?NS_PUBSUB_OWNER, ejabberd_local, iq_local}, - {?NS_PUBSUB, ejabberd_sm, iq_sm}, - {?NS_PUBSUB_OWNER, ejabberd_sm, iq_sm}]), + ejabberd_hooks:add(anonymous_purge_hook, ServerHost, ?MODULE, remove_user, 50), + gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB, ?MODULE, iq_sm, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB_OWNER, ?MODULE, iq_sm, IQDisc), + case lists:member(?PEPNODE, Plugins) of + true -> + ejabberd_hooks:add(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 75), + ejabberd_hooks:add(disco_local_features, ServerHost, ?MODULE, disco_local_features, 75), + ejabberd_hooks:add(disco_local_items, ServerHost, ?MODULE, disco_local_items, 75), + gen_iq_handler:add_iq_handler(ejabberd_local, ServerHost, ?NS_PUBSUB, ?MODULE, iq_local, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, ServerHost, ?NS_PUBSUB_OWNER, ?MODULE, iq_local, IQDisc); + false -> + ok + end, ejabberd_router:register_route(Host), - {Plugins, NodeTree} = init_plugins(Host, ServerHost, Opts), - update_database(Host), - ets:new(gen_mod:get_module_proc(Host, pubsub_state), [set, named_table]), - ets:insert(gen_mod:get_module_proc(Host, pubsub_state), {nodetree, NodeTree}), - ets:insert(gen_mod:get_module_proc(Host, pubsub_state), {plugins, Plugins}), - ets:new(gen_mod:get_module_proc(ServerHost, pubsub_state), [set, named_table]), - ets:insert(gen_mod:get_module_proc(ServerHost, pubsub_state), {nodetree, NodeTree}), - ets:insert(gen_mod:get_module_proc(ServerHost, pubsub_state), {plugins, Plugins}), - init_nodes(Host, ServerHost, ServedHosts), - {ok, #state{host = Host, + update_node_database(Host, ServerHost), + update_state_database(Host, ServerHost), + init_nodes(Host, ServerHost, NodeTree, Plugins), + State = #state{host = Host, server_host = ServerHost, access = Access, + pep_mapping = PepMapping, + ignore_pep_from_offline = PepOffline, + last_item_cache = LastItemCache, + max_items_node = MaxItemsNode, nodetree = NodeTree, - plugins = Plugins}}. + plugins = Plugins}, + SendLoop = spawn(?MODULE, send_loop, [State]), + {ok, State#state{send_loop = SendLoop}}. %% @spec (Host, ServerHost, Opts) -> Plugins %% Host = mod_pubsub:host() Opts = [{Key,Value}] @@ -202,101 +242,369 @@ init([ServerHost, Opts]) -> %% <em>node_plugin</em>. The 'node_' prefix is mandatory.</p> %% <p>The modules are initialized in alphetical order and the list is checked %% and sorted to ensure that each module is initialized only once.</p> -%% <p>See {@link node_default:init/1} for an example implementation.</p> +%% <p>See {@link node_hometree:init/1} for an example implementation.</p> init_plugins(Host, ServerHost, Opts) -> TreePlugin = list_to_atom(?TREE_PREFIX ++ gen_mod:get_opt(nodetree, Opts, ?STDTREE)), - ?INFO_MSG("** tree plugin is ~p",[TreePlugin]), + ?DEBUG("** tree plugin is ~p",[TreePlugin]), TreePlugin:init(Host, ServerHost, Opts), - Plugins = lists:usort(gen_mod:get_opt(plugins, Opts, []) ++ [?STDNODE]), + Plugins = gen_mod:get_opt(plugins, Opts, [?STDNODE]), + PepMapping = gen_mod:get_opt(pep_mapping, Opts, []), + ?DEBUG("** PEP Mapping : ~p~n",[PepMapping]), lists:foreach(fun(Name) -> - ?INFO_MSG("** init ~s plugin",[Name]), + ?DEBUG("** init ~s plugin",[Name]), Plugin = list_to_atom(?PLUGIN_PREFIX ++ Name), Plugin:init(Host, ServerHost, Opts) end, Plugins), - {Plugins, TreePlugin}. + {Plugins, TreePlugin, PepMapping}. terminate_plugins(Host, ServerHost, Plugins, TreePlugin) -> lists:foreach(fun(Name) -> - ?INFO_MSG("** terminate ~s plugin",[Name]), + ?DEBUG("** terminate ~s plugin",[Name]), Plugin = list_to_atom(?PLUGIN_PREFIX++Name), Plugin:terminate(Host, ServerHost) end, Plugins), TreePlugin:terminate(Host, ServerHost), ok. -init_nodes(Host, ServerHost, ServedHosts) -> - create_node(Host, ServerHost, ["pubsub"], service_jid(Host), ?STDNODE), - create_node(Host, ServerHost, ["pubsub", "nodes"], service_jid(Host), ?STDNODE), - create_node(Host, ServerHost, ["home"], service_jid(Host), ?STDNODE), - lists:foreach( - fun(H) -> - create_node(Host, ServerHost, ["home", H], service_jid(Host), ?STDNODE) - end, [ServerHost | ServedHosts]), - ok. +init_nodes(Host, ServerHost, _NodeTree, Plugins) -> + %% TODO, this call should be done plugin side + case lists:member("hometree", Plugins) of + true -> + create_node(Host, ServerHost, string_to_node("/home"), service_jid(Host), "hometree"), + create_node(Host, ServerHost, string_to_node("/home/"++ServerHost), service_jid(Host), "hometree"); + false -> + ok + end. -update_database(Host) -> +update_node_database(Host, ServerHost) -> + mnesia:del_table_index(pubsub_node, type), + mnesia:del_table_index(pubsub_node, parentid), case catch mnesia:table_info(pubsub_node, attributes) of [host_node, host_parent, info] -> - ?INFO_MSG("upgrade pubsub tables",[]), + ?INFO_MSG("upgrade node pubsub tables",[]), F = fun() -> - NewRecords = - lists:foldl( - fun({pubsub_node, NodeId, ParentId, {nodeinfo, Items, Options, Entities}}, RecList) -> - ItemsList = - lists:foldl( - fun({item, IID, Publisher, Payload}, Acc) -> - C = {Publisher, unknown}, - M = {Publisher, now()}, - mnesia:write( - #pubsub_item{itemid = {IID, NodeId}, - creation = C, - modification = M, - payload = Payload}), - [{Publisher, IID} | Acc] - end, [], Items), - Owners = - dict:fold( - fun(JID, {entity, Aff, Sub}, Acc) -> - UsrItems = - lists:foldl( - fun({P, I}, IAcc) -> - case P of - JID -> [I | IAcc]; - _ -> IAcc - end - end, [], ItemsList), - mnesia:write( - #pubsub_state{stateid = {JID, NodeId}, - items = UsrItems, - affiliation = Aff, - subscription = Sub}), - case Aff of - owner -> [JID | Acc]; - _ -> Acc - end - end, [], Entities), - mnesia:delete({pubsub_node, NodeId}), - [#pubsub_node{nodeid = NodeId, - parentid = ParentId, - owners = Owners, - options = Options} | - RecList] - end, [], - mnesia:match_object( - {pubsub_node, {Host, '_'}, '_', '_'})), - mnesia:delete_table(pubsub_node), - mnesia:create_table(pubsub_node, - [{disc_copies, [node()]}, - {attributes, record_info(fields, pubsub_node)}, - {index, [type, parentid]}]), - lists:foreach(fun(Record) -> - mnesia:write(Record) - end, NewRecords) + {Result, LastIdx} = lists:foldl( + fun({pubsub_node, NodeId, ParentId, {nodeinfo, Items, Options, Entities}}, {RecList, NodeIdx}) -> + ItemsList = + lists:foldl( + fun({item, IID, Publisher, Payload}, Acc) -> + C = {unknown, Publisher}, + M = {now(), Publisher}, + mnesia:write( + #pubsub_item{itemid = {IID, NodeIdx}, + creation = C, + modification = M, + payload = Payload}), + [{Publisher, IID} | Acc] + end, [], Items), + Owners = + dict:fold( + fun(JID, {entity, Aff, Sub}, Acc) -> + UsrItems = + lists:foldl( + fun({P, I}, IAcc) -> + case P of + JID -> [I | IAcc]; + _ -> IAcc + end + end, [], ItemsList), + mnesia:write({pubsub_state, + {JID, NodeIdx}, + UsrItems, + Aff, + Sub}), + case Aff of + owner -> [JID | Acc]; + _ -> Acc + end + end, [], Entities), + mnesia:delete({pubsub_node, NodeId}), + {[#pubsub_node{nodeid = NodeId, + id = NodeIdx, + parents = [element(2, ParentId)], + owners = Owners, + options = Options} | + RecList], NodeIdx + 1} + end, {[], 1}, + mnesia:match_object( + {pubsub_node, {Host, '_'}, '_', '_'})), + mnesia:write(#pubsub_index{index = node, last = LastIdx, free = []}), + Result + end, + {atomic, NewRecords} = mnesia:transaction(F), + {atomic, ok} = mnesia:delete_table(pubsub_node), + {atomic, ok} = mnesia:create_table(pubsub_node, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_node)}]), + FNew = fun() -> lists:foreach(fun(Record) -> + mnesia:write(Record) + end, NewRecords) + end, + case mnesia:transaction(FNew) of + {atomic, Result} -> + ?INFO_MSG("Pubsub node tables updated correctly: ~p", [Result]); + {aborted, Reason} -> + ?ERROR_MSG("Problem updating Pubsub node tables:~n~p", [Reason]) + end; + [nodeid, parentid, type, owners, options] -> + F = fun({pubsub_node, NodeId, {_, Parent}, Type, Owners, Options}) -> + #pubsub_node{ + nodeid = NodeId, + id = 0, + parents = [Parent], + type = Type, + owners = Owners, + options = Options} end, - mnesia:transaction(F); + mnesia:transform_table(pubsub_node, F, [nodeid, id, parents, type, owners, options]), + FNew = fun() -> + LastIdx = lists:foldl(fun(#pubsub_node{nodeid = NodeId} = PubsubNode, NodeIdx) -> + mnesia:write(PubsubNode#pubsub_node{id = NodeIdx}), + lists:foreach(fun(#pubsub_state{stateid = StateId} = State) -> + {JID, _} = StateId, + mnesia:delete({pubsub_state, StateId}), + mnesia:write(State#pubsub_state{stateid = {JID, NodeIdx}}) + end, mnesia:match_object(#pubsub_state{stateid = {'_', NodeId}, _ = '_'})), + lists:foreach(fun(#pubsub_item{itemid = ItemId} = Item) -> + {IID, _} = ItemId, + {M1, M2} = Item#pubsub_item.modification, + {C1, C2} = Item#pubsub_item.creation, + mnesia:delete({pubsub_item, ItemId}), + mnesia:write(Item#pubsub_item{itemid = {IID, NodeIdx}, + modification = {M2, M1}, + creation = {C2, C1}}) + end, mnesia:match_object(#pubsub_item{itemid = {'_', NodeId}, _ = '_'})), + NodeIdx + 1 + end, 1, mnesia:match_object( + {pubsub_node, {Host, '_'}, '_', '_', '_', '_', '_'}) + ++ mnesia:match_object( + {pubsub_node, {{'_', ServerHost, '_'}, '_'}, '_', '_', '_', '_', '_'})), + mnesia:write(#pubsub_index{index = node, last = LastIdx, free = []}) + end, + case mnesia:transaction(FNew) of + {atomic, Result} -> + rename_default_nodeplugin(), + ?INFO_MSG("Pubsub node tables updated correctly: ~p", [Result]); + {aborted, Reason} -> + ?ERROR_MSG("Problem updating Pubsub node tables:~n~p", [Reason]) + end; + [nodeid, id, parent, type, owners, options] -> + F = fun({pubsub_node, NodeId, Id, Parent, Type, Owners, Options}) -> + #pubsub_node{ + nodeid = NodeId, + id = Id, + parents = [Parent], + type = Type, + owners = Owners, + options = Options} + end, + mnesia:transform_table(pubsub_node, F, [nodeid, id, parents, type, owners, options]), + rename_default_nodeplugin(); + _ -> + ok + end, + mnesia:transaction(fun() -> + case catch mnesia:first(pubsub_node) of + {_, L} when is_list(L) -> + lists:foreach( + fun({H, N}) when is_list(N) -> + [Node] = mnesia:read({pubsub_node, {H, N}}), + Type = Node#pubsub_node.type, + BN = element(2, node_call(Type, path_to_node, [N])), + BP = case [element(2, node_call(Type, path_to_node, [P])) || P <- Node#pubsub_node.parents] of + [<<>>] -> []; + Parents -> Parents + end, + mnesia:write(Node#pubsub_node{nodeid={H, BN}, parents=BP}), + mnesia:delete({pubsub_node, {H, N}}); + (_) -> + ok + end, mnesia:all_keys(pubsub_node)); _ -> ok + end + end). + +rename_default_nodeplugin() -> + lists:foreach(fun(Node) -> + mnesia:dirty_write(Node#pubsub_node{type = "hometree"}) + end, mnesia:dirty_match_object(#pubsub_node{type = "default", _ = '_'})). + +update_state_database(_Host, _ServerHost) -> + case catch mnesia:table_info(pubsub_state, attributes) of + [stateid, items, affiliation, subscription] -> + ?INFO_MSG("upgrade state pubsub tables", []), + F = fun ({pubsub_state, {JID, NodeID}, Items, Aff, Sub}, Acc) -> + Subs = case Sub of + none -> + []; + _ -> + {result, SubID} = pubsub_subscription:subscribe_node(JID, NodeID, []), + [{Sub, SubID}] + end, + NewState = #pubsub_state{stateid = {JID, NodeID}, + items = Items, + affiliation = Aff, + subscriptions = Subs}, + [NewState | Acc] + end, + {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3, + [F, [], pubsub_state]), + {atomic, ok} = mnesia:delete_table(pubsub_state), + {atomic, ok} = mnesia:create_table(pubsub_state, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_state)}]), + FNew = fun () -> + lists:foreach(fun mnesia:write/1, NewRecs) + end, + case mnesia:transaction(FNew) of + {atomic, Result} -> + ?INFO_MSG("Pubsub state tables updated correctly: ~p", + [Result]); + {aborted, Reason} -> + ?ERROR_MSG("Problem updating Pubsub state tables:~n~p", + [Reason]) + end; + _ -> + ok + end. + +send_queue(State, Msg) -> + Pid = State#state.send_loop, + case is_process_alive(Pid) of + true -> + Pid ! Msg, + State; + false -> + SendLoop = spawn(?MODULE, send_loop, [State]), + SendLoop ! Msg, + State#state{send_loop = SendLoop} + end. + +send_loop(State) -> + receive + {presence, JID, Pid} -> + Host = State#state.host, + ServerHost = State#state.server_host, + LJID = jlib:jid_tolower(JID), + BJID = jlib:jid_remove_resource(LJID), + %% for each node From is subscribed to + %% and if the node is so configured, send the last published item to From + lists:foreach(fun(PType) -> + {result, Subscriptions} = node_action(Host, PType, get_entity_subscriptions, [Host, JID]), + lists:foreach( + fun({Node, subscribed, _, SubJID}) -> + if (SubJID == LJID) or (SubJID == BJID) -> + #pubsub_node{nodeid = {H, N}, type = Type, id = NodeId, options = Options} = Node, + case get_option(Options, send_last_published_item) of + on_sub_and_presence -> + send_items(H, N, NodeId, Type, LJID, last); + _ -> + ok + end; + true -> + % resource not concerned about that subscription + ok + end; + (_) -> + ok + end, Subscriptions) + end, State#state.plugins), + %% and force send the last PEP events published by its offline and local contacts + %% only if pubsub is explicitely configured for that. + %% this is a hack in a sense that PEP should only be based on presence + %% and is not able to "store" events of remote users (via s2s) + %% this makes that hack only work for local domain by now + if not State#state.ignore_pep_from_offline -> + {User, Server, Resource} = jlib:jid_tolower(JID), + case mod_caps:get_caps({User, Server, Resource}) of + nothing -> + %% we don't have caps, no need to handle PEP items + ok; + _ -> + case catch ejabberd_c2s:get_subscribed(Pid) of + Contacts when is_list(Contacts) -> + lists:foreach( + fun({U, S, R}) -> + case S of + ServerHost -> %% local contacts + case ejabberd_sm:get_user_resources(U, S) of + [] -> %% offline + PeerJID = jlib:make_jid(U, S, R), + self() ! {presence, User, Server, [Resource], PeerJID}; + _ -> %% online + % this is already handled by presence probe + ok + end; + _ -> %% remote contacts + % we can not do anything in any cases + ok + end + end, Contacts); + _ -> + ok + end + end; + true -> + ok + end, + send_loop(State); + {presence, User, Server, Resources, JID} -> + %% get resources caps and check if processing is needed + spawn(fun() -> + {HasCaps, ResourcesCaps} = lists:foldl(fun(Resource, {R, L}) -> + case mod_caps:get_caps({User, Server, Resource}) of + nothing -> {R, L}; + Caps -> {true, [{Resource, Caps} | L]} + end + end, {false, []}, Resources), + case HasCaps of + true -> + Host = State#state.host, + ServerHost = State#state.server_host, + Owner = jlib:jid_remove_resource(jlib:jid_tolower(JID)), + lists:foreach(fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = NodeId, options = Options}) -> + case get_option(Options, send_last_published_item) of + on_sub_and_presence -> + lists:foreach(fun({Resource, Caps}) -> + CapsNotify = case catch mod_caps:get_features(ServerHost, Caps) of + Features when is_list(Features) -> lists:member(node_to_string(Node) ++ "+notify", Features); + _ -> false + end, + case CapsNotify of + true -> + LJID = {User, Server, Resource}, + Subscribed = case get_option(Options, access_model) of + open -> true; + presence -> true; + whitelist -> false; % subscribers are added manually + authorize -> false; % likewise + roster -> + Grps = get_option(Options, roster_groups_allowed, []), + {OU, OS, _} = Owner, + element(2, get_roster_info(OU, OS, LJID, Grps)) + end, + if Subscribed -> + send_items(Owner, Node, NodeId, Type, LJID, last); + true -> + ok + end; + false -> + ok + end + end, ResourcesCaps); + _ -> + ok + end + end, tree_action(Host, get_nodes, [Owner, JID])); + false -> + ok + end + end), + send_loop(State); + stop -> + ok end. %% ------- @@ -306,7 +614,7 @@ update_database(Host) -> identity(Host) -> Identity = case lists:member(?PEPNODE, plugins(Host)) of true -> [{"category", "pubsub"}, {"type", "pep"}]; - false -> [{"category", "pubsub"}] + false -> [{"category", "pubsub"}, {"type", "service"}] end, {xmlelement, "identity", Identity, []}. @@ -355,60 +663,87 @@ disco_sm_features(Acc, From, To, Node, _Lang) -> Acc end. -disco_sm_items(Acc, _From, To, [], _Lang) -> - %% TODO, use iq_disco_items(Host, [], From) +disco_sm_items(Acc, From, To, [], _Lang) -> Host = To#jid.lserver, - LJID = jlib:jid_tolower(jlib:jid_remove_resource(To)), - case tree_action(Host, get_nodes, [Host]) of + case tree_action(Host, get_subnodes, [Host, <<>>, From]) of [] -> Acc; Nodes -> + SBJID = jlib:jid_to_string(jlib:jid_remove_resource(To)), Items = case Acc of {result, I} -> I; _ -> [] end, NodeItems = lists:map( - fun(Node) -> + fun(#pubsub_node{nodeid = {_, Node}}) -> {xmlelement, "item", - [{"jid", jlib:jid_to_string(LJID)}, - {"node", node_to_string(Node)}], + [{"jid", SBJID}|nodeAttr(Node)], []} end, Nodes), {result, NodeItems ++ Items} end; -disco_sm_items(Acc, _From, To, Node, _Lang) -> - %% TODO, use iq_disco_items(Host, Node, From) +disco_sm_items(Acc, From, To, SNode, _Lang) -> Host = To#jid.lserver, - LJID = jlib:jid_tolower(jlib:jid_remove_resource(To)), - case get_items(Host, Node) of - [] -> - Acc; - AllItems -> - Items = case Acc of + Node = string_to_node(SNode), + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + % TODO call get_items/6 instead for access control (EJAB-1033) + case node_call(Type, get_items, [NodeId, From]) of + {result, []} -> + none; + {result, AllItems} -> + SBJID = jlib:jid_to_string(jlib:jid_remove_resource(To)), + Items = case Acc of {result, I} -> I; _ -> [] end, - NodeItems = lists:map( - fun(#pubsub_item{itemid = Id}) -> - %% "jid" is required by XEP-0030, and - %% "node" is forbidden by XEP-0060. - {xmlelement, "item", - [{"jid", jlib:jid_to_string(LJID)}, - {"name", get_item_name(Host, Node, Id)}], - []} + NodeItems = lists:map( + fun(#pubsub_item{itemid = {Id, _}}) -> + {result, Name} = node_call(Type, get_item_name, [Host, Node, Id]), + {xmlelement, "item", [{"jid", SBJID}, {"name", Name}], []} end, AllItems), - {result, NodeItems ++ Items} + {result, NodeItems ++ Items}; + _ -> + none + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Items}} -> {result, Items}; + _ -> Acc end. %% ------- %% presence hooks handling functions %% -presence_probe(#jid{lserver = Host} = JID, JID, Pid) -> +presence_probe(#jid{luser = User, lserver = Server, lresource = Resource} = JID, JID, Pid) -> + Proc = gen_mod:get_module_proc(Server, ?PROCNAME), + gen_server:cast(Proc, {presence, JID, Pid}), + gen_server:cast(Proc, {presence, User, Server, [Resource], JID}); +presence_probe(#jid{luser = User, lserver = Server, lresource = Resource}, #jid{lserver = Host} = JID, _Pid) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:cast(Proc, {presence, JID, Pid}); -presence_probe(_, _, _) -> + gen_server:cast(Proc, {presence, User, Server, [Resource], JID}). + +%% ------- +%% subscription hooks handling functions +%% + +out_subscription(User, Server, JID, subscribed) -> + Owner = jlib:make_jid(User, Server, ""), + {PUser, PServer, PResource} = jlib:jid_tolower(JID), + PResources = case PResource of + [] -> user_resources(PUser, PServer); + _ -> [PResource] + end, + Proc = gen_mod:get_module_proc(Server, ?PROCNAME), + gen_server:cast(Proc, {presence, PUser, PServer, PResources, Owner}); +out_subscription(_,_,_,_) -> + ok. +in_subscription(_, User, Server, Owner, unsubscribed, _) -> + Subscriber = jlib:make_jid(User, Server, ""), + Proc = gen_mod:get_module_proc(Server, ?PROCNAME), + gen_server:cast(Proc, {unsubscribe, Subscriber, Owner}); +in_subscription(_, _, _, _, _, _) -> ok. %% ------- @@ -419,16 +754,16 @@ remove_user(User, Server) -> LUser = jlib:nodeprep(User), LServer = jlib:nameprep(Server), Proc = gen_mod:get_module_proc(Server, ?PROCNAME), - gen_server:cast(Proc, {remove, LUser, LServer}). + gen_server:cast(Proc, {remove_user, LUser, LServer}). %%-------------------------------------------------------------------- %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} %% Description: Handling call messages %%-------------------------------------------------------------------- %% @private @@ -436,6 +771,8 @@ handle_call(server_host, _From, State) -> {reply, State#state.server_host, State}; handle_call(plugins, _From, State) -> {reply, State#state.plugins, State}; +handle_call(pep_mapping, _From, State) -> + {reply, State#state.pep_mapping, State}; handle_call(nodetree, _From, State) -> {reply, State#state.nodetree, State}; handle_call(stop, _From, State) -> @@ -443,91 +780,77 @@ handle_call(stop, _From, State) -> %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} +%% {noreply, State, Timeout} | +%% {stop, Reason, State} %% Description: Handling cast messages %%-------------------------------------------------------------------- %% @private handle_cast({presence, JID, Pid}, State) -> %% A new resource is available. send last published items - LJID = jlib:jid_tolower(JID), - Host = State#state.host, - ServerHost = State#state.server_host, - %% for each node From is subscribed to - %% and if the node is so configured, send the last published item to From - lists:foreach(fun(Type) -> - {result, Subscriptions} = node_action(Type, get_entity_subscriptions, [Host, JID]), - lists:foreach( - fun({Node, subscribed}) -> - case tree_action(Host, get_node, [Host, Node]) of - #pubsub_node{options = Options} -> - case get_option(Options, send_last_published_item) of - on_sub_and_presence -> - send_last_item(Host, Node, LJID); - _ -> - ok - end; - _ -> - ok - end; + {noreply, send_queue(State, {presence, JID, Pid})}; + +handle_cast({presence, User, Server, Resources, JID}, State) -> + %% A new resource is available. send last published PEP items + {noreply, send_queue(State, {presence, User, Server, Resources, JID})}; + +handle_cast({remove_user, LUser, LServer}, State) -> + spawn(fun() -> + Host = State#state.host, + Owner = jlib:make_jid(LUser, LServer, ""), + %% remove user's subscriptions + lists:foreach(fun(PType) -> + {result, Subscriptions} = node_action(Host, PType, get_entity_subscriptions, [Host, Owner]), + lists:foreach(fun + ({#pubsub_node{nodeid = {H, N}}, subscribed, _, JID}) -> + unsubscribe_node(H, N, Owner, JID, all); (_) -> - ok - end, Subscriptions) - end, State#state.plugins), - %% and send to From last PEP events published by its contacts - case catch ejabberd_c2s:get_subscribed_and_online(Pid) of - ContactsWithCaps when is_list(ContactsWithCaps) -> - Caps = proplists:get_value(LJID, ContactsWithCaps), - ContactsUsers = lists:usort(lists:map( - fun({{User, Server, _}, _}) -> {User, Server} end, ContactsWithCaps)), - lists:foreach( - fun({User, Server}) -> - PepKey = {User, Server, ""}, - lists:foreach(fun(#pubsub_node{nodeid = {_, Node}, options = Options}) -> - case get_option(Options, send_last_published_item) of - on_sub_and_presence -> - case is_caps_notify(ServerHost, Node, Caps) of - true -> - Subscribed = case get_option(Options, access_model) of - open -> true; - presence -> true; - whitelist -> false; % subscribers are added manually - authorize -> false; % likewise - roster -> - Grps = get_option(Options, roster_groups_allowed), - element(2, get_roster_info(User, Server, LJID, Grps)) - end, - if Subscribed -> - ?DEBUG("send ~s's ~s event to ~s",[jlib:jid_to_string(PepKey),Node,jlib:jid_to_string(LJID)]), - send_last_item(PepKey, Node, LJID); - true -> - ok - end; - false -> - ok - end; - _ -> - ok - end - end, tree_action(Host, get_nodes, [PepKey])) - end, ContactsUsers); - _ -> - ok - end, + ok + end, Subscriptions), + {result, Affiliations} = node_action(Host, PType, get_entity_affiliations, [Host, Owner]), + lists:foreach(fun + ({#pubsub_node{nodeid = {H, N}}, owner}) -> + delete_node(H, N, Owner); + (_) -> + ok + end, Affiliations) + end, State#state.plugins) + end), {noreply, State}; -handle_cast({remove, User, Server}, State) -> - Owner = jlib:make_jid(User, Server, ""), - delete_nodes(Server, Owner), - {noreply, State}; +handle_cast({unsubscribe, Subscriber, Owner}, State) -> + spawn(fun() -> + Host = State#state.host, + BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + lists:foreach(fun(PType) -> + {result, Subscriptions} = node_action(Host, PType, get_entity_subscriptions, [Host, Subscriber]), + lists:foreach(fun + ({Node, subscribed, _, JID}) -> + #pubsub_node{options = Options, owners = Owners, type = Type, id = NodeId} = Node, + case get_option(Options, access_model) of + presence -> + case lists:member(BJID, Owners) of + true -> + node_action(Host, Type, unsubscribe_node, [NodeId, Subscriber, JID, all]); + false -> + {result, ok} + end; + _ -> + {result, ok} + end; + (_) -> + ok + end, Subscriptions) + end, State#state.plugins) + end), + {noreply, State}; handle_cast(_Msg, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} +%% {noreply, State, Timeout} | +%% {stop, Reason, State} %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- %% @private @@ -554,25 +877,32 @@ handle_info(_Info, State) -> terminate(_Reason, #state{host = Host, server_host = ServerHost, nodetree = TreePlugin, - plugins = Plugins}) -> - terminate_plugins(Host, ServerHost, Plugins, TreePlugin), + plugins = Plugins, + send_loop = SendLoop}) -> ejabberd_router:unregister_route(Host), - ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 75), - ejabberd_hooks:delete(disco_local_features, ServerHost, ?MODULE, disco_local_features, 75), - ejabberd_hooks:delete(disco_local_items, ServerHost, ?MODULE, disco_local_items, 75), + case lists:member(?PEPNODE, Plugins) of + true -> + ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 75), + ejabberd_hooks:delete(disco_local_features, ServerHost, ?MODULE, disco_local_features, 75), + ejabberd_hooks:delete(disco_local_items, ServerHost, ?MODULE, disco_local_items, 75), + gen_iq_handler:remove_iq_handler(ejabberd_local, ServerHost, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_local, ServerHost, ?NS_PUBSUB_OWNER); + false -> + ok + end, ejabberd_hooks:delete(disco_sm_identity, ServerHost, ?MODULE, disco_sm_identity, 75), ejabberd_hooks:delete(disco_sm_features, ServerHost, ?MODULE, disco_sm_features, 75), ejabberd_hooks:delete(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75), - ejabberd_hooks:delete(presence_probe_hook, ServerHost, ?MODULE, presence_probe, 50), + ejabberd_hooks:delete(presence_probe_hook, ServerHost, ?MODULE, presence_probe, 80), + ejabberd_hooks:delete(roster_in_subscription, ServerHost, ?MODULE, in_subscription, 50), + ejabberd_hooks:delete(roster_out_subscription, ServerHost, ?MODULE, out_subscription, 50), ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, remove_user, 50), - lists:foreach(fun({NS,Mod}) -> - gen_iq_handler:remove_iq_handler(Mod, ServerHost, NS) - end, [{?NS_PUBSUB, ejabberd_local}, - {?NS_PUBSUB_OWNER, ejabberd_local}, - {?NS_PUBSUB, ejabberd_sm}, - {?NS_PUBSUB_OWNER, ejabberd_sm}]), + ejabberd_hooks:delete(anonymous_purge_hook, ServerHost, ?MODULE, remove_user, 50), + gen_iq_handler:remove_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB_OWNER), mod_disco:unregister_feature(ServerHost, ?NS_PUBSUB), - ok. + SendLoop ! stop, + terminate_plugins(Host, ServerHost, Plugins, TreePlugin). %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} @@ -596,12 +926,15 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> sub_el = SubEl, lang = Lang} = IQ -> {xmlelement, _, QAttrs, _} = SubEl, Node = xml:get_attr_s("node", QAttrs), + Info = ejabberd_hooks:run_fold( + disco_info, ServerHost, [], + [ServerHost, ?MODULE, "", ""]), Res = case iq_disco_info(Host, Node, From, Lang) of {result, IQRes} -> jlib:iq_to_xml( IQ#iq{type = result, sub_el = [{xmlelement, "query", - QAttrs, IQRes}]}); + QAttrs, IQRes++Info}]}); {error, Error} -> jlib:make_error_reply(Packet, Error) end, @@ -635,7 +968,7 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> #iq{type = IQType, xmlns = ?NS_PUBSUB_OWNER, lang = Lang, sub_el = SubEl} = IQ -> Res = - case iq_pubsub_owner(Host, From, IQType, SubEl, Lang) of + case iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) of {result, IQRes} -> jlib:iq_to_xml( IQ#iq{type = result, @@ -650,6 +983,15 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> sub_el = [{xmlelement, "vCard", [{"xmlns", XMLNS}], iq_get_vcard(Lang)}]}, ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); + #iq{type = set, xmlns = ?NS_COMMANDS} = IQ -> + Res = case iq_command(Host, ServerHost, From, IQ, Access, Plugins) of + {error, Error} -> + jlib:make_error_reply(Packet, Error); + {result, IQRes} -> + jlib:iq_to_xml(IQ#iq{type = result, + sub_el = IQRes}) + end, + ejabberd_router:route(To, From, Res); #iq{} -> Err = jlib:make_error_reply( Packet, @@ -696,7 +1038,7 @@ node_disco_features(Host, Node, From) -> node_disco_info(Host, Node, From, false, true). node_disco_info(Host, Node, From, Identity, Features) -> Action = - fun(#pubsub_node{type = Type}) -> + fun(#pubsub_node{type = Type, id = NodeId}) -> I = case Identity of false -> []; @@ -706,7 +1048,7 @@ node_disco_info(Host, Node, From, Identity, Features) -> [] -> ["leaf"]; %% No sub-nodes: it's a leaf node _ -> - case node_call(Type, get_items, [Host, Node]) of + case node_call(Type, get_items, [NodeId, From]) of {result, []} -> ["collection"]; {result, _} -> ["leaf", "collection"]; _ -> [] @@ -729,74 +1071,73 @@ node_disco_info(Host, Node, From, Identity, Features) -> %% TODO: add meta-data info (spec section 5.4) {result, I ++ F} end, - transaction(Host, Node, Action, sync_dirty). + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end. iq_disco_info(Host, SNode, From, Lang) -> - Node = string_to_node(SNode), + [RealSNode|_] = case SNode of + [] -> [[]]; + _ -> string:tokens(SNode, "!") + end, + Node = string_to_node(RealSNode), case Node of - [] -> + <<>> -> {result, [{xmlelement, "identity", [{"category", "pubsub"}, {"type", "service"}, {"name", translate:translate(Lang, "Publish-Subscribe")}], []}, - {xmlelement, "feature", [{"var", ?NS_PUBSUB}], []}, - {xmlelement, "feature", [{"var", ?NS_VCARD}], []}] ++ + {xmlelement, "feature", [{"var", ?NS_DISCO_INFO}], []}, + {xmlelement, "feature", [{"var", ?NS_DISCO_ITEMS}], []}, + {xmlelement, "feature", [{"var", ?NS_PUBSUB}], []}, + {xmlelement, "feature", [{"var", ?NS_VCARD}], []}] ++ lists:map(fun(Feature) -> {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++Feature}], []} - end, features(Host, SNode))}; + end, features(Host, Node))}; _ -> node_disco_info(Host, Node, From) end. iq_disco_items(Host, [], From) -> {result, lists:map( - fun(#pubsub_node{nodeid = {_, SubNode}}) -> - SN = node_to_string(SubNode), - RN = lists:last(SubNode), - %% remove name attribute - {xmlelement, "item", [{"jid", Host}, - {"node", SN}, - {"name", RN}], []} - end, tree_action(Host, get_subnodes, [Host, [], From]))}; + fun(#pubsub_node{nodeid = {_, SubNode}, type = Type}) -> + {result, Path} = node_call(Type, node_to_path, [SubNode]), + [Name|_] = lists:reverse(Path), + {xmlelement, "item", [{"jid", Host}, {"name", Name}|nodeAttr(SubNode)], []} + end, tree_action(Host, get_subnodes, [Host, <<>>, From]))}; iq_disco_items(Host, Item, From) -> case string:tokens(Item, "!") of [_SNode, _ItemID] -> {result, []}; [SNode] -> Node = string_to_node(SNode), - %% Note: Multiple Node Discovery not supported (mask on pubsub#type) - %% TODO this code is also back-compatible with pubsub v1.8 (for client issue) - %% TODO make it pubsub v1.10 compliant (this breaks client compatibility) - %% TODO That is, remove name attribute Action = - fun(#pubsub_node{type = Type}) -> - NodeItems = case node_call(Type, get_items, [Host, Node]) of + fun(#pubsub_node{type = Type, id = NodeId}) -> + % TODO call get_items/6 instead for access control (EJAB-1033) + NodeItems = case node_call(Type, get_items, [NodeId, From]) of {result, I} -> I; _ -> [] end, Nodes = lists:map( fun(#pubsub_node{nodeid = {_, SubNode}}) -> - SN = node_to_string(SubNode), - RN = lists:last(SubNode), - {xmlelement, "item", [{"jid", Host}, {"node", SN}, - {"name", RN}], []} + {xmlelement, "item", [{"jid", Host}|nodeAttr(SubNode)], []} end, tree_call(Host, get_subnodes, [Host, Node, From])), Items = lists:map( fun(#pubsub_item{itemid = {RN, _}}) -> - SN = node_to_string(Node) ++ "!" ++ RN, - {xmlelement, "item", [{"jid", Host}, {"node", SN}, - {"name", get_item_name(Host, Node, RN)}], []} + {result, Name} = node_call(Type, get_item_name, [Host, Node, RN]), + {xmlelement, "item", [{"jid", Host}, {"name", Name}], []} end, NodeItems), {result, Nodes ++ Items} end, - transaction(Host, Node, Action, sync_dirty) + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end end. -iq_local(From, To, #iq{type = Type, - sub_el = SubEl, - xmlns = XMLNS, - lang = Lang} = IQ) -> +iq_local(From, To, #iq{type = Type, sub_el = SubEl, xmlns = XMLNS, lang = Lang} = IQ) -> ServerHost = To#jid.lserver, %% Accept IQs to server only from our own users. if @@ -806,7 +1147,7 @@ iq_local(From, To, #iq{type = Type, LOwner = jlib:jid_tolower(jlib:jid_remove_resource(From)), Res = case XMLNS of ?NS_PUBSUB -> iq_pubsub(LOwner, ServerHost, From, Type, SubEl, Lang); - ?NS_PUBSUB_OWNER -> iq_pubsub_owner(LOwner, From, Type, SubEl, Lang) + ?NS_PUBSUB_OWNER -> iq_pubsub_owner(LOwner, ServerHost, From, Type, SubEl, Lang) end, case Res of {result, IQRes} -> IQ#iq{type = result, sub_el = IQRes}; @@ -819,7 +1160,7 @@ iq_sm(From, To, #iq{type = Type, sub_el = SubEl, xmlns = XMLNS, lang = Lang} = I LOwner = jlib:jid_tolower(jlib:jid_remove_resource(To)), Res = case XMLNS of ?NS_PUBSUB -> iq_pubsub(LOwner, ServerHost, From, Type, SubEl, Lang); - ?NS_PUBSUB_OWNER -> iq_pubsub_owner(LOwner, From, Type, SubEl, Lang) + ?NS_PUBSUB_OWNER -> iq_pubsub_owner(LOwner, ServerHost, From, Type, SubEl, Lang) end, case Res of {result, IQRes} -> IQ#iq{type = result, sub_el = IQRes}; @@ -833,50 +1174,37 @@ iq_get_vcard(Lang) -> [{xmlcdata, translate:translate(Lang, "ejabberd Publish-Subscribe module") ++ - "\nCopyright (c) 2004-2008 Process-One"}]}]. + "\nCopyright (c) 2004-2009 Process-One"}]}]. iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang) -> iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, all, plugins(ServerHost)). -iq_pubsub(Host, ServerHost, From, IQType, SubEl, _Lang, Access, Plugins) -> +iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) -> {xmlelement, _, _, SubEls} = SubEl, - WithoutCdata = xml:remove_cdata(SubEls), - Configuration = lists:filter(fun({xmlelement, Name, _, _}) -> - Name == "configure" - end, WithoutCdata), - Action = WithoutCdata -- Configuration, - case Action of - [{xmlelement, Name, Attrs, Els}] -> - Node = case Host of - {_, _, _} -> xml:get_attr_s("node", Attrs); - _ -> string_to_node(xml:get_attr_s("node", Attrs)) - end, + case xml:remove_cdata(SubEls) of + [{xmlelement, Name, Attrs, Els} | Rest] -> + Node = string_to_node(xml:get_attr_s("node", Attrs)), case {IQType, Name} of {set, "create"} -> - case Configuration of - [{xmlelement, "configure", _, Config}] -> - %% Get the type of the node - Type = case xml:get_attr_s("type", Attrs) of - [] -> hd(Plugins); - T -> T - end, - %% we use Plugins list matching because we do not want to allocate - %% atoms for non existing type, this prevent atom allocation overflow - case lists:member(Type, Plugins) of - false -> - {error, extended_error( - ?ERR_FEATURE_NOT_IMPLEMENTED, - unsupported, "create-nodes")}; - true -> - create_node(Host, ServerHost, Node, From, - Type, Access, Config) - end; - _ -> - %% this breaks backward compatibility! - %% can not create node without <configure/> - %% but this is the new spec anyway - ?INFO_MSG("Node ~p ; invalid configuration: ~p", [Node, Configuration]), - {error, ?ERR_BAD_REQUEST} + Config = case Rest of + [{xmlelement, "configure", _, C}] -> C; + _ -> [] + end, + %% Get the type of the node + Type = case xml:get_attr_s("type", Attrs) of + [] -> hd(Plugins); + T -> T + end, + %% we use Plugins list matching because we do not want to allocate + %% atoms for non existing type, this prevent atom allocation overflow + case lists:member(Type, Plugins) of + false -> + {error, extended_error( + ?ERR_FEATURE_NOT_IMPLEMENTED, + unsupported, "create-nodes")}; + true -> + create_node(Host, ServerHost, Node, From, + Type, Access, Config) end; {set, "publish"} -> case xml:remove_cdata(Els) of @@ -908,39 +1236,58 @@ iq_pubsub(Host, ServerHost, From, IQType, SubEl, _Lang, Access, Plugins) -> "item-required")} end; {set, "subscribe"} -> + Config = case Rest of + [{xmlelement, "options", _, C}] -> C; + _ -> [] + end, JID = xml:get_attr_s("jid", Attrs), - subscribe_node(Host, Node, From, JID); + subscribe_node(Host, Node, From, JID, Config); {set, "unsubscribe"} -> JID = xml:get_attr_s("jid", Attrs), SubId = xml:get_attr_s("subid", Attrs), unsubscribe_node(Host, Node, From, JID, SubId); {get, "items"} -> MaxItems = xml:get_attr_s("max_items", Attrs), - get_items(Host, Node, From, MaxItems); + SubId = xml:get_attr_s("subid", Attrs), + ItemIDs = lists:foldl(fun + ({xmlelement, "item", ItemAttrs, _}, Acc) -> + case xml:get_attr_s("id", ItemAttrs) of + "" -> Acc; + ItemID -> [ItemID|Acc] + end; + (_, Acc) -> + Acc + end, [], xml:remove_cdata(Els)), + get_items(Host, Node, From, SubId, MaxItems, ItemIDs); {get, "subscriptions"} -> - get_subscriptions(Host, From, Plugins); + get_subscriptions(Host, Node, From, Plugins); {get, "affiliations"} -> get_affiliations(Host, From, Plugins); + {get, "options"} -> + SubID = xml:get_attr_s("subid", Attrs), + JID = xml:get_attr_s("jid", Attrs), + get_options(Host, Node, JID, SubID, Lang); + {set, "options"} -> + SubID = xml:get_attr_s("subid", Attrs), + JID = xml:get_attr_s("jid", Attrs), + set_options(Host, Node, JID, SubID, Els); _ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} end; - _ -> - ?INFO_MSG("Too many actions: ~p", [Action]), + Other -> + ?INFO_MSG("Too many actions: ~p", [Other]), {error, ?ERR_BAD_REQUEST} end. -iq_pubsub_owner(Host, From, IQType, SubEl, Lang) -> +iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) -> {xmlelement, _, _, SubEls} = SubEl, Action = xml:remove_cdata(SubEls), case Action of [{xmlelement, Name, Attrs, Els}] -> - Node = case Host of - {_, _, _} -> xml:get_attr_s("node", Attrs); - _ -> string_to_node(xml:get_attr_s("node", Attrs)) - end, + Node = string_to_node(xml:get_attr_s("node", Attrs)), case {IQType, Name} of {get, "configure"} -> - get_configure(Host, Node, From, Lang); + get_configure(Host, ServerHost, Node, From, Lang); {set, "configure"} -> set_configure(Host, Node, From, Els, Lang); {get, "default"} -> @@ -965,9 +1312,139 @@ iq_pubsub_owner(Host, From, IQType, SubEl, Lang) -> {error, ?ERR_BAD_REQUEST} end. +iq_command(Host, ServerHost, From, IQ, Access, Plugins) -> + case adhoc:parse_request(IQ) of + Req when is_record(Req, adhoc_request) -> + case adhoc_request(Host, ServerHost, From, Req, Access, Plugins) of + Resp when is_record(Resp, adhoc_response) -> + {result, [adhoc:produce_response(Req, Resp)]}; + Error -> + Error + end; + Err -> + Err + end. + +%% @doc <p>Processes an Ad Hoc Command.</p> +adhoc_request(Host, _ServerHost, Owner, + #adhoc_request{node = ?NS_PUBSUB_GET_PENDING, + lang = Lang, + action = "execute", + xdata = false}, + _Access, Plugins) -> + send_pending_node_form(Host, Owner, Lang, Plugins); +adhoc_request(Host, _ServerHost, Owner, + #adhoc_request{node = ?NS_PUBSUB_GET_PENDING, + action = "execute", + xdata = XData}, + _Access, _Plugins) -> + ParseOptions = case XData of + {xmlelement, "x", _Attrs, _SubEls} = XEl -> + case jlib:parse_xdata_submit(XEl) of + invalid -> + {error, ?ERR_BAD_REQUEST}; + XData2 -> + case set_xoption(Host, XData2, []) of + NewOpts when is_list(NewOpts) -> + {result, NewOpts}; + Err -> + Err + end + end; + _ -> + ?INFO_MSG("Bad XForm: ~p", [XData]), + {error, ?ERR_BAD_REQUEST} + end, + case ParseOptions of + {result, XForm} -> + case lists:keysearch(node, 1, XForm) of + {value, {_, Node}} -> + send_pending_auth_events(Host, Node, Owner); + false -> + {error, extended_error(?ERR_BAD_REQUEST, "bad-payload")} + end; + Error -> + Error + end; +adhoc_request(_Host, _ServerHost, _Owner, Other, _Access, _Plugins) -> + ?DEBUG("Couldn't process ad hoc command:~n~p", [Other]), + {error, ?ERR_ITEM_NOT_FOUND}. + +%% @spec (Host, Owner, Lang, Plugins) -> iqRes() +%% @doc <p>Sends the process pending subscriptions XForm for Host to +%% Owner.</p> +send_pending_node_form(Host, Owner, _Lang, Plugins) -> + Filter = + fun (Plugin) -> + lists:member("get-pending", features(Plugin)) + end, + case lists:filter(Filter, Plugins) of + [] -> + {error, ?ERR_FEATURE_NOT_IMPLEMENTED}; + Ps -> + XOpts = lists:map(fun (Node) -> + {xmlelement, "option", [], + [{xmlelement, "value", [], + [{xmlcdata, node_to_string(Node)}]}]} + end, get_pending_nodes(Host, Owner, Ps)), + XForm = {xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], + [{xmlelement, "field", + [{"type", "list-single"}, {"var", "pubsub#node"}], + lists:usort(XOpts)}]}, + #adhoc_response{status = executing, + defaultaction = "execute", + elements = [XForm]} + end. + +get_pending_nodes(Host, Owner, Plugins) -> + Tr = + fun (Type) -> + case node_call(Type, get_pending_nodes, [Host, Owner]) of + {result, Nodes} -> Nodes; + _ -> [] + end + end, + case transaction(fun () -> {result, lists:flatmap(Tr, Plugins)} end, + sync_dirty) of + {result, Res} -> Res; + Err -> Err + end. + +%% @spec (Host, Node, Owner) -> iqRes() +%% @doc <p>Send a subscription approval form to Owner for all pending +%% subscriptions on Host and Node.</p> +send_pending_auth_events(Host, Node, Owner) -> + ?DEBUG("Sending pending auth events for ~s on ~s:~s", + [jlib:jid_to_string(Owner), Host, node_to_string(Node)]), + Action = + fun (#pubsub_node{id = NodeID, type = Type} = N) -> + case lists:member("get-pending", features(Type)) of + true -> + case node_call(Type, get_affiliation, [NodeID, Owner]) of + {result, owner} -> + {result, Subscriptions} = node_call(Type, get_node_subscriptions, [NodeID]), + lists:foreach(fun({J, pending, _SubID}) -> send_authorization_request(N, jlib:make_jid(J)); + ({J, pending}) -> send_authorization_request(N, jlib:make_jid(J)); + (_) -> ok + end, Subscriptions), + {result, ok}; + _ -> + {error, ?ERR_FORBIDDEN} + end; + false -> + {error, ?ERR_FEATURE_NOT_IMPLEMENTED} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, _} -> + #adhoc_response{}; + Err -> + Err + end. + %%% authorization handling -send_authorization_request(Host, Node, Subscriber) -> +send_authorization_request(#pubsub_node{owners = Owners, nodeid = {Host, Node}}, Subscriber) -> Lang = "en", %% TODO fix Stanza = {xmlelement, "message", [], @@ -992,18 +1469,11 @@ send_authorization_request(Host, Node, Subscriber) -> {xmlelement, "field", [{"var", "pubsub#allow"}, {"type", "boolean"}, - {"label", translate:translate(Lang, "Allow this JID to subscribe to this pubsub node?")}], + {"label", translate:translate(Lang, "Allow this Jabber ID to subscribe to this pubsub node?")}], [{xmlelement, "value", [], [{xmlcdata, "false"}]}]}]}]}, - case tree_action(Host, get_node, [Host, Node]) of - #pubsub_node{owners = Owners} -> - lists:foreach( - fun(Owner) -> - ejabberd_router ! {route, service_jid(Host), jlib:make_jid(Owner), Stanza} - end, Owners), - ok; - _ -> - ok - end. + lists:foreach(fun(Owner) -> + ejabberd_router ! {route, service_jid(Host), jlib:make_jid(Owner), Stanza} + end, Owners). find_authorization_response(Packet) -> {xmlelement, _Name, _Attrs, Els} = Packet, @@ -1027,46 +1497,56 @@ find_authorization_response(Packet) -> [invalid] -> invalid; [] -> none; [XFields] when is_list(XFields) -> + ?DEBUG("XFields: ~p", [XFields]), case lists:keysearch("FORM_TYPE", 1, XFields) of - {value, {_, ?NS_PUBSUB_SUB_AUTH}} -> + {value, {_, [?NS_PUBSUB_SUB_AUTH]}} -> XFields; _ -> invalid end end. +%% @spec (Host, JID, Node, Subscription) -> void +%% Host = mod_pubsub:host() +%% JID = jlib:jid() +%% SNode = string() +%% Subscription = atom() | {atom(), mod_pubsub:subid()} +%% @doc Send a message to JID with the supplied Subscription +send_authorization_approval(Host, JID, SNode, Subscription) -> + SubAttrs = case Subscription of + {S, SID} -> [{"subscription", subscription_to_string(S)}, + {"subid", SID}]; + S -> [{"subscription", subscription_to_string(S)}] + end, + Stanza = event_stanza( + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(JID)}|nodeAttr(SNode)] ++ SubAttrs, + []}]), + ejabberd_router ! {route, service_jid(Host), JID, Stanza}. + handle_authorization_response(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, {_, SNode}}, {value, {_, SSubscriber}}, - {value, {_, SAllow}}} -> - Node = case Host of - {_, _, _} -> [SNode]; - _ -> string:tokens(SNode, "/") - end, + {{value, {_, [SNode]}}, {value, {_, [SSubscriber]}}, + {value, {_, [SAllow]}}} -> + Node = string_to_node(SNode), Subscriber = jlib:string_to_jid(SSubscriber), Allow = case SAllow of "1" -> true; "true" -> true; _ -> false end, - Action = fun(#pubsub_node{type = Type, - %%options = Options, - owners = Owners}) -> + Action = fun(#pubsub_node{type = Type, owners = Owners, id = NodeId}) -> IsApprover = lists:member(jlib:jid_tolower(jlib:jid_remove_resource(From)), Owners), - Subscription = node_call(Type, get_subscription, [Host, Node, Subscriber]), + {result, Subscriptions} = node_call(Type, get_subscriptions, [NodeId, Subscriber]), if not IsApprover -> {error, ?ERR_FORBIDDEN}; - Subscription /= pending -> - {error, ?ERR_UNEXPECTED_REQUEST}; true -> - NewSubscription = case Allow of - true -> subscribed; - false -> none - end, - node_call(Type, set_subscription, [Host, Node, Subscriber, NewSubscription]) + update_auth(Host, SNode, Type, NodeId, + Subscriber, Allow, + Subscriptions) end end, case transaction(Host, Node, Action, sync_dirty) of @@ -1074,7 +1554,7 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> ejabberd_router:route( To, From, jlib:make_error_reply(Packet, Error)); - {result, _NewSubscription} -> + {result, {_, _NewSubscription}} -> %% XXX: notify about subscription state change, section 12.11 ok; _ -> @@ -1088,6 +1568,26 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> jlib:make_error_reply(Packet, ?ERR_NOT_ACCEPTABLE)) end. +update_auth(Host, Node, Type, NodeId, Subscriber, + Allow, Subscriptions) -> + Subscription = lists:filter(fun({pending, _}) -> true; + (_) -> false + end, Subscriptions), + case Subscription of + [{pending, SubID}] -> %% TODO does not work if several pending + NewSubscription = case Allow of + true -> subscribed; + false -> none + end, + node_call(Type, set_subscriptions, + [NodeId, Subscriber, NewSubscription, SubID]), + send_authorization_approval(Host, Subscriber, Node, + NewSubscription), + {result, ok}; + _ -> + {error, ?ERR_UNEXPECTED_REQUEST} + end. + -define(XFIELD(Type, Label, Var, Val), {xmlelement, "field", [{"type", Type}, {"label", translate:translate(Lang, Label)}, @@ -1104,6 +1604,12 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> -define(STRINGXFIELD(Label, Var, Val), ?XFIELD("text-single", Label, Var, Val)). +-define(STRINGMXFIELD(Label, Var, Vals), + {xmlelement, "field", [{"type", "text-multi"}, + {"label", translate:translate(Lang, Label)}, + {"var", Var}], + [{xmlelement, "value", [], [{xmlcdata, V}]} || V <- Vals]}). + -define(XFIELDOPT(Type, Label, Var, Val, Opts), {xmlelement, "field", [{"type", Type}, {"label", translate:translate(Lang, Label)}, @@ -1118,6 +1624,20 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> -define(LISTXFIELD(Label, Var, Val, Opts), ?XFIELDOPT("list-single", Label, Var, Val, Opts)). +-define(LISTMXFIELD(Label, Var, Vals, Opts), + {xmlelement, "field", [{"type", "list-multi"}, + {"label", translate:translate(Lang, Label)}, + {"var", Var}], + lists:map(fun(Opt) -> + {xmlelement, "option", [], + [{xmlelement, "value", [], + [{xmlcdata, Opt}]}]} + end, Opts) ++ + lists:map(fun(Val) -> + {xmlelement, "value", [], + [{xmlcdata, Val}]} + end, Vals)}). + %% @spec (Host::host(), ServerHost::host(), Node::pubsubNode(), Owner::jid(), NodeType::nodeType()) -> %% {error, Reason::stanzaError()} | %% {result, []} @@ -1140,33 +1660,25 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> %%</ul> create_node(Host, ServerHost, Node, Owner, Type) -> create_node(Host, ServerHost, Node, Owner, Type, all, []). - -create_node(Host, ServerHost, [], Owner, Type, Access, Configuration) -> +create_node(Host, ServerHost, <<>>, Owner, Type, Access, Configuration) -> case lists:member("instant-nodes", features(Type)) of true -> - {LOU, LOS, _} = jlib:jid_tolower(Owner), - HomeNode = ["home", LOS, LOU], - create_node(Host, ServerHost, - HomeNode, Owner, Type, Access, Configuration), - NewNode = HomeNode ++ [randoms:get_string()], + NewNode = string_to_node(randoms:get_string()), case create_node(Host, ServerHost, NewNode, Owner, Type, Access, Configuration) of {result, []} -> {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], - [{xmlelement, "create", [{"node", node_to_string(NewNode)}], []}]}]}; - Error -> Error + [{xmlelement, "create", nodeAttr(NewNode), []}]}]}; + Error -> + Error end; false -> %% Service does not support instant nodes {error, extended_error(?ERR_NOT_ACCEPTABLE, "nodeid-required")} end; create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> - Type = case Host of - {_User, _Server, _Resource} -> ?PEPNODE; - _ -> GivenType - end, - Parent = lists:sublist(Node, length(Node) - 1), + Type = select_type(ServerHost, Host, Node, GivenType), %% TODO, check/set node_type = Type ParseOptions = case xml:remove_cdata(Configuration) of [] -> @@ -1176,7 +1688,7 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> invalid -> {error, ?ERR_BAD_REQUEST}; XData -> - case set_xoption(XData, node_options(Type)) of + case set_xoption(Host, XData, node_options(Type)) of NewOpts when is_list(NewOpts) -> {result, NewOpts}; Err -> @@ -1191,16 +1703,22 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> {result, NodeOptions} -> CreateNode = fun() -> + SNode = node_to_string(Node), + Parent = case node_call(Type, node_to_path, [Node]) of + {result, [SNode]} -> <<>>; + {result, Path} -> element(2, node_call(Type, path_to_node, [lists:sublist(Path, length(Path)-1)])) + end, + Parents = case Parent of + <<>> -> []; + _ -> [Parent] + end, case node_call(Type, create_node_permission, [Host, ServerHost, Node, Parent, Owner, Access]) of {result, true} -> - case tree_call(Host, create_node, [Host, Node, Type, Owner, NodeOptions]) of - ok -> - node_call(Type, create_node, [Host, Node, Owner]); - {error, ?ERR_CONFLICT} -> - case ets:lookup(gen_mod:get_module_proc(ServerHost, pubsub_state), nodetree) of - [{nodetree, nodetree_virtual}] -> node_call(Type, create_node, [Host, Node, Owner]); - _ -> {error, ?ERR_CONFLICT} - end; + case tree_call(Host, create_node, [Host, Node, Type, Owner, NodeOptions, Parents]) of + {ok, NodeId} -> + node_call(Type, create_node, [NodeId, Owner]); + {error, {virtual, NodeId}} -> + node_call(Type, create_node, [NodeId, Owner]); Error -> Error end; @@ -1209,15 +1727,9 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> end end, Reply = [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], - [{xmlelement, "create", [{"node", node_to_string(Node)}], + [{xmlelement, "create", nodeAttr(Node), []}]}], case transaction(CreateNode, transaction) of - {error, Error} -> - %% in case we change transaction to sync_dirty... - %%node_action: - %% node_call(Type, delete_node, [Host, Node]), - %% tree_call(Host, delete_node, [Host, Node]), - {error, Error}; {result, {Result, broadcast}} -> %%Lang = "en", %% TODO: fix %%OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), @@ -1225,7 +1737,6 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> %% [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}], %% [?XFIELD("hidden", "", "FORM_TYPE", ?NS_PUBSUB_NMI), %% ?XFIELD("jid-single", "Node Creator", "creator", jlib:jid_to_string(OwnerKey))]}]), - %% todo publish_item(Host, ServerHost, ["pubsub", "nodes"], node_to_string(Node)), case Result of default -> {result, Reply}; _ -> {result, Result} @@ -1233,7 +1744,12 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> {result, default} -> {result, Reply}; {result, Result} -> - {result, Result} + {result, Result}; + Error -> + %% in case we change transaction to sync_dirty... + %% node_call(Type, delete_node, [Host, Node]), + %% tree_call(Host, delete_node, [Host, Node]), + Error end; Error -> Error @@ -1256,53 +1772,56 @@ delete_node(_Host, [], _Owner) -> %% Node is the root {error, ?ERR_NOT_ALLOWED}; delete_node(Host, Node, Owner) -> - Action = fun(#pubsub_node{type = Type}) -> - case node_call(Type, get_affiliation, [Host, Node, Owner]) of - {result, owner} -> - Removed = tree_call(Host, delete_node, [Host, Node]), - node_call(Type, delete_node, [Host, Removed]); - _ -> - %% Entity is not an owner - {error, ?ERR_FORBIDDEN} - end + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + case node_call(Type, get_affiliation, [NodeId, Owner]) of + {result, owner} -> + ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]), + SubsByDepth = [{Depth, [{N, get_node_subs(N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree], + Removed = tree_call(Host, delete_node, [Host, Node]), + case node_call(Type, delete_node, [Removed]) of + {result, Res} -> {result, {SubsByDepth, Res}}; + Error -> Error + end; + _ -> + %% Entity is not an owner + {error, ?ERR_FORBIDDEN} + end end, Reply = [], case transaction(Host, Node, Action, transaction) of - {error, Error} -> - {error, Error}; - {result, {Result, broadcast, Removed}} -> - broadcast_removed_node(Host, Removed), - %%broadcast_retract_item(Host, ["pubsub", "nodes"], node_to_string(Node)), + {result, {_, {SubsByDepth, {Result, broadcast, Removed}}}} -> + lists:foreach(fun({RNode, _RSubscriptions}) -> + {RH, RN} = RNode#pubsub_node.nodeid, + NodeId = RNode#pubsub_node.id, + Type = RNode#pubsub_node.type, + Options = RNode#pubsub_node.options, + broadcast_removed_node(RH, RN, NodeId, Type, Options, SubsByDepth) + end, Removed), case Result of default -> {result, Reply}; _ -> {result, Result} end; - {result, {Result, Removed}} -> - broadcast_removed_node(Host, Removed), + {result, {_, {_, {Result, _Removed}}}} -> case Result of default -> {result, Reply}; _ -> {result, Result} end; - {result, default} -> + {result, {_, {_, default}}} -> {result, Reply}; - {result, Result} -> - {result, Result} + {result, {_, {_, Result}}} -> + {result, Result}; + Error -> + Error end. -delete_nodes(Host, Owner) -> - %% This removes only PEP nodes when user is removed - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - lists:foreach(fun(#pubsub_node{nodeid={NodeKey, NodeName}}) -> - delete_node(NodeKey, NodeName, Owner) - end, tree_action(Host, get_nodes, [OwnerKey])). -%% @spec (Host, Node, From, JID) -> +%% @spec (Host, Node, From, JID, Configuration) -> %% {error, Reason::stanzaError()} | %% {result, []} %% Host = host() %% Node = pubsubNode() %% From = jid() %% JID = jid() -%% @see node_default:subscribe_node/5 +%% @see node_hometree:subscribe_node/5 %% @doc <p>Accepts or rejects subcription requests on a PubSub node.</p> %%<p>There are several reasons why the subscription request might fail:</p> %%<ul> @@ -1317,75 +1836,98 @@ delete_nodes(Host, Owner) -> %%<li>The node does not support subscriptions.</li> %%<li>The node does not exist.</li> %%</ul> -subscribe_node(Host, Node, From, JID) -> +subscribe_node(Host, Node, From, JID, Configuration) -> + SubOpts = case pubsub_subscription:parse_options_xform(Configuration) of + {result, GoodSubOpts} -> GoodSubOpts; + _ -> invalid + end, Subscriber = case jlib:string_to_jid(JID) of error -> {"", "", ""}; J -> jlib:jid_tolower(J) end, - SubId = uniqid(), - Action = fun(#pubsub_node{options = Options, type = Type}) -> - Features = features(Type), - SubscribeFeature = lists:member("subscribe", Features), - SubscribeConfig = get_option(Options, subscribe), - AccessModel = get_option(Options, access_model), - SendLast = get_option(Options, send_last_published_item), - AllowedGroups = get_option(Options, roster_groups_allowed), - {PresenceSubscription, RosterGroup} = - case Host of - {OUser, OServer, _} -> - get_roster_info(OUser, OServer, - Subscriber, AllowedGroups); - _ -> - {true, true} - end, - if - not SubscribeFeature -> - %% Node does not support subscriptions - {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "subscribe")}; - not SubscribeConfig -> - %% Node does not support subscriptions - {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "subscribe")}; - true -> - node_call(Type, subscribe_node, - [Host, Node, From, Subscriber, + Action = fun(#pubsub_node{options = Options, owners = [Owner|_], type = Type, id = NodeId}) -> + Features = features(Type), + SubscribeFeature = lists:member("subscribe", Features), + OptionsFeature = lists:member("subscription-options", Features), + HasOptions = not (SubOpts == []), + SubscribeConfig = get_option(Options, subscribe), + AccessModel = get_option(Options, access_model), + SendLast = get_option(Options, send_last_published_item), + AllowedGroups = get_option(Options, roster_groups_allowed, []), + {PresenceSubscription, RosterGroup} = + case Host of + {OUser, OServer, _} -> + get_roster_info(OUser, OServer, + Subscriber, AllowedGroups); + _ -> + case Subscriber of + {"", "", ""} -> + {false, false}; + _ -> + {OU, OS, _} = Owner, + get_roster_info(OU, OS, + Subscriber, AllowedGroups) + end + end, + if + not SubscribeFeature -> + %% Node does not support subscriptions + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "subscribe")}; + not SubscribeConfig -> + %% Node does not support subscriptions + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "subscribe")}; + HasOptions andalso not OptionsFeature -> + %% Node does not support subscription options + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "subscription-options")}; + SubOpts == invalid -> + %% Passed invalit options submit form + {error, extended_error(?ERR_BAD_REQUEST, "invalid-options")}; + true -> + node_call(Type, subscribe_node, + [NodeId, From, Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup]) - end - end, + PresenceSubscription, RosterGroup, + SubOpts]) + end + end, Reply = fun(Subscription) -> %% TODO, this is subscription-notification, should depends on node features + SubAttrs = case Subscription of + {subscribed, SubId} -> + [{"subscription", subscription_to_string(subscribed)}, + {"subid", SubId}]; + Other -> + [{"subscription", subscription_to_string(Other)}] + end, Fields = - [{"node", node_to_string(Node)}, - {"jid", jlib:jid_to_string(Subscriber)}, - {"subscription", subscription_to_string(Subscription)}], + [{"jid", jlib:jid_to_string(Subscriber)} | SubAttrs], [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], - [{xmlelement, "subscription", - case Subscription of - subscribed -> [{"subid", SubId}|Fields]; - _ -> Fields - end, []}]}] + [{xmlelement, "subscription", Fields, []}]}] end, case transaction(Host, Node, Action, sync_dirty) of - {error, Error} -> - {error, Error}; - {result, {Result, subscribed, send_last}} -> - send_all_items(Host, Node, Subscriber), - case Result of - default -> {result, Reply(subscribed)}; - _ -> {result, Result} - end; - {result, {Result, Subscription}} -> - case Subscription of - pending -> send_authorization_request(Host, Node, Subscriber); - _ -> ok - end, + {result, {TNode, {Result, subscribed, SubId, send_last}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + send_items(Host, Node, NodeId, Type, Subscriber, last), case Result of - default -> {result, Reply(Subscription)}; + default -> {result, Reply({subscribed, SubId})}; _ -> {result, Result} end; - {result, Result} -> + {result, {_TNode, {default, subscribed, SubId}}} -> + {result, Reply({subscribed, SubId})}; + {result, {_TNode, {Result, subscribed, _SubId}}} -> + {result, Result}; + {result, {TNode, {default, pending, _SubId}}} -> + send_authorization_request(TNode, Subscriber), + {result, Reply(pending)}; + {result, {TNode, {Result, pending}}} -> + send_authorization_request(TNode, Subscriber), + {result, Result}; + {result, {_, Result}} -> %% this case should never occure anyway - {result, Result} + {result, Result}; + Error -> + Error end. %% @spec (Host, Noce, From, JID, SubId) -> {error, Reason} | {result, []} @@ -1404,19 +1946,23 @@ subscribe_node(Host, Node, From, JID) -> %%<li>The node does not exist.</li> %%<li>The request specifies a subscription ID that is not valid or current.</li> %%</ul> -unsubscribe_node(Host, Node, From, JID, SubId) -> +unsubscribe_node(Host, Node, From, JID, SubId) when is_list(JID) -> Subscriber = case jlib:string_to_jid(JID) of error -> {"", "", ""}; J -> jlib:jid_tolower(J) end, - case node_action(Host, Node, unsubscribe_node, - [Host, Node, From, Subscriber, SubId]) of - {error, Error} -> - {error, Error}; - {result, default} -> + unsubscribe_node(Host, Node, From, Subscriber, SubId); +unsubscribe_node(Host, Node, From, Subscriber, SubId) -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + node_call(Type, unsubscribe_node, [NodeId, From, Subscriber, SubId]) + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, default}} -> {result, []}; - {result, Result} -> - {result, Result} + {result, {_, Result}} -> + {result, Result}; + Error -> + Error end. %% @spec (Host::host(), ServerHost::host(), JID::jid(), Node::pubsubNode(), ItemId::string(), Payload::term()) -> @@ -1437,48 +1983,84 @@ publish_item(Host, ServerHost, Node, Publisher, "", Payload) -> %% if publisher does not specify an ItemId, the service MUST generate the ItemId publish_item(Host, ServerHost, Node, Publisher, uniqid(), Payload); publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) -> - Action = fun(#pubsub_node{options = Options, type = Type}) -> - Features = features(Type), - PublishFeature = lists:member("publish", Features), - Model = get_option(Options, publish_model), - MaxItems = max_items(Options), - PayloadSize = size(term_to_binary(Payload)), - PayloadMaxSize = get_option(Options, max_payload_size), - if - not PublishFeature -> - %% Node does not support item publication - {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "publish")}; - PayloadSize > PayloadMaxSize -> - %% Entity attempts to publish very large payload - {error, extended_error(?ERR_NOT_ACCEPTABLE, "payload-too-big")}; - %%?? -> iq_pubsub just does that matchs - %% % Entity attempts to publish item with multiple payload elements or namespace does not match - %% {error, extended_error(?ERR_BAD_REQUEST, "invalid-payload")}; - %% % Publisher attempts to publish to persistent node with no item - %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; - Payload == "" -> - %% Publisher attempts to publish to payload node with no payload - {error, extended_error(?ERR_BAD_REQUEST, "payload-required")}; - %%?? -> - %% % Publisher attempts to publish to transient notification node with item - %% {error, extended_error(?ERR_BAD_REQUEST, "item-forbidden")}; - true -> - node_call(Type, publish_item, [Host, Node, Publisher, Model, MaxItems, ItemId, Payload]) - end - end, - %%ejabberd_hooks:run(pubsub_publish_item, Host, [Host, Node, JID, service_jid(Host), ItemId, Payload]), - Reply = [], + Action = fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> + Features = features(Type), + PublishFeature = lists:member("publish", Features), + PublishModel = get_option(Options, publish_model), + MaxItems = max_items(Host, Options), + DeliverPayloads = get_option(Options, deliver_payloads), + PersistItems = get_option(Options, persist_items), + PayloadCount = payload_xmlelements(Payload), + PayloadSize = size(term_to_binary(Payload)), + PayloadMaxSize = get_option(Options, max_payload_size), + % pubsub#deliver_payloads true + % pubsub#persist_items true -> 1 item; false -> 0 item + if + not PublishFeature -> + %% Node does not support item publication + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "publish")}; + PayloadSize > PayloadMaxSize -> + %% Entity attempts to publish very large payload + {error, extended_error(?ERR_NOT_ACCEPTABLE, "payload-too-big")}; + (PayloadCount == 0) and (Payload == []) -> + %% Publisher attempts to publish to payload node with no payload + {error, extended_error(?ERR_BAD_REQUEST, "payload-required")}; + (PayloadCount > 1) or (PayloadCount == 0) -> + %% Entity attempts to publish item with multiple payload elements + {error, extended_error(?ERR_BAD_REQUEST, "invalid-payload")}; + (DeliverPayloads == 0) and (PersistItems == 0) and (PayloadSize > 0) -> + %% Publisher attempts to publish to transient notification node with item + {error, extended_error(?ERR_BAD_REQUEST, "item-forbidden")}; + ((DeliverPayloads == 1) or (PersistItems == 1)) and (PayloadSize == 0) -> + %% Publisher attempts to publish to persistent node with no item + {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; + true -> + node_call(Type, publish_item, [NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload]) + end + end, + ejabberd_hooks:run(pubsub_publish_item, ServerHost, [ServerHost, Node, Publisher, service_jid(Host), ItemId, Payload]), + Reply = [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "publish", nodeAttr(Node), + [{xmlelement, "item", itemAttr(ItemId), []}]}]}], case transaction(Host, Node, Action, sync_dirty) of + {result, {TNode, {Result, Broadcast, Removed}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + BroadcastPayload = case Broadcast of + default -> Payload; + broadcast -> Payload; + PluginPayload -> PluginPayload + end, + broadcast_publish_item(Host, Node, NodeId, Type, Options, Removed, ItemId, jlib:jid_tolower(Publisher), BroadcastPayload), + set_cached_item(Host, NodeId, ItemId, Payload), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {TNode, {default, Removed}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_retract_items(Host, Node, NodeId, Type, Options, Removed), + set_cached_item(Host, NodeId, ItemId, Payload), + {result, Reply}; + {result, {TNode, {Result, Removed}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_retract_items(Host, Node, NodeId, Type, Options, Removed), + set_cached_item(Host, NodeId, ItemId, Payload), + {result, Result}; + {result, {_, default}} -> + {result, Reply}; + {result, {_, Result}} -> + {result, Result}; {error, ?ERR_ITEM_NOT_FOUND} -> %% handles auto-create feature %% for automatic node creation. we'll take the default node type: %% first listed into the plugins configuration option, or pep - Type = case Host of - {_User, _Server, _Resource} -> - ?PEPNODE; - _ -> - hd(plugins(ServerHost)) - end, + Type = select_type(ServerHost, Host, Node), case lists:member("auto-create", features(Type)) of true -> case create_node(Host, ServerHost, Node, Publisher, Type) of @@ -1490,31 +2072,8 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) -> false -> {error, ?ERR_ITEM_NOT_FOUND} end; - {error, Reason} -> - {error, Reason}; - {result, {Result, broadcast, Removed}} -> - lists:foreach(fun(OldItem) -> - broadcast_retract_item(Host, Node, OldItem) - end, Removed), - broadcast_publish_item(Host, Node, ItemId, jlib:jid_tolower(Publisher), Payload), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, default, Removed} -> - lists:foreach(fun(OldItem) -> - broadcast_retract_item(Host, Node, OldItem) - end, Removed), - {result, Reply}; - {result, Result, Removed} -> - lists:foreach(fun(OldItem) -> - broadcast_retract_item(Host, Node, OldItem) - end, Removed), - {result, Result}; - {result, default} -> - {result, Reply}; - {result, Result} -> - {result, Result} + Error -> + Error end. %% @spec (Host::host(), JID::jid(), Node::pubsubNode(), ItemId::string()) -> @@ -1537,38 +2096,46 @@ delete_item(_, "", _, _, _) -> %% Request does not specify a node {error, extended_error(?ERR_BAD_REQUEST, "node-required")}; delete_item(Host, Node, Publisher, ItemId, ForceNotify) -> - Action = fun(#pubsub_node{type = Type}) -> - Features = features(Type), - PersistentFeature = lists:member("persistent-items", Features), - DeleteFeature = lists:member("delete-nodes", Features), - if - %%?? -> iq_pubsub just does that matchs - %% %% Request does not specify an item - %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; - not PersistentFeature -> - %% Node does not support persistent items - {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "persistent-items")}; - not DeleteFeature -> - %% Service does not support item deletion - {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "delete-nodes")}; - true -> - node_call(Type, delete_item, [Host, Node, Publisher, ItemId]) - end + Action = fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> + Features = features(Type), + PersistentFeature = lists:member("persistent-items", Features), + DeleteFeature = lists:member("delete-items", Features), + PublishModel = get_option(Options, publish_model), + if + %%-> iq_pubsub just does that matchs + %% %% Request does not specify an item + %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; + not PersistentFeature -> + %% Node does not support persistent items + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "persistent-items")}; + not DeleteFeature -> + %% Service does not support item deletion + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "delete-items")}; + true -> + node_call(Type, delete_item, [NodeId, Publisher, PublishModel, ItemId]) + end end, Reply = [], case transaction(Host, Node, Action, sync_dirty) of - {error, Reason} -> - {error, Reason}; - {result, {Result, broadcast}} -> - broadcast_retract_item(Host, Node, ItemId, ForceNotify), + {result, {TNode, {Result, broadcast}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_retract_items(Host, Node, NodeId, Type, Options, [ItemId], ForceNotify), + case get_cached_item(Host, NodeId) of + #pubsub_item{itemid = {ItemId, NodeId}, _ = '_'} -> unset_cached_item(Host, NodeId); + _ -> ok + end, case Result of default -> {result, Reply}; _ -> {result, Result} end; - {result, default} -> + {result, {_, default}} -> {result, Reply}; - {result, Result} -> - {result, Result} + {result, {_, Result}} -> + {result, Result}; + Error -> + Error end. %% @spec (Host, JID, Node) -> @@ -1586,7 +2153,7 @@ delete_item(Host, Node, Publisher, ItemId, ForceNotify) -> %%<li>The specified node does not exist.</li> %%</ul> purge_node(Host, Node, Owner) -> - Action = fun(#pubsub_node{type = Type, options = Options}) -> + Action = fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> Features = features(Type), PurgeFeature = lists:member("purge-nodes", Features), PersistentFeature = lists:member("persistent-items", Features), @@ -1602,23 +2169,27 @@ purge_node(Host, Node, Owner) -> %% Node is not configured for persistent items {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "persistent-items")}; true -> - node_call(Type, purge_node, [Host, Node, Owner]) + node_call(Type, purge_node, [NodeId, Owner]) end end, Reply = [], case transaction(Host, Node, Action, sync_dirty) of - {error, Reason} -> - {error, Reason}; - {result, {Result, broadcast}} -> - broadcast_purge_node(Host, Node), + {result, {TNode, {Result, broadcast}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_purge_node(Host, Node, NodeId, Type, Options), + unset_cached_item(Host, NodeId), case Result of default -> {result, Reply}; _ -> {result, Result} end; - {result, default} -> + {result, {_, default}} -> {result, Reply}; - {result, Result} -> - {result, Result} + {result, {_, Result}} -> + {result, Result}; + Error -> + Error end. %% @doc <p>Return the items of a given node.</p> @@ -1626,10 +2197,10 @@ purge_node(Host, Node, Owner) -> %% <p>The permission are not checked in this function.</p> %% @todo We probably need to check that the user doing the query has the right %% to read the items. -get_items(Host, Node, _JID, SMaxItems) -> +get_items(Host, Node, From, SubId, SMaxItems, ItemIDs) -> MaxItems = if - SMaxItems == "" -> ?MAXITEMS; + SMaxItems == "" -> get_max_items_node(Host); true -> case catch list_to_integer(SMaxItems) of {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; @@ -1640,65 +2211,113 @@ get_items(Host, Node, _JID, SMaxItems) -> {error, Error} -> {error, Error}; _ -> - case get_items(Host, Node) of - [] -> - {error, ?ERR_ITEM_NOT_FOUND}; - Items -> + Action = fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> + Features = features(Type), + RetreiveFeature = lists:member("retrieve-items", Features), + PersistentFeature = lists:member("persistent-items", Features), + AccessModel = get_option(Options, access_model), + AllowedGroups = get_option(Options, roster_groups_allowed, []), + {PresenceSubscription, RosterGroup} = + case Host of + {OUser, OServer, _} -> + get_roster_info(OUser, OServer, + jlib:jid_tolower(From), AllowedGroups); + _ -> + {true, true} + end, + if + not RetreiveFeature -> + %% Item Retrieval Not Supported + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "retrieve-items")}; + not PersistentFeature -> + %% Persistent Items Not Supported + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "persistent-items")}; + true -> + node_call(Type, get_items, + [NodeId, From, + AccessModel, PresenceSubscription, RosterGroup, + SubId]) + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Items}} -> + SendItems = case ItemIDs of + [] -> + Items; + _ -> + lists:filter(fun(#pubsub_item{itemid = {ItemId, _}}) -> + lists:member(ItemId, ItemIDs) + end, Items) + end, %% Generate the XML response (Item list), limiting the %% number of items sent to MaxItems: - ItemsEls = lists:map( - fun(#pubsub_item{itemid = {ItemId, _}, - payload = Payload}) -> - ItemAttrs = case ItemId of - "" -> []; - _ -> [{"id", ItemId}] - end, - {xmlelement, "item", ItemAttrs, Payload} - end, lists:sublist(Items, MaxItems)), {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], - [{xmlelement, "items", [{"node", node_to_string(Node)}], - ItemsEls}]}]} + [{xmlelement, "items", nodeAttr(Node), + itemsEls(lists:sublist(SendItems, MaxItems))}]}]}; + Error -> + Error end end. - get_items(Host, Node) -> - case node_action(Host, Node, get_items, [Host, Node]) of - {result, Items} -> Items; - _ -> [] + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + node_call(Type, get_items, [NodeId, service_jid(Host)]) + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Items}} -> Items; + Error -> Error + end. +get_item(Host, Node, ItemId) -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + node_call(Type, get_item, [NodeId, ItemId]) + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Items}} -> Items; + Error -> Error end. -%% @spec (Host, Node, LJID) -> any() -%% Host = host() +%% @spec (Host, Node, NodeId, Type, LJID, Number) -> any() +%% Host = pubsubHost() %% Node = pubsubNode() +%% NodeId = pubsubNodeId() +%% Type = pubsubNodeType() %% LJID = {U, S, []} +%% Number = last | integer() %% @doc <p>Resend the items of a node to the user.</p> -send_all_items(Host, Node, LJID) -> - send_items(Host, Node, LJID, all). - -send_last_item(Host, Node, LJID) -> - send_items(Host, Node, LJID, last). - -%% TODO use cache-last-item feature -send_items(Host, Node, LJID, Number) -> - Items = get_items(Host, Node), - ToSend = case Number of - last -> lists:sublist(Items, 1); - all -> Items; - N when N > 0 -> lists:sublist(Items, N); - _ -> Items - end, - ItemsEls = lists:map( - fun(#pubsub_item{itemid = {ItemId, _}, payload = Payload}) -> - ItemAttrs = case ItemId of - "" -> []; - _ -> [{"id", ItemId}] - end, - {xmlelement, "item", ItemAttrs, Payload} - end, ToSend), - Stanza = {xmlelement, "message", [], - [{xmlelement, "event", [{"xmlns", ?NS_PUBSUB_EVENT}], - [{xmlelement, "items", [{"node", node_to_string(Node)}], - ItemsEls}]}]}, +%% @todo use cache-last-item feature +send_items(Host, Node, NodeId, Type, LJID, last) -> + case get_cached_item(Host, NodeId) of + undefined -> + send_items(Host, Node, NodeId, Type, LJID, 1); + LastItem -> + {ModifNow, ModifLjid} = LastItem#pubsub_item.modification, + Stanza = event_stanza_with_delay( + [{xmlelement, "items", nodeAttr(Node), + itemsEls([LastItem])}], ModifNow, ModifLjid), + ejabberd_router ! {route, service_jid(Host), jlib:make_jid(LJID), Stanza} + end; +send_items(Host, Node, NodeId, Type, LJID, Number) -> + ToSend = case node_action(Host, Type, get_items, [NodeId, LJID]) of + {result, []} -> + []; + {result, Items} -> + case Number of + N when N > 0 -> lists:sublist(Items, N); + _ -> Items + end; + _ -> + [] + end, + Stanza = case ToSend of + [LastItem] -> + {ModifNow, ModifLjid} = LastItem#pubsub_item.modification, + event_stanza_with_delay( + [{xmlelement, "items", nodeAttr(Node), + itemsEls(ToSend)}], ModifNow, ModifLjid); + _ -> + event_stanza( + [{xmlelement, "items", nodeAttr(Node), + itemsEls(ToSend)}]) + end, ejabberd_router ! {route, service_jid(Host), jlib:make_jid(LJID), Stanza}. %% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response} @@ -1718,7 +2337,7 @@ get_affiliations(Host, JID, Plugins) when is_list(Plugins) -> %% Service does not support retreive affiliatons {{error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "retrieve-affiliations")}, Acc}; true -> - {result, Affiliations} = node_action(Type, get_entity_affiliations, [Host, JID]), + {result, Affiliations} = node_action(Host, Type, get_entity_affiliations, [Host, JID]), {Status, [Affiliations|Acc]} end end, {ok, []}, Plugins), @@ -1726,10 +2345,9 @@ get_affiliations(Host, JID, Plugins) when is_list(Plugins) -> {ok, Affiliations} -> Entities = lists:flatmap( fun({_, none}) -> []; - ({Node, Affiliation}) -> + ({#pubsub_node{nodeid = {_, Node}}, Affiliation}) -> [{xmlelement, "affiliation", - [{"node", node_to_string(Node)}, - {"affiliation", affiliation_to_string(Affiliation)}], + [{"affiliation", affiliation_to_string(Affiliation)}|nodeAttr(Node)], []}] end, lists:usort(lists:flatten(Affiliations))), {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], @@ -1739,27 +2357,25 @@ get_affiliations(Host, JID, Plugins) when is_list(Plugins) -> Error end; get_affiliations(Host, Node, JID) -> - Action = fun(#pubsub_node{type = Type}) -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> Features = features(Type), RetrieveFeature = lists:member("modify-affiliations", Features), - Affiliation = node_call(Type, get_affiliation, [Host, Node, JID]), + {result, Affiliation} = node_call(Type, get_affiliation, [NodeId, JID]), if not RetrieveFeature -> %% Service does not support modify affiliations {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "modify-affiliations")}; - Affiliation /= {result, owner} -> + Affiliation /= owner -> %% Entity is not an owner {error, ?ERR_FORBIDDEN}; true -> - node_call(Type, get_node_affiliations, [Host, Node]) + node_call(Type, get_node_affiliations, [NodeId]) end end, case transaction(Host, Node, Action, sync_dirty) of - {error, Reason} -> - {error, Reason}; - {result, []} -> + {result, {_, []}} -> {error, ?ERR_ITEM_NOT_FOUND}; - {result, Affiliations} -> + {result, {_, Affiliations}} -> Entities = lists:flatmap( fun({_, none}) -> []; ({AJID, Affiliation}) -> @@ -1769,8 +2385,10 @@ get_affiliations(Host, Node, JID) -> []}] end, Affiliations), {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_OWNER}], - [{xmlelement, "affiliations", [{"node", node_to_string(Node)}], - Entities}]}]} + [{xmlelement, "affiliations", nodeAttr(Node), + Entities}]}]}; + Error -> + Error end. set_affiliations(Host, Node, From, EntitiesEls) -> @@ -1802,32 +2420,156 @@ set_affiliations(Host, Node, From, EntitiesEls) -> error -> {error, ?ERR_BAD_REQUEST}; _ -> - Action = fun(#pubsub_node{type = Type, owners = Owners}) -> - case lists:member(Owner, Owners) of - true -> - lists:foreach( - fun({JID, Affiliation}) -> - node_call( - Type, set_affiliation, - [Host, Node, JID, Affiliation]) - end, Entities), - {result, []}; - _ -> - {error, ?ERR_NOT_ALLOWED} - end + Action = fun(#pubsub_node{owners = Owners, type = Type, id = NodeId}=N) -> + case lists:member(Owner, Owners) of + true -> + lists:foreach( + fun({JID, Affiliation}) -> + node_call(Type, set_affiliation, [NodeId, JID, Affiliation]), + case Affiliation of + owner -> + NewOwner = jlib:jid_tolower(jlib:jid_remove_resource(JID)), + NewOwners = [NewOwner|Owners], + tree_call(Host, set_node, [N#pubsub_node{owners = NewOwners}]); + none -> + OldOwner = jlib:jid_tolower(jlib:jid_remove_resource(JID)), + case lists:member(OldOwner, Owners) of + true -> + NewOwners = Owners--[OldOwner], + tree_call(Host, set_node, [N#pubsub_node{owners = NewOwners}]); + _ -> + ok + end; + _ -> + ok + end + end, Entities), + {result, []}; + _ -> + {error, ?ERR_FORBIDDEN} + end end, - transaction(Host, Node, Action, sync_dirty) + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end end. +get_options(Host, Node, JID, SubID, Lang) -> + Action = fun(#pubsub_node{type = Type, id = NodeID}) -> + case lists:member("subscription-options", features(Type)) of + true -> + get_options_helper(JID, Lang, Node, NodeID, SubID, Type); + false -> + {error, extended_error( + ?ERR_FEATURE_NOT_IMPLEMENTED, + unsupported, "subscription-options")} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_Node, XForm}} -> {result, [XForm]}; + Error -> Error + end. -%% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response} +get_options_helper(JID, Lang, Node, NodeID, SubID, Type) -> + Subscriber = case jlib:string_to_jid(JID) of + error -> {"", "", ""}; + J -> jlib:jid_tolower(J) + end, + {result, Subs} = node_call(Type, get_subscriptions, + [NodeID, Subscriber]), + SubIDs = lists:foldl(fun({subscribed, SID}, Acc) -> + [SID | Acc]; + (_, Acc) -> + Acc + end, [], Subs), + case {SubID, SubIDs} of + {_, []} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "not-subscribed")}; + {[], [SID]} -> + read_sub(Subscriber, Node, NodeID, SID, Lang); + {[], _} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "subid-required")}; + {_, _} -> + read_sub(Subscriber, Node, NodeID, SubID, Lang) + end. + +read_sub(Subscriber, Node, NodeID, SubID, Lang) -> + case pubsub_subscription:get_subscription(Subscriber, NodeID, SubID) of + {error, notfound} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + {result, #pubsub_subscription{options = Options}} -> + {result, XdataEl} = pubsub_subscription:get_options_xform(Lang, Options), + OptionsEl = {xmlelement, "options", [{"jid", jlib:jid_to_string(Subscriber)}, + {"subid", SubID}|nodeAttr(Node)], + [XdataEl]}, + PubsubEl = {xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], [OptionsEl]}, + {result, PubsubEl} + end. + +set_options(Host, Node, JID, SubID, Configuration) -> + Action = fun(#pubsub_node{type = Type, id = NodeID}) -> + case lists:member("subscription-options", features(Type)) of + true -> + set_options_helper(Configuration, JID, NodeID, + SubID, Type); + false -> + {error, extended_error( + ?ERR_FEATURE_NOT_IMPLEMENTED, + unsupported, "subscription-options")} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_Node, Result}} -> {result, Result}; + Error -> Error + end. + +set_options_helper(Configuration, JID, NodeID, SubID, Type) -> + SubOpts = case pubsub_subscription:parse_options_xform(Configuration) of + {result, GoodSubOpts} -> GoodSubOpts; + _ -> invalid + end, + Subscriber = case jlib:string_to_jid(JID) of + error -> {"", "", ""}; + J -> jlib:jid_tolower(J) + end, + {result, Subs} = node_call(Type, get_subscriptions, + [NodeID, Subscriber]), + SubIDs = lists:foldl(fun({subscribed, SID}, Acc) -> + [SID | Acc]; + (_, Acc) -> + Acc + end, [], Subs), + case {SubID, SubIDs} of + {_, []} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "not-subscribed")}; + {[], [SID]} -> + write_sub(Subscriber, NodeID, SID, SubOpts); + {[], _} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "subid-required")}; + {_, _} -> + write_sub(Subscriber, NodeID, SubID, SubOpts) + end. + +write_sub(_Subscriber, _NodeID, _SubID, invalid) -> + {error, extended_error(?ERR_BAD_REQUEST, "invalid-options")}; +write_sub(Subscriber, NodeID, SubID, Options) -> + case pubsub_subscription:set_subscription(Subscriber, NodeID, SubID, Options) of + {error, notfound} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + {result, _} -> + {result, []} + end. + +%% @spec (Host, Node, JID, Plugins) -> {error, Reason} | {result, Response} %% Host = host() +%% Node = pubsubNode() %% JID = jid() %% Plugins = [Plugin::string()] %% Reason = stanzaError() %% Response = [pubsubIQResponse()] %% @doc <p>Return the list of subscriptions as an XMPP response.</p> -get_subscriptions(Host, JID, Plugins) when is_list(Plugins) -> +get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> Result = lists:foldl( fun(Type, {Status, Acc}) -> Features = features(Type), @@ -1837,57 +2579,91 @@ get_subscriptions(Host, JID, Plugins) when is_list(Plugins) -> %% Service does not support retreive subscriptions {{error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "retrieve-subscriptions")}, Acc}; true -> - {result, Subscriptions} = node_action(Type, get_entity_subscriptions, [Host, JID]), + Subscriber = jlib:jid_remove_resource(JID), + {result, Subscriptions} = node_action(Host, Type, get_entity_subscriptions, [Host, Subscriber]), {Status, [Subscriptions|Acc]} end end, {ok, []}, Plugins), case Result of {ok, Subscriptions} -> Entities = lists:flatmap( - fun({_, none}) -> []; - ({Node, Subscription}) -> + fun({_, none}) -> + []; + ({#pubsub_node{nodeid = {_, SubsNode}}, Subscription}) -> + case Node of + <<>> -> + [{xmlelement, "subscription", + [{"subscription", subscription_to_string(Subscription)}|nodeAttr(SubsNode)], + []}]; + SubsNode -> + [{xmlelement, "subscription", + [{"subscription", subscription_to_string(Subscription)}], + []}]; + _ -> + [] + end; + ({_, none, _}) -> + []; + ({#pubsub_node{nodeid = {_, SubsNode}}, Subscription, SubID, SubJID}) -> + case Node of + <<>> -> + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(SubJID)}, + {"subid", SubID}, + {"subscription", subscription_to_string(Subscription)}|nodeAttr(SubsNode)], + []}]; + SubsNode -> [{xmlelement, "subscription", - [{"node", node_to_string(Node)}, + [{"jid", jlib:jid_to_string(SubJID)}, + {"subid", SubID}, {"subscription", subscription_to_string(Subscription)}], []}]; - ({_, none, _}) -> []; - ({Node, Subscription, SubJID}) -> + _ -> + [] + end; + ({#pubsub_node{nodeid = {_, SubsNode}}, Subscription, SubJID}) -> + case Node of + <<>> -> [{xmlelement, "subscription", - [{"node", node_to_string(Node)}, - {"jid", jlib:jid_to_string(SubJID)}, + [{"jid", jlib:jid_to_string(SubJID)}, + {"subscription", subscription_to_string(Subscription)}|nodeAttr(SubsNode)], + []}]; + SubsNode -> + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(SubJID)}, {"subscription", subscription_to_string(Subscription)}], - []}] + []}]; + _ -> + [] + end end, lists:usort(lists:flatten(Subscriptions))), {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], [{xmlelement, "subscriptions", [], Entities}]}]}; {Error, _} -> Error - end; + end. get_subscriptions(Host, Node, JID) -> - Action = fun(#pubsub_node{type = Type}) -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> Features = features(Type), RetrieveFeature = lists:member("manage-subscriptions", Features), - Affiliation = node_call(Type, get_affiliation, [Host, Node, JID]), + {result, Affiliation} = node_call(Type, get_affiliation, [NodeId, JID]), if not RetrieveFeature -> %% Service does not support manage subscriptions - {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "manage-affiliations")}; - Affiliation /= {result, owner} -> - % Entity is not an owner + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "manage-subscriptions")}; + Affiliation /= owner -> + %% Entity is not an owner {error, ?ERR_FORBIDDEN}; true -> - node_call(Type, get_node_subscriptions, [Host, Node]) + node_call(Type, get_node_subscriptions, [NodeId]) end end, case transaction(Host, Node, Action, sync_dirty) of - {error, Reason} -> - {error, Reason}; - {result, []} -> - {error, ?ERR_ITEM_NOT_FOUND}; - {result, Subscriptions} -> + {result, {_, Subscriptions}} -> Entities = lists:flatmap( fun({_, none}) -> []; + ({_, pending, _}) -> []; ({AJID, Subscription}) -> [{xmlelement, "subscription", [{"jid", jlib:jid_to_string(AJID)}, @@ -1901,8 +2677,10 @@ get_subscriptions(Host, Node, JID) -> []}] end, Subscriptions), {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_OWNER}], - [{xmlelement, "subscriptions", [{"node", node_to_string(Node)}], - Entities}]}]} + [{xmlelement, "subscriptions", nodeAttr(Node), + Entities}]}]}; + Error -> + Error end. set_subscriptions(Host, Node, From, EntitiesEls) -> @@ -1920,12 +2698,13 @@ set_subscriptions(Host, Node, From, EntitiesEls) -> xml:get_attr_s("jid", Attrs)), Subscription = string_to_subscription( xml:get_attr_s("subscription", Attrs)), + SubId = xml:get_attr_s("subid", Attrs), if (JID == error) or (Subscription == false) -> error; true -> - [{jlib:jid_tolower(JID), Subscription} | Acc] + [{jlib:jid_tolower(JID), Subscription, SubId} | Acc] end end end @@ -1934,20 +2713,38 @@ set_subscriptions(Host, Node, From, EntitiesEls) -> error -> {error, ?ERR_BAD_REQUEST}; _ -> - Action = fun(#pubsub_node{type = Type, owners = Owners}) -> - case lists:member(Owner, Owners) of - true -> - lists:foreach(fun({JID, Subscription}) -> - node_call(Type, set_subscription, [Host, Node, JID, Subscription]) - end, Entities), - {result, []}; - _ -> - {error, ?ERR_NOT_ALLOWED} - end - end, - transaction(Host, Node, Action, sync_dirty) - end. + Notify = fun(JID, Sub, _SubId) -> + Stanza = {xmlelement, "message", [], + [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(JID)}, + %{"subid", SubId}, + {"subscription", subscription_to_string(Sub)} | nodeAttr(Node)], []}]}]}, + ejabberd_router ! {route, service_jid(Host), jlib:make_jid(JID), Stanza} + end, + Action = fun(#pubsub_node{owners = Owners, type = Type, id = NodeId}) -> + case lists:member(Owner, Owners) of + true -> + Result = lists:foldl(fun({JID, Subscription, SubId}, Acc) -> + case node_call(Type, set_subscriptions, [NodeId, JID, Subscription, SubId]) of + {error, Err} -> [{error, Err} | Acc]; + _ -> Notify(JID, Subscription, SubId), Acc + end + end, [], Entities), + case Result of + [] -> {result, []}; + _ -> {error, ?ERR_NOT_ACCEPTABLE} + end; + _ -> + {error, ?ERR_FORBIDDEN} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end + end. %% @spec (OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, SubscriberResource}, AllowedGroups) %% -> {PresenceSubscription, RosterGroup} @@ -1957,7 +2754,8 @@ get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, A roster_get_jid_info, OwnerServer, {none, []}, [OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, ""}]), - PresenceSubscription = (Subscription == both) orelse (Subscription == from), + PresenceSubscription = (Subscription == both) orelse (Subscription == from) + orelse ({OwnerUser, OwnerServer} == {SubscriberUser, SubscriberServer}), RosterGroup = lists:any(fun(Group) -> lists:member(Group, AllowedGroups) end, Groups), @@ -1969,6 +2767,7 @@ get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, A %% @doc <p>Convert an affiliation type from string to atom.</p> string_to_affiliation("owner") -> owner; string_to_affiliation("publisher") -> publisher; +string_to_affiliation("member") -> member; string_to_affiliation("outcast") -> outcast; string_to_affiliation("none") -> none; string_to_affiliation(_) -> false. @@ -1989,6 +2788,7 @@ string_to_subscription(_) -> false. %% @doc <p>Convert an affiliation type from atom to string.</p> affiliation_to_string(owner) -> "owner"; affiliation_to_string(publisher) -> "publisher"; +affiliation_to_string(member) -> "member"; affiliation_to_string(outcast) -> "outcast"; affiliation_to_string(_) -> "none". @@ -2005,14 +2805,8 @@ subscription_to_string(_) -> "none". %% Node = pubsubNode() %% NodeStr = string() %% @doc <p>Convert a node type from pubsubNode to string.</p> -node_to_string([]) -> "/"; -node_to_string(Node) -> - case Node of - [[_ | _] | _] -> string:strip(lists:flatten(["/", lists:map(fun(S) -> [S, "/"] end, Node)]), right, $/); - [Head | _] when is_integer(Head) -> Node - end. -string_to_node(SNode) -> - string:tokens(SNode, "/"). +node_to_string(Node) -> binary_to_list(Node). +string_to_node(SNode) -> list_to_binary(SNode). %% @spec (Host) -> jid() %% Host = host() @@ -2023,303 +2817,347 @@ service_jid(Host) -> _ -> {jid, "", Host, "", "", Host, ""} end. -%% @spec (LJID, Subscription, PresenceDelivery) -> boolean() +%% @spec (LJID, NotifyType, Depth, NodeOptions, SubOptions) -> boolean() %% LJID = jid() -%% Subscription = atom() -%% PresenceDelivery = boolean() -%% @doc <p>Check if a notification must be delivered or not.</p> -is_to_delivered(_, none, _) -> false; -is_to_delivered(_, pending, _) -> false; -is_to_delivered(_, _, false) -> true; -is_to_delivered({User, Server, _}, _, true) -> +%% NotifyType = items | nodes +%% Depth = integer() +%% NodeOptions = [{atom(), term()}] +%% SubOptions = [{atom(), term()}] +%% @doc <p>Check if a notification must be delivered or not based on +%% node and subscription options.</p> +is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) -> + sub_to_deliver(LJID, NotifyType, Depth, SubOptions) + andalso node_to_deliver(LJID, NodeOptions). + +sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) -> + lists:all(fun (Option) -> + sub_option_can_deliver(NotifyType, Depth, Option) + end, SubOptions). + +sub_option_can_deliver(items, _, {subscription_type, nodes}) -> false; +sub_option_can_deliver(nodes, _, {subscription_type, items}) -> false; +sub_option_can_deliver(_, _, {subscription_depth, all}) -> true; +sub_option_can_deliver(_, Depth, {subscription_depth, D}) -> Depth =< D; +sub_option_can_deliver(_, _, {deliver, false}) -> false; +sub_option_can_deliver(_, _, {expire, When}) -> now() < When; +sub_option_can_deliver(_, _, _) -> true. + +node_to_deliver(LJID, NodeOptions) -> + PresenceDelivery = get_option(NodeOptions, presence_based_delivery), + presence_can_deliver(LJID, PresenceDelivery). + +presence_can_deliver(_, false) -> true; +presence_can_deliver({User, Server, _}, true) -> case mnesia:dirty_match_object({session, '_', '_', {User, Server}, '_', '_'}) of [] -> false; Ss -> lists:foldl(fun({session, _, _, _, undefined, _}, Acc) -> Acc; - ({session, _, _, _, _Priority, _}, _Acc) -> true + ({session, _, _, _, _Priority, _}, _Acc) -> true end, false, Ss) end. +%% @spec (Payload) -> int() +%% Payload = term() +%% @doc <p>Count occurence of XML elements in payload.</p> +payload_xmlelements(Payload) -> payload_xmlelements(Payload, 0). +payload_xmlelements([], Count) -> Count; +payload_xmlelements([{xmlelement, _, _, _}|Tail], Count) -> payload_xmlelements(Tail, Count+1); +payload_xmlelements([_|Tail], Count) -> payload_xmlelements(Tail, Count). + +%% @spec (Els) -> stanza() +%% Els = [xmlelement()] +%% @doc <p>Build pubsub event stanza</p> +event_stanza(Els) -> + event_stanza_withmoreels(Els, []). + +event_stanza_with_delay(Els, ModifNow, ModifLjid) -> + DateTime = calendar:now_to_datetime(ModifNow), + MoreEls = [jlib:timestamp_to_xml(DateTime, utc, ModifLjid, "")], + event_stanza_withmoreels(Els, MoreEls). + +event_stanza_withmoreels(Els, MoreEls) -> + {xmlelement, "message", [], + [{xmlelement, "event", [{"xmlns", ?NS_PUBSUB_EVENT}], Els} | MoreEls]}. + %%%%%% broadcast functions -broadcast_publish_item(Host, Node, ItemId, _From, Payload) -> - Action = - fun(#pubsub_node{options = Options, type = Type}) -> - case node_call(Type, get_states, [Host, Node]) of - {error, _} -> {result, false}; - {result, []} -> {result, false}; - {result, States} -> - PresenceDelivery = get_option(Options, presence_based_delivery), - BroadcastAll = get_option(Options, broadcast_all_resources), - Content = case get_option(Options, deliver_payloads) of - true -> Payload; - false -> [] - end, - ItemAttrs = case ItemId of - "" -> []; - _ -> [{"id", ItemId}] - end, - Stanza = make_stanza(Node, ItemAttrs, Content), - lists:foreach( - fun(#pubsub_state{stateid = {LJID, _}, - subscription = Subscription}) -> - case is_to_delivered(LJID, Subscription, PresenceDelivery) of - true -> - DestJIDs = case BroadcastAll of - true -> ejabberd_sm:get_user_resources(element(1, LJID), element(2, LJID)); - false -> [LJID] - end, - route_stanza(Host, DestJIDs, Stanza); - false -> - ok - end - end, States), - broadcast_by_caps(Host, Node, Type, Stanza), - {result, true} - end - end, - transaction(Host, Node, Action, sync_dirty). +broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, Removed, ItemId, _From, Payload) -> + %broadcast(Host, Node, NodeId, NodeOptions, none, true, "items", ItemEls) + case get_collection_subscriptions(Host, Node) of + SubsByDepth when is_list(SubsByDepth) -> + Content = case get_option(NodeOptions, deliver_payloads) of + true -> Payload; + false -> [] + end, + Stanza = event_stanza( + [{xmlelement, "items", nodeAttr(Node), + [{xmlelement, "item", itemAttr(ItemId), Content}]}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, items, Stanza), + case Removed of + [] -> + ok; + _ -> + case get_option(NodeOptions, notify_retract) of + true -> + RetractStanza = event_stanza( + [{xmlelement, "items", nodeAttr(Node), + [{xmlelement, "retract", itemAttr(RId), []} || RId <- Removed]}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, + items, RetractStanza); + _ -> + ok + end + end, + {result, true}; + _ -> + {result, false} + end. -%% ItemAttrs is a list of tuples: -%% For example: [{"id", ItemId}] -make_stanza(Node, ItemAttrs, Payload) -> - {xmlelement, "message", [], - [{xmlelement, "event", - [{"xmlns", ?NS_PUBSUB_EVENT}], - [{xmlelement, "items", [{"node", node_to_string(Node)}], - [{xmlelement, "item", ItemAttrs, Payload}]}]}]}. - -%% DestJIDs = [{LUser, LServer, LResource}] -route_stanza(Host, DestJIDs, Stanza) -> - lists:foreach( - fun(DestJID) -> - ejabberd_router ! {route, service_jid(Host), jlib:make_jid(DestJID), Stanza} - end, DestJIDs). - -broadcast_retract_item(Host, Node, ItemId) -> - broadcast_retract_item(Host, Node, ItemId, false). -broadcast_retract_item(Host, Node, ItemId, ForceNotify) -> - Action = - fun(#pubsub_node{options = Options, type = Type}) -> - case node_call(Type, get_states, [Host, Node]) of - {error, _} -> {result, false}; - {result, []} -> {result, false}; - {result, States} -> - Notify = case ForceNotify of - true -> true; - _ -> get_option(Options, notify_retract) - end, - ItemAttrs = case ItemId of - "" -> []; - _ -> [{"id", ItemId}] - end, - Stanza = {xmlelement, "message", [], - [{xmlelement, "event", - [{"xmlns", ?NS_PUBSUB_EVENT}], - [{xmlelement, "items", [{"node", node_to_string(Node)}], - [{xmlelement, "retract", ItemAttrs, []}]}]}]}, - case Notify of - true -> - lists:foreach( - fun(#pubsub_state{stateid = {JID, _}, - subscription = Subscription}) -> - if (Subscription /= none) and - (Subscription /= pending) -> - ejabberd_router ! {route, service_jid(Host), jlib:make_jid(JID), Stanza}; - true -> - ok - end - end, States), - broadcast_by_caps(Host, Node, Type, Stanza), - {result, true}; - false -> - {result, false} - end - end - end, - transaction(Host, Node, Action, sync_dirty). +broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds) -> + broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, false). +broadcast_retract_items(_Host, _Node, _NodeId, _Type, _NodeOptions, [], _ForceNotify) -> + {result, false}; +broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, ForceNotify) -> + %broadcast(Host, Node, NodeId, NodeOptions, notify_retract, ForceNotify, "retract", RetractEls) + case (get_option(NodeOptions, notify_retract) or ForceNotify) of + true -> + case get_collection_subscriptions(Host, Node) of + SubsByDepth when is_list(SubsByDepth) -> + Stanza = event_stanza( + [{xmlelement, "items", nodeAttr(Node), + [{xmlelement, "retract", itemAttr(ItemId), []} || ItemId <- ItemIds]}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, items, Stanza), + {result, true}; + _ -> + {result, false} + end; + _ -> + {result, false} + end. -broadcast_purge_node(Host, Node) -> - Action = - fun(#pubsub_node{options = Options, type = Type}) -> - case node_call(Type, get_states, [Host, Node]) of - {error, _} -> {result, false}; - {result, []} -> {result, false}; - {result, States} -> - Stanza = {xmlelement, "message", [], - [{xmlelement, "event", - [{"xmlns", ?NS_PUBSUB_EVENT}], - [{xmlelement, "purge", [{"node", node_to_string(Node)}], - []}]}]}, - case get_option(Options, notify_retract) of - true -> - lists:foreach( - fun(#pubsub_state{stateid = {JID,_}, - subscription = Subscription}) -> - if (Subscription /= none) and - (Subscription /= pending) -> - ejabberd_router ! {route, service_jid(Host), jlib:make_jid(JID), Stanza}; - true -> - ok - end - end, States), - broadcast_by_caps(Host, Node, Type, Stanza), - {result, true}; - false -> - {result, false} - end - end +broadcast_purge_node(Host, Node, NodeId, Type, NodeOptions) -> + %broadcast(Host, Node, NodeId, NodeOptions, notify_retract, false, "purge", []) + case get_option(NodeOptions, notify_retract) of + true -> + case get_collection_subscriptions(Host, Node) of + SubsByDepth when is_list(SubsByDepth) -> + Stanza = event_stanza( + [{xmlelement, "purge", nodeAttr(Node), + []}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, nodes, Stanza), + {result, true}; + _ -> + {result, false} + end; + _ -> + {result, false} + end. + +broadcast_removed_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth) -> + %broadcast(Host, Node, NodeId, NodeOptions, notify_delete, false, "delete", []) + case get_option(NodeOptions, notify_delete) of + true -> + case SubsByDepth of + [] -> + {result, false}; + _ -> + Stanza = event_stanza( + [{xmlelement, "delete", nodeAttr(Node), + []}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, nodes, Stanza), + {result, true} + end; + _ -> + {result, false} + end. + +broadcast_config_notification(Host, Node, NodeId, Type, NodeOptions, Lang) -> + %broadcast(Host, Node, NodeId, NodeOptions, notify_config, false, "items", ConfigEls) + case get_option(NodeOptions, notify_config) of + true -> + case get_collection_subscriptions(Host, Node) of + SubsByDepth when is_list(SubsByDepth) -> + Content = case get_option(NodeOptions, deliver_payloads) of + true -> + [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}], + get_configure_xfields(Type, NodeOptions, Lang, [])}]; + false -> + [] + end, + Stanza = event_stanza( + [{xmlelement, "configuration", nodeAttr(Node), Content}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, nodes, Stanza), + {result, true}; + _ -> + {result, false} + end; + _ -> + {result, false} + end. + +get_collection_subscriptions(Host, Node) -> + Action = fun() -> + {result, lists:map(fun({Depth, Nodes}) -> + {Depth, [{N, get_node_subs(N)} || N <- Nodes]} + end, tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]))} end, - transaction(Host, Node, Action, sync_dirty). - -broadcast_removed_node(Host, Removed) -> - lists:foreach( - fun(Node) -> - Action = - fun(#pubsub_node{options = Options, type = Type}) -> - Stanza = {xmlelement, "message", [], - [{xmlelement, "event", [{"xmlns", ?NS_PUBSUB_EVENT}], - [{xmlelement, "delete", [{"node", node_to_string(Node)}], - []}]}]}, - case get_option(Options, notify_delete) of - true -> - case node_call(Type, get_states, [Host, Node]) of - {result, States} -> - lists:foreach( - fun(#pubsub_state{stateid = {JID, _}, - subscription = Subscription}) -> - if (Subscription /= none) and - (Subscription /= pending) -> - ejabberd_router ! {route, service_jid(Host), jlib:make_jid(JID), Stanza}; - true -> - ok - end - end, States), - broadcast_by_caps(Host, Node, Type, Stanza), - {result, true}; - _ -> - {result, false} - end; - _ -> - {result, false} - end - end, - transaction(Host, Node, Action, sync_dirty) - end, Removed). + case transaction(Action, sync_dirty) of + {result, CollSubs} -> CollSubs; + _ -> [] + end. -broadcast_config_notification(Host, Node, Lang) -> - Action = - fun(#pubsub_node{options = Options, owners = Owners, type = Type}) -> - case node_call(Type, get_states, [Host, Node]) of - {error, _} -> {result, false}; - {result, []} -> {result, false}; - {result, States} -> - case get_option(Options, notify_config) of - true -> - PresenceDelivery = get_option(Options, presence_based_delivery), - Content = case get_option(Options, deliver_payloads) of - true -> - [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], - get_configure_xfields(Type, Options, Lang, Owners)}]; - false -> - [] - end, - Stanza = {xmlelement, "message", [], - [{xmlelement, "event", [{"xmlns", ?NS_PUBSUB_EVENT}], - [{xmlelement, "items", [{"node", node_to_string(Node)}], - [{xmlelement, "item", [{"id", "configuration"}], - Content}]}]}]}, - lists:foreach( - fun(#pubsub_state{stateid = {LJID, _}, - subscription = Subscription}) -> - case is_to_delivered(LJID, Subscription, PresenceDelivery) of - true -> - ejabberd_router ! {route, service_jid(Host), jlib:make_jid(LJID), Stanza}; - false -> - ok - end - end, States), - broadcast_by_caps(Host, Node, Type, Stanza), - {result, true}; - _ -> - {result, false} - end - end +get_node_subs(#pubsub_node{type = Type, + id = NodeID}) -> + case node_call(Type, get_node_subscriptions, [NodeID]) of + {result, Subs} -> get_options_for_subs(NodeID, Subs); + Other -> Other + end. + +get_options_for_subs(NodeID, Subs) -> + lists:foldl(fun({JID, subscribed, SubID}, Acc) -> + case pubsub_subscription:read_subscription(JID, NodeID, SubID) of + {error, notfound} -> [{JID, SubID, []} | Acc]; + #pubsub_subscription{options = Options} -> [{JID, SubID, Options} | Acc]; + _ -> Acc + end; + (_, Acc) -> + Acc + end, [], Subs). + +% TODO: merge broadcast code that way +%broadcast(Host, Node, NodeId, Type, NodeOptions, Feature, Force, ElName, SubEls) -> +% case (get_option(NodeOptions, Feature) or Force) of +% true -> +% case node_action(Host, Type, get_node_subscriptions, [NodeId]) of +% {result, []} -> +% {result, false}; +% {result, Subs} -> +% Stanza = event_stanza([{xmlelement, ElName, nodeAttr(Node), SubEls}]), +% broadcast_stanza(Host, Node, Type, NodeOptions, SubOpts, Stanza), +% {result, true}; +% _ -> +% {result, false} +% end; +% _ -> +% {result, false} +% end + +broadcast_stanza(Host, Node, _NodeId, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza) -> + NotificationType = get_option(NodeOptions, notification_type, headline), + BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but usefull + From = service_jid(Host), + Stanza = case NotificationType of + normal -> BaseStanza; + MsgType -> add_message_type(BaseStanza, atom_to_list(MsgType)) end, - transaction(Host, Node, Action, sync_dirty). - -%TODO: simplify broadcast_* using a generic function like that: -%broadcast(Host, Node, Fun) -> -% transaction(fun() -> -% case tree_call(Host, get_node, [Host, Node]) of -% #pubsub_node{options = Options, owners = Owners, type = Type} -> -% case node_call(Type, get_states, [Host, Node]) of -% {error, _} -> {result, false}; -% {result, []} -> {result, false}; -% {result, States} -> -% lists:foreach(fun(#pubsub_state{stateid = {JID,_}, subscription = Subscription}) -> -% Fun(Host, Node, Options, Owners, JID, Subscription) -% end, States), -% {result, true} -% end; -% Other -> -% Other -% end -% end, sync_dirty). - - -%% broadcast Stanza to all contacts of the user that are advertising -%% interest in this kind of Node. -broadcast_by_caps({LUser, LServer, LResource}, Node, _Type, Stanza) -> - ?DEBUG("looking for pid of ~p@~p/~p", [LUser, LServer, LResource]), - %% We need to know the resource, so we can ask for presence data. - SenderResource = case LResource of - "" -> - %% If we don't know the resource, just pick one. - case ejabberd_sm:get_user_resources(LUser, LServer) of - [R|_] -> - R; - [] -> - ?ERROR_MSG("~p@~p is offline; can't deliver ~p to contacts", [LUser, LServer, Stanza]), - "" - end; - _ -> - LResource - end, - case ejabberd_sm:get_session_pid(LUser, LServer, SenderResource) of - C2SPid when is_pid(C2SPid) -> - %% set the from address on the notification to the bare JID of the account owner - %% Also, add "replyto" if entity has presence subscription to the account owner - %% See XEP-0163 1.1 section 4.3.1 - Sender = jlib:make_jid(LUser, LServer, ""), - %%ReplyTo = jlib:make_jid(LUser, LServer, SenderResource), % This has to be used - case catch ejabberd_c2s:get_subscribed_and_online(C2SPid) of - ContactsWithCaps when is_list(ContactsWithCaps) -> - ?DEBUG("found contacts with caps: ~p", [ContactsWithCaps]), - lists:foreach( - fun({JID, Caps}) -> - case is_caps_notify(LServer, Node, Caps) of - true -> - To = jlib:make_jid(JID), - ejabberd_router ! {route, Sender, To, Stanza}; - false -> - ok - end - end, ContactsWithCaps); + %% Handles explicit subscriptions + NodesByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth), + lists:foreach(fun ({LJID, Nodes}) -> + LJIDs = case BroadcastAll of + true -> + {U, S, _} = LJID, + [{U, S, R} || R <- user_resources(U, S)]; + false -> + [LJID] + end, + SHIMStanza = add_headers(Stanza, collection_shim(Node, Nodes)), + lists:foreach(fun(To) -> + ejabberd_router ! {route, From, jlib:make_jid(To), SHIMStanza} + end, LJIDs) + end, NodesByJID), + %% Handles implicit presence subscriptions + case Host of + {LUser, LServer, LResource} -> + SenderResource = case LResource of + [] -> + case user_resources(LUser, LServer) of + [Resource|_] -> Resource; + _ -> "" + end; _ -> - ok + LResource end, - ok; + case ejabberd_sm:get_session_pid(LUser, LServer, SenderResource) of + C2SPid when is_pid(C2SPid) -> + %% set the from address on the notification to the bare JID of the account owner + %% Also, add "replyto" if entity has presence subscription to the account owner + %% See XEP-0163 1.1 section 4.3.1 + Sender = jlib:make_jid(LUser, LServer, ""), + %%ReplyTo = jlib:make_jid(LUser, LServer, SenderResource), % This has to be used + case catch ejabberd_c2s:get_subscribed(C2SPid) of + Contacts when is_list(Contacts) -> + lists:foreach(fun({U, S, _}) -> + spawn(fun() -> + LJIDs = lists:foldl(fun(R, Acc) -> + LJID = {U, S, R}, + case is_caps_notify(LServer, Node, LJID) of + true -> [LJID | Acc]; + false -> Acc + end + end, [], user_resources(U, S)), + lists:foreach(fun(To) -> + ejabberd_router ! {route, Sender, jlib:make_jid(To), Stanza} + end, LJIDs) + end) + end, Contacts); + _ -> + ok + end, + ok; + _ -> + ?DEBUG("~p@~p has no session; can't deliver ~p to contacts", [LUser, LServer, Stanza]), + ok + end; _ -> ok - end; -broadcast_by_caps(_, _, _, _) -> - ok. + end. -is_caps_notify(Host, Node, Caps) -> - case catch mod_caps:get_features(Host, Caps) of - Features when is_list(Features) -> lists:member(Node ++ "+notify", Features); - _ -> false +subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> + NodesToDeliver = fun(Depth, Node, Subs, Acc) -> + NodeId = case Node#pubsub_node.nodeid of + {_, N} -> N; + Other -> Other + end, + NodeOptions = Node#pubsub_node.options, + lists:foldl(fun({LJID, _SubID, SubOptions}, Acc2) -> + case is_to_deliver(LJID, NotifyType, Depth, + NodeOptions, SubOptions) of + true -> [{LJID, NodeId}|Acc2]; + false -> Acc2 + end + end, Acc, Subs) + end, + DepthsToDeliver = fun({Depth, SubsByNode}, Acc) -> + lists:foldl(fun({Node, Subs}, Acc2) -> + NodesToDeliver(Depth, Node, Subs, Acc2) + end, Acc, SubsByNode) + end, + JIDSubs = lists:foldl(DepthsToDeliver, [], SubsByDepth), + [{LJID, proplists:append_values(LJID, JIDSubs)} || LJID <- proplists:get_keys(JIDSubs)]. + +%% If we don't know the resource, just pick first if any +%% If no resource available, check if caps anyway (remote online) +user_resources(User, Server) -> + case ejabberd_sm:get_user_resources(User, Server) of + [] -> mod_caps:get_user_resources(User, Server); + Rs -> Rs + end. + +is_caps_notify(Host, Node, LJID) -> + case mod_caps:get_caps(LJID) of + nothing -> + false; + Caps -> + case catch mod_caps:get_features(Host, Caps) of + Features when is_list(Features) -> lists:member(node_to_string(Node) ++ "+notify", Features); + _ -> false + end end. %%%%%%% Configuration handling @@ -2329,50 +3167,47 @@ is_caps_notify(Host, Node, Caps) -> %%<li>The service does not support node configuration.</li> %%<li>The service does not support retrieval of default node configuration.</li> %%</ul> -get_configure(Host, Node, From, Lang) -> +get_configure(Host, ServerHost, Node, From, Lang) -> Action = - fun(#pubsub_node{options = Options, owners = Owners, type = Type}) -> - case node_call(Type, get_affiliation, [Host, Node, From]) of + fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> + case node_call(Type, get_affiliation, [NodeId, From]) of {result, owner} -> + Groups = ejabberd_hooks:run_fold(roster_groups, ServerHost, [], [ServerHost]), {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_OWNER}], - [{xmlelement, "configure", - [{"node", node_to_string(Node)}], + [{xmlelement, "configure", nodeAttr(Node), [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], - get_configure_xfields(Type, Options, Lang, Owners) + get_configure_xfields(Type, Options, Lang, Groups) }]}]}]}; _ -> {error, ?ERR_FORBIDDEN} end end, - transaction(Host, Node, Action, sync_dirty). + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end. -get_default(Host, Node, From, Lang) -> - Action = - fun(#pubsub_node{owners = Owners, type = Type}) -> - case node_call(Type, get_affiliation, [Host, Node, From]) of - {result, owner} -> - Options = node_options(Type), - {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_OWNER}], - [{xmlelement, "default", [], - [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], - get_configure_xfields(Type, Options, Lang, Owners) - }]}]}]}; - _ -> - {error, ?ERR_FORBIDDEN} - end - end, - transaction(Host, Node, Action, sync_dirty). +get_default(Host, Node, _From, Lang) -> + Type = select_type(Host, Host, Node), + Options = node_options(Type), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_OWNER}], + [{xmlelement, "default", [], + [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], + get_configure_xfields(Type, Options, Lang, []) + }]}]}]}. %% Get node option %% The result depend of the node type plugin system. get_option([], _) -> false; get_option(Options, Var) -> + get_option(Options, Var, false). +get_option(Options, Var, Def) -> case lists:keysearch(Var, 1, Options) of {value, {_Val, Ret}} -> Ret; - _ -> false + _ -> Def end. %% Get default options from the module plugin. @@ -2386,7 +3221,8 @@ node_options(Type) -> Result end. -%% @spec (Options) -> MaxItems +%% @spec (Host, Options) -> MaxItems +%% Host = host() %% Options = [Option] %% Option = {Key::atom(), Value::term()} %% MaxItems = integer() | unlimited @@ -2396,7 +3232,7 @@ node_options(Type) -> %% @todo In practice, the current data structure means that we cannot manage %% millions of items on a given node. This should be addressed in a new %% version. -max_items(Options) -> +max_items(Host, Options) -> case get_option(Options, persist_items) of true -> case get_option(Options, max_items) of @@ -2406,8 +3242,13 @@ max_items(Options) -> end; false -> case get_option(Options, send_last_published_item) of - never -> 0; - _ -> 1 + never -> + 0; + _ -> + case is_last_item_cache_enabled(Host) of + true -> 0; + false -> 1 + end end end. @@ -2417,7 +3258,7 @@ max_items(Options) -> -define(STRING_CONFIG_FIELD(Label, Var), ?STRINGXFIELD(Label, "pubsub#" ++ atom_to_list(Var), - get_option(Options, Var))). + get_option(Options, Var, ""))). -define(INTEGER_CONFIG_FIELD(Label, Var), ?STRINGXFIELD(Label, "pubsub#" ++ atom_to_list(Var), @@ -2433,7 +3274,15 @@ max_items(Options) -> atom_to_list(get_option(Options, Var)), [atom_to_list(O) || O <- Opts])). -get_configure_xfields(_Type, Options, _Owners, Lang) -> +-define(LISTM_CONFIG_FIELD(Label, Var, Opts), + ?LISTMXFIELD(Label, "pubsub#" ++ atom_to_list(Var), + get_option(Options, Var), Opts)). + +-define(NLIST_CONFIG_FIELD(Label, Var), + ?STRINGMXFIELD(Label, "pubsub#" ++ atom_to_list(Var), + [node_to_string(N) || N <- get_option(Options, Var, [])])). + +get_configure_xfields(_Type, Options, Lang, Groups) -> [?XFIELD("hidden", "", "FORM_TYPE", ?NS_PUBSUB_NODE_CONFIG), ?BOOL_CONFIG_FIELD("Deliver payloads with event notifications", deliver_payloads), ?BOOL_CONFIG_FIELD("Deliver event notifications", deliver_notifications), @@ -2441,22 +3290,22 @@ get_configure_xfields(_Type, Options, _Owners, Lang) -> ?BOOL_CONFIG_FIELD("Notify subscribers when the node is deleted", notify_delete), ?BOOL_CONFIG_FIELD("Notify subscribers when items are removed from the node", notify_retract), ?BOOL_CONFIG_FIELD("Persist items to storage", persist_items), + ?STRING_CONFIG_FIELD("A friendly name for the node", title), ?INTEGER_CONFIG_FIELD("Max # of items to persist", max_items), ?BOOL_CONFIG_FIELD("Whether to allow subscriptions", subscribe), ?ALIST_CONFIG_FIELD("Specify the access model", access_model, [open, authorize, presence, roster, whitelist]), %% XXX: change to list-multi, include current roster groups as options - {xmlelement, "field", [{"type", "text-multi"}, - {"label", translate:translate(Lang, "Roster groups allowed to subscribe")}, - {"var", "pubsub#roster_groups_allowed"}], - [{xmlelement, "value", [], [{xmlcdata, Value}]} || - Value <- get_option(Options, roster_groups_allowed)]}, + ?LISTM_CONFIG_FIELD("Roster groups allowed to subscribe", roster_groups_allowed, Groups), ?ALIST_CONFIG_FIELD("Specify the publisher model", publish_model, [publishers, subscribers, open]), + ?ALIST_CONFIG_FIELD("Specify the event message type", notification_type, + [headline, normal]), ?INTEGER_CONFIG_FIELD("Max payload size in bytes", max_payload_size), ?ALIST_CONFIG_FIELD("When to send the last published item", send_last_published_item, [never, on_sub, on_sub_and_presence]), - ?BOOL_CONFIG_FIELD("Only deliver notifications to available users", presence_based_delivery) + ?BOOL_CONFIG_FIELD("Only deliver notifications to available users", presence_based_delivery), + ?NLIST_CONFIG_FIELD("The collections with which a node is affiliated", collection) ]. %%<p>There are several reasons why the node configuration request might fail:</p> @@ -2475,9 +3324,8 @@ set_configure(Host, Node, From, Els, Lang) -> {result, []}; {?NS_XDATA, "submit"} -> Action = - fun(#pubsub_node{options = Options, type = Type}=N) -> - case node_call(Type, get_affiliation, - [Host, Node, From]) of + fun(#pubsub_node{options = Options, type = Type, id = NodeId} = N) -> + case node_call(Type, get_affiliation, [NodeId, From]) of {result, owner} -> case jlib:parse_xdata_submit(XEl) of invalid -> @@ -2487,11 +3335,12 @@ set_configure(Host, Node, From, Els, Lang) -> [] -> node_options(Type); _ -> Options end, - case set_xoption(XData, OldOpts) of + case set_xoption(Host, XData, OldOpts) of NewOpts when is_list(NewOpts) -> - tree_call(Host, set_node, - [N#pubsub_node{options = NewOpts}]), - {result, ok}; + case tree_call(Host, set_node, [N#pubsub_node{options = NewOpts}]) of + ok -> {result, ok}; + Err -> Err + end; Err -> Err end @@ -2501,8 +3350,11 @@ set_configure(Host, Node, From, Els, Lang) -> end end, case transaction(Host, Node, Action, transaction) of - {result, ok} -> - broadcast_config_notification(Host, Node, Lang), + {result, {TNode, ok}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_config_notification(Host, Node, NodeId, Type, Options, Lang), {result, []}; Other -> Other @@ -2528,110 +3380,185 @@ add_opt(Key, Value, Opts) -> end, case BoolVal of error -> {error, ?ERR_NOT_ACCEPTABLE}; - _ -> set_xoption(Opts, add_opt(Opt, BoolVal, NewOpts)) + _ -> set_xoption(Host, Opts, add_opt(Opt, BoolVal, NewOpts)) end). -define(SET_STRING_XOPT(Opt, Val), - set_xoption(Opts, add_opt(Opt, Val, NewOpts))). + set_xoption(Host, Opts, add_opt(Opt, Val, NewOpts))). -define(SET_INTEGER_XOPT(Opt, Val, Min, Max), case catch list_to_integer(Val) of IVal when is_integer(IVal), IVal >= Min, IVal =< Max -> - set_xoption(Opts, add_opt(Opt, IVal, NewOpts)); + set_xoption(Host, Opts, add_opt(Opt, IVal, NewOpts)); _ -> {error, ?ERR_NOT_ACCEPTABLE} end). -define(SET_ALIST_XOPT(Opt, Val, Vals), case lists:member(Val, [atom_to_list(V) || V <- Vals]) of - true -> set_xoption(Opts, add_opt(Opt, list_to_atom(Val), NewOpts)); + true -> set_xoption(Host, Opts, add_opt(Opt, list_to_atom(Val), NewOpts)); false -> {error, ?ERR_NOT_ACCEPTABLE} end). -define(SET_LIST_XOPT(Opt, Val), - set_xoption(Opts, add_opt(Opt, list_to_atom(Val), NewOpts))). + set_xoption(Host, Opts, add_opt(Opt, Val, NewOpts))). -set_xoption([], NewOpts) -> +set_xoption(_Host, [], NewOpts) -> NewOpts; -set_xoption([{"FORM_TYPE", _} | Opts], NewOpts) -> - set_xoption(Opts, NewOpts); -set_xoption([{"pubsub#roster_groups_allowed", Value} | Opts], NewOpts) -> +set_xoption(Host, [{"FORM_TYPE", _} | Opts], NewOpts) -> + set_xoption(Host, Opts, NewOpts); +set_xoption(Host, [{"pubsub#roster_groups_allowed", Value} | Opts], NewOpts) -> ?SET_LIST_XOPT(roster_groups_allowed, Value); -set_xoption([{"pubsub#deliver_payloads", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#deliver_payloads", [Val]} | Opts], NewOpts) -> ?SET_BOOL_XOPT(deliver_payloads, Val); -set_xoption([{"pubsub#deliver_notifications", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#deliver_notifications", [Val]} | Opts], NewOpts) -> ?SET_BOOL_XOPT(deliver_notifications, Val); -set_xoption([{"pubsub#notify_config", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#notify_config", [Val]} | Opts], NewOpts) -> ?SET_BOOL_XOPT(notify_config, Val); -set_xoption([{"pubsub#notify_delete", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#notify_delete", [Val]} | Opts], NewOpts) -> ?SET_BOOL_XOPT(notify_delete, Val); -set_xoption([{"pubsub#notify_retract", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#notify_retract", [Val]} | Opts], NewOpts) -> ?SET_BOOL_XOPT(notify_retract, Val); -set_xoption([{"pubsub#persist_items", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#persist_items", [Val]} | Opts], NewOpts) -> ?SET_BOOL_XOPT(persist_items, Val); -set_xoption([{"pubsub#max_items", [Val]} | Opts], NewOpts) -> - ?SET_INTEGER_XOPT(max_items, Val, 0, ?MAXITEMS); -set_xoption([{"pubsub#subscribe", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#max_items", [Val]} | Opts], NewOpts) -> + MaxItems = get_max_items_node(Host), + ?SET_INTEGER_XOPT(max_items, Val, 0, MaxItems); +set_xoption(Host, [{"pubsub#subscribe", [Val]} | Opts], NewOpts) -> ?SET_BOOL_XOPT(subscribe, Val); -set_xoption([{"pubsub#access_model", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#access_model", [Val]} | Opts], NewOpts) -> ?SET_ALIST_XOPT(access_model, Val, [open, authorize, presence, roster, whitelist]); -set_xoption([{"pubsub#publish_model", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#publish_model", [Val]} | Opts], NewOpts) -> ?SET_ALIST_XOPT(publish_model, Val, [publishers, subscribers, open]); -set_xoption([{"pubsub#node_type", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#notification_type", [Val]} | Opts], NewOpts) -> + ?SET_ALIST_XOPT(notification_type, Val, [headline, normal]); +set_xoption(Host, [{"pubsub#node_type", [Val]} | Opts], NewOpts) -> ?SET_ALIST_XOPT(node_type, Val, [leaf, collection]); -set_xoption([{"pubsub#max_payload_size", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#max_payload_size", [Val]} | Opts], NewOpts) -> ?SET_INTEGER_XOPT(max_payload_size, Val, 0, ?MAX_PAYLOAD_SIZE); -set_xoption([{"pubsub#send_last_published_item", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#send_last_published_item", [Val]} | Opts], NewOpts) -> ?SET_ALIST_XOPT(send_last_published_item, Val, [never, on_sub, on_sub_and_presence]); -set_xoption([{"pubsub#presence_based_delivery", [Val]} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#presence_based_delivery", [Val]} | Opts], NewOpts) -> ?SET_BOOL_XOPT(presence_based_delivery, Val); -set_xoption([{"pubsub#title", Value} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#title", Value} | Opts], NewOpts) -> ?SET_STRING_XOPT(title, Value); -set_xoption([{"pubsub#type", Value} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#type", Value} | Opts], NewOpts) -> ?SET_STRING_XOPT(type, Value); -set_xoption([{"pubsub#body_xslt", Value} | Opts], NewOpts) -> +set_xoption(Host, [{"pubsub#body_xslt", Value} | Opts], NewOpts) -> ?SET_STRING_XOPT(body_xslt, Value); -set_xoption([_ | _Opts], _NewOpts) -> - {error, ?ERR_NOT_ACCEPTABLE}. +set_xoption(Host, [{"pubsub#collection", Value} | Opts], NewOpts) -> + NewValue = [string_to_node(V) || V <- Value], + ?SET_LIST_XOPT(collection, NewValue); +set_xoption(Host, [{"pubsub#node", [Value]} | Opts], NewOpts) -> + NewValue = string_to_node(Value), + ?SET_LIST_XOPT(node, NewValue); +set_xoption(Host, [_ | Opts], NewOpts) -> + % skip unknown field + set_xoption(Host, Opts, NewOpts). + +get_max_items_node({_, ServerHost, _}) -> + get_max_items_node(ServerHost); +get_max_items_node(Host) -> + case catch ets:lookup(gen_mod:get_module_proc(Host, config), max_items_node) of + [{max_items_node, Integer}] -> Integer; + _ -> ?MAXITEMS + end. + +%%%% last item cache handling + +is_last_item_cache_enabled({_, ServerHost, _}) -> + is_last_item_cache_enabled(ServerHost); +is_last_item_cache_enabled(Host) -> + case catch ets:lookup(gen_mod:get_module_proc(Host, config), last_item_cache) of + [{last_item_cache, true}] -> true; + _ -> false + end. + +set_cached_item({_, ServerHost, _}, NodeId, ItemId, Payload) -> + set_cached_item(ServerHost, NodeId, ItemId, Payload); +set_cached_item(Host, NodeId, ItemId, Payload) -> + case is_last_item_cache_enabled(Host) of + true -> ets:insert(gen_mod:get_module_proc(Host, last_items), {NodeId, {ItemId, Payload}}); + _ -> ok + end. +unset_cached_item({_, ServerHost, _}, NodeId) -> + unset_cached_item(ServerHost, NodeId); +unset_cached_item(Host, NodeId) -> + case is_last_item_cache_enabled(Host) of + true -> ets:delete(gen_mod:get_module_proc(Host, last_items), NodeId); + _ -> ok + end. +get_cached_item({_, ServerHost, _}, NodeId) -> + get_cached_item(ServerHost, NodeId); +get_cached_item(Host, NodeId) -> + case is_last_item_cache_enabled(Host) of + true -> + case catch ets:lookup(gen_mod:get_module_proc(Host, last_items), NodeId) of + [{NodeId, {ItemId, Payload}}] -> + #pubsub_item{itemid = {ItemId, NodeId}, payload = Payload}; + _ -> + undefined + end; + _ -> + undefined + end. %%%% plugin handling plugins(Host) -> - case ets:lookup(gen_mod:get_module_proc(Host, pubsub_state), plugins) of + case catch ets:lookup(gen_mod:get_module_proc(Host, config), plugins) of + [{plugins, []}] -> [?STDNODE]; [{plugins, PL}] -> PL; _ -> [?STDNODE] end. +select_type(ServerHost, Host, Node, Type)-> + SelectedType = case Host of + {_User, _Server, _Resource} -> + case catch ets:lookup(gen_mod:get_module_proc(ServerHost, config), pep_mapping) of + [{pep_mapping, PM}] -> proplists:get_value(Node, PM, ?PEPNODE); + _ -> ?PEPNODE + end; + _ -> + Type + end, + ConfiguredTypes = plugins(ServerHost), + case lists:member(SelectedType, ConfiguredTypes) of + true -> SelectedType; + false -> hd(ConfiguredTypes) + end. +select_type(ServerHost, Host, Node) -> + select_type(ServerHost, Host, Node, hd(plugins(ServerHost))). features() -> [ - %"access-authorize", % OPTIONAL - "access-open", % OPTIONAL this relates to access_model option in node_default - %"access-presence", % OPTIONAL - %"access-roster", % OPTIONAL - %"access-whitelist", % OPTIONAL + % see plugin "access-authorize", % OPTIONAL + "access-open", % OPTIONAL this relates to access_model option in node_hometree + "access-presence", % OPTIONAL this relates to access_model option in node_pep + %TODO "access-roster", % OPTIONAL + "access-whitelist", % OPTIONAL % see plugin "auto-create", % OPTIONAL % see plugin "auto-subscribe", % RECOMMENDED "collections", % RECOMMENDED "config-node", % RECOMMENDED "create-and-configure", % RECOMMENDED % see plugin "create-nodes", % RECOMMENDED - %TODO "delete-any", % OPTIONAL + % see plugin "delete-items", % RECOMMENDED % see plugin "delete-nodes", % RECOMMENDED % see plugin "filtered-notifications", % RECOMMENDED - %TODO "get-pending", % OPTIONAL + % see plugin "get-pending", % OPTIONAL % see plugin "instant-nodes", % RECOMMENDED - %TODO "item-ids", % RECOMMENDED + "item-ids", % RECOMMENDED "last-published", % RECOMMENDED %TODO "cache-last-item", %TODO "leased-subscription", % OPTIONAL % see plugin "manage-subscriptions", % OPTIONAL - %TODO "member-affiliation", % RECOMMENDED + "member-affiliation", % RECOMMENDED %TODO "meta-data", % RECOMMENDED % see plugin "modify-affiliations", % OPTIONAL - %TODO "multi-collection", % OPTIONAL - %TODO "multi-subscribe", % OPTIONAL + % see plugin "multi-collection", % OPTIONAL + % see plugin "multi-subscribe", % OPTIONAL % see plugin "outcast-affiliation", % RECOMMENDED % see plugin "persistent-items", % RECOMMENDED "presence-notifications", % OPTIONAL @@ -2645,8 +3572,9 @@ features() -> "retrieve-default" % RECOMMENDED % see plugin "retrieve-items", % RECOMMENDED % see plugin "retrieve-subscriptions", % RECOMMENDED + %TODO "shim", % OPTIONAL % see plugin "subscribe", % REQUIRED - %TODO "subscription-options", % OPTIONAL + % see plugin "subscription-options", % OPTIONAL % see plugin "subscription-notifications" % OPTIONAL ]. features(Type) -> @@ -2660,24 +3588,30 @@ features(Host, []) -> Acc ++ features(Plugin) end, [], plugins(Host))); features(Host, Node) -> - {result, Features} = node_action(Host, Node, features, []), - lists:usort(features() ++ Features). + Action = fun(#pubsub_node{type = Type}) -> {result, features(Type)} end, + case transaction(Host, Node, Action, sync_dirty) of + {result, Features} -> lists:usort(features() ++ Features); + _ -> features() + end. %% @doc <p>node tree plugin call.</p> tree_call({_User, Server, _Resource}, Function, Args) -> tree_call(Server, Function, Args); tree_call(Host, Function, Args) -> - Module = case ets:lookup(gen_mod:get_module_proc(Host, pubsub_state), nodetree) of + ?DEBUG("tree_call ~p ~p ~p",[Host, Function, Args]), + Module = case catch ets:lookup(gen_mod:get_module_proc(Host, config), nodetree) of [{nodetree, N}] -> N; - _ -> list_to_atom(?TREE_PREFIX ++ ?STDNODE) + _ -> list_to_atom(?TREE_PREFIX ++ ?STDTREE) end, catch apply(Module, Function, Args). tree_action(Host, Function, Args) -> + ?DEBUG("tree_action ~p ~p ~p",[Host,Function,Args]), Fun = fun() -> tree_call(Host, Function, Args) end, catch mnesia:sync_dirty(Fun). %% @doc <p>node plugin call.</p> node_call(Type, Function, Args) -> + ?DEBUG("node_call ~p ~p ~p",[Type, Function, Args]), Module = list_to_atom(?PLUGIN_PREFIX++Type), case catch apply(Module, Function, Args) of {result, Result} -> {result, Result}; @@ -2691,24 +3625,24 @@ node_call(Type, Function, Args) -> Result -> {result, Result} %% any other return value is forced as result end. -node_action(Type, Function, Args) -> +node_action(Host, Type, Function, Args) -> + ?DEBUG("node_action ~p ~p ~p ~p",[Host,Type,Function,Args]), transaction(fun() -> node_call(Type, Function, Args) end, sync_dirty). -node_action(Host, Node, Function, Args) -> - transaction(fun() -> - case tree_call(Host, get_node, [Host, Node]) of - #pubsub_node{type=Type} -> node_call(Type, Function, Args); - Other -> Other - end - end, sync_dirty). %% @doc <p>plugin transaction handling.</p> transaction(Host, Node, Action, Trans) -> transaction(fun() -> case tree_call(Host, get_node, [Host, Node]) of - Record when is_record(Record, pubsub_node) -> Action(Record); - Other -> Other + N when is_record(N, pubsub_node) -> + case Action(N) of + {result, Result} -> {result, {N, Result}}; + {atomic, {result, Result}} -> {result, {N, Result}}; + Other -> Other + end; + Error -> + Error end end, Trans). @@ -2733,7 +3667,8 @@ transaction(Fun, Trans) -> %% Add pubsub-specific error element extended_error(Error, Ext) -> - extended_error(Error, Ext, [{"xmlns", ?NS_PUBSUB_ERRORS}]). + extended_error(Error, Ext, + [{"xmlns", ?NS_PUBSUB_ERRORS}]). extended_error(Error, unsupported, Feature) -> extended_error(Error, "unsupported", [{"xmlns", ?NS_PUBSUB_ERRORS}, @@ -2747,7 +3682,31 @@ uniqid() -> {T1, T2, T3} = now(), lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). -%% @doc Return the name of a given node if available. -get_item_name(Host, Node, Id) -> - {result, Name} = node_action(Host, Node, get_item_name, [Host, Node, Id]), - Name. +% node attributes +nodeAttr(Node) when is_list(Node) -> + [{"node", Node}]; +nodeAttr(Node) -> + [{"node", node_to_string(Node)}]. + +% item attributes +itemAttr([]) -> []; +itemAttr(ItemId) -> [{"id", ItemId}]. + +% build item elements from item list +itemsEls(Items) -> + lists:map(fun(#pubsub_item{itemid = {ItemId, _}, payload = Payload}) -> + {xmlelement, "item", itemAttr(ItemId), Payload} + end, Items). + +add_message_type({xmlelement, "message", Attrs, Els}, Type) -> + {xmlelement, "message", [{"type", Type}|Attrs], Els}; +add_message_type(XmlEl, _Type) -> + XmlEl. + +add_headers({xmlelement, Name, Attrs, Els}, HeaderEls) -> + HeaderEl = {xmlelement, "headers", [{"xmlns", ?NS_SHIM}], HeaderEls}, + {xmlelement, Name, Attrs, [HeaderEl | Els]}. + +collection_shim(Node, Nodes) -> + [{xmlelement, "header", [{"name", "Collection"}], + [{xmlcdata, node_to_string(N)}]} || N <- Nodes -- [Node]]. diff --git a/src/mod_pubsub/mod_pubsub_odbc.erl b/src/mod_pubsub/mod_pubsub_odbc.erl new file mode 100644 index 000000000..8b3d38f4a --- /dev/null +++ b/src/mod_pubsub/mod_pubsub_odbc.erl @@ -0,0 +1,3577 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% @copyright 2006-2009 ProcessOne +%%% @author Christophe Romain <christophe.romain@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + + +%%% @doc The module <strong>{@module}</strong> is the core of the PubSub +%%% extension. It relies on PubSub plugins for a large part of its functions. +%%% +%%% @headerfile "pubsub.hrl" +%%% +%%% @reference See <a href="http://www.xmpp.org/extensions/xep-0060.html">XEP-0060: Pubsub</a> for +%%% the latest version of the PubSub specification. +%%% This module uses version 1.12 of the specification as a base. +%%% Most of the specification is implemented. +%%% Functions concerning configuration should be rewritten. +%%% +%%% Support for subscription-options and multi-subscribe features was +%%% added by Brian Cully (bjc AT kublai.com). Subscriptions and options are +%%% stored in the pubsub_subscription table, with a link to them provided +%%% by the subscriptions field of pubsub_state. For information on +%%% subscription-options and mulit-subscribe see XEP-0060 sections 6.1.6, +%%% 6.2.3.1, 6.2.3.5, and 6.3. For information on subscription leases see +%%% XEP-0060 section 12.18. + +-module(mod_pubsub_odbc). +-author('christophe.romain@process-one.net'). +-version('1.13-0'). + +-behaviour(gen_server). +-behaviour(gen_mod). + +-include("ejabberd.hrl"). +-include("adhoc.hrl"). +-include("jlib.hrl"). +-include("pubsub.hrl"). + +-define(STDTREE, "tree_odbc"). +-define(STDNODE, "flat_odbc"). +-define(PEPNODE, "pep_odbc"). + +%% exports for hooks +-export([presence_probe/3, + in_subscription/6, + out_subscription/4, + remove_user/2, + disco_local_identity/5, + disco_local_features/5, + disco_local_items/5, + disco_sm_identity/5, + disco_sm_features/5, + disco_sm_items/5 + ]). +%% exported iq handlers +-export([iq_local/3, + iq_sm/3 + ]). + +%% exports for console debug manual use +-export([create_node/5, + delete_node/3, + subscribe_node/5, + unsubscribe_node/5, + publish_item/6, + delete_item/4, + send_items/6, + get_items/2, + get_item/3, + get_cached_item/2, + broadcast_stanza/8, + get_configure/5, + set_configure/5, + tree_action/3, + node_action/4 + ]). + +%% general helpers for plugins +-export([node_to_string/1, + string_to_node/1, + subscription_to_string/1, + affiliation_to_string/1, + string_to_subscription/1, + string_to_affiliation/1, + extended_error/2, + extended_error/3, + escape/1 + ]). + +%% API and gen_server callbacks +-export([start_link/2, + start/2, + stop/1, + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3 + ]). + +%% calls for parallel sending of last items +-export([send_loop/1 + ]). + +-define(PROCNAME, ejabberd_mod_pubsub_odbc). +-define(PLUGIN_PREFIX, "node_"). +-define(TREE_PREFIX, "nodetree_"). + +-record(state, {server_host, + host, + access, + pep_mapping = [], + ignore_pep_from_offline = true, + last_item_cache = false, + max_items_node = ?MAXITEMS, + nodetree = ?STDTREE, + plugins = [?STDNODE], + send_loop}). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). + +start(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = {Proc, + {?MODULE, start_link, [Host, Opts]}, + transient, 1000, worker, [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:delete_child(ejabberd_sup, Proc). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([ServerHost, Opts]) -> + ?DEBUG("pubsub init ~p ~p",[ServerHost,Opts]), + Host = gen_mod:get_opt_host(ServerHost, Opts, "pubsub.@HOST@"), + Access = gen_mod:get_opt(access_createnode, Opts, all), + PepOffline = gen_mod:get_opt(ignore_pep_from_offline, Opts, true), + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), + LastItemCache = gen_mod:get_opt(last_item_cache, Opts, false), + MaxItemsNode = gen_mod:get_opt(max_items_node, Opts, ?MAXITEMS), + pubsub_index:init(Host, ServerHost, Opts), + ets:new(gen_mod:get_module_proc(Host, config), [set, named_table]), + ets:new(gen_mod:get_module_proc(ServerHost, config), [set, named_table]), + ets:new(gen_mod:get_module_proc(Host, last_items), [set, named_table]), + ets:new(gen_mod:get_module_proc(ServerHost, last_items), [set, named_table]), + {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), + mod_disco:register_feature(ServerHost, ?NS_PUBSUB), + ets:insert(gen_mod:get_module_proc(Host, config), {nodetree, NodeTree}), + ets:insert(gen_mod:get_module_proc(Host, config), {plugins, Plugins}), + ets:insert(gen_mod:get_module_proc(Host, config), {last_item_cache, LastItemCache}), + ets:insert(gen_mod:get_module_proc(Host, config), {max_items_node, MaxItemsNode}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {nodetree, NodeTree}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {plugins, Plugins}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {last_item_cache, Plugins}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {max_items_node, LastItemCache}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {pep_mapping, PepMapping}), + ejabberd_hooks:add(disco_sm_identity, ServerHost, ?MODULE, disco_sm_identity, 75), + ejabberd_hooks:add(disco_sm_features, ServerHost, ?MODULE, disco_sm_features, 75), + ejabberd_hooks:add(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75), + ejabberd_hooks:add(presence_probe_hook, ServerHost, ?MODULE, presence_probe, 80), + ejabberd_hooks:add(roster_in_subscription, ServerHost, ?MODULE, in_subscription, 50), + ejabberd_hooks:add(roster_out_subscription, ServerHost, ?MODULE, out_subscription, 50), + ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, remove_user, 50), + ejabberd_hooks:add(anonymous_purge_hook, ServerHost, ?MODULE, remove_user, 50), + gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB, ?MODULE, iq_sm, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB_OWNER, ?MODULE, iq_sm, IQDisc), + case lists:member(?PEPNODE, Plugins) of + true -> + ejabberd_hooks:add(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 75), + ejabberd_hooks:add(disco_local_features, ServerHost, ?MODULE, disco_local_features, 75), + ejabberd_hooks:add(disco_local_items, ServerHost, ?MODULE, disco_local_items, 75), + gen_iq_handler:add_iq_handler(ejabberd_local, ServerHost, ?NS_PUBSUB, ?MODULE, iq_local, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, ServerHost, ?NS_PUBSUB_OWNER, ?MODULE, iq_local, IQDisc); + false -> + ok + end, + ejabberd_router:register_route(Host), + init_nodes(Host, ServerHost, NodeTree, Plugins), + State = #state{host = Host, + server_host = ServerHost, + access = Access, + pep_mapping = PepMapping, + ignore_pep_from_offline = PepOffline, + last_item_cache = LastItemCache, + max_items_node = MaxItemsNode, + nodetree = NodeTree, + plugins = Plugins}, + SendLoop = spawn(?MODULE, send_loop, [State]), + {ok, State#state{send_loop = SendLoop}}. + +%% @spec (Host, ServerHost, Opts) -> Plugins +%% Host = mod_pubsub:host() Opts = [{Key,Value}] +%% ServerHost = host() +%% Key = atom() +%% Value = term() +%% Plugins = [Plugin::string()] +%% @doc Call the init/1 function for each plugin declared in the config file. +%% The default plugin module is implicit. +%% <p>The Erlang code for the plugin is located in a module called +%% <em>node_plugin</em>. The 'node_' prefix is mandatory.</p> +%% <p>The modules are initialized in alphetical order and the list is checked +%% and sorted to ensure that each module is initialized only once.</p> +%% <p>See {@link node_hometree:init/1} for an example implementation.</p> +init_plugins(Host, ServerHost, Opts) -> + TreePlugin = list_to_atom(?TREE_PREFIX ++ + gen_mod:get_opt(nodetree, Opts, ?STDTREE)), + ?DEBUG("** tree plugin is ~p",[TreePlugin]), + TreePlugin:init(Host, ServerHost, Opts), + Plugins = gen_mod:get_opt(plugins, Opts, [?STDNODE]), + PepMapping = gen_mod:get_opt(pep_mapping, Opts, []), + ?DEBUG("** PEP Mapping : ~p~n",[PepMapping]), + lists:foreach(fun(Name) -> + ?DEBUG("** init ~s plugin",[Name]), + Plugin = list_to_atom(?PLUGIN_PREFIX ++ Name), + Plugin:init(Host, ServerHost, Opts) + end, Plugins), + {Plugins, TreePlugin, PepMapping}. + +terminate_plugins(Host, ServerHost, Plugins, TreePlugin) -> + lists:foreach(fun(Name) -> + ?DEBUG("** terminate ~s plugin",[Name]), + Plugin = list_to_atom(?PLUGIN_PREFIX++Name), + Plugin:terminate(Host, ServerHost) + end, Plugins), + TreePlugin:terminate(Host, ServerHost), + ok. + +init_nodes(Host, ServerHost, _NodeTree, Plugins) -> + %% TODO, this call should be done plugin side + case lists:member("hometree_odbc", Plugins) of + true -> + create_node(Host, ServerHost, string_to_node("/home"), service_jid(Host), "hometree_odbc"), + create_node(Host, ServerHost, string_to_node("/home/"++ServerHost), service_jid(Host), "hometree_odbc"); + false -> + ok + end. + +send_queue(State, Msg) -> + Pid = State#state.send_loop, + case is_process_alive(Pid) of + true -> + Pid ! Msg, + State; + false -> + SendLoop = spawn(?MODULE, send_loop, [State]), + SendLoop ! Msg, + State#state{send_loop = SendLoop} + end. + +send_loop(State) -> + receive + {presence, JID, Pid} -> + Host = State#state.host, + ServerHost = State#state.server_host, + LJID = jlib:jid_tolower(JID), + BJID = jlib:jid_remove_resource(LJID), + %% for each node From is subscribed to + %% and if the node is so configured, send the last published item to From + lists:foreach(fun(PType) -> + Subscriptions = case catch node_action(Host, PType, get_entity_subscriptions_for_send_last, [Host, JID]) of + {result, S} -> S; + _ -> [] + end, + lists:foreach( + fun({Node, subscribed, _, SubJID}) -> + if (SubJID == LJID) or (SubJID == BJID) -> + #pubsub_node{nodeid = {H, N}, type = Type, id = NodeId} = Node, + send_items(H, N, NodeId, Type, LJID, last); + true -> + % resource not concerned about that subscription + ok + end; + (_) -> + ok + end, Subscriptions) + end, State#state.plugins), + %% and force send the last PEP events published by its offline and local contacts + %% only if pubsub is explicitely configured for that. + %% this is a hack in a sense that PEP should only be based on presence + %% and is not able to "store" events of remote users (via s2s) + %% this makes that hack only work for local domain by now + if not State#state.ignore_pep_from_offline -> + {User, Server, Resource} = jlib:jid_tolower(JID), + case mod_caps:get_caps({User, Server, Resource}) of + nothing -> + %% we don't have caps, no need to handle PEP items + ok; + _ -> + case catch ejabberd_c2s:get_subscribed(Pid) of + Contacts when is_list(Contacts) -> + lists:foreach( + fun({U, S, R}) -> + case S of + ServerHost -> %% local contacts + case ejabberd_sm:get_user_resources(U, S) of + [] -> %% offline + PeerJID = jlib:make_jid(U, S, R), + self() ! {presence, User, Server, [Resource], PeerJID}; + _ -> %% online + % this is already handled by presence probe + ok + end; + _ -> %% remote contacts + % we can not do anything in any cases + ok + end + end, Contacts); + _ -> + ok + end + end; + true -> + ok + end, + send_loop(State); + {presence, User, Server, Resources, JID} -> + %% get resources caps and check if processing is needed + spawn(fun() -> + {HasCaps, ResourcesCaps} = lists:foldl(fun(Resource, {R, L}) -> + case mod_caps:get_caps({User, Server, Resource}) of + nothing -> {R, L}; + Caps -> {true, [{Resource, Caps} | L]} + end + end, {false, []}, Resources), + case HasCaps of + true -> + Host = State#state.host, + ServerHost = State#state.server_host, + Owner = jlib:jid_remove_resource(jlib:jid_tolower(JID)), + lists:foreach(fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = NodeId, options = Options}) -> + case get_option(Options, send_last_published_item) of + on_sub_and_presence -> + lists:foreach(fun({Resource, Caps}) -> + CapsNotify = case catch mod_caps:get_features(ServerHost, Caps) of + Features when is_list(Features) -> lists:member(node_to_string(Node) ++ "+notify", Features); + _ -> false + end, + case CapsNotify of + true -> + LJID = {User, Server, Resource}, + Subscribed = case get_option(Options, access_model) of + open -> true; + presence -> true; + whitelist -> false; % subscribers are added manually + authorize -> false; % likewise + roster -> + Grps = get_option(Options, roster_groups_allowed, []), + {OU, OS, _} = Owner, + element(2, get_roster_info(OU, OS, LJID, Grps)) + end, + if Subscribed -> + send_items(Owner, Node, NodeId, Type, LJID, last); + true -> + ok + end; + false -> + ok + end + end, ResourcesCaps); + _ -> + ok + end + end, tree_action(Host, get_nodes, [Owner, JID])); + false -> + ok + end + end), + send_loop(State); + stop -> + ok + end. + +%% ------- +%% disco hooks handling functions +%% + +identity(Host) -> + Identity = case lists:member(?PEPNODE, plugins(Host)) of + true -> [{"category", "pubsub"}, {"type", "pep"}]; + false -> [{"category", "pubsub"}, {"type", "service"}] + end, + {xmlelement, "identity", Identity, []}. + +disco_local_identity(Acc, _From, To, [], _Lang) -> + Acc ++ [identity(To#jid.lserver)]; +disco_local_identity(Acc, _From, _To, _Node, _Lang) -> + Acc. + +disco_local_features(Acc, _From, To, [], _Lang) -> + Host = To#jid.lserver, + Feats = case Acc of + {result, I} -> I; + _ -> [] + end, + {result, Feats ++ lists:map(fun(Feature) -> + ?NS_PUBSUB++"#"++Feature + end, features(Host, []))}; +disco_local_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +disco_local_items(Acc, _From, _To, [], _Lang) -> + Acc; +disco_local_items(Acc, _From, _To, _Node, _Lang) -> + Acc. + +disco_sm_identity(Acc, _From, To, [], _Lang) -> + Acc ++ [identity(To#jid.lserver)]; +disco_sm_identity(Acc, From, To, Node, _Lang) -> + LOwner = jlib:jid_tolower(jlib:jid_remove_resource(To)), + Acc ++ case node_disco_identity(LOwner, From, Node) of + {result, I} -> I; + _ -> [] + end. + +disco_sm_features(Acc, _From, _To, [], _Lang) -> + Acc; +disco_sm_features(Acc, From, To, Node, _Lang) -> + LOwner = jlib:jid_tolower(jlib:jid_remove_resource(To)), + Features = node_disco_features(LOwner, From, Node), + case {Acc, Features} of + {{result, AccFeatures}, {result, AddFeatures}} -> + {result, AccFeatures++AddFeatures}; + {_, {result, AddFeatures}} -> + {result, AddFeatures}; + {_, _} -> + Acc + end. + +disco_sm_items(Acc, From, To, [], _Lang) -> + Host = To#jid.lserver, + case tree_action(Host, get_subnodes, [Host, <<>>, From]) of + [] -> + Acc; + Nodes -> + SBJID = jlib:jid_to_string(jlib:jid_remove_resource(To)), + Items = case Acc of + {result, I} -> I; + _ -> [] + end, + NodeItems = lists:map( + fun(#pubsub_node{nodeid = {_, Node}}) -> + {xmlelement, "item", + [{"jid", SBJID}|nodeAttr(Node)], + []} + end, Nodes), + {result, NodeItems ++ Items} + end; + +disco_sm_items(Acc, From, To, SNode, _Lang) -> + Host = To#jid.lserver, + Node = string_to_node(SNode), + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + % TODO call get_items/6 instead for access control (EJAB-1033) + case node_call(Type, get_items, [NodeId, From]) of + {result, []} -> + none; + {result, AllItems} -> + SBJID = jlib:jid_to_string(jlib:jid_remove_resource(To)), + Items = case Acc of + {result, I} -> I; + _ -> [] + end, + NodeItems = lists:map( + fun(#pubsub_item{itemid = {Id, _}}) -> + {result, Name} = node_call(Type, get_item_name, [Host, Node, Id]), + {xmlelement, "item", [{"jid", SBJID}, {"name", Name}], []} + end, AllItems), + {result, NodeItems ++ Items}; + _ -> + none + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Items}} -> {result, Items}; + _ -> Acc + end. + +%% ------- +%% presence hooks handling functions +%% + +presence_probe(#jid{luser = User, lserver = Server, lresource = Resource} = JID, JID, Pid) -> + Proc = gen_mod:get_module_proc(Server, ?PROCNAME), + gen_server:cast(Proc, {presence, JID, Pid}), + gen_server:cast(Proc, {presence, User, Server, [Resource], JID}); +presence_probe(#jid{luser = User, lserver = Server, lresource = Resource}, #jid{lserver = Host} = JID, _Pid) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:cast(Proc, {presence, User, Server, [Resource], JID}). + +%% ------- +%% subscription hooks handling functions +%% + +out_subscription(User, Server, JID, subscribed) -> + Owner = jlib:make_jid(User, Server, ""), + {PUser, PServer, PResource} = jlib:jid_tolower(JID), + PResources = case PResource of + [] -> user_resources(PUser, PServer); + _ -> [PResource] + end, + Proc = gen_mod:get_module_proc(Server, ?PROCNAME), + gen_server:cast(Proc, {presence, PUser, PServer, PResources, Owner}); +out_subscription(_,_,_,_) -> + ok. +in_subscription(_, User, Server, Owner, unsubscribed, _) -> + Subscriber = jlib:make_jid(User, Server, ""), + Proc = gen_mod:get_module_proc(Server, ?PROCNAME), + gen_server:cast(Proc, {unsubscribe, Subscriber, Owner}); +in_subscription(_, _, _, _, _, _) -> + ok. + +%% ------- +%% user remove hook handling function +%% + +remove_user(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + Proc = gen_mod:get_module_proc(Server, ?PROCNAME), + gen_server:cast(Proc, {remove_user, LUser, LServer}). + +%%-------------------------------------------------------------------- +%% Function: +%% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +%% @private +handle_call(server_host, _From, State) -> + {reply, State#state.server_host, State}; +handle_call(plugins, _From, State) -> + {reply, State#state.plugins, State}; +handle_call(pep_mapping, _From, State) -> + {reply, State#state.pep_mapping, State}; +handle_call(nodetree, _From, State) -> + {reply, State#state.nodetree, State}; +handle_call(stop, _From, State) -> + {stop, normal, ok, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +%% @private +handle_cast({presence, JID, Pid}, State) -> + %% A new resource is available. send last published items + {noreply, send_queue(State, {presence, JID, Pid})}; + +handle_cast({presence, User, Server, Resources, JID}, State) -> + %% A new resource is available. send last published PEP items + {noreply, send_queue(State, {presence, User, Server, Resources, JID})}; + +handle_cast({remove_user, LUser, LServer}, State) -> + spawn(fun() -> + Host = State#state.host, + Owner = jlib:make_jid(LUser, LServer, ""), + %% remove user's subscriptions + lists:foreach(fun(PType) -> + {result, Subscriptions} = node_action(Host, PType, get_entity_subscriptions, [Host, Owner]), + lists:foreach(fun + ({#pubsub_node{nodeid = {H, N}}, subscribed, _, JID}) -> + unsubscribe_node(H, N, Owner, JID, all); + (_) -> + ok + end, Subscriptions), + {result, Affiliations} = node_action(Host, PType, get_entity_affiliations, [Host, Owner]), + lists:foreach(fun + ({#pubsub_node{nodeid = {H, N}}, owner}) -> + delete_node(H, N, Owner); + (_) -> + ok + end, Affiliations) + end, State#state.plugins) + end), + {noreply, State}; + +handle_cast({unsubscribe, Subscriber, Owner}, State) -> + spawn(fun() -> + Host = State#state.host, + BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + lists:foreach(fun(PType) -> + {result, Subscriptions} = node_action(Host, PType, get_entity_subscriptions, [Host, Subscriber]), + lists:foreach(fun + ({Node, subscribed, _, JID}) -> + #pubsub_node{options = Options, type = Type, id = NodeId} = Node, + case get_option(Options, access_model) of + presence -> + case lists:member(BJID, node_owners(Host, Type, NodeId)) of + true -> + node_action(Host, Type, unsubscribe_node, [NodeId, Subscriber, JID, all]); + false -> + {result, ok} + end; + _ -> + {result, ok} + end; + (_) -> + ok + end, Subscriptions) + end, State#state.plugins) + end), + {noreply, State}; + +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +%% @private +handle_info({route, From, To, Packet}, + #state{server_host = ServerHost, + access = Access, + plugins = Plugins} = State) -> + case catch do_route(ServerHost, Access, Plugins, To#jid.lserver, From, To, Packet) of + {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); + _ -> ok + end, + {noreply, State}; +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +%% @private +terminate(_Reason, #state{host = Host, + server_host = ServerHost, + nodetree = TreePlugin, + plugins = Plugins, + send_loop = SendLoop}) -> + ejabberd_router:unregister_route(Host), + case lists:member(?PEPNODE, Plugins) of + true -> + ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 75), + ejabberd_hooks:delete(disco_local_features, ServerHost, ?MODULE, disco_local_features, 75), + ejabberd_hooks:delete(disco_local_items, ServerHost, ?MODULE, disco_local_items, 75), + gen_iq_handler:remove_iq_handler(ejabberd_local, ServerHost, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_local, ServerHost, ?NS_PUBSUB_OWNER); + false -> + ok + end, + ejabberd_hooks:delete(disco_sm_identity, ServerHost, ?MODULE, disco_sm_identity, 75), + ejabberd_hooks:delete(disco_sm_features, ServerHost, ?MODULE, disco_sm_features, 75), + ejabberd_hooks:delete(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75), + ejabberd_hooks:delete(presence_probe_hook, ServerHost, ?MODULE, presence_probe, 80), + ejabberd_hooks:delete(roster_in_subscription, ServerHost, ?MODULE, in_subscription, 50), + ejabberd_hooks:delete(roster_out_subscription, ServerHost, ?MODULE, out_subscription, 50), + ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, remove_user, 50), + ejabberd_hooks:delete(anonymous_purge_hook, ServerHost, ?MODULE, remove_user, 50), + gen_iq_handler:remove_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB_OWNER), + mod_disco:unregister_feature(ServerHost, ?NS_PUBSUB), + SendLoop ! stop, + terminate_plugins(Host, ServerHost, Plugins, TreePlugin). + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +%% @private +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> + {xmlelement, Name, Attrs, _Els} = Packet, + case To of + #jid{luser = "", lresource = ""} -> + case Name of + "iq" -> + case jlib:iq_query_info(Packet) of + #iq{type = get, xmlns = ?NS_DISCO_INFO, + sub_el = SubEl, lang = Lang} = IQ -> + {xmlelement, _, QAttrs, _} = SubEl, + Node = xml:get_attr_s("node", QAttrs), + Info = ejabberd_hooks:run_fold( + disco_info, ServerHost, [], + [ServerHost, ?MODULE, "", ""]), + Res = case iq_disco_info(Host, Node, From, Lang) of + {result, IQRes} -> + jlib:iq_to_xml( + IQ#iq{type = result, + sub_el = [{xmlelement, "query", + QAttrs, IQRes++Info}]}); + {error, Error} -> + jlib:make_error_reply(Packet, Error) + end, + ejabberd_router:route(To, From, Res); + #iq{type = get, xmlns = ?NS_DISCO_ITEMS, + sub_el = SubEl} = IQ -> + {xmlelement, _, QAttrs, _} = SubEl, + Node = xml:get_attr_s("node", QAttrs), + Rsm = jlib:rsm_decode(IQ), + Res = case iq_disco_items(Host, Node, From, Rsm) of + {result, IQRes} -> + jlib:iq_to_xml( + IQ#iq{type = result, + sub_el = [{xmlelement, "query", + QAttrs, IQRes}]}); + {error, Error} -> + jlib:make_error_reply(Packet, Error) + end, + ejabberd_router:route(To, From, Res); + #iq{type = IQType, xmlns = ?NS_PUBSUB, + lang = Lang, sub_el = SubEl} = IQ -> + Res = + case iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) of + {result, IQRes} -> + jlib:iq_to_xml( + IQ#iq{type = result, + sub_el = IQRes}); + {error, Error} -> + jlib:make_error_reply(Packet, Error) + end, + ejabberd_router:route(To, From, Res); + #iq{type = IQType, xmlns = ?NS_PUBSUB_OWNER, + lang = Lang, sub_el = SubEl} = IQ -> + Res = + case iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) of + {result, IQRes} -> + jlib:iq_to_xml( + IQ#iq{type = result, + sub_el = IQRes}); + {error, Error} -> + jlib:make_error_reply(Packet, Error) + end, + ejabberd_router:route(To, From, Res); + #iq{type = get, xmlns = ?NS_VCARD = XMLNS, + lang = Lang, sub_el = _SubEl} = IQ -> + Res = IQ#iq{type = result, + sub_el = [{xmlelement, "vCard", [{"xmlns", XMLNS}], + iq_get_vcard(Lang)}]}, + ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); + #iq{type = set, xmlns = ?NS_COMMANDS} = IQ -> + Res = case iq_command(Host, ServerHost, From, IQ, Access, Plugins) of + {error, Error} -> + jlib:make_error_reply(Packet, Error); + {result, IQRes} -> + jlib:iq_to_xml(IQ#iq{type = result, + sub_el = IQRes}) + end, + ejabberd_router:route(To, From, Res); + #iq{} -> + Err = jlib:make_error_reply( + Packet, + ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(To, From, Err); + _ -> + ok + end; + "message" -> + case xml:get_attr_s("type", Attrs) of + "error" -> + ok; + _ -> + case find_authorization_response(Packet) of + none -> + ok; + invalid -> + ejabberd_router:route(To, From, + jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST)); + XFields -> + handle_authorization_response(Host, From, To, Packet, XFields) + end + end; + _ -> + ok + end; + _ -> + case xml:get_attr_s("type", Attrs) of + "error" -> + ok; + "result" -> + ok; + _ -> + Err = jlib:make_error_reply(Packet, ?ERR_ITEM_NOT_FOUND), + ejabberd_router:route(To, From, Err) + end + end. + +node_disco_info(Host, Node, From) -> + node_disco_info(Host, Node, From, true, true). +node_disco_identity(Host, Node, From) -> + node_disco_info(Host, Node, From, true, false). +node_disco_features(Host, Node, From) -> + node_disco_info(Host, Node, From, false, true). +node_disco_info(Host, Node, From, Identity, Features) -> + Action = + fun(#pubsub_node{type = Type, id = NodeId}) -> + I = case Identity of + false -> + []; + true -> + Types = + case tree_call(Host, get_subnodes, [Host, Node, From]) of + [] -> + ["leaf"]; %% No sub-nodes: it's a leaf node + _ -> + case node_call(Type, get_items, [NodeId, From, none]) of + {result, []} -> ["collection"]; + {result, _} -> ["leaf", "collection"]; + _ -> [] + end + end, + lists:map(fun(T) -> + {xmlelement, "identity", [{"category", "pubsub"}, + {"type", T}], []} + end, Types) + end, + F = case Features of + false -> + []; + true -> + [{xmlelement, "feature", [{"var", ?NS_PUBSUB}], []} | + lists:map(fun + ("rsm")-> {xmlelement, "feature", [{"var", ?NS_RSM}], []}; + (T) -> {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++T}], []} + end, features(Type))] + end, + %% TODO: add meta-data info (spec section 5.4) + {result, I ++ F} + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end. + +iq_disco_info(Host, SNode, From, Lang) -> + [RealSNode|_] = case SNode of + [] -> [[]]; + _ -> string:tokens(SNode, "!") + end, + Node = string_to_node(RealSNode), + case Node of + <<>> -> + {result, + [{xmlelement, "identity", + [{"category", "pubsub"}, + {"type", "service"}, + {"name", translate:translate(Lang, "Publish-Subscribe")}], []}, + {xmlelement, "feature", [{"var", ?NS_DISCO_INFO}], []}, + {xmlelement, "feature", [{"var", ?NS_DISCO_ITEMS}], []}, + {xmlelement, "feature", [{"var", ?NS_PUBSUB}], []}, + {xmlelement, "feature", [{"var", ?NS_VCARD}], []}] ++ + lists:map(fun + ("rsm")-> {xmlelement, "feature", [{"var", ?NS_RSM}], []}; + (T) -> {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++T}], []} + end, features(Host, Node))}; + _ -> + node_disco_info(Host, Node, From) + end. + +iq_disco_items(Host, [], From, _RSM) -> + {result, lists:map( + fun(#pubsub_node{nodeid = {_, SubNode}, type = Type}) -> + {result, Path} = node_call(Type, node_to_path, [SubNode]), + [Name|_] = lists:reverse(Path), + {xmlelement, "item", [{"jid", Host}, {"name", Name}|nodeAttr(SubNode)], []} + end, tree_action(Host, get_subnodes, [Host, <<>>, From]))}; +iq_disco_items(Host, Item, From, RSM) -> + case string:tokens(Item, "!") of + [_SNode, _ItemID] -> + {result, []}; + [SNode] -> + Node = string_to_node(SNode), + Action = + fun(#pubsub_node{type = Type, id = NodeId}) -> + %% TODO call get_items/6 instead for access control (EJAB-1033) + {NodeItems, RsmOut} = case node_call(Type, get_items, [NodeId, From, RSM]) of + {result, I} -> I; + _ -> {[], none} + end, + Nodes = lists:map( + fun(#pubsub_node{nodeid = {_, SubNode}}) -> + {xmlelement, "item", [{"jid", Host}|nodeAttr(SubNode)], []} + end, tree_call(Host, get_subnodes, [Host, Node, From])), + Items = lists:map( + fun(#pubsub_item{itemid = {RN, _}}) -> + {result, Name} = node_call(Type, get_item_name, [Host, Node, RN]), + {xmlelement, "item", [{"jid", Host}, {"name", Name}], []} + end, NodeItems), + {result, Nodes ++ Items ++ jlib:rsm_encode(RsmOut)} + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end + end. + +iq_local(From, To, #iq{type = Type, sub_el = SubEl, xmlns = XMLNS, lang = Lang} = IQ) -> + ServerHost = To#jid.lserver, + %% Accept IQs to server only from our own users. + if + From#jid.lserver /= ServerHost -> + IQ#iq{type = error, sub_el = [?ERR_FORBIDDEN, SubEl]}; + true -> + LOwner = jlib:jid_tolower(jlib:jid_remove_resource(From)), + Res = case XMLNS of + ?NS_PUBSUB -> iq_pubsub(LOwner, ServerHost, From, Type, SubEl, Lang); + ?NS_PUBSUB_OWNER -> iq_pubsub_owner(LOwner, ServerHost, From, Type, SubEl, Lang) + end, + case Res of + {result, IQRes} -> IQ#iq{type = result, sub_el = IQRes}; + {error, Error} -> IQ#iq{type = error, sub_el = [Error, SubEl]} + end + end. + +iq_sm(From, To, #iq{type = Type, sub_el = SubEl, xmlns = XMLNS, lang = Lang} = IQ) -> + ServerHost = To#jid.lserver, + LOwner = jlib:jid_tolower(jlib:jid_remove_resource(To)), + Res = case XMLNS of + ?NS_PUBSUB -> iq_pubsub(LOwner, ServerHost, From, Type, SubEl, Lang); + ?NS_PUBSUB_OWNER -> iq_pubsub_owner(LOwner, ServerHost, From, Type, SubEl, Lang) + end, + case Res of + {result, IQRes} -> IQ#iq{type = result, sub_el = IQRes}; + {error, Error} -> IQ#iq{type = error, sub_el = [Error, SubEl]} + end. + +iq_get_vcard(Lang) -> + [{xmlelement, "FN", [], [{xmlcdata, "ejabberd/mod_pubsub"}]}, + {xmlelement, "URL", [], [{xmlcdata, ?EJABBERD_URI}]}, + {xmlelement, "DESC", [], + [{xmlcdata, + translate:translate(Lang, + "ejabberd Publish-Subscribe module") ++ + "\nCopyright (c) 2004-2009 Process-One"}]}]. + +iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang) -> + iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, all, plugins(ServerHost)). + +iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) -> + {xmlelement, _, _, SubEls} = SubEl, + case xml:remove_cdata(SubEls) of + [{xmlelement, Name, Attrs, Els} | Rest] -> + Node = string_to_node(xml:get_attr_s("node", Attrs)), + case {IQType, Name} of + {set, "create"} -> + Config = case Rest of + [{xmlelement, "configure", _, C}] -> C; + _ -> [] + end, + %% Get the type of the node + Type = case xml:get_attr_s("type", Attrs) of + [] -> hd(Plugins); + T -> T + end, + %% we use Plugins list matching because we do not want to allocate + %% atoms for non existing type, this prevent atom allocation overflow + case lists:member(Type, Plugins) of + false -> + {error, extended_error( + ?ERR_FEATURE_NOT_IMPLEMENTED, + unsupported, "create-nodes")}; + true -> + create_node(Host, ServerHost, Node, From, + Type, Access, Config) + end; + {set, "publish"} -> + case xml:remove_cdata(Els) of + [{xmlelement, "item", ItemAttrs, Payload}] -> + ItemId = xml:get_attr_s("id", ItemAttrs), + publish_item(Host, ServerHost, Node, From, ItemId, Payload); + [] -> + %% Publisher attempts to publish to persistent node with no item + {error, extended_error(?ERR_BAD_REQUEST, + "item-required")}; + _ -> + %% Entity attempts to publish item with multiple payload elements or namespace does not match + {error, extended_error(?ERR_BAD_REQUEST, + "invalid-payload")} + end; + {set, "retract"} -> + ForceNotify = case xml:get_attr_s("notify", Attrs) of + "1" -> true; + "true" -> true; + _ -> false + end, + case xml:remove_cdata(Els) of + [{xmlelement, "item", ItemAttrs, _}] -> + ItemId = xml:get_attr_s("id", ItemAttrs), + delete_item(Host, Node, From, ItemId, ForceNotify); + _ -> + %% Request does not specify an item + {error, extended_error(?ERR_BAD_REQUEST, + "item-required")} + end; + {set, "subscribe"} -> + Config = case Rest of + [{xmlelement, "options", _, C}] -> C; + _ -> [] + end, + JID = xml:get_attr_s("jid", Attrs), + subscribe_node(Host, Node, From, JID, Config); + {set, "unsubscribe"} -> + JID = xml:get_attr_s("jid", Attrs), + SubId = xml:get_attr_s("subid", Attrs), + unsubscribe_node(Host, Node, From, JID, SubId); + {get, "items"} -> + MaxItems = xml:get_attr_s("max_items", Attrs), + SubId = xml:get_attr_s("subid", Attrs), + ItemIDs = lists:foldl(fun + ({xmlelement, "item", ItemAttrs, _}, Acc) -> + case xml:get_attr_s("id", ItemAttrs) of + "" -> Acc; + ItemID -> [ItemID|Acc] + end; + (_, Acc) -> + Acc + end, [], xml:remove_cdata(Els)), + RSM = jlib:rsm_decode(SubEl), + get_items(Host, Node, From, SubId, MaxItems, ItemIDs, RSM); + {get, "subscriptions"} -> + get_subscriptions(Host, Node, From, Plugins); + {get, "affiliations"} -> + get_affiliations(Host, From, Plugins); + {get, "options"} -> + SubID = xml:get_attr_s("subid", Attrs), + JID = xml:get_attr_s("jid", Attrs), + get_options(Host, Node, JID, SubID, Lang); + {set, "options"} -> + SubID = xml:get_attr_s("subid", Attrs), + JID = xml:get_attr_s("jid", Attrs), + set_options(Host, Node, JID, SubID, Els); + _ -> + {error, ?ERR_FEATURE_NOT_IMPLEMENTED} + end; + Other -> + ?INFO_MSG("Too many actions: ~p", [Other]), + {error, ?ERR_BAD_REQUEST} + end. + +iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) -> + {xmlelement, _, _, SubEls} = SubEl, + Action = lists:filter(fun({xmlelement, "set", _, _}) -> false; + (_) -> true + end, xml:remove_cdata(SubEls)), + case Action of + [{xmlelement, Name, Attrs, Els}] -> + Node = string_to_node(xml:get_attr_s("node", Attrs)), + case {IQType, Name} of + {get, "configure"} -> + get_configure(Host, ServerHost, Node, From, Lang); + {set, "configure"} -> + set_configure(Host, Node, From, Els, Lang); + {get, "default"} -> + get_default(Host, Node, From, Lang); + {set, "delete"} -> + delete_node(Host, Node, From); + {set, "purge"} -> + purge_node(Host, Node, From); + {get, "subscriptions"} -> + get_subscriptions(Host, Node, From); + {set, "subscriptions"} -> + set_subscriptions(Host, Node, From, xml:remove_cdata(Els)); + {get, "affiliations"} -> + get_affiliations(Host, Node, From); + {set, "affiliations"} -> + set_affiliations(Host, Node, From, xml:remove_cdata(Els)); + _ -> + {error, ?ERR_FEATURE_NOT_IMPLEMENTED} + end; + _ -> + ?INFO_MSG("Too many actions: ~p", [Action]), + {error, ?ERR_BAD_REQUEST} + end. + +iq_command(Host, ServerHost, From, IQ, Access, Plugins) -> + case adhoc:parse_request(IQ) of + Req when is_record(Req, adhoc_request) -> + case adhoc_request(Host, ServerHost, From, Req, Access, Plugins) of + Resp when is_record(Resp, adhoc_response) -> + {result, [adhoc:produce_response(Req, Resp)]}; + Error -> + Error + end; + Err -> + Err + end. + +%% @doc <p>Processes an Ad Hoc Command.</p> +adhoc_request(Host, _ServerHost, Owner, + #adhoc_request{node = ?NS_PUBSUB_GET_PENDING, + lang = Lang, + action = "execute", + xdata = false}, + _Access, Plugins) -> + send_pending_node_form(Host, Owner, Lang, Plugins); +adhoc_request(Host, _ServerHost, Owner, + #adhoc_request{node = ?NS_PUBSUB_GET_PENDING, + action = "execute", + xdata = XData}, + _Access, _Plugins) -> + ParseOptions = case XData of + {xmlelement, "x", _Attrs, _SubEls} = XEl -> + case jlib:parse_xdata_submit(XEl) of + invalid -> + {error, ?ERR_BAD_REQUEST}; + XData2 -> + case set_xoption(Host, XData2, []) of + NewOpts when is_list(NewOpts) -> + {result, NewOpts}; + Err -> + Err + end + end; + _ -> + ?INFO_MSG("Bad XForm: ~p", [XData]), + {error, ?ERR_BAD_REQUEST} + end, + case ParseOptions of + {result, XForm} -> + case lists:keysearch(node, 1, XForm) of + {value, {_, Node}} -> + send_pending_auth_events(Host, Node, Owner); + false -> + {error, extended_error(?ERR_BAD_REQUEST, "bad-payload")} + end; + Error -> + Error + end; +adhoc_request(_Host, _ServerHost, _Owner, Other, _Access, _Plugins) -> + ?DEBUG("Couldn't process ad hoc command:~n~p", [Other]), + {error, ?ERR_ITEM_NOT_FOUND}. + +%% @spec (Host, Owner, Lang, Plugins) -> iqRes() +%% @doc <p>Sends the process pending subscriptions XForm for Host to +%% Owner.</p> +send_pending_node_form(Host, Owner, _Lang, Plugins) -> + Filter = + fun (Plugin) -> + lists:member("get-pending", features(Plugin)) + end, + case lists:filter(Filter, Plugins) of + [] -> + {error, ?ERR_FEATURE_NOT_IMPLEMENTED}; + Ps -> + XOpts = lists:map(fun (Node) -> + {xmlelement, "option", [], + [{xmlelement, "value", [], + [{xmlcdata, node_to_string(Node)}]}]} + end, get_pending_nodes(Host, Owner, Ps)), + XForm = {xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], + [{xmlelement, "field", + [{"type", "list-single"}, {"var", "pubsub#node"}], + lists:usort(XOpts)}]}, + #adhoc_response{status = executing, + defaultaction = "execute", + elements = [XForm]} + end. + +get_pending_nodes(Host, Owner, Plugins) -> + Tr = + fun (Type) -> + case node_call(Type, get_pending_nodes, [Host, Owner]) of + {result, Nodes} -> Nodes; + _ -> [] + end + end, + case transaction(Host, + fun () -> {result, lists:flatmap(Tr, Plugins)} end, + sync_dirty) of + {result, Res} -> Res; + Err -> Err + end. + +%% @spec (Host, Node, Owner) -> iqRes() +%% @doc <p>Send a subscription approval form to Owner for all pending +%% subscriptions on Host and Node.</p> +send_pending_auth_events(Host, Node, Owner) -> + ?DEBUG("Sending pending auth events for ~s on ~s:~s", + [jlib:jid_to_string(Owner), Host, node_to_string(Node)]), + Action = + fun (#pubsub_node{id = NodeID, type = Type} = N) -> + case lists:member("get-pending", features(Type)) of + true -> + case node_call(Type, get_affiliation, [NodeID, Owner]) of + {result, owner} -> + {result, Subscriptions} = node_call(Type, get_node_subscriptions, [NodeID]), + lists:foreach(fun({J, pending, _SubID}) -> send_authorization_request(N, jlib:make_jid(J)); + ({J, pending}) -> send_authorization_request(N, jlib:make_jid(J)); + (_) -> ok + end, Subscriptions), + {result, ok}; + _ -> + {error, ?ERR_FORBIDDEN} + end; + false -> + {error, ?ERR_FEATURE_NOT_IMPLEMENTED} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, _} -> + #adhoc_response{}; + Err -> + Err + end. + +%%% authorization handling + +send_authorization_request(#pubsub_node{nodeid = {Host, Node}, type = Type, id = NodeId}, Subscriber) -> + Lang = "en", %% TODO fix + Stanza = {xmlelement, "message", + [], + [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], + [{xmlelement, "title", [], + [{xmlcdata, translate:translate(Lang, "PubSub subscriber request")}]}, + {xmlelement, "instructions", [], + [{xmlcdata, translate:translate(Lang, "Choose whether to approve this entity's subscription.")}]}, + {xmlelement, "field", + [{"var", "FORM_TYPE"}, {"type", "hidden"}], + [{xmlelement, "value", [], [{xmlcdata, ?NS_PUBSUB_SUB_AUTH}]}]}, + {xmlelement, "field", + [{"var", "pubsub#node"}, {"type", "text-single"}, + {"label", translate:translate(Lang, "Node ID")}], + [{xmlelement, "value", [], + [{xmlcdata, node_to_string(Node)}]}]}, + {xmlelement, "field", [{"var", "pubsub#subscriber_jid"}, + {"type", "jid-single"}, + {"label", translate:translate(Lang, "Subscriber Address")}], + [{xmlelement, "value", [], + [{xmlcdata, jlib:jid_to_string(Subscriber)}]}]}, + {xmlelement, "field", + [{"var", "pubsub#allow"}, + {"type", "boolean"}, + {"label", translate:translate(Lang, "Allow this Jabber ID to subscribe to this pubsub node?")}], + [{xmlelement, "value", [], [{xmlcdata, "false"}]}]}]}]}, + lists:foreach(fun(Owner) -> + ejabberd_router ! {route, service_jid(Host), jlib:make_jid(Owner), Stanza} + end, node_owners(Host, Type, NodeId)). + +find_authorization_response(Packet) -> + {xmlelement, _Name, _Attrs, Els} = Packet, + XData1 = lists:map(fun({xmlelement, "x", XAttrs, _} = XEl) -> + case xml:get_attr_s("xmlns", XAttrs) of + ?NS_XDATA -> + case xml:get_attr_s("type", XAttrs) of + "cancel" -> + none; + _ -> + jlib:parse_xdata_submit(XEl) + end; + _ -> + none + end; + (_) -> + none + end, xml:remove_cdata(Els)), + XData = lists:filter(fun(E) -> E /= none end, XData1), + case XData of + [invalid] -> invalid; + [] -> none; + [XFields] when is_list(XFields) -> + ?DEBUG("XFields: ~p", [XFields]), + case lists:keysearch("FORM_TYPE", 1, XFields) of + {value, {_, [?NS_PUBSUB_SUB_AUTH]}} -> + XFields; + _ -> + invalid + end + end. + +%% @spec (Host, JID, Node, Subscription) -> void +%% Host = mod_pubsub:host() +%% JID = jlib:jid() +%% SNode = string() +%% Subscription = atom() | {atom(), mod_pubsub:subid()} +%% @doc Send a message to JID with the supplied Subscription +send_authorization_approval(Host, JID, SNode, Subscription) -> + SubAttrs = case Subscription of + {S, SID} -> [{"subscription", subscription_to_string(S)}, + {"subid", SID}]; + S -> [{"subscription", subscription_to_string(S)}] + end, + Stanza = event_stanza( + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(JID)}|nodeAttr(SNode)] ++ SubAttrs, + []}]), + ejabberd_router ! {route, service_jid(Host), JID, Stanza}. + +handle_authorization_response(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, {_, [SNode]}}, {value, {_, [SSubscriber]}}, + {value, {_, [SAllow]}}} -> + Node = string_to_node(SNode), + Subscriber = jlib:string_to_jid(SSubscriber), + Allow = case SAllow of + "1" -> true; + "true" -> true; + _ -> false + end, + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + IsApprover = lists:member(jlib:jid_tolower(jlib:jid_remove_resource(From)), node_owners_call(Type, NodeId)), + {result, Subscriptions} = node_call(Type, get_subscriptions, [NodeId, Subscriber]), + if + not IsApprover -> + {error, ?ERR_FORBIDDEN}; + true -> + update_auth(Host, SNode, Type, NodeId, + Subscriber, Allow, + Subscriptions) + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {error, Error} -> + ejabberd_router:route( + To, From, + jlib:make_error_reply(Packet, Error)); + {result, {_, _NewSubscription}} -> + %% XXX: notify about subscription state change, section 12.11 + ok; + _ -> + ejabberd_router:route( + To, From, + jlib:make_error_reply(Packet, ?ERR_INTERNAL_SERVER_ERROR)) + end; + _ -> + ejabberd_router:route( + To, From, + jlib:make_error_reply(Packet, ?ERR_NOT_ACCEPTABLE)) + end. + +update_auth(Host, Node, Type, NodeId, Subscriber, + Allow, Subscriptions) -> + Subscription = lists:filter(fun({pending, _}) -> true; + (_) -> false + end, Subscriptions), + case Subscription of + [{pending, SubID}] -> %% TODO does not work if several pending + NewSubscription = case Allow of + true -> subscribed; + false -> none + end, + node_call(Type, set_subscriptions, + [NodeId, Subscriber, NewSubscription, SubID]), + send_authorization_approval(Host, Subscriber, Node, + NewSubscription), + {result, ok}; + _ -> + {error, ?ERR_UNEXPECTED_REQUEST} + end. + +-define(XFIELD(Type, Label, Var, Val), + {xmlelement, "field", [{"type", Type}, + {"label", translate:translate(Lang, Label)}, + {"var", Var}], + [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). + +-define(BOOLXFIELD(Label, Var, Val), + ?XFIELD("boolean", Label, Var, + case Val of + true -> "1"; + _ -> "0" + end)). + +-define(STRINGXFIELD(Label, Var, Val), + ?XFIELD("text-single", Label, Var, Val)). + +-define(STRINGMXFIELD(Label, Var, Vals), + {xmlelement, "field", [{"type", "text-multi"}, + {"label", translate:translate(Lang, Label)}, + {"var", Var}], + [{xmlelement, "value", [], [{xmlcdata, V}]} || V <- Vals]}). + +-define(XFIELDOPT(Type, Label, Var, Val, Opts), + {xmlelement, "field", [{"type", Type}, + {"label", translate:translate(Lang, Label)}, + {"var", Var}], + lists:map(fun(Opt) -> + {xmlelement, "option", [], + [{xmlelement, "value", [], + [{xmlcdata, Opt}]}]} + end, Opts) ++ + [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). + +-define(LISTXFIELD(Label, Var, Val, Opts), + ?XFIELDOPT("list-single", Label, Var, Val, Opts)). + +-define(LISTMXFIELD(Label, Var, Vals, Opts), + {xmlelement, "field", [{"type", "list-multi"}, + {"label", translate:translate(Lang, Label)}, + {"var", Var}], + lists:map(fun(Opt) -> + {xmlelement, "option", [], + [{xmlelement, "value", [], + [{xmlcdata, Opt}]}]} + end, Opts) ++ + lists:map(fun(Val) -> + {xmlelement, "value", [], + [{xmlcdata, Val}]} + end, Vals)}). + +%% @spec (Host::host(), ServerHost::host(), Node::pubsubNode(), Owner::jid(), NodeType::nodeType()) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% @doc <p>Create new pubsub nodes</p> +%%<p>In addition to method-specific error conditions, there are several general reasons why the node creation request might fail:</p> +%%<ul> +%%<li>The service does not support node creation.</li> +%%<li>Only entities that are registered with the service are allowed to create nodes but the requesting entity is not registered.</li> +%%<li>The requesting entity does not have sufficient privileges to create nodes.</li> +%%<li>The requested NodeID already exists.</li> +%%<li>The request did not include a NodeID and "instant nodes" are not supported.</li> +%%</ul> +%%<p>ote: node creation is a particular case, error return code is evaluated at many places:</p> +%%<ul> +%%<li>iq_pubsub checks if service supports node creation (type exists)</li> +%%<li>create_node checks if instant nodes are supported</li> +%%<li>create_node asks node plugin if entity have sufficient privilege</li> +%%<li>nodetree create_node checks if nodeid already exists</li> +%%<li>node plugin create_node just sets default affiliation/subscription</li> +%%</ul> +create_node(Host, ServerHost, Node, Owner, Type) -> + create_node(Host, ServerHost, Node, Owner, Type, all, []). +create_node(Host, ServerHost, <<>>, Owner, Type, Access, Configuration) -> + case lists:member("instant-nodes", features(Type)) of + true -> + NewNode = string_to_node(randoms:get_string()), + case create_node(Host, ServerHost, + NewNode, Owner, Type, Access, Configuration) of + {result, []} -> + {result, + [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "create", nodeAttr(NewNode), []}]}]}; + Error -> + Error + end; + false -> + %% Service does not support instant nodes + {error, extended_error(?ERR_NOT_ACCEPTABLE, "nodeid-required")} + end; +create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> + Type = select_type(ServerHost, Host, Node, GivenType), + %% TODO, check/set node_type = Type + ParseOptions = case xml:remove_cdata(Configuration) of + [] -> + {result, node_options(Type)}; + [{xmlelement, "x", _Attrs, _SubEls} = XEl] -> + case jlib:parse_xdata_submit(XEl) of + invalid -> + {error, ?ERR_BAD_REQUEST}; + XData -> + case set_xoption(Host, XData, node_options(Type)) of + NewOpts when is_list(NewOpts) -> + {result, NewOpts}; + Err -> + Err + end + end; + _ -> + ?INFO_MSG("Node ~p; bad configuration: ~p", [Node, Configuration]), + {error, ?ERR_BAD_REQUEST} + end, + case ParseOptions of + {result, NodeOptions} -> + CreateNode = + fun() -> + SNode = node_to_string(Node), + Parent = case node_call(Type, node_to_path, [Node]) of + {result, [SNode]} -> <<>>; + {result, Path} -> element(2, node_call(Type, path_to_node, [lists:sublist(Path, length(Path)-1)])) + end, + Parents = case Parent of + <<>> -> []; + _ -> [Parent] + end, + case node_call(Type, create_node_permission, [Host, ServerHost, Node, Parent, Owner, Access]) of + {result, true} -> + case tree_call(Host, create_node, [Host, Node, Type, Owner, NodeOptions, Parents]) of + {ok, NodeId} -> + node_call(Type, create_node, [NodeId, Owner]); + {error, {virtual, NodeId}} -> + node_call(Type, create_node, [NodeId, Owner]); + Error -> + Error + end; + _ -> + {error, ?ERR_FORBIDDEN} + end + end, + Reply = [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "create", nodeAttr(Node), + []}]}], + case transaction(Host, CreateNode, transaction) of + {result, {Result, broadcast}} -> + %%Lang = "en", %% TODO: fix + %%OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + %%broadcast_publish_item(Host, Node, uniqid(), Owner, + %% [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}], + %% [?XFIELD("hidden", "", "FORM_TYPE", ?NS_PUBSUB_NMI), + %% ?XFIELD("jid-single", "Node Creator", "creator", jlib:jid_to_string(OwnerKey))]}]), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, default} -> + {result, Reply}; + {result, Result} -> + {result, Result}; + Error -> + %% in case we change transaction to sync_dirty... + %% node_call(Type, delete_node, [Host, Node]), + %% tree_call(Host, delete_node, [Host, Node]), + Error + end; + Error -> + Error + end. + +%% @spec (Host, Node, Owner) -> +%% {error, Reason} | {result, []} +%% Host = host() +%% Node = pubsubNode() +%% Owner = jid() +%% Reason = stanzaError() +%% @doc <p>Delete specified node and all childs.</p> +%%<p>There are several reasons why the node deletion request might fail:</p> +%%<ul> +%%<li>The requesting entity does not have sufficient privileges to delete the node.</li> +%%<li>The node is the root collection node, which cannot be deleted.</li> +%%<li>The specified node does not exist.</li> +%%</ul> +delete_node(_Host, [], _Owner) -> + %% Node is the root + {error, ?ERR_NOT_ALLOWED}; +delete_node(Host, Node, Owner) -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + case node_call(Type, get_affiliation, [NodeId, Owner]) of + {result, owner} -> + ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]), + SubsByDepth = [{Depth, [{N, get_node_subs(N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree], + Removed = tree_call(Host, delete_node, [Host, Node]), + case node_call(Type, delete_node, [Removed]) of + {result, Res} -> {result, {SubsByDepth, Res}}; + Error -> Error + end; + _ -> + %% Entity is not an owner + {error, ?ERR_FORBIDDEN} + end + end, + Reply = [], + case transaction(Host, Node, Action, transaction) of + {result, {_, {SubsByDepth, {Result, broadcast, Removed}}}} -> + lists:foreach(fun({RNode, _RSubscriptions}) -> + {RH, RN} = RNode#pubsub_node.nodeid, + NodeId = RNode#pubsub_node.id, + Type = RNode#pubsub_node.type, + Options = RNode#pubsub_node.options, + broadcast_removed_node(RH, RN, NodeId, Type, Options, SubsByDepth) + end, Removed), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {_, {_, {Result, _Removed}}}} -> + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {_, {_, default}}} -> + {result, Reply}; + {result, {_, {_, Result}}} -> + {result, Result}; + Error -> + Error + end. + +%% @spec (Host, Node, From, JID, Configuration) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% Host = host() +%% Node = pubsubNode() +%% From = jid() +%% JID = jid() +%% @see node_hometree:subscribe_node/5 +%% @doc <p>Accepts or rejects subcription requests on a PubSub node.</p> +%%<p>There are several reasons why the subscription request might fail:</p> +%%<ul> +%%<li>The bare JID portions of the JIDs do not match.</li> +%%<li>The node has an access model of "presence" and the requesting entity is not subscribed to the owner's presence.</li> +%%<li>The node has an access model of "roster" and the requesting entity is not in one of the authorized roster groups.</li> +%%<li>The node has an access model of "whitelist" and the requesting entity is not on the whitelist.</li> +%%<li>The service requires payment for subscriptions to the node.</li> +%%<li>The requesting entity is anonymous and the service does not allow anonymous entities to subscribe.</li> +%%<li>The requesting entity has a pending subscription.</li> +%%<li>The requesting entity is blocked from subscribing (e.g., because having an affiliation of outcast).</li> +%%<li>The node does not support subscriptions.</li> +%%<li>The node does not exist.</li> +%%</ul> +subscribe_node(Host, Node, From, JID, Configuration) -> + SubOpts = case pubsub_subscription_odbc:parse_options_xform(Configuration) of + {result, GoodSubOpts} -> GoodSubOpts; + _ -> invalid + end, + Subscriber = case jlib:string_to_jid(JID) of + error -> {"", "", ""}; + J -> jlib:jid_tolower(J) + end, + Action = fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> + Features = features(Type), + SubscribeFeature = lists:member("subscribe", Features), + OptionsFeature = lists:member("subscription-options", Features), + HasOptions = not (SubOpts == []), + SubscribeConfig = get_option(Options, subscribe), + AccessModel = get_option(Options, access_model), + SendLast = get_option(Options, send_last_published_item), + AllowedGroups = get_option(Options, roster_groups_allowed, []), + {PresenceSubscription, RosterGroup} = + case Host of + {OUser, OServer, _} -> + get_roster_info(OUser, OServer, + Subscriber, AllowedGroups); + _ -> + case Subscriber of + {"", "", ""} -> + {false, false}; + _ -> + case node_owners_call(Type, NodeId) of + [{OU, OS, _}|_] -> + get_roster_info(OU, OS, + Subscriber, AllowedGroups); + _ -> + {false, false} + end + end + end, + if + not SubscribeFeature -> + %% Node does not support subscriptions + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "subscribe")}; + not SubscribeConfig -> + %% Node does not support subscriptions + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "subscribe")}; + HasOptions andalso not OptionsFeature -> + %% Node does not support subscription options + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "subscription-options")}; + SubOpts == invalid -> + %% Passed invalit options submit form + {error, extended_error(?ERR_BAD_REQUEST, "invalid-options")}; + true -> + node_call(Type, subscribe_node, + [NodeId, From, Subscriber, + AccessModel, SendLast, + PresenceSubscription, RosterGroup, + SubOpts]) + end + end, + Reply = fun(Subscription) -> + %% TODO, this is subscription-notification, should depends on node features + SubAttrs = case Subscription of + {subscribed, SubId} -> + [{"subscription", subscription_to_string(subscribed)}, + {"subid", SubId}]; + Other -> + [{"subscription", subscription_to_string(Other)}] + end, + Fields = + [{"jid", jlib:jid_to_string(Subscriber)} | SubAttrs], + [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "subscription", Fields, []}]}] + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {TNode, {Result, subscribed, SubId, send_last}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + send_items(Host, Node, NodeId, Type, Subscriber, last), + case Result of + default -> {result, Reply({subscribed, SubId})}; + _ -> {result, Result} + end; + {result, {_TNode, {default, subscribed, SubId}}} -> + {result, Reply({subscribed, SubId})}; + {result, {_TNode, {Result, subscribed, _SubId}}} -> + {result, Result}; + {result, {TNode, {default, pending, _SubId}}} -> + send_authorization_request(TNode, Subscriber), + {result, Reply(pending)}; + {result, {TNode, {Result, pending}}} -> + send_authorization_request(TNode, Subscriber), + {result, Result}; + {result, {_, Result}} -> + %% this case should never occure anyway + {result, Result}; + Error -> + Error + end. + +%% @spec (Host, Noce, From, JID, SubId) -> {error, Reason} | {result, []} +%% Host = host() +%% Node = pubsubNode() +%% From = jid() +%% JID = string() +%% SubId = string() +%% Reason = stanzaError() +%% @doc <p>Unsubscribe <tt>JID</tt> from the <tt>Node</tt>.</p> +%%<p>There are several reasons why the unsubscribe request might fail:</p> +%%<ul> +%%<li>The requesting entity has multiple subscriptions to the node but does not specify a subscription ID.</li> +%%<li>The request does not specify an existing subscriber.</li> +%%<li>The requesting entity does not have sufficient privileges to unsubscribe the specified JID.</li> +%%<li>The node does not exist.</li> +%%<li>The request specifies a subscription ID that is not valid or current.</li> +%%</ul> +unsubscribe_node(Host, Node, From, JID, SubId) when is_list(JID) -> + Subscriber = case jlib:string_to_jid(JID) of + error -> {"", "", ""}; + J -> jlib:jid_tolower(J) + end, + unsubscribe_node(Host, Node, From, Subscriber, SubId); +unsubscribe_node(Host, Node, From, Subscriber, SubId) -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + node_call(Type, unsubscribe_node, [NodeId, From, Subscriber, SubId]) + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, default}} -> + {result, []}; + {result, {_, Result}} -> + {result, Result}; + Error -> + Error + end. + +%% @spec (Host::host(), ServerHost::host(), JID::jid(), Node::pubsubNode(), ItemId::string(), Payload::term()) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% @doc <p>Publish item to a PubSub node.</p> +%% <p>The permission to publish an item must be verified by the plugin implementation.</p> +%%<p>There are several reasons why the publish request might fail:</p> +%%<ul> +%%<li>The requesting entity does not have sufficient privileges to publish.</li> +%%<li>The node does not support item publication.</li> +%%<li>The node does not exist.</li> +%%<li>The payload size exceeds a service-defined limit.</li> +%%<li>The item contains more than one payload element or the namespace of the root payload element does not match the configured namespace for the node.</li> +%%<li>The request does not match the node configuration.</li> +%%</ul> +publish_item(Host, ServerHost, Node, Publisher, "", Payload) -> + %% if publisher does not specify an ItemId, the service MUST generate the ItemId + publish_item(Host, ServerHost, Node, Publisher, uniqid(), Payload); +publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) -> + Action = fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> + Features = features(Type), + PublishFeature = lists:member("publish", Features), + PublishModel = get_option(Options, publish_model), + MaxItems = max_items(Host, Options), + DeliverPayloads = get_option(Options, deliver_payloads), + PersistItems = get_option(Options, persist_items), + PayloadCount = payload_xmlelements(Payload), + PayloadSize = size(term_to_binary(Payload)), + PayloadMaxSize = get_option(Options, max_payload_size), + % pubsub#deliver_payloads true + % pubsub#persist_items true -> 1 item; false -> 0 item + if + not PublishFeature -> + %% Node does not support item publication + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "publish")}; + PayloadSize > PayloadMaxSize -> + %% Entity attempts to publish very large payload + {error, extended_error(?ERR_NOT_ACCEPTABLE, "payload-too-big")}; + (PayloadCount == 0) and (Payload == []) -> + %% Publisher attempts to publish to payload node with no payload + {error, extended_error(?ERR_BAD_REQUEST, "payload-required")}; + (PayloadCount > 1) or (PayloadCount == 0) -> + %% Entity attempts to publish item with multiple payload elements + {error, extended_error(?ERR_BAD_REQUEST, "invalid-payload")}; + (DeliverPayloads == 0) and (PersistItems == 0) and (PayloadSize > 0) -> + %% Publisher attempts to publish to transient notification node with item + {error, extended_error(?ERR_BAD_REQUEST, "item-forbidden")}; + ((DeliverPayloads == 1) or (PersistItems == 1)) and (PayloadSize == 0) -> + %% Publisher attempts to publish to persistent node with no item + {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; + true -> + node_call(Type, publish_item, [NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload]) + end + end, + ejabberd_hooks:run(pubsub_publish_item, ServerHost, [ServerHost, Node, Publisher, service_jid(Host), ItemId, Payload]), + Reply = [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "publish", nodeAttr(Node), + [{xmlelement, "item", itemAttr(ItemId), []}]}]}], + case transaction(Host, Node, Action, sync_dirty) of + {result, {TNode, {Result, Broadcast, Removed}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + BroadcastPayload = case Broadcast of + default -> Payload; + broadcast -> Payload; + PluginPayload -> PluginPayload + end, + broadcast_publish_item(Host, Node, NodeId, Type, Options, Removed, ItemId, jlib:jid_tolower(Publisher), BroadcastPayload), + set_cached_item(Host, NodeId, ItemId, Payload), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {TNode, {default, Removed}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_retract_items(Host, Node, NodeId, Type, Options, Removed), + set_cached_item(Host, NodeId, ItemId, Payload), + {result, Reply}; + {result, {TNode, {Result, Removed}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_retract_items(Host, Node, NodeId, Type, Options, Removed), + set_cached_item(Host, NodeId, ItemId, Payload), + {result, Result}; + {result, {_, default}} -> + {result, Reply}; + {result, {_, Result}} -> + {result, Result}; + {error, ?ERR_ITEM_NOT_FOUND} -> + %% handles auto-create feature + %% for automatic node creation. we'll take the default node type: + %% first listed into the plugins configuration option, or pep + Type = select_type(ServerHost, Host, Node), + case lists:member("auto-create", features(Type)) of + true -> + case create_node(Host, ServerHost, Node, Publisher, Type) of + {result, _} -> + publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload); + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end; + false -> + {error, ?ERR_ITEM_NOT_FOUND} + end; + Error -> + Error + end. + +%% @spec (Host::host(), JID::jid(), Node::pubsubNode(), ItemId::string()) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% @doc <p>Delete item from a PubSub node.</p> +%% <p>The permission to delete an item must be verified by the plugin implementation.</p> +%%<p>There are several reasons why the item retraction request might fail:</p> +%%<ul> +%%<li>The publisher does not have sufficient privileges to delete the requested item.</li> +%%<li>The node or item does not exist.</li> +%%<li>The request does not specify a node.</li> +%%<li>The request does not include an <item/> element or the <item/> element does not specify an ItemId.</li> +%%<li>The node does not support persistent items.</li> +%%<li>The service does not support the deletion of items.</li> +%%</ul> +delete_item(Host, Node, Publisher, ItemId) -> + delete_item(Host, Node, Publisher, ItemId, false). +delete_item(_, "", _, _, _) -> + %% Request does not specify a node + {error, extended_error(?ERR_BAD_REQUEST, "node-required")}; +delete_item(Host, Node, Publisher, ItemId, ForceNotify) -> + Action = fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> + Features = features(Type), + PersistentFeature = lists:member("persistent-items", Features), + DeleteFeature = lists:member("delete-items", Features), + PublishModel = get_option(Options, publish_model), + if + %%-> iq_pubsub just does that matchs + %% %% Request does not specify an item + %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; + not PersistentFeature -> + %% Node does not support persistent items + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "persistent-items")}; + not DeleteFeature -> + %% Service does not support item deletion + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "delete-items")}; + true -> + node_call(Type, delete_item, [NodeId, Publisher, PublishModel, ItemId]) + end + end, + Reply = [], + case transaction(Host, Node, Action, sync_dirty) of + {result, {TNode, {Result, broadcast}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_retract_items(Host, Node, NodeId, Type, Options, [ItemId], ForceNotify), + case get_cached_item(Host, NodeId) of + #pubsub_item{itemid = {ItemId, NodeId}, _ = '_'} -> unset_cached_item(Host, NodeId); + _ -> ok + end, + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {_, default}} -> + {result, Reply}; + {result, {_, Result}} -> + {result, Result}; + Error -> + Error + end. + +%% @spec (Host, JID, Node) -> +%% {error, Reason} | {result, []} +%% Host = host() +%% Node = pubsubNode() +%% JID = jid() +%% Reason = stanzaError() +%% @doc <p>Delete all items of specified node owned by JID.</p> +%%<p>There are several reasons why the node purge request might fail:</p> +%%<ul> +%%<li>The node or service does not support node purging.</li> +%%<li>The requesting entity does not have sufficient privileges to purge the node.</li> +%%<li>The node is not configured to persist items.</li> +%%<li>The specified node does not exist.</li> +%%</ul> +purge_node(Host, Node, Owner) -> + Action = fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> + Features = features(Type), + PurgeFeature = lists:member("purge-nodes", Features), + PersistentFeature = lists:member("persistent-items", Features), + PersistentConfig = get_option(Options, persist_items), + if + not PurgeFeature -> + %% Service does not support node purging + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "purge-nodes")}; + not PersistentFeature -> + %% Node does not support persistent items + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "persistent-items")}; + not PersistentConfig -> + %% Node is not configured for persistent items + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "persistent-items")}; + true -> + node_call(Type, purge_node, [NodeId, Owner]) + end + end, + Reply = [], + case transaction(Host, Node, Action, sync_dirty) of + {result, {TNode, {Result, broadcast}}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_purge_node(Host, Node, NodeId, Type, Options), + unset_cached_item(Host, NodeId), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {_, default}} -> + {result, Reply}; + {result, {_, Result}} -> + {result, Result}; + Error -> + Error + end. + +%% @doc <p>Return the items of a given node.</p> +%% <p>The number of items to return is limited by MaxItems.</p> +%% <p>The permission are not checked in this function.</p> +%% @todo We probably need to check that the user doing the query has the right +%% to read the items. +get_items(Host, Node, From, SubId, SMaxItems, ItemIDs, RSM) -> + MaxItems = + if + SMaxItems == "" -> get_max_items_node(Host); + true -> + case catch list_to_integer(SMaxItems) of + {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; + Val -> Val + end + end, + case MaxItems of + {error, Error} -> + {error, Error}; + _ -> + Action = fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> + Features = features(Type), + RetreiveFeature = lists:member("retrieve-items", Features), + PersistentFeature = lists:member("persistent-items", Features), + AccessModel = get_option(Options, access_model), + AllowedGroups = get_option(Options, roster_groups_allowed, []), + {PresenceSubscription, RosterGroup} = + case Host of + {OUser, OServer, _} -> + get_roster_info(OUser, OServer, + jlib:jid_tolower(From), AllowedGroups); + _ -> + {true, true} + end, + if + not RetreiveFeature -> + %% Item Retrieval Not Supported + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "retrieve-items")}; + not PersistentFeature -> + %% Persistent Items Not Supported + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "persistent-items")}; + true -> + node_call(Type, get_items, + [NodeId, From, + AccessModel, PresenceSubscription, RosterGroup, + SubId, RSM]) + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, {Items, RSMOut}}} -> + SendItems = case ItemIDs of + [] -> + Items; + _ -> + lists:filter(fun(#pubsub_item{itemid = {ItemId, _}}) -> + lists:member(ItemId, ItemIDs) + end, Items) + end, + %% Generate the XML response (Item list), limiting the + %% number of items sent to MaxItems: + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "items", nodeAttr(Node), + itemsEls(lists:sublist(SendItems, MaxItems))} + | jlib:rsm_encode(RSMOut)]}]}; + Error -> + Error + end + end. +get_items(Host, Node) -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + node_call(Type, get_items, [NodeId, service_jid(Host)]) + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Items}} -> Items; + Error -> Error + end. +get_item(Host, Node, ItemId) -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + node_call(Type, get_item, [NodeId, ItemId]) + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Items}} -> Items; + Error -> Error + end. + +%% @spec (Host, Node, NodeId, Type, LJID, Number) -> any() +%% Host = pubsubHost() +%% Node = pubsubNode() +%% NodeId = pubsubNodeId() +%% Type = pubsubNodeType() +%% LJID = {U, S, []} +%% Number = last | integer() +%% @doc <p>Resend the items of a node to the user.</p> +%% @todo use cache-last-item feature +send_items(Host, Node, NodeId, Type, LJID, last) -> + Stanza = case get_cached_item(Host, NodeId) of + undefined -> + % special ODBC optimization, works only with node_hometree_odbc, node_flat_odbc and node_pep_odbc + case node_action(Host, Type, get_last_items, [NodeId, LJID, 1]) of + {result, [LastItem]} -> + {ModifNow, ModifLjid} = LastItem#pubsub_item.modification, + event_stanza_with_delay( + [{xmlelement, "items", nodeAttr(Node), + itemsEls([LastItem])}], ModifNow, ModifLjid); + _ -> + event_stanza( + [{xmlelement, "items", nodeAttr(Node), + itemsEls([])}]) + end; + LastItem -> + {ModifNow, ModifLjid} = LastItem#pubsub_item.modification, + event_stanza_with_delay( + [{xmlelement, "items", nodeAttr(Node), + itemsEls([LastItem])}], ModifNow, ModifLjid) + end, + ejabberd_router ! {route, service_jid(Host), jlib:make_jid(LJID), Stanza}; +send_items(Host, Node, NodeId, Type, LJID, Number) -> + ToSend = case node_action(Host, Type, get_items, [NodeId, LJID]) of + {result, []} -> + []; + {result, Items} -> + case Number of + N when N > 0 -> lists:sublist(Items, N); + _ -> Items + end; + _ -> + [] + end, + Stanza = case ToSend of + [LastItem] -> + {ModifNow, ModifLjid} = LastItem#pubsub_item.modification, + event_stanza_with_delay( + [{xmlelement, "items", nodeAttr(Node), + itemsEls(ToSend)}], ModifNow, ModifLjid); + _ -> + event_stanza( + [{xmlelement, "items", nodeAttr(Node), + itemsEls(ToSend)}]) + end, + ejabberd_router ! {route, service_jid(Host), jlib:make_jid(LJID), Stanza}. + +%% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response} +%% Host = host() +%% JID = jid() +%% Plugins = [Plugin::string()] +%% Reason = stanzaError() +%% Response = [pubsubIQResponse()] +%% @doc <p>Return the list of affiliations as an XMPP response.</p> +get_affiliations(Host, JID, Plugins) when is_list(Plugins) -> + Result = lists:foldl( + fun(Type, {Status, Acc}) -> + Features = features(Type), + RetrieveFeature = lists:member("retrieve-affiliations", Features), + if + not RetrieveFeature -> + %% Service does not support retreive affiliatons + {{error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "retrieve-affiliations")}, Acc}; + true -> + {result, Affiliations} = node_action(Host, Type, get_entity_affiliations, [Host, JID]), + {Status, [Affiliations|Acc]} + end + end, {ok, []}, Plugins), + case Result of + {ok, Affiliations} -> + Entities = lists:flatmap( + fun({_, none}) -> []; + ({#pubsub_node{nodeid = {_, Node}}, Affiliation}) -> + [{xmlelement, "affiliation", + [{"affiliation", affiliation_to_string(Affiliation)}|nodeAttr(Node)], + []}] + end, lists:usort(lists:flatten(Affiliations))), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "affiliations", [], + Entities}]}]}; + {Error, _} -> + Error + end; +get_affiliations(Host, Node, JID) -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + Features = features(Type), + RetrieveFeature = lists:member("modify-affiliations", Features), + {result, Affiliation} = node_call(Type, get_affiliation, [NodeId, JID]), + if + not RetrieveFeature -> + %% Service does not support modify affiliations + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "modify-affiliations")}; + Affiliation /= owner -> + %% Entity is not an owner + {error, ?ERR_FORBIDDEN}; + true -> + node_call(Type, get_node_affiliations, [NodeId]) + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, []}} -> + {error, ?ERR_ITEM_NOT_FOUND}; + {result, {_, Affiliations}} -> + Entities = lists:flatmap( + fun({_, none}) -> []; + ({AJID, Affiliation}) -> + [{xmlelement, "affiliation", + [{"jid", jlib:jid_to_string(AJID)}, + {"affiliation", affiliation_to_string(Affiliation)}], + []}] + end, Affiliations), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_OWNER}], + [{xmlelement, "affiliations", nodeAttr(Node), + Entities}]}]}; + Error -> + Error + end. + +set_affiliations(Host, Node, From, EntitiesEls) -> + Owner = jlib:jid_tolower(jlib:jid_remove_resource(From)), + Entities = + lists:foldl( + fun(El, Acc) -> + case Acc of + error -> + error; + _ -> + case El of + {xmlelement, "affiliation", Attrs, _} -> + JID = jlib:string_to_jid( + xml:get_attr_s("jid", Attrs)), + Affiliation = string_to_affiliation( + xml:get_attr_s("affiliation", Attrs)), + if + (JID == error) or + (Affiliation == false) -> + error; + true -> + [{jlib:jid_tolower(JID), Affiliation} | Acc] + end + end + end + end, [], EntitiesEls), + case Entities of + error -> + {error, ?ERR_BAD_REQUEST}; + _ -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + case lists:member(Owner, node_owners_call(Type, NodeId)) of + true -> + lists:foreach( + fun({JID, Affiliation}) -> + node_call(Type, set_affiliation, [NodeId, JID, Affiliation]) + end, Entities), + {result, []}; + _ -> + {error, ?ERR_FORBIDDEN} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end + end. + +get_options(Host, Node, JID, SubID, Lang) -> + Action = fun(#pubsub_node{type = Type, id = NodeID}) -> + case lists:member("subscription-options", features(Type)) of + true -> + get_options_helper(JID, Lang, Node, NodeID, SubID, Type); + false -> + {error, extended_error( + ?ERR_FEATURE_NOT_IMPLEMENTED, + unsupported, "subscription-options")} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_Node, XForm}} -> {result, [XForm]}; + Error -> Error + end. + +get_options_helper(JID, Lang, Node, NodeID, SubID, Type) -> + Subscriber = case jlib:string_to_jid(JID) of + error -> {"", "", ""}; + J -> jlib:jid_tolower(J) + end, + {result, Subs} = node_call(Type, get_subscriptions, + [NodeID, Subscriber]), + SubIDs = lists:foldl(fun({subscribed, SID}, Acc) -> + [SID | Acc]; + (_, Acc) -> + Acc + end, [], Subs), + case {SubID, SubIDs} of + {_, []} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "not-subscribed")}; + {[], [SID]} -> + read_sub(Subscriber, Node, NodeID, SID, Lang); + {[], _} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "subid-required")}; + {_, _} -> + read_sub(Subscriber, Node, NodeID, SubID, Lang) + end. + +read_sub(Subscriber, Node, NodeID, SubID, Lang) -> + case pubsub_subscription_odbc:get_subscription(Subscriber, NodeID, SubID) of + {error, notfound} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + {result, #pubsub_subscription{options = Options}} -> + {result, XdataEl} = pubsub_subscription_odbc:get_options_xform(Lang, Options), + OptionsEl = {xmlelement, "options", [{"jid", jlib:jid_to_string(Subscriber)}, + {"subid", SubID}|nodeAttr(Node)], + [XdataEl]}, + PubsubEl = {xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], [OptionsEl]}, + {result, PubsubEl} + end. + +set_options(Host, Node, JID, SubID, Configuration) -> + Action = fun(#pubsub_node{type = Type, id = NodeID}) -> + case lists:member("subscription-options", features(Type)) of + true -> + set_options_helper(Configuration, JID, NodeID, + SubID, Type); + false -> + {error, extended_error( + ?ERR_FEATURE_NOT_IMPLEMENTED, + unsupported, "subscription-options")} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_Node, Result}} -> {result, Result}; + Error -> Error + end. + +set_options_helper(Configuration, JID, NodeID, SubID, Type) -> + SubOpts = case pubsub_subscription_odbc:parse_options_xform(Configuration) of + {result, GoodSubOpts} -> GoodSubOpts; + _ -> invalid + end, + Subscriber = case jlib:string_to_jid(JID) of + error -> {"", "", ""}; + J -> jlib:jid_tolower(J) + end, + {result, Subs} = node_call(Type, get_subscriptions, + [NodeID, Subscriber]), + SubIDs = lists:foldl(fun({subscribed, SID}, Acc) -> + [SID | Acc]; + (_, Acc) -> + Acc + end, [], Subs), + case {SubID, SubIDs} of + {_, []} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "not-subscribed")}; + {[], [SID]} -> + write_sub(Subscriber, NodeID, SID, SubOpts); + {[], _} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "subid-required")}; + {_, _} -> + write_sub(Subscriber, NodeID, SubID, SubOpts) + end. + +write_sub(_Subscriber, _NodeID, _SubID, invalid) -> + {error, extended_error(?ERR_BAD_REQUEST, "invalid-options")}; +write_sub(Subscriber, NodeID, SubID, Options) -> + case pubsub_subscription_odbc:set_subscription(Subscriber, NodeID, SubID, Options) of + {error, notfound} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + {result, _} -> + {result, []} + end. + +%% @spec (Host, Node, JID, Plugins) -> {error, Reason} | {result, Response} +%% Host = host() +%% Node = pubsubNode() +%% JID = jid() +%% Plugins = [Plugin::string()] +%% Reason = stanzaError() +%% Response = [pubsubIQResponse()] +%% @doc <p>Return the list of subscriptions as an XMPP response.</p> +get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> + Result = lists:foldl( + fun(Type, {Status, Acc}) -> + Features = features(Type), + RetrieveFeature = lists:member("retrieve-subscriptions", Features), + if + not RetrieveFeature -> + %% Service does not support retreive subscriptions + {{error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "retrieve-subscriptions")}, Acc}; + true -> + Subscriber = jlib:jid_remove_resource(JID), + {result, Subscriptions} = node_action(Host, Type, get_entity_subscriptions, [Host, Subscriber]), + {Status, [Subscriptions|Acc]} + end + end, {ok, []}, Plugins), + case Result of + {ok, Subscriptions} -> + Entities = lists:flatmap( + fun({_, none}) -> + []; + ({#pubsub_node{nodeid = {_, SubsNode}}, Subscription}) -> + case Node of + <<>> -> + [{xmlelement, "subscription", + [{"subscription", subscription_to_string(Subscription)}|nodeAttr(SubsNode)], + []}]; + SubsNode -> + [{xmlelement, "subscription", + [{"subscription", subscription_to_string(Subscription)}], + []}]; + _ -> + [] + end; + ({_, none, _}) -> + []; + ({#pubsub_node{nodeid = {_, SubsNode}}, Subscription, SubID, SubJID}) -> + case Node of + <<>> -> + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(SubJID)}, + {"subid", SubID}, + {"subscription", subscription_to_string(Subscription)}|nodeAttr(SubsNode)], + []}]; + SubsNode -> + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(SubJID)}, + {"subid", SubID}, + {"subscription", subscription_to_string(Subscription)}], + []}]; + _ -> + [] + end; + ({#pubsub_node{nodeid = {_, SubsNode}}, Subscription, SubJID}) -> + case Node of + <<>> -> + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(SubJID)}, + {"subscription", subscription_to_string(Subscription)}|nodeAttr(SubsNode)], + []}]; + SubsNode -> + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(SubJID)}, + {"subscription", subscription_to_string(Subscription)}], + []}]; + _ -> + [] + end + end, lists:usort(lists:flatten(Subscriptions))), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "subscriptions", [], + Entities}]}]}; + {Error, _} -> + Error + end. +get_subscriptions(Host, Node, JID) -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + Features = features(Type), + RetrieveFeature = lists:member("manage-subscriptions", Features), + {result, Affiliation} = node_call(Type, get_affiliation, [NodeId, JID]), + if + not RetrieveFeature -> + %% Service does not support manage subscriptions + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "manage-subscriptions")}; + Affiliation /= owner -> + %% Entity is not an owner + {error, ?ERR_FORBIDDEN}; + true -> + node_call(Type, get_node_subscriptions, [NodeId]) + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Subscriptions}} -> + Entities = lists:flatmap( + fun({_, none}) -> []; + ({_, pending, _}) -> []; + ({AJID, Subscription}) -> + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(AJID)}, + {"subscription", subscription_to_string(Subscription)}], + []}]; + ({AJID, Subscription, SubId}) -> + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(AJID)}, + {"subscription", subscription_to_string(Subscription)}, + {"subid", SubId}], + []}] + end, Subscriptions), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_OWNER}], + [{xmlelement, "subscriptions", nodeAttr(Node), + Entities}]}]}; + Error -> + Error + end. + +set_subscriptions(Host, Node, From, EntitiesEls) -> + Owner = jlib:jid_tolower(jlib:jid_remove_resource(From)), + Entities = + lists:foldl( + fun(El, Acc) -> + case Acc of + error -> + error; + _ -> + case El of + {xmlelement, "subscription", Attrs, _} -> + JID = jlib:string_to_jid( + xml:get_attr_s("jid", Attrs)), + Subscription = string_to_subscription( + xml:get_attr_s("subscription", Attrs)), + SubId = xml:get_attr_s("subid", Attrs), + if + (JID == error) or + (Subscription == false) -> + error; + true -> + [{jlib:jid_tolower(JID), Subscription, SubId} | Acc] + end + end + end + end, [], EntitiesEls), + case Entities of + error -> + {error, ?ERR_BAD_REQUEST}; + _ -> + Notify = fun(JID, Sub, _SubId) -> + Stanza = {xmlelement, "message", [], + [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(JID)}, + %{"subid", SubId}, + {"subscription", subscription_to_string(Sub)} | nodeAttr(Node)], []}]}]}, + ejabberd_router ! {route, service_jid(Host), jlib:make_jid(JID), Stanza} + end, + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + case lists:member(Owner, node_owners_call(Type, NodeId)) of + true -> + Result = lists:foldl(fun({JID, Subscription, SubId}, Acc) -> + + case node_call(Type, set_subscriptions, [NodeId, JID, Subscription, SubId]) of + {error, Err} -> [{error, Err} | Acc]; + _ -> Notify(JID, Subscription, SubId), Acc + end + end, [], Entities), + case Result of + [] -> {result, []}; + _ -> {error, ?ERR_NOT_ACCEPTABLE} + end; + _ -> + {error, ?ERR_FORBIDDEN} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end + end. + +%% @spec (OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, SubscriberResource}, AllowedGroups) +%% -> {PresenceSubscription, RosterGroup} +get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, AllowedGroups) -> + {Subscription, Groups} = + ejabberd_hooks:run_fold( + roster_get_jid_info, OwnerServer, + {none, []}, + [OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, ""}]), + PresenceSubscription = (Subscription == both) orelse (Subscription == from) + orelse ({OwnerUser, OwnerServer} == {SubscriberUser, SubscriberServer}), + RosterGroup = lists:any(fun(Group) -> + lists:member(Group, AllowedGroups) + end, Groups), + {PresenceSubscription, RosterGroup}. + +%% @spec (AffiliationStr) -> Affiliation +%% AffiliationStr = string() +%% Affiliation = atom() +%% @doc <p>Convert an affiliation type from string to atom.</p> +string_to_affiliation("owner") -> owner; +string_to_affiliation("publisher") -> publisher; +string_to_affiliation("member") -> member; +string_to_affiliation("outcast") -> outcast; +string_to_affiliation("none") -> none; +string_to_affiliation(_) -> false. + +%% @spec (SubscriptionStr) -> Subscription +%% SubscriptionStr = string() +%% Subscription = atom() +%% @doc <p>Convert a subscription type from string to atom.</p> +string_to_subscription("subscribed") -> subscribed; +string_to_subscription("pending") -> pending; +string_to_subscription("unconfigured") -> unconfigured; +string_to_subscription("none") -> none; +string_to_subscription(_) -> false. + +%% @spec (Affiliation) -> AffiliationStr +%% Affiliation = atom() +%% AffiliationStr = string() +%% @doc <p>Convert an affiliation type from atom to string.</p> +affiliation_to_string(owner) -> "owner"; +affiliation_to_string(publisher) -> "publisher"; +affiliation_to_string(member) -> "member"; +affiliation_to_string(outcast) -> "outcast"; +affiliation_to_string(_) -> "none". + +%% @spec (Subscription) -> SubscriptionStr +%% Subscription = atom() +%% SubscriptionStr = string() +%% @doc <p>Convert a subscription type from atom to string.</p> +subscription_to_string(subscribed) -> "subscribed"; +subscription_to_string(pending) -> "pending"; +subscription_to_string(unconfigured) -> "unconfigured"; +subscription_to_string(_) -> "none". + +%% @spec (Node) -> NodeStr +%% Node = pubsubNode() +%% NodeStr = string() +%% @doc <p>Convert a node type from pubsubNode to string.</p> +node_to_string(Node) -> binary_to_list(Node). +string_to_node(SNode) -> list_to_binary(SNode). + +%% @spec (Host) -> jid() +%% Host = host() +%% @doc <p>Generate pubsub service JID.</p> +service_jid(Host) -> + case Host of + {U,S,_} -> {jid, U, S, "", U, S, ""}; + _ -> {jid, "", Host, "", "", Host, ""} + end. + +%% @spec (LJID, NotifyType, Depth, NodeOptions, SubOptions) -> boolean() +%% LJID = jid() +%% NotifyType = items | nodes +%% Depth = integer() +%% NodeOptions = [{atom(), term()}] +%% SubOptions = [{atom(), term()}] +%% @doc <p>Check if a notification must be delivered or not based on +%% node and subscription options.</p> +is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) -> + sub_to_deliver(LJID, NotifyType, Depth, SubOptions) + andalso node_to_deliver(LJID, NodeOptions). + +sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) -> + lists:all(fun (Option) -> + sub_option_can_deliver(NotifyType, Depth, Option) + end, SubOptions). + +sub_option_can_deliver(items, _, {subscription_type, nodes}) -> false; +sub_option_can_deliver(nodes, _, {subscription_type, items}) -> false; +sub_option_can_deliver(_, _, {subscription_depth, all}) -> true; +sub_option_can_deliver(_, Depth, {subscription_depth, D}) -> Depth =< D; +sub_option_can_deliver(_, _, {deliver, false}) -> false; +sub_option_can_deliver(_, _, {expire, When}) -> now() < When; +sub_option_can_deliver(_, _, _) -> true. + +node_to_deliver(LJID, NodeOptions) -> + PresenceDelivery = get_option(NodeOptions, presence_based_delivery), + presence_can_deliver(LJID, PresenceDelivery). + +presence_can_deliver(_, false) -> true; +presence_can_deliver({User, Server, _}, true) -> + case mnesia:dirty_match_object({session, '_', '_', {User, Server}, '_', '_'}) of + [] -> false; + Ss -> + lists:foldl(fun({session, _, _, _, undefined, _}, Acc) -> Acc; + ({session, _, _, _, _Priority, _}, _Acc) -> true + end, false, Ss) + end. + +%% @spec (Payload) -> int() +%% Payload = term() +%% @doc <p>Count occurence of XML elements in payload.</p> +payload_xmlelements(Payload) -> payload_xmlelements(Payload, 0). +payload_xmlelements([], Count) -> Count; +payload_xmlelements([{xmlelement, _, _, _}|Tail], Count) -> payload_xmlelements(Tail, Count+1); +payload_xmlelements([_|Tail], Count) -> payload_xmlelements(Tail, Count). + +%% @spec (Els) -> stanza() +%% Els = [xmlelement()] +%% @doc <p>Build pubsub event stanza</p> +event_stanza(Els) -> + event_stanza_withmoreels(Els, []). + +event_stanza_with_delay(Els, ModifNow, ModifLjid) -> + DateTime = calendar:now_to_datetime(ModifNow), + MoreEls = [jlib:timestamp_to_xml(DateTime, utc, ModifLjid, "")], + event_stanza_withmoreels(Els, MoreEls). + +event_stanza_withmoreels(Els, MoreEls) -> + {xmlelement, "message", [], + [{xmlelement, "event", [{"xmlns", ?NS_PUBSUB_EVENT}], Els} | MoreEls]}. + +%%%%%% broadcast functions + +broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, Removed, ItemId, _From, Payload) -> + %broadcast(Host, Node, NodeId, NodeOptions, none, true, "items", ItemEls) + case get_collection_subscriptions(Host, Node) of + SubsByDepth when is_list(SubsByDepth) -> + Content = case get_option(NodeOptions, deliver_payloads) of + true -> Payload; + false -> [] + end, + Stanza = event_stanza( + [{xmlelement, "items", nodeAttr(Node), + [{xmlelement, "item", itemAttr(ItemId), Content}]}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, items, Stanza), + case Removed of + [] -> + ok; + _ -> + case get_option(NodeOptions, notify_retract) of + true -> + RetractStanza = event_stanza( + [{xmlelement, "items", nodeAttr(Node), + [{xmlelement, "retract", itemAttr(RId), []} || RId <- Removed]}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, + items, RetractStanza); + _ -> + ok + end + end, + {result, true}; + _ -> + {result, false} + end. + +broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds) -> + broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, false). +broadcast_retract_items(_Host, _Node, _NodeId, _Type, _NodeOptions, [], _ForceNotify) -> + {result, false}; +broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, ForceNotify) -> + %broadcast(Host, Node, NodeId, NodeOptions, notify_retract, ForceNotify, "retract", RetractEls) + case (get_option(NodeOptions, notify_retract) or ForceNotify) of + true -> + case get_collection_subscriptions(Host, Node) of + SubsByDepth when is_list(SubsByDepth) -> + Stanza = event_stanza( + [{xmlelement, "items", nodeAttr(Node), + [{xmlelement, "retract", itemAttr(ItemId), []} || ItemId <- ItemIds]}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, items, Stanza), + {result, true}; + _ -> + {result, false} + end; + _ -> + {result, false} + end. + +broadcast_purge_node(Host, Node, NodeId, Type, NodeOptions) -> + %broadcast(Host, Node, NodeId, NodeOptions, notify_retract, false, "purge", []) + case get_option(NodeOptions, notify_retract) of + true -> + case get_collection_subscriptions(Host, Node) of + SubsByDepth when is_list(SubsByDepth) -> + Stanza = event_stanza( + [{xmlelement, "purge", nodeAttr(Node), + []}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, nodes, Stanza), + {result, true}; + _ -> + {result, false} + end; + _ -> + {result, false} + end. + +broadcast_removed_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth) -> + %broadcast(Host, Node, NodeId, NodeOptions, notify_delete, false, "delete", []) + case get_option(NodeOptions, notify_delete) of + true -> + case SubsByDepth of + [] -> + {result, false}; + _ -> + Stanza = event_stanza( + [{xmlelement, "delete", nodeAttr(Node), + []}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, nodes, Stanza), + {result, true} + end; + _ -> + {result, false} + end. + +broadcast_config_notification(Host, Node, NodeId, Type, NodeOptions, Lang) -> + %broadcast(Host, Node, NodeId, NodeOptions, notify_config, false, "items", ConfigEls) + case get_option(NodeOptions, notify_config) of + true -> + case get_collection_subscriptions(Host, Node) of + SubsByDepth when is_list(SubsByDepth) -> + Content = case get_option(NodeOptions, deliver_payloads) of + true -> + [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}], + get_configure_xfields(Type, NodeOptions, Lang, [])}]; + false -> + [] + end, + Stanza = event_stanza( + [{xmlelement, "configuration", nodeAttr(Node), Content}]), + broadcast_stanza(Host, Node, NodeId, Type, + NodeOptions, SubsByDepth, nodes, Stanza), + {result, true}; + _ -> + {result, false} + end; + _ -> + {result, false} + end. + +get_collection_subscriptions(Host, Node) -> + Action = fun() -> + {result, lists:map(fun({Depth, Nodes}) -> + {Depth, [{N, get_node_subs(N)} || N <- Nodes]} + end, tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]))} + end, + case transaction(Host, Action, sync_dirty) of + {result, CollSubs} -> CollSubs; + _ -> [] + end. + +get_node_subs(#pubsub_node{type = Type, + id = NodeID}) -> + case node_call(Type, get_node_subscriptions, [NodeID]) of + {result, Subs} -> get_options_for_subs(NodeID, Subs); + Other -> Other + end. + +get_options_for_subs(NodeID, Subs) -> + lists:foldl(fun({JID, subscribed, SubID}, Acc) -> + case pubsub_subscription_odbc:get_subscription(JID, NodeID, SubID) of + {error, notfound} -> [{JID, SubID, []} | Acc]; + {result, #pubsub_subscription{options = Options}} -> [{JID, SubID, Options} | Acc]; + _ -> Acc + end; + (_, Acc) -> + Acc + end, [], Subs). + +% TODO: merge broadcast code that way +%broadcast(Host, Node, NodeId, Type, NodeOptions, Feature, Force, ElName, SubEls) -> +% case (get_option(NodeOptions, Feature) or Force) of +% true -> +% case node_action(Host, Type, get_node_subscriptions, [NodeId]) of +% {result, []} -> +% {result, false}; +% {result, Subs} -> +% Stanza = event_stanza([{xmlelement, ElName, nodeAttr(Node), SubEls}]), +% broadcast_stanza(Host, Node, Type, NodeOptions, SubOpts, Stanza), +% {result, true}; +% _ -> +% {result, false} +% end; +% _ -> +% {result, false} +% end + +broadcast_stanza(Host, Node, _NodeId, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza) -> + NotificationType = get_option(NodeOptions, notification_type, headline), + BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but usefull + From = service_jid(Host), + Stanza = case NotificationType of + normal -> BaseStanza; + MsgType -> add_message_type(BaseStanza, atom_to_list(MsgType)) + end, + %% Handles explicit subscriptions + NodesByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth), + lists:foreach(fun ({LJID, Nodes}) -> + LJIDs = case BroadcastAll of + true -> + {U, S, _} = LJID, + [{U, S, R} || R <- user_resources(U, S)]; + false -> + [LJID] + end, + SHIMStanza = add_headers(Stanza, collection_shim(Node, Nodes)), + lists:foreach(fun(To) -> + ejabberd_router ! {route, From, jlib:make_jid(To), SHIMStanza} + end, LJIDs) + end, NodesByJID), + %% Handles implicit presence subscriptions + case Host of + {LUser, LServer, LResource} -> + SenderResource = case LResource of + [] -> + case user_resources(LUser, LServer) of + [Resource|_] -> Resource; + _ -> "" + end; + _ -> + LResource + end, + case ejabberd_sm:get_session_pid(LUser, LServer, SenderResource) of + C2SPid when is_pid(C2SPid) -> + %% set the from address on the notification to the bare JID of the account owner + %% Also, add "replyto" if entity has presence subscription to the account owner + %% See XEP-0163 1.1 section 4.3.1 + Sender = jlib:make_jid(LUser, LServer, ""), + %%ReplyTo = jlib:make_jid(LUser, LServer, SenderResource), % This has to be used + case catch ejabberd_c2s:get_subscribed(C2SPid) of + Contacts when is_list(Contacts) -> + lists:foreach(fun({U, S, _}) -> + spawn(fun() -> + LJIDs = lists:foldl(fun(R, Acc) -> + LJID = {U, S, R}, + case is_caps_notify(LServer, Node, LJID) of + true -> [LJID | Acc]; + false -> Acc + end + end, [], user_resources(U, S)), + lists:foreach(fun(To) -> + ejabberd_router ! {route, Sender, jlib:make_jid(To), Stanza} + end, LJIDs) + end) + end, Contacts); + _ -> + ok + end, + ok; + _ -> + ?DEBUG("~p@~p has no session; can't deliver ~p to contacts", [LUser, LServer, Stanza]), + ok + end; + _ -> + ok + end. + +subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> + NodesToDeliver = fun(Depth, Node, Subs, Acc) -> + NodeId = case Node#pubsub_node.nodeid of + {_, N} -> N; + Other -> Other + end, + NodeOptions = Node#pubsub_node.options, + lists:foldl(fun({LJID, _SubID, SubOptions}, Acc2) -> + case is_to_deliver(LJID, NotifyType, Depth, + NodeOptions, SubOptions) of + true -> [{LJID, NodeId}|Acc2]; + false -> Acc2 + end + end, Acc, Subs) + end, + DepthsToDeliver = fun({Depth, SubsByNode}, Acc) -> + lists:foldl(fun({Node, Subs}, Acc2) -> + NodesToDeliver(Depth, Node, Subs, Acc2) + end, Acc, SubsByNode) + end, + JIDSubs = lists:foldl(DepthsToDeliver, [], SubsByDepth), + [{LJID, proplists:append_values(LJID, JIDSubs)} || LJID <- proplists:get_keys(JIDSubs)]. + +%% If we don't know the resource, just pick first if any +%% If no resource available, check if caps anyway (remote online) +user_resources(User, Server) -> + case ejabberd_sm:get_user_resources(User, Server) of + [] -> mod_caps:get_user_resources(User, Server); + Rs -> Rs + end. + +is_caps_notify(Host, Node, LJID) -> + case mod_caps:get_caps(LJID) of + nothing -> + false; + Caps -> + case catch mod_caps:get_features(Host, Caps) of + Features when is_list(Features) -> lists:member(node_to_string(Node) ++ "+notify", Features); + _ -> false + end + end. + +%%%%%%% Configuration handling + +%%<p>There are several reasons why the default node configuration options request might fail:</p> +%%<ul> +%%<li>The service does not support node configuration.</li> +%%<li>The service does not support retrieval of default node configuration.</li> +%%</ul> +get_configure(Host, ServerHost, Node, From, Lang) -> + Action = + fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> + case node_call(Type, get_affiliation, [NodeId, From]) of + {result, owner} -> + Groups = ejabberd_hooks:run_fold(roster_groups, ServerHost, [], [ServerHost]), + {result, + [{xmlelement, "pubsub", + [{"xmlns", ?NS_PUBSUB_OWNER}], + [{xmlelement, "configure", nodeAttr(Node), + [{xmlelement, "x", + [{"xmlns", ?NS_XDATA}, {"type", "form"}], + get_configure_xfields(Type, Options, Lang, Groups) + }]}]}]}; + _ -> + {error, ?ERR_FORBIDDEN} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end. + +get_default(Host, Node, _From, Lang) -> + Type = select_type(Host, Host, Node), + Options = node_options(Type), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_OWNER}], + [{xmlelement, "default", [], + [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], + get_configure_xfields(Type, Options, Lang, []) + }]}]}]}. + +%% Get node option +%% The result depend of the node type plugin system. +get_option([], _) -> false; +get_option(Options, Var) -> + get_option(Options, Var, false). +get_option(Options, Var, Def) -> + case lists:keysearch(Var, 1, Options) of + {value, {_Val, Ret}} -> Ret; + _ -> Def + end. + +%% Get default options from the module plugin. +node_options(Type) -> + Module = list_to_atom(?PLUGIN_PREFIX ++ Type), + case catch Module:options() of + {'EXIT',{undef,_}} -> + DefaultModule = list_to_atom(?PLUGIN_PREFIX++?STDNODE), + DefaultModule:options(); + Result -> + Result + end. + +%% @spec (Host, Type, NodeId) -> [ljid()] +%% NodeId = pubsubNodeId() +%% @doc <p>Return list of node owners.</p> +node_owners(Host, Type, NodeId) -> + case node_action(Host, Type, get_node_affiliations, [NodeId]) of + {result, Affiliations} -> + lists:foldl( + fun({LJID, owner}, Acc) -> [LJID|Acc]; + (_, Acc) -> Acc + end, [], Affiliations); + _ -> + [] + end. +node_owners_call(Type, NodeId) -> + case node_call(Type, get_node_affiliations, [NodeId]) of + {result, Affiliations} -> + lists:foldl( + fun({LJID, owner}, Acc) -> [LJID|Acc]; + (_, Acc) -> Acc + end, [], Affiliations); + _ -> + [] + end. + +%% @spec (Host, Options) -> MaxItems +%% Host = host() +%% Options = [Option] +%% Option = {Key::atom(), Value::term()} +%% MaxItems = integer() | unlimited +%% @doc <p>Return the maximum number of items for a given node.</p> +%% <p>Unlimited means that there is no limit in the number of items that can +%% be stored.</p> +%% @todo In practice, the current data structure means that we cannot manage +%% millions of items on a given node. This should be addressed in a new +%% version. +max_items(Host, Options) -> + case get_option(Options, persist_items) of + true -> + case get_option(Options, max_items) of + false -> unlimited; + Result when (Result < 0) -> 0; + Result -> Result + end; + false -> + case get_option(Options, send_last_published_item) of + never -> + 0; + _ -> + case is_last_item_cache_enabled(Host) of + true -> 0; + false -> 1 + end + end + end. + +-define(BOOL_CONFIG_FIELD(Label, Var), + ?BOOLXFIELD(Label, "pubsub#" ++ atom_to_list(Var), + get_option(Options, Var))). + +-define(STRING_CONFIG_FIELD(Label, Var), + ?STRINGXFIELD(Label, "pubsub#" ++ atom_to_list(Var), + get_option(Options, Var, ""))). + +-define(INTEGER_CONFIG_FIELD(Label, Var), + ?STRINGXFIELD(Label, "pubsub#" ++ atom_to_list(Var), + integer_to_list(get_option(Options, Var)))). + +-define(JLIST_CONFIG_FIELD(Label, Var, Opts), + ?LISTXFIELD(Label, "pubsub#" ++ atom_to_list(Var), + jlib:jid_to_string(get_option(Options, Var)), + [jlib:jid_to_string(O) || O <- Opts])). + +-define(ALIST_CONFIG_FIELD(Label, Var, Opts), + ?LISTXFIELD(Label, "pubsub#" ++ atom_to_list(Var), + atom_to_list(get_option(Options, Var)), + [atom_to_list(O) || O <- Opts])). + +-define(LISTM_CONFIG_FIELD(Label, Var, Opts), + ?LISTMXFIELD(Label, "pubsub#" ++ atom_to_list(Var), + get_option(Options, Var), Opts)). + +-define(NLIST_CONFIG_FIELD(Label, Var), + ?STRINGMXFIELD(Label, "pubsub#" ++ atom_to_list(Var), + [node_to_string(N) || N <- get_option(Options, Var, [])])). + +get_configure_xfields(_Type, Options, Lang, Groups) -> + [?XFIELD("hidden", "", "FORM_TYPE", ?NS_PUBSUB_NODE_CONFIG), + ?BOOL_CONFIG_FIELD("Deliver payloads with event notifications", deliver_payloads), + ?BOOL_CONFIG_FIELD("Deliver event notifications", deliver_notifications), + ?BOOL_CONFIG_FIELD("Notify subscribers when the node configuration changes", notify_config), + ?BOOL_CONFIG_FIELD("Notify subscribers when the node is deleted", notify_delete), + ?BOOL_CONFIG_FIELD("Notify subscribers when items are removed from the node", notify_retract), + ?BOOL_CONFIG_FIELD("Persist items to storage", persist_items), + ?STRING_CONFIG_FIELD("A friendly name for the node", title), + ?INTEGER_CONFIG_FIELD("Max # of items to persist", max_items), + ?BOOL_CONFIG_FIELD("Whether to allow subscriptions", subscribe), + ?ALIST_CONFIG_FIELD("Specify the access model", access_model, + [open, authorize, presence, roster, whitelist]), + %% XXX: change to list-multi, include current roster groups as options + ?LISTM_CONFIG_FIELD("Roster groups allowed to subscribe", roster_groups_allowed, Groups), + ?ALIST_CONFIG_FIELD("Specify the publisher model", publish_model, + [publishers, subscribers, open]), + ?ALIST_CONFIG_FIELD("Specify the event message type", notification_type, + [headline, normal]), + ?INTEGER_CONFIG_FIELD("Max payload size in bytes", max_payload_size), + ?ALIST_CONFIG_FIELD("When to send the last published item", send_last_published_item, + [never, on_sub, on_sub_and_presence]), + ?BOOL_CONFIG_FIELD("Only deliver notifications to available users", presence_based_delivery), + ?NLIST_CONFIG_FIELD("The collections with which a node is affiliated", collection) + ]. + +%%<p>There are several reasons why the node configuration request might fail:</p> +%%<ul> +%%<li>The service does not support node configuration.</li> +%%<li>The requesting entity does not have sufficient privileges to configure the node.</li> +%%<li>The request did not specify a node.</li> +%%<li>The node has no configuration options.</li> +%%<li>The specified node does not exist.</li> +%%</ul> +set_configure(Host, Node, From, Els, Lang) -> + case xml:remove_cdata(Els) of + [{xmlelement, "x", _Attrs1, _Els1} = XEl] -> + case {xml:get_tag_attr_s("xmlns", XEl), xml:get_tag_attr_s("type", XEl)} of + {?NS_XDATA, "cancel"} -> + {result, []}; + {?NS_XDATA, "submit"} -> + Action = + fun(#pubsub_node{options = Options, type = Type, id = NodeId} = N) -> + case node_call(Type, get_affiliation, [NodeId, From]) of + {result, owner} -> + case jlib:parse_xdata_submit(XEl) of + invalid -> + {error, ?ERR_BAD_REQUEST}; + XData -> + OldOpts = case Options of + [] -> node_options(Type); + _ -> Options + end, + case set_xoption(Host, XData, OldOpts) of + NewOpts when is_list(NewOpts) -> + case tree_call(Host, set_node, [N#pubsub_node{options = NewOpts}]) of + ok -> {result, ok}; + Err -> Err + end; + Err -> + Err + end + end; + _ -> + {error, ?ERR_FORBIDDEN} + end + end, + case transaction(Host, Node, Action, transaction) of + {result, {TNode, ok}} -> + NodeId = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_config_notification(Host, Node, NodeId, Type, Options, Lang), + {result, []}; + Other -> + Other + end; + _ -> + {error, ?ERR_BAD_REQUEST} + end; + _ -> + {error, ?ERR_BAD_REQUEST} + end. + +add_opt(Key, Value, Opts) -> + Opts1 = lists:keydelete(Key, 1, Opts), + [{Key, Value} | Opts1]. + +-define(SET_BOOL_XOPT(Opt, Val), + BoolVal = case Val of + "0" -> false; + "1" -> true; + "false" -> false; + "true" -> true; + _ -> error + end, + case BoolVal of + error -> {error, ?ERR_NOT_ACCEPTABLE}; + _ -> set_xoption(Host, Opts, add_opt(Opt, BoolVal, NewOpts)) + end). + +-define(SET_STRING_XOPT(Opt, Val), + set_xoption(Host, Opts, add_opt(Opt, Val, NewOpts))). + +-define(SET_INTEGER_XOPT(Opt, Val, Min, Max), + case catch list_to_integer(Val) of + IVal when is_integer(IVal), + IVal >= Min, + IVal =< Max -> + set_xoption(Host, Opts, add_opt(Opt, IVal, NewOpts)); + _ -> + {error, ?ERR_NOT_ACCEPTABLE} + end). + +-define(SET_ALIST_XOPT(Opt, Val, Vals), + case lists:member(Val, [atom_to_list(V) || V <- Vals]) of + true -> set_xoption(Host, Opts, add_opt(Opt, list_to_atom(Val), NewOpts)); + false -> {error, ?ERR_NOT_ACCEPTABLE} + end). + +-define(SET_LIST_XOPT(Opt, Val), + set_xoption(Host, Opts, add_opt(Opt, Val, NewOpts))). + +set_xoption(_Host, [], NewOpts) -> + NewOpts; +set_xoption(Host, [{"FORM_TYPE", _} | Opts], NewOpts) -> + set_xoption(Host, Opts, NewOpts); +set_xoption(Host, [{"pubsub#roster_groups_allowed", Value} | Opts], NewOpts) -> + ?SET_LIST_XOPT(roster_groups_allowed, Value); +set_xoption(Host, [{"pubsub#deliver_payloads", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(deliver_payloads, Val); +set_xoption(Host, [{"pubsub#deliver_notifications", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(deliver_notifications, Val); +set_xoption(Host, [{"pubsub#notify_config", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(notify_config, Val); +set_xoption(Host, [{"pubsub#notify_delete", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(notify_delete, Val); +set_xoption(Host, [{"pubsub#notify_retract", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(notify_retract, Val); +set_xoption(Host, [{"pubsub#persist_items", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(persist_items, Val); +set_xoption(Host, [{"pubsub#max_items", [Val]} | Opts], NewOpts) -> + MaxItems = get_max_items_node(Host), + ?SET_INTEGER_XOPT(max_items, Val, 0, MaxItems); +set_xoption(Host, [{"pubsub#subscribe", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(subscribe, Val); +set_xoption(Host, [{"pubsub#access_model", [Val]} | Opts], NewOpts) -> + ?SET_ALIST_XOPT(access_model, Val, [open, authorize, presence, roster, whitelist]); +set_xoption(Host, [{"pubsub#publish_model", [Val]} | Opts], NewOpts) -> + ?SET_ALIST_XOPT(publish_model, Val, [publishers, subscribers, open]); +set_xoption(Host, [{"pubsub#notification_type", [Val]} | Opts], NewOpts) -> + ?SET_ALIST_XOPT(notification_type, Val, [headline, normal]); +set_xoption(Host, [{"pubsub#node_type", [Val]} | Opts], NewOpts) -> + ?SET_ALIST_XOPT(node_type, Val, [leaf, collection]); +set_xoption(Host, [{"pubsub#max_payload_size", [Val]} | Opts], NewOpts) -> + ?SET_INTEGER_XOPT(max_payload_size, Val, 0, ?MAX_PAYLOAD_SIZE); +set_xoption(Host, [{"pubsub#send_last_published_item", [Val]} | Opts], NewOpts) -> + ?SET_ALIST_XOPT(send_last_published_item, Val, [never, on_sub, on_sub_and_presence]); +set_xoption(Host, [{"pubsub#presence_based_delivery", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(presence_based_delivery, Val); +set_xoption(Host, [{"pubsub#title", Value} | Opts], NewOpts) -> + ?SET_STRING_XOPT(title, Value); +set_xoption(Host, [{"pubsub#type", Value} | Opts], NewOpts) -> + ?SET_STRING_XOPT(type, Value); +set_xoption(Host, [{"pubsub#body_xslt", Value} | Opts], NewOpts) -> + ?SET_STRING_XOPT(body_xslt, Value); +set_xoption(Host, [{"pubsub#collection", Value} | Opts], NewOpts) -> + NewValue = [string_to_node(V) || V <- Value], + ?SET_LIST_XOPT(collection, NewValue); +set_xoption(Host, [{"pubsub#node", [Value]} | Opts], NewOpts) -> + NewValue = string_to_node(Value), + ?SET_LIST_XOPT(node, NewValue); +set_xoption(Host, [_ | Opts], NewOpts) -> + % skip unknown field + set_xoption(Host, Opts, NewOpts). + +get_max_items_node({_, ServerHost, _}) -> + get_max_items_node(ServerHost); +get_max_items_node(Host) -> + case catch ets:lookup(gen_mod:get_module_proc(Host, config), max_items_node) of + [{max_items_node, Integer}] -> Integer; + _ -> ?MAXITEMS + end. + +%%%% last item cache handling + +is_last_item_cache_enabled({_, ServerHost, _}) -> + is_last_item_cache_enabled(ServerHost); +is_last_item_cache_enabled(Host) -> + case catch ets:lookup(gen_mod:get_module_proc(Host, config), last_item_cache) of + [{last_item_cache, true}] -> true; + _ -> false + end. + +set_cached_item({_, ServerHost, _}, NodeId, ItemId, Payload) -> + set_cached_item(ServerHost, NodeId, ItemId, Payload); +set_cached_item(Host, NodeId, ItemId, Payload) -> + case is_last_item_cache_enabled(Host) of + true -> ets:insert(gen_mod:get_module_proc(Host, last_items), {NodeId, {ItemId, Payload}}); + _ -> ok + end. +unset_cached_item({_, ServerHost, _}, NodeId) -> + unset_cached_item(ServerHost, NodeId); +unset_cached_item(Host, NodeId) -> + case is_last_item_cache_enabled(Host) of + true -> ets:delete(gen_mod:get_module_proc(Host, last_items), NodeId); + _ -> ok + end. +get_cached_item({_, ServerHost, _}, NodeId) -> + get_cached_item(ServerHost, NodeId); +get_cached_item(Host, NodeId) -> + case is_last_item_cache_enabled(Host) of + true -> + case catch ets:lookup(gen_mod:get_module_proc(Host, last_items), NodeId) of + [{NodeId, {ItemId, Payload}}] -> + #pubsub_item{itemid = {ItemId, NodeId}, payload = Payload}; + _ -> + undefined + end; + _ -> + undefined + end. + +%%%% plugin handling + +plugins(Host) -> + case catch ets:lookup(gen_mod:get_module_proc(Host, config), plugins) of + [{plugins, []}] -> [?STDNODE]; + [{plugins, PL}] -> PL; + _ -> [?STDNODE] + end. +select_type(ServerHost, Host, Node, Type)-> + SelectedType = case Host of + {_User, _Server, _Resource} -> + case catch ets:lookup(gen_mod:get_module_proc(ServerHost, config), pep_mapping) of + [{pep_mapping, PM}] -> proplists:get_value(Node, PM, ?PEPNODE); + _ -> ?PEPNODE + end; + _ -> + Type + end, + ConfiguredTypes = plugins(ServerHost), + case lists:member(SelectedType, ConfiguredTypes) of + true -> SelectedType; + false -> hd(ConfiguredTypes) + end. +select_type(ServerHost, Host, Node) -> + select_type(ServerHost, Host, Node, hd(plugins(ServerHost))). + +features() -> + [ + % see plugin "access-authorize", % OPTIONAL + "access-open", % OPTIONAL this relates to access_model option in node_hometree + "access-presence", % OPTIONAL this relates to access_model option in node_pep + %TODO "access-roster", % OPTIONAL + "access-whitelist", % OPTIONAL + % see plugin "auto-create", % OPTIONAL + % see plugin "auto-subscribe", % RECOMMENDED + "collections", % RECOMMENDED + "config-node", % RECOMMENDED + "create-and-configure", % RECOMMENDED + % see plugin "create-nodes", % RECOMMENDED + % see plugin "delete-items", % RECOMMENDED + % see plugin "delete-nodes", % RECOMMENDED + % see plugin "filtered-notifications", % RECOMMENDED + % see plugin "get-pending", % OPTIONAL + % see plugin "instant-nodes", % RECOMMENDED + "item-ids", % RECOMMENDED + "last-published", % RECOMMENDED + %TODO "cache-last-item", + %TODO "leased-subscription", % OPTIONAL + % see plugin "manage-subscriptions", % OPTIONAL + "member-affiliation", % RECOMMENDED + %TODO "meta-data", % RECOMMENDED + % see plugin "modify-affiliations", % OPTIONAL + % see plugin "multi-collection", % OPTIONAL + % see plugin "multi-subscribe", % OPTIONAL + % see plugin "outcast-affiliation", % RECOMMENDED + % see plugin "persistent-items", % RECOMMENDED + "presence-notifications", % OPTIONAL + "presence-subscribe", % RECOMMENDED + % see plugin "publish", % REQUIRED + %TODO "publish-options", % OPTIONAL + "publisher-affiliation", % RECOMMENDED + % see plugin "purge-nodes", % OPTIONAL + % see plugin "retract-items", % OPTIONAL + % see plugin "retrieve-affiliations", % RECOMMENDED + "retrieve-default" % RECOMMENDED + % see plugin "retrieve-items", % RECOMMENDED + % see plugin "retrieve-subscriptions", % RECOMMENDED + %TODO "shim", % OPTIONAL + % see plugin "subscribe", % REQUIRED + % see plugin "subscription-options", % OPTIONAL + % see plugin "subscription-notifications" % OPTIONAL + ]. +features(Type) -> + Module = list_to_atom(?PLUGIN_PREFIX++Type), + features() ++ case catch Module:features() of + {'EXIT', {undef, _}} -> []; + Result -> Result + end. +features(Host, []) -> + lists:usort(lists:foldl(fun(Plugin, Acc) -> + Acc ++ features(Plugin) + end, [], plugins(Host))); +features(Host, Node) -> + Action = fun(#pubsub_node{type = Type}) -> {result, features(Type)} end, + case transaction(Host, Node, Action, sync_dirty) of + {result, Features} -> lists:usort(features() ++ Features); + _ -> features() + end. + +%% @doc <p>node tree plugin call.</p> +tree_call({_User, Server, _Resource}, Function, Args) -> + tree_call(Server, Function, Args); +tree_call(Host, Function, Args) -> + ?DEBUG("tree_call ~p ~p ~p",[Host, Function, Args]), + Module = case catch ets:lookup(gen_mod:get_module_proc(Host, config), nodetree) of + [{nodetree, N}] -> N; + _ -> list_to_atom(?TREE_PREFIX ++ ?STDTREE) + end, + catch apply(Module, Function, Args). +tree_action(Host, Function, Args) -> + ?DEBUG("tree_action ~p ~p ~p",[Host,Function,Args]), + Fun = fun() -> tree_call(Host, Function, Args) end, + case catch ejabberd_odbc:sql_bloc(odbc_conn(Host), Fun) of + {atomic, Result} -> + Result; + {aborted, Reason} -> + ?ERROR_MSG("transaction return internal error: ~p~n",[{aborted, Reason}]), + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + +%% @doc <p>node plugin call.</p> +node_call(Type, Function, Args) -> + ?DEBUG("node_call ~p ~p ~p",[Type, Function, Args]), + Module = list_to_atom(?PLUGIN_PREFIX++Type), + case catch apply(Module, Function, Args) of + {result, Result} -> {result, Result}; + {error, Error} -> {error, Error}; + {'EXIT', {undef, Undefined}} -> + case Type of + ?STDNODE -> {error, {undef, Undefined}}; + _ -> node_call(?STDNODE, Function, Args) + end; + {'EXIT', Reason} -> {error, Reason}; + Result -> {result, Result} %% any other return value is forced as result + end. + +node_action(Host, Type, Function, Args) -> + ?DEBUG("node_action ~p ~p ~p ~p",[Host,Type,Function,Args]), + transaction(Host, fun() -> + node_call(Type, Function, Args) + end, sync_dirty). + +%% @doc <p>plugin transaction handling.</p> +transaction(Host, Node, Action, Trans) -> + transaction(Host, fun() -> + case tree_call(Host, get_node, [Host, Node]) of + N when is_record(N, pubsub_node) -> + case Action(N) of + {result, Result} -> {result, {N, Result}}; + {atomic, {result, Result}} -> {result, {N, Result}}; + Other -> Other + end; + Error -> + Error + end + end, Trans). + +transaction(Host, Fun, Trans) -> + transaction_retry(Host, Fun, Trans, 2). +transaction_retry(Host, Fun, Trans, Count) -> + SqlFun = case Trans of + transaction -> sql_transaction; + _ -> sql_bloc + end, + case catch ejabberd_odbc:SqlFun(odbc_conn(Host), Fun) of + {result, Result} -> {result, Result}; + {error, Error} -> {error, Error}; + {atomic, {result, Result}} -> {result, Result}; + {atomic, {error, Error}} -> {error, Error}; + {aborted, Reason} -> + ?ERROR_MSG("transaction return internal error: ~p~n", [{aborted, Reason}]), + {error, ?ERR_INTERNAL_SERVER_ERROR}; + {'EXIT', {timeout, _} = Reason} -> + case Count of + 0 -> + ?ERROR_MSG("transaction return internal error: ~p~n", [{'EXIT', Reason}]), + {error, ?ERR_INTERNAL_SERVER_ERROR}; + N -> + erlang:yield(), + transaction_retry(Host, Fun, Trans, N-1) + end; + {'EXIT', Reason} -> + ?ERROR_MSG("transaction return internal error: ~p~n", [{'EXIT', Reason}]), + {error, ?ERR_INTERNAL_SERVER_ERROR}; + Other -> + ?ERROR_MSG("transaction return internal error: ~p~n", [Other]), + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + +odbc_conn({_U, Host, _R})-> + Host; +odbc_conn(Host) -> + Host--"pubsub.". %% TODO, improve that for custom host + +%% escape value for database storage +escape({_U, _H, _R}=JID)-> + ejabberd_odbc:escape(jlib:jid_to_string(JID)); +escape(Value)-> + ejabberd_odbc:escape(Value). + +%%%% helpers + +%% Add pubsub-specific error element +extended_error(Error, Ext) -> + extended_error(Error, Ext, + [{"xmlns", ?NS_PUBSUB_ERRORS}]). +extended_error(Error, unsupported, Feature) -> + extended_error(Error, "unsupported", + [{"xmlns", ?NS_PUBSUB_ERRORS}, + {"feature", Feature}]); +extended_error({xmlelement, Error, Attrs, SubEls}, Ext, ExtAttrs) -> + {xmlelement, Error, Attrs, + lists:reverse([{xmlelement, Ext, ExtAttrs, []} | SubEls])}. + +%% Give a uniq identifier +uniqid() -> + {T1, T2, T3} = now(), + lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). + +% node attributes +nodeAttr(Node) when is_list(Node) -> + [{"node", Node}]; +nodeAttr(Node) -> + [{"node", node_to_string(Node)}]. + +% item attributes +itemAttr([]) -> []; +itemAttr(ItemId) -> [{"id", ItemId}]. + +% build item elements from item list +itemsEls(Items) -> + lists:map(fun(#pubsub_item{itemid = {ItemId, _}, payload = Payload}) -> + {xmlelement, "item", itemAttr(ItemId), Payload} + end, Items). + +add_message_type({xmlelement, "message", Attrs, Els}, Type) -> + {xmlelement, "message", [{"type", Type}|Attrs], Els}; +add_message_type(XmlEl, _Type) -> + XmlEl. + +add_headers({xmlelement, Name, Attrs, Els}, HeaderEls) -> + HeaderEl = {xmlelement, "headers", [{"xmlns", ?NS_SHIM}], HeaderEls}, + {xmlelement, Name, Attrs, [HeaderEl | Els]}. + +collection_shim(Node, Nodes) -> + [{xmlelement, "header", [{"name", "Collection"}], + [{xmlcdata, node_to_string(N)}]} || N <- Nodes -- [Node]]. diff --git a/src/mod_pubsub/node.template b/src/mod_pubsub/node.template index 850a38ec9..e0c743d38 100644 --- a/src/mod_pubsub/node.template +++ b/src/mod_pubsub/node.template @@ -10,12 +10,12 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% -%%% @copyright 2006-2008 Process-one +%%% @copyright 2006-2009 ProcessOne %%% @author Christophe romain <christophe.romain@process-one.net> %%% [http://www.process-one.net/] %%% @version {@vsn}, {@date} {@time} @@ -35,66 +35,70 @@ %% it's possible not to define some function at all %% in that case, warning will be generated at compilation %% and function call will fail, -%% then mod_pubsub will call function from node_default +%% then mod_pubsub will call function from node_hometree %% (this makes code cleaner, but execution a little bit longer) %% API definition -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, - create_node/3, - delete_node/2, - purge_node/3, + create_node/2, + delete_node/1, + purge_node/2, subscribe_node/8, - unsubscribe_node/5, - publish_item/7, + unsubscribe_node/4, + publish_item/6, delete_item/4, - remove_extra_items/4, + remove_extra_items/3, get_entity_affiliations/2, - get_node_affiliations/2, - get_affiliation/3, - set_affiliation/4, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, get_entity_subscriptions/2, - get_node_subscriptions/2, - get_subscription/3, - set_subscription/4, - get_states/2, - get_state/3, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, set_state/1, + get_items/6, get_items/2, - get_item/3, - set_item/1 + get_item/7, + get_item/2, + set_item/1, + get_item_name/3 ]). init(Host, ServerHost, Opts) -> - node_default:init(Host, ServerHost, Opts). + node_hometree:init(Host, ServerHost, Opts). terminate(Host, ServerHost) -> - node_default:terminate(Host, ServerHost). + node_hometree:terminate(Host, ServerHost). options() -> - [{node_type, __TO_BE_DEFINED__}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, {persist_items, true}, - {max_items, ?MAXITEMS div 2}, + {max_items, ?MAXITEMS}, {subscribe, true}, {access_model, open}, {roster_groups_allowed, []}, {publish_model, publishers}, + {notification_type, headline}, {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, never}, + {send_last_published_item, on_sub_and_presence}, {deliver_notifications, true}, {presence_based_delivery, false}]. features() -> ["create-nodes", "delete-nodes", + "delete-items", "instant-nodes", - "item-ids", "outcast-affiliation", "persistent-items", "publish", @@ -108,70 +112,82 @@ features() -> ]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> - node_default:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). -create_node(Host, Node, Owner) -> - node_default:create_node(Host, Node, Owner). +create_node(NodeId, Owner) -> + node_hometree:create_node(NodeId, Owner). -delete_node(Host, Removed) -> - node_default:delete_node(Host, Removed). +delete_node(Removed) -> + node_hometree:delete_node(Removed). -subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> - node_default:subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). +subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). -unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> - node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID). +unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> + node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID). -publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> - node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> + node_hometree:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). -remove_extra_items(Host, Node, MaxItems, ItemIds) -> - node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). +remove_extra_items(NodeId, MaxItems, ItemIds) -> + node_hometree:remove_extra_items(NodeId, MaxItems, ItemIds). -delete_item(Host, Node, JID, ItemId) -> - node_default:delete_item(Host, Node, JID, ItemId). +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + node_hometree:delete_item(NodeId, Publisher, PublishModel, ItemId). -purge_node(Host, Node, Owner) -> - node_default:purge_node(Host, Node, Owner). +purge_node(NodeId, Owner) -> + node_hometree:purge_node(NodeId, Owner). get_entity_affiliations(Host, Owner) -> - node_default:get_entity_affiliations(Host, Owner). + node_hometree:get_entity_affiliations(Host, Owner). -get_node_affiliations(Host, Node) -> - node_default:get_node_affiliations(Host, Node). +get_node_affiliations(NodeId) -> + node_hometree:get_node_affiliations(NodeId). -get_affiliation(Host, Node, Owner) -> - node_default:get_affiliation(Host, Node, Owner). +get_affiliation(NodeId, Owner) -> + node_hometree:get_affiliation(NodeId, Owner). -set_affiliation(Host, Node, Owner, Affiliation) -> - node_default:set_affiliation(Host, Node, Owner, Affiliation). +set_affiliation(NodeId, Owner, Affiliation) -> + node_hometree:set_affiliation(NodeId, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> - node_default:get_entity_subscriptions(Host, Owner). + node_hometree:get_entity_subscriptions(Host, Owner). -get_node_subscriptions(Host, Node) -> - node_default:get_node_subscriptions(Host, Node). +get_node_subscriptions(NodeId) -> + node_hometree:get_node_subscriptions(NodeId). -get_subscription(Host, Node, Owner) -> - node_default:get_subscription(Host, Node, Owner). +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). -set_subscription(Host, Node, Owner, Subscription) -> - node_default:set_subscription(Host, Node, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). -get_states(Host, Node) -> - node_default:get_states(Host, Node). +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). -get_state(Host, Node, JID) -> - node_default:get_state(Host, Node, JID). +get_states(NodeId) -> + node_hometree:get_states(NodeId). + +get_state(NodeId, JID) -> + node_hometree:get_state(NodeId, JID). set_state(State) -> - node_default:set_state(State). + node_hometree:set_state(State). -get_items(Host, Node) -> - node_default:get_items(Host, Node). +get_items(NodeId, From) -> + node_hometree:get_items(NodeId, From). -get_item(Host, Node, ItemId) -> - node_default:get_item(Host, Node, ItemId). +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). + +get_item(NodeId, ItemId) -> + node_hometree:get_item(NodeId, ItemId). +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). + set_item(Item) -> - node_default:set_item(Item). + node_hometree:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_hometree:get_item_name(Host, Node, Id). diff --git a/src/mod_pubsub/node_buddy.erl b/src/mod_pubsub/node_buddy.erl index cdf3a696f..c952252bd 100644 --- a/src/mod_pubsub/node_buddy.erl +++ b/src/mod_pubsub/node_buddy.erl @@ -10,13 +10,13 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% %%% -%%% @copyright 2006-2008 Process-one +%%% @copyright 2006-2009 ProcessOne %%% @author Christophe romain <christophe.romain@process-one.net> %%% [http://www.process-one.net/] %%% @version {@vsn}, {@date} {@time} @@ -36,57 +36,62 @@ %% it's possible not to define some function at all %% in that case, warning will be generated at compilation %% and function call will fail, -%% then mod_pubsub will call function from node_default +%% then mod_pubsub will call function from node_hometree %% (this makes code cleaner, but execution a little bit longer) %% API definition -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, - create_node/3, - delete_node/2, - purge_node/3, + create_node/2, + delete_node/1, + purge_node/2, subscribe_node/8, - unsubscribe_node/5, - publish_item/7, + unsubscribe_node/4, + publish_item/6, delete_item/4, - remove_extra_items/4, + remove_extra_items/3, get_entity_affiliations/2, - get_node_affiliations/2, - get_affiliation/3, - set_affiliation/4, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, get_entity_subscriptions/2, - get_node_subscriptions/2, - get_subscription/3, - set_subscription/4, - get_states/2, - get_state/3, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, set_state/1, + get_items/6, get_items/2, - get_item/3, + get_item/7, + get_item/2, set_item/1, - get_item_name/3 + get_item_name/3, + node_to_path/1, + path_to_node/1 ]). init(Host, ServerHost, Opts) -> - node_default:init(Host, ServerHost, Opts). + node_hometree:init(Host, ServerHost, Opts). terminate(Host, ServerHost) -> - node_default:terminate(Host, ServerHost). + node_hometree:terminate(Host, ServerHost). options() -> - [{node_type, buddy}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, {persist_items, true}, - {max_items, ?MAXITEMS div 2}, + {max_items, ?MAXITEMS}, {subscribe, true}, {access_model, presence}, {roster_groups_allowed, []}, {publish_model, publishers}, + {notification_type, headline}, {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, never}, {deliver_notifications, true}, @@ -95,6 +100,7 @@ options() -> features() -> ["create-nodes", "delete-nodes", + "delete-items", "instant-nodes", "item-ids", "outcast-affiliation", @@ -110,73 +116,89 @@ features() -> ]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> - node_default:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). -create_node(Host, Node, Owner) -> - node_default:create_node(Host, Node, Owner). +create_node(NodeId, Owner) -> + node_hometree:create_node(NodeId, Owner). -delete_node(Host, Removed) -> - node_default:delete_node(Host, Removed). +delete_node(Removed) -> + node_hometree:delete_node(Removed). -subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> - node_default:subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). +subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). -unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> - node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID). +unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> + node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID). -publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> - node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> + node_hometree:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). -remove_extra_items(Host, Node, MaxItems, ItemIds) -> - node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). +remove_extra_items(NodeId, MaxItems, ItemIds) -> + node_hometree:remove_extra_items(NodeId, MaxItems, ItemIds). -delete_item(Host, Node, JID, ItemId) -> - node_default:delete_item(Host, Node, JID, ItemId). +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + node_hometree:delete_item(NodeId, Publisher, PublishModel, ItemId). -purge_node(Host, Node, Owner) -> - node_default:purge_node(Host, Node, Owner). +purge_node(NodeId, Owner) -> + node_hometree:purge_node(NodeId, Owner). get_entity_affiliations(Host, Owner) -> - node_default:get_entity_affiliations(Host, Owner). + node_hometree:get_entity_affiliations(Host, Owner). -get_node_affiliations(Host, Node) -> - node_default:get_node_affiliations(Host, Node). +get_node_affiliations(NodeId) -> + node_hometree:get_node_affiliations(NodeId). -get_affiliation(Host, Node, Owner) -> - node_default:get_affiliation(Host, Node, Owner). +get_affiliation(NodeId, Owner) -> + node_hometree:get_affiliation(NodeId, Owner). -set_affiliation(Host, Node, Owner, Affiliation) -> - node_default:set_affiliation(Host, Node, Owner, Affiliation). +set_affiliation(NodeId, Owner, Affiliation) -> + node_hometree:set_affiliation(NodeId, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> - node_default:get_entity_subscriptions(Host, Owner). + node_hometree:get_entity_subscriptions(Host, Owner). -get_node_subscriptions(Host, Node) -> - node_default:get_node_subscriptions(Host, Node). +get_node_subscriptions(NodeId) -> + node_hometree:get_node_subscriptions(NodeId). -get_subscription(Host, Node, Owner) -> - node_default:get_subscription(Host, Node, Owner). +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). -set_subscription(Host, Node, Owner, Subscription) -> - node_default:set_subscription(Host, Node, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). -get_states(Host, Node) -> - node_default:get_states(Host, Node). +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). -get_state(Host, Node, JID) -> - node_default:get_state(Host, Node, JID). +get_states(NodeId) -> + node_hometree:get_states(NodeId). + +get_state(NodeId, JID) -> + node_hometree:get_state(NodeId, JID). set_state(State) -> - node_default:set_state(State). + node_hometree:set_state(State). + +get_items(NodeId, From) -> + node_hometree:get_items(NodeId, From). + +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). -get_items(Host, Node) -> - node_default:get_items(Host, Node). +get_item(NodeId, ItemId) -> + node_hometree:get_item(NodeId, ItemId). -get_item(Host, Node, ItemId) -> - node_default:get_item(Host, Node, ItemId). +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_default:set_item(Item). + node_hometree:set_item(Item). get_item_name(Host, Node, Id) -> - node_default:get_item_name(Host, Node, Id). + node_hometree:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat:node_to_path(Node). + +path_to_node(Path) -> + node_flat:path_to_node(Path). + diff --git a/src/mod_pubsub/node_club.erl b/src/mod_pubsub/node_club.erl index d927053fb..d77cde702 100644 --- a/src/mod_pubsub/node_club.erl +++ b/src/mod_pubsub/node_club.erl @@ -10,13 +10,13 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% %%% -%%% @copyright 2006-2008 Process-one +%%% @copyright 2006-2009 ProcessOne %%% @author Christophe romain <christophe.romain@process-one.net> %%% [http://www.process-one.net/] %%% @version {@vsn}, {@date} {@time} @@ -36,57 +36,62 @@ %% it's possible not to define some function at all %% in that case, warning will be generated at compilation %% and function call will fail, -%% then mod_pubsub will call function from node_default +%% then mod_pubsub will call function from node_hometree %% (this makes code cleaner, but execution a little bit longer) %% API definition -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, - create_node/3, - delete_node/2, - purge_node/3, + create_node/2, + delete_node/1, + purge_node/2, subscribe_node/8, - unsubscribe_node/5, - publish_item/7, + unsubscribe_node/4, + publish_item/6, delete_item/4, - remove_extra_items/4, + remove_extra_items/3, get_entity_affiliations/2, - get_node_affiliations/2, - get_affiliation/3, - set_affiliation/4, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, get_entity_subscriptions/2, - get_node_subscriptions/2, - get_subscription/3, - set_subscription/4, - get_states/2, - get_state/3, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, set_state/1, + get_items/6, get_items/2, - get_item/3, + get_item/7, + get_item/2, set_item/1, - get_item_name/3 + get_item_name/3, + node_to_path/1, + path_to_node/1 ]). init(Host, ServerHost, Opts) -> - node_default:init(Host, ServerHost, Opts). + node_hometree:init(Host, ServerHost, Opts). terminate(Host, ServerHost) -> - node_default:terminate(Host, ServerHost). + node_hometree:terminate(Host, ServerHost). options() -> - [{node_type, club}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, {persist_items, true}, - {max_items, ?MAXITEMS div 2}, + {max_items, ?MAXITEMS}, {subscribe, true}, {access_model, authorize}, {roster_groups_allowed, []}, {publish_model, publishers}, + {notification_type, headline}, {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, never}, {deliver_notifications, true}, @@ -95,8 +100,8 @@ options() -> features() -> ["create-nodes", "delete-nodes", + "delete-items", "instant-nodes", - "item-ids", "outcast-affiliation", "persistent-items", "publish", @@ -110,73 +115,89 @@ features() -> ]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> - node_default:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). -create_node(Host, Node, Owner) -> - node_default:create_node(Host, Node, Owner). +create_node(NodeId, Owner) -> + node_hometree:create_node(NodeId, Owner). -delete_node(Host, Removed) -> - node_default:delete_node(Host, Removed). +delete_node(Removed) -> + node_hometree:delete_node(Removed). -subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> - node_default:subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). +subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). -unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> - node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID). +unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> + node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID). -publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> - node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> + node_hometree:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). -remove_extra_items(Host, Node, MaxItems, ItemIds) -> - node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). +remove_extra_items(NodeId, MaxItems, ItemIds) -> + node_hometree:remove_extra_items(NodeId, MaxItems, ItemIds). -delete_item(Host, Node, JID, ItemId) -> - node_default:delete_item(Host, Node, JID, ItemId). +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + node_hometree:delete_item(NodeId, Publisher, PublishModel, ItemId). -purge_node(Host, Node, Owner) -> - node_default:purge_node(Host, Node, Owner). +purge_node(NodeId, Owner) -> + node_hometree:purge_node(NodeId, Owner). get_entity_affiliations(Host, Owner) -> - node_default:get_entity_affiliations(Host, Owner). + node_hometree:get_entity_affiliations(Host, Owner). -get_node_affiliations(Host, Node) -> - node_default:get_node_affiliations(Host, Node). +get_node_affiliations(NodeId) -> + node_hometree:get_node_affiliations(NodeId). -get_affiliation(Host, Node, Owner) -> - node_default:get_affiliation(Host, Node, Owner). +get_affiliation(NodeId, Owner) -> + node_hometree:get_affiliation(NodeId, Owner). -set_affiliation(Host, Node, Owner, Affiliation) -> - node_default:set_affiliation(Host, Node, Owner, Affiliation). +set_affiliation(NodeId, Owner, Affiliation) -> + node_hometree:set_affiliation(NodeId, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> - node_default:get_entity_subscriptions(Host, Owner). + node_hometree:get_entity_subscriptions(Host, Owner). -get_node_subscriptions(Host, Node) -> - node_default:get_node_subscriptions(Host, Node). +get_node_subscriptions(NodeId) -> + node_hometree:get_node_subscriptions(NodeId). -get_subscription(Host, Node, Owner) -> - node_default:get_subscription(Host, Node, Owner). +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). -set_subscription(Host, Node, Owner, Subscription) -> - node_default:set_subscription(Host, Node, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). -get_states(Host, Node) -> - node_default:get_states(Host, Node). +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). -get_state(Host, Node, JID) -> - node_default:get_state(Host, Node, JID). +get_states(NodeId) -> + node_hometree:get_states(NodeId). + +get_state(NodeId, JID) -> + node_hometree:get_state(NodeId, JID). set_state(State) -> - node_default:set_state(State). + node_hometree:set_state(State). + +get_items(NodeId, From) -> + node_hometree:get_items(NodeId, From). + +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). -get_items(Host, Node) -> - node_default:get_items(Host, Node). +get_item(NodeId, ItemId) -> + node_hometree:get_item(NodeId, ItemId). -get_item(Host, Node, ItemId) -> - node_default:get_item(Host, Node, ItemId). +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_default:set_item(Item). + node_hometree:set_item(Item). get_item_name(Host, Node, Id) -> - node_default:get_item_name(Host, Node, Id). + node_hometree:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat:node_to_path(Node). + +path_to_node(Path) -> + node_flat:path_to_node(Path). + diff --git a/src/mod_pubsub/node_dag.erl b/src/mod_pubsub/node_dag.erl new file mode 100644 index 000000000..b70169460 --- /dev/null +++ b/src/mod_pubsub/node_dag.erl @@ -0,0 +1,184 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% @author Brian Cully <bjc@kublai.com> +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(node_dag). +-author('bjc@kublai.com'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/2, + delete_node/1, + purge_node/2, + subscribe_node/8, + unsubscribe_node/4, + publish_item/6, + delete_item/4, + remove_extra_items/3, + get_entity_affiliations/2, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, + get_entity_subscriptions/2, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, + set_state/1, + get_items/6, + get_items/2, + get_item/7, + get_item/2, + set_item/1, + get_item_name/3, + node_to_path/1, + path_to_node/1]). + + +init(Host, ServerHost, Opts) -> + node_hometree:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_hometree:terminate(Host, ServerHost). + +options() -> + [{node_type, leaf} | node_hometree:options()]. + +features() -> + ["multi-collection" | node_hometree:features()]. + +create_node_permission(_Host, _ServerHost, _Node, _ParentNode, + _Owner, _Access) -> + {result, true}. + +create_node(NodeID, Owner) -> + node_hometree:create_node(NodeID, Owner). + +delete_node(Removed) -> + node_hometree:delete_node(Removed). + +subscribe_node(NodeID, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node(NodeID, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, + Options). + +unsubscribe_node(NodeID, Sender, Subscriber, SubID) -> + node_hometree:unsubscribe_node(NodeID, Sender, Subscriber, SubID). + +publish_item(NodeID, Publisher, Model, MaxItems, ItemID, Payload) -> + %% TODO: should look up the NodeTree plugin here. There's no + %% access to the Host of the request at this level, so for now we + %% just use nodetree_dag. + case nodetree_dag:get_node(NodeID) of + #pubsub_node{options = Options} -> + case find_opt(node_type, Options) of + collection -> + {error, ?ERR_EXTENDED(?ERR_NOT_ALLOWED, "publish")}; + _ -> + node_hometree:publish_item(NodeID, Publisher, Model, + MaxItems, ItemID, Payload) + end; + Err -> + Err + end. + +find_opt(_, []) -> false; +find_opt(Option, [{Option, Value} | _]) -> Value; +find_opt(Option, [_ | T]) -> find_opt(Option, T). + +remove_extra_items(NodeID, MaxItems, ItemIDs) -> + node_hometree:remove_extra_items(NodeID, MaxItems, ItemIDs). + +delete_item(NodeID, Publisher, PublishModel, ItemID) -> + node_hometree:delete_item(NodeID, Publisher, PublishModel, ItemID). + +purge_node(NodeID, Owner) -> + node_hometree:purge_node(NodeID, Owner). + +get_entity_affiliations(Host, Owner) -> + node_hometree:get_entity_affiliations(Host, Owner). + +get_node_affiliations(NodeID) -> + node_hometree:get_node_affiliations(NodeID). + +get_affiliation(NodeID, Owner) -> + node_hometree:get_affiliation(NodeID, Owner). + +set_affiliation(NodeID, Owner, Affiliation) -> + node_hometree:set_affiliation(NodeID, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_hometree:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(NodeID) -> + node_hometree:get_node_subscriptions(NodeID). + +get_subscriptions(NodeID, Owner) -> + node_hometree:get_subscriptions(NodeID, Owner). + +set_subscriptions(NodeID, Owner, Subscription, SubID) -> + node_hometree:set_subscriptions(NodeID, Owner, Subscription, SubID). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). + +get_states(NodeID) -> + node_hometree:get_states(NodeID). + +get_state(NodeID, JID) -> + node_hometree:get_state(NodeID, JID). + +set_state(State) -> + node_hometree:set_state(State). + +get_items(NodeID, From) -> + node_hometree:get_items(NodeID, From). + +get_items(NodeID, JID, AccessModel, PresenceSubscription, + RosterGroup, SubID) -> + node_hometree:get_items(NodeID, JID, AccessModel, PresenceSubscription, + RosterGroup, SubID). + +get_item(NodeID, ItemID) -> + node_hometree:get_item(NodeID, ItemID). + +get_item(NodeID, ItemID, JID, AccessModel, PresenceSubscription, + RosterGroup, SubID) -> + node_hometree:get_item(NodeID, ItemID, JID, AccessModel, + PresenceSubscription, RosterGroup, SubID). + +set_item(Item) -> + node_hometree:set_item(Item). + +get_item_name(Host, Node, ID) -> + node_hometree:get_item_name(Host, Node, ID). + +node_to_path(Node) -> + node_hometree:node_to_path(Node). + +path_to_node(Path) -> + node_hometree:path_to_node(Path). + diff --git a/src/mod_pubsub/node_default.erl b/src/mod_pubsub/node_default.erl deleted file mode 100644 index fc4194c32..000000000 --- a/src/mod_pubsub/node_default.erl +++ /dev/null @@ -1,742 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. -%%% -%%% -%%% @copyright 2006-2008 Process-one -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @todo The item table should be handled by the plugin, but plugin that do -%%% not want to manage it should be able to use the default behaviour. -%%% @todo Plugin modules should be able to register to receive presence update -%%% send to pubsub. - -%%% @doc The module <strong>{@module}</strong> is the default PubSub plugin. -%%% <p>It is used as a default for all unknown PubSub node type. It can serve -%%% as a developer basis and reference to build its own custom pubsub node -%%% types.</p> -%%% <p>PubSub plugin nodes are using the {@link gen_node} behaviour.</p> -%%% <p><strong>The API isn't stabilized yet</strong>. The pubsub plugin -%%% development is still a work in progress. However, the system is already -%%% useable and useful as is. Please, send us comments, feedback and -%%% improvements.</p> - --module(node_default). --author('christophe.romain@process-one.net'). - --include("pubsub.hrl"). --include("jlib.hrl"). - --behaviour(gen_pubsub_node). - -%% API definition --export([init/3, terminate/2, - options/0, features/0, - create_node_permission/6, - create_node/3, - delete_node/2, - purge_node/3, - subscribe_node/8, - unsubscribe_node/5, - publish_item/7, - delete_item/4, - remove_extra_items/4, - get_entity_affiliations/2, - get_node_affiliations/2, - get_affiliation/3, - set_affiliation/4, - get_entity_subscriptions/2, - get_node_subscriptions/2, - get_subscription/3, - set_subscription/4, - get_states/2, - get_state/3, - set_state/1, - get_items/2, - get_item/3, - set_item/1, - get_item_name/3 - ]). - -%% ================ -%% API definition -%% ================ - -%% @spec (Host, ServerHost, Opts) -> any() -%% Host = mod_pubsub:host() -%% ServerHost = mod_pubsub:host() -%% Opts = list() -%% @doc <p>Called during pubsub modules initialisation. Any pubsub plugin must -%% implement this function. It can return anything.</p> -%% <p>This function is mainly used to trigger the setup task necessary for the -%% plugin. It can be used for example by the developer to create the specific -%% module database schema if it does not exists yet.</p> -init(_Host, _ServerHost, _Opts) -> - mnesia:create_table(pubsub_state, - [{disc_copies, [node()]}, - {attributes, record_info(fields, pubsub_state)}]), - StatesFields = record_info(fields, pubsub_state), - case mnesia:table_info(pubsub_state, attributes) of - StatesFields -> ok; - _ -> - mnesia:transform_table(pubsub_state, ignore, StatesFields) - end, - mnesia:create_table(pubsub_item, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, pubsub_item)}]), - ItemsFields = record_info(fields, pubsub_item), - case mnesia:table_info(pubsub_item, attributes) of - ItemsFields -> ok; - _ -> - mnesia:transform_table(pubsub_item, ignore, ItemsFields) - end, - ok. - -%% @spec (Host, ServerHost) -> any() -%% Host = mod_pubsub:host() -%% ServerHost = host() -%% @doc <p>Called during pubsub modules termination. Any pubsub plugin must -%% implement this function. It can return anything.</p> -terminate(_Host, _ServerHost) -> - ok. - -%% @spec () -> [Option] -%% Option = mod_pubsub:nodeOption() -%% @doc Returns the default pubsub node options. -%% <p>Example of function return value:</p> -%% ``` -%% [{deliver_payloads, true}, -%% {notify_config, false}, -%% {notify_delete, false}, -%% {notify_retract, true}, -%% {persist_items, true}, -%% {max_items, 10}, -%% {subscribe, true}, -%% {access_model, open}, -%% {publish_model, publishers}, -%% {max_payload_size, 100000}, -%% {send_last_published_item, never}, -%% {presence_based_delivery, false}]''' -options() -> - [{node_type, default}, - {deliver_payloads, true}, - {notify_config, false}, - {notify_delete, false}, - {notify_retract, true}, - {persist_items, true}, - {max_items, ?MAXITEMS div 2}, - {subscribe, true}, - {access_model, open}, - {roster_groups_allowed, []}, - {publish_model, publishers}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, false}]. - -%% @spec () -> [] -%% @doc Returns the node features -features() -> - ["create-nodes", - "auto-create", - "delete-nodes", - "instant-nodes", - "item-ids", - "manage-subscriptions", - "modify-affiliations", - "outcast-affiliation", - "persistent-items", - "publish", - "purge-nodes", - "retract-items", - "retrieve-affiliations", - "retrieve-items", - "retrieve-subscriptions", - "subscribe", - "subscription-notifications" - ]. - -%% @spec (Host, ServerHost, Node, ParentNode, Owner, Access) -> bool() -%% Host = mod_pubsub:host() -%% ServerHost = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% ParentNode = mod_pubsub:pubsubNode() -%% Owner = mod_pubsub:jid() -%% Access = all | atom() -%% @doc Checks if the current user has the permission to create the requested node -%% <p>In {@link node_default}, the permission is decided by the place in the -%% hierarchy where the user is creating the node. The access parameter is also -%% checked in the default module. This parameter depends on the value of the -%% <tt>access_createnode</tt> ACL value in ejabberd config file.</p> -%% <p>This function also check that node can be created a a children of its -%% parent node</p> -%% <p>PubSub plugins can redefine the PubSub node creation rights as they -%% which. They can simply delegate this check to the {@link node_default} -%% module by implementing this function like this: -%% ```check_create_user_permission(Host, Node, Owner, Access) -> -%% node_default:check_create_user_permission(Host, Node, Owner, Access).'''</p> -create_node_permission(Host, ServerHost, Node, _ParentNode, Owner, Access) -> - LOwner = jlib:jid_tolower(Owner), - {User, Server, _Resource} = LOwner, - Allowed = case acl:match_rule(ServerHost, Access, LOwner) of - allow -> - if Server == Host -> %% Server == ServerHost ?? - true; - true -> - case Node of - ["home", Server, User | _] -> true; - _ -> false - end - end; - _ -> - case Owner of - {jid, "", _, "", "", _, ""} -> true; - _ -> false - end - end, - ChildOK = true, %% TODO test with ParentNode - {result, Allowed and ChildOK}. - -%% @spec (Host, Node, Owner) -> -%% {result, Result} | exit -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% Owner = mod_pubsub:jid() -%% @doc <p></p> -create_node(Host, Node, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - mnesia:write(#pubsub_state{stateid = {OwnerKey, {Host, Node}}, - affiliation = owner, subscription = none}), - {result, {default, broadcast}}. - - -%% @spec (Host, Removed) -> ok -%% Host = mod_pubsub:host() -%% Removed = [mod_pubsub:pubsubNode()] -%% @doc <p>purge items of deleted nodes after effective deletion.</p> -delete_node(Host, Removed) -> - lists:foreach( - fun(Node) -> - lists:foreach( - fun(#pubsub_state{stateid = StateId, items = Items}) -> - lists:foreach( - fun(ItemId) -> - mnesia:delete( - {pubsub_item, {ItemId, {Host, Node}}}) - end, Items), - mnesia:delete({pubsub_state, StateId}) - end, - mnesia:match_object( - #pubsub_state{stateid = {'_', {Host, Node}}, _ = '_'})) - end, Removed), - {result, {default, broadcast, Removed}}. - -%% @spec (Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> -%% {error, Reason} | {result, Result} -%% @doc <p>Accepts or rejects subcription requests on a PubSub node.</p> -%% <p>The mechanism works as follow: -%% <ul> -%% <li>The main PubSub module prepares the subscription and passes the -%% result of the preparation as a record.</li> -%% <li>This function gets the prepared record and several other parameters and -%% can decide to:<ul> -%% <li>reject the subscription;</li> -%% <li>allow it as is, letting the main module perform the database -%% persistance;</li> -%% <li>allow it, modifying the record. The main module will store the -%% modified record;</li> -%% <li>allow it, but perform the needed persistance operations.</li></ul> -%% </li></ul></p> -%% <p>The selected behaviour depends on the return parameter: -%% <ul> -%% <li><tt>{error, Reason}</tt>: an IQ error result will be returned. No -%% subscription will actually be performed.</li> -%% <li><tt>true</tt>: Subscribe operation is allowed, based on the -%% unmodified record passed in parameter <tt>SubscribeResult</tt>. If this -%% parameter contains an error, no subscription will be performed.</li> -%% <li><tt>{true, PubsubState}</tt>: Subscribe operation is allowed, but -%% the {@link mod_pubsub:pubsubState()} record returned replaces the value -%% passed in parameter <tt>SubscribeResult</tt>.</li> -%% <li><tt>{true, done}</tt>: Subscribe operation is allowed, but the -%% {@link mod_pubsub:pubsubState()} will be considered as already stored and -%% no further persistance operation will be performed. This case is used, -%% when the plugin module is doing the persistance by itself or when it want -%% to completly disable persistance.</li></ul> -%% </p> -%% <p>In the default plugin module, the record is unchanged.</p> -subscribe_node(Host, Node, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup) -> - SenderKey = jlib:jid_tolower(Sender), - Authorized = (jlib:jid_remove_resource(SenderKey) == jlib:jid_remove_resource(Subscriber)), - % TODO add some acl check for Authorized ? - State = case get_state(Host, Node, Subscriber) of - {error, ?ERR_ITEM_NOT_FOUND} -> - #pubsub_state{stateid = {Subscriber, {Host, Node}}}; % TODO: bug on Key ? - {result, S} -> S - end, - #pubsub_state{affiliation = Affiliation, - subscription = Subscription} = State, - if - not Authorized -> - %% JIDs do not match - {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "invalid-jid")}; - Subscription == pending -> - %% Requesting entity has pending subscription - {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "pending-subscription")}; - Affiliation == outcast -> - %% Requesting entity is blocked - {error, ?ERR_FORBIDDEN}; - (AccessModel == presence) and (not PresenceSubscription) -> - %% Entity is not authorized to create a subscription (presence subscription required) - {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "presence-subscription-required")}; - (AccessModel == roster) and (not RosterGroup) -> - %% Entity is not authorized to create a subscription (not in roster group) - {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "not-in-roster-group")}; - (AccessModel == whitelist) -> % TODO: to be done - %% Node has whitelist access model - {error, ?ERR_EXTENDED(?ERR_NOT_ALLOWED, "closed-node")}; - (AccessModel == authorize) -> % TODO: to be done - %% Node has authorize access model - {error, ?ERR_FORBIDDEN}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - %%ForbiddenAnonymous -> - %% % Requesting entity is anonymous - %% {error, ?ERR_FORBIDDEN}; - true -> - NewSubscription = - if - AccessModel == authorize -> - pending; - %%TODO Affiliation == none -> ? - %%NeedConfiguration -> - %% unconfigured - true -> - subscribed - end, - set_state(State#pubsub_state{subscription = NewSubscription}), - case NewSubscription of - subscribed -> - case SendLast of - never -> {result, {default, NewSubscription}}; - _ -> {result, {default, NewSubscription, send_last}} - end; - _ -> - {result, {default, NewSubscription}} - end - end. - -%% @spec (Host, Node, Sender, Subscriber, SubID) -> -%% {error, Reason} | {result, []} -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% Sender = mod_pubsub:jid() -%% Subscriber = mod_pubsub:jid() -%% SubID = string() -%% Reason = mod_pubsub:stanzaError() -%% @doc <p>Unsubscribe the <tt>Subscriber</tt> from the <tt>Node</tt>.</p> -unsubscribe_node(Host, Node, Sender, Subscriber, _SubId) -> - SenderKey = jlib:jid_tolower(Sender), - Match = jlib:jid_remove_resource(SenderKey) == jlib:jid_remove_resource(Subscriber), - Authorized = case Match of - true -> - true; - false -> - case get_state(Host, Node, SenderKey) of % TODO: bug on Key ? - {result, #pubsub_state{affiliation=owner}} -> true; - _ -> false - end - end, - case get_state(Host, Node, Subscriber) of - {error, ?ERR_ITEM_NOT_FOUND} -> - %% Requesting entity is not a subscriber - {error, ?ERR_EXTENDED(?ERR_UNEXPECTED_REQUEST, "not-subscribed")}; - {result, State} -> - if - %% Entity did not specify SubID - %%SubID == "", ?? -> - %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %% Invalid subscription identifier - %%InvalidSubID -> - %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - %% Requesting entity is not a subscriber - State#pubsub_state.subscription == none -> - {error, ?ERR_EXTENDED(?ERR_UNEXPECTED_REQUEST, "not-subscribed")}; - %% Requesting entity is prohibited from unsubscribing entity - not Authorized -> - {error, ?ERR_FORBIDDEN}; - true -> - set_state(State#pubsub_state{subscription = none}), - {result, default} - end - end. - -%% @spec (Host, Node, Publisher, PublishModel, MaxItems, ItemId, Payload) -> -%% {true, PubsubItem} | {result, Reply} -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% Publisher = mod_pubsub:jid() -%% PublishModel = atom() -%% MaxItems = integer() -%% ItemId = string() -%% Payload = term() -%% @doc <p>Publishes the item passed as parameter.</p> -%% <p>The mechanism works as follow: -%% <ul> -%% <li>The main PubSub module prepares the item to publish and passes the -%% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.</li> -%% <li>This function gets the prepared record and several other parameters and can decide to:<ul> -%% <li>reject the publication;</li> -%% <li>allow the publication as is, letting the main module perform the database persistance;</li> -%% <li>allow the publication, modifying the record. The main module will store the modified record;</li> -%% <li>allow it, but perform the needed persistance operations.</li></ul> -%% </li></ul></p> -%% <p>The selected behaviour depends on the return parameter: -%% <ul> -%% <li><tt>{error, Reason}</tt>: an iq error result will be return. No -%% publication is actually performed.</li> -%% <li><tt>true</tt>: Publication operation is allowed, based on the -%% unmodified record passed in parameter <tt>Item</tt>. If the <tt>Item</tt> -%% parameter contains an error, no subscription will actually be -%% performed.</li> -%% <li><tt>{true, Item}</tt>: Publication operation is allowed, but the -%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed -%% in parameter <tt>Item</tt>. The persistance will be performed by the main -%% module.</li> -%% <li><tt>{true, done}</tt>: Publication operation is allowed, but the -%% {@link mod_pubsub:pubsubItem()} will be considered as already stored and -%% no further persistance operation will be performed. This case is used, -%% when the plugin module is doing the persistance by itself or when it want -%% to completly disable persistance.</li></ul> -%% </p> -%% <p>In the default plugin module, the record is unchanged.</p> -publish_item(Host, Node, Publisher, PublishModel, MaxItems, ItemId, Payload) -> - PublisherKey = jlib:jid_tolower(jlib:jid_remove_resource(Publisher)), - State = case get_state(Host, Node, PublisherKey) of - {error, ?ERR_ITEM_NOT_FOUND} -> #pubsub_state{stateid={PublisherKey, {Host, Node}}}; - {result, S} -> S - end, - #pubsub_state{affiliation = Affiliation, - subscription = Subscription} = State, - if - not ((PublishModel == open) - or ((PublishModel == publishers) - and ((Affiliation == owner) or (Affiliation == publisher))) - or ((PublishModel == subscribers) - and (Subscription == subscribed))) -> - %% Entity does not have sufficient privileges to publish to node - {error, ?ERR_FORBIDDEN}; - true -> - PubId = {PublisherKey, now()}, - Item = case get_item(Host, Node, ItemId) of - {error, ?ERR_ITEM_NOT_FOUND} -> - #pubsub_item{itemid = {ItemId, {Host, Node}}, - creation = PubId, - modification = PubId, - payload = Payload}; - {result, OldItem} -> - OldItem#pubsub_item{modification = PubId, - payload = Payload} - end, - Items = [ItemId | State#pubsub_state.items], - {result, {NI, OI}} = remove_extra_items( - Host, Node, MaxItems, Items), - set_item(Item), - set_state(State#pubsub_state{items = NI}), - {result, {default, broadcast, OI}} - end. - -%% @spec (Host, Node, MaxItems, ItemIds) -> {NewItemIds,OldItemIds} -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% MaxItems = integer() | unlimited -%% ItemIds = [ItemId::string()] -%% NewItemIds = [ItemId::string()] -%% @doc <p>This function is used to remove extra items, most notably when the -%% maximum number of items has been reached.</p> -%% <p>This function is used internally by the core PubSub module, as no -%% permission check is performed.</p> -%% <p>In the default plugin module, the oldest items are removed, but other -%% rules can be used.</p> -%% <p>If another PubSub plugin wants to delegate the item removal (and if the -%% plugin is using the default pubsub storage), it can implements this function like this: -%% ```remove_extra_items(Host, Node, MaxItems, ItemIds) -> -%% node_default:remove_extra_items(Host, Node, MaxItems, ItemIds).'''</p> -remove_extra_items(_Host, _Node, unlimited, ItemIds) -> - {result, {ItemIds, []}}; -remove_extra_items(Host, Node, MaxItems, ItemIds) -> - NewItems = lists:sublist(ItemIds, MaxItems), - OldItems = lists:nthtail(length(NewItems), ItemIds), - %% Remove extra items: - lists:foreach(fun(ItemId) -> - mnesia:delete({pubsub_item, {ItemId, {Host, Node}}}) - end, OldItems), - %% Return the new items list: - {result, {NewItems, OldItems}}. - -%% @spec (Host, Node, JID, ItemId) -> -%% {error, Reason::stanzaError()} | -%% {result, []} -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% JID = mod_pubsub:jid() -%% ItemId = string() -%% @doc <p>Triggers item deletion.</p> -%% <p>Default plugin: The user performing the deletion must be the node owner -%% or a node publisher e item publisher.</p> -delete_item(Host, Node, Publisher, ItemId) -> - PublisherKey = jlib:jid_tolower(jlib:jid_remove_resource(Publisher)), - State = case get_state(Host, Node, PublisherKey) of - {error, ?ERR_ITEM_NOT_FOUND} -> - #pubsub_state{stateid = {PublisherKey, {Host, Node}}}; - {result, S} -> - S - end, - #pubsub_state{affiliation = Affiliation, items = Items} = State, - Allowed = (Affiliation == publisher) orelse (Affiliation == owner) - orelse case get_item(Host, Node, ItemId) of - {result, #pubsub_item{creation = {PublisherKey, _}}} -> true; - _ -> false - end, - if - not Allowed -> - %% Requesting entity does not have sufficient privileges - {error, ?ERR_FORBIDDEN}; - true -> - case get_item(Host, Node, ItemId) of - {result, _} -> - mnesia:delete({pubsub_item, {ItemId, {Host, Node}}}), - NewItems = lists:delete(ItemId, Items), - set_state(State#pubsub_state{items = NewItems}), - {result, {default, broadcast}}; - _ -> - %% Non-existent node or item - {error, ?ERR_ITEM_NOT_FOUND} - end - end. - -%% @spec (Host, Node, Owner) -> -%% {error, Reason::stanzaError()} | -%% {result, {default, broadcast}} -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% Owner = mod_pubsub:jid() -purge_node(Host, Node, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - case get_state(Host, Node, OwnerKey) of - {error, ?ERR_ITEM_NOT_FOUND} -> - %% This should not append (case node does not exists) - {error, ?ERR_ITEM_NOT_FOUND}; - {result, #pubsub_state{items = Items, affiliation = owner}} -> - lists:foreach(fun(ItemId) -> - mnesia:delete({pubsub_item, {ItemId, {Host, Node}}}) - end, Items), - {result, {default, broadcast}}; - _ -> - %% Entity is not an owner - {error, ?ERR_FORBIDDEN} - end. - -%% @spec (Host, JID) -> [{Node,Affiliation}] -%% Host = host() -%% JID = mod_pubsub:jid() -%% @doc <p>Return the current affiliations for the given user</p> -%% <p>The default module reads affiliations in the main Mnesia -%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same -%% table, it should return an empty list, as the affiliation will be read by -%% the default PubSub module. Otherwise, it should return its own affiliation, -%% that will be added to the affiliation stored in the main -%% <tt>pubsub_state</tt> table.</p> -get_entity_affiliations(Host, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - States = mnesia:match_object( - #pubsub_state{stateid = {OwnerKey, {Host, '_'}}, - _ = '_'}), - Tr = fun(#pubsub_state{stateid = {_, {_, N}}, affiliation = A}) -> - {N, A} - end, - {result, lists:map(Tr, States)}. - -get_node_affiliations(Host, Node) -> - States = mnesia:match_object( - #pubsub_state{stateid = {'_', {Host, Node}}, - _ = '_'}), - Tr = fun(#pubsub_state{stateid = {J, {_, _}}, affiliation = A}) -> - {J, A} - end, - {result, lists:map(Tr, States)}. - -get_affiliation(Host, Node, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - Affiliation = case get_state(Host, Node, OwnerKey) of - {result, #pubsub_state{affiliation = A}} -> A; - _ -> unknown - end, - {result, Affiliation}. - -set_affiliation(Host, Node, Owner, Affiliation) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - Record = case get_state(Host, Node, OwnerKey) of - {error, ?ERR_ITEM_NOT_FOUND} -> - #pubsub_state{stateid = {OwnerKey, {Host, Node}}, - affiliation = Affiliation}; - {result, State} -> - State#pubsub_state{affiliation = Affiliation} - end, - set_state(Record), - ok. - -%% @spec (Host, Owner) -> [{Node,Subscription}] -%% Host = host() -%% Owner = mod_pubsub:jid() -%% Node = mod_pubsub:pubsubNode() -%% @doc <p>Return the current subscriptions for the given user</p> -%% <p>The default module reads subscriptions in the main Mnesia -%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same -%% table, it should return an empty list, as the affiliation will be read by -%% the default PubSub module. Otherwise, it should return its own affiliation, -%% that will be added to the affiliation stored in the main -%% <tt>pubsub_state</tt> table.</p> -get_entity_subscriptions(Host, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - States = mnesia:match_object( - #pubsub_state{stateid = {OwnerKey, {Host, '_'}}, - _ = '_'}), - Tr = fun(#pubsub_state{stateid = {_, {_, N}}, subscription = S}) -> - {N, S} - end, - {result, lists:map(Tr, States)}. - -get_node_subscriptions(Host, Node) -> - States = mnesia:match_object( - #pubsub_state{stateid = {'_', {Host, Node}}, - _ = '_'}), - Tr = fun(#pubsub_state{stateid = {J, {_, _}}, subscription = S}) -> - {J, S} - end, - {result, lists:map(Tr, States)}. - -get_subscription(Host, Node, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - Subscription = case get_state(Host, Node, OwnerKey) of - {result, #pubsub_state{subscription = S}} -> S; - _ -> unknown - end, - {result, Subscription}. - -set_subscription(Host, Node, Owner, Subscription) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - Record = case get_state(Host, Node, OwnerKey) of - {error, ?ERR_ITEM_NOT_FOUND} -> - #pubsub_state{stateid = {OwnerKey, {Host, Node}}, - subscription = Subscription}; - {result, State} -> - State#pubsub_state{subscription = Subscription} - end, - set_state(Record), - ok. - -%% @spec (Host, Node) -> [States] | [] -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% Item = mod_pubsub:pubsubItems() -%% @doc Returns the list of stored states for a given node. -%% <p>For the default PubSub module, states are stored in Mnesia database.</p> -%% <p>We can consider that the pubsub_state table have been created by the main -%% mod_pubsub module.</p> -%% <p>PubSub plugins can store the states where they wants (for example in a -%% relational database).</p> -%% <p>If a PubSub plugin wants to delegate the states storage to the default node, -%% they can implement this function like this: -%% ```get_states(Host, Node) -> -%% node_default:get_states(Host, Node).'''</p> -get_states(Host, Node) -> - States = mnesia:match_object( - #pubsub_state{stateid = {'_', {Host, Node}}, _ = '_'}), - {result, States}. - -%% @spec (JID, Host, Node) -> [State] | [] -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% JID = mod_pubsub:jid() -%% State = mod_pubsub:pubsubItems() -%% @doc <p>Returns a state (one state list), given its reference.</p> -get_state(Host, Node, JID) -> - case mnesia:read({pubsub_state, {JID, {Host, Node}}}) of - [State] when is_record(State, pubsub_state) -> - {result, State}; - _ -> - {error, ?ERR_ITEM_NOT_FOUND} - end. - -%% @spec (State) -> ok | {error, Reason::stanzaError()} -%% State = mod_pubsub:pubsubStates() -%% @doc <p>Write a state into database.</p> -set_state(State) when is_record(State, pubsub_state) -> - mnesia:write(State); -set_state(_) -> - {error, ?ERR_INTERNAL_SERVER_ERROR}. - -%% @spec (Host, Node) -> [Items] | [] -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% Items = mod_pubsub:pubsubItems() -%% @doc Returns the list of stored items for a given node. -%% <p>For the default PubSub module, items are stored in Mnesia database.</p> -%% <p>We can consider that the pubsub_item table have been created by the main -%% mod_pubsub module.</p> -%% <p>PubSub plugins can store the items where they wants (for example in a -%% relational database), or they can even decide not to persist any items.</p> -%% <p>If a PubSub plugin wants to delegate the item storage to the default node, -%% they can implement this function like this: -%% ```get_items(Host, Node) -> -%% node_default:get_items(Host, Node).'''</p> -get_items(Host, Node) -> - Items = mnesia:match_object( - #pubsub_item{itemid = {'_', {Host, Node}}, _ = '_'}), - {result, Items}. - -%% @spec (Host, Node, ItemId) -> [Item] | [] -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% ItemId = string() -%% Item = mod_pubsub:pubsubItems() -%% @doc <p>Returns an item (one item list), given its reference.</p> -get_item(Host, Node, ItemId) -> - case mnesia:read({pubsub_item, {ItemId, {Host, Node}}}) of - [Item] when is_record(Item, pubsub_item) -> - {result, Item}; - _ -> - {error, ?ERR_ITEM_NOT_FOUND} - end. - -%% @spec (Item) -> ok | {error, Reason::stanzaError()} -%% Item = mod_pubsub:pubsubItems() -%% @doc <p>Write a state into database.</p> -set_item(Item) when is_record(Item, pubsub_item) -> - mnesia:write(Item); -set_item(_) -> - {error, ?ERR_INTERNAL_SERVER_ERROR}. - -%% @doc <p>Return the name of the node if known: Default is to return -%% node id.</p> -get_item_name(_Host, _Node, Id) -> - Id. diff --git a/src/mod_pubsub/node_dispatch.erl b/src/mod_pubsub/node_dispatch.erl index 3b4418c3e..d81848fe3 100644 --- a/src/mod_pubsub/node_dispatch.erl +++ b/src/mod_pubsub/node_dispatch.erl @@ -10,13 +10,13 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% %%% -%%% @copyright 2006-2008 Process-one +%%% @copyright 2006-2009 ProcessOne %%% @author Christophe romain <christophe.romain@process-one.net> %%% [http://www.process-one.net/] %%% @version {@vsn}, {@date} {@time} @@ -41,50 +41,55 @@ -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, - create_node/3, - delete_node/2, - purge_node/3, + create_node/2, + delete_node/1, + purge_node/2, subscribe_node/8, - unsubscribe_node/5, - publish_item/7, + unsubscribe_node/4, + publish_item/6, delete_item/4, - remove_extra_items/4, + remove_extra_items/3, get_entity_affiliations/2, - get_node_affiliations/2, - get_affiliation/3, - set_affiliation/4, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, get_entity_subscriptions/2, - get_node_subscriptions/2, - get_subscription/3, - set_subscription/4, - get_states/2, - get_state/3, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, set_state/1, + get_items/6, get_items/2, - get_item/3, + get_item/7, + get_item/2, set_item/1, - get_item_name/3 + get_item_name/3, + node_to_path/1, + path_to_node/1 ]). init(Host, ServerHost, Opts) -> - node_default:init(Host, ServerHost, Opts). + node_hometree:init(Host, ServerHost, Opts). terminate(Host, ServerHost) -> - node_default:terminate(Host, ServerHost). + node_hometree:terminate(Host, ServerHost). options() -> - [{node_type, dispatch}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, {persist_items, true}, - {max_items, ?MAXITEMS div 2}, + {max_items, ?MAXITEMS}, {subscribe, true}, {access_model, open}, {roster_groups_allowed, []}, {publish_model, publishers}, + {notification_type, headline}, {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, never}, {deliver_notifications, true}, @@ -94,7 +99,6 @@ features() -> ["create-nodes", "delete-nodes", "instant-nodes", - "item-ids", "outcast-affiliation", "persistent-items", "publish", @@ -108,78 +112,96 @@ features() -> ]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> - node_default:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). -create_node(Host, Node, Owner) -> - node_default:create_node(Host, Node, Owner). +create_node(NodeId, Owner) -> + node_hometree:create_node(NodeId, Owner). -delete_node(Host, Removed) -> - node_default:delete_node(Host, Removed). +delete_node(Removed) -> + node_hometree:delete_node(Removed). -subscribe_node(_Host, _Node, _Sender, _Subscriber, _AccessModel, - _SendLast, _PresenceSubscription, _RosterGroup) -> +subscribe_node(_NodeId, _Sender, _Subscriber, _AccessModel, + _SendLast, _PresenceSubscription, _RosterGroup, _Options) -> {error, ?ERR_FORBIDDEN}. -unsubscribe_node(_Host, _Node, _Sender, _Subscriber, _SubID) -> +unsubscribe_node(_NodeId, _Sender, _Subscriber, _SubID) -> {error, ?ERR_FORBIDDEN}. -publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> lists:foreach(fun(SubNode) -> - node_default:publish_item( - Host, SubNode, Publisher, Model, + node_hometree:publish_item( + SubNode#pubsub_node.id, Publisher, Model, MaxItems, ItemId, Payload) - end, nodetree_default:get_subnodes(Host, Node)). + end, nodetree_tree:get_subnodes(NodeId, Publisher, Publisher)). -remove_extra_items(_Host, _Node, _MaxItems, ItemIds) -> +remove_extra_items(_NodeId, _MaxItems, ItemIds) -> {result, {ItemIds, []}}. -delete_item(_Host, _Node, _JID, _ItemId) -> +delete_item(_NodeId, _Publisher, _PublishModel, _ItemId) -> {error, ?ERR_ITEM_NOT_FOUND}. -purge_node(_Host, _Node, _Owner) -> +purge_node(_NodeId, _Owner) -> {error, ?ERR_FORBIDDEN}. get_entity_affiliations(_Host, _Owner) -> {result, []}. -get_node_affiliations(_Host, _Node) -> +get_node_affiliations(_NodeId) -> {result, []}. -get_affiliation(_Host, _Node, _Owner) -> +get_affiliation(_NodeId, _Owner) -> {result, []}. -set_affiliation(Host, Node, Owner, Affiliation) -> - node_default:set_affiliation(Host, Node, Owner, Affiliation). +set_affiliation(NodeId, Owner, Affiliation) -> + node_hometree:set_affiliation(NodeId, Owner, Affiliation). get_entity_subscriptions(_Host, _Owner) -> {result, []}. -get_node_subscriptions(_Host, _Node) -> - {result, []}. +get_node_subscriptions(NodeId) -> + %% note: get_node_subscriptions is used for broadcasting + %% DO NOT REMOVE + node_hometree:get_node_subscriptions(NodeId). -get_subscription(_Host, _Node, _Owner) -> +get_subscriptions(_NodeId, _Owner) -> {result, []}. -set_subscription(Host, Node, Owner, Subscription) -> - node_default:set_subscription(Host, Node, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). -get_states(Host, Node) -> - node_default:get_states(Host, Node). +get_states(NodeId) -> + node_hometree:get_states(NodeId). -get_state(Host, Node, JID) -> - node_default:get_state(Host, Node, JID). +get_state(NodeId, JID) -> + node_hometree:get_state(NodeId, JID). set_state(State) -> - node_default:set_state(State). + node_hometree:set_state(State). + +get_items(NodeId, From) -> + node_hometree:get_items(NodeId, From). + +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). -get_items(Host, Node) -> - node_default:get_items(Host, Node). +get_item(NodeId, ItemId) -> + node_hometree:get_item(NodeId, ItemId). -get_item(Host, Node, ItemId) -> - node_default:get_item(Host, Node, ItemId). +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_default:set_item(Item). + node_hometree:set_item(Item). get_item_name(Host, Node, Id) -> - node_default:get_item_name(Host, Node, Id). + node_hometree:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat:node_to_path(Node). + +path_to_node(Path) -> + node_flat:path_to_node(Path). + diff --git a/src/mod_pubsub/node_flat.erl b/src/mod_pubsub/node_flat.erl new file mode 100644 index 000000000..854e64321 --- /dev/null +++ b/src/mod_pubsub/node_flat.erl @@ -0,0 +1,196 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% @copyright 2006-2009 ProcessOne +%%% @author Christophe Romain <christophe.romain@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(node_flat). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/2, + delete_node/1, + purge_node/2, + subscribe_node/8, + unsubscribe_node/4, + publish_item/6, + delete_item/4, + remove_extra_items/3, + get_entity_affiliations/2, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, + get_entity_subscriptions/2, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, + set_state/1, + get_items/6, + get_items/2, + get_item/7, + get_item/2, + set_item/1, + get_item_name/3, + node_to_path/1, + path_to_node/1 + ]). + + +init(Host, ServerHost, Opts) -> + node_hometree:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_hometree:terminate(Host, ServerHost). + +options() -> + [{deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, open}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, false}]. + +features() -> + node_hometree:features(). + +%% use same code as node_hometree, but do not limite node to +%% the home/localhost/user/... hierarchy +%% any node is allowed +create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> + LOwner = jlib:jid_tolower(Owner), + Allowed = case LOwner of + {"", Host, ""} -> + true; % pubsub service always allowed + _ -> + acl:match_rule(ServerHost, Access, LOwner) =:= allow + end, + {result, Allowed}. + +create_node(NodeId, Owner) -> + node_hometree:create_node(NodeId, Owner). + +delete_node(Removed) -> + node_hometree:delete_node(Removed). + +subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). + +unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> + node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID). + +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> + node_hometree:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(NodeId, MaxItems, ItemIds) -> + node_hometree:remove_extra_items(NodeId, MaxItems, ItemIds). + +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + node_hometree:delete_item(NodeId, Publisher, PublishModel, ItemId). + +purge_node(NodeId, Owner) -> + node_hometree:purge_node(NodeId, Owner). + +get_entity_affiliations(Host, Owner) -> + node_hometree:get_entity_affiliations(Host, Owner). + +get_node_affiliations(NodeId) -> + node_hometree:get_node_affiliations(NodeId). + +get_affiliation(NodeId, Owner) -> + node_hometree:get_affiliation(NodeId, Owner). + +set_affiliation(NodeId, Owner, Affiliation) -> + node_hometree:set_affiliation(NodeId, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_hometree:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(NodeId) -> + node_hometree:get_node_subscriptions(NodeId). + +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). + +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). + +get_states(NodeId) -> + node_hometree:get_states(NodeId). + +get_state(NodeId, JID) -> + node_hometree:get_state(NodeId, JID). + +set_state(State) -> + node_hometree:set_state(State). + +get_items(NodeId, From) -> + node_hometree:get_items(NodeId, From). + +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). + +get_item(NodeId, ItemId) -> + node_hometree:get_item(NodeId, ItemId). + +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_hometree:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_hometree:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + [binary_to_list(Node)]. + +path_to_node(Path) -> + case Path of + % default slot + [Node] -> list_to_binary(Node); + % handle old possible entries, used when migrating database content to new format + [Node|_] when is_list(Node) -> list_to_binary(string:join([""|Path], "/")); + % default case (used by PEP for example) + _ -> list_to_binary(Path) + end. diff --git a/src/mod_pubsub/node_flat_odbc.erl b/src/mod_pubsub/node_flat_odbc.erl new file mode 100644 index 000000000..0e6c8faa1 --- /dev/null +++ b/src/mod_pubsub/node_flat_odbc.erl @@ -0,0 +1,213 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% @copyright 2006-2009 ProcessOne +%%% @author Christophe Romain <christophe.romain@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(node_flat_odbc). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/2, + delete_node/1, + purge_node/2, + subscribe_node/8, + unsubscribe_node/4, + publish_item/6, + delete_item/4, + remove_extra_items/3, + get_entity_affiliations/2, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, + get_entity_subscriptions/2, + get_entity_subscriptions_for_send_last/2, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, + set_state/1, + get_items/7, + get_items/6, + get_items/3, + get_items/2, + get_item/7, + get_item/2, + set_item/1, + get_item_name/3, + get_last_items/3, + node_to_path/1, + path_to_node/1 + ]). + + +init(Host, ServerHost, Opts) -> + node_hometree_odbc:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_hometree_odbc:terminate(Host, ServerHost). + +options() -> + [{node_type, flat}, + {deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, open}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, false}, + {odbc, true}, + {rsm, true}]. + +features() -> + node_hometree_odbc:features(). + +%% use same code as node_hometree_odbc, but do not limite node to +%% the home/localhost/user/... hierarchy +%% any node is allowed +create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> + LOwner = jlib:jid_tolower(Owner), + Allowed = case LOwner of + {"", Host, ""} -> + true; % pubsub service always allowed + _ -> + acl:match_rule(ServerHost, Access, LOwner) =:= allow + end, + {result, Allowed}. + +create_node(NodeId, Owner) -> + node_hometree_odbc:create_node(NodeId, Owner). + +delete_node(Removed) -> + node_hometree_odbc:delete_node(Removed). + +subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree_odbc:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). + +unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> + node_hometree_odbc:unsubscribe_node(NodeId, Sender, Subscriber, SubID). + +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> + node_hometree_odbc:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(NodeId, MaxItems, ItemIds) -> + node_hometree_odbc:remove_extra_items(NodeId, MaxItems, ItemIds). + +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + node_hometree_odbc:delete_item(NodeId, Publisher, PublishModel, ItemId). + +purge_node(NodeId, Owner) -> + node_hometree_odbc:purge_node(NodeId, Owner). + +get_entity_affiliations(Host, Owner) -> + node_hometree_odbc:get_entity_affiliations(Host, Owner). + +get_node_affiliations(NodeId) -> + node_hometree_odbc:get_node_affiliations(NodeId). + +get_affiliation(NodeId, Owner) -> + node_hometree_odbc:get_affiliation(NodeId, Owner). + +set_affiliation(NodeId, Owner, Affiliation) -> + node_hometree_odbc:set_affiliation(NodeId, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_hometree_odbc:get_entity_subscriptions(Host, Owner). + +get_entity_subscriptions_for_send_last(Host, Owner) -> + node_hometree_odbc:get_entity_subscriptions_for_send_last(Host, Owner). + +get_node_subscriptions(NodeId) -> + node_hometree_odbc:get_node_subscriptions(NodeId). + +get_subscriptions(NodeId, Owner) -> + node_hometree_odbc:get_subscriptions(NodeId, Owner). + +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree_odbc:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree_odbc:get_pending_nodes(Host, Owner). + +get_states(NodeId) -> + node_hometree_odbc:get_states(NodeId). + +get_state(NodeId, JID) -> + node_hometree_odbc:get_state(NodeId, JID). + +set_state(State) -> + node_hometree_odbc:set_state(State). + +get_items(NodeId, From) -> + node_hometree_odbc:get_items(NodeId, From). +get_items(NodeId, From, RSM) -> + node_hometree_odbc:get_items(NodeId, From, RSM). +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, none). +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> + node_hometree_odbc:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). + +get_item(NodeId, ItemId) -> + node_hometree_odbc:get_item(NodeId, ItemId). + +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree_odbc:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_hometree_odbc:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_hometree_odbc:get_item_name(Host, Node, Id). + +get_last_items(NodeId, From, Count) -> + node_hometree_odbc:get_last_items(NodeId, From, Count). + +node_to_path(Node) -> + [binary_to_list(Node)]. + +path_to_node(Path) -> + case Path of + % default slot + [Node] -> list_to_binary(Node); + % handle old possible entries, used when migrating database content to new format + [Node|_] when is_list(Node) -> list_to_binary(string:join([""|Path], "/")); + % default case (used by PEP for example) + _ -> list_to_binary(Path) + end. + diff --git a/src/mod_pubsub/node_hometree.erl b/src/mod_pubsub/node_hometree.erl new file mode 100644 index 000000000..5ce7b9724 --- /dev/null +++ b/src/mod_pubsub/node_hometree.erl @@ -0,0 +1,1027 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% +%%% @copyright 2006-2009 ProcessOne +%%% @author Christophe Romain <christophe.romain@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @todo The item table should be handled by the plugin, but plugin that do +%%% not want to manage it should be able to use the default behaviour. +%%% @todo Plugin modules should be able to register to receive presence update +%%% send to pubsub. + +%%% @doc The module <strong>{@module}</strong> is the default PubSub plugin. +%%% <p>It is used as a default for all unknown PubSub node type. It can serve +%%% as a developer basis and reference to build its own custom pubsub node +%%% types.</p> +%%% <p>PubSub plugin nodes are using the {@link gen_node} behaviour.</p> +%%% <p><strong>The API isn't stabilized yet</strong>. The pubsub plugin +%%% development is still a work in progress. However, the system is already +%%% useable and useful as is. Please, send us comments, feedback and +%%% improvements.</p> + +-module(node_hometree). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/2, + delete_node/1, + purge_node/2, + subscribe_node/8, + unsubscribe_node/4, + publish_item/6, + delete_item/4, + remove_extra_items/3, + get_entity_affiliations/2, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, + get_entity_subscriptions/2, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, + set_state/1, + get_items/6, + get_items/2, + get_item/7, + get_item/2, + set_item/1, + get_item_name/3, + node_to_path/1, + path_to_node/1 + ]). + +%% ================ +%% API definition +%% ================ + +%% @spec (Host, ServerHost, Opts) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = mod_pubsub:host() +%% Opts = list() +%% @doc <p>Called during pubsub modules initialisation. Any pubsub plugin must +%% implement this function. It can return anything.</p> +%% <p>This function is mainly used to trigger the setup task necessary for the +%% plugin. It can be used for example by the developer to create the specific +%% module database schema if it does not exists yet.</p> +init(_Host, _ServerHost, _Opts) -> + pubsub_subscription:init(), + mnesia:create_table(pubsub_state, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_state)}]), + mnesia:create_table(pubsub_item, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, pubsub_item)}]), + ItemsFields = record_info(fields, pubsub_item), + case mnesia:table_info(pubsub_item, attributes) of + ItemsFields -> ok; + _ -> + mnesia:transform_table(pubsub_item, ignore, ItemsFields) + end, + ok. + +%% @spec (Host, ServerHost) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = host() +%% @doc <p>Called during pubsub modules termination. Any pubsub plugin must +%% implement this function. It can return anything.</p> +terminate(_Host, _ServerHost) -> + ok. + +%% @spec () -> [Option] +%% Option = mod_pubsub:nodeOption() +%% @doc Returns the default pubsub node options. +%% <p>Example of function return value:</p> +%% ``` +%% [{deliver_payloads, true}, +%% {notify_config, false}, +%% {notify_delete, false}, +%% {notify_retract, true}, +%% {persist_items, true}, +%% {max_items, 10}, +%% {subscribe, true}, +%% {access_model, open}, +%% {publish_model, publishers}, +%% {max_payload_size, 100000}, +%% {send_last_published_item, never}, +%% {presence_based_delivery, false}]''' +options() -> + [{deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, open}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, false}]. + +%% @spec () -> [] +%% @doc Returns the node features +features() -> + ["create-nodes", + "auto-create", + "access-authorize", + "delete-nodes", + "delete-items", + "get-pending", + "instant-nodes", + "manage-subscriptions", + "modify-affiliations", + "multi-subscribe", + "outcast-affiliation", + "persistent-items", + "publish", + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", + "retrieve-subscriptions", + "subscribe", + "subscription-notifications", + "subscription-options" + ]. + +%% @spec (Host, ServerHost, Node, ParentNode, Owner, Access) -> bool() +%% Host = mod_pubsub:host() +%% ServerHost = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% ParentNode = mod_pubsub:pubsubNode() +%% Owner = mod_pubsub:jid() +%% Access = all | atom() +%% @doc Checks if the current user has the permission to create the requested node +%% <p>In {@link node_default}, the permission is decided by the place in the +%% hierarchy where the user is creating the node. The access parameter is also +%% checked in the default module. This parameter depends on the value of the +%% <tt>access_createnode</tt> ACL value in ejabberd config file.</p> +%% <p>This function also check that node can be created a a children of its +%% parent node</p> +%% <p>PubSub plugins can redefine the PubSub node creation rights as they +%% which. They can simply delegate this check to the {@link node_default} +%% module by implementing this function like this: +%% ```check_create_user_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> +%% node_default:check_create_user_permission(Host, ServerHost, Node, ParentNode, Owner, Access).'''</p> +create_node_permission(Host, ServerHost, Node, _ParentNode, Owner, Access) -> + LOwner = jlib:jid_tolower(Owner), + {User, Server, _Resource} = LOwner, + Allowed = case LOwner of + {"", Host, ""} -> + true; % pubsub service always allowed + _ -> + case acl:match_rule(ServerHost, Access, LOwner) of + allow -> + case node_to_path(Node) of + ["home", Server, User | _] -> true; + _ -> false + end; + _ -> + false + end + end, + {result, Allowed}. + +%% @spec (NodeId, Owner) -> +%% {result, Result} | exit +%% NodeId = mod_pubsub:pubsubNodeId() +%% Owner = mod_pubsub:jid() +%% @doc <p></p> +create_node(NodeId, Owner) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + set_state(#pubsub_state{stateid = {OwnerKey, NodeId}, affiliation = owner}), + {result, {default, broadcast}}. + +%% @spec (Removed) -> ok +%% Removed = [mod_pubsub:pubsubNode()] +%% @doc <p>purge items of deleted nodes after effective deletion.</p> +delete_node(Removed) -> + Tr = fun(#pubsub_state{stateid = {J, _}, subscriptions = Ss}) -> + lists:map(fun(S) -> + {J, S} + end, Ss) + end, + Reply = lists:map( + fun(#pubsub_node{id = NodeId} = PubsubNode) -> + {result, States} = get_states(NodeId), + lists:foreach( + fun(#pubsub_state{stateid = {LJID, _}, items = Items}) -> + del_items(NodeId, Items), + del_state(NodeId, LJID) + end, States), + {PubsubNode, lists:flatmap(Tr, States)} + end, Removed), + {result, {default, broadcast, Reply}}. + +%% @spec (NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> +%% {error, Reason} | {result, Result} +%% @doc <p>Accepts or rejects subcription requests on a PubSub node.</p> +%% <p>The mechanism works as follow: +%% <ul> +%% <li>The main PubSub module prepares the subscription and passes the +%% result of the preparation as a record.</li> +%% <li>This function gets the prepared record and several other parameters and +%% can decide to:<ul> +%% <li>reject the subscription;</li> +%% <li>allow it as is, letting the main module perform the database +%% persistance;</li> +%% <li>allow it, modifying the record. The main module will store the +%% modified record;</li> +%% <li>allow it, but perform the needed persistance operations.</li></ul> +%% </li></ul></p> +%% <p>The selected behaviour depends on the return parameter: +%% <ul> +%% <li><tt>{error, Reason}</tt>: an IQ error result will be returned. No +%% subscription will actually be performed.</li> +%% <li><tt>true</tt>: Subscribe operation is allowed, based on the +%% unmodified record passed in parameter <tt>SubscribeResult</tt>. If this +%% parameter contains an error, no subscription will be performed.</li> +%% <li><tt>{true, PubsubState}</tt>: Subscribe operation is allowed, but +%% the {@link mod_pubsub:pubsubState()} record returned replaces the value +%% passed in parameter <tt>SubscribeResult</tt>.</li> +%% <li><tt>{true, done}</tt>: Subscribe operation is allowed, but the +%% {@link mod_pubsub:pubsubState()} will be considered as already stored and +%% no further persistance operation will be performed. This case is used, +%% when the plugin module is doing the persistance by itself or when it want +%% to completly disable persistance.</li></ul> +%% </p> +%% <p>In the default plugin module, the record is unchanged.</p> +subscribe_node(NodeId, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + SubKey = jlib:jid_tolower(Subscriber), + GenKey = jlib:jid_remove_resource(SubKey), + Authorized = (jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == GenKey), + GenState = get_state(NodeId, GenKey), + SubState = case SubKey of + GenKey -> GenState; + _ -> get_state(NodeId, SubKey) + end, + Affiliation = GenState#pubsub_state.affiliation, + Subscriptions = SubState#pubsub_state.subscriptions, + Whitelisted = lists:member(Affiliation, [member, publisher, owner]), + PendingSubscription = lists:any(fun({pending, _}) -> true; + (_) -> false + end, Subscriptions), + if + not Authorized -> + %% JIDs do not match + {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "invalid-jid")}; + Affiliation == outcast -> + %% Requesting entity is blocked + {error, ?ERR_FORBIDDEN}; + PendingSubscription -> + %% Requesting entity has pending subscription + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "pending-subscription")}; + (AccessModel == presence) and (not PresenceSubscription) -> + %% Entity is not authorized to create a subscription (presence subscription required) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "presence-subscription-required")}; + (AccessModel == roster) and (not RosterGroup) -> + %% Entity is not authorized to create a subscription (not in roster group) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "not-in-roster-group")}; + (AccessModel == whitelist) and (not Whitelisted) -> + %% Node has whitelist access model and entity lacks required affiliation + {error, ?ERR_EXTENDED(?ERR_NOT_ALLOWED, "closed-node")}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + %%ForbiddenAnonymous -> + %% % Requesting entity is anonymous + %% {error, ?ERR_FORBIDDEN}; + true -> + case pubsub_subscription:add_subscription(Subscriber, NodeId, Options) of + SubId when is_list(SubId) -> + NewSub = case AccessModel of + authorize -> pending; + _ -> subscribed + end, + set_state(SubState#pubsub_state{subscriptions = [{NewSub, SubId} | Subscriptions]}), + case {NewSub, SendLast} of + {subscribed, never} -> + {result, {default, subscribed, SubId}}; + {subscribed, _} -> + {result, {default, subscribed, SubId, send_last}}; + {_, _} -> + {result, {default, pending, SubId}} + end; + _ -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end + end. + +%% @spec (NodeId, Sender, Subscriber, SubId) -> +%% {error, Reason} | {result, []} +%% NodeId = mod_pubsub:pubsubNodeId() +%% Sender = mod_pubsub:jid() +%% Subscriber = mod_pubsub:jid() +%% SubId = mod_pubsub:subid() +%% Reason = mod_pubsub:stanzaError() +%% @doc <p>Unsubscribe the <tt>Subscriber</tt> from the <tt>Node</tt>.</p> +unsubscribe_node(NodeId, Sender, Subscriber, SubId) -> + SubKey = jlib:jid_tolower(Subscriber), + GenKey = jlib:jid_remove_resource(SubKey), + Authorized = (jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == GenKey), + GenState = get_state(NodeId, GenKey), + SubState = case SubKey of + GenKey -> GenState; + _ -> get_state(NodeId, SubKey) + end, + Subscriptions = lists:filter(fun({_Sub, _SubId}) -> true; + (_SubId) -> false + end, SubState#pubsub_state.subscriptions), + SubIdExists = case SubId of + [] -> false; + List when is_list(List) -> true; + _ -> false + end, + if + %% Requesting entity is prohibited from unsubscribing entity + not Authorized -> + {error, ?ERR_FORBIDDEN}; + %% Entity did not specify SubId + %%SubId == "", ?? -> + %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %% Invalid subscription identifier + %%InvalidSubId -> + %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + %% Requesting entity is not a subscriber + Subscriptions == [] -> + {error, ?ERR_EXTENDED(?ERR_UNEXPECTED_REQUEST, "not-subscribed")}; + %% Subid supplied, so use that. + SubIdExists -> + Sub = first_in_list(fun(S) -> + case S of + {_Sub, SubId} -> true; + _ -> false + end + end, SubState#pubsub_state.subscriptions), + case Sub of + {value, S} -> + delete_subscription(SubKey, NodeId, S, SubState), + {result, default}; + false -> + {error, ?ERR_EXTENDED(?ERR_UNEXPECTED_REQUEST, + "not-subscribed")} + end; + %% No subid supplied, but there's only one matching + %% subscription, so use that. + length(Subscriptions) == 1 -> + delete_subscription(SubKey, NodeId, hd(Subscriptions), SubState), + {result, default}; + %% No subid and more than one possible subscription match. + true -> + {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")} + end. + +delete_subscription(SubKey, NodeID, {Subscription, SubId}, SubState) -> + Affiliation = SubState#pubsub_state.affiliation, + AllSubs = SubState#pubsub_state.subscriptions, + NewSubs = AllSubs -- [{Subscription, SubId}], + pubsub_subscription:delete_subscription(SubKey, NodeID, SubId), + case {Affiliation, NewSubs} of + {none, []} -> + % Just a regular subscriber, and this is final item, so + % delete the state. + del_state(NodeID, SubKey); + _ -> + set_state(SubState#pubsub_state{subscriptions = NewSubs}) + end. + + +%% @spec (NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload) -> +%% {true, PubsubItem} | {result, Reply} +%% NodeId = mod_pubsub:pubsubNodeId() +%% Publisher = mod_pubsub:jid() +%% PublishModel = atom() +%% MaxItems = integer() +%% ItemId = string() +%% Payload = term() +%% @doc <p>Publishes the item passed as parameter.</p> +%% <p>The mechanism works as follow: +%% <ul> +%% <li>The main PubSub module prepares the item to publish and passes the +%% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.</li> +%% <li>This function gets the prepared record and several other parameters and can decide to:<ul> +%% <li>reject the publication;</li> +%% <li>allow the publication as is, letting the main module perform the database persistance;</li> +%% <li>allow the publication, modifying the record. The main module will store the modified record;</li> +%% <li>allow it, but perform the needed persistance operations.</li></ul> +%% </li></ul></p> +%% <p>The selected behaviour depends on the return parameter: +%% <ul> +%% <li><tt>{error, Reason}</tt>: an iq error result will be return. No +%% publication is actually performed.</li> +%% <li><tt>true</tt>: Publication operation is allowed, based on the +%% unmodified record passed in parameter <tt>Item</tt>. If the <tt>Item</tt> +%% parameter contains an error, no subscription will actually be +%% performed.</li> +%% <li><tt>{true, Item}</tt>: Publication operation is allowed, but the +%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed +%% in parameter <tt>Item</tt>. The persistance will be performed by the main +%% module.</li> +%% <li><tt>{true, done}</tt>: Publication operation is allowed, but the +%% {@link mod_pubsub:pubsubItem()} will be considered as already stored and +%% no further persistance operation will be performed. This case is used, +%% when the plugin module is doing the persistance by itself or when it want +%% to completly disable persistance.</li></ul> +%% </p> +%% <p>In the default plugin module, the record is unchanged.</p> +publish_item(NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload) -> + SubKey = jlib:jid_tolower(Publisher), + GenKey = jlib:jid_remove_resource(SubKey), + GenState = get_state(NodeId, GenKey), + SubState = case SubKey of + GenKey -> GenState; + _ -> get_state(NodeId, SubKey) + end, + Affiliation = GenState#pubsub_state.affiliation, + Subscribed = case PublishModel of + subscribers -> is_subscribed(SubState#pubsub_state.subscriptions); + _ -> undefined + end, + if + not ((PublishModel == open) + or ((PublishModel == publishers) + and ((Affiliation == owner) or (Affiliation == publisher))) + or (Subscribed == true)) -> + %% Entity does not have sufficient privileges to publish to node + {error, ?ERR_FORBIDDEN}; + true -> + %% TODO: check creation, presence, roster + if MaxItems > 0 -> + Now = now(), + PubId = {Now, SubKey}, + Item = case get_item(NodeId, ItemId) of + {result, OldItem} -> + OldItem#pubsub_item{modification = PubId, + payload = Payload}; + _ -> + #pubsub_item{itemid = {ItemId, NodeId}, + creation = {Now, GenKey}, + modification = PubId, + payload = Payload} + end, + Items = [ItemId | GenState#pubsub_state.items--[ItemId]], + {result, {NI, OI}} = remove_extra_items(NodeId, MaxItems, Items), + set_item(Item), + set_state(GenState#pubsub_state{items = NI}), + {result, {default, broadcast, OI}}; + true -> + {result, {default, broadcast, []}} + end + end. + +%% @spec (NodeId, MaxItems, ItemIds) -> {NewItemIds,OldItemIds} +%% NodeId = mod_pubsub:pubsubNodeId() +%% MaxItems = integer() | unlimited +%% ItemIds = [ItemId::string()] +%% NewItemIds = [ItemId::string()] +%% @doc <p>This function is used to remove extra items, most notably when the +%% maximum number of items has been reached.</p> +%% <p>This function is used internally by the core PubSub module, as no +%% permission check is performed.</p> +%% <p>In the default plugin module, the oldest items are removed, but other +%% rules can be used.</p> +%% <p>If another PubSub plugin wants to delegate the item removal (and if the +%% plugin is using the default pubsub storage), it can implements this function like this: +%% ```remove_extra_items(NodeId, MaxItems, ItemIds) -> +%% node_default:remove_extra_items(NodeId, MaxItems, ItemIds).'''</p> +remove_extra_items(_NodeId, unlimited, ItemIds) -> + {result, {ItemIds, []}}; +remove_extra_items(NodeId, MaxItems, ItemIds) -> + NewItems = lists:sublist(ItemIds, MaxItems), + OldItems = lists:nthtail(length(NewItems), ItemIds), + %% Remove extra items: + del_items(NodeId, OldItems), + %% Return the new items list: + {result, {NewItems, OldItems}}. + +%% @spec (NodeId, Publisher, PublishModel, ItemId) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% NodeId = mod_pubsub:pubsubNodeId() +%% Publisher = mod_pubsub:jid() +%% PublishModel = atom() +%% ItemId = string() +%% @doc <p>Triggers item deletion.</p> +%% <p>Default plugin: The user performing the deletion must be the node owner +%% or a publisher, or PublishModel being open.</p> +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + SubKey = jlib:jid_tolower(Publisher), + GenKey = jlib:jid_remove_resource(SubKey), + GenState = get_state(NodeId, GenKey), + #pubsub_state{affiliation = Affiliation, items = Items} = GenState, + Allowed = (Affiliation == publisher) orelse (Affiliation == owner) + orelse (PublishModel == open) + orelse case get_item(NodeId, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}}} -> true; + _ -> false + end, + if + not Allowed -> + %% Requesting entity does not have sufficient privileges + {error, ?ERR_FORBIDDEN}; + true -> + case lists:member(ItemId, Items) of + true -> + del_item(NodeId, ItemId), + set_state(GenState#pubsub_state{items = lists:delete(ItemId, Items)}), + {result, {default, broadcast}}; + false -> + case Affiliation of + owner -> + %% Owner can delete other publishers items as well + {result, States} = get_states(NodeId), + lists:foldl( + fun(#pubsub_state{items = PI, affiliation = publisher} = S, Res) -> + case lists:member(ItemId, PI) of + true -> + del_item(NodeId, ItemId), + set_state(S#pubsub_state{items = lists:delete(ItemId, PI)}), + {result, {default, broadcast}}; + false -> + Res + end; + (_, Res) -> + Res + end, {error, ?ERR_ITEM_NOT_FOUND}, States); + _ -> + %% Non-existent node or item + {error, ?ERR_ITEM_NOT_FOUND} + end + end + end. + +%% @spec (NodeId, Owner) -> +%% {error, Reason::stanzaError()} | +%% {result, {default, broadcast}} +%% NodeId = mod_pubsub:pubsubNodeId() +%% Owner = mod_pubsub:jid() +purge_node(NodeId, Owner) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + GenState = get_state(NodeId, GenKey), + case GenState of + #pubsub_state{affiliation = owner} -> + {result, States} = get_states(NodeId), + lists:foreach( + fun(#pubsub_state{items = []}) -> + ok; + (#pubsub_state{items = Items} = S) -> + del_items(NodeId, Items), + set_state(S#pubsub_state{items = []}) + end, States), + {result, {default, broadcast}}; + _ -> + %% Entity is not owner + {error, ?ERR_FORBIDDEN} + end. + +%% @spec (Host, JID) -> [{Node,Affiliation}] +%% Host = host() +%% JID = mod_pubsub:jid() +%% @doc <p>Return the current affiliations for the given user</p> +%% <p>The default module reads affiliations in the main Mnesia +%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same +%% table, it should return an empty list, as the affiliation will be read by +%% the default PubSub module. Otherwise, it should return its own affiliation, +%% that will be added to the affiliation stored in the main +%% <tt>pubsub_state</tt> table.</p> +get_entity_affiliations(Host, Owner) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), + NodeTree = case catch ets:lookup(gen_mod:get_module_proc(Host, config), nodetree) of + [{nodetree, N}] -> N; + _ -> nodetree_tree + end, + Reply = lists:foldl(fun(#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {Host, _}} = Node -> [{Node, A}|Acc]; + _ -> Acc + end + end, [], States), + {result, Reply}. + +get_node_affiliations(NodeId) -> + {result, States} = get_states(NodeId), + Tr = fun(#pubsub_state{stateid = {J, _}, affiliation = A}) -> + {J, A} + end, + {result, lists:map(Tr, States)}. + +get_affiliation(NodeId, Owner) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + GenState = get_state(NodeId, GenKey), + {result, GenState#pubsub_state.affiliation}. + +set_affiliation(NodeId, Owner, Affiliation) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + GenState = get_state(NodeId, GenKey), + case {Affiliation, GenState#pubsub_state.subscriptions} of + {none, none} -> + del_state(NodeId, GenKey); + _ -> + set_state(GenState#pubsub_state{affiliation = Affiliation}) + end. + +%% @spec (Host, Owner) -> [{Node,Subscription}] +%% Host = host() +%% Owner = mod_pubsub:jid() +%% @doc <p>Return the current subscriptions for the given user</p> +%% <p>The default module reads subscriptions in the main Mnesia +%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same +%% table, it should return an empty list, as the affiliation will be read by +%% the default PubSub module. Otherwise, it should return its own affiliation, +%% that will be added to the affiliation stored in the main +%% <tt>pubsub_state</tt> table.</p> +get_entity_subscriptions(Host, Owner) -> + {U, D, _} = SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + States = case SubKey of + GenKey -> mnesia:match_object( + #pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); + _ -> mnesia:match_object( + #pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) + ++ mnesia:match_object( + #pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) + end, + NodeTree = case catch ets:lookup(gen_mod:get_module_proc(Host, config), nodetree) of + [{nodetree, N}] -> N; + _ -> nodetree_tree + end, + Reply = lists:foldl(fun(#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {Host, _}} = Node -> + lists:foldl(fun({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, J} | Acc2]; + (S, Acc2) -> + [{Node, S, J} | Acc2] + end, Acc, Ss); + _ -> Acc + end + end, [], States), + {result, Reply}. + +get_node_subscriptions(NodeId) -> + {result, States} = get_states(NodeId), + Tr = fun(#pubsub_state{stateid = {J, _}, subscriptions = Subscriptions}) -> + %% TODO: get rid of cases to handle non-list subscriptions + case Subscriptions of + [_|_] -> + lists:foldl(fun({S, SubId}, Acc) -> + [{J, S, SubId} | Acc]; + (S, Acc) -> + [{J, S} | Acc] + end, [], Subscriptions); + [] -> + []; + _ -> + [{J, none}] + end + end, + {result, lists:flatmap(Tr, States)}. + +get_subscriptions(NodeId, Owner) -> + SubKey = jlib:jid_tolower(Owner), + SubState = get_state(NodeId, SubKey), + {result, SubState#pubsub_state.subscriptions}. + +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + SubKey = jlib:jid_tolower(Owner), + SubState = get_state(NodeId, SubKey), + case {SubId, SubState#pubsub_state.subscriptions} of + {_, []} -> + case Subscription of + none -> {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "not-subscribed")}; + _ -> new_subscription(NodeId, Owner, Subscription, SubState) + end; + {"", [{_, SID}]} -> + case Subscription of + none -> unsub_with_subid(NodeId, SID, SubState); + _ -> replace_subscription({Subscription, SID}, SubState) + end; + {"", [_|_]} -> + {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + _ -> + case Subscription of + none -> unsub_with_subid(NodeId, SubId, SubState); + _ -> replace_subscription({Subscription, SubId}, SubState) + end + end. + +replace_subscription(NewSub, SubState) -> + NewSubs = replace_subscription(NewSub, + SubState#pubsub_state.subscriptions, []), + set_state(SubState#pubsub_state{subscriptions = NewSubs}). + +replace_subscription(_, [], Acc) -> + Acc; +replace_subscription({Sub, SubId}, [{_, SubID} | T], Acc) -> + replace_subscription({Sub, SubId}, T, [{Sub, SubID} | Acc]). + +new_subscription(NodeId, Owner, Subscription, SubState) -> + SubId = pubsub_subscription:add_subscription(Owner, NodeId, []), + Subscriptions = SubState#pubsub_state.subscriptions, + set_state(SubState#pubsub_state{subscriptions = [{Subscription, SubId} | Subscriptions]}), + {Subscription, SubId}. + +unsub_with_subid(NodeId, SubId, SubState) -> + pubsub_subscription:delete_subscription(SubState#pubsub_state.stateid, + NodeId, SubId), + NewSubs = lists:filter(fun ({_, SID}) -> SubId =/= SID end, + SubState#pubsub_state.subscriptions), + case {NewSubs, SubState#pubsub_state.affiliation} of + {[], none} -> + del_state(NodeId, element(1, SubState#pubsub_state.stateid)); + _ -> + set_state(SubState#pubsub_state{subscriptions = NewSubs}) + end. + +%% @spec (Host, Owner) -> {result, [Node]} | {error, Reason} +%% Host = host() +%% Owner = jid() +%% Node = pubsubNode() +%% @doc <p>Returns a list of Owner's nodes on Host with pending +%% subscriptions.</p> +get_pending_nodes(Host, Owner) -> + GenKey = jlib:jid_remove_resource(jlib:jid_tolower(Owner)), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, + affiliation = owner, + _ = '_'}), + NodeIDs = [ID || #pubsub_state{stateid = {_, ID}} <- States], + NodeTree = case catch ets:lookup(gen_mod:get_module_proc(Host, config), nodetree) of + [{nodetree, N}] -> N; + _ -> nodetree_tree + end, + Reply = mnesia:foldl(fun(#pubsub_state{stateid = {_, NID}} = S, Acc) -> + case lists:member(NID, NodeIDs) of + true -> + case get_nodes_helper(NodeTree, S) of + {value, Node} -> [Node | Acc]; + false -> Acc + end; + false -> + Acc + end + end, [], pubsub_state), + {result, Reply}. + +get_nodes_helper(NodeTree, + #pubsub_state{stateid = {_, N}, subscriptions = Subs}) -> + HasPending = fun ({pending, _}) -> true; + (pending) -> true; + (_) -> false + end, + case lists:any(HasPending, Subs) of + true -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {_, Node}} -> + {value, Node}; + _ -> + false + end; + false -> + false + end. + +%% @spec (NodeId) -> [States] | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% @doc Returns the list of stored states for a given node. +%% <p>For the default PubSub module, states are stored in Mnesia database.</p> +%% <p>We can consider that the pubsub_state table have been created by the main +%% mod_pubsub module.</p> +%% <p>PubSub plugins can store the states where they wants (for example in a +%% relational database).</p> +%% <p>If a PubSub plugin wants to delegate the states storage to the default node, +%% they can implement this function like this: +%% ```get_states(NodeId) -> +%% node_default:get_states(NodeId).'''</p> +get_states(NodeId) -> + States = case catch mnesia:match_object( + #pubsub_state{stateid = {'_', NodeId}, _ = '_'}) of + List when is_list(List) -> List; + _ -> [] + end, + {result, States}. + +%% @spec (NodeId, JID) -> [State] | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% JID = mod_pubsub:jid() +%% State = mod_pubsub:pubsubItems() +%% @doc <p>Returns a state (one state list), given its reference.</p> +get_state(NodeId, JID) -> + StateId = {JID, NodeId}, + case catch mnesia:read({pubsub_state, StateId}) of + [State] when is_record(State, pubsub_state) -> State; + _ -> #pubsub_state{stateid=StateId} + end. + +%% @spec (State) -> ok | {error, Reason::stanzaError()} +%% State = mod_pubsub:pubsubStates() +%% @doc <p>Write a state into database.</p> +set_state(State) when is_record(State, pubsub_state) -> + mnesia:write(State); +set_state(_) -> + {error, ?ERR_INTERNAL_SERVER_ERROR}. + +%% @spec (NodeId, JID) -> ok | {error, Reason::stanzaError()} +%% NodeId = mod_pubsub:pubsubNodeId() +%% JID = mod_pubsub:jid() +%% @doc <p>Delete a state from database.</p> +del_state(NodeId, JID) -> + mnesia:delete({pubsub_state, {JID, NodeId}}). + +%% @spec (NodeId, From) -> [Items] | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% Items = mod_pubsub:pubsubItems() +%% @doc Returns the list of stored items for a given node. +%% <p>For the default PubSub module, items are stored in Mnesia database.</p> +%% <p>We can consider that the pubsub_item table have been created by the main +%% mod_pubsub module.</p> +%% <p>PubSub plugins can store the items where they wants (for example in a +%% relational database), or they can even decide not to persist any items.</p> +%% <p>If a PubSub plugin wants to delegate the item storage to the default node, +%% they can implement this function like this: +%% ```get_items(NodeId, From) -> +%% node_default:get_items(NodeId, From).'''</p> +get_items(NodeId, _From) -> + Items = mnesia:match_object(#pubsub_item{itemid = {'_', NodeId}, _ = '_'}), + {result, lists:reverse(lists:keysort(#pubsub_item.modification, Items))}. +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> + SubKey = jlib:jid_tolower(JID), + GenKey = jlib:jid_remove_resource(SubKey), + GenState = get_state(NodeId, GenKey), + SubState = get_state(NodeId, SubKey), + Affiliation = GenState#pubsub_state.affiliation, + Subscriptions = SubState#pubsub_state.subscriptions, + Whitelisted = can_fetch_item(Affiliation, Subscriptions), + if + %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + GenState#pubsub_state.affiliation == outcast -> + %% Requesting entity is blocked + {error, ?ERR_FORBIDDEN}; + (AccessModel == presence) and (not PresenceSubscription) -> + %% Entity is not authorized to create a subscription (presence subscription required) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "presence-subscription-required")}; + (AccessModel == roster) and (not RosterGroup) -> + %% Entity is not authorized to create a subscription (not in roster group) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "not-in-roster-group")}; + (AccessModel == whitelist) and (not Whitelisted) -> + %% Node has whitelist access model and entity lacks required affiliation + {error, ?ERR_EXTENDED(?ERR_NOT_ALLOWED, "closed-node")}; + (AccessModel == authorize) and (not Whitelisted) -> + %% Node has authorize access model + {error, ?ERR_FORBIDDEN}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_items(NodeId, JID) + end. + +%% @spec (NodeId, ItemId) -> [Item] | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% ItemId = string() +%% Item = mod_pubsub:pubsubItems() +%% @doc <p>Returns an item (one item list), given its reference.</p> +get_item(NodeId, ItemId) -> + case mnesia:read({pubsub_item, {ItemId, NodeId}}) of + [Item] when is_record(Item, pubsub_item) -> + {result, Item}; + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end. +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> + SubKey = jlib:jid_tolower(JID), + GenKey = jlib:jid_remove_resource(SubKey), + GenState = get_state(NodeId, GenKey), + Affiliation = GenState#pubsub_state.affiliation, + Subscriptions = GenState#pubsub_state.subscriptions, + Whitelisted = can_fetch_item(Affiliation, Subscriptions), + if + %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + GenState#pubsub_state.affiliation == outcast -> + %% Requesting entity is blocked + {error, ?ERR_FORBIDDEN}; + (AccessModel == presence) and (not PresenceSubscription) -> + %% Entity is not authorized to create a subscription (presence subscription required) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "presence-subscription-required")}; + (AccessModel == roster) and (not RosterGroup) -> + %% Entity is not authorized to create a subscription (not in roster group) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "not-in-roster-group")}; + (AccessModel == whitelist) and (not Whitelisted) -> + %% Node has whitelist access model and entity lacks required affiliation + {error, ?ERR_EXTENDED(?ERR_NOT_ALLOWED, "closed-node")}; + (AccessModel == authorize) and (not Whitelisted) -> + %% Node has authorize access model + {error, ?ERR_FORBIDDEN}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_item(NodeId, ItemId) + end. + +%% @spec (Item) -> ok | {error, Reason::stanzaError()} +%% Item = mod_pubsub:pubsubItems() +%% @doc <p>Write an item into database.</p> +set_item(Item) when is_record(Item, pubsub_item) -> + mnesia:write(Item); +set_item(_) -> + {error, ?ERR_INTERNAL_SERVER_ERROR}. + +%% @spec (NodeId, ItemId) -> ok | {error, Reason::stanzaError()} +%% NodeId = mod_pubsub:pubsubNodeId() +%% ItemId = string() +%% @doc <p>Delete an item from database.</p> +del_item(NodeId, ItemId) -> + mnesia:delete({pubsub_item, {ItemId, NodeId}}). +del_items(NodeId, ItemIds) -> + lists:foreach(fun(ItemId) -> + del_item(NodeId, ItemId) + end, ItemIds). + +%% @doc <p>Return the name of the node if known: Default is to return +%% node id.</p> +get_item_name(_Host, _Node, Id) -> + Id. + +node_to_path(Node) -> + string:tokens(binary_to_list(Node), "/"). + +path_to_node([]) -> + <<>>; +path_to_node(Path) -> + list_to_binary(string:join([""|Path], "/")). + +%% @spec (Affiliation, Subscription) -> true | false +%% Affiliation = owner | member | publisher | outcast | none +%% Subscription = subscribed | none +%% @doc Determines if the combination of Affiliation and Subscribed +%% are allowed to get items from a node. +can_fetch_item(owner, _) -> true; +can_fetch_item(member, _) -> true; +can_fetch_item(publisher, _) -> true; +can_fetch_item(outcast, _) -> false; +can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions); +can_fetch_item(_Affiliation, _Subscription) -> false. + +is_subscribed(Subscriptions) -> + lists:any(fun ({subscribed, _SubId}) -> true; + (_) -> false + end, Subscriptions). + +%% Returns the first item where Pred() is true in List +first_in_list(_Pred, []) -> + false; +first_in_list(Pred, [H | T]) -> + case Pred(H) of + true -> {value, H}; + _ -> first_in_list(Pred, T) + end. + diff --git a/src/mod_pubsub/node_hometree_odbc.erl b/src/mod_pubsub/node_hometree_odbc.erl new file mode 100644 index 000000000..141d3275c --- /dev/null +++ b/src/mod_pubsub/node_hometree_odbc.erl @@ -0,0 +1,1383 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% +%%% @copyright 2006-2009 ProcessOne +%%% @author Christophe Romain <christophe.romain@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @todo The item table should be handled by the plugin, but plugin that do +%%% not want to manage it should be able to use the default behaviour. +%%% @todo Plugin modules should be able to register to receive presence update +%%% send to pubsub. + +%%% @doc The module <strong>{@module}</strong> is the default PubSub plugin. +%%% <p>It is used as a default for all unknown PubSub node type. It can serve +%%% as a developer basis and reference to build its own custom pubsub node +%%% types.</p> +%%% <p>PubSub plugin nodes are using the {@link gen_node} behaviour.</p> +%%% <p><strong>The API isn't stabilized yet</strong>. The pubsub plugin +%%% development is still a work in progress. However, the system is already +%%% useable and useful as is. Please, send us comments, feedback and +%%% improvements.</p> + +-module(node_hometree_odbc). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-define(PUBSUB, mod_pubsub_odbc). + +-behaviour(gen_pubsub_node). + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/2, + delete_node/1, + purge_node/2, + subscribe_node/8, + unsubscribe_node/4, + publish_item/6, + delete_item/4, + remove_extra_items/3, + get_entity_affiliations/2, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, + get_entity_subscriptions/2, + get_entity_subscriptions_for_send_last/2, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, + set_state/1, + get_items/7, + get_items/6, + get_items/3, + get_items/2, + get_item/7, + get_item/2, + set_item/1, + get_item_name/3, + get_last_items/3, + path_to_node/1, + node_to_path/1 + ]). + +-export([ + decode_jid/1, + decode_node/1, + decode_affiliation/1, + decode_subscriptions/1, + encode_jid/1, + encode_affiliation/1, + encode_subscriptions/1 + ]). + +%% ================ +%% API definition +%% ================ + +%% @spec (Host, ServerHost, Opts) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = mod_pubsub:host() +%% Opts = list() +%% @doc <p>Called during pubsub modules initialisation. Any pubsub plugin must +%% implement this function. It can return anything.</p> +%% <p>This function is mainly used to trigger the setup task necessary for the +%% plugin. It can be used for example by the developer to create the specific +%% module database schema if it does not exists yet.</p> +init(_Host, _ServerHost, _Opts) -> + pubsub_subscription_odbc:init(), + ok. + +%% @spec (Host, ServerHost) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = host() +%% @doc <p>Called during pubsub modules termination. Any pubsub plugin must +%% implement this function. It can return anything.</p> +terminate(_Host, _ServerHost) -> + ok. + +%% @spec () -> [Option] +%% Option = mod_pubsub:nodeOption() +%% @doc Returns the default pubsub node options. +%% <p>Example of function return value:</p> +%% ``` +%% [{deliver_payloads, true}, +%% {notify_config, false}, +%% {notify_delete, false}, +%% {notify_retract, true}, +%% {persist_items, true}, +%% {max_items, 10}, +%% {subscribe, true}, +%% {access_model, open}, +%% {publish_model, publishers}, +%% {max_payload_size, 100000}, +%% {send_last_published_item, never}, +%% {presence_based_delivery, false}]''' +options() -> + [{deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, open}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, false}, + {odbc, true}, + {rsm, true}]. + +%% @spec () -> [] +%% @doc Returns the node features +features() -> + ["create-nodes", + "auto-create", + "access-authorize", + "delete-nodes", + "delete-items", + "get-pending", + "instant-nodes", + "manage-subscriptions", + "modify-affiliations", + "multi-subscribe", + "outcast-affiliation", + "persistent-items", + "publish", + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", + "retrieve-subscriptions", + "subscribe", + "subscription-notifications", + "subscription-options", + "rsm" + ]. + +%% @spec (Host, ServerHost, Node, ParentNode, Owner, Access) -> bool() +%% Host = mod_pubsub:host() +%% ServerHost = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% ParentNode = mod_pubsub:pubsubNode() +%% Owner = mod_pubsub:jid() +%% Access = all | atom() +%% @doc Checks if the current user has the permission to create the requested node +%% <p>In {@link node_default}, the permission is decided by the place in the +%% hierarchy where the user is creating the node. The access parameter is also +%% checked in the default module. This parameter depends on the value of the +%% <tt>access_createnode</tt> ACL value in ejabberd config file.</p> +%% <p>This function also check that node can be created a a children of its +%% parent node</p> +%% <p>PubSub plugins can redefine the PubSub node creation rights as they +%% which. They can simply delegate this check to the {@link node_default} +%% module by implementing this function like this: +%% ```check_create_user_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> +%% node_default:check_create_user_permission(Host, ServerHost, Node, ParentNode, Owner, Access).'''</p> +create_node_permission(Host, ServerHost, Node, _ParentNode, Owner, Access) -> + LOwner = jlib:jid_tolower(Owner), + {User, Server, _Resource} = LOwner, + Allowed = case LOwner of + {"", Host, ""} -> + true; % pubsub service always allowed + _ -> + case acl:match_rule(ServerHost, Access, LOwner) of + allow -> + case node_to_path(Node) of + ["home", Server, User | _] -> true; + _ -> false + end; + _ -> + false + end + end, + {result, Allowed}. + +%% @spec (NodeId, Owner) -> +%% {result, Result} | exit +%% NodeId = mod_pubsub:pubsubNodeId() +%% Owner = mod_pubsub:jid() +%% @doc <p></p> +create_node(NodeId, Owner) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + State = #pubsub_state{stateid = {OwnerKey, NodeId}, affiliation = owner}, + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values(", state_to_raw(NodeId, State), ");"]), + {result, {default, broadcast}}. + +%% @spec (Removed) -> ok +%% Removed = [mod_pubsub:pubsubNode()] +%% @doc <p>purge items of deleted nodes after effective deletion.</p> +delete_node(Removed) -> + Reply = lists:map( + fun(#pubsub_node{id = NodeId} = PubsubNode) -> + Subscriptions = case catch ejabberd_odbc:sql_query_t( + ["select jid, subscriptions " + "from pubsub_state " + "where nodeid='", NodeId, "';"]) of + {selected, ["jid", "subscriptions"], RItems} -> + lists:map(fun({SJID, Subscriptions}) -> + {decode_jid(SJID), decode_subscriptions(Subscriptions)} + end, RItems); + _ -> + [] + end, + %% state and item remove already done thanks to DELETE CASCADE + %% but here we get nothing in States, making notify_retract unavailable ! + %% TODO, remove DELETE CASCADE from schema + {PubsubNode, Subscriptions} + end, Removed), + {result, {default, broadcast, Reply}}. + +%% @spec (NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> +%% {error, Reason} | {result, Result} +%% @doc <p>Accepts or rejects subcription requests on a PubSub node.</p> +%% <p>The mechanism works as follow: +%% <ul> +%% <li>The main PubSub module prepares the subscription and passes the +%% result of the preparation as a record.</li> +%% <li>This function gets the prepared record and several other parameters and +%% can decide to:<ul> +%% <li>reject the subscription;</li> +%% <li>allow it as is, letting the main module perform the database +%% persistance;</li> +%% <li>allow it, modifying the record. The main module will store the +%% modified record;</li> +%% <li>allow it, but perform the needed persistance operations.</li></ul> +%% </li></ul></p> +%% <p>The selected behaviour depends on the return parameter: +%% <ul> +%% <li><tt>{error, Reason}</tt>: an IQ error result will be returned. No +%% subscription will actually be performed.</li> +%% <li><tt>true</tt>: Subscribe operation is allowed, based on the +%% unmodified record passed in parameter <tt>SubscribeResult</tt>. If this +%% parameter contains an error, no subscription will be performed.</li> +%% <li><tt>{true, PubsubState}</tt>: Subscribe operation is allowed, but +%% the {@link mod_pubsub:pubsubState()} record returned replaces the value +%% passed in parameter <tt>SubscribeResult</tt>.</li> +%% <li><tt>{true, done}</tt>: Subscribe operation is allowed, but the +%% {@link mod_pubsub:pubsubState()} will be considered as already stored and +%% no further persistance operation will be performed. This case is used, +%% when the plugin module is doing the persistance by itself or when it want +%% to completly disable persistance.</li></ul> +%% </p> +%% <p>In the default plugin module, the record is unchanged.</p> +subscribe_node(NodeId, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + SubKey = jlib:jid_tolower(Subscriber), + GenKey = jlib:jid_remove_resource(SubKey), + Authorized = (jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == GenKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), + Whitelisted = lists:member(Affiliation, [member, publisher, owner]), + PendingSubscription = lists:any(fun({pending, _}) -> true; + (_) -> false + end, Subscriptions), + if + not Authorized -> + %% JIDs do not match + {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "invalid-jid")}; + Affiliation == outcast -> + %% Requesting entity is blocked + {error, ?ERR_FORBIDDEN}; + PendingSubscription -> + %% Requesting entity has pending subscription + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "pending-subscription")}; + (AccessModel == presence) and (not PresenceSubscription) -> + %% Entity is not authorized to create a subscription (presence subscription required) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "presence-subscription-required")}; + (AccessModel == roster) and (not RosterGroup) -> + %% Entity is not authorized to create a subscription (not in roster group) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "not-in-roster-group")}; + (AccessModel == whitelist) and (not Whitelisted) -> + %% Node has whitelist access model and entity lacks required affiliation + {error, ?ERR_EXTENDED(?ERR_NOT_ALLOWED, "closed-node")}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + %%ForbiddenAnonymous -> + %% % Requesting entity is anonymous + %% {error, ?ERR_FORBIDDEN}; + true -> + case pubsub_subscription_odbc:subscribe_node(Subscriber, NodeId, Options) of + {result, SubId} -> + NewSub = case AccessModel of + authorize -> pending; + _ -> subscribed + end, + update_subscription(NodeId, SubKey, [{NewSub, SubId} | Subscriptions]), + case {NewSub, SendLast} of + {subscribed, never} -> + {result, {default, subscribed, SubId}}; + {subscribed, _} -> + {result, {default, subscribed, SubId, send_last}}; + {_, _} -> + {result, {default, pending, SubId}} + end; + _ -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end + end. + +%% @spec (NodeId, Sender, Subscriber, SubId) -> +%% {error, Reason} | {result, []} +%% NodeId = mod_pubsub:pubsubNodeId() +%% Sender = mod_pubsub:jid() +%% Subscriber = mod_pubsub:jid() +%% SubId = mod_pubsub:subid() +%% Reason = mod_pubsub:stanzaError() +%% @doc <p>Unsubscribe the <tt>Subscriber</tt> from the <tt>Node</tt>.</p> +unsubscribe_node(NodeId, Sender, Subscriber, SubId) -> + SubKey = jlib:jid_tolower(Subscriber), + GenKey = jlib:jid_remove_resource(SubKey), + Authorized = (jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == GenKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, SubKey), + SubIdExists = case SubId of + [] -> false; + List when is_list(List) -> true; + _ -> false + end, + if + %% Requesting entity is prohibited from unsubscribing entity + not Authorized -> + {error, ?ERR_FORBIDDEN}; + %% Entity did not specify SubId + %%SubId == "", ?? -> + %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %% Invalid subscription identifier + %%InvalidSubId -> + %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + %% Requesting entity is not a subscriber + Subscriptions == [] -> + {error, ?ERR_EXTENDED(?ERR_UNEXPECTED_REQUEST, "not-subscribed")}; + %% Subid supplied, so use that. + SubIdExists -> + Sub = first_in_list(fun(S) -> + case S of + {_Sub, SubId} -> true; + _ -> false + end + end, Subscriptions), + case Sub of + {value, S} -> + delete_subscription(SubKey, NodeId, S, Affiliation, Subscriptions), + {result, default}; + false -> + {error, ?ERR_EXTENDED(?ERR_UNEXPECTED_REQUEST, "not-subscribed")} + end; + %% No subid supplied, but there's only one matching + %% subscription, so use that. + length(Subscriptions) == 1 -> + delete_subscription(SubKey, NodeId, hd(Subscriptions), Affiliation, Subscriptions), + {result, default}; + %% No subid and more than one possible subscription match. + true -> + {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")} + end. + +delete_subscription(SubKey, NodeId, {Subscription, SubId}, Affiliation, Subscriptions) -> + NewSubs = Subscriptions -- [{Subscription, SubId}], + pubsub_subscription_odbc:unsubscribe_node(SubKey, NodeId, SubId), + case {Affiliation, NewSubs} of + {none, []} -> + % Just a regular subscriber, and this is final item, so + % delete the state. + del_state(NodeId, SubKey); + _ -> + update_subscription(NodeId, SubKey, NewSubs) + end. + +%% @spec (NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload) -> +%% {true, PubsubItem} | {result, Reply} +%% NodeId = mod_pubsub:pubsubNodeId() +%% Publisher = mod_pubsub:jid() +%% PublishModel = atom() +%% MaxItems = integer() +%% ItemId = string() +%% Payload = term() +%% @doc <p>Publishes the item passed as parameter.</p> +%% <p>The mechanism works as follow: +%% <ul> +%% <li>The main PubSub module prepares the item to publish and passes the +%% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.</li> +%% <li>This function gets the prepared record and several other parameters and can decide to:<ul> +%% <li>reject the publication;</li> +%% <li>allow the publication as is, letting the main module perform the database persistance;</li> +%% <li>allow the publication, modifying the record. The main module will store the modified record;</li> +%% <li>allow it, but perform the needed persistance operations.</li></ul> +%% </li></ul></p> +%% <p>The selected behaviour depends on the return parameter: +%% <ul> +%% <li><tt>{error, Reason}</tt>: an iq error result will be return. No +%% publication is actually performed.</li> +%% <li><tt>true</tt>: Publication operation is allowed, based on the +%% unmodified record passed in parameter <tt>Item</tt>. If the <tt>Item</tt> +%% parameter contains an error, no subscription will actually be +%% performed.</li> +%% <li><tt>{true, Item}</tt>: Publication operation is allowed, but the +%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed +%% in parameter <tt>Item</tt>. The persistance will be performed by the main +%% module.</li> +%% <li><tt>{true, done}</tt>: Publication operation is allowed, but the +%% {@link mod_pubsub:pubsubItem()} will be considered as already stored and +%% no further persistance operation will be performed. This case is used, +%% when the plugin module is doing the persistance by itself or when it want +%% to completly disable persistance.</li></ul> +%% </p> +%% <p>In the default plugin module, the record is unchanged.</p> +publish_item(NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload) -> + SubKey = jlib:jid_tolower(Publisher), + GenKey = jlib:jid_remove_resource(SubKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), + Subscribed = case PublishModel of + subscribers -> is_subscribed(Subscriptions); + _ -> undefined + end, + if + not ((PublishModel == open) + or ((PublishModel == publishers) + and ((Affiliation == owner) or (Affiliation == publisher))) + or (Subscribed == true)) -> + %% Entity does not have sufficient privileges to publish to node + {error, ?ERR_FORBIDDEN}; + true -> + %% TODO: check creation, presence, roster + if MaxItems > 0 -> + %% Note: this works cause set_item tries an update before + %% the insert, and the update just ignore creation field. + PubId = {now(), SubKey}, + set_item(#pubsub_item{itemid = {ItemId, NodeId}, + creation = {now(), GenKey}, + modification = PubId, + payload = Payload}), + Items = [ItemId | itemids(NodeId, GenKey)--[ItemId]], + {result, {_, OI}} = remove_extra_items(NodeId, MaxItems, Items), + %% set new item list use useless + {result, {default, broadcast, OI}}; + true -> + {result, {default, broadcast, []}} + end + end. + +%% @spec (NodeId, MaxItems, ItemIds) -> {NewItemIds,OldItemIds} +%% NodeId = mod_pubsub:pubsubNodeId() +%% MaxItems = integer() | unlimited +%% ItemIds = [ItemId::string()] +%% NewItemIds = [ItemId::string()] +%% @doc <p>This function is used to remove extra items, most notably when the +%% maximum number of items has been reached.</p> +%% <p>This function is used internally by the core PubSub module, as no +%% permission check is performed.</p> +%% <p>In the default plugin module, the oldest items are removed, but other +%% rules can be used.</p> +%% <p>If another PubSub plugin wants to delegate the item removal (and if the +%% plugin is using the default pubsub storage), it can implements this function like this: +%% ```remove_extra_items(NodeId, MaxItems, ItemIds) -> +%% node_default:remove_extra_items(NodeId, MaxItems, ItemIds).'''</p> +remove_extra_items(_NodeId, unlimited, ItemIds) -> + {result, {ItemIds, []}}; +remove_extra_items(NodeId, MaxItems, ItemIds) -> + NewItems = lists:sublist(ItemIds, MaxItems), + OldItems = lists:nthtail(length(NewItems), ItemIds), + %% Remove extra items: + del_items(NodeId, OldItems), + %% Return the new items list: + {result, {NewItems, OldItems}}. + +%% @spec (NodeId, Publisher, PublishModel, ItemId) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% NodeId = mod_pubsub:pubsubNodeId() +%% Publisher = mod_pubsub:jid() +%% PublishModel = atom() +%% ItemId = string() +%% @doc <p>Triggers item deletion.</p> +%% <p>Default plugin: The user performing the deletion must be the node owner +%% or a publisher.</p> +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + SubKey = jlib:jid_tolower(Publisher), + GenKey = jlib:jid_remove_resource(SubKey), + {result, Affiliation} = get_affiliation(NodeId, GenKey), + Allowed = (Affiliation == publisher) orelse (Affiliation == owner) + orelse (PublishModel == open) + orelse case get_item(NodeId, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}}} -> true; + _ -> false + end, + if + not Allowed -> + %% Requesting entity does not have sufficient privileges + {error, ?ERR_FORBIDDEN}; + true -> + case del_item(NodeId, ItemId) of + {updated, 1} -> + %% set new item list use useless + {result, {default, broadcast}}; + _ -> + %% Non-existent node or item + {error, ?ERR_ITEM_NOT_FOUND} + end + end. + +%% @spec (NodeId, Owner) -> +%% {error, Reason::stanzaError()} | +%% {result, {default, broadcast}} +%% NodeId = mod_pubsub:pubsubNodeId() +%% Owner = mod_pubsub:jid() +purge_node(NodeId, Owner) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + GenState = get_state(NodeId, GenKey), + case GenState of + #pubsub_state{affiliation = owner} -> + {result, States} = get_states(NodeId), + lists:foreach( + fun(#pubsub_state{items = []}) -> ok; + (#pubsub_state{items = Items}) -> del_items(NodeId, Items) + end, States), + {result, {default, broadcast}}; + _ -> + %% Entity is not owner + {error, ?ERR_FORBIDDEN} + end. + +%% @spec (Host, JID) -> [{Node,Affiliation}] +%% Host = host() +%% JID = mod_pubsub:jid() +%% @doc <p>Return the current affiliations for the given user</p> +%% <p>The default module reads affiliations in the main Mnesia +%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same +%% table, it should return an empty list, as the affiliation will be read by +%% the default PubSub module. Otherwise, it should return its own affiliation, +%% that will be added to the affiliation stored in the main +%% <tt>pubsub_state</tt> table.</p> +get_entity_affiliations(Host, Owner) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + H = ?PUBSUB:escape(Host), + J = encode_jid(GenKey), + Reply = case catch ejabberd_odbc:sql_query_t( + ["select node, type, i.nodeid, affiliation " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid " + "and jid='", J, "' " + "and host='", H, "';"]) of + {selected, ["node", "type", "nodeid", "affiliation"], RItems} -> + lists:map(fun({N, T, I, A}) -> + Node = nodetree_tree_odbc:raw_to_node(Host, {N, "", T, I}), + {Node, decode_affiliation(A)} + end, RItems); + _ -> + [] + end, + {result, Reply}. + +get_node_affiliations(NodeId) -> + Reply = case catch ejabberd_odbc:sql_query_t( + ["select jid, affiliation " + "from pubsub_state " + "where nodeid='", NodeId, "';"]) of + {selected, ["jid", "affiliation"], RItems} -> + lists:map(fun({J, A}) -> {decode_jid(J), decode_affiliation(A)} end, RItems); + _ -> + [] + end, + {result, Reply}. + +get_affiliation(NodeId, Owner) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + J = encode_jid(GenKey), + Reply = case catch ejabberd_odbc:sql_query_t( + ["select affiliation from pubsub_state " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {selected, ["affiliation"], [{A}]} -> decode_affiliation(A); + _ -> none + end, + {result, Reply}. + +set_affiliation(NodeId, Owner, Affiliation) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + {_, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey), + case {Affiliation, Subscriptions} of + {none, none} -> + del_state(NodeId, GenKey); + _ -> + update_affiliation(NodeId, GenKey, Affiliation) + end. + +%% @spec (Host, Owner) -> [{Node,Subscription}] +%% Host = host() +%% Owner = mod_pubsub:jid() +%% @doc <p>Return the current subscriptions for the given user</p> +%% <p>The default module reads subscriptions in the main Mnesia +%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same +%% table, it should return an empty list, as the affiliation will be read by +%% the default PubSub module. Otherwise, it should return its own affiliation, +%% that will be added to the affiliation stored in the main +%% <tt>pubsub_state</tt> table.</p> +get_entity_subscriptions(Host, Owner) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + H = ?PUBSUB:escape(Host), + SJ = encode_jid(SubKey), + GJ = encode_jid(GenKey), + Query = case SubKey of + GenKey -> + ["select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid " + "and jid like '", GJ, "%' " + "and host='", H, "';"]; + _ -> + ["select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid " + "and jid in ('", SJ, "', '", GJ, "') " + "and host='", H, "';"] + end, + Reply = case catch ejabberd_odbc:sql_query_t(Query) of + {selected, ["node", "type", "nodeid", "jid", "subscriptions"], RItems} -> + lists:foldl(fun({N, T, I, J, S}, Acc) -> + Node = nodetree_tree_odbc:raw_to_node(Host, {N, "", T, I}), + Jid = decode_jid(J), + case decode_subscriptions(S) of + [] -> + [{Node, none, Jid}|Acc]; + Subs -> + lists:foldl(fun({Sub, SubId}, Acc2) -> [{Node, Sub, SubId, Jid}|Acc2]; + (Sub, Acc2) -> [{Node, Sub, Jid}|Acc2] + end, Acc, Subs) + end + end, [], RItems); + _ -> + [] + end, + {result, Reply}. + +%% do the same as get_entity_subscriptions but filter result only to +%% nodes having send_last_published_item=on_sub_and_presence +%% as this call avoid seeking node, it must return node and type as well +get_entity_subscriptions_for_send_last(Host, Owner) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + H = ?PUBSUB:escape(Host), + SJ = encode_jid(SubKey), + GJ = encode_jid(GenKey), + Query = case SubKey of + GenKey -> + ["select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid " + "and name='send_last_published_item' and val='on_sub_and_presence' " + "and jid like '", GJ, "%' " + "and host='", H, "';"]; + _ -> + ["select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid " + "and name='send_last_published_item' and val='on_sub_and_presence' " + "and jid in ('", SJ, "', '", GJ, "') " + "and host='", H, "';"] + end, + Reply = case catch ejabberd_odbc:sql_query_t(Query) of + {selected, ["node", "type", "nodeid", "jid", "subscriptions"], RItems} -> + lists:foldl(fun({N, T, I, J, S}, Acc) -> + Node = nodetree_tree_odbc:raw_to_node(Host, {N, "", T, I}), + Jid = decode_jid(J), + case decode_subscriptions(S) of + [] -> + [{Node, none, Jid}|Acc]; + Subs -> + lists:foldl(fun({Sub, SubId}, Acc2) -> [{Node, Sub, SubId, Jid}|Acc2]; + (Sub, Acc2) -> [{Node, Sub, Jid}|Acc2] + end, Acc, Subs) + end + end, [], RItems); + _ -> + [] + end, + {result, Reply}. + +get_node_subscriptions(NodeId) -> + Reply = case catch ejabberd_odbc:sql_query_t( + ["select jid, subscriptions " + "from pubsub_state " + "where nodeid='", NodeId, "';"]) of + {selected, ["jid", "subscriptions"], RItems} -> + lists:foldl(fun({J, S}, Acc) -> + Jid = decode_jid(J), + case decode_subscriptions(S) of + [] -> + [{Jid, none}|Acc]; + Subs -> + lists:foldl(fun({Sub, SubId}, Acc2) -> [{Jid, Sub, SubId}|Acc2]; + (Sub, Acc2) -> [{Jid, Sub}|Acc2] + end, Acc, Subs) + end + end, [], RItems); + _ -> + [] + end, + {result, Reply}. + +get_subscriptions(NodeId, Owner) -> + SubKey = jlib:jid_tolower(Owner), + J = encode_jid(SubKey), + Reply = case catch ejabberd_odbc:sql_query_t( + ["select subscriptions from pubsub_state " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {selected, ["subscriptions"], [{S}]} -> decode_subscriptions(S); + _ -> [] + end, + {result, Reply}. + +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + SubKey = jlib:jid_tolower(Owner), + SubState = get_state_without_itemids(NodeId, SubKey), + case {SubId, SubState#pubsub_state.subscriptions} of + {_, []} -> + case Subscription of + none -> {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "not-subscribed")}; + _ -> new_subscription(NodeId, Owner, Subscription, SubState) + end; + {"", [{_, SID}]} -> + case Subscription of + none -> unsub_with_subid(NodeId, SID, SubState); + _ -> replace_subscription({Subscription, SID}, SubState) + end; + {"", [_|_]} -> + {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + _ -> + case Subscription of + none -> unsub_with_subid(NodeId, SubId, SubState); + _ -> replace_subscription({Subscription, SubId}, SubState) + end + end. + +replace_subscription(NewSub, SubState) -> + NewSubs = replace_subscription(NewSub, + SubState#pubsub_state.subscriptions, []), + set_state(SubState#pubsub_state{subscriptions = NewSubs}). + +replace_subscription(_, [], Acc) -> + Acc; +replace_subscription({Sub, SubId}, [{_, SubID} | T], Acc) -> + replace_subscription({Sub, SubId}, T, [{Sub, SubID} | Acc]). + +new_subscription(NodeId, Owner, Subscription, SubState) -> + case pubsub_subscription_odbc:subscribe_node(Owner, NodeId, []) of + {result, SubId} -> + Subscriptions = SubState#pubsub_state.subscriptions, + set_state(SubState#pubsub_state{subscriptions = [{Subscription, SubId} | Subscriptions]}), + {Subscription, SubId}; + _ -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + +unsub_with_subid(NodeId, SubId, SubState) -> + pubsub_subscription_odbc:unsubscribe_node(SubState#pubsub_state.stateid, + NodeId, SubId), + NewSubs = lists:filter(fun ({_, SID}) -> SubId =/= SID end, + SubState#pubsub_state.subscriptions), + case {NewSubs, SubState#pubsub_state.affiliation} of + {[], none} -> + del_state(NodeId, element(1, SubState#pubsub_state.stateid)); + _ -> + set_state(SubState#pubsub_state{subscriptions = NewSubs}) + end. + + +%% @spec (Host, Owner) -> {result, [Node]} | {error, Reason} +%% Host = host() +%% Owner = jid() +%% Node = pubsubNode() +%% @doc <p>Returns a list of Owner's nodes on Host with pending +%% subscriptions.</p> +get_pending_nodes(Host, Owner) -> + GenKey = jlib:jid_remove_resource(jlib:jid_tolower(Owner)), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, + affiliation = owner, + _ = '_'}), + NodeIDs = [ID || #pubsub_state{stateid = {_, ID}} <- States], + NodeTree = case catch ets:lookup(gen_mod:get_module_proc(Host, config), nodetree) of + [{nodetree, N}] -> N; + _ -> nodetree_tree_odbc + end, + Reply = mnesia:foldl(fun(#pubsub_state{stateid = {_, NID}} = S, Acc) -> + case lists:member(NID, NodeIDs) of + true -> + case get_nodes_helper(NodeTree, S) of + {value, Node} -> [Node | Acc]; + false -> Acc + end; + false -> + Acc + end + end, [], pubsub_state), + {result, Reply}. + +get_nodes_helper(NodeTree, + #pubsub_state{stateid = {_, N}, subscriptions = Subs}) -> + HasPending = fun ({pending, _}) -> true; + (pending) -> true; + (_) -> false + end, + case lists:any(HasPending, Subs) of + true -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {_, Node}} -> + {value, Node}; + _ -> + false + end; + false -> + false + end. + +%% @spec (NodeId) -> [States] | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% @doc Returns the list of stored states for a given node. +%% <p>For the default PubSub module, states are stored in Mnesia database.</p> +%% <p>We can consider that the pubsub_state table have been created by the main +%% mod_pubsub module.</p> +%% <p>PubSub plugins can store the states where they wants (for example in a +%% relational database).</p> +%% <p>If a PubSub plugin wants to delegate the states storage to the default node, +%% they can implement this function like this: +%% ```get_states(NodeId) -> +%% node_default:get_states(NodeId).'''</p> +get_states(NodeId) -> + case catch ejabberd_odbc:sql_query_t( + ["select jid, affiliation, subscriptions " + "from pubsub_state " + "where nodeid='", NodeId, "';"]) of + {selected, ["jid", "affiliation", "subscriptions"], RItems} -> + {result, lists:map(fun({SJID, Affiliation, Subscriptions}) -> + #pubsub_state{stateid = {decode_jid(SJID), NodeId}, + items = itemids(NodeId, SJID), + affiliation = decode_affiliation(Affiliation), + subscriptions = decode_subscriptions(Subscriptions)} + end, RItems)}; + _ -> + {result, []} + end. + +%% @spec (NodeId, JID) -> [State] | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% JID = mod_pubsub:jid() +%% State = mod_pubsub:pubsubItems() +%% @doc <p>Returns a state (one state list), given its reference.</p> +get_state(NodeId, JID) -> + State = get_state_without_itemids(NodeId, JID), + {SJID, _} = State#pubsub_state.stateid, + State#pubsub_state{items = itemids(NodeId, SJID)}. +get_state_without_itemids(NodeId, JID) -> + J = encode_jid(JID), + case catch ejabberd_odbc:sql_query_t( + ["select jid, affiliation, subscriptions " + "from pubsub_state " + "where jid='", J, "' " + "and nodeid='", NodeId, "';"]) of + {selected, ["jid", "affiliation", "subscriptions"], [{SJID, Affiliation, Subscriptions}]} -> + #pubsub_state{stateid = {decode_jid(SJID), NodeId}, + affiliation = decode_affiliation(Affiliation), + subscriptions = decode_subscriptions(Subscriptions)}; + _ -> + #pubsub_state{stateid={JID, NodeId}} + end. + +%% @spec (State) -> ok | {error, Reason::stanzaError()} +%% State = mod_pubsub:pubsubStates() +%% @doc <p>Write a state into database.</p> +set_state(State) -> + {_, NodeId} = State#pubsub_state.stateid, + set_state(NodeId, State). +set_state(NodeId, State) -> + %% NOTE: in odbc version, as we do not handle item list, + %% we just need to update affiliation and subscription + %% cause {JID,NodeId} is the key. if it does not exists, then we insert it. + %% MySQL can be optimized using INSERT ... ON DUPLICATE KEY as well + {JID, _} = State#pubsub_state.stateid, + J = encode_jid(JID), + S = encode_subscriptions(State#pubsub_state.subscriptions), + A = encode_affiliation(State#pubsub_state.affiliation), + case catch ejabberd_odbc:sql_query_t( + ["update pubsub_state " + "set subscriptions='", S, "', affiliation='", A, "' " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {updated, 1} -> + ok; + _ -> + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values('", NodeId, "', '", J, "', '", A, "', '", S, "');"]) + end, + {result, []}. + +%% @spec (NodeId, JID) -> ok | {error, Reason::stanzaError()} +%% NodeId = mod_pubsub:pubsubNodeId() +%% JID = mod_pubsub:jid() +%% @doc <p>Delete a state from database.</p> +del_state(NodeId, JID) -> + J = encode_jid(JID), + catch ejabberd_odbc:sql_query_t( + ["delete from pubsub_state " + "where jid='", J, "' " + "and nodeid='", NodeId, "';"]), + ok. + +%% @spec (NodeId, From) -> {[Items],RsmOut} | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% Items = mod_pubsub:pubsubItems() +%% @doc Returns the list of stored items for a given node. +%% <p>For the default PubSub module, items are stored in Mnesia database.</p> +%% <p>We can consider that the pubsub_item table have been created by the main +%% mod_pubsub module.</p> +%% <p>PubSub plugins can store the items where they wants (for example in a +%% relational database), or they can even decide not to persist any items.</p> +%% <p>If a PubSub plugin wants to delegate the item storage to the default node, +%% they can implement this function like this: +%% ```get_items(NodeId, From) -> +%% node_default:get_items(NodeId, From).'''</p> +get_items(NodeId, _From) -> + case catch ejabberd_odbc:sql_query_t( + ["select itemid, publisher, creation, modification, payload " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "order by modification desc;"]) of + {selected, ["itemid", "publisher", "creation", "modification", "payload"], RItems} -> + {result, lists:map(fun(RItem) -> raw_to_item(NodeId, RItem) end, RItems)}; + _ -> + {result, []} + end. +get_items(NodeId, From, none) -> + MaxItems = case catch ejabberd_odbc:sql_query_t( + ["select val from pubsub_node_option " + "where nodeid='", NodeId, "' " + "and name='max_items';"]) of + {selected, ["val"], [{Value}]} -> + Tokens = element(2, erl_scan:string(Value++".")), + element(2, erl_parse:parse_term(Tokens)); + _ -> + ?MAXITEMS + end, + get_items(NodeId, From, #rsm_in{max=MaxItems}); +get_items(NodeId, _From, #rsm_in{max=M, direction=Direction, id=I, index=IncIndex})-> + Max = ?PUBSUB:escape(i2l(M)), + + {Way, Order} = case Direction of + aft -> {"<", "desc"}; + before when I == [] -> {"is not", "asc"}; + before -> {">", "asc"}; + _ when IncIndex =/= undefined -> {"<", "desc"}; % using index + _ -> {"is not", "desc"}% Can be better + end, + [AttrName, Id] = case I of + undefined when IncIndex =/= undefined -> + case catch ejabberd_odbc:sql_query_t( + ["select modification from pubsub_item pi " + "where exists ( " + "select count(*) as count1 " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "and modification > pi.modification " + "having count1 = ",?PUBSUB:escape(i2l(IncIndex))," );"]) of + {selected, [_], [{O}]} -> ["modification", "'"++O++"'"]; + _ -> ["modification", "null"] + end; + undefined -> ["modification", "null"]; + [] -> ["modification", "null"]; + I -> [A, B] = string:tokens(?PUBSUB:escape(i2l(I)), "@"), + [A, "'"++B++"'"] + end, + Count= case catch ejabberd_odbc:sql_query_t( + ["select count(*) " + "from pubsub_item " + "where nodeid='", NodeId, "';"]) of + {selected, [_], [{C}]} -> C; + _ -> "0" + end, + + case catch ejabberd_odbc:sql_query_t( + ["select itemid, publisher, creation, modification, payload " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "and ", AttrName," ", Way, " ", Id, " " + "order by ", AttrName," ", Order," limit ", i2l(Max)," ;"]) of + {selected, ["itemid", "publisher", "creation", "modification", "payload"], RItems} -> + case length(RItems) of + 0 -> {result, {[], #rsm_out{count=Count}}}; + _ -> + {_, _, _, F, _} = hd(RItems), + Index = case catch ejabberd_odbc:sql_query_t( + ["select count(*) " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "and ", AttrName," > '", F, "';"]) of + %{selected, [_], [{C}, {In}]} -> [string:strip(C, both, $"), string:strip(In, both, $")]; + {selected, [_], [{In}]} -> In; + _ -> "0" + end, + %{F, _} = string:to_integer(FStr), + {_, _, _, L, _} = lists:last(RItems), + RsmOut = #rsm_out{count=Count, index=Index, first="modification@"++F, last="modification@"++i2l(L)}, + {result, {lists:map(fun(RItem) -> raw_to_item(NodeId, RItem) end, RItems), RsmOut}} + end; + _ -> + {result, {[], none}} + end. + +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, none). +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> + SubKey = jlib:jid_tolower(JID), + GenKey = jlib:jid_remove_resource(SubKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), + Whitelisted = can_fetch_item(Affiliation, Subscriptions), + if + %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + Affiliation == outcast -> + %% Requesting entity is blocked + {error, ?ERR_FORBIDDEN}; + (AccessModel == presence) and (not PresenceSubscription) -> + %% Entity is not authorized to create a subscription (presence subscription required) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "presence-subscription-required")}; + (AccessModel == roster) and (not RosterGroup) -> + %% Entity is not authorized to create a subscription (not in roster group) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "not-in-roster-group")}; + (AccessModel == whitelist) and (not Whitelisted) -> + %% Node has whitelist access model and entity lacks required affiliation + {error, ?ERR_EXTENDED(?ERR_NOT_ALLOWED, "closed-node")}; + (AccessModel == authorize) and (not Whitelisted) -> + %% Node has authorize access model + {error, ?ERR_FORBIDDEN}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_items(NodeId, JID, RSM) + end. + +get_last_items(NodeId, _From, Count) -> + case catch ejabberd_odbc:sql_query_t( + ["select itemid, publisher, creation, modification, payload " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "order by modification desc limit ", i2l(Count), ";"]) of + {selected, ["itemid", "publisher", "creation", "modification", "payload"], RItems} -> + {result, lists:map(fun(RItem) -> raw_to_item(NodeId, RItem) end, RItems)}; + _ -> + {result, []} + end. + +%% @spec (NodeId, ItemId) -> [Item] | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% ItemId = string() +%% Item = mod_pubsub:pubsubItems() +%% @doc <p>Returns an item (one item list), given its reference.</p> +get_item(NodeId, ItemId) -> + I = ?PUBSUB:escape(ItemId), + case catch ejabberd_odbc:sql_query_t( + ["select itemid, publisher, creation, modification, payload " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "and itemid='", I,"';"]) of + {selected, ["itemid", "publisher", "creation", "modification", "payload"], [RItem]} -> + {result, raw_to_item(NodeId, RItem)}; + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end. +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> + SubKey = jlib:jid_tolower(JID), + GenKey = jlib:jid_remove_resource(SubKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), + Whitelisted = can_fetch_item(Affiliation, Subscriptions), + if + %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + Affiliation == outcast -> + %% Requesting entity is blocked + {error, ?ERR_FORBIDDEN}; + (AccessModel == presence) and (not PresenceSubscription) -> + %% Entity is not authorized to create a subscription (presence subscription required) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "presence-subscription-required")}; + (AccessModel == roster) and (not RosterGroup) -> + %% Entity is not authorized to create a subscription (not in roster group) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "not-in-roster-group")}; + (AccessModel == whitelist) and (not Whitelisted) -> + %% Node has whitelist access model and entity lacks required affiliation + {error, ?ERR_EXTENDED(?ERR_NOT_ALLOWED, "closed-node")}; + (AccessModel == authorize) and (not Whitelisted) -> + %% Node has authorize access model + {error, ?ERR_FORBIDDEN}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_item(NodeId, ItemId) + end. + +%% @spec (Item) -> ok | {error, Reason::stanzaError()} +%% Item = mod_pubsub:pubsubItems() +%% @doc <p>Write an item into database.</p> +set_item(Item) -> + {ItemId, NodeId} = Item#pubsub_item.itemid, + I = ?PUBSUB:escape(ItemId), + {C, _} = Item#pubsub_item.creation, + {M, JID} = Item#pubsub_item.modification, + P = encode_jid(JID), + Payload = Item#pubsub_item.payload, + XML = ?PUBSUB:escape(lists:flatten(lists:map(fun(X) -> xml:element_to_string(X) end, Payload))), + S = fun({T1, T2, T3}) -> + lists:flatten([i2l(T1, 6), ":", i2l(T2, 6), ":", i2l(T3, 6)]) + end, + case catch ejabberd_odbc:sql_query_t( + ["update pubsub_item " + "set publisher='", P, "', modification='", S(M), "', payload='", XML, "' " + "where nodeid='", NodeId, "' and itemid='", I, "';"]) of + {updated, 1} -> + ok; + _ -> + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_item " + "(nodeid, itemid, publisher, creation, modification, payload) " + "values('", NodeId, "', '", I, "', '", P, "', '", S(C), "', '", S(M), "', '", XML, "');"]) + end, + {result, []}. + +%% @spec (NodeId, ItemId) -> ok | {error, Reason::stanzaError()} +%% NodeId = mod_pubsub:pubsubNodeId() +%% ItemId = string() +%% @doc <p>Delete an item from database.</p> +del_item(NodeId, ItemId) -> + I = ?PUBSUB:escape(ItemId), + catch ejabberd_odbc:sql_query_t( + ["delete from pubsub_item " + "where itemid='", I, "' " + "and nodeid='", NodeId, "';"]). +del_items(_, []) -> + ok; +del_items(NodeId, [ItemId]) -> + del_item(NodeId, ItemId); +del_items(NodeId, ItemIds) -> + I = string:join([["'", ?PUBSUB:escape(X), "'"] || X <- ItemIds], ","), + catch ejabberd_odbc:sql_query_t( + ["delete from pubsub_item " + "where itemid in (", I, ") " + "and nodeid='", NodeId, "';"]). + +%% @doc <p>Return the name of the node if known: Default is to return +%% node id.</p> +get_item_name(_Host, _Node, Id) -> + Id. + +node_to_path(Node) -> + string:tokens(binary_to_list(Node), "/"). + +path_to_node([]) -> + <<>>; +path_to_node(Path) -> + list_to_binary(string:join([""|Path], "/")). + +%% @spec (Affiliation, Subscription) -> true | false +%% Affiliation = owner | member | publisher | outcast | none +%% Subscription = subscribed | none +%% @doc Determines if the combination of Affiliation and Subscribed +%% are allowed to get items from a node. +can_fetch_item(owner, _) -> true; +can_fetch_item(member, _) -> true; +can_fetch_item(publisher, _) -> true; +can_fetch_item(outcast, _) -> false; +can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions); +can_fetch_item(_Affiliation, _Subscription) -> false. + +is_subscribed(Subscriptions) -> + lists:any(fun ({subscribed, _SubId}) -> true; + (_) -> false + end, Subscriptions). + +%% Returns the first item where Pred() is true in List +first_in_list(_Pred, []) -> + false; +first_in_list(Pred, [H | T]) -> + case Pred(H) of + true -> {value, H}; + _ -> first_in_list(Pred, T) + end. + +itemids(NodeId, {U, S, R}) -> + itemids(NodeId, encode_jid({U, S, R})); +itemids(NodeId, SJID) -> + case catch ejabberd_odbc:sql_query_t( + ["select itemid " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "and publisher like '", SJID, "%' " + "order by modification desc;"]) of + {selected, ["itemid"], RItems} -> + lists:map(fun({ItemId}) -> ItemId end, RItems); + _ -> + [] + end. + +select_affiliation_subscriptions(NodeId, JID) -> + J = encode_jid(JID), + case catch ejabberd_odbc:sql_query_t( + ["select affiliation,subscriptions from pubsub_state " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {selected, ["affiliation", "subscriptions"], [{A, S}]} -> + {decode_affiliation(A), decode_subscriptions(S)}; + _ -> + {none, []} + end. +select_affiliation_subscriptions(NodeId, JID, JID) -> + select_affiliation_subscriptions(NodeId, JID); +select_affiliation_subscriptions(NodeId, GenKey, SubKey) -> + {result, Affiliation} = get_affiliation(NodeId, GenKey), + {result, Subscriptions} = get_subscriptions(NodeId, SubKey), + {Affiliation, Subscriptions}. + +update_affiliation(NodeId, JID, Affiliation) -> + J = encode_jid(JID), + A = encode_affiliation(Affiliation), + case catch ejabberd_odbc:sql_query_t( + ["update pubsub_state " + "set affiliation='", A, "' " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {updated, 1} -> + ok; + _ -> + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values('", NodeId, "', '", J, "', '", A, "', '');"]) + end. + +update_subscription(NodeId, JID, Subscription) -> + J = encode_jid(JID), + S = encode_subscriptions(Subscription), + case catch ejabberd_odbc:sql_query_t( + ["update pubsub_state " + "set subscriptions='", S, "' " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {updated, 1} -> + ok; + _ -> + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values('", NodeId, "', '", J, "', 'n', '", S, "');"]) + end. + +decode_jid(SJID) -> jlib:jid_tolower(jlib:string_to_jid(SJID)). + +decode_node(N) -> ?PUBSUB:string_to_node(N). + +decode_affiliation("o") -> owner; +decode_affiliation("p") -> publisher; +decode_affiliation("m") -> member; +decode_affiliation("c") -> outcast; +decode_affiliation(_) -> none. + +decode_subscription("s") -> subscribed; +decode_subscription("p") -> pending; +decode_subscription("u") -> unconfigured; +decode_subscription(_) -> none. +decode_subscriptions(Subscriptions) -> + lists:foldl(fun(Subscription, Acc) -> + case string:tokens(Subscription, ":") of + [S, SubId] -> [{decode_subscription(S), SubId}|Acc]; + _ -> Acc + end + end, [], string:tokens(Subscriptions, ",")). + +encode_jid(JID) -> ?PUBSUB:escape(jlib:jid_to_string(JID)). + +encode_affiliation(owner) -> "o"; +encode_affiliation(publisher) -> "p"; +encode_affiliation(member) -> "m"; +encode_affiliation(outcast) -> "c"; +encode_affiliation(_) -> "n". + +encode_subscription(subscribed) -> "s"; +encode_subscription(pending) -> "p"; +encode_subscription(unconfigured) -> "u"; +encode_subscription(_) -> "n". +encode_subscriptions(Subscriptions) -> + string:join(lists:map(fun({S, SubId}) -> + encode_subscription(S)++":"++SubId + end, Subscriptions), ","). + +%%% record getter/setter + +state_to_raw(NodeId, State) -> + {JID, _} = State#pubsub_state.stateid, + J = encode_jid(JID), + A = encode_affiliation(State#pubsub_state.affiliation), + S = encode_subscriptions(State#pubsub_state.subscriptions), + ["'", NodeId, "', '", J, "', '", A, "', '", S, "'"]. + +raw_to_item(NodeId, {ItemId, SJID, Creation, Modification, XML}) -> + JID = decode_jid(SJID), + ToTime = fun(Str) -> + [T1,T2,T3] = string:tokens(Str, ":"), + {l2i(T1), l2i(T2), l2i(T3)} + end, + Payload = case xml_stream:parse_element(XML) of + {error, _Reason} -> []; + El -> [El] + end, + #pubsub_item{itemid = {ItemId, NodeId}, + creation={ToTime(Creation), JID}, + modification={ToTime(Modification), JID}, + payload = Payload}. + +l2i(L) when is_list(L) -> list_to_integer(L); +l2i(I) when is_integer(I) -> I. +i2l(I) when is_integer(I) -> integer_to_list(I); +i2l(L) when is_list(L) -> L. +i2l(I, N) when is_integer(I) -> i2l(i2l(I), N); +i2l(L, N) when is_list(L) -> + case length(L) of + N -> L; + C when C > N -> L; + _ -> i2l([$0|L], N) + end. diff --git a/src/mod_pubsub/node_mb.erl b/src/mod_pubsub/node_mb.erl new file mode 100644 index 000000000..a497c17ba --- /dev/null +++ b/src/mod_pubsub/node_mb.erl @@ -0,0 +1,211 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% +%%% @copyright 2006-2009 ProcessOne +%%% @author Eric Cestari <eric@ohmforce.com> +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + + +%%% @doc The module <strong>{@module}</strong> is the pep microblog PubSub plugin. +%%% <p> To be used, mod_pubsub must be configured : +%%% {mod_pubsub, [ % requires mod_caps +%%% {access_createnode, pubsub_createnode}, +%%% {plugins, ["default", "pep","mb"]}, +%%% {pep_mapping, [{"urn:xmpp:microblog", "mb"}]} +%%% ]}, +%%% </p> +%%% <p>PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.</p> + +-module(node_mb). +-author('eric@ohmforce.com'). + +-include("ejabberd.hrl"). +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/2, + delete_node/1, + purge_node/2, + subscribe_node/8, + unsubscribe_node/4, + publish_item/6, + delete_item/4, + remove_extra_items/3, + get_entity_affiliations/2, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, + get_entity_subscriptions/2, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, + set_state/1, + get_items/6, + get_items/2, + get_item/7, + get_item/2, + set_item/1, + get_item_name/3, + node_to_path/1, + path_to_node/1 + ]). + +init(Host, ServerHost, Opts) -> + node_pep:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_pep:terminate(Host, ServerHost), + ok. + +options() -> + [{deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, false}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, presence}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, true}]. + +features() -> + ["create-nodes", %* + "auto-create", %* + "auto-subscribe", %* + "delete-nodes", %* + "delete-items", %* + "filtered-notifications", %* + "modify-affiliations", + "outcast-affiliation", + "persistent-items", + "publish", %* + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", %* + "retrieve-subscriptions", + "subscribe" %* + ]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_pep:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(NodeId, Owner) -> + node_pep:create_node(NodeId, Owner). + +delete_node(Removed) -> + node_pep:delete_node(Removed). + +subscribe_node(NodeId, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_pep:subscribe_node( + NodeId, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). + +unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> + node_pep:unsubscribe_node(NodeId, Sender, Subscriber, SubID). + +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> + node_pep:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(NodeId, MaxItems, ItemIds) -> + node_pep:remove_extra_items(NodeId, MaxItems, ItemIds). + +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + node_pep:delete_item(NodeId, Publisher, PublishModel, ItemId). + +purge_node(NodeId, Owner) -> + node_pep:purge_node(NodeId, Owner). + +get_entity_affiliations(Host, Owner) -> + node_pep:get_entity_affiliations(Host, Owner). + +get_node_affiliations(NodeId) -> + node_pep:get_node_affiliations(NodeId). + +get_affiliation(NodeId, Owner) -> + node_pep:get_affiliation(NodeId, Owner). + +set_affiliation(NodeId, Owner, Affiliation) -> + node_pep:set_affiliation(NodeId, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_pep:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(NodeId) -> + node_pep:get_node_subscriptions(NodeId). + +get_subscriptions(NodeId, Owner) -> + node_pep:get_subscriptions(NodeId, Owner). + +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_pep:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). + +get_states(NodeId) -> + node_pep:get_states(NodeId). + +get_state(NodeId, JID) -> + node_pep:get_state(NodeId, JID). + +set_state(State) -> + node_pep:set_state(State). + +get_items(NodeId, From) -> + node_pep:get_items(NodeId, From). + +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_pep:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). + +get_item(NodeId, ItemId) -> + node_pep:get_item(NodeId, ItemId). + +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_pep:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_pep:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_pep:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_pep:node_to_path(Node). + +path_to_node(Path) -> + node_pep:path_to_node(Path). + diff --git a/src/mod_pubsub/node_pep.erl b/src/mod_pubsub/node_pep.erl index 97bf1b2fa..5bdb4915f 100644 --- a/src/mod_pubsub/node_pep.erl +++ b/src/mod_pubsub/node_pep.erl @@ -10,13 +10,13 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% %%% -%%% @copyright 2006-2008 Process-one +%%% @copyright 2006-2009 ProcessOne %%% @author Christophe Romain <christophe.romain@process-one.net> %%% [http://www.process-one.net/] %%% @version {@vsn}, {@date} {@time} @@ -29,6 +29,7 @@ -module(node_pep). -author('christophe.romain@process-one.net'). +-include("ejabberd.hrl"). -include("pubsub.hrl"). -include("jlib.hrl"). @@ -38,51 +39,57 @@ -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, - create_node/3, - delete_node/2, - purge_node/3, + create_node/2, + delete_node/1, + purge_node/2, subscribe_node/8, - unsubscribe_node/5, - publish_item/7, + unsubscribe_node/4, + publish_item/6, delete_item/4, - remove_extra_items/4, + remove_extra_items/3, get_entity_affiliations/2, - get_node_affiliations/2, - get_affiliation/3, - set_affiliation/4, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, get_entity_subscriptions/2, - get_node_subscriptions/2, - get_subscription/3, - set_subscription/4, - get_states/2, - get_state/3, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, set_state/1, + get_items/6, get_items/2, - get_item/3, + get_item/7, + get_item/2, set_item/1, - get_item_name/3 + get_item_name/3, + node_to_path/1, + path_to_node/1 ]). init(Host, ServerHost, Opts) -> - node_default:init(Host, ServerHost, Opts), + node_hometree:init(Host, ServerHost, Opts), + complain_if_modcaps_disabled(ServerHost), ok. terminate(Host, ServerHost) -> - node_default:terminate(Host, ServerHost), + node_hometree:terminate(Host, ServerHost), ok. options() -> - [{node_type, pep}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, false}, {persist_items, false}, - {max_items, ?MAXITEMS div 2}, + {max_items, ?MAXITEMS}, {subscribe, true}, {access_model, presence}, {roster_groups_allowed, []}, {publish_model, publishers}, + {notification_type, headline}, {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, on_sub_and_presence}, {deliver_notifications, true}, @@ -93,8 +100,8 @@ features() -> "auto-create", %* "auto-subscribe", %* "delete-nodes", %* + "delete-items", %* "filtered-notifications", %* - "item-ids", "modify-affiliations", "outcast-affiliation", "persistent-items", @@ -107,104 +114,185 @@ features() -> "subscribe" %* ]. -create_node_permission(_Host, _ServerHost, _Node, _ParentNode, _Owner, _Access) -> - %% TODO may we check bare JID match ? - {result, true}. - -create_node(Host, Node, Owner) -> - case node_default:create_node(Host, Node, Owner) of +create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> + LOwner = jlib:jid_tolower(Owner), + {User, Server, _Resource} = LOwner, + Allowed = case LOwner of + {"", Host, ""} -> + true; % pubsub service always allowed + _ -> + case acl:match_rule(ServerHost, Access, LOwner) of + allow -> + case Host of + {User, Server, _} -> true; + _ -> false + end; + E -> + ?DEBUG("Create not allowed : ~p~n", [E]), + false + end + end, + {result, Allowed}. + +create_node(NodeId, Owner) -> + case node_hometree:create_node(NodeId, Owner) of {result, _} -> {result, []}; Error -> Error end. -delete_node(Host, Removed) -> - case node_default:delete_node(Host, Removed) of +delete_node(Removed) -> + case node_hometree:delete_node(Removed) of {result, {_, _, Removed}} -> {result, {[], Removed}}; Error -> Error end. -subscribe_node(Host, Node, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup) -> - node_default:subscribe_node( - Host, Node, Sender, Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup). +subscribe_node(NodeId, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node( + NodeId, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). -unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> - case node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID) of +unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> + case node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID) of {error, Error} -> {error, Error}; {result, _} -> {result, []} end. -publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> - node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> + node_hometree:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). -remove_extra_items(Host, Node, MaxItems, ItemIds) -> - node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). +remove_extra_items(NodeId, MaxItems, ItemIds) -> + node_hometree:remove_extra_items(NodeId, MaxItems, ItemIds). -delete_item(Host, Node, JID, ItemId) -> - node_default:delete_item(Host, Node, JID, ItemId). +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + node_hometree:delete_item(NodeId, Publisher, PublishModel, ItemId). -purge_node(Host, Node, Owner) -> - node_default:purge_node(Host, Node, Owner). +purge_node(NodeId, Owner) -> + node_hometree:purge_node(NodeId, Owner). get_entity_affiliations(_Host, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - node_default:get_entity_affiliations(OwnerKey, Owner). - -get_node_affiliations(_Host, Node) -> - States = mnesia:match_object( - #pubsub_state{stateid = {'_', {'_', Node}}, - _ = '_'}), - Tr = fun(#pubsub_state{stateid = {J, {_, _}}, affiliation = A}) -> - {J, A} + {_, D, _} = SubKey = jlib:jid_tolower(Owner), + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), + NodeTree = case catch ets:lookup(gen_mod:get_module_proc(D, config), nodetree) of + [{nodetree, N}] -> N; + _ -> nodetree_tree end, - {result, lists:map(Tr, States)}. - -get_affiliation(_Host, Node, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - node_default:get_affiliation(OwnerKey, Node, Owner). - -set_affiliation(_Host, Node, Owner, Affiliation) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - Record = case get_state(OwnerKey, Node, OwnerKey) of - {error, ?ERR_ITEM_NOT_FOUND} -> - #pubsub_state{stateid = {OwnerKey, {OwnerKey, Node}}, - affiliation = Affiliation}; - {result, State} -> - State#pubsub_state{affiliation = Affiliation} - end, - set_state(Record), - ok. + Reply = lists:foldl(fun(#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> [{Node, A}|Acc]; + _ -> Acc + end + end, [], States), + {result, Reply}. + +get_node_affiliations(NodeId) -> + node_hometree:get_node_affiliations(NodeId). + +get_affiliation(NodeId, Owner) -> + node_hometree:get_affiliation(NodeId, Owner). + +set_affiliation(NodeId, Owner, Affiliation) -> + node_hometree:set_affiliation(NodeId, Owner, Affiliation). + +get_entity_subscriptions(_Host, Owner) -> + {U, D, _} = SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + States = case SubKey of + GenKey -> mnesia:match_object( + #pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); + _ -> mnesia:match_object( + #pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) + ++ mnesia:match_object( + #pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) + end, + NodeTree = case catch ets:lookup(gen_mod:get_module_proc(D, config), nodetree) of + [{nodetree, N}] -> N; + _ -> nodetree_tree + end, + Reply = lists:foldl(fun(#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> + lists:foldl(fun({subscribed, SubID}, Acc2) -> + [{Node, subscribed, SubID, J} | Acc2]; + ({pending, _SubID}, Acc2) -> + [{Node, pending, J} | Acc2]; + (S, Acc2) -> + [{Node, S, J} | Acc2] + end, Acc, Ss); + _ -> Acc + end + end, [], States), + {result, Reply}. + +get_node_subscriptions(NodeId) -> + %% note: get_node_subscriptions is used for broadcasting + %% there should not have any subscriptions + %% but that call returns also all subscription to none + %% and this is required for broadcast to occurs + %% DO NOT REMOVE + node_hometree:get_node_subscriptions(NodeId). + +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). + +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). + +get_states(NodeId) -> + node_hometree:get_states(NodeId). + +get_state(NodeId, JID) -> + node_hometree:get_state(NodeId, JID). -get_entity_subscriptions(_Host, _Owner) -> - {result, []}. +set_state(State) -> + node_hometree:set_state(State). -get_node_subscriptions(_Host, _Node) -> - {result, []}. +get_items(NodeId, From) -> + node_hometree:get_items(NodeId, From). -get_subscription(_Host, _Node, _Owner) -> - {result, unknown}. +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). -set_subscription(_Host, _Node, _Owner, _Subscription) -> - ok. +get_item(NodeId, ItemId) -> + node_hometree:get_item(NodeId, ItemId). -get_states(Host, Node) -> - node_default:get_states(Host, Node). +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). -get_state(Host, Node, JID) -> - node_default:get_state(Host, Node, JID). +set_item(Item) -> + node_hometree:set_item(Item). -set_state(State) -> - node_default:set_state(State). +get_item_name(Host, Node, Id) -> + node_hometree:get_item_name(Host, Node, Id). -get_items(Host, Node) -> - node_default:get_items(Host, Node). +node_to_path(Node) -> + node_flat:node_to_path(Node). -get_item(Host, Node, ItemId) -> - node_default:get_item(Host, Node, ItemId). +path_to_node(Path) -> + node_flat:path_to_node(Path). -set_item(Item) -> - node_default:set_item(Item). -get_item_name(Host, Node, Id) -> - node_default:get_item_name(Host, Node, Id). +%%% +%%% Internal +%%% + +%% @doc Check mod_caps is enabled, otherwise show warning. +%% The PEP plugin for mod_pubsub requires mod_caps to be enabled in the host. +%% Check that the mod_caps module is enabled in that Jabber Host +%% If not, show a warning message in the ejabberd log file. +complain_if_modcaps_disabled(ServerHost) -> + Modules = ejabberd_config:get_local_option({modules, ServerHost}), + ModCaps = [mod_caps_enabled || {mod_caps, _Opts} <- Modules], + case ModCaps of + [] -> + ?WARNING_MSG("The PEP plugin is enabled in mod_pubsub of host ~p. " + "This plugin requires mod_caps to be enabled, " + "but it isn't.", [ServerHost]); + _ -> + ok + end. diff --git a/src/mod_pubsub/node_pep_odbc.erl b/src/mod_pubsub/node_pep_odbc.erl new file mode 100644 index 000000000..e24c35a14 --- /dev/null +++ b/src/mod_pubsub/node_pep_odbc.erl @@ -0,0 +1,335 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% +%%% @copyright 2006-2009 ProcessOne +%%% @author Christophe Romain <christophe.romain@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @doc The module <strong>{@module}</strong> is the pep PubSub plugin. +%%% <p>PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.</p> + +-module(node_pep_odbc). +-author('christophe.romain@process-one.net'). + +-include("ejabberd.hrl"). +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-define(PUBSUB, mod_pubsub_odbc). + +-behaviour(gen_pubsub_node). + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/2, + delete_node/1, + purge_node/2, + subscribe_node/8, + unsubscribe_node/4, + publish_item/6, + delete_item/4, + remove_extra_items/3, + get_entity_affiliations/2, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, + get_entity_subscriptions/2, + get_entity_subscriptions_for_send_last/2, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, + set_state/1, + get_items/7, + get_items/6, + get_items/3, + get_items/2, + get_item/7, + get_item/2, + set_item/1, + get_item_name/3, + get_last_items/3, + node_to_path/1, + path_to_node/1 + ]). + +init(Host, ServerHost, Opts) -> + node_hometree_odbc:init(Host, ServerHost, Opts), + complain_if_modcaps_disabled(ServerHost), + ok. + +terminate(Host, ServerHost) -> + node_hometree_odbc:terminate(Host, ServerHost), + ok. + +options() -> + [{odbc, true}, + {node_type, pep}, + {deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, false}, + {persist_items, false}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, presence}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, true}]. + +features() -> + ["create-nodes", %* + "auto-create", %* + "auto-subscribe", %* + "delete-nodes", %* + "delete-items", %* + "filtered-notifications", %* + "modify-affiliations", + "outcast-affiliation", + "persistent-items", + "publish", %* + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", %* + "retrieve-subscriptions", + "subscribe" %* + ]. + +create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> + LOwner = jlib:jid_tolower(Owner), + {User, Server, _Resource} = LOwner, + Allowed = case LOwner of + {"", Host, ""} -> + true; % pubsub service always allowed + _ -> + case acl:match_rule(ServerHost, Access, LOwner) of + allow -> + case Host of + {User, Server, _} -> true; + _ -> false + end; + E -> + ?DEBUG("Create not allowed : ~p~n", [E]), + false + end + end, + {result, Allowed}. + +create_node(NodeId, Owner) -> + case node_hometree_odbc:create_node(NodeId, Owner) of + {result, _} -> {result, []}; + Error -> Error + end. + +delete_node(Removed) -> + case node_hometree_odbc:delete_node(Removed) of + {result, {_, _, Removed}} -> {result, {[], Removed}}; + Error -> Error + end. + +subscribe_node(NodeId, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree_odbc:subscribe_node( + NodeId, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). + +unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> + case node_hometree_odbc:unsubscribe_node(NodeId, Sender, Subscriber, SubID) of + {error, Error} -> {error, Error}; + {result, _} -> {result, []} + end. + +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> + node_hometree_odbc:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(NodeId, MaxItems, ItemIds) -> + node_hometree_odbc:remove_extra_items(NodeId, MaxItems, ItemIds). + +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + node_hometree_odbc:delete_item(NodeId, Publisher, PublishModel, ItemId). + +purge_node(NodeId, Owner) -> + node_hometree_odbc:purge_node(NodeId, Owner). + +get_entity_affiliations(_Host, Owner) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + node_hometree_odbc:get_entity_affiliations(OwnerKey, Owner). + +get_node_affiliations(NodeId) -> + node_hometree_odbc:get_node_affiliations(NodeId). + +get_affiliation(NodeId, Owner) -> + node_hometree_odbc:get_affiliation(NodeId, Owner). + +set_affiliation(NodeId, Owner, Affiliation) -> + node_hometree_odbc:set_affiliation(NodeId, Owner, Affiliation). + +get_entity_subscriptions(_Host, Owner) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + Host = ?PUBSUB:escape(element(2, SubKey)), + SJ = node_hometree_odbc:encode_jid(SubKey), + GJ = node_hometree_odbc:encode_jid(GenKey), + Query = case SubKey of + GenKey -> + ["select host, node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid " + "and jid like '", GJ, "%' " + "and host like '%@", Host, "';"]; + _ -> + ["select host, node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid " + "and jid in ('", SJ, "', '", GJ, "') " + "and host like '%@", Host, "';"] + end, + Reply = case catch ejabberd_odbc:sql_query_t(Query) of + {selected, ["host", "node", "type", "nodeid", "jid", "subscriptions"], RItems} -> + lists:map(fun({H, N, T, I, J, S}) -> + O = node_hometree_odbc:decode_jid(H), + Node = nodetree_odbc:raw_to_node(O, {N, "", T, I}), + {Node, node_hometree_odbc:decode_subscriptions(S), node_hometree_odbc:decode_jid(J)} + end, RItems); + _ -> + [] + end, + {result, Reply}. + +get_entity_subscriptions_for_send_last(_Host, Owner) -> + SubKey = jlib:jid_tolower(Owner), + GenKey = jlib:jid_remove_resource(SubKey), + Host = ?PUBSUB:escape(element(2, SubKey)), + SJ = node_hometree_odbc:encode_jid(SubKey), + GJ = node_hometree_odbc:encode_jid(GenKey), + Query = case SubKey of + GenKey -> + ["select host, node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid " + "and name='send_last_published_item' and val='on_sub_and_presence' " + "and jid like '", GJ, "%' " + "and host like '%@", Host, "';"]; + _ -> + ["select host, node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid " + "and name='send_last_published_item' and val='on_sub_and_presence' " + "and jid in ('", SJ, "', '", GJ, "') " + "and host like '%@", Host, "';"] + end, + Reply = case catch ejabberd_odbc:sql_query_t(Query) of + {selected, ["host", "node", "type", "nodeid", "jid", "subscriptions"], RItems} -> + lists:map(fun({H, N, T, I, J, S}) -> + O = node_hometree_odbc:decode_jid(H), + Node = nodetree_odbc:raw_to_node(O, {N, "", T, I}), + {Node, node_hometree_odbc:decode_subscriptions(S), node_hometree_odbc:decode_jid(J)} + end, RItems); + _ -> + [] + end, + {result, Reply}. + +get_node_subscriptions(NodeId) -> + %% note: get_node_subscriptions is used for broadcasting + %% there should not have any subscriptions + %% but that call returns also all subscription to none + %% and this is required for broadcast to occurs + %% DO NOT REMOVE + node_hometree_odbc:get_node_subscriptions(NodeId). + +get_subscriptions(NodeId, Owner) -> + node_hometree_odbc:get_subscriptions(NodeId, Owner). + +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree_odbc:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree_odbc:get_pending_nodes(Host, Owner). + +get_states(NodeId) -> + node_hometree_odbc:get_states(NodeId). + +get_state(NodeId, JID) -> + node_hometree_odbc:get_state(NodeId, JID). + +set_state(State) -> + node_hometree_odbc:set_state(State). + +get_items(NodeId, From) -> + node_hometree_odbc:get_items(NodeId, From). +get_items(NodeId, From, RSM) -> + node_hometree_odbc:get_items(NodeId, From, RSM). +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, none). +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> + node_hometree_odbc:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). + +get_last_items(NodeId, JID, Count) -> + node_hometree_odbc:get_last_items(NodeId, JID, Count). + +get_item(NodeId, ItemId) -> + node_hometree_odbc:get_item(NodeId, ItemId). + +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree_odbc:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_hometree_odbc:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_hometree_odbc:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat_odbc:node_to_path(Node). + +path_to_node(Path) -> + node_flat_odbc:path_to_node(Path). + +%%% +%%% Internal +%%% + +%% @doc Check mod_caps is enabled, otherwise show warning. +%% The PEP plugin for mod_pubsub requires mod_caps to be enabled in the host. +%% Check that the mod_caps module is enabled in that Jabber Host +%% If not, show a warning message in the ejabberd log file. +complain_if_modcaps_disabled(ServerHost) -> + Modules = ejabberd_config:get_local_option({modules, ServerHost}), + ModCaps = [mod_caps_enabled || {mod_caps, _Opts} <- Modules], + case ModCaps of + [] -> + ?WARNING_MSG("The PEP plugin is enabled in mod_pubsub of host ~p. " + "This plugin requires mod_caps to be enabled, " + "but it isn't.", [ServerHost]); + _ -> + ok + end. + diff --git a/src/mod_pubsub/node_private.erl b/src/mod_pubsub/node_private.erl index 35f63c925..10f019305 100644 --- a/src/mod_pubsub/node_private.erl +++ b/src/mod_pubsub/node_private.erl @@ -10,13 +10,13 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% %%% -%%% @copyright 2006-2008 Process-one +%%% @copyright 2006-2009 ProcessOne %%% @author Christophe romain <christophe.romain@process-one.net> %%% [http://www.process-one.net/] %%% @version {@vsn}, {@date} {@time} @@ -36,57 +36,62 @@ %% it's possible not to define some function at all %% in that case, warning will be generated at compilation %% and function call will fail, -%% then mod_pubsub will call function from node_default +%% then mod_pubsub will call function from node_hometree %% (this makes code cleaner, but execution a little bit longer) %% API definition -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, - create_node/3, - delete_node/2, - purge_node/3, + create_node/2, + delete_node/1, + purge_node/2, subscribe_node/8, - unsubscribe_node/5, - publish_item/7, + unsubscribe_node/4, + publish_item/6, delete_item/4, - remove_extra_items/4, + remove_extra_items/3, get_entity_affiliations/2, - get_node_affiliations/2, - get_affiliation/3, - set_affiliation/4, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, get_entity_subscriptions/2, - get_node_subscriptions/2, - get_subscription/3, - set_subscription/4, - get_states/2, - get_state/3, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, set_state/1, + get_items/6, get_items/2, - get_item/3, + get_item/7, + get_item/2, set_item/1, - get_item_name/3 + get_item_name/3, + node_to_path/1, + path_to_node/1 ]). init(Host, ServerHost, Opts) -> - node_default:init(Host, ServerHost, Opts). + node_hometree:init(Host, ServerHost, Opts). terminate(Host, ServerHost) -> - node_default:terminate(Host, ServerHost). + node_hometree:terminate(Host, ServerHost). options() -> - [{node_type, private}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, {persist_items, true}, - {max_items, ?MAXITEMS div 2}, + {max_items, ?MAXITEMS}, {subscribe, true}, {access_model, whitelist}, {roster_groups_allowed, []}, {publish_model, publishers}, + {notification_type, headline}, {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, never}, {deliver_notifications, false}, @@ -95,8 +100,8 @@ options() -> features() -> ["create-nodes", "delete-nodes", + "delete-items", "instant-nodes", - "item-ids", "outcast-affiliation", "persistent-items", "publish", @@ -110,76 +115,93 @@ features() -> ]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> - node_default:create_node_permission(Host, ServerHost, Node, ParentNode, + node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). -create_node(Host, Node, Owner) -> - node_default:create_node(Host, Node, Owner). +create_node(NodeId, Owner) -> + node_hometree:create_node(NodeId, Owner). -delete_node(Host, Removed) -> - node_default:delete_node(Host, Removed). +delete_node(Removed) -> + node_hometree:delete_node(Removed). -subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup) -> - node_default:subscribe_node(Host, Node, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup). +subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, + Options). -unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> - node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID). +unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> + node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID). -publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> - node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> + node_hometree:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). -remove_extra_items(Host, Node, MaxItems, ItemIds) -> - node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). +remove_extra_items(NodeId, MaxItems, ItemIds) -> + node_hometree:remove_extra_items(NodeId, MaxItems, ItemIds). -delete_item(Host, Node, JID, ItemId) -> - node_default:delete_item(Host, Node, JID, ItemId). +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + node_hometree:delete_item(NodeId, Publisher, PublishModel, ItemId). -purge_node(Host, Node, Owner) -> - node_default:purge_node(Host, Node, Owner). +purge_node(NodeId, Owner) -> + node_hometree:purge_node(NodeId, Owner). get_entity_affiliations(Host, Owner) -> - node_default:get_entity_affiliations(Host, Owner). + node_hometree:get_entity_affiliations(Host, Owner). -get_node_affiliations(Host, Node) -> - node_default:get_node_affiliations(Host, Node). +get_node_affiliations(NodeId) -> + node_hometree:get_node_affiliations(NodeId). -get_affiliation(Host, Node, Owner) -> - node_default:get_affiliation(Host, Node, Owner). +get_affiliation(NodeId, Owner) -> + node_hometree:get_affiliation(NodeId, Owner). -set_affiliation(Host, Node, Owner, Affiliation) -> - node_default:set_affiliation(Host, Node, Owner, Affiliation). +set_affiliation(NodeId, Owner, Affiliation) -> + node_hometree:set_affiliation(NodeId, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> - node_default:get_entity_subscriptions(Host, Owner). + node_hometree:get_entity_subscriptions(Host, Owner). -get_node_subscriptions(Host, Node) -> - node_default:get_node_subscriptions(Host, Node). +get_node_subscriptions(NodeId) -> + node_hometree:get_node_subscriptions(NodeId). -get_subscription(Host, Node, Owner) -> - node_default:get_subscription(Host, Node, Owner). +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). -set_subscription(Host, Node, Owner, Subscription) -> - node_default:set_subscription(Host, Node, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). -get_states(Host, Node) -> - node_default:get_states(Host, Node). +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). -get_state(Host, Node, JID) -> - node_default:get_state(Host, Node, JID). +get_states(NodeId) -> + node_hometree:get_states(NodeId). + +get_state(NodeId, JID) -> + node_hometree:get_state(NodeId, JID). set_state(State) -> - node_default:set_state(State). + node_hometree:set_state(State). + +get_items(NodeId, From) -> + node_hometree:get_items(NodeId, From). + +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). -get_items(Host, Node) -> - node_default:get_items(Host, Node). +get_item(NodeId, ItemId) -> + node_hometree:get_item(NodeId, ItemId). -get_item(Host, Node, ItemId) -> - node_default:get_item(Host, Node, ItemId). +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_default:set_item(Item). + node_hometree:set_item(Item). get_item_name(Host, Node, Id) -> - node_default:get_item_name(Host, Node, Id). + node_hometree:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat:node_to_path(Node). + +path_to_node(Path) -> + node_flat:path_to_node(Path). + diff --git a/src/mod_pubsub/node_public.erl b/src/mod_pubsub/node_public.erl index 1586abe86..5e26d8273 100644 --- a/src/mod_pubsub/node_public.erl +++ b/src/mod_pubsub/node_public.erl @@ -10,13 +10,13 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% %%% -%%% @copyright 2006-2008 Process-one +%%% @copyright 2006-2009 ProcessOne %%% @author Christophe romain <christophe.romain@process-one.net> %%% [http://www.process-one.net/] %%% @version {@vsn}, {@date} {@time} @@ -36,57 +36,62 @@ %% it's possible not to define some function at all %% in that case, warning will be generated at compilation %% and function call will fail, -%% then mod_pubsub will call function from node_default +%% then mod_pubsub will call function from node_hometree %% (this makes code cleaner, but execution a little bit longer) %% API definition -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, - create_node/3, - delete_node/2, - purge_node/3, + create_node/2, + delete_node/1, + purge_node/2, subscribe_node/8, - unsubscribe_node/5, - publish_item/7, + unsubscribe_node/4, + publish_item/6, delete_item/4, - remove_extra_items/4, + remove_extra_items/3, get_entity_affiliations/2, - get_node_affiliations/2, - get_affiliation/3, - set_affiliation/4, + get_node_affiliations/1, + get_affiliation/2, + set_affiliation/3, get_entity_subscriptions/2, - get_node_subscriptions/2, - get_subscription/3, - set_subscription/4, - get_states/2, - get_state/3, + get_node_subscriptions/1, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, set_state/1, + get_items/6, get_items/2, - get_item/3, + get_item/7, + get_item/2, set_item/1, - get_item_name/3 + get_item_name/3, + node_to_path/1, + path_to_node/1 ]). init(Host, ServerHost, Opts) -> - node_default:init(Host, ServerHost, Opts). + node_hometree:init(Host, ServerHost, Opts). terminate(Host, ServerHost) -> - node_default:terminate(Host, ServerHost). + node_hometree:terminate(Host, ServerHost). options() -> - [{node_type, public}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, {persist_items, true}, - {max_items, ?MAXITEMS div 2}, + {max_items, ?MAXITEMS}, {subscribe, true}, {access_model, open}, {roster_groups_allowed, []}, {publish_model, publishers}, + {notification_type, headline}, {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, never}, {deliver_notifications, true}, @@ -95,8 +100,8 @@ options() -> features() -> ["create-nodes", "delete-nodes", + "delete-items", "instant-nodes", - "item-ids", "outcast-affiliation", "persistent-items", "publish", @@ -110,75 +115,91 @@ features() -> ]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> - node_default:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). -create_node(Host, Node, Owner) -> - node_default:create_node(Host, Node, Owner). +create_node(NodeId, Owner) -> + node_hometree:create_node(NodeId, Owner). -delete_node(Host, Removed) -> - node_default:delete_node(Host, Removed). +delete_node(Removed) -> + node_hometree:delete_node(Removed). -subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> - node_default:subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). +subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). -unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> - node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID). +unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> + node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID). -publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> - node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> + node_hometree:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). -remove_extra_items(Host, Node, MaxItems, ItemIds) -> - node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). +remove_extra_items(NodeId, MaxItems, ItemIds) -> + node_hometree:remove_extra_items(NodeId, MaxItems, ItemIds). -delete_item(Host, Node, JID, ItemId) -> - node_default:delete_item(Host, Node, JID, ItemId). +delete_item(NodeId, Publisher, PublishModel, ItemId) -> + node_hometree:delete_item(NodeId, Publisher, PublishModel, ItemId). -purge_node(Host, Node, Owner) -> - node_default:purge_node(Host, Node, Owner). +purge_node(NodeId, Owner) -> + node_hometree:purge_node(NodeId, Owner). get_entity_affiliations(Host, Owner) -> - node_default:get_entity_affiliations(Host, Owner). + node_hometree:get_entity_affiliations(Host, Owner). -get_node_affiliations(Host, Node) -> - node_default:get_node_affiliations(Host, Node). +get_node_affiliations(NodeId) -> + node_hometree:get_node_affiliations(NodeId). -get_affiliation(Host, Node, Owner) -> - node_default:get_affiliation(Host, Node, Owner). +get_affiliation(NodeId, Owner) -> + node_hometree:get_affiliation(NodeId, Owner). -set_affiliation(Host, Node, Owner, Affiliation) -> - node_default:set_affiliation(Host, Node, Owner, Affiliation). +set_affiliation(NodeId, Owner, Affiliation) -> + node_hometree:set_affiliation(NodeId, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> - node_default:get_entity_subscriptions(Host, Owner). + node_hometree:get_entity_subscriptions(Host, Owner). -get_node_subscriptions(Host, Node) -> - node_default:get_node_subscriptions(Host, Node). +get_node_subscriptions(NodeId) -> + node_hometree:get_node_subscriptions(NodeId). -get_subscription(Host, Node, Owner) -> - node_default:get_subscription(Host, Node, Owner). +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). -set_subscription(Host, Node, Owner, Subscription) -> - node_default:set_subscription(Host, Node, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). -get_states(Host, Node) -> - node_default:get_states(Host, Node). +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). -get_state(Host, Node, JID) -> - node_default:get_state(Host, Node, JID). +get_states(NodeId) -> + node_hometree:get_states(NodeId). + +get_state(NodeId, JID) -> + node_hometree:get_state(NodeId, JID). set_state(State) -> - node_default:set_state(State). + node_hometree:set_state(State). + +get_items(NodeId, From) -> + node_hometree:get_items(NodeId, From). + +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). -get_items(Host, Node) -> - node_default:get_items(Host, Node). +get_item(NodeId, ItemId) -> + node_hometree:get_item(NodeId, ItemId). -get_item(Host, Node, ItemId) -> - node_default:get_item(Host, Node, ItemId). +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_hometree:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_default:set_item(Item). + node_hometree:set_item(Item). %% @doc <p>Return the name of the node if known: Default is to return %% node id.</p> get_item_name(Host, Node, Id) -> - node_default:get_item_name(Host, Node, Id). + node_hometree:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat:node_to_path(Node). + +path_to_node(Path) -> + node_flat:path_to_node(Path). + diff --git a/src/mod_pubsub/nodetree_dag.erl b/src/mod_pubsub/nodetree_dag.erl new file mode 100644 index 000000000..d7094e3e0 --- /dev/null +++ b/src/mod_pubsub/nodetree_dag.erl @@ -0,0 +1,248 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% @author Brian Cully <bjc@kublai.com> +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(nodetree_dag). +-author('bjc@kublai.com'). + +%% API +-export([init/3, + terminate/2, + options/0, + set_node/1, + get_node/3, + get_node/2, + get_node/1, + get_nodes/2, + get_nodes/1, + get_parentnodes/3, + get_parentnodes_tree/3, + get_subnodes/3, + get_subnodes_tree/3, + create_node/6, + delete_node/2]). + +-include_lib("stdlib/include/qlc.hrl"). + +-include("ejabberd.hrl"). +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_nodetree). + +-define(DEFAULT_NODETYPE, leaf). +-define(DEFAULT_PARENTS, []). +-define(DEFAULT_CHILDREN, []). + +-compile(export_all). + +%%==================================================================== +%% API +%%==================================================================== +init(Host, ServerHost, Opts) -> + nodetree_tree:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + nodetree_tree:terminate(Host, ServerHost). + +create_node(Key, NodeID, Type, Owner, Options, Parents) -> + OwnerJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + case find_node(Key, NodeID) of + false -> + ID = pubsub_index:new(node), + N = #pubsub_node{nodeid = oid(Key, NodeID), + id = ID, + type = Type, + parents = Parents, + owners = [OwnerJID], + options = Options}, + case set_node(N) of + ok -> {ok, ID}; + Other -> Other + end; + _ -> + {error, ?ERR_CONFLICT} + end. + +set_node(#pubsub_node{nodeid = {Key, _}, + owners = Owners, + options = Options} = Node) -> + Parents = find_opt(collection, ?DEFAULT_PARENTS, Options), + case validate_parentage(Key, Owners, Parents) of + true -> + %% Update parents whenever the config changes. + mnesia:write(Node#pubsub_node{parents = Parents}); + Other -> + Other + end. + +delete_node(Key, NodeID) -> + case find_node(Key, NodeID) of + false -> + {error, ?ERR_ITEM_NOT_FOUND}; + Node -> + %% Find all of N's children, update their configs to + %% remove N from the collection setting. + lists:foreach(fun (#pubsub_node{options = Opts} = Child) -> + NewOpts = remove_config_parent(NodeID, Opts), + Parents = find_opt(collection, ?DEFAULT_PARENTS, NewOpts), + ok = mnesia:write(pubsub_node, + Child#pubsub_node{ + parents = Parents, + options = NewOpts}, + write) + end, get_subnodes(Key, NodeID)), + + %% Remove and return the requested node. + pubsub_index:free(node, Node#pubsub_node.id), + mnesia:delete_object(pubsub_node, Node, write), + [Node] + end. + +options() -> + nodetree_tree:options(). + +get_node(Host, NodeID, _From) -> + get_node(Host, NodeID). + +get_node(Host, NodeID) -> + case find_node(Host, NodeID) of + false -> {error, ?ERR_ITEM_NOT_FOUND}; + Node -> Node + end. + +get_node(NodeId) -> + nodetree_tree:get_node(NodeId). + +get_nodes(Key, From) -> + nodetree_tree:get_nodes(Key, From). + +get_nodes(Key) -> + nodetree_tree:get_nodes(Key). + +get_parentnodes(Host, NodeID, _From) -> + case find_node(Host, NodeID) of + false -> {error, ?ERR_ITEM_NOT_FOUND}; + #pubsub_node{parents = Parents} -> + Q = qlc:q([N || #pubsub_node{nodeid = {NHost, NNode}} = N <- mnesia:table(pubsub_node), + Parent <- Parents, + Host == NHost, + Parent == NNode]), + qlc:e(Q) + end. + +get_parentnodes_tree(Host, NodeID, _From) -> + Pred = fun (NID, #pubsub_node{nodeid = {_, NNodeID}}) -> + NID == NNodeID + end, + Tr = fun (#pubsub_node{parents = Parents}) -> Parents end, + traversal_helper(Pred, Tr, Host, [NodeID]). + +get_subnodes(Host, NodeID, _From) -> + get_subnodes(Host, NodeID). + +get_subnodes(Host, NodeID) -> + case find_node(Host, NodeID) of + false -> {error, ?ERR_ITEM_NOT_FOUND}; + _ -> + Q = qlc:q([Node || #pubsub_node{nodeid = {NHost, _}, + parents = Parents} = Node <- mnesia:table(pubsub_node), + Host == NHost, + lists:member(NodeID, Parents)]), + qlc:e(Q) + end. + +get_subnodes_tree(Host, NodeID, From) -> + Pred = fun (NID, #pubsub_node{parents = Parents}) -> + lists:member(NID, Parents) + end, + Tr = fun (#pubsub_node{nodeid = {_, N}}) -> [N] end, + traversal_helper(Pred, Tr, 1, Host, [NodeID], + [{0, [get_node(Host, NodeID, From)]}]). + +%%==================================================================== +%% Internal functions +%%==================================================================== +oid(Key, Name) -> {Key, Name}. + +%% Key = jlib:jid() | host() +%% NodeID = string() +find_node(Key, NodeID) -> + case mnesia:read(pubsub_node, oid(Key, NodeID), read) of + [] -> false; + [Node] -> Node + end. + +%% Key = jlib:jid() | host() +%% Default = term() +%% Options = [{Key = atom(), Value = term()}] +find_opt(Key, Default, Options) -> + case lists:keysearch(Key, 1, Options) of + {value, {Key, Val}} -> Val; + _ -> Default + end. + +traversal_helper(Pred, Tr, Host, NodeIDs) -> + traversal_helper(Pred, Tr, 0, Host, NodeIDs, []). + +traversal_helper(_Pred, _Tr, _Depth, _Host, [], Acc) -> + Acc; +traversal_helper(Pred, Tr, Depth, Host, NodeIDs, Acc) -> + Q = qlc:q([Node || #pubsub_node{nodeid = {NHost, _}} = Node <- mnesia:table(pubsub_node), + NodeID <- NodeIDs, + Host == NHost, + Pred(NodeID, Node)]), + Nodes = qlc:e(Q), + IDs = lists:flatmap(Tr, Nodes), + traversal_helper(Pred, Tr, Depth + 1, Host, IDs, [{Depth, Nodes} | Acc]). + +remove_config_parent(NodeID, Options) -> + remove_config_parent(NodeID, Options, []). + +remove_config_parent(_NodeID, [], Acc) -> + lists:reverse(Acc); +remove_config_parent(NodeID, [{collection, Parents} | T], Acc) -> + remove_config_parent(NodeID, T, + [{collection, lists:delete(NodeID, Parents)} | Acc]); +remove_config_parent(NodeID, [H | T], Acc) -> + remove_config_parent(NodeID, T, [H | Acc]). + +validate_parentage(_Key, _Owners, []) -> + true; +validate_parentage(Key, Owners, [[] | T]) -> + validate_parentage(Key, Owners, T); +validate_parentage(Key, Owners, [ParentID | T]) -> + case find_node(Key, ParentID) of + false -> {error, ?ERR_ITEM_NOT_FOUND}; + #pubsub_node{owners = POwners, options = POptions} -> + NodeType = find_opt(node_type, ?DEFAULT_NODETYPE, POptions), + MutualOwners = [O || O <- Owners, PO <- POwners, + O == PO], + case {MutualOwners, NodeType} of + {[], _} -> {error, ?ERR_FORBIDDEN}; + {_, collection} -> validate_parentage(Key, Owners, T); + {_, _} -> {error, ?ERR_NOT_ALLOWED} + end + end. + +%% @spec (Host) -> jid() +%% Host = host() +%% @doc <p>Generate pubsub service JID.</p> +service_jid(Host) -> + case Host of + {U,S,_} -> {jid, U, S, "", U, S, ""}; + _ -> {jid, "", Host, "", "", Host, ""} + end. diff --git a/src/mod_pubsub/nodetree_default.erl b/src/mod_pubsub/nodetree_default.erl deleted file mode 100644 index 0a36e3362..000000000 --- a/src/mod_pubsub/nodetree_default.erl +++ /dev/null @@ -1,184 +0,0 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. -%%% -%%% -%%% @copyright 2006-2008 Process-one -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @doc The module <strong>{@module}</strong> is the default PubSub node tree plugin. -%%% <p>It is used as a default for all unknown PubSub node type. It can serve -%%% as a developer basis and reference to build its own custom pubsub node tree -%%% types.</p> -%%% <p>PubSub node tree plugins are using the {@link gen_nodetree} behaviour.</p> -%%% <p><strong>The API isn't stabilized yet</strong>. The pubsub plugin -%%% development is still a work in progress. However, the system is already -%%% useable and useful as is. Please, send us comments, feedback and -%%% improvements.</p> - --module(nodetree_default). --author('christophe.romain@process-one.net'). - --include("pubsub.hrl"). --include("jlib.hrl"). - --behaviour(gen_pubsub_nodetree). - --export([init/3, - terminate/2, - options/0, - set_node/1, - get_node/2, - get_nodes/1, - get_subnodes/3, - get_subnodes_tree/2, - create_node/5, - delete_node/2 - ]). - - -%% ================ -%% API definition -%% ================ - -%% @spec (Host, ServerHost, Opts) -> any() -%% Host = mod_pubsub:host() -%% ServerHost = host() -%% Opts = list() -%% @doc <p>Called during pubsub modules initialisation. Any pubsub plugin must -%% implement this function. It can return anything.</p> -%% <p>This function is mainly used to trigger the setup task necessary for the -%% plugin. It can be used for example by the developer to create the specific -%% module database schema if it does not exists yet.</p> -init(_Host, _ServerHost, _Opts) -> - mnesia:create_table(pubsub_node, - [{disc_copies, [node()]}, - {attributes, record_info(fields, pubsub_node)}, - {index, [type,parentid]}]), - NodesFields = record_info(fields, pubsub_node), - case mnesia:table_info(pubsub_node, attributes) of - NodesFields -> ok; - _ -> mnesia:transform_table(pubsub_node, ignore, NodesFields) - end, - ok. -terminate(_Host, _ServerHost) -> - ok. - -%% @spec () -> [Option] -%% Option = mod_pubsub:nodetreeOption() -%% @doc Returns the default pubsub node tree options. -options() -> - [{virtual_tree, false}]. - -%% @spec (NodeRecord) -> ok | {error, Reason} -%% Record = mod_pubsub:pubsub_node() -set_node(Record) when is_record(Record, pubsub_node) -> - mnesia:write(Record); -set_node(_) -> - {error, ?ERR_INTERNAL_SERVER_ERROR}. - -%% @spec (Host, Node) -> pubsubNode() | {error, Reason} -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -get_node(Host, Node) -> - case catch mnesia:read({pubsub_node, {Host, Node}}) of - [Record] when is_record(Record, pubsub_node) -> Record; - [] -> {error, ?ERR_ITEM_NOT_FOUND}; - Error -> Error - end. - -%% @spec (Key) -> [pubsubNode()] | {error, Reason} -%% Key = mod_pubsub:host() | mod_pubsub:jid() -get_nodes(Key) -> - mnesia:match_object(#pubsub_node{nodeid = {Key, '_'}, _ = '_'}). - -%% @spec (Host, Node, From) -> [pubsubNode()] | {error, Reason} -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% From = mod_pubsub:jid() -get_subnodes(Host, Node, _From) -> - mnesia:index_read(pubsub_node, {Host, Node}, #pubsub_node.parentid). - -%% @spec (Host, Index) -> [pubsubNode()] | {error, Reason} -%% Host = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -get_subnodes_tree(Host, Node) -> - mnesia:foldl(fun(#pubsub_node{nodeid = {H, N}}, Acc) -> - case lists:prefix(Node, N) and (H == Host) of - true -> [N | Acc]; - _ -> Acc - end - end, [], pubsub_node). - -%% @spec (Key, Node, Type, Owner, Options) -> ok | {error, Reason} -%% Key = mod_pubsub:host() | mod_pubsub:jid() -%% Node = mod_pubsub:pubsubNode() -%% NodeType = mod_pubsub:nodeType() -%% Owner = mod_pubsub:jid() -%% Options = list() -create_node(Key, Node, Type, Owner, Options) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - case mnesia:read({pubsub_node, {Key, Node}}) of - [] -> - {ParentNode, ParentExists} = - case Key of - {_U, _S, _R} -> - %% This is special case for PEP handling - %% PEP does not uses hierarchy - {[], true}; - _ -> - Parent = lists:sublist(Node, length(Node) - 1), - case Parent of - [] -> - {[], true}; - _ -> - case mnesia:read({pubsub_node, {Key, Parent}}) of - [] -> {Parent, false}; - _ -> {Parent, true} - end - end - end, - case ParentExists of - true -> - %% Service requires registration - %%{error, ?ERR_REGISTRATION_REQUIRED}; - mnesia:write(#pubsub_node{nodeid = {Key, Node}, - parentid = {Key, ParentNode}, - type = Type, - owners = [OwnerKey], - options = Options}); - false -> - %% Requesting entity is prohibited from creating nodes - {error, ?ERR_FORBIDDEN} - end; - _ -> - %% NodeID already exists - {error, ?ERR_CONFLICT} - end. - -%% @spec (Key, Node) -> [mod_pubsub:node()] -%% Key = mod_pubsub:host() | mod_pubsub:jid() -%% Node = mod_pubsub:pubsubNode() -delete_node(Key, Node) -> - Removed = get_subnodes_tree(Key, Node), - lists:foreach(fun(N) -> - mnesia:delete({pubsub_node, {Key, N}}) - end, Removed), - Removed. diff --git a/src/mod_pubsub/nodetree_tree.erl b/src/mod_pubsub/nodetree_tree.erl new file mode 100644 index 000000000..39160e7b5 --- /dev/null +++ b/src/mod_pubsub/nodetree_tree.erl @@ -0,0 +1,257 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% +%%% @copyright 2006-2009 ProcessOne +%%% @author Christophe Romain <christophe.romain@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @doc The module <strong>{@module}</strong> is the default PubSub node tree plugin. +%%% <p>It is used as a default for all unknown PubSub node type. It can serve +%%% as a developer basis and reference to build its own custom pubsub node tree +%%% types.</p> +%%% <p>PubSub node tree plugins are using the {@link gen_nodetree} behaviour.</p> +%%% <p><strong>The API isn't stabilized yet</strong>. The pubsub plugin +%%% development is still a work in progress. However, the system is already +%%% useable and useful as is. Please, send us comments, feedback and +%%% improvements.</p> + +-module(nodetree_tree). +-author('christophe.romain@process-one.net'). + +-include_lib("stdlib/include/qlc.hrl"). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_nodetree). + +-export([init/3, + terminate/2, + options/0, + set_node/1, + get_node/3, + get_node/2, + get_node/1, + get_nodes/2, + get_nodes/1, + get_parentnodes/3, + get_parentnodes_tree/3, + get_subnodes/3, + get_subnodes_tree/3, + create_node/6, + delete_node/2 + ]). + + +%% ================ +%% API definition +%% ================ + +%% @spec (Host, ServerHost, Opts) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = host() +%% Opts = list() +%% @doc <p>Called during pubsub modules initialisation. Any pubsub plugin must +%% implement this function. It can return anything.</p> +%% <p>This function is mainly used to trigger the setup task necessary for the +%% plugin. It can be used for example by the developer to create the specific +%% module database schema if it does not exists yet.</p> +init(_Host, _ServerHost, _Opts) -> + mnesia:create_table(pubsub_node, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_node)}]), + mnesia:add_table_index(pubsub_node, id), + NodesFields = record_info(fields, pubsub_node), + case mnesia:table_info(pubsub_node, attributes) of + NodesFields -> ok; + _ -> + ok + %% mnesia:transform_table(pubsub_state, ignore, StatesFields) + end, + ok. +terminate(_Host, _ServerHost) -> + ok. + +%% @spec () -> [Option] +%% Option = mod_pubsub:nodetreeOption() +%% @doc Returns the default pubsub node tree options. +options() -> + [{virtual_tree, false}]. + +%% @spec (NodeRecord) -> ok | {error, Reason} +%% Record = mod_pubsub:pubsub_node() +set_node(Record) when is_record(Record, pubsub_node) -> + mnesia:write(Record); +set_node(_) -> + {error, ?ERR_INTERNAL_SERVER_ERROR}. + +get_node(Host, Node, _From) -> + get_node(Host, Node). + +%% @spec (Host, Node) -> pubsubNode() | {error, Reason} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +get_node(Host, Node) -> + case catch mnesia:read({pubsub_node, {Host, Node}}) of + [Record] when is_record(Record, pubsub_node) -> Record; + [] -> {error, ?ERR_ITEM_NOT_FOUND}; + Error -> Error + end. +get_node(NodeId) -> + case catch mnesia:index_read(pubsub_node, NodeId, #pubsub_node.id) of + [Record] when is_record(Record, pubsub_node) -> Record; + [] -> {error, ?ERR_ITEM_NOT_FOUND}; + Error -> Error + end. + +get_nodes(Host, _From) -> + get_nodes(Host). + +%% @spec (Host) -> [pubsubNode()] | {error, Reason} +%% Host = mod_pubsub:host() | mod_pubsub:jid() +get_nodes(Host) -> + mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}). + +%% @spec (Host, Node, From) -> [{Depth, Record}] | {error, Reason} +%% Host = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() +%% Depth = integer() +%% Record = pubsubNode() +%% @doc <p>Default node tree does not handle parents, return empty list.</p> +get_parentnodes(_Host, _Node, _From) -> + []. + +%% @spec (Host, Node, From) -> [{Depth, Record}] | {error, Reason} +%% Host = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() +%% Depth = integer() +%% Record = pubsubNode() +%% @doc <p>Default node tree does not handle parents, return a list +%% containing just this node.</p> +get_parentnodes_tree(Host, Node, From) -> + case get_node(Host, Node, From) of + N when is_record(N, pubsub_node) -> [{0, [N]}]; + _Error -> [] + end. + +%% @spec (Host, Node, From) -> [pubsubNode()] | {error, Reason} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() +get_subnodes(Host, Node, _From) -> + get_subnodes(Host, Node). +get_subnodes(Host, <<>>) -> + Q = qlc:q([N || #pubsub_node{nodeid = {NHost, _}, + parents = Parents} = N <- mnesia:table(pubsub_node), + Host == NHost, + Parents == []]), + qlc:e(Q); +get_subnodes(Host, Node) -> + Q = qlc:q([N || #pubsub_node{nodeid = {NHost, _}, + parents = Parents} = N <- mnesia:table(pubsub_node), + Host == NHost, + lists:member(Node, Parents)]), + qlc:e(Q). + +get_subnodes_tree(Host, Node, _From) -> + get_subnodes_tree(Host, Node). + +%% @spec (Host, Index) -> [pubsubNodeIdx()] | {error, Reason} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() +get_subnodes_tree(Host, Node) -> + case get_node(Host, Node) of + {error, _} -> + []; + Rec -> + BasePlugin = list_to_atom("node_"++Rec#pubsub_node.type), + BasePath = BasePlugin:node_to_path(Node), + mnesia:foldl(fun(#pubsub_node{nodeid = {H, N}} = R, Acc) -> + Plugin = list_to_atom("node_"++R#pubsub_node.type), + Path = Plugin:node_to_path(N), + case lists:prefix(BasePath, Path) and (H == Host) of + true -> [R | Acc]; + false -> Acc + end + end, [], pubsub_node) + end. + +%% @spec (Host, Node, Type, Owner, Options) -> ok | {error, Reason} +%% Host = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +%% NodeType = mod_pubsub:nodeType() +%% Owner = mod_pubsub:jid() +%% Options = list() +create_node(Host, Node, Type, Owner, Options, Parents) -> + BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + case catch mnesia:read({pubsub_node, {Host, Node}}) of + [] -> + ParentExists = + case Host of + {_U, _S, _R} -> + %% This is special case for PEP handling + %% PEP does not uses hierarchy + true; + _ -> + case Parents of + [] -> true; + [Parent|_] -> + case catch mnesia:read({pubsub_node, {Host, Parent}}) of + [#pubsub_node{owners = [{[], Host, []}]}] -> true; + [#pubsub_node{owners = Owners}] -> lists:member(BJID, Owners); + _ -> false + end; + _ -> + false + end + end, + case ParentExists of + true -> + NodeId = pubsub_index:new(node), + mnesia:write(#pubsub_node{nodeid = {Host, Node}, + id = NodeId, + parents = Parents, + type = Type, + owners = [BJID], + options = Options}), + {ok, NodeId}; + false -> + %% Requesting entity is prohibited from creating nodes + {error, ?ERR_FORBIDDEN} + end; + _ -> + %% NodeID already exists + {error, ?ERR_CONFLICT} + end. + +%% @spec (Host, Node) -> [mod_pubsub:node()] +%% Host = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +delete_node(Host, Node) -> + Removed = get_subnodes_tree(Host, Node), + lists:foreach(fun(#pubsub_node{nodeid = {_, N}, id = I}) -> + pubsub_index:free(node, I), + mnesia:delete({pubsub_node, {Host, N}}) + end, Removed), + Removed. diff --git a/src/mod_pubsub/nodetree_tree_odbc.erl b/src/mod_pubsub/nodetree_tree_odbc.erl new file mode 100644 index 000000000..d8ff30832 --- /dev/null +++ b/src/mod_pubsub/nodetree_tree_odbc.erl @@ -0,0 +1,371 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% +%%% @copyright 2006-2009 ProcessOne +%%% @author Christophe Romain <christophe.romain@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @doc The module <strong>{@module}</strong> is the default PubSub node tree plugin. +%%% <p>It is used as a default for all unknown PubSub node type. It can serve +%%% as a developer basis and reference to build its own custom pubsub node tree +%%% types.</p> +%%% <p>PubSub node tree plugins are using the {@link gen_nodetree} behaviour.</p> +%%% <p><strong>The API isn't stabilized yet</strong>. The pubsub plugin +%%% development is still a work in progress. However, the system is already +%%% useable and useful as is. Please, send us comments, feedback and +%%% improvements.</p> + +-module(nodetree_tree_odbc). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-define(PUBSUB, mod_pubsub_odbc). +-define(PLUGIN_PREFIX, "node_"). + +-behaviour(gen_pubsub_nodetree). + +-export([init/3, + terminate/2, + options/0, + set_node/1, + get_node/3, + get_node/2, + get_node/1, + get_nodes/2, + get_nodes/1, + get_parentnodes/3, + get_parentnodes_tree/3, + get_subnodes/3, + get_subnodes_tree/3, + create_node/6, + delete_node/2 + ]). + +-export([raw_to_node/2]). + +%% ================ +%% API definition +%% ================ + +%% @spec (Host, ServerHost, Opts) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = host() +%% Opts = list() +%% @doc <p>Called during pubsub modules initialisation. Any pubsub plugin must +%% implement this function. It can return anything.</p> +%% <p>This function is mainly used to trigger the setup task necessary for the +%% plugin. It can be used for example by the developer to create the specific +%% module database schema if it does not exists yet.</p> +init(_Host, _ServerHost, _Opts) -> + ok. +terminate(_Host, _ServerHost) -> + ok. + +%% @spec () -> [Option] +%% Option = mod_pubsub:nodetreeOption() +%% @doc Returns the default pubsub node tree options. +options() -> + [{virtual_tree, false}, + {odbc, true}]. + +%% @spec (Host, Node, From) -> pubsubNode() | {error, Reason} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +get_node(Host, Node, _From) -> + get_node(Host, Node). +get_node(Host, Node) -> + H = ?PUBSUB:escape(Host), + N = ?PUBSUB:escape(?PUBSUB:node_to_string(Node)), + case catch ejabberd_odbc:sql_query_t( + ["select node, parent, type, nodeid " + "from pubsub_node " + "where host='", H, "' and node='", N, "';"]) + of + {selected, ["node", "parent", "type", "nodeid"], [RItem]} -> + raw_to_node(Host, RItem); + {'EXIT', _Reason} -> + {error, ?ERR_INTERNAL_SERVER_ERROR}; + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end. +get_node(NodeId) -> + case catch ejabberd_odbc:sql_query_t( + ["select host, node, parent, type " + "from pubsub_node " + "where nodeid='", NodeId, "';"]) + of + {selected, ["host", "node", "parent", "type"], [{Host, Node, Parent, Type}]} -> + raw_to_node(Host, {Node, Parent, Type, NodeId}); + {'EXIT', _Reason} -> + {error, ?ERR_INTERNAL_SERVER_ERROR}; + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end. + +%% @spec (Host, From) -> [pubsubNode()] | {error, Reason} +%% Host = mod_pubsub:host() | mod_pubsub:jid() +get_nodes(Host, _From) -> + get_nodes(Host). +get_nodes(Host) -> + H = ?PUBSUB:escape(Host), + case catch ejabberd_odbc:sql_query_t( + ["select node, parent, type, nodeid " + "from pubsub_node " + "where host='", H, "';"]) + of + {selected, ["node", "parent", "type", "nodeid"], RItems} -> + lists:map(fun(Item) -> raw_to_node(Host, Item) end, RItems); + _ -> + [] + end. + +%% @spec (Host, Node, From) -> [{Depth, Record}] | {error, Reason} +%% Host = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() +%% Depth = integer() +%% Record = pubsubNode() +%% @doc <p>Default node tree does not handle parents, return empty list.</p> +get_parentnodes(_Host, _Node, _From) -> + []. + +%% @spec (Host, Node, From) -> [{Depth, Record}] | {error, Reason} +%% Host = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() +%% Depth = integer() +%% Record = pubsubNode() +%% @doc <p>Default node tree does not handle parents, return a list +%% containing just this node.</p> +get_parentnodes_tree(Host, Node, From) -> + case get_node(Host, Node, From) of + N when is_record(N, pubsub_node) -> [{0, [N]}]; + _Error -> [] + end. + +get_subnodes(Host, Node, _From) -> + get_subnodes(Host, Node). + +%% @spec (Host, Index) -> [pubsubNode()] | {error, Reason} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +get_subnodes(Host, Node) -> + H = ?PUBSUB:escape(Host), + N = ?PUBSUB:escape(?PUBSUB:node_to_string(Node)), + case catch ejabberd_odbc:sql_query_t( + ["select node, parent, type, nodeid " + "from pubsub_node " + "where host='", H, "' and parent='", N, "';"]) + of + {selected, ["node", "parent", "type", "nodeid"], RItems} -> + lists:map(fun(Item) -> raw_to_node(Host, Item) end, RItems); + _ -> + [] + end. + +get_subnodes_tree(Host, Node, _From) -> + get_subnodes_tree(Host, Node). + +%% @spec (Host, Index) -> [pubsubNode()] | {error, Reason} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +get_subnodes_tree(Host, Node) -> + H = ?PUBSUB:escape(Host), + N = ?PUBSUB:escape(?PUBSUB:node_to_string(Node)), + case catch ejabberd_odbc:sql_query_t( + ["select node, parent, type, nodeid " + "from pubsub_node " + "where host='", H, "' and node like '", N, "%';"]) + of + {selected, ["node", "parent", "type", "nodeid"], RItems} -> + lists:map(fun(Item) -> raw_to_node(Host, Item) end, RItems); + _ -> + [] + end. + +%% @spec (Host, Node, Type, Owner, Options) -> ok | {error, Reason} +%% Host = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +%% NodeType = mod_pubsub:nodeType() +%% Owner = mod_pubsub:jid() +%% Options = list() +%% Parents = list() +create_node(Host, Node, Type, Owner, Options, Parents) -> + BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + case nodeid(Host, Node) of + {error, ?ERR_ITEM_NOT_FOUND} -> + ParentExists = + case Host of + {_U, _S, _R} -> + %% This is special case for PEP handling + %% PEP does not uses hierarchy + true; + _ -> + case Parents of + [] -> true; + [Parent|_] -> + case nodeid(Host, Parent) of + {result, PNodeId} -> + case nodeowners(PNodeId) of + [{[], Host, []}] -> true; + Owners -> lists:member(BJID, Owners) + end; + _ -> + false + end; + _ -> + false + end + end, + case ParentExists of + true -> + case set_node(#pubsub_node{ + nodeid={Host, Node}, + parents=Parents, + type=Type, + options=Options}) of + {result, NodeId} -> {ok, NodeId}; + Other -> Other + end; + false -> + %% Requesting entity is prohibited from creating nodes + {error, ?ERR_FORBIDDEN} + end; + {result, _} -> + %% NodeID already exists + {error, ?ERR_CONFLICT}; + Error -> + Error + end. + +%% @spec (Host, Node) -> [mod_pubsub:node()] +%% Host = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +delete_node(Host, Node) -> + H = ?PUBSUB:escape(Host), + N = ?PUBSUB:escape(?PUBSUB:node_to_string(Node)), + Removed = get_subnodes_tree(Host, Node), + catch ejabberd_odbc:sql_query_t( + ["delete from pubsub_node " + "where host='", H, "' and node like '", N, "%';"]), + Removed. + +%% helpers + +raw_to_node(Host, {Node, Parent, Type, NodeId}) -> + Options = case catch ejabberd_odbc:sql_query_t( + ["select name,val " + "from pubsub_node_option " + "where nodeid='", NodeId, "';"]) + of + {selected, ["name", "val"], ROptions} -> + DbOpts = lists:map(fun({Key, Value}) -> + RKey = list_to_atom(Key), + Tokens = element(2, erl_scan:string(Value++".")), + RValue = element(2, erl_parse:parse_term(Tokens)), + {RKey, RValue} + end, ROptions), + Module = list_to_atom(?PLUGIN_PREFIX++Type), + StdOpts = Module:options(), + lists:foldl(fun({Key, Value}, Acc)-> + lists:keyreplace(Key, 1, Acc, {Key, Value}) + end, StdOpts, DbOpts); + _ -> + [] + end, + #pubsub_node{ + nodeid = {Host, ?PUBSUB:string_to_node(Node)}, + parents = [?PUBSUB:string_to_node(Parent)], + id = NodeId, + type = Type, + options = Options}. + +%% @spec (NodeRecord) -> ok | {error, Reason} +%% Record = mod_pubsub:pubsub_node() +set_node(Record) -> + {Host, Node} = Record#pubsub_node.nodeid, + Parent = case Record#pubsub_node.parents of + [] -> <<>>; + [First|_] -> First + end, + Type = Record#pubsub_node.type, + H = ?PUBSUB:escape(Host), + N = ?PUBSUB:escape(?PUBSUB:node_to_string(Node)), + P = ?PUBSUB:escape(?PUBSUB:node_to_string(Parent)), + NodeId = case nodeid(Host, Node) of + {result, OldNodeId} -> + catch ejabberd_odbc:sql_query_t( + ["delete from pubsub_node_option " + "where nodeid='", OldNodeId, "';"]), + catch ejabberd_odbc:sql_query_t( + ["update pubsub_node " + "set host='", H, "' " + "node='", N, "' " + "parent='", P, "' " + "type='", Type, "' " + "where nodeid='", OldNodeId, "';"]), + OldNodeId; + _ -> + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_node(host, node, parent, type) " + "values('", H, "', '", N, "', '", P, "', '", Type, "');"]), + case nodeid(Host, Node) of + {result, NewNodeId} -> NewNodeId; + _ -> none % this should not happen + end + end, + case NodeId of + none -> + {error, ?ERR_INTERNAL_SERVER_ERROR}; + _ -> + lists:foreach(fun({Key, Value}) -> + SKey = atom_to_list(Key), + SValue = ?PUBSUB:escape(lists:flatten(io_lib:fwrite("~p",[Value]))), + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_node_option(nodeid, name, val) " + "values('", NodeId, "', '", SKey, "', '", SValue, "');"]) + end, Record#pubsub_node.options), + {result, NodeId} + end. + +nodeid(Host, Node) -> + H = ?PUBSUB:escape(Host), + N = ?PUBSUB:escape(?PUBSUB:node_to_string(Node)), + case catch ejabberd_odbc:sql_query_t( + ["select nodeid " + "from pubsub_node " + "where host='", H, "' and node='", N, "';"]) + of + {selected, ["nodeid"], [{NodeId}]} -> + {result, NodeId}; + {'EXIT', _Reason} -> + {error, ?ERR_INTERNAL_SERVER_ERROR}; + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end. + +nodeowners(NodeId) -> + {result, Res} = node_hometree_odbc:get_node_affiliations(NodeId), + lists:foldl(fun({LJID, owner}, Acc) -> [LJID|Acc]; + (_, Acc) -> Acc + end, [], Res). diff --git a/src/mod_pubsub/nodetree_virtual.erl b/src/mod_pubsub/nodetree_virtual.erl index 5ce256140..ec35b91d9 100644 --- a/src/mod_pubsub/nodetree_virtual.erl +++ b/src/mod_pubsub/nodetree_virtual.erl @@ -10,13 +10,13 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% %%% -%%% @copyright 2006-2008 Process-one +%%% @copyright 2006-2009 ProcessOne %%% @author Christophe Romain <christophe.romain@process-one.net> %%% [http://www.process-one.net/] %%% @version {@vsn}, {@date} {@time} @@ -26,10 +26,9 @@ %%% @doc The module <strong>{@module}</strong> is the PubSub node tree plugin that %%% allow virtual nodes handling. %%% <p>PubSub node tree plugins are using the {@link gen_nodetree} behaviour.</p> -%%% <p><strong>The API isn't stabilized yet</strong>. The pubsub plugin -%%% development is still a work in progress. However, the system is already -%%% useable and useful as is. Please, send us comments, feedback and -%%% improvements.</p> +%%% <p>This plugin development is still a work in progress. Due to optimizations in +%%% mod_pubsub, this plugin can not work anymore without altering functioning. +%%% Please, send us comments, feedback and improvements.</p> -module(nodetree_virtual). -author('christophe.romain@process-one.net'). @@ -43,11 +42,16 @@ terminate/2, options/0, set_node/1, + get_node/3, get_node/2, + get_node/1, + get_nodes/2, get_nodes/1, + get_parentnodes/3, + get_parentnodes_tree/3, get_subnodes/3, - get_subnodes_tree/2, - create_node/5, + get_subnodes_tree/3, + create_node/6, delete_node/2 ]). @@ -81,19 +85,39 @@ options() -> set_node(_NodeRecord) -> ok. -%% @spec (Host, Node) -> pubsubNode() +%% @spec (Host, Node, From) -> pubsubNode() %% Host = mod_pubsub:host() %% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() %% @doc <p>Virtual node tree does not handle a node database. Any node is considered %% as existing. Node record contains default values.</p> +get_node(Host, Node, _From) -> + get_node(Host, Node). get_node(Host, Node) -> - #pubsub_node{nodeid = {Host, Node}}. + get_node({Host, Node}). +get_node({Host, _} = NodeId) -> + Record = #pubsub_node{nodeid = NodeId, id = NodeId}, + Module = list_to_atom("node_" ++ Record#pubsub_node.type), + Options = Module:options(), + Owners = [{"", Host, ""}], + Record#pubsub_node{owners = Owners, options = Options}. -%% @spec (Key) -> [pubsubNode()] +%% @spec (Host, From) -> [pubsubNode()] %% Host = mod_pubsub:host() | mod_pubsub:jid() +%% From = mod_pubsub:jid() %% @doc <p>Virtual node tree does not handle a node database. Any node is considered %% as existing. Nodes list can not be determined.</p> -get_nodes(_Key) -> +get_nodes(Host, _From) -> + get_nodes(Host). +get_nodes(_Host) -> + []. + +%% @spec (Host, Node, From) -> [pubsubNode()] +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() +%% @doc <p>Virtual node tree does not handle parent/child. Child list is empty.</p> +get_parentnodes(_Host, _Node, _From) -> []. %% @spec (Host, Node, From) -> [pubsubNode()] @@ -101,13 +125,26 @@ get_nodes(_Key) -> %% Node = mod_pubsub:pubsubNode() %% From = mod_pubsub:jid() %% @doc <p>Virtual node tree does not handle parent/child. Child list is empty.</p> -get_subnodes(_Host, _Node, _From) -> +get_parentnodes_tree(_Host, _Node, _From) -> []. -%% @spec (Host, Index) -> [pubsubNode()] +%% @spec (Host, Node, From) -> [pubsubNode()] %% Host = mod_pubsub:host() %% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() +%% @doc <p>Virtual node tree does not handle parent/child. Child list is empty.</p> +get_subnodes(Host, Node, _From) -> + get_subnodes(Host, Node). +get_subnodes(_Host, _Node) -> + []. + +%% @spec (Host, Node, From) -> [pubsubNode()] +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() %% @doc <p>Virtual node tree does not handle parent/child. Child list is empty.</p> +get_subnodes_tree(Host, Node, _From) -> + get_subnodes_tree(Host, Node). get_subnodes_tree(_Host, _Node) -> []. @@ -120,18 +157,13 @@ get_subnodes_tree(_Host, _Node) -> %% @doc <p>No node record is stored on database. Any valid node %% is considered as already created.</p> %% <p>default allowed nodes: /home/host/user/any/node/name</p> -create_node(_Host, Node, _Type, Owner, _Options) -> - UserName = Owner#jid.luser, - UserHost = Owner#jid.lserver, - case Node of - ["home", UserHost, UserName | _] -> {error, ?ERR_CONFLICT}; - _ -> {error, ?ERR_NOT_ALLOWED} - end. +create_node(Host, Node, _Type, _Owner, _Options, _Parents) -> + {error, {virtual, {Host, Node}}}. %% @spec (Host, Node) -> [mod_pubsub:node()] %% Host = mod_pubsub:host() %% Node = mod_pubsub:pubsubNode() %% @doc <p>Virtual node tree does not handle parent/child. %% node deletion just affects the corresponding node.</p> -delete_node(_Host, Node) -> - [Node]. +delete_node(Host, Node) -> + [get_node(Host, Node)]. diff --git a/src/mod_pubsub/pubsub.hrl b/src/mod_pubsub/pubsub.hrl index 25f60c36d..c1ce99b5e 100644 --- a/src/mod_pubsub/pubsub.hrl +++ b/src/mod_pubsub/pubsub.hrl @@ -10,13 +10,13 @@ %%% the License for the specific language governing rights and limitations %%% under the License. %%% -%%% The Initial Developer of the Original Code is Process-one. -%%% Portions created by Process-one are Copyright 2006-2008, Process-one +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne %%% All Rights Reserved.'' -%%% This software is copyright 2006-2008, Process-one. +%%% This software is copyright 2006-2009, ProcessOne. %%% %%% -%%% copyright 2006-2008 Process-one +%%% copyright 2006-2009 ProcessOne %%% %%% This file contains pubsub types definition. %%% ==================================================================== @@ -25,9 +25,11 @@ %% Pubsub constants -define(ERR_EXTENDED(E,C), mod_pubsub:extended_error(E,C)). +%% The actual limit can be configured with mod_pubsub's option max_items_node +-define(MAXITEMS, 10). + %% this is currently a hard limit. %% Would be nice to have it configurable. --define(MAXITEMS, 20). -define(MAX_PAYLOAD_SIZE, 60000). %% ------------------------------- @@ -62,7 +64,7 @@ %%% @type nodeType() = string(). %%% <p>The <tt>nodeType</tt> is a string containing the name of the PubSub %%% plugin to use to manage a given node. For example, it can be -%%% <tt>"default"</tt>, <tt>"collection"</tt> or <tt>"blog"</tt>.</p> +%%% <tt>"flat"</tt>, <tt>"hometree"</tt> or <tt>"blog"</tt>.</p> %%% @type jid() = #jid{ %%% user = string(), @@ -72,45 +74,50 @@ %%% lserver = string(), %%% lresource = string()}. -%%% @type usr() = {User::string(), Server::string(), Resource::string()}. +%%% @type ljid() = {User::string(), Server::string(), Resource::string()}. %%% @type affiliation() = none | owner | publisher | outcast. %%% @type subscription() = none | pending | unconfigured | subscribed. +%%% internal pubsub index table +-record(pubsub_index, {index, last, free}). + %%% @type pubsubNode() = #pubsub_node{ %%% nodeid = {Host::host(), Node::pubsubNode()}, -%%% parentid = {Host::host(), Node::pubsubNode()}, +%%% parentid = Node::pubsubNode(), +%%% nodeidx = int(), %%% type = nodeType(), -%%% owners = [usr()], %%% options = [nodeOption()]}. %%% <p>This is the format of the <tt>nodes</tt> table. The type of the table %%% is: <tt>set</tt>,<tt>ram/disc</tt>.</p> %%% <p>The <tt>parentid</tt> and <tt>type</tt> fields are indexed.</p> +%%% nodeidx can be anything you want. -record(pubsub_node, {nodeid, - parentid = {}, - type = "", + id, + parents = [], + type = "flat", owners = [], options = [] }). %%% @type pubsubState() = #pubsub_state{ -%%% stateid = {jid(), {Host::host(), Node::pubsubNode()}}, +%%% stateid = {ljid(), nodeidx()}, %%% items = [ItemId::string()], %%% affiliation = affiliation(), -%%% subscription = subscription()}. +%%% subscriptions = [subscription()]}. %%% <p>This is the format of the <tt>affiliations</tt> table. The type of the %%% table is: <tt>set</tt>,<tt>ram/disc</tt>.</p> -record(pubsub_state, {stateid, items = [], affiliation = none, - subscription = none + subscriptions = [] }). -%% @type pubsubItem() = #pubsub_item{ -%% itemid = {ItemId::string(), {Host::host(),Node::pubsubNode()}}, -%% creation = {JID::jid(), now()}, -%% modification = {JID::jid(), now()}, -%% payload = XMLContent::string()}. +%%% @type pubsubItem() = #pubsub_item{ +%%% itemid = {ItemId::string(), nodeidx()}, +%%% creation = {now(), ljid()}, +%%% modification = {now(), ljid()}, +%%% payload = XMLContent::string()}. %%% <p>This is the format of the <tt>published items</tt> table. The type of the %%% table is: <tt>set</tt>,<tt>disc</tt>,<tt>fragmented</tt>.</p> -record(pubsub_item, {itemid, @@ -120,12 +127,11 @@ }). -%% @type pubsubPresence() = #pubsub_presence{ -%% key = {Host::host(), User::string(), Server::string()}, -%% presence = list()}. -%%% <p>This is the format of the <tt>published presence</tt> table. The type of the -%%% table is: <tt>set</tt>,<tt>ram</tt>.</p> --record(pubsub_presence, {key, - resource - }). - +%% @type pubsubSubscription() = #pubsub_subscription{ +%% subid = string(), +%% state_key = {ljid(), pubsubNodeId()}, +%% options = [{atom(), term()}] +%% }. +%% <p>This is the format of the <tt>subscriptions</tt> table. The type of the +%% table is: <tt>set</tt>,<tt>ram/disc</tt>.</p> +-record(pubsub_subscription, {subid, options}). diff --git a/src/mod_pubsub/pubsub_db_odbc.erl b/src/mod_pubsub/pubsub_db_odbc.erl new file mode 100644 index 000000000..3df26c171 --- /dev/null +++ b/src/mod_pubsub/pubsub_db_odbc.erl @@ -0,0 +1,138 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% @author Pablo Polvorin <pablo.polvorin@process-one.net> +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== +-module(pubsub_db_odbc). +-author("pablo.polvorin@process-one.net"). + +-include("pubsub.hrl"). + +-export([add_subscription/1, + read_subscription/1, + delete_subscription/1, + update_subscription/1]). + + +%% TODO: Those -spec lines produce errors in old Erlang versions. +%% They can be enabled again in ejabberd 3.0 because it uses R12B or higher. +%% -spec read_subscription(SubID :: string()) -> {ok, #pubsub_subscription{}} | notfound. +read_subscription(SubID) -> + case ejabberd_odbc:sql_query_t( + ["select opt_name, opt_value " + "from pubsub_subscription_opt " + "where subid = '", ejabberd_odbc:escape(SubID), "'"]) of + {selected, ["opt_name", "opt_value"], []} -> + notfound; + + {selected, ["opt_name", "opt_value"], Options} -> + + {ok, #pubsub_subscription{subid = SubID, + options = lists:map(fun subscription_opt_from_odbc/1, Options)}} + end. + + + +%% -spec delete_subscription(SubID :: string()) -> ok. +delete_subscription(SubID) -> + ejabberd_odbc:sql_query_t(["delete from pubsub_subscription_opt " + "where subid = '", ejabberd_odbc:escape(SubID), "'"]), + ok. + + +%% -spec update_subscription(#pubsub_subscription{}) -> ok . +update_subscription(#pubsub_subscription{subid = SubId} = Sub) -> + delete_subscription(SubId), + add_subscription(Sub). + +%% -spec add_subscription(#pubsub_subscription{}) -> ok. +add_subscription(#pubsub_subscription{subid = SubId, options = Opts}) -> + EscapedSubId = ejabberd_odbc:escape(SubId), + lists:foreach(fun(Opt) -> + {OdbcOptName, OdbcOptValue} = subscription_opt_to_odbc(Opt), + ejabberd_odbc:sql_query_t( + ["insert into pubsub_subscription_opt(subid, opt_name, opt_value)" + "values ('", EscapedSubId, "','", OdbcOptName, "','", OdbcOptValue, "')"]) + end, Opts), + ok. + + + +%% -------------- Internal utilities ----------------------- +subscription_opt_from_odbc({"DELIVER", Value}) -> + {deliver, odbc_to_boolean(Value)}; +subscription_opt_from_odbc({"DIGEST", Value}) -> + {digest, odbc_to_boolean(Value)}; +subscription_opt_from_odbc({"DIGEST_FREQUENCY", Value}) -> + {digest_frequency, odbc_to_integer(Value)}; +subscription_opt_from_odbc({"EXPIRE", Value}) -> + {expire, odbc_to_timestamp(Value)}; +subscription_opt_from_odbc({"INCLUDE_BODY", Value}) -> + {include_body, odbc_to_boolean(Value)}; + +%%TODO: might be > than 1 show_values value??. +%% need to use compact all in only 1 opt. +subscription_opt_from_odbc({"SHOW_VALUES", Value}) -> + {show_values, Value}; +subscription_opt_from_odbc({"SUBSCRIPTION_TYPE", Value}) -> + {subscription_type, case Value of + "items" -> items; + "nodes" -> nodes + end}; + +subscription_opt_from_odbc({"SUBSCRIPTION_DEPTH", Value}) -> + {subscription_depth, case Value of + "all" -> all; + N -> odbc_to_integer(N) + end}. + +subscription_opt_to_odbc({deliver, Bool}) -> + {"DELIVER", boolean_to_odbc(Bool)}; +subscription_opt_to_odbc({digest, Bool}) -> + {"DIGEST", boolean_to_odbc(Bool)}; +subscription_opt_to_odbc({digest_frequency, Int}) -> + {"DIGEST_FREQUENCY", integer_to_odbc(Int)}; +subscription_opt_to_odbc({expire, Timestamp}) -> + {"EXPIRE", timestamp_to_odbc(Timestamp)}; +subscription_opt_to_odbc({include_body, Bool}) -> + {"INCLUDE_BODY", boolean_to_odbc(Bool)}; +subscription_opt_to_odbc({show_values, Values}) -> + {"SHOW_VALUES", Values}; +subscription_opt_to_odbc({subscription_type, Type}) -> + {"SUBSCRIPTION_TYPE", case Type of + items -> "items"; + nodes -> "nodes" + end}; +subscription_opt_to_odbc({subscription_depth, Depth}) -> + {"SUBSCRIPTION_DEPTH", case Depth of + all -> "all"; + N -> integer_to_odbc(N) + end}. + +integer_to_odbc(N) -> + integer_to_list(N). + +boolean_to_odbc(true) -> "1"; +boolean_to_odbc(false) -> "0". +timestamp_to_odbc(T) -> jlib:now_to_utc_string(T). + + +odbc_to_integer(N) -> list_to_integer(N). +odbc_to_boolean(B) -> B == "1". +odbc_to_timestamp(T) -> jlib:datetime_string_to_timestamp(T). diff --git a/src/mod_pubsub/pubsub_index.erl b/src/mod_pubsub/pubsub_index.erl new file mode 100644 index 000000000..2ba5d4b20 --- /dev/null +++ b/src/mod_pubsub/pubsub_index.erl @@ -0,0 +1,65 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% +%%% @copyright 2006-2009 ProcessOne +%%% @author Christophe Romain <christophe.romain@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%% important note: +%% new/1 and free/2 MUST be called inside a transaction bloc + +-module(pubsub_index). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). + +-export([init/3, new/1, free/2]). + +init(_Host, _ServerHost, _Opts) -> + mnesia:create_table(pubsub_index, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_index)}]). + +new(Index) -> + case mnesia:read({pubsub_index, Index}) of + [I] -> + case I#pubsub_index.free of + [] -> + Id = I#pubsub_index.last + 1, + mnesia:write(I#pubsub_index{last = Id}), + Id; + [Id|Free] -> + mnesia:write(I#pubsub_index{free = Free}), + Id + end; + _ -> + mnesia:write(#pubsub_index{index = Index, last = 1, free = []}), + 1 + end. + +free(Index, Id) -> + case mnesia:read({pubsub_index, Index}) of + [I] -> + Free = I#pubsub_index.free, + mnesia:write(I#pubsub_index{free = [Id|Free]}); + _ -> + ok + end. diff --git a/src/mod_pubsub/pubsub_odbc.patch b/src/mod_pubsub/pubsub_odbc.patch new file mode 100644 index 000000000..a23147bc5 --- /dev/null +++ b/src/mod_pubsub/pubsub_odbc.patch @@ -0,0 +1,761 @@ +--- mod_pubsub.erl 2009-10-20 16:33:47.000000000 +0200 ++++ mod_pubsub_odbc.erl 2009-10-20 16:33:26.000000000 +0200 +@@ -42,7 +42,7 @@ + %%% 6.2.3.1, 6.2.3.5, and 6.3. For information on subscription leases see + %%% XEP-0060 section 12.18. + +--module(mod_pubsub). ++-module(mod_pubsub_odbc). + -author('christophe.romain@process-one.net'). + -version('1.13-0'). + +@@ -54,9 +54,9 @@ + -include("jlib.hrl"). + -include("pubsub.hrl"). + +--define(STDTREE, "tree"). +--define(STDNODE, "flat"). +--define(PEPNODE, "pep"). ++-define(STDTREE, "tree_odbc"). ++-define(STDNODE, "flat_odbc"). ++-define(PEPNODE, "pep_odbc"). + + %% exports for hooks + -export([presence_probe/3, +@@ -102,7 +102,7 @@ + string_to_affiliation/1, + extended_error/2, + extended_error/3, +- rename_default_nodeplugin/0 ++ escape/1 + ]). + + %% API and gen_server callbacks +@@ -121,7 +121,7 @@ + -export([send_loop/1 + ]). + +--define(PROCNAME, ejabberd_mod_pubsub). ++-define(PROCNAME, ejabberd_mod_pubsub_odbc). + -define(PLUGIN_PREFIX, "node_"). + -define(TREE_PREFIX, "nodetree_"). + +@@ -215,8 +215,6 @@ + ok + end, + ejabberd_router:register_route(Host), +- update_node_database(Host, ServerHost), +- update_state_database(Host, ServerHost), + init_nodes(Host, ServerHost, NodeTree, Plugins), + State = #state{host = Host, + server_host = ServerHost, +@@ -269,207 +267,14 @@ + + init_nodes(Host, ServerHost, _NodeTree, Plugins) -> + %% TODO, this call should be done plugin side +- case lists:member("hometree", Plugins) of ++ case lists:member("hometree_odbc", Plugins) of + true -> +- create_node(Host, ServerHost, string_to_node("/home"), service_jid(Host), "hometree"), +- create_node(Host, ServerHost, string_to_node("/home/"++ServerHost), service_jid(Host), "hometree"); ++ create_node(Host, ServerHost, string_to_node("/home"), service_jid(Host), "hometree_odbc"), ++ create_node(Host, ServerHost, string_to_node("/home/"++ServerHost), service_jid(Host), "hometree_odbc"); + false -> + ok + end. + +-update_node_database(Host, ServerHost) -> +- mnesia:del_table_index(pubsub_node, type), +- mnesia:del_table_index(pubsub_node, parentid), +- case catch mnesia:table_info(pubsub_node, attributes) of +- [host_node, host_parent, info] -> +- ?INFO_MSG("upgrade node pubsub tables",[]), +- F = fun() -> +- {Result, LastIdx} = lists:foldl( +- fun({pubsub_node, NodeId, ParentId, {nodeinfo, Items, Options, Entities}}, {RecList, NodeIdx}) -> +- ItemsList = +- lists:foldl( +- fun({item, IID, Publisher, Payload}, Acc) -> +- C = {unknown, Publisher}, +- M = {now(), Publisher}, +- mnesia:write( +- #pubsub_item{itemid = {IID, NodeIdx}, +- creation = C, +- modification = M, +- payload = Payload}), +- [{Publisher, IID} | Acc] +- end, [], Items), +- Owners = +- dict:fold( +- fun(JID, {entity, Aff, Sub}, Acc) -> +- UsrItems = +- lists:foldl( +- fun({P, I}, IAcc) -> +- case P of +- JID -> [I | IAcc]; +- _ -> IAcc +- end +- end, [], ItemsList), +- mnesia:write({pubsub_state, +- {JID, NodeIdx}, +- UsrItems, +- Aff, +- Sub}), +- case Aff of +- owner -> [JID | Acc]; +- _ -> Acc +- end +- end, [], Entities), +- mnesia:delete({pubsub_node, NodeId}), +- {[#pubsub_node{nodeid = NodeId, +- id = NodeIdx, +- parents = [element(2, ParentId)], +- owners = Owners, +- options = Options} | +- RecList], NodeIdx + 1} +- end, {[], 1}, +- mnesia:match_object( +- {pubsub_node, {Host, '_'}, '_', '_'})), +- mnesia:write(#pubsub_index{index = node, last = LastIdx, free = []}), +- Result +- end, +- {atomic, NewRecords} = mnesia:transaction(F), +- {atomic, ok} = mnesia:delete_table(pubsub_node), +- {atomic, ok} = mnesia:create_table(pubsub_node, +- [{disc_copies, [node()]}, +- {attributes, record_info(fields, pubsub_node)}]), +- FNew = fun() -> lists:foreach(fun(Record) -> +- mnesia:write(Record) +- end, NewRecords) +- end, +- case mnesia:transaction(FNew) of +- {atomic, Result} -> +- ?INFO_MSG("Pubsub node tables updated correctly: ~p", [Result]); +- {aborted, Reason} -> +- ?ERROR_MSG("Problem updating Pubsub node tables:~n~p", [Reason]) +- end; +- [nodeid, parentid, type, owners, options] -> +- F = fun({pubsub_node, NodeId, {_, Parent}, Type, Owners, Options}) -> +- #pubsub_node{ +- nodeid = NodeId, +- id = 0, +- parents = [Parent], +- type = Type, +- owners = Owners, +- options = Options} +- end, +- mnesia:transform_table(pubsub_node, F, [nodeid, id, parents, type, owners, options]), +- FNew = fun() -> +- LastIdx = lists:foldl(fun(#pubsub_node{nodeid = NodeId} = PubsubNode, NodeIdx) -> +- mnesia:write(PubsubNode#pubsub_node{id = NodeIdx}), +- lists:foreach(fun(#pubsub_state{stateid = StateId} = State) -> +- {JID, _} = StateId, +- mnesia:delete({pubsub_state, StateId}), +- mnesia:write(State#pubsub_state{stateid = {JID, NodeIdx}}) +- end, mnesia:match_object(#pubsub_state{stateid = {'_', NodeId}, _ = '_'})), +- lists:foreach(fun(#pubsub_item{itemid = ItemId} = Item) -> +- {IID, _} = ItemId, +- {M1, M2} = Item#pubsub_item.modification, +- {C1, C2} = Item#pubsub_item.creation, +- mnesia:delete({pubsub_item, ItemId}), +- mnesia:write(Item#pubsub_item{itemid = {IID, NodeIdx}, +- modification = {M2, M1}, +- creation = {C2, C1}}) +- end, mnesia:match_object(#pubsub_item{itemid = {'_', NodeId}, _ = '_'})), +- NodeIdx + 1 +- end, 1, mnesia:match_object( +- {pubsub_node, {Host, '_'}, '_', '_', '_', '_', '_'}) +- ++ mnesia:match_object( +- {pubsub_node, {{'_', ServerHost, '_'}, '_'}, '_', '_', '_', '_', '_'})), +- mnesia:write(#pubsub_index{index = node, last = LastIdx, free = []}) +- end, +- case mnesia:transaction(FNew) of +- {atomic, Result} -> +- rename_default_nodeplugin(), +- ?INFO_MSG("Pubsub node tables updated correctly: ~p", [Result]); +- {aborted, Reason} -> +- ?ERROR_MSG("Problem updating Pubsub node tables:~n~p", [Reason]) +- end; +- [nodeid, id, parent, type, owners, options] -> +- F = fun({pubsub_node, NodeId, Id, Parent, Type, Owners, Options}) -> +- #pubsub_node{ +- nodeid = NodeId, +- id = Id, +- parents = [Parent], +- type = Type, +- owners = Owners, +- options = Options} +- end, +- mnesia:transform_table(pubsub_node, F, [nodeid, id, parents, type, owners, options]), +- rename_default_nodeplugin(); +- _ -> +- ok +- end, +- mnesia:transaction(fun() -> +- case catch mnesia:first(pubsub_node) of +- {_, L} when is_list(L) -> +- lists:foreach( +- fun({H, N}) when is_list(N) -> +- [Node] = mnesia:read({pubsub_node, {H, N}}), +- Type = Node#pubsub_node.type, +- BN = element(2, node_call(Type, path_to_node, [N])), +- BP = case [element(2, node_call(Type, path_to_node, [P])) || P <- Node#pubsub_node.parents] of +- [<<>>] -> []; +- Parents -> Parents +- end, +- mnesia:write(Node#pubsub_node{nodeid={H, BN}, parents=BP}), +- mnesia:delete({pubsub_node, {H, N}}); +- (_) -> +- ok +- end, mnesia:all_keys(pubsub_node)); +- _ -> +- ok +- end +- end). +- +-rename_default_nodeplugin() -> +- lists:foreach(fun(Node) -> +- mnesia:dirty_write(Node#pubsub_node{type = "hometree"}) +- end, mnesia:dirty_match_object(#pubsub_node{type = "default", _ = '_'})). +- +-update_state_database(_Host, _ServerHost) -> +- case catch mnesia:table_info(pubsub_state, attributes) of +- [stateid, items, affiliation, subscription] -> +- ?INFO_MSG("upgrade state pubsub tables", []), +- F = fun ({pubsub_state, {JID, NodeID}, Items, Aff, Sub}, Acc) -> +- Subs = case Sub of +- none -> +- []; +- _ -> +- {result, SubID} = pubsub_subscription:subscribe_node(JID, NodeID, []), +- [{Sub, SubID}] +- end, +- NewState = #pubsub_state{stateid = {JID, NodeID}, +- items = Items, +- affiliation = Aff, +- subscriptions = Subs}, +- [NewState | Acc] +- end, +- {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3, +- [F, [], pubsub_state]), +- {atomic, ok} = mnesia:delete_table(pubsub_state), +- {atomic, ok} = mnesia:create_table(pubsub_state, +- [{disc_copies, [node()]}, +- {attributes, record_info(fields, pubsub_state)}]), +- FNew = fun () -> +- lists:foreach(fun mnesia:write/1, NewRecs) +- end, +- case mnesia:transaction(FNew) of +- {atomic, Result} -> +- ?INFO_MSG("Pubsub state tables updated correctly: ~p", +- [Result]); +- {aborted, Reason} -> +- ?ERROR_MSG("Problem updating Pubsub state tables:~n~p", +- [Reason]) +- end; +- _ -> +- ok +- end. +- + send_queue(State, Msg) -> + Pid = State#state.send_loop, + case is_process_alive(Pid) of +@@ -492,17 +297,15 @@ + %% for each node From is subscribed to + %% and if the node is so configured, send the last published item to From + lists:foreach(fun(PType) -> +- {result, Subscriptions} = node_action(Host, PType, get_entity_subscriptions, [Host, JID]), ++ Subscriptions = case catch node_action(Host, PType, get_entity_subscriptions_for_send_last, [Host, JID]) of ++ {result, S} -> S; ++ _ -> [] ++ end, + lists:foreach( + fun({Node, subscribed, _, SubJID}) -> + if (SubJID == LJID) or (SubJID == BJID) -> +- #pubsub_node{nodeid = {H, N}, type = Type, id = NodeId, options = Options} = Node, +- case get_option(Options, send_last_published_item) of +- on_sub_and_presence -> +- send_items(H, N, NodeId, Type, LJID, last); +- _ -> +- ok +- end; ++ #pubsub_node{nodeid = {H, N}, type = Type, id = NodeId} = Node, ++ send_items(H, N, NodeId, Type, LJID, last); + true -> + % resource not concerned about that subscription + ok +@@ -825,10 +628,10 @@ + {result, Subscriptions} = node_action(Host, PType, get_entity_subscriptions, [Host, Subscriber]), + lists:foreach(fun + ({Node, subscribed, _, JID}) -> +- #pubsub_node{options = Options, owners = Owners, type = Type, id = NodeId} = Node, ++ #pubsub_node{options = Options, type = Type, id = NodeId} = Node, + case get_option(Options, access_model) of + presence -> +- case lists:member(BJID, Owners) of ++ case lists:member(BJID, node_owners(Host, Type, NodeId)) of + true -> + node_action(Host, Type, unsubscribe_node, [NodeId, Subscriber, JID, all]); + false -> +@@ -943,7 +746,8 @@ + sub_el = SubEl} = IQ -> + {xmlelement, _, QAttrs, _} = SubEl, + Node = xml:get_attr_s("node", QAttrs), +- Res = case iq_disco_items(Host, Node, From) of ++ Rsm = jlib:rsm_decode(IQ), ++ Res = case iq_disco_items(Host, Node, From, Rsm) of + {result, IQRes} -> + jlib:iq_to_xml( + IQ#iq{type = result, +@@ -1048,7 +852,7 @@ + [] -> + ["leaf"]; %% No sub-nodes: it's a leaf node + _ -> +- case node_call(Type, get_items, [NodeId, From]) of ++ case node_call(Type, get_items, [NodeId, From, none]) of + {result, []} -> ["collection"]; + {result, _} -> ["leaf", "collection"]; + _ -> [] +@@ -1064,8 +868,9 @@ + []; + true -> + [{xmlelement, "feature", [{"var", ?NS_PUBSUB}], []} | +- lists:map(fun(T) -> +- {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++T}], []} ++ lists:map(fun ++ ("rsm")-> {xmlelement, "feature", [{"var", ?NS_RSM}], []}; ++ (T) -> {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++T}], []} + end, features(Type))] + end, + %% TODO: add meta-data info (spec section 5.4) +@@ -1093,21 +898,22 @@ + {xmlelement, "feature", [{"var", ?NS_DISCO_ITEMS}], []}, + {xmlelement, "feature", [{"var", ?NS_PUBSUB}], []}, + {xmlelement, "feature", [{"var", ?NS_VCARD}], []}] ++ +- lists:map(fun(Feature) -> +- {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++Feature}], []} ++ lists:map(fun ++ ("rsm")-> {xmlelement, "feature", [{"var", ?NS_RSM}], []}; ++ (T) -> {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++T}], []} + end, features(Host, Node))}; + _ -> + node_disco_info(Host, Node, From) + end. + +-iq_disco_items(Host, [], From) -> ++iq_disco_items(Host, [], From, _RSM) -> + {result, lists:map( + fun(#pubsub_node{nodeid = {_, SubNode}, type = Type}) -> + {result, Path} = node_call(Type, node_to_path, [SubNode]), + [Name|_] = lists:reverse(Path), + {xmlelement, "item", [{"jid", Host}, {"name", Name}|nodeAttr(SubNode)], []} + end, tree_action(Host, get_subnodes, [Host, <<>>, From]))}; +-iq_disco_items(Host, Item, From) -> ++iq_disco_items(Host, Item, From, RSM) -> + case string:tokens(Item, "!") of + [_SNode, _ItemID] -> + {result, []}; +@@ -1115,10 +921,10 @@ + Node = string_to_node(SNode), + Action = + fun(#pubsub_node{type = Type, id = NodeId}) -> +- % TODO call get_items/6 instead for access control (EJAB-1033) +- NodeItems = case node_call(Type, get_items, [NodeId, From]) of ++ %% TODO call get_items/6 instead for access control (EJAB-1033) ++ {NodeItems, RsmOut} = case node_call(Type, get_items, [NodeId, From, RSM]) of + {result, I} -> I; +- _ -> [] ++ _ -> {[], none} + end, + Nodes = lists:map( + fun(#pubsub_node{nodeid = {_, SubNode}}) -> +@@ -1129,7 +935,7 @@ + {result, Name} = node_call(Type, get_item_name, [Host, Node, RN]), + {xmlelement, "item", [{"jid", Host}, {"name", Name}], []} + end, NodeItems), +- {result, Nodes ++ Items} ++ {result, Nodes ++ Items ++ jlib:rsm_encode(RsmOut)} + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; +@@ -1258,7 +1064,8 @@ + (_, Acc) -> + Acc + end, [], xml:remove_cdata(Els)), +- get_items(Host, Node, From, SubId, MaxItems, ItemIDs); ++ RSM = jlib:rsm_decode(SubEl), ++ get_items(Host, Node, From, SubId, MaxItems, ItemIDs, RSM); + {get, "subscriptions"} -> + get_subscriptions(Host, Node, From, Plugins); + {get, "affiliations"} -> +@@ -1281,7 +1088,9 @@ + + iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) -> + {xmlelement, _, _, SubEls} = SubEl, +- Action = xml:remove_cdata(SubEls), ++ Action = lists:filter(fun({xmlelement, "set", _, _}) -> false; ++ (_) -> true ++ end, xml:remove_cdata(SubEls)), + case Action of + [{xmlelement, Name, Attrs, Els}] -> + Node = string_to_node(xml:get_attr_s("node", Attrs)), +@@ -1404,7 +1213,8 @@ + _ -> [] + end + end, +- case transaction(fun () -> {result, lists:flatmap(Tr, Plugins)} end, ++ case transaction(Host, ++ fun () -> {result, lists:flatmap(Tr, Plugins)} end, + sync_dirty) of + {result, Res} -> Res; + Err -> Err +@@ -1444,7 +1254,7 @@ + + %%% authorization handling + +-send_authorization_request(#pubsub_node{owners = Owners, nodeid = {Host, Node}}, Subscriber) -> ++send_authorization_request(#pubsub_node{nodeid = {Host, Node}, type = Type, id = NodeId}, Subscriber) -> + Lang = "en", %% TODO fix + Stanza = {xmlelement, "message", + [], +@@ -1473,7 +1283,7 @@ + [{xmlelement, "value", [], [{xmlcdata, "false"}]}]}]}]}, + lists:foreach(fun(Owner) -> + ejabberd_router ! {route, service_jid(Host), jlib:make_jid(Owner), Stanza} +- end, Owners). ++ end, node_owners(Host, Type, NodeId)). + + find_authorization_response(Packet) -> + {xmlelement, _Name, _Attrs, Els} = Packet, +@@ -1537,8 +1347,8 @@ + "true" -> true; + _ -> false + end, +- Action = fun(#pubsub_node{type = Type, owners = Owners, id = NodeId}) -> +- IsApprover = lists:member(jlib:jid_tolower(jlib:jid_remove_resource(From)), Owners), ++ Action = fun(#pubsub_node{type = Type, id = NodeId}) -> ++ IsApprover = lists:member(jlib:jid_tolower(jlib:jid_remove_resource(From)), node_owners_call(Type, NodeId)), + {result, Subscriptions} = node_call(Type, get_subscriptions, [NodeId, Subscriber]), + if + not IsApprover -> +@@ -1729,7 +1539,7 @@ + Reply = [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "create", nodeAttr(Node), + []}]}], +- case transaction(CreateNode, transaction) of ++ case transaction(Host, CreateNode, transaction) of + {result, {Result, broadcast}} -> + %%Lang = "en", %% TODO: fix + %%OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), +@@ -1837,7 +1647,7 @@ + %%<li>The node does not exist.</li> + %%</ul> + subscribe_node(Host, Node, From, JID, Configuration) -> +- SubOpts = case pubsub_subscription:parse_options_xform(Configuration) of ++ SubOpts = case pubsub_subscription_odbc:parse_options_xform(Configuration) of + {result, GoodSubOpts} -> GoodSubOpts; + _ -> invalid + end, +@@ -1845,7 +1655,7 @@ + error -> {"", "", ""}; + J -> jlib:jid_tolower(J) + end, +- Action = fun(#pubsub_node{options = Options, owners = [Owner|_], type = Type, id = NodeId}) -> ++ Action = fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> + Features = features(Type), + SubscribeFeature = lists:member("subscribe", Features), + OptionsFeature = lists:member("subscription-options", Features), +@@ -1864,9 +1674,13 @@ + {"", "", ""} -> + {false, false}; + _ -> +- {OU, OS, _} = Owner, +- get_roster_info(OU, OS, +- Subscriber, AllowedGroups) ++ case node_owners_call(Type, NodeId) of ++ [{OU, OS, _}|_] -> ++ get_roster_info(OU, OS, ++ Subscriber, AllowedGroups); ++ _ -> ++ {false, false} ++ end + end + end, + if +@@ -2197,7 +2011,7 @@ + %% <p>The permission are not checked in this function.</p> + %% @todo We probably need to check that the user doing the query has the right + %% to read the items. +-get_items(Host, Node, From, SubId, SMaxItems, ItemIDs) -> ++get_items(Host, Node, From, SubId, SMaxItems, ItemIDs, RSM) -> + MaxItems = + if + SMaxItems == "" -> get_max_items_node(Host); +@@ -2236,11 +2050,11 @@ + node_call(Type, get_items, + [NodeId, From, + AccessModel, PresenceSubscription, RosterGroup, +- SubId]) ++ SubId, RSM]) + end + end, + case transaction(Host, Node, Action, sync_dirty) of +- {result, {_, Items}} -> ++ {result, {_, {Items, RSMOut}}} -> + SendItems = case ItemIDs of + [] -> + Items; +@@ -2253,7 +2067,8 @@ + %% number of items sent to MaxItems: + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "items", nodeAttr(Node), +- itemsEls(lists:sublist(SendItems, MaxItems))}]}]}; ++ itemsEls(lists:sublist(SendItems, MaxItems))} ++ | jlib:rsm_encode(RSMOut)]}]}; + Error -> + Error + end +@@ -2285,16 +2100,27 @@ + %% @doc <p>Resend the items of a node to the user.</p> + %% @todo use cache-last-item feature + send_items(Host, Node, NodeId, Type, LJID, last) -> +- case get_cached_item(Host, NodeId) of ++ Stanza = case get_cached_item(Host, NodeId) of + undefined -> +- send_items(Host, Node, NodeId, Type, LJID, 1); ++ % special ODBC optimization, works only with node_hometree_odbc, node_flat_odbc and node_pep_odbc ++ case node_action(Host, Type, get_last_items, [NodeId, LJID, 1]) of ++ {result, [LastItem]} -> ++ {ModifNow, ModifLjid} = LastItem#pubsub_item.modification, ++ event_stanza_with_delay( ++ [{xmlelement, "items", nodeAttr(Node), ++ itemsEls([LastItem])}], ModifNow, ModifLjid); ++ _ -> ++ event_stanza( ++ [{xmlelement, "items", nodeAttr(Node), ++ itemsEls([])}]) ++ end; + LastItem -> + {ModifNow, ModifLjid} = LastItem#pubsub_item.modification, +- Stanza = event_stanza_with_delay( ++ event_stanza_with_delay( + [{xmlelement, "items", nodeAttr(Node), +- itemsEls([LastItem])}], ModifNow, ModifLjid), +- ejabberd_router ! {route, service_jid(Host), jlib:make_jid(LJID), Stanza} +- end; ++ itemsEls([LastItem])}], ModifNow, ModifLjid) ++ end, ++ ejabberd_router ! {route, service_jid(Host), jlib:make_jid(LJID), Stanza}; + send_items(Host, Node, NodeId, Type, LJID, Number) -> + ToSend = case node_action(Host, Type, get_items, [NodeId, LJID]) of + {result, []} -> +@@ -2420,29 +2246,12 @@ + error -> + {error, ?ERR_BAD_REQUEST}; + _ -> +- Action = fun(#pubsub_node{owners = Owners, type = Type, id = NodeId}=N) -> +- case lists:member(Owner, Owners) of ++ Action = fun(#pubsub_node{type = Type, id = NodeId}) -> ++ case lists:member(Owner, node_owners_call(Type, NodeId)) of + true -> + lists:foreach( + fun({JID, Affiliation}) -> +- node_call(Type, set_affiliation, [NodeId, JID, Affiliation]), +- case Affiliation of +- owner -> +- NewOwner = jlib:jid_tolower(jlib:jid_remove_resource(JID)), +- NewOwners = [NewOwner|Owners], +- tree_call(Host, set_node, [N#pubsub_node{owners = NewOwners}]); +- none -> +- OldOwner = jlib:jid_tolower(jlib:jid_remove_resource(JID)), +- case lists:member(OldOwner, Owners) of +- true -> +- NewOwners = Owners--[OldOwner], +- tree_call(Host, set_node, [N#pubsub_node{owners = NewOwners}]); +- _ -> +- ok +- end; +- _ -> +- ok +- end ++ node_call(Type, set_affiliation, [NodeId, JID, Affiliation]) + end, Entities), + {result, []}; + _ -> +@@ -2495,11 +2304,11 @@ + end. + + read_sub(Subscriber, Node, NodeID, SubID, Lang) -> +- case pubsub_subscription:get_subscription(Subscriber, NodeID, SubID) of ++ case pubsub_subscription_odbc:get_subscription(Subscriber, NodeID, SubID) of + {error, notfound} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + {result, #pubsub_subscription{options = Options}} -> +- {result, XdataEl} = pubsub_subscription:get_options_xform(Lang, Options), ++ {result, XdataEl} = pubsub_subscription_odbc:get_options_xform(Lang, Options), + OptionsEl = {xmlelement, "options", [{"jid", jlib:jid_to_string(Subscriber)}, + {"subid", SubID}|nodeAttr(Node)], + [XdataEl]}, +@@ -2525,7 +2334,7 @@ + end. + + set_options_helper(Configuration, JID, NodeID, SubID, Type) -> +- SubOpts = case pubsub_subscription:parse_options_xform(Configuration) of ++ SubOpts = case pubsub_subscription_odbc:parse_options_xform(Configuration) of + {result, GoodSubOpts} -> GoodSubOpts; + _ -> invalid + end, +@@ -2554,7 +2363,7 @@ + write_sub(_Subscriber, _NodeID, _SubID, invalid) -> + {error, extended_error(?ERR_BAD_REQUEST, "invalid-options")}; + write_sub(Subscriber, NodeID, SubID, Options) -> +- case pubsub_subscription:set_subscription(Subscriber, NodeID, SubID, Options) of ++ case pubsub_subscription_odbc:set_subscription(Subscriber, NodeID, SubID, Options) of + {error, notfound} -> + {error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + {result, _} -> +@@ -2722,8 +2531,8 @@ + {"subscription", subscription_to_string(Sub)} | nodeAttr(Node)], []}]}]}, + ejabberd_router ! {route, service_jid(Host), jlib:make_jid(JID), Stanza} + end, +- Action = fun(#pubsub_node{owners = Owners, type = Type, id = NodeId}) -> +- case lists:member(Owner, Owners) of ++ Action = fun(#pubsub_node{type = Type, id = NodeId}) -> ++ case lists:member(Owner, node_owners_call(Type, NodeId)) of + true -> + Result = lists:foldl(fun({JID, Subscription, SubId}, Acc) -> + +@@ -3007,7 +2816,7 @@ + {Depth, [{N, get_node_subs(N)} || N <- Nodes]} + end, tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]))} + end, +- case transaction(Action, sync_dirty) of ++ case transaction(Host, Action, sync_dirty) of + {result, CollSubs} -> CollSubs; + _ -> [] + end. +@@ -3021,9 +2830,9 @@ + + get_options_for_subs(NodeID, Subs) -> + lists:foldl(fun({JID, subscribed, SubID}, Acc) -> +- case pubsub_subscription:read_subscription(JID, NodeID, SubID) of ++ case pubsub_subscription_odbc:get_subscription(JID, NodeID, SubID) of + {error, notfound} -> [{JID, SubID, []} | Acc]; +- #pubsub_subscription{options = Options} -> [{JID, SubID, Options} | Acc]; ++ {result, #pubsub_subscription{options = Options}} -> [{JID, SubID, Options} | Acc]; + _ -> Acc + end; + (_, Acc) -> +@@ -3221,6 +3030,30 @@ + Result + end. + ++%% @spec (Host, Type, NodeId) -> [ljid()] ++%% NodeId = pubsubNodeId() ++%% @doc <p>Return list of node owners.</p> ++node_owners(Host, Type, NodeId) -> ++ case node_action(Host, Type, get_node_affiliations, [NodeId]) of ++ {result, Affiliations} -> ++ lists:foldl( ++ fun({LJID, owner}, Acc) -> [LJID|Acc]; ++ (_, Acc) -> Acc ++ end, [], Affiliations); ++ _ -> ++ [] ++ end. ++node_owners_call(Type, NodeId) -> ++ case node_call(Type, get_node_affiliations, [NodeId]) of ++ {result, Affiliations} -> ++ lists:foldl( ++ fun({LJID, owner}, Acc) -> [LJID|Acc]; ++ (_, Acc) -> Acc ++ end, [], Affiliations); ++ _ -> ++ [] ++ end. ++ + %% @spec (Host, Options) -> MaxItems + %% Host = host() + %% Options = [Option] +@@ -3607,7 +3440,13 @@ + tree_action(Host, Function, Args) -> + ?DEBUG("tree_action ~p ~p ~p",[Host,Function,Args]), + Fun = fun() -> tree_call(Host, Function, Args) end, +- catch mnesia:sync_dirty(Fun). ++ case catch ejabberd_odbc:sql_bloc(odbc_conn(Host), Fun) of ++ {atomic, Result} -> ++ Result; ++ {aborted, Reason} -> ++ ?ERROR_MSG("transaction return internal error: ~p~n",[{aborted, Reason}]), ++ {error, ?ERR_INTERNAL_SERVER_ERROR} ++ end. + + %% @doc <p>node plugin call.</p> + node_call(Type, Function, Args) -> +@@ -3627,13 +3466,13 @@ + + node_action(Host, Type, Function, Args) -> + ?DEBUG("node_action ~p ~p ~p ~p",[Host,Type,Function,Args]), +- transaction(fun() -> ++ transaction(Host, fun() -> + node_call(Type, Function, Args) + end, sync_dirty). + + %% @doc <p>plugin transaction handling.</p> + transaction(Host, Node, Action, Trans) -> +- transaction(fun() -> ++ transaction(Host, fun() -> + case tree_call(Host, get_node, [Host, Node]) of + N when is_record(N, pubsub_node) -> + case Action(N) of +@@ -3646,8 +3485,14 @@ + end + end, Trans). + +-transaction(Fun, Trans) -> +- case catch mnesia:Trans(Fun) of ++transaction(Host, Fun, Trans) -> ++ transaction_retry(Host, Fun, Trans, 2). ++transaction_retry(Host, Fun, Trans, Count) -> ++ SqlFun = case Trans of ++ transaction -> sql_transaction; ++ _ -> sql_bloc ++ end, ++ case catch ejabberd_odbc:SqlFun(odbc_conn(Host), Fun) of + {result, Result} -> {result, Result}; + {error, Error} -> {error, Error}; + {atomic, {result, Result}} -> {result, Result}; +@@ -3655,6 +3500,15 @@ + {aborted, Reason} -> + ?ERROR_MSG("transaction return internal error: ~p~n", [{aborted, Reason}]), + {error, ?ERR_INTERNAL_SERVER_ERROR}; ++ {'EXIT', {timeout, _} = Reason} -> ++ case Count of ++ 0 -> ++ ?ERROR_MSG("transaction return internal error: ~p~n", [{'EXIT', Reason}]), ++ {error, ?ERR_INTERNAL_SERVER_ERROR}; ++ N -> ++ erlang:yield(), ++ transaction_retry(Host, Fun, Trans, N-1) ++ end; + {'EXIT', Reason} -> + ?ERROR_MSG("transaction return internal error: ~p~n", [{'EXIT', Reason}]), + {error, ?ERR_INTERNAL_SERVER_ERROR}; +@@ -3663,6 +3517,17 @@ + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + ++odbc_conn({_U, Host, _R})-> ++ Host; ++odbc_conn(Host) -> ++ Host--"pubsub.". %% TODO, improve that for custom host ++ ++%% escape value for database storage ++escape({_U, _H, _R}=JID)-> ++ ejabberd_odbc:escape(jlib:jid_to_string(JID)); ++escape(Value)-> ++ ejabberd_odbc:escape(Value). ++ + %%%% helpers + + %% Add pubsub-specific error element diff --git a/src/mod_pubsub/pubsub_subscription.erl b/src/mod_pubsub/pubsub_subscription.erl new file mode 100644 index 000000000..15efd9f86 --- /dev/null +++ b/src/mod_pubsub/pubsub_subscription.erl @@ -0,0 +1,322 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% @author Brian Cully <bjc@kublai.com> +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(pubsub_subscription). +-author("bjc@kublai.com"). + +%% API +-export([init/0, + subscribe_node/3, + unsubscribe_node/3, + get_subscription/3, + set_subscription/4, + get_options_xform/2, + parse_options_xform/1]). + +% Internal function also exported for use in transactional bloc from pubsub plugins +-export([add_subscription/3, + delete_subscription/3, + read_subscription/3, + write_subscription/4]). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-define(PUBSUB_DELIVER, "pubsub#deliver"). +-define(PUBSUB_DIGEST, "pubsub#digest"). +-define(PUBSUB_DIGEST_FREQUENCY, "pubsub#digest_frequency"). +-define(PUBSUB_EXPIRE, "pubsub#expire"). +-define(PUBSUB_INCLUDE_BODY, "pubsub#include_body"). +-define(PUBSUB_SHOW_VALUES, "pubsub#show-values"). +-define(PUBSUB_SUBSCRIPTION_TYPE, "pubsub#subscription_type"). +-define(PUBSUB_SUBSCRIPTION_DEPTH, "pubsub#subscription_depth"). + +-define(DELIVER_LABEL, + "Whether an entity wants to receive or disable notifications"). +-define(DIGEST_LABEL, + "Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually"). +-define(DIGEST_FREQUENCY_LABEL, + "The minimum number of milliseconds between sending any two notification digests"). +-define(EXPIRE_LABEL, + "The DateTime at which a leased subscription will end or has ended"). +-define(INCLUDE_BODY_LABEL, + "Whether an entity wants to receive an XMPP message body in addition to the payload format"). +-define(SHOW_VALUES_LABEL, + "The presence states for which an entity wants to receive notifications"). +-define(SUBSCRIPTION_TYPE_LABEL, + "Type of notification to receive"). +-define(SUBSCRIPTION_DEPTH_LABEL, + "Depth from subscription for which to receive notifications"). + +-define(SHOW_VALUE_AWAY_LABEL, "XMPP Show Value of Away"). +-define(SHOW_VALUE_CHAT_LABEL, "XMPP Show Value of Chat"). +-define(SHOW_VALUE_DND_LABEL, "XMPP Show Value of DND (Do Not Disturb)"). +-define(SHOW_VALUE_ONLINE_LABEL, "Mere Availability in XMPP (No Show Value)"). +-define(SHOW_VALUE_XA_LABEL, "XMPP Show Value of XA (Extended Away)"). + +-define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, + "Receive notification of new items only"). +-define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, + "Receive notification of new nodes only"). + +-define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, + "Receive notification from direct child nodes only"). +-define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, + "Receive notification from all descendent nodes"). + +%%==================================================================== +%% API +%%==================================================================== +init() -> + ok = create_table(). + +subscribe_node(JID, NodeID, Options) -> + case catch mnesia:sync_dirty(fun add_subscription/3, + [JID, NodeID, Options]) of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} + end. + +unsubscribe_node(JID, NodeID, SubID) -> + case catch mnesia:sync_dirty(fun delete_subscription/3, + [JID, NodeID, SubID]) of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} + end. + +get_subscription(JID, NodeID, SubID) -> + case catch mnesia:sync_dirty(fun read_subscription/3, + [JID, NodeID, SubID]) of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} + end. + +set_subscription(JID, NodeID, SubID, Options) -> + case catch mnesia:sync_dirty(fun write_subscription/4, + [JID, NodeID, SubID, Options]) of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} + end. + +get_options_xform(Lang, Options) -> + Keys = [deliver, show_values, subscription_type, subscription_depth], + XFields = [get_option_xfield(Lang, Key, Options) || Key <- Keys], + + {result, {xmlelement, "x", [{"xmlns", ?NS_XDATA}], + [{xmlelement, "field", [{"var", "FORM_TYPE"}, {"type", "hidden"}], + [{xmlelement, "value", [], + [{xmlcdata, ?NS_PUBSUB_SUB_OPTIONS}]}]}] ++ XFields}}. + +parse_options_xform(XFields) -> + case xml:remove_cdata(XFields) of + [] -> {result, []}; + [{xmlelement, "x", _Attrs, _Els} = XEl] -> + case jlib:parse_xdata_submit(XEl) of + XData when is_list(XData) -> + case set_xoption(XData, []) of + Opts when is_list(Opts) -> {result, Opts}; + Other -> Other + end; + Other -> + Other + end; + Other -> + Other + end. + +%%==================================================================== +%% Internal functions +%%==================================================================== +create_table() -> + case mnesia:create_table(pubsub_subscription, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_subscription)}, + {type, set}]) of + {atomic, ok} -> ok; + {aborted, {already_exists, _}} -> ok; + Other -> Other + end. + +add_subscription(_JID, _NodeID, Options) -> + SubID = make_subid(), + mnesia:write(#pubsub_subscription{subid = SubID, options = Options}), + SubID. + +delete_subscription(_JID, _NodeID, SubID) -> + mnesia:delete({pubsub_subscription, SubID}). + +read_subscription(_JID, _NodeID, SubID) -> + case mnesia:read({pubsub_subscription, SubID}) of + [Sub] -> Sub; + _ -> {error, notfound} + end. + +write_subscription(JID, NodeID, SubID, Options) -> + case read_subscription(JID, NodeID, SubID) of + {error, notfound} -> {error, notfound}; + Sub -> mnesia:write(Sub#pubsub_subscription{options = Options}) + end. + +make_subid() -> + {T1, T2, T3} = now(), + lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). + +%% +%% Subscription XForm processing. +%% + +%% Return processed options, with types converted and so forth, using +%% Opts as defaults. +set_xoption([], Opts) -> + Opts; +set_xoption([{Var, Value} | T], Opts) -> + NewOpts = case var_xfield(Var) of + {error, _} -> + Opts; + Key -> + Val = val_xfield(Key, Value), + lists:keystore(Key, 1, Opts, {Key, Val}) + end, + set_xoption(T, NewOpts). + +%% Return the options list's key for an XForm var. +var_xfield(?PUBSUB_DELIVER) -> deliver; +var_xfield(?PUBSUB_DIGEST) -> digest; +var_xfield(?PUBSUB_DIGEST_FREQUENCY) -> digest_frequency; +var_xfield(?PUBSUB_EXPIRE) -> expire; +var_xfield(?PUBSUB_INCLUDE_BODY) -> include_body; +var_xfield(?PUBSUB_SHOW_VALUES) -> show_values; +var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> subscription_type; +var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> subscription_depth; +var_xfield(_) -> {error, badarg}. + +%% Convert Values for option list's Key. +val_xfield(deliver, [Val]) -> xopt_to_bool(Val); +val_xfield(digest, [Val]) -> xopt_to_bool(Val); +val_xfield(digest_frequency, [Val]) -> list_to_integer(Val); +val_xfield(expire, [Val]) -> jlib:datetime_string_to_timestamp(Val); +val_xfield(include_body, [Val]) -> xopt_to_bool(Val); +val_xfield(show_values, Vals) -> Vals; +val_xfield(subscription_type, ["items"]) -> items; +val_xfield(subscription_type, ["nodes"]) -> nodes; +val_xfield(subscription_depth, ["all"]) -> all; +val_xfield(subscription_depth, [Depth]) -> + case catch list_to_integer(Depth) of + N when is_integer(N) -> N; + _ -> {error, ?ERR_NOT_ACCEPTABLE} + end. + +%% Convert XForm booleans to Erlang booleans. +xopt_to_bool("0") -> false; +xopt_to_bool("1") -> true; +xopt_to_bool("false") -> false; +xopt_to_bool("true") -> true; +xopt_to_bool(_) -> {error, ?ERR_NOT_ACCEPTABLE}. + +%% Return a field for an XForm for Key, with data filled in, if +%% applicable, from Options. +get_option_xfield(Lang, Key, Options) -> + Var = xfield_var(Key), + Label = xfield_label(Key), + {Type, OptEls} = type_and_options(xfield_type(Key), Lang), + Vals = case lists:keysearch(Key, 1, Options) of + {value, {_, Val}} -> + [tr_xfield_values(Vals) || Vals <- xfield_val(Key, Val)]; + false -> + [] + end, + {xmlelement, "field", + [{"var", Var}, {"type", Type}, + {"label", translate:translate(Lang, Label)}], + OptEls ++ Vals}. + +type_and_options({Type, Options}, Lang) -> + {Type, [tr_xfield_options(O, Lang) || O <- Options]}; +type_and_options(Type, _Lang) -> + {Type, []}. + +tr_xfield_options({Value, Label}, Lang) -> + {xmlelement, "option", + [{"label", translate:translate(Lang, Label)}], [{xmlelement, "value", [], + [{xmlcdata, Value}]}]}. + +tr_xfield_values(Value) -> + {xmlelement, "value", [], [{xmlcdata, Value}]}. + +%% Return the XForm variable name for a subscription option key. +xfield_var(deliver) -> ?PUBSUB_DELIVER; +xfield_var(digest) -> ?PUBSUB_DIGEST; +xfield_var(digest_frequency) -> ?PUBSUB_DIGEST_FREQUENCY; +xfield_var(expire) -> ?PUBSUB_EXPIRE; +xfield_var(include_body) -> ?PUBSUB_INCLUDE_BODY; +xfield_var(show_values) -> ?PUBSUB_SHOW_VALUES; +xfield_var(subscription_type) -> ?PUBSUB_SUBSCRIPTION_TYPE; +xfield_var(subscription_depth) -> ?PUBSUB_SUBSCRIPTION_DEPTH. + +%% Return the XForm variable type for a subscription option key. +xfield_type(deliver) -> "boolean"; +xfield_type(digest) -> "boolean"; +xfield_type(digest_frequency) -> "text-single"; +xfield_type(expire) -> "text-single"; +xfield_type(include_body) -> "boolean"; +xfield_type(show_values) -> + {"list-multi", [{"away", ?SHOW_VALUE_AWAY_LABEL}, + {"chat", ?SHOW_VALUE_CHAT_LABEL}, + {"dnd", ?SHOW_VALUE_DND_LABEL}, + {"online", ?SHOW_VALUE_ONLINE_LABEL}, + {"xa", ?SHOW_VALUE_XA_LABEL}]}; +xfield_type(subscription_type) -> + {"list-single", [{"items", ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, + {"nodes", ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; +xfield_type(subscription_depth) -> + {"list-single", [{"1", ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, + {"all", ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. + +%% Return the XForm variable label for a subscription option key. +xfield_label(deliver) -> ?DELIVER_LABEL; +xfield_label(digest) -> ?DIGEST_LABEL; +xfield_label(digest_frequency) -> ?DIGEST_FREQUENCY_LABEL; +xfield_label(expire) -> ?EXPIRE_LABEL; +xfield_label(include_body) -> ?INCLUDE_BODY_LABEL; +xfield_label(show_values) -> ?SHOW_VALUES_LABEL; +xfield_label(subscription_type) -> ?SUBSCRIPTION_TYPE_LABEL; +xfield_label(subscription_depth) -> ?SUBSCRIPTION_DEPTH_LABEL. + +%% Return the XForm value for a subscription option key. +xfield_val(deliver, Val) -> [bool_to_xopt(Val)]; +xfield_val(digest, Val) -> [bool_to_xopt(Val)]; +xfield_val(digest_frequency, Val) -> [integer_to_list(Val)]; +xfield_val(expire, Val) -> [jlib:now_to_utc_string(Val)]; +xfield_val(include_body, Val) -> [bool_to_xopt(Val)]; +xfield_val(show_values, Val) -> Val; +xfield_val(subscription_type, items) -> ["items"]; +xfield_val(subscription_type, nodes) -> ["nodes"]; +xfield_val(subscription_depth, all) -> ["all"]; +xfield_val(subscription_depth, N) -> [integer_to_list(N)]. + +%% Convert erlang booleans to XForms. +bool_to_xopt(false) -> "false"; +bool_to_xopt(true) -> "true". diff --git a/src/mod_pubsub/pubsub_subscription_odbc.erl b/src/mod_pubsub/pubsub_subscription_odbc.erl new file mode 100644 index 000000000..f05523081 --- /dev/null +++ b/src/mod_pubsub/pubsub_subscription_odbc.erl @@ -0,0 +1,294 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% @author Pablo Polvorin <pablo.polvorin@process-one.net> +%%% @author based on pubsub_subscription.erl by Brian Cully <bjc@kublai.com> +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(pubsub_subscription_odbc). +-author("pablo.polvorin@process-one.net"). + +%% API +-export([init/0, + subscribe_node/3, + unsubscribe_node/3, + get_subscription/3, + set_subscription/4, + get_options_xform/2, + parse_options_xform/1]). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-define(PUBSUB_DELIVER, "pubsub#deliver"). +-define(PUBSUB_DIGEST, "pubsub#digest"). +-define(PUBSUB_DIGEST_FREQUENCY, "pubsub#digest_frequency"). +-define(PUBSUB_EXPIRE, "pubsub#expire"). +-define(PUBSUB_INCLUDE_BODY, "pubsub#include_body"). +-define(PUBSUB_SHOW_VALUES, "pubsub#show-values"). +-define(PUBSUB_SUBSCRIPTION_TYPE, "pubsub#subscription_type"). +-define(PUBSUB_SUBSCRIPTION_DEPTH, "pubsub#subscription_depth"). + +-define(DELIVER_LABEL, + "Whether an entity wants to receive or disable notifications"). +-define(DIGEST_LABEL, + "Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually"). +-define(DIGEST_FREQUENCY_LABEL, + "The minimum number of milliseconds between sending any two notification digests"). +-define(EXPIRE_LABEL, + "The DateTime at which a leased subscription will end or has ended"). +-define(INCLUDE_BODY_LABEL, + "Whether an entity wants to receive an XMPP message body in addition to the payload format"). +-define(SHOW_VALUES_LABEL, + "The presence states for which an entity wants to receive notifications"). +-define(SUBSCRIPTION_TYPE_LABEL, + "Type of notification to receive"). +-define(SUBSCRIPTION_DEPTH_LABEL, + "Depth from subscription for which to receive notifications"). + +-define(SHOW_VALUE_AWAY_LABEL, "XMPP Show Value of Away"). +-define(SHOW_VALUE_CHAT_LABEL, "XMPP Show Value of Chat"). +-define(SHOW_VALUE_DND_LABEL, "XMPP Show Value of DND (Do Not Disturb)"). +-define(SHOW_VALUE_ONLINE_LABEL, "Mere Availability in XMPP (No Show Value)"). +-define(SHOW_VALUE_XA_LABEL, "XMPP Show Value of XA (Extended Away)"). + +-define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, + "Receive notification of new items only"). +-define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, + "Receive notification of new nodes only"). + +-define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, + "Receive notification from direct child nodes only"). +-define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, + "Receive notification from all descendent nodes"). + + +-define(DB_MOD, pubsub_db_odbc). +%%==================================================================== +%% API +%%==================================================================== +init() -> + ok = create_table(). + +subscribe_node(_JID, _NodeID, Options) -> + SubID = make_subid(), + ?DB_MOD:add_subscription(#pubsub_subscription{subid = SubID, options = Options}), + {result, SubID}. + + +unsubscribe_node(_JID, _NodeID, SubID) -> + case ?DB_MOD:read_subscription(SubID) of + {ok, Sub} -> + ?DB_MOD:delete_subscription(SubID), + {result, Sub}; + notfound -> + {error, notfound} + end. + +get_subscription(_JID, _NodeID, SubID) -> + case ?DB_MOD:read_subscription(SubID) of + {ok, Sub} -> {result, Sub}; + notfound -> {error, notfound} + end. + + +set_subscription(_JID, _NodeID, SubID, Options) -> + case ?DB_MOD:read_subscription(SubID) of + {ok, _} -> + ?DB_MOD:update_subscription(#pubsub_subscription{subid = SubID, options = Options}), + {result, ok}; + notfound -> + ?DB_MOD:add_subscription(#pubsub_subscription{subid = SubID, options = Options}), + {result, ok} + end. + + +get_options_xform(Lang, Options) -> + Keys = [deliver, show_values, subscription_type, subscription_depth], + XFields = [get_option_xfield(Lang, Key, Options) || Key <- Keys], + + {result, {xmlelement, "x", [{"xmlns", ?NS_XDATA}], + [{xmlelement, "field", [{"var", "FORM_TYPE"}, {"type", "hidden"}], + [{xmlelement, "value", [], + [{xmlcdata, ?NS_PUBSUB_SUB_OPTIONS}]}]}] ++ XFields}}. + +parse_options_xform(XFields) -> + case xml:remove_cdata(XFields) of + [] -> {result, []}; + [{xmlelement, "x", _Attrs, _Els} = XEl] -> + case jlib:parse_xdata_submit(XEl) of + XData when is_list(XData) -> + case set_xoption(XData, []) of + Opts when is_list(Opts) -> {result, Opts}; + Other -> Other + end; + Other -> + Other + end; + Other -> + Other + end. + +%%==================================================================== +%% Internal functions +%%==================================================================== +create_table() -> + ok. + + +make_subid() -> + {T1, T2, T3} = now(), + lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). + +%% +%% Subscription XForm processing. +%% + +%% Return processed options, with types converted and so forth, using +%% Opts as defaults. +set_xoption([], Opts) -> + Opts; +set_xoption([{Var, Value} | T], Opts) -> + NewOpts = case var_xfield(Var) of + {error, _} -> + Opts; + Key -> + Val = val_xfield(Key, Value), + lists:keystore(Key, 1, Opts, {Key, Val}) + end, + set_xoption(T, NewOpts). + +%% Return the options list's key for an XForm var. +var_xfield(?PUBSUB_DELIVER) -> deliver; +var_xfield(?PUBSUB_DIGEST) -> digest; +var_xfield(?PUBSUB_DIGEST_FREQUENCY) -> digest_frequency; +var_xfield(?PUBSUB_EXPIRE) -> expire; +var_xfield(?PUBSUB_INCLUDE_BODY) -> include_body; +var_xfield(?PUBSUB_SHOW_VALUES) -> show_values; +var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> subscription_type; +var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> subscription_depth; +var_xfield(_) -> {error, badarg}. + +%% Convert Values for option list's Key. +val_xfield(deliver, [Val]) -> xopt_to_bool(Val); +val_xfield(digest, [Val]) -> xopt_to_bool(Val); +val_xfield(digest_frequency, [Val]) -> list_to_integer(Val); +val_xfield(expire, [Val]) -> jlib:datetime_string_to_timestamp(Val); +val_xfield(include_body, [Val]) -> xopt_to_bool(Val); +val_xfield(show_values, Vals) -> Vals; +val_xfield(subscription_type, ["items"]) -> items; +val_xfield(subscription_type, ["nodes"]) -> nodes; +val_xfield(subscription_depth, ["all"]) -> all; +val_xfield(subscription_depth, [Depth]) -> + case catch list_to_integer(Depth) of + N when is_integer(N) -> N; + _ -> {error, ?ERR_NOT_ACCEPTABLE} + end. + +%% Convert XForm booleans to Erlang booleans. +xopt_to_bool("0") -> false; +xopt_to_bool("1") -> true; +xopt_to_bool("false") -> false; +xopt_to_bool("true") -> true; +xopt_to_bool(_) -> {error, ?ERR_NOT_ACCEPTABLE}. + +%% Return a field for an XForm for Key, with data filled in, if +%% applicable, from Options. +get_option_xfield(Lang, Key, Options) -> + Var = xfield_var(Key), + Label = xfield_label(Key), + {Type, OptEls} = type_and_options(xfield_type(Key), Lang), + Vals = case lists:keysearch(Key, 1, Options) of + {value, {_, Val}} -> + [tr_xfield_values(Vals) || Vals <- xfield_val(Key, Val)]; + false -> + [] + end, + {xmlelement, "field", + [{"var", Var}, {"type", Type}, + {"label", translate:translate(Lang, Label)}], + OptEls ++ Vals}. + +type_and_options({Type, Options}, Lang) -> + {Type, [tr_xfield_options(O, Lang) || O <- Options]}; +type_and_options(Type, _Lang) -> + {Type, []}. + +tr_xfield_options({Value, Label}, Lang) -> + {xmlelement, "option", + [{"label", translate:translate(Lang, Label)}], [{xmlelement, "value", [], + [{xmlcdata, Value}]}]}. + +tr_xfield_values(Value) -> + {xmlelement, "value", [], [{xmlcdata, Value}]}. + +%% Return the XForm variable name for a subscription option key. +xfield_var(deliver) -> ?PUBSUB_DELIVER; +xfield_var(digest) -> ?PUBSUB_DIGEST; +xfield_var(digest_frequency) -> ?PUBSUB_DIGEST_FREQUENCY; +xfield_var(expire) -> ?PUBSUB_EXPIRE; +xfield_var(include_body) -> ?PUBSUB_INCLUDE_BODY; +xfield_var(show_values) -> ?PUBSUB_SHOW_VALUES; +xfield_var(subscription_type) -> ?PUBSUB_SUBSCRIPTION_TYPE; +xfield_var(subscription_depth) -> ?PUBSUB_SUBSCRIPTION_DEPTH. + +%% Return the XForm variable type for a subscription option key. +xfield_type(deliver) -> "boolean"; +xfield_type(digest) -> "boolean"; +xfield_type(digest_frequency) -> "text-single"; +xfield_type(expire) -> "text-single"; +xfield_type(include_body) -> "boolean"; +xfield_type(show_values) -> + {"list-multi", [{"away", ?SHOW_VALUE_AWAY_LABEL}, + {"chat", ?SHOW_VALUE_CHAT_LABEL}, + {"dnd", ?SHOW_VALUE_DND_LABEL}, + {"online", ?SHOW_VALUE_ONLINE_LABEL}, + {"xa", ?SHOW_VALUE_XA_LABEL}]}; +xfield_type(subscription_type) -> + {"list-single", [{"items", ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, + {"nodes", ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; +xfield_type(subscription_depth) -> + {"list-single", [{"1", ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, + {"all", ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. + +%% Return the XForm variable label for a subscription option key. +xfield_label(deliver) -> ?DELIVER_LABEL; +xfield_label(digest) -> ?DIGEST_LABEL; +xfield_label(digest_frequency) -> ?DIGEST_FREQUENCY_LABEL; +xfield_label(expire) -> ?EXPIRE_LABEL; +xfield_label(include_body) -> ?INCLUDE_BODY_LABEL; +xfield_label(show_values) -> ?SHOW_VALUES_LABEL; +xfield_label(subscription_type) -> ?SUBSCRIPTION_TYPE_LABEL; +xfield_label(subscription_depth) -> ?SUBSCRIPTION_DEPTH_LABEL. + +%% Return the XForm value for a subscription option key. +xfield_val(deliver, Val) -> [bool_to_xopt(Val)]; +xfield_val(digest, Val) -> [bool_to_xopt(Val)]; +xfield_val(digest_frequency, Val) -> [integer_to_list(Val)]; +xfield_val(expire, Val) -> [jlib:now_to_utc_string(Val)]; +xfield_val(include_body, Val) -> [bool_to_xopt(Val)]; +xfield_val(show_values, Val) -> Val; +xfield_val(subscription_type, items) -> ["items"]; +xfield_val(subscription_type, nodes) -> ["nodes"]; +xfield_val(subscription_depth, all) -> ["all"]; +xfield_val(subscription_depth, N) -> [integer_to_list(N)]. + +%% Convert erlang booleans to XForms. +bool_to_xopt(false) -> "false"; +bool_to_xopt(true) -> "true". |