From 5598d3447854308f29ace5a021cb7712ef61ceb7 Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Fri, 7 Aug 2009 08:26:47 +0000 Subject: [PATCH] initial merge of pubsub odbc, compilation pass ok SVN Revision: 2437 --- src/mod_pubsub/Makefile.in | 5 +- src/mod_pubsub/mod_pubsub_odbc.erl | 3680 +++++++++++++++++++ src/mod_pubsub/node_flat_odbc.erl | 188 + src/mod_pubsub/node_hometree.erl | 89 +- src/mod_pubsub/node_hometree_odbc.erl | 1324 +++++++ src/mod_pubsub/node_pep_odbc.erl | 327 ++ src/mod_pubsub/nodetree_tree.erl | 6 +- src/mod_pubsub/nodetree_tree_odbc.erl | 353 ++ src/mod_pubsub/pubsub_db_odbc.erl | 136 + src/mod_pubsub/pubsub_odbc.patch | 484 +++ src/mod_pubsub/pubsub_subscription.erl | 8 +- src/mod_pubsub/pubsub_subscription_odbc.erl | 292 ++ src/odbc/mysql.sql | 55 + src/odbc/pg.sql | 51 + 14 files changed, 6941 insertions(+), 57 deletions(-) create mode 100644 src/mod_pubsub/mod_pubsub_odbc.erl create mode 100644 src/mod_pubsub/node_flat_odbc.erl create mode 100644 src/mod_pubsub/node_hometree_odbc.erl create mode 100644 src/mod_pubsub/node_pep_odbc.erl create mode 100644 src/mod_pubsub/nodetree_tree_odbc.erl create mode 100644 src/mod_pubsub/pubsub_db_odbc.erl create mode 100644 src/mod_pubsub/pubsub_odbc.patch create mode 100644 src/mod_pubsub/pubsub_subscription_odbc.erl diff --git a/src/mod_pubsub/Makefile.in b/src/mod_pubsub/Makefile.in index 4088303cb..549c907b7 100644 --- a/src/mod_pubsub/Makefile.in +++ b/src/mod_pubsub/Makefile.in @@ -25,7 +25,7 @@ ERLBEHAVBEAMS = $(addprefix $(OUTDIR)/,$(ERLBEHAVS:.erl=.beam)) BEAMS = $(addprefix $(OUTDIR)/,$(SOURCES:.erl=.beam)) -all: $(ERLBEHAVBEAMS) $(BEAMS) +all: odbc $(ERLBEHAVBEAMS) $(BEAMS) $(BEAMS): $(ERLBEHAVBEAMS) @@ -38,6 +38,9 @@ clean: distclean: clean rm -f Makefile +odbc: + patch -o mod_pubsub_odbc.erl mod_pubsub.erl pubsub_odbc.patch + TAGS: etags *.erl diff --git a/src/mod_pubsub/mod_pubsub_odbc.erl b/src/mod_pubsub/mod_pubsub_odbc.erl new file mode 100644 index 000000000..c07b470a8 --- /dev/null +++ b/src/mod_pubsub/mod_pubsub_odbc.erl @@ -0,0 +1,3680 @@ +%%% ==================================================================== +%%% ``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 +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + + +%%% @doc The module {@module} is the core of the PubSub +%%% extension. It relies on PubSub plugins for a large part of its functions. +%%% +%%% @headerfile "pubsub.hrl" +%%% +%%% @reference See XEP-0060: Pubsub 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 . 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. + +%%% TODO +%%% plugin: generate Reply (do not use broadcast atom anymore) + +-module(mod_pubsub_odbc). +-author('christophe.romain@process-one.net'). +-version('1.12-06'). + +-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 = [], + pep_sendlast_offline = false, + last_item_cache = false, + 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(pep_sendlast_offline, Opts, false), + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), + LastItemCache = gen_mod:get_opt(last_item_cache, Opts, false), + 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(ServerHost, config), {nodetree, NodeTree}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {plugins, Plugins}), + 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), + State = #state{host = Host, + server_host = ServerHost, + access = Access, + pep_mapping = PepMapping, + pep_sendlast_offline = PepOffline, + last_item_cache = LastItemCache, + 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. +%%

The Erlang code for the plugin is located in a module called +%% node_plugin. The 'node_' prefix is mandatory.

+%%

The modules are initialized in alphetical order and the list is checked +%% and sorted to ensure that each module is initialized only once.

+%%

See {@link node_hometree:init/1} for an example implementation.

+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) -> + create_node(Host, ServerHost, ["home"], service_jid(Host), ?STDNODE), + create_node(Host, ServerHost, ["home", ServerHost], service_jid(Host), ?STDNODE), + ok. + +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() -> + 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, '_'}, '_', '_'})) + 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() -> + 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, '_'}, '_'}, '_', '_', '_', '_', '_'})) + 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, 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]); + _ -> + ok + end. + +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) -> + 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, SubJID, 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 State#state.pep_sendlast_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 ++ "+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, Node, _Lang) -> + Host = To#jid.lserver, + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + 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. + {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) -> + 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), + {noreply, State}; + +handle_cast({unsubscribe, Subscriber, Owner}, State) -> + 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), + {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}}) -> + 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]))}; +iq_disco_items(Host, Item, From, RSM) -> + 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.12 compliant (breaks client compatibility ?) + %% TODO That is, remove name attribute (or node?, please check for 2.1) + Action = + fun(#pubsub_node{type = Type, id = NodeId}) -> + {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}}) -> + SN = node_to_string(SubNode), + RN = lists:last(SubNode), + {xmlelement, "item", [{"jid", Host}, {"node", SN}, + {"name", RN}], []} + end, tree_call(Host, get_subnodes, [Host, Node, From])), + Items = lists:map( + fun(#pubsub_item{itemid = {RN, _}}) -> + SN = node_to_string(Node) ++ "!" ++ RN, + {result, Name} = node_call(Type, get_item_name, [Host, Node, RN]), + {xmlelement, "item", [{"jid", Host}, {"node", SN}, + {"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 = case Host of + {_, _, _} -> xml:get_attr_s("node", Attrs); + _ -> string_to_node(xml:get_attr_s("node", Attrs)) + end, + 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, + NoRSM = lists:filter(fun({xmlelement, Name, _, _}) -> + Name == "set" + end, SubEls), + Action = xml:remove_cdata(SubEls) -- NoRSM, + 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 {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

Processes an Ad Hoc Command.

+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(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, ?ERR_EXTENDED(?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) -> iqRes() +%% @doc

Sends the process pending subscriptions XForm for Host to +%% Owner.

+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

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

+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} -> + broadcast_pending_auth_events(N), + {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. + +broadcast_pending_auth_events(#pubsub_node{type = Type, id = NodeID} = Node) -> + {result, Subscriptions} = node_call(Type, get_node_subscriptions, [NodeID]), + lists:foreach(fun ({J, pending, _SubID}) -> + send_authorization_request(Node, jlib:make_jid(J)); + ({J, pending}) -> + send_authorization_request(Node, jlib:make_jid(J)) + end, Subscriptions). + +%%% 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", + [{"node", SNode}, {"jid", jlib:jid_to_string(JID)}] ++ 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, + 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

Create new pubsub nodes

+%%

In addition to method-specific error conditions, there are several general reasons why the node creation request might fail:

+%%
    +%%
  • The service does not support node creation.
  • +%%
  • Only entities that are registered with the service are allowed to create nodes but the requesting entity is not registered.
  • +%%
  • The requesting entity does not have sufficient privileges to create nodes.
  • +%%
  • The requested NodeID already exists.
  • +%%
  • The request did not include a NodeID and "instant nodes" are not supported.
  • +%%
+%%

ote: node creation is a particular case, error return code is evaluated at many places:

+%%
    +%%
  • iq_pubsub checks if service supports node creation (type exists)
  • +%%
  • create_node checks if instant nodes are supported
  • +%%
  • create_node asks node plugin if entity have sufficient privilege
  • +%%
  • nodetree create_node checks if nodeid already exists
  • +%%
  • node plugin create_node just sets default affiliation/subscription
  • +%%
+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 -> + {LOU, LOS, _} = jlib:jid_tolower(Owner), + HomeNode = ["home", LOS, LOU], + create_node(Host, ServerHost, + HomeNode, Owner, Type, Access, Configuration), + NewNode = HomeNode ++ [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), + Parent = lists:sublist(Node, length(Node) - 1), + %% 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(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() -> + 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, 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

Delete specified node and all childs.

+%%

There are several reasons why the node deletion request might fail:

+%%
    +%%
  • The requesting entity does not have sufficient privileges to delete the node.
  • +%%
  • The node is the root collection node, which cannot be deleted.
  • +%%
  • The specified node does not exist.
  • +%%
+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}) -> + SubsByDepth = get_collection_subscriptions(Host, Node), + case node_call(Type, get_affiliation, [NodeId, Owner]) of + {result, owner} -> + 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) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% Host = host() +%% Node = pubsubNode() +%% From = jid() +%% JID = jid() +%% @see node_hometree:subscribe_node/5 +%% @doc

Accepts or rejects subcription requests on a PubSub node.

+%%

There are several reasons why the subscription request might fail:

+%%
    +%%
  • The bare JID portions of the JIDs do not match.
  • +%%
  • The node has an access model of "presence" and the requesting entity is not subscribed to the owner's presence.
  • +%%
  • The node has an access model of "roster" and the requesting entity is not in one of the authorized roster groups.
  • +%%
  • The node has an access model of "whitelist" and the requesting entity is not on the whitelist.
  • +%%
  • The service requires payment for subscriptions to the node.
  • +%%
  • The requesting entity is anonymous and the service does not allow anonymous entities to subscribe.
  • +%%
  • The requesting entity has a pending subscription.
  • +%%
  • The requesting entity is blocked from subscribing (e.g., because having an affiliation of outcast).
  • +%%
  • The node does not support subscriptions.
  • +%%
  • The node does not exist.
  • +%%
+subscribe_node(Host, Node, From, JID, Configuration) -> + {result, SubOpts} = pubsub_subscription:parse_options_xform(Configuration), + 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")}; + 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

Unsubscribe JID from the Node.

+%%

There are several reasons why the unsubscribe request might fail:

+%%
    +%%
  • The requesting entity has multiple subscriptions to the node but does not specify a subscription ID.
  • +%%
  • The request does not specify an existing subscriber.
  • +%%
  • The requesting entity does not have sufficient privileges to unsubscribe the specified JID.
  • +%%
  • The node does not exist.
  • +%%
  • The request specifies a subscription ID that is not valid or current.
  • +%%
+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

Publish item to a PubSub node.

+%%

The permission to publish an item must be verified by the plugin implementation.

+%%

There are several reasons why the publish request might fail:

+%%
    +%%
  • The requesting entity does not have sufficient privileges to publish.
  • +%%
  • The node does not support item publication.
  • +%%
  • The node does not exist.
  • +%%
  • The payload size exceeds a service-defined limit.
  • +%%
  • 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.
  • +%%
  • The request does not match the node configuration.
  • +%%
+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, + broadcast_publish_item(Host, Node, NodeId, Type, Options, Removed, ItemId, jlib:jid_tolower(Publisher), Payload), + 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

Delete item from a PubSub node.

+%%

The permission to delete an item must be verified by the plugin implementation.

+%%

There are several reasons why the item retraction request might fail:

+%%
    +%%
  • The publisher does not have sufficient privileges to delete the requested item.
  • +%%
  • The node or item does not exist.
  • +%%
  • The request does not specify a node.
  • +%%
  • The request does not include an element or the element does not specify an ItemId.
  • +%%
  • The node does not support persistent items.
  • +%%
  • The service does not support the deletion of items.
  • +%%
+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

Delete all items of specified node owned by JID.

+%%

There are several reasons why the node purge request might fail:

+%%
    +%%
  • The node or service does not support node purging.
  • +%%
  • The requesting entity does not have sufficient privileges to purge the node.
  • +%%
  • The node is not configured to persist items.
  • +%%
  • The specified node does not exist.
  • +%%
+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

Return the items of a given node.

+%%

The number of items to return is limited by MaxItems.

+%%

The permission are not checked in this function.

+%% @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 == "" -> ?MAXITEMS; + 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

Resend the items of a node to the user.

+%% @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 + ToSend = case node_action(Host, Type, get_last_items, [NodeId, LJID, 1]) of + {result, []} -> []; + {result, Items} -> Items + end, + event_stanza( + [{xmlelement, "items", nodeAttr(Node), + itemsEls(ToSend)}]); + LastItem -> + event_stanza( + [{xmlelement, "items", nodeAttr(Node), + itemsEls([LastItem])}]) + 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 = event_stanza( + [{xmlelement, "items", nodeAttr(Node), + itemsEls(ToSend)}]), + 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

Return the list of affiliations as an XMPP response.

+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, 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, 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, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "not-subscribed")}; + {[], [SID]} -> + read_sub(Subscriber, NodeID, SID, Lang); + {[], _} -> + {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "subid-required")}; + {_, _} -> + read_sub(Subscriber, NodeID, SubID, Lang) + end. + +read_sub(Subscriber, NodeID, SubID, Lang) -> + case pubsub_subscription:get_subscription(Subscriber, NodeID, SubID) of + {error, notfound} -> + {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + {result, #pubsub_subscription{options = Options}} -> + pubsub_subscription:get_options_xform(Lang, Options) + 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) -> + Subscriber = case jlib:string_to_jid(JID) of + error -> {"", "", ""}; + J -> jlib:jid_tolower(J) + end, + {result, SubOpts} = pubsub_subscription:parse_options_xform(Configuration), + {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, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "not-subscribed")}; + {[], [SID]} -> + write_sub(Subscriber, NodeID, SID, SubOpts); + {[], _} -> + {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "subid-required")}; + {_, _} -> + write_sub(Subscriber, NodeID, SubID, SubOpts) + end. + +write_sub(Subscriber, NodeID, SubID, Options) -> + case pubsub_subscription:set_subscription(Subscriber, NodeID, SubID, + Options) of + {error, notfound} -> + {error, ?ERR_EXTENDED(?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

Return the list of subscriptions as an XMPP response.

+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}; + _ -> + Action = fun(#pubsub_node{type = Type, id = NodeId}) -> + case lists:member(Owner, node_owners_call(Type, NodeId)) of + true -> + lists:foreach(fun({JID, Subscription, SubId}) -> + node_call(Type, set_subscriptions, [NodeId, JID, Subscription, SubId]) + end, Entities), + {result, []}; + _ -> + {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

Convert an affiliation type from string to atom.

+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

Convert a subscription type from string to atom.

+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

Convert an affiliation type from atom to string.

+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

Convert a subscription type from atom to string.

+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

Convert a node type from pubsubNode to string.

+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, "/"). + +%% @spec (Host) -> jid() +%% Host = host() +%% @doc

Generate pubsub service JID.

+service_jid(Host) -> + case Host of + {U,S,_} -> {jid, U, S, "", U, S, ""}; + _ -> {jid, "", Host, "", "", Host, ""} + end. + +%% @spec (LJID, PresenceDelivery) -> boolean() +%% LJID = jid() +%% NotifyType = items | nodes +%% Depth = integer() +%% NodeOptions = [{atom(), term()}] +%% SubOptions = [{atom(), term()}] +%% @doc

Check if a notification must be delivered or not based on +%% node and subscription options.

+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

Count occurence of XML elements in payload.

+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

Build pubsub event stanza

+event_stanza(Els) -> + {xmlelement, "message", [], + [{xmlelement, "event", [{"xmlns", ?NS_PUBSUB_EVENT}], Els}]}. + +%%%%%% 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) -> + lists:map(fun ({Depth, Nodes}) -> + {Depth, [{N, get_node_subs(N)} || N <- Nodes]} + end, tree_action(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)])). + +get_node_subs(#pubsub_node{type = Type, + nodeid = {Host, Node}, + id = NodeID}) -> + case node_action(Host, Type, get_node_subscriptions, [NodeID]) of + {result, Subs} -> + get_options_for_subs(Host, Node, NodeID, Subs); + Other -> + Other + end. + +get_options_for_subs(_Host, Node, NodeID, Subs) -> + lists:foldl(fun({JID, subscribed, SubID}, Acc) -> + {result, #pubsub_subscription{options = Options}} = pubsub_subscription:get_subscription(JID, NodeID, SubID), + [{JID, Node, Options} | Acc]; + (_, 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, [{"node", node_to_string(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, Stanza) -> + %AccessModel = get_option(NodeOptions, access_model), + BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but usefull + From = service_jid(Host), + %% Handles explicit subscriptions + FilteredSubsByDepth = depths_to_deliver(NotifyType, SubsByDepth), + NodesByJID = collate_subs_by_jid(FilteredSubsByDepth), + 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. + +depths_to_deliver(NotifyType, SubsByDepth) -> + NodesToDeliver = + fun (Depth, Node, Subs, Acc) -> + lists:foldl(fun ({LJID, _Node, SubOptions} = S, Acc2) -> + case is_to_deliver(LJID, NotifyType, Depth, + Node#pubsub_node.options, + SubOptions) of + true -> [S | 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, + + lists:foldl(DepthsToDeliver, [], SubsByDepth). + +collate_subs_by_jid(SubsByDepth) -> + lists:foldl(fun ({JID, Node, _Options}, Acc) -> + OldNodes = case lists:keysearch(JID, 1, Acc) of + {value, {JID, Nodes}} -> Nodes; + false -> [] + end, + lists:keystore(JID, 1, Acc, {JID, [Node | OldNodes]}) + end, [], SubsByDepth). + +%% 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 ++ "+notify", Features); + _ -> false + end + end. + +%%%%%%% Configuration handling + +%%

There are several reasons why the default node configuration options request might fail:

+%%
    +%%
  • The service does not support node configuration.
  • +%%
  • The service does not support retrieval of default node configuration.
  • +%%
+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 (NodeId) -> [ljid()] +%% NodeId = pubsubNodeId() +%% @doc

Return list of node owners.

+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 (Options) -> MaxItems +%% Host = host() +%% Options = [Option] +%% Option = {Key::atom(), Value::term()} +%% MaxItems = integer() | unlimited +%% @doc

Return the maximum number of items for a given node.

+%%

Unlimited means that there is no limit in the number of items that can +%% be stored.

+%% @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]), + ?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) + ]. + +%%

There are several reasons why the node configuration request might fail:

+%%
    +%%
  • The service does not support node configuration.
  • +%%
  • The requesting entity does not have sufficient privileges to configure the node.
  • +%%
  • The request did not specify a node.
  • +%%
  • The node has no configuration options.
  • +%%
  • The specified node does not exist.
  • +%%
+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(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(Opts, add_opt(Opt, BoolVal, NewOpts)) + end). + +-define(SET_STRING_XOPT(Opt, Val), + set_xoption(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)); + _ -> + {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)); + false -> {error, ?ERR_NOT_ACCEPTABLE} + end). + +-define(SET_LIST_XOPT(Opt, Val), + set_xoption(Opts, add_opt(Opt, Val, NewOpts))). + +set_xoption([], NewOpts) -> + NewOpts; +set_xoption([{"FORM_TYPE", _} | Opts], NewOpts) -> + set_xoption(Opts, NewOpts); +set_xoption([{"pubsub#roster_groups_allowed", Value} | Opts], NewOpts) -> + ?SET_LIST_XOPT(roster_groups_allowed, Value); +set_xoption([{"pubsub#deliver_payloads", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(deliver_payloads, Val); +set_xoption([{"pubsub#deliver_notifications", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(deliver_notifications, Val); +set_xoption([{"pubsub#notify_config", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(notify_config, Val); +set_xoption([{"pubsub#notify_delete", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(notify_delete, Val); +set_xoption([{"pubsub#notify_retract", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(notify_retract, Val); +set_xoption([{"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_BOOL_XOPT(subscribe, Val); +set_xoption([{"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_ALIST_XOPT(publish_model, Val, [publishers, subscribers, open]); +set_xoption([{"pubsub#node_type", [Val]} | Opts], NewOpts) -> + ?SET_ALIST_XOPT(node_type, Val, [leaf, collection]); +set_xoption([{"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_ALIST_XOPT(send_last_published_item, Val, [never, on_sub, on_sub_and_presence]); +set_xoption([{"pubsub#presence_based_delivery", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(presence_based_delivery, Val); +set_xoption([{"pubsub#title", Value} | Opts], NewOpts) -> + ?SET_STRING_XOPT(title, Value); +set_xoption([{"pubsub#type", Value} | Opts], NewOpts) -> + ?SET_STRING_XOPT(type, Value); +set_xoption([{"pubsub#body_xslt", Value} | Opts], NewOpts) -> + ?SET_STRING_XOPT(body_xslt, Value); +set_xoption([{"pubsub#collection", Value} | Opts], NewOpts) -> + NewValue = [string_to_node(V) || V <- Value], + ?SET_LIST_XOPT(collection, NewValue); +set_xoption([{"pubsub#node", [Value]} | Opts], NewOpts) -> + NewValue = string_to_node(Value), + ?SET_LIST_XOPT(node, NewValue); +set_xoption([_ | Opts], NewOpts) -> + % skip unknown field + set_xoption(Opts, NewOpts). + +%%%% last item cache handling + +is_last_item_cache_enabled({_, ServerHost, _}) -> + is_last_item_cache_enabled(ServerHost); +is_last_item_cache_enabled(Host) -> + case 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 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, config), plugins) of + [{plugins, []}] -> [?STDNODE]; + [{plugins, PL}] -> PL; + _ -> [?STDNODE] + end. +select_type(ServerHost, Host, Node, Type)-> + SelectedType = case Host of + {_User, _Server, _Resource} -> + case 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

node tree plugin call.

+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 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

node plugin call.

+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

plugin transaction handling.

+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) -> + [{"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_headers({xmlelement, Name, Attrs, Els}, Headers) -> + {xmlelement, Name, Attrs, Els ++ Headers}. + +collection_shim(Node, Nodes) -> + [{xmlelement, "header", [{"name", "Collection"}], + [{xmlcdata, node_to_string(N)}]} || N <- Nodes -- [Node]]. diff --git a/src/mod_pubsub/node_flat_odbc.erl b/src/mod_pubsub/node_flat_odbc.erl new file mode 100644 index 000000000..9df3aa77f --- /dev/null +++ b/src/mod_pubsub/node_flat_odbc.erl @@ -0,0 +1,188 @@ +%%% ==================================================================== +%%% ``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 +%%% [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/7, + 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_subscription/2, + set_subscription/3, + 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 + ]). + + +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 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}, + {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) -> + node_hometree_odbc:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). + +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_subscription(NodeId, Owner) -> + node_hometree_odbc:get_subscription(NodeId, Owner). + +set_subscription(NodeId, Owner, Subscription) -> + node_hometree_odbc:set_subscription(NodeId, Owner, Subscription). + +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). diff --git a/src/mod_pubsub/node_hometree.erl b/src/mod_pubsub/node_hometree.erl index 680fec39d..595796aa8 100644 --- a/src/mod_pubsub/node_hometree.erl +++ b/src/mod_pubsub/node_hometree.erl @@ -92,7 +92,7 @@ %% plugin. It can be used for example by the developer to create the specific %% module database schema if it does not exists yet.

init(_Host, _ServerHost, _Opts) -> - ok = pubsub_subscription:init(), + pubsub_subscription:init(), mnesia:create_table(pubsub_state, [{disc_copies, [node()]}, {attributes, record_info(fields, pubsub_state)}]), @@ -311,8 +311,9 @@ subscribe_node(NodeId, Sender, Subscriber, AccessModel, (AccessModel == whitelist) and (not Whitelisted) -> %% Node has whitelist access model and entity lacks required affiliation {error, ?ERR_EXTENDED(?ERR_NOT_ALLOWED, "closed-node")}; - PendingSubscription -> - {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "pending-subscription")}; + (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}; @@ -320,33 +321,32 @@ subscribe_node(NodeId, Sender, Subscriber, AccessModel, %% % Requesting entity is anonymous %% {error, ?ERR_FORBIDDEN}; true -> - case pubsub_subscription:subscribe_node(Subscriber, - NodeId, Options) of - {result, SubID} -> + case pubsub_subscription:subscribe_node(Subscriber, NodeId, Options) of + {result, SubId} -> NewSub = case AccessModel of authorize -> pending; - _ -> subscribed + _ -> subscribed end, - set_state(SubState#pubsub_state{subscriptions = [{NewSub, SubID} | Subscriptions]}), + set_state(SubState#pubsub_state{subscriptions = [{NewSub, SubId} | Subscriptions]}), case {NewSub, SendLast} of {subscribed, never} -> - {result, {default, subscribed, SubID}}; + {result, {default, subscribed, SubId}}; {subscribed, _} -> - {result, {default, subscribed, SubID, send_last}}; + {result, {default, subscribed, SubId, send_last}}; {_, _} -> - {result, {default, pending, SubID}} + {result, {default, pending, SubId}} end; _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} end end. -%% @spec (NodeId, Sender, Subscriber, SubID) -> +%% @spec (NodeId, Sender, Subscriber, SubId) -> %% {error, Reason} | {result, []} %% NodeId = mod_pubsub:pubsubNodeId() %% Sender = mod_pubsub:jid() %% Subscriber = mod_pubsub:jid() -%% SubID = mod_pubsub:subid() +%% SubId = mod_pubsub:subid() %% Reason = mod_pubsub:stanzaError() %% @doc

Unsubscribe the Subscriber from the Node.

unsubscribe_node(NodeId, Sender, Subscriber, SubId) -> @@ -358,8 +358,8 @@ unsubscribe_node(NodeId, Sender, Subscriber, SubId) -> GenKey -> GenState; _ -> get_state(NodeId, SubKey) end, - Subscriptions = lists:filter(fun({_Sub, _SubID}) -> true; - (_SubID) -> false + Subscriptions = lists:filter(fun({_Sub, _SubId}) -> true; + (_SubId) -> false end, SubState#pubsub_state.subscriptions), SubIdExists = case SubId of [] -> false; @@ -370,11 +370,11 @@ unsubscribe_node(NodeId, Sender, Subscriber, SubId) -> %% Requesting entity is prohibited from unsubscribing entity not Authorized -> {error, ?ERR_FORBIDDEN}; - %% Entity did not specify SubID - %%SubID == "", ?? -> + %% Entity did not specify SubId + %%SubId == "", ?? -> %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; %% Invalid subscription identifier - %%InvalidSubID -> + %%InvalidSubId -> %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; %% Requesting entity is not a subscriber Subscriptions == [] -> @@ -405,11 +405,11 @@ unsubscribe_node(NodeId, Sender, Subscriber, SubId) -> {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")} end. -delete_subscription(SubKey, NodeID, {Subscription, SubID}, SubState) -> +delete_subscription(SubKey, NodeID, {Subscription, SubId}, SubState) -> Affiliation = SubState#pubsub_state.affiliation, AllSubs = SubState#pubsub_state.subscriptions, - NewSubs = AllSubs -- [{Subscription, SubID}], - pubsub_subscription:unsubscribe_node(SubKey, NodeID, SubID), + NewSubs = AllSubs -- [{Subscription, SubId}], + pubsub_subscription:unsubscribe_node(SubKey, NodeID, SubId), case {Affiliation, NewSubs} of {none, []} -> % Just a regular subscriber, and this is final item, so @@ -662,8 +662,8 @@ get_entity_subscriptions(Host, Owner) -> 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]; + lists:foldl(fun({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, J} | Acc2]; (S, Acc2) -> [{Node, S, J} | Acc2] end, Acc, Ss); @@ -678,8 +678,8 @@ get_node_subscriptions(NodeId) -> %% TODO: get rid of cases to handle non-list subscriptions case Subscriptions of [_|_] -> - lists:foldl(fun({S, SubID}, Acc) -> - [{J, S, SubID} | Acc]; + lists:foldl(fun({S, SubId}, Acc) -> + [{J, S, SubId} | Acc]; (S, Acc) -> [{J, S} | Acc] end, [], Subscriptions); @@ -696,19 +696,6 @@ get_subscriptions(NodeId, Owner) -> SubState = get_state(NodeId, SubKey), {result, SubState#pubsub_state.subscriptions}. -set_subscriptions(NodeId, Owner, none, SubId) -> - SubKey = jlib:jid_tolower(Owner), - SubState = get_state(NodeId, SubKey), - case {SubId, SubState#pubsub_state.subscriptions} of - {_, []} -> - {error, ?ERR_ITEM_NOT_FOUND}; - {"", [{_, SID}]} -> - unsub_with_subid(NodeId, SID, SubState); - {"", [_|_]} -> - {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - _ -> - unsub_with_subid(NodeId, SubId, SubState) - end; set_subscriptions(NodeId, Owner, Subscription, SubId) -> SubKey = jlib:jid_tolower(Owner), SubState = get_state(NodeId, SubKey), @@ -716,11 +703,17 @@ set_subscriptions(NodeId, Owner, Subscription, SubId) -> {_, []} -> {error, ?ERR_ITEM_NOT_FOUND}; {"", [{_, SID}]} -> - replace_subscription({Subscription, SID}, SubState); + case Subscription of + none -> unsub_with_subid(NodeId, SID, SubState); + _ -> replace_subscription({Subscription, SID}, SubState) + end; {"", [_|_]} -> {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; _ -> - replace_subscription({Subscription, SubId}, SubState) + case Subscription of + none -> unsub_with_subid(NodeId, SubId, SubState); + _ -> replace_subscription({Subscription, SubId}, SubState) + end end. replace_subscription(NewSub, SubState) -> @@ -730,8 +723,8 @@ replace_subscription(NewSub, SubState) -> replace_subscription(_, [], Acc) -> Acc; -replace_subscription({Sub, SubID}, [{_, SubID} | T], Acc) -> - replace_subscription({Sub, SubID}, T, [{Sub, SubID} | Acc]). +replace_subscription({Sub, SubId}, [{_, SubID} | T], Acc) -> + replace_subscription({Sub, SubId}, T, [{Sub, SubID} | Acc]). unsub_with_subid(NodeId, SubId, SubState) -> pubsub_subscription:unsubscribe_node(SubState#pubsub_state.stateid, @@ -863,10 +856,10 @@ get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) - Subscriptions = SubState#pubsub_state.subscriptions, Whitelisted = can_fetch_item(Affiliation, Subscriptions), if - %%SubID == "", ?? -> + %%SubId == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubID -> + %%InvalidSubId -> %% Entity is subscribed but specifies an invalid subscription ID %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; GenState#pubsub_state.affiliation == outcast -> @@ -881,7 +874,7 @@ get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) - (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) -> + (AccessModel == authorize) -> % TODO: to be done %% Node has authorize access model {error, ?ERR_FORBIDDEN}; %%MustPay -> @@ -911,10 +904,10 @@ get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _S Subscriptions = GenState#pubsub_state.subscriptions, Whitelisted = can_fetch_item(Affiliation, Subscriptions), if - %%SubID == "", ?? -> + %%SubId == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubID -> + %%InvalidSubId -> %% Entity is subscribed but specifies an invalid subscription ID %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; GenState#pubsub_state.affiliation == outcast -> @@ -976,7 +969,7 @@ can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions); can_fetch_item(_Affiliation, _Subscription) -> false. is_subscribed(Subscriptions) -> - lists:any(fun ({subscribed, _SubID}) -> true; + lists:any(fun ({subscribed, _SubId}) -> true; (_) -> false end, Subscriptions). diff --git a/src/mod_pubsub/node_hometree_odbc.erl b/src/mod_pubsub/node_hometree_odbc.erl new file mode 100644 index 000000000..01a21d5b8 --- /dev/null +++ b/src/mod_pubsub/node_hometree_odbc.erl @@ -0,0 +1,1324 @@ +%%% ==================================================================== +%%% ``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 +%%% [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 {@module} is the default PubSub plugin. +%%%

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.

+%%%

PubSub plugin nodes are using the {@link gen_node} behaviour.

+%%%

The API isn't stabilized yet. 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.

+ +-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 + ]). + +-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

Called during pubsub modules initialisation. Any pubsub plugin must +%% implement this function. It can return anything.

+%%

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.

+init(_Host, _ServerHost, _Opts) -> + pubsub_subscription_odbc:init(), + ok. + +%% @spec (Host, ServerHost) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = host() +%% @doc

Called during pubsub modules termination. Any pubsub plugin must +%% implement this function. It can return anything.

+terminate(_Host, _ServerHost) -> + ok. + +%% @spec () -> [Option] +%% Option = mod_pubsub:nodeOption() +%% @doc Returns the default pubsub node options. +%%

Example of function return value:

+%% ``` +%% [{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 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}, + {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 +%%

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 +%% access_createnode ACL value in ejabberd config file.

+%%

This function also check that node can be created a a children of its +%% parent node

+%%

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).'''

+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 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

+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

purge items of deleted nodes after effective deletion.

+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) -> +%% {error, Reason} | {result, Result} +%% @doc

Accepts or rejects subcription requests on a PubSub node.

+%%

The mechanism works as follow: +%%

    +%%
  • The main PubSub module prepares the subscription and passes the +%% result of the preparation as a record.
  • +%%
  • This function gets the prepared record and several other parameters and +%% can decide to:
      +%%
    • reject the subscription;
    • +%%
    • allow it as is, letting the main module perform the database +%% persistance;
    • +%%
    • allow it, modifying the record. The main module will store the +%% modified record;
    • +%%
    • allow it, but perform the needed persistance operations.
    +%%

+%%

The selected behaviour depends on the return parameter: +%%

    +%%
  • {error, Reason}: an IQ error result will be returned. No +%% subscription will actually be performed.
  • +%%
  • true: Subscribe operation is allowed, based on the +%% unmodified record passed in parameter SubscribeResult. If this +%% parameter contains an error, no subscription will be performed.
  • +%%
  • {true, PubsubState}: Subscribe operation is allowed, but +%% the {@link mod_pubsub:pubsubState()} record returned replaces the value +%% passed in parameter SubscribeResult.
  • +%%
  • {true, done}: 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.
+%%

+%%

In the default plugin module, the record is unchanged.

+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")}; + (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 -> + 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

Unsubscribe the Subscriber from the Node.

+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

Publishes the item passed as parameter.

+%%

The mechanism works as follow: +%%

    +%%
  • The main PubSub module prepares the item to publish and passes the +%% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.
  • +%%
  • This function gets the prepared record and several other parameters and can decide to:
      +%%
    • reject the publication;
    • +%%
    • allow the publication as is, letting the main module perform the database persistance;
    • +%%
    • allow the publication, modifying the record. The main module will store the modified record;
    • +%%
    • allow it, but perform the needed persistance operations.
    +%%

+%%

The selected behaviour depends on the return parameter: +%%

    +%%
  • {error, Reason}: an iq error result will be return. No +%% publication is actually performed.
  • +%%
  • true: Publication operation is allowed, based on the +%% unmodified record passed in parameter Item. If the Item +%% parameter contains an error, no subscription will actually be +%% performed.
  • +%%
  • {true, Item}: Publication operation is allowed, but the +%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed +%% in parameter Item. The persistance will be performed by the main +%% module.
  • +%%
  • {true, done}: 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.
+%%

+%%

In the default plugin module, the record is unchanged.

+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

This function is used to remove extra items, most notably when the +%% maximum number of items has been reached.

+%%

This function is used internally by the core PubSub module, as no +%% permission check is performed.

+%%

In the default plugin module, the oldest items are removed, but other +%% rules can be used.

+%%

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).'''

+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

Triggers item deletion.

+%%

Default plugin: The user performing the deletion must be the node owner +%% or a publisher.

+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{items = Items, affiliation = owner} -> + del_items(NodeId, Items), + %% set new item list use useless + {result, {default, broadcast}}; + _ -> + %% Entity is not owner + {error, ?ERR_FORBIDDEN} + end. + +%% @spec (Host, JID) -> [{Node,Affiliation}] +%% Host = host() +%% JID = mod_pubsub:jid() +%% @doc

Return the current affiliations for the given user

+%%

The default module reads affiliations in the main Mnesia +%% pubsub_state 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 +%% pubsub_state table.

+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

Return the current subscriptions for the given user

+%%

The default module reads subscriptions in the main Mnesia +%% pubsub_state 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 +%% pubsub_state table.

+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:map(fun({N, T, I, J, S}) -> + Node = nodetree_tree_odbc:raw_to_node(Host, {N, "", T, I}), + {Node, decode_subscriptions(S), decode_jid(J)} + 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:map(fun({N, T, I, J, S}) -> + Node = nodetree_tree_odbc:raw_to_node(Host, {N, "", T, I}), + {Node, decode_subscriptions(S), decode_jid(J)} + 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:map(fun({J, S}) -> {decode_jid(J), decode_subscriptions(S)} end, RItems); + % TODO {J, S, SubId} + _ -> + [] + 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); + _ -> none + 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 + {_, []} -> + {error, ?ERR_ITEM_NOT_FOUND}; + {"", [{_, 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]). + +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

Returns a list of Owner's nodes on Host with pending +%% subscriptions.

+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 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. +%%

For the default PubSub module, states are stored in Mnesia database.

+%%

We can consider that the pubsub_state table have been created by the main +%% mod_pubsub module.

+%%

PubSub plugins can store the states where they wants (for example in a +%% relational database).

+%%

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).'''

+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

Returns a state (one state list), given its reference.

+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

Write a state into database.

+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 (StateId) -> ok | {error, Reason::stanzaError()} +%% StateId = mod_pubsub:pubsubStateId() +%% @doc

Delete a state from database.

+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, Rsm) -> {[Items],RsmOut} | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% Items = mod_pubsub:pubsubItems() +%% Rsm = jlib:rsm_in() | none +%% RsmOut=jlib:rsm_out() | none +%% @doc Returns the list of stored items for a given node. +%%

For the default PubSub module, items are stored in Mnesia database.

+%%

We can consider that the pubsub_item table have been created by the main +%% mod_pubsub module.

+%%

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.

+%%

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).'''

+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) -> + get_items(NodeId, From, #rsm_in{max=?MAXITEMS div 2}); +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) -> % TODO: to be done + %% 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

Returns an item (one item list), given its reference.

+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) -> % TODO: to be done + %% 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

Write an item into database.

+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

Delete an item from database.

+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

Return the name of the node if known: Default is to return +%% node id.

+get_item_name(_Host, _Node, Id) -> + Id. + +%% @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, 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("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(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 list(L) -> list_to_integer(L); +l2i(I) when integer(I) -> I. +i2l(I) when integer(I) -> integer_to_list(I); +i2l(L) when list(L) -> L. +i2l(I, N) when integer(I) -> i2l(i2l(I), N); +i2l(L, N) when 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_pep_odbc.erl b/src/mod_pubsub/node_pep_odbc.erl new file mode 100644 index 000000000..cbb3a81fb --- /dev/null +++ b/src/mod_pubsub/node_pep_odbc.erl @@ -0,0 +1,327 @@ +%%% ==================================================================== +%%% ``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 +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @doc The module {@module} is the pep PubSub plugin. +%%%

PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.

+ +-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 + ]). + +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 div 2}, + {subscribe, true}, + {access_model, presence}, + {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, 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, subscription " + "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, subscription " + "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", "subscription"], 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_subscription(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, subscription " + "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, subscription " + "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", "subscription"], 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_subscription(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). + + +%%% +%%% 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/nodetree_tree.erl b/src/mod_pubsub/nodetree_tree.erl index 9410a374a..3bd7bba4b 100644 --- a/src/mod_pubsub/nodetree_tree.erl +++ b/src/mod_pubsub/nodetree_tree.erl @@ -148,10 +148,8 @@ get_parentnodes(_Host, _Node, _From) -> %% containing just this node.

get_parentnodes_tree(Host, Node, From) -> case get_node(Host, Node, From) of - N when is_record(N, pubsub_node) -> - [{0, mnesia:read(pubsub_node, {Host, Node})}]; - Error -> - Error + N when is_record(N, pubsub_node) -> [{0, [N]}]; + Error -> Error end. %% @spec (Host, Node, From) -> [pubsubNode()] | {error, Reason} diff --git a/src/mod_pubsub/nodetree_tree_odbc.erl b/src/mod_pubsub/nodetree_tree_odbc.erl new file mode 100644 index 000000000..97833f649 --- /dev/null +++ b/src/mod_pubsub/nodetree_tree_odbc.erl @@ -0,0 +1,353 @@ +%%% ==================================================================== +%%% ``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 +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @doc The module {@module} is the default PubSub node tree plugin. +%%%

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.

+%%%

PubSub node tree plugins are using the {@link gen_nodetree} behaviour.

+%%%

The API isn't stabilized yet. 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.

+ +-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/5, + delete_node/2 + ]). + +-export([raw_to_node/2]). + +%% ================ +%% API definition +%% ================ + +%% @spec (Host, ServerHost, Opts) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = host() +%% Opts = list() +%% @doc

Called during pubsub modules initialisation. Any pubsub plugin must +%% implement this function. It can return anything.

+%%

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.

+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) -> 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) -> [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

Default node tree does not handle parents, return empty list.

+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

Default node tree does not handle parents, return a list +%% containing just this node.

+get_parentnodes_tree(Host, Node, From) -> + case get_node(Host, Node, From) of + N when is_record(N, pubsub_node) -> [{0, [N]}]; + Error -> Error + end. + +%% @spec (Host, Index) -> [pubsubNode()] | {error, Reason} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +get_subnodes(Host, Node, _From) -> + get_subnodes(Host, Node). +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. + +%% @spec (Host, Index) -> [pubsubNode()] | {error, Reason} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +get_subnodes_tree(Host, Node, _From) -> + get_subnodes_tree(Host, Node). +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() +create_node(Host, Node, Type, _Owner, Options) -> + case nodeid(Host, Node) of + {error, ?ERR_ITEM_NOT_FOUND} -> + {ParentNode, ParentExists} = case Host of + {_U, _S, _R} -> + %% This is special case for PEP handling + %% PEP does not uses hierarchy + {[], true}; + _ -> + Parent = lists:sublist(Node, length(Node) - 1), + ParentE = (Parent == []) orelse + case nodeid(Host, Parent) of + {result, _} -> true; + _ -> false + end, + {Parent, ParentE} + end, + case ParentExists of + true -> + case set_node(#pubsub_node{ + nodeid={Host, Node}, + parents=[ParentNode], + 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, string_to_node(Host, Node)}, + parents = [string_to_node(Host, 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] = Record#pubsub_node.parents, + 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. + +string_to_node({_, _, _}, Node) -> Node; +string_to_node(_, Node) -> ?PUBSUB:string_to_node(Node). diff --git a/src/mod_pubsub/pubsub_db_odbc.erl b/src/mod_pubsub/pubsub_db_odbc.erl new file mode 100644 index 000000000..e31ad2aca --- /dev/null +++ b/src/mod_pubsub/pubsub_db_odbc.erl @@ -0,0 +1,136 @@ +%%% ==================================================================== +%%% ``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 +%%% @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]). + + +-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_odbc.patch b/src/mod_pubsub/pubsub_odbc.patch new file mode 100644 index 000000000..673d530c2 --- /dev/null +++ b/src/mod_pubsub/pubsub_odbc.patch @@ -0,0 +1,484 @@ +--- mod_pubsub.erl 2009-07-31 16:53:48.000000000 +0200 ++++ mod_pubsub_odbc.erl 2009-07-31 17:07:00.000000000 +0200 +@@ -45,7 +45,7 @@ + %%% TODO + %%% plugin: generate Reply (do not use broadcast atom anymore) + +--module(mod_pubsub). ++-module(mod_pubsub_odbc). + -author('christophe.romain@process-one.net'). + -version('1.12-06'). + +@@ -57,9 +57,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, +@@ -104,7 +104,8 @@ + string_to_subscription/1, + string_to_affiliation/1, + extended_error/2, +- extended_error/3 ++ extended_error/3, ++ escape/1 + ]). + + %% API and gen_server callbacks +@@ -123,7 +124,7 @@ + -export([send_loop/1 + ]). + +--define(PROCNAME, ejabberd_mod_pubsub). ++-define(PROCNAME, ejabberd_mod_pubsub_odbc). + -define(PLUGIN_PREFIX, "node_"). + -define(TREE_PREFIX, "nodetree_"). + +@@ -212,8 +213,6 @@ + ok + end, + ejabberd_router:register_route(Host), +- update_node_database(Host, ServerHost), +- update_state_database(Host, ServerHost), + init_nodes(Host, ServerHost), + State = #state{host = Host, + server_host = ServerHost, +@@ -456,17 +455,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{options = Options, type = Type, id = NodeId} = Node, +- case get_option(Options, send_last_published_item) of +- on_sub_and_presence -> +- send_items(Host, Node, NodeId, Type, SubJID, last); +- _ -> +- ok +- end; ++ #pubsub_node{nodeid = {H, N}, type = Type, id = NodeId} = Node, ++ send_items(H, N, NodeId, Type, SubJID, last); + true -> + % resource not concerned about that subscription + ok +@@ -789,10 +786,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 -> +@@ -906,7 +903,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, +@@ -1011,7 +1009,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"]; + _ -> [] +@@ -1027,8 +1025,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) +@@ -1056,14 +1055,15 @@ + {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}}) -> + SN = node_to_string(SubNode), +@@ -1073,7 +1073,7 @@ + {"node", SN}, + {"name", RN}], []} + 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, []}; +@@ -1085,9 +1085,9 @@ + %% TODO That is, remove name attribute (or node?, please check for 2.1) + Action = + fun(#pubsub_node{type = Type, id = NodeId}) -> +- NodeItems = case node_call(Type, get_items, [NodeId, From]) of ++ {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}}) -> +@@ -1103,7 +1103,7 @@ + {xmlelement, "item", [{"jid", Host}, {"node", SN}, + {"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}; +@@ -1235,7 +1235,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"} -> +@@ -1258,7 +1259,10 @@ + + iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) -> + {xmlelement, _, _, SubEls} = SubEl, +- Action = xml:remove_cdata(SubEls), ++ NoRSM = lists:filter(fun({xmlelement, Name, _, _}) -> ++ Name == "set" ++ end, SubEls), ++ Action = xml:remove_cdata(SubEls) -- NoRSM, + case Action of + [{xmlelement, Name, Attrs, Els}] -> + Node = case Host of +@@ -1384,7 +1388,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 +@@ -1428,7 +1433,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", + [], +@@ -1457,7 +1462,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, +@@ -1524,8 +1529,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 -> +@@ -1711,7 +1716,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)), +@@ -1823,7 +1828,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), +@@ -1842,9 +1847,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 +@@ -2167,7 +2176,7 @@ + %%

The permission are not checked in this function.

+ %% @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 == "" -> ?MAXITEMS; +@@ -2206,11 +2215,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; +@@ -2223,7 +2232,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 +@@ -2255,15 +2265,22 @@ + %% @doc

Resend the items of a node to the user.

+ %% @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 ++ ToSend = case node_action(Host, Type, get_last_items, [NodeId, LJID, 1]) of ++ {result, []} -> []; ++ {result, Items} -> Items ++ end, ++ event_stanza( ++ [{xmlelement, "items", nodeAttr(Node), ++ itemsEls(ToSend)}]); + LastItem -> +- Stanza = event_stanza( ++ event_stanza( + [{xmlelement, "items", nodeAttr(Node), +- itemsEls([LastItem])}]), +- ejabberd_router ! {route, service_jid(Host), jlib:make_jid(LJID), Stanza} +- end; ++ itemsEls([LastItem])}]) ++ 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, []} -> +@@ -2381,29 +2398,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, []}; + _ -> +@@ -2665,8 +2665,8 @@ + error -> + {error, ?ERR_BAD_REQUEST}; + _ -> +- 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 -> + lists:foreach(fun({JID, Subscription, SubId}) -> + node_call(Type, set_subscriptions, [NodeId, JID, Subscription, SubId]) +@@ -3154,6 +3154,30 @@ + Result + end. + ++%% @spec (NodeId) -> [ljid()] ++%% NodeId = pubsubNodeId() ++%% @doc

Return list of node owners.

++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 (Options) -> MaxItems + %% Host = host() + %% Options = [Option] +@@ -3527,7 +3551,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

node plugin call.

+ node_call(Type, Function, Args) -> +@@ -3547,13 +3577,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

plugin transaction handling.

+ 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 +@@ -3566,8 +3596,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}; +@@ -3575,6 +3611,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}; +@@ -3583,6 +3628,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 index 0ea20672e..754a9d8f8 100644 --- a/src/mod_pubsub/pubsub_subscription.erl +++ b/src/mod_pubsub/pubsub_subscription.erl @@ -86,28 +86,28 @@ init() -> ok = create_table(). subscribe_node(JID, NodeID, Options) -> - case mnesia:transaction(fun add_subscription/3, + case mnesia:sync_dirty(fun add_subscription/3, [JID, NodeID, Options]) of {atomic, Result} -> {result, Result}; {aborted, Error} -> Error end. unsubscribe_node(JID, NodeID, SubID) -> - case mnesia:transaction(fun delete_subscription/3, + case mnesia:sync_dirty(fun delete_subscription/3, [JID, NodeID, SubID]) of {atomic, Result} -> {result, Result}; {aborted, Error} -> Error end. get_subscription(JID, NodeID, SubID) -> - case mnesia:transaction(fun read_subscription/3, + case mnesia:sync_dirty(fun read_subscription/3, [JID, NodeID, SubID]) of {atomic, Result} -> {result, Result}; {aborted, Error} -> Error end. set_subscription(JID, NodeID, SubID, Options) -> - case mnesia:transaction(fun write_subscription/4, + case mnesia:sync_dirty(fun write_subscription/4, [JID, NodeID, SubID, Options]) of {atomic, Result} -> {result, Result}; {aborted, Error} -> Error diff --git a/src/mod_pubsub/pubsub_subscription_odbc.erl b/src/mod_pubsub/pubsub_subscription_odbc.erl new file mode 100644 index 000000000..9ea535a18 --- /dev/null +++ b/src/mod_pubsub/pubsub_subscription_odbc.erl @@ -0,0 +1,292 @@ +%%% ==================================================================== +%%% ``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 , based on +%% pubsub_subscription.erl by Brian Cully +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(pubsub_subscription_odbc). +-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]). + +-include_lib("stdlib/include/qlc.hrl"). + +-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(), + ok = pubsub_db_odbc:add_subscription(#pubsub_subscription{subid = SubId, + options = Options}), + {result, SubId}. + + +unsubscribe_node(_JID, _NodeID, SubID) -> + {ok, Sub} = ?DB_MOD:read_subscription(SubID), + ok = ?DB_MOD:delete_subscription(SubID), + {result, Sub}. + +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, _} -> + ok = ?DB_MOD:update_subscription(#pubsub_subscription{subid = SubID, options = Options}), + {result, ok}; + notfound -> + {error, notfound} + 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". diff --git a/src/odbc/mysql.sql b/src/odbc/mysql.sql index dfbf69437..be182bf5f 100644 --- a/src/odbc/mysql.sql +++ b/src/odbc/mysql.sql @@ -158,3 +158,58 @@ CREATE TABLE roster_version ( -- ALTER TABLE rosterusers ADD COLUMN askmessage text AFTER ask; -- UPDATE rosterusers SET askmessage = ''; -- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; + +CREATE TABLE pubsub_node ( + host text, + node text, + parent text, + type text, + nodeid bigint auto_increment primary key +) CHARACTER SET utf8; +CREATE INDEX i_pubsub_node_parent ON pubsub_node(parent(120)); +CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node(host(20), node(120)); + +CREATE TABLE pubsub_node_option ( + nodeid bigint, + name text, + val text +) CHARACTER SET utf8; +CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option(nodeid); +ALTER TABLE `pubsub_node_option` ADD FOREIGN KEY (`nodeid`) REFERENCES `ejabberd`.`pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_node_owner ( + nodeid bigint, + owner text +) CHARACTER SET utf8; +CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner(nodeid); +ALTER TABLE `pubsub_node_owner` ADD FOREIGN KEY (`nodeid`) REFERENCES `ejabberd`.`pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_state ( + nodeid bigint, + jid text, + affiliation character(1), + subscription character(1), + stateid bigint auto_increment primary key +) CHARACTER SET utf8; +CREATE INDEX i_pubsub_state_jid ON pubsub_state(jid(60)); +CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state(nodeid, jid(60)); +ALTER TABLE `pubsub_state` ADD FOREIGN KEY (`nodeid`) REFERENCES `ejabberd`.`pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_item ( + nodeid bigint, + itemid text, + publisher text, + creation text, + modification text, + payload text +) CHARACTER SET utf8; +CREATE INDEX i_pubsub_item_itemid ON pubsub_item(itemid(36)); +CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item(nodeid, itemid(36)); +ALTER TABLE `pubsub_item` ADD FOREIGN KEY (`nodeid`) REFERENCES `ejabberd`.`pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_subscription_opt ( + subid text, + opt_name varchar(32), + opt_value text +); +CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt(subid(32), opt_name(32)); diff --git a/src/odbc/pg.sql b/src/odbc/pg.sql index 2273ad954..d02a9154e 100644 --- a/src/odbc/pg.sql +++ b/src/odbc/pg.sql @@ -162,3 +162,54 @@ CREATE TABLE roster_version ( -- ALTER TABLE rosterusers ADD COLUMN askmessage text; -- UPDATE rosterusers SET askmessage = ''; -- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; + +CREATE TABLE pubsub_node ( + host text, + node text, + parent text, + "type" text, + nodeid SERIAL UNIQUE +); +CREATE INDEX i_pubsub_node_parent ON pubsub_node USING btree (parent); +CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node USING btree (host, node); + +CREATE TABLE pubsub_node_option ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + name text, + val text +); +CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option USING btree (nodeid); + +CREATE TABLE pubsub_node_owner ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + owner text +); +CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner USING btree (nodeid); + +CREATE TABLE pubsub_state ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + jid text, + affiliation character(1), + subscription character(1), + stateid SERIAL UNIQUE +); +CREATE INDEX i_pubsub_state_jid ON pubsub_state USING btree (jid); +CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state USING btree (nodeid, jid); + +CREATE TABLE pubsub_item ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + itemid text, + publisher text, + creation text, + modification text, + payload text +); +CREATE INDEX i_pubsub_item_itemid ON pubsub_item USING btree (itemid); +CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item USING btree (nodeid, itemid); + +CREATE TABLE pubsub_subscription_opt ( + subid text, + opt_name varchar(32), + opt_value text +); +CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt USING btree (subid, opt_name);