24
1
mirror of https://github.com/processone/ejabberd.git synced 2024-05-31 21:07:55 +02:00
xmpp.chapril.org-ejabberd/src/mod_pubsub.erl
Evgeniy Khramtsov 66fc1bf3b6 Remove 'iqdisc' option
Since we got rid of all bottle-neck processes and we have
a connection pool for every database, the option is no longer
needed and in fact is detrimental: in practice what you get
is just a bunch of overloaded processes in the IQ handlers pool
no matter how much you increase the `iqdisc` value.

Given that there are close to zero operators understanding
the meaning of the option and, hence, not using it all,
it's not simply deprecated but completely removed.

The commit also deprecates the following functions:
- gen_iq_handler:add_iq_handler/6
- gen_iq_handler:handle/5
- gen_iq_handler:iqdisc/1
2018-02-11 12:54:15 +03:00

3889 lines
137 KiB
Erlang

%%%----------------------------------------------------------------------
%%% File : mod_pubsub.erl
%%% Author : Christophe Romain <christophe.romain@process-one.net>
%%% Purpose : Publish Subscribe service (XEP-0060)
%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2018 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%%% Support for subscription-options and multi-subscribe features was
%%% added by Brian Cully (bjc AT kublai.com). Subscriptions and options are
%%% stored in the pubsub_subscription table, with a link to them provided
%%% by the subscriptions field of pubsub_state. For information on
%%% subscription-options and mulit-subscribe see XEP-0060 sections 6.1.6,
%%% 6.2.3.1, 6.2.3.5, and 6.3. For information on subscription leases see
%%% XEP-0060 section 12.18.
-module(mod_pubsub).
-behaviour(gen_mod).
-behaviour(gen_server).
-author('christophe.romain@process-one.net').
-protocol({xep, 60, '1.14'}).
-protocol({xep, 163, '1.2'}).
-protocol({xep, 248, '0.2'}).
-include("ejabberd.hrl").
-include("logger.hrl").
-include("xmpp.hrl").
-include("pubsub.hrl").
-include("mod_roster.hrl").
-include("translate.hrl").
-define(STDTREE, <<"tree">>).
-define(STDNODE, <<"flat">>).
-define(PEPNODE, <<"pep">>).
%% exports for hooks
-export([presence_probe/3, caps_add/3, caps_update/3,
in_subscription/2, out_subscription/1,
on_self_presence/1, on_user_offline/2, 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,
c2s_handle_info/2]).
%% exported iq handlers
-export([iq_sm/1, process_disco_info/1, process_disco_items/1,
process_pubsub/1, process_pubsub_owner/1, process_vcard/1,
process_commands/1]).
%% exports for console debug manual use
-export([create_node/5, create_node/7, delete_node/3,
subscribe_node/5, unsubscribe_node/5, publish_item/6,
delete_item/4, delete_item/5, send_items/7, get_items/2, get_item/3,
get_cached_item/2, get_configure/5, set_configure/5,
tree_action/3, node_action/4, node_call/4]).
%% general helpers for plugins
-export([extended_error/2, service_jid/1,
tree/1, tree/2, plugin/2, plugins/1, config/3,
host/1, serverhost/1]).
%% pubsub#errors
-export([err_closed_node/0, err_configuration_required/0,
err_invalid_jid/0, err_invalid_options/0, err_invalid_payload/0,
err_invalid_subid/0, err_item_forbidden/0, err_item_required/0,
err_jid_required/0, err_max_items_exceeded/0, err_max_nodes_exceeded/0,
err_nodeid_required/0, err_not_in_roster_group/0, err_not_subscribed/0,
err_payload_too_big/0, err_payload_required/0,
err_pending_subscription/0, err_precondition_not_met/0,
err_presence_subscription_required/0, err_subid_required/0,
err_too_many_subscriptions/0, err_unsupported/1,
err_unsupported_access_model/0]).
%% API and gen_server callbacks
-export([start/2, stop/1, init/1,
handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1]).
%%====================================================================
%% API
%%====================================================================
%%--------------------------------------------------------------------
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
%% Description: Starts the server
%%--------------------------------------------------------------------
-export_type([
host/0,
hostPubsub/0,
hostPEP/0,
%%
nodeIdx/0,
nodeId/0,
itemId/0,
subId/0,
payload/0,
%%
nodeOption/0,
nodeOptions/0,
subOption/0,
subOptions/0,
pubOption/0,
pubOptions/0,
%%
affiliation/0,
subscription/0,
accessModel/0,
publishModel/0
]).
%% -type payload() defined here because the -type xmlel() is not accessible
%% from pubsub.hrl
-type(payload() :: [] | [xmlel(),...]).
-export_type([
pubsubNode/0,
pubsubState/0,
pubsubItem/0,
pubsubSubscription/0,
pubsubLastItem/0
]).
-type(pubsubNode() ::
#pubsub_node{
nodeid :: {Host::mod_pubsub:host(), Node::mod_pubsub:nodeId()},
id :: Nidx::mod_pubsub:nodeIdx(),
parents :: [Node::mod_pubsub:nodeId()],
type :: Type::binary(),
owners :: [Owner::ljid(),...],
options :: Opts::mod_pubsub:nodeOptions()
}
).
-type(pubsubState() ::
#pubsub_state{
stateid :: {Entity::ljid(), Nidx::mod_pubsub:nodeIdx()},
nodeidx :: Nidx::mod_pubsub:nodeIdx(),
items :: [ItemId::mod_pubsub:itemId()],
affiliation :: Affs::mod_pubsub:affiliation(),
subscriptions :: [{Sub::mod_pubsub:subscription(), SubId::mod_pubsub:subId()}]
}
).
-type(pubsubItem() ::
#pubsub_item{
itemid :: {ItemId::mod_pubsub:itemId(), Nidx::mod_pubsub:nodeIdx()},
nodeidx :: Nidx::mod_pubsub:nodeIdx(),
creation :: {erlang:timestamp(), ljid()},
modification :: {erlang:timestamp(), ljid()},
payload :: mod_pubsub:payload()
}
).
-type(pubsubSubscription() ::
#pubsub_subscription{
subid :: SubId::mod_pubsub:subId(),
options :: [] | mod_pubsub:subOptions()
}
).
-type(pubsubLastItem() ::
#pubsub_last_item{
nodeid :: {binary(), mod_pubsub:nodeIdx()},
itemid :: mod_pubsub:itemId(),
creation :: {erlang:timestamp(), ljid()},
payload :: mod_pubsub:payload()
}
).
-record(state,
{
server_host,
hosts,
access,
pep_mapping = [],
ignore_pep_from_offline = true,
last_item_cache = false,
max_items_node = ?MAXITEMS,
max_subscriptions_node = undefined,
default_node_config = [],
nodetree = <<"nodetree_", (?STDTREE)/binary>>,
plugins = [?STDNODE],
db_type
}).
-type(state() ::
#state{
server_host :: binary(),
hosts :: [mod_pubsub:hostPubsub()],
access :: atom(),
pep_mapping :: [{binary(), binary()}],
ignore_pep_from_offline :: boolean(),
last_item_cache :: boolean(),
max_items_node :: non_neg_integer(),
max_subscriptions_node :: non_neg_integer()|undefined,
default_node_config :: [{atom(), binary()|boolean()|integer()|atom()}],
nodetree :: binary(),
plugins :: [binary(),...],
db_type :: atom()
}
).
start(Host, Opts) ->
gen_mod:start_child(?MODULE, Host, Opts).
stop(Host) ->
gen_mod:stop_child(?MODULE, Host).
%%====================================================================
%% gen_server callbacks
%%====================================================================
%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% Description: Initiates the server
%%--------------------------------------------------------------------
-spec init([binary() | [{_,_}],...]) -> {'ok',state()}.
init([ServerHost, Opts]) ->
process_flag(trap_exit, true),
?DEBUG("pubsub init ~p ~p", [ServerHost, Opts]),
Hosts = gen_mod:get_opt_hosts(ServerHost, Opts),
Access = gen_mod:get_opt(access_createnode, Opts),
PepOffline = gen_mod:get_opt(ignore_pep_from_offline, Opts),
LastItemCache = gen_mod:get_opt(last_item_cache, Opts),
MaxItemsNode = gen_mod:get_opt(max_items_node, Opts),
MaxSubsNode = gen_mod:get_opt(max_subscriptions_node, Opts),
ejabberd_mnesia:create(?MODULE, pubsub_last_item,
[{ram_copies, [node()]},
{attributes, record_info(fields, pubsub_last_item)}]),
AllPlugins =
lists:flatmap(
fun(Host) ->
ejabberd_router:register_route(Host, ServerHost),
case gen_mod:get_module_opt(ServerHost, ?MODULE, db_type) of
mnesia -> pubsub_index:init(Host, ServerHost, Opts);
_ -> ok
end,
{Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts),
DefaultModule = plugin(Host, hd(Plugins)),
DefaultNodeCfg = merge_config(
gen_mod:get_opt(default_node_config, Opts),
DefaultModule:options()),
lists:foreach(
fun(H) ->
T = gen_mod:get_module_proc(H, config),
try
ets:new(T, [set, named_table]),
ets:insert(T, {nodetree, NodeTree}),
ets:insert(T, {plugins, Plugins}),
ets:insert(T, {last_item_cache, LastItemCache}),
ets:insert(T, {max_items_node, MaxItemsNode}),
ets:insert(T, {max_subscriptions_node, MaxSubsNode}),
ets:insert(T, {default_node_config, DefaultNodeCfg}),
ets:insert(T, {pep_mapping, PepMapping}),
ets:insert(T, {ignore_pep_from_offline, PepOffline}),
ets:insert(T, {host, Host}),
ets:insert(T, {access, Access})
catch error:badarg when H == ServerHost ->
ok
end
end, [Host, ServerHost]),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO,
?MODULE, process_disco_info),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS,
?MODULE, process_disco_items),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB,
?MODULE, process_pubsub),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER,
?MODULE, process_pubsub_owner),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD,
?MODULE, process_vcard),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_COMMANDS,
?MODULE, process_commands),
Plugins
end, Hosts),
ejabberd_hooks:add(c2s_self_presence, ServerHost,
?MODULE, on_self_presence, 75),
ejabberd_hooks:add(c2s_terminated, ServerHost,
?MODULE, on_user_offline, 75),
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),
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(c2s_handle_info, ServerHost,
?MODULE, c2s_handle_info, 50),
case lists:member(?PEPNODE, AllPlugins) of
true ->
ejabberd_hooks:add(caps_add, ServerHost,
?MODULE, caps_add, 80),
ejabberd_hooks:add(caps_update, ServerHost,
?MODULE, caps_update, 80),
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),
gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost,
?NS_PUBSUB, ?MODULE, iq_sm),
gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost,
?NS_PUBSUB_OWNER, ?MODULE, iq_sm);
false ->
ok
end,
NodeTree = config(ServerHost, nodetree),
Plugins = config(ServerHost, plugins),
PepMapping = config(ServerHost, pep_mapping),
DBType = gen_mod:get_module_opt(ServerHost, ?MODULE, db_type),
{ok, #state{hosts = Hosts, server_host = ServerHost,
access = Access, pep_mapping = PepMapping,
ignore_pep_from_offline = PepOffline,
last_item_cache = LastItemCache,
max_items_node = MaxItemsNode, nodetree = NodeTree,
plugins = Plugins, db_type = DBType}}.
depends(ServerHost, Opts0) ->
Opts = Opts0 ++ mod_options(ServerHost),
[Host|_] = gen_mod:get_opt_hosts(ServerHost, Opts),
Plugins = gen_mod:get_opt(plugins, Opts),
lists:flatmap(
fun(Name) ->
Plugin = plugin(ServerHost, Name),
try apply(Plugin, depends, [Host, ServerHost, Opts])
catch _:undef -> []
end
end, Plugins).
%% @doc Call the init/1 function for each plugin declared in the config file.
%% The default plugin module is implicit.
%% <p>The Erlang code for the plugin is located in a module called
%% <em>node_plugin</em>. The 'node_' prefix is mandatory.</p>
%% <p>See {@link node_hometree:init/1} for an example implementation.</p>
init_plugins(Host, ServerHost, Opts) ->
TreePlugin = tree(Host, gen_mod:get_opt(nodetree, Opts)),
?DEBUG("** tree plugin is ~p", [TreePlugin]),
TreePlugin:init(Host, ServerHost, Opts),
Plugins = gen_mod:get_opt(plugins, Opts),
PepMapping = gen_mod:get_opt(pep_mapping, Opts),
?DEBUG("** PEP Mapping : ~p~n", [PepMapping]),
PluginsOK = lists:foldl(
fun (Name, Acc) ->
Plugin = plugin(Host, Name),
case catch apply(Plugin, init, [Host, ServerHost, Opts]) of
{'EXIT', _Error} ->
Acc;
_ ->
?DEBUG("** init ~s plugin", [Name]),
[Name | Acc]
end
end,
[], Plugins),
{lists:reverse(PluginsOK), TreePlugin, PepMapping}.
terminate_plugins(Host, ServerHost, Plugins, TreePlugin) ->
lists:foreach(
fun (Name) ->
?DEBUG("** terminate ~s plugin", [Name]),
Plugin = plugin(Host, Name),
Plugin:terminate(Host, ServerHost)
end,
Plugins),
TreePlugin:terminate(Host, ServerHost),
ok.
%% -------
%% disco hooks handling functions
%%
-spec disco_local_identity([identity()], jid(), jid(),
binary(), binary()) -> [identity()].
disco_local_identity(Acc, _From, To, <<>>, _Lang) ->
case lists:member(?PEPNODE, plugins(host(To#jid.lserver))) of
true ->
[#identity{category = <<"pubsub">>, type = <<"pep">>} | Acc];
false ->
Acc
end;
disco_local_identity(Acc, _From, _To, _Node, _Lang) ->
Acc.
-spec disco_local_features({error, stanza_error()} | {result, [binary()]} | empty,
jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | {result, [binary()]} | empty.
disco_local_features(Acc, _From, To, <<>>, _Lang) ->
Host = host(To#jid.lserver),
Feats = case Acc of
{result, I} -> I;
_ -> []
end,
{result, Feats ++ [?NS_PUBSUB|[feature(F) || F <- features(Host, <<>>)]]};
disco_local_features(Acc, _From, _To, _Node, _Lang) ->
Acc.
-spec disco_local_items({error, stanza_error()} | {result, [disco_item()]} | empty,
jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | {result, [disco_item()]} | empty.
disco_local_items(Acc, _From, _To, <<>>, _Lang) -> Acc;
disco_local_items(Acc, _From, _To, _Node, _Lang) -> Acc.
-spec disco_sm_identity([identity()], jid(), jid(),
binary(), binary()) -> [identity()].
disco_sm_identity(Acc, From, To, Node, _Lang) ->
disco_identity(jid:tolower(jid:remove_resource(To)), Node, From)
++ Acc.
-spec disco_identity(binary(), binary(), jid()) -> [identity()].
disco_identity(_Host, <<>>, _From) ->
[#identity{category = <<"pubsub">>, type = <<"pep">>}];
disco_identity(Host, Node, From) ->
Action =
fun(#pubsub_node{id = Nidx, type = Type,
options = Options, owners = O}) ->
Owners = node_owners_call(Host, Type, Nidx, O),
case get_allowed_items_call(Host, Nidx, From, Type,
Options, Owners) of
{result, _} ->
{result, [#identity{category = <<"pubsub">>, type = <<"pep">>},
#identity{category = <<"pubsub">>, type = <<"leaf">>,
name = get_option(Options, title, <<>>)}]};
_ ->
{result, []}
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Result}} -> Result;
_ -> []
end.
-spec disco_sm_features({error, stanza_error()} | {result, [binary()]} | empty,
jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | {result, [binary()]}.
disco_sm_features(empty, From, To, Node, Lang) ->
disco_sm_features({result, []}, From, To, Node, Lang);
disco_sm_features({result, OtherFeatures} = _Acc, From, To, Node, _Lang) ->
{result,
OtherFeatures ++
disco_features(jid:tolower(jid:remove_resource(To)), Node, From)};
disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc.
-spec disco_features(ljid(), binary(), jid()) -> [binary()].
disco_features(Host, <<>>, _From) ->
[?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, <<"pep">>)]];
disco_features(Host, Node, From) ->
Action =
fun(#pubsub_node{id = Nidx, type = Type,
options = Options, owners = O}) ->
Owners = node_owners_call(Host, Type, Nidx, O),
case get_allowed_items_call(Host, Nidx, From,
Type, Options, Owners) of
{result, _} ->
{result,
[?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, <<"pep">>)]]};
_ ->
{result, []}
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Result}} -> Result;
_ -> []
end.
-spec disco_sm_items({error, stanza_error()} | {result, [disco_item()]} | empty,
jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | {result, [disco_item()]}.
disco_sm_items(empty, From, To, Node, Lang) ->
disco_sm_items({result, []}, From, To, Node, Lang);
disco_sm_items({result, OtherItems}, From, To, Node, _Lang) ->
{result, lists:usort(OtherItems ++
disco_items(jid:tolower(jid:remove_resource(To)), Node, From))};
disco_sm_items(Acc, _From, _To, _Node, _Lang) -> Acc.
-spec disco_items(ljid(), binary(), jid()) -> [disco_item()].
disco_items(Host, <<>>, From) ->
Action =
fun(#pubsub_node{nodeid = {_, Node}, options = Options,
type = Type, id = Nidx, owners = O}, Acc) ->
Owners = node_owners_call(Host, Type, Nidx, O),
case get_allowed_items_call(Host, Nidx, From,
Type, Options, Owners) of
{result, _} ->
[#disco_item{node = Node,
jid = jid:make(Host),
name = get_option(Options, title, <<>>)} | Acc];
_ ->
Acc
end
end,
NodeBloc = fun() ->
{result,
lists:foldl(Action, [], tree_call(Host, get_nodes, [Host]))}
end,
case transaction(Host, NodeBloc, sync_dirty) of
{result, Items} -> Items;
_ -> []
end;
disco_items(Host, Node, From) ->
Action =
fun(#pubsub_node{id = Nidx, type = Type,
options = Options, owners = O}) ->
Owners = node_owners_call(Host, Type, Nidx, O),
case get_allowed_items_call(Host, Nidx, From,
Type, Options, Owners) of
{result, Items} ->
{result, [#disco_item{jid = jid:make(Host),
name = ItemId}
|| #pubsub_item{itemid = {ItemId, _}} <- Items]};
_ ->
{result, []}
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Result}} -> Result;
_ -> []
end.
%% -------
%% presence and session hooks handling functions
%%
-spec caps_add(jid(), jid(), [binary()]) -> ok.
caps_add(JID, JID, _Features) ->
%% Send the owner his last PEP items.
send_last_pep(JID, JID);
caps_add(#jid{lserver = S1} = From, #jid{lserver = S2} = To, _Features)
when S1 =/= S2 ->
%% When a remote contact goes online while the local user is offline, the
%% remote contact won't receive last items from the local user even if
%% ignore_pep_from_offline is set to false. To work around this issue a bit,
%% we'll also send the last items to remote contacts when the local user
%% connects. That's the reason to use the caps_add hook instead of the
%% presence_probe_hook for remote contacts: The latter is only called when a
%% contact becomes available; the former is also executed when the local
%% user goes online (because that triggers the contact to send a presence
%% packet with CAPS).
send_last_pep(To, From);
caps_add(_From, _To, _Feature) ->
ok.
-spec caps_update(jid(), jid(), [binary()]) -> ok.
caps_update(From, To, _Features) ->
send_last_pep(To, From).
-spec presence_probe(jid(), jid(), pid()) -> ok.
presence_probe(#jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, _Pid) ->
%% ignore presence_probe from my other ressources
ok;
presence_probe(#jid{lserver = S} = From, #jid{lserver = S} = To, _Pid) ->
send_last_pep(To, From);
presence_probe(_From, _To, _Pid) ->
%% ignore presence_probe from remote contacts, those are handled via caps_add
ok.
-spec on_self_presence({presence(), ejabberd_c2s:state()})
-> {presence(), ejabberd_c2s:state()}.
on_self_presence({_, #{pres_last := _}} = Acc) -> % Just a presence update.
Acc;
on_self_presence({#presence{type = available}, #{jid := JID}} = Acc) ->
send_last_items(JID),
Acc;
on_self_presence(Acc) ->
Acc.
-spec on_user_offline(ejabberd_c2s:state(), atom()) -> ejabberd_c2s:state().
on_user_offline(#{jid := JID} = C2SState, _Reason) ->
purge_offline(jid:tolower(JID)),
C2SState;
on_user_offline(C2SState, _Reason) ->
C2SState.
%% -------
%% subscription hooks handling functions
%%
-spec out_subscription(presence()) -> any().
out_subscription(#presence{type = subscribed, from = From, to = To}) ->
send_last_pep(jid:remove_resource(From), To);
out_subscription(_) ->
ok.
-spec in_subscription(boolean(), presence()) -> true.
in_subscription(_, #presence{to = To, from = Owner, type = unsubscribed}) ->
unsubscribe_user(jid:remove_resource(To), Owner),
true;
in_subscription(_, _) ->
true.
unsubscribe_user(Entity, Owner) ->
spawn(fun () ->
[unsubscribe_user(ServerHost, Entity, Owner) ||
ServerHost <- lists:usort(lists:foldl(
fun(UserHost, Acc) ->
case gen_mod:is_loaded(UserHost, mod_pubsub) of
true -> [UserHost|Acc];
false -> Acc
end
end, [], [Entity#jid.lserver, Owner#jid.lserver]))]
end).
unsubscribe_user(Host, Entity, Owner) ->
BJID = jid:tolower(jid:remove_resource(Owner)),
lists:foreach(fun (PType) ->
{result, Subs} = node_action(Host, PType,
get_entity_subscriptions,
[Host, Entity]),
lists:foreach(fun
({#pubsub_node{options = Options,
owners = O,
id = Nidx},
subscribed, _, JID}) ->
Unsubscribe = match_option(Options, access_model, presence)
andalso lists:member(BJID, node_owners_action(Host, PType, Nidx, O)),
case Unsubscribe of
true ->
node_action(Host, PType,
unsubscribe_node, [Nidx, Entity, JID, all]);
false ->
ok
end;
(_) ->
ok
end,
Subs)
end,
plugins(Host)).
%% -------
%% user remove hook handling function
%%
-spec remove_user(binary(), binary()) -> ok.
remove_user(User, Server) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
Entity = jid:make(LUser, LServer),
Host = host(LServer),
HomeTreeBase = <<"/home/", LServer/binary, "/", LUser/binary>>,
spawn(fun () ->
lists:foreach(fun (PType) ->
{result, Subs} = node_action(Host, PType,
get_entity_subscriptions,
[Host, Entity]),
lists:foreach(fun
({#pubsub_node{id = Nidx}, _, _, JID}) ->
node_action(Host, PType,
unsubscribe_node,
[Nidx, Entity, JID, all]);
(_) ->
ok
end,
Subs),
{result, Affs} = node_action(Host, PType,
get_entity_affiliations,
[Host, Entity]),
lists:foreach(fun
({#pubsub_node{nodeid = {H, N}, parents = []}, owner}) ->
delete_node(H, N, Entity);
({#pubsub_node{nodeid = {H, N}, type = Type}, owner})
when N == HomeTreeBase, Type == <<"hometree">> ->
delete_node(H, N, Entity);
({#pubsub_node{id = Nidx}, publisher}) ->
node_action(Host, PType,
set_affiliation,
[Nidx, Entity, none]);
(_) ->
ok
end,
Affs)
end,
plugins(Host))
end),
ok.
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(_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, #iq{to = To} = IQ},
State) when To#jid.lresource == <<"">> ->
ejabberd_router:process_iq(IQ),
{noreply, State};
handle_info({route, Packet}, State) ->
To = xmpp:get_to(Packet),
case catch do_route(To#jid.lserver, 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{hosts = Hosts, server_host = ServerHost, nodetree = TreePlugin, plugins = Plugins}) ->
case lists:member(?PEPNODE, Plugins) of
true ->
ejabberd_hooks:delete(caps_add, ServerHost,
?MODULE, caps_add, 80),
ejabberd_hooks:delete(caps_update, ServerHost,
?MODULE, caps_update, 80),
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),
gen_iq_handler:remove_iq_handler(ejabberd_sm,
ServerHost, ?NS_PUBSUB),
gen_iq_handler:remove_iq_handler(ejabberd_sm,
ServerHost, ?NS_PUBSUB_OWNER);
false ->
ok
end,
ejabberd_hooks:delete(c2s_self_presence, ServerHost,
?MODULE, on_self_presence, 75),
ejabberd_hooks:delete(c2s_terminated, ServerHost,
?MODULE, on_user_offline, 75),
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),
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(c2s_handle_info, ServerHost,
?MODULE, c2s_handle_info, 50),
lists:foreach(
fun(Host) ->
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS),
terminate_plugins(Host, ServerHost, Plugins, TreePlugin),
ejabberd_router:unregister_route(Host)
end, Hosts).
%%--------------------------------------------------------------------
%% 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
%%--------------------------------------------------------------------
-spec process_disco_info(iq()) -> iq().
process_disco_info(#iq{type = set, lang = Lang} = IQ) ->
Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_disco_info(#iq{from = From, to = To, lang = Lang, type = get,
sub_els = [#disco_info{node = Node}]} = IQ) ->
Host = To#jid.lserver,
ServerHost = ejabberd_router:host_of_route(Host),
Info = ejabberd_hooks:run_fold(disco_info, ServerHost,
[],
[ServerHost, ?MODULE, <<>>, <<>>]),
case iq_disco_info(ServerHost, Host, Node, From, Lang) of
{result, IQRes} ->
XData = IQRes#disco_info.xdata ++ Info,
xmpp:make_iq_result(IQ, IQRes#disco_info{node = Node, xdata = XData});
{error, Error} ->
xmpp:make_error(IQ, Error)
end.
-spec process_disco_items(iq()) -> iq().
process_disco_items(#iq{type = set, lang = Lang} = IQ) ->
Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
process_disco_items(#iq{type = get, from = From, to = To,
sub_els = [#disco_items{node = Node} = SubEl]} = IQ) ->
Host = To#jid.lserver,
case iq_disco_items(Host, Node, From, SubEl#disco_items.rsm) of
{result, IQRes} ->
xmpp:make_iq_result(IQ, IQRes#disco_items{node = Node});
{error, Error} ->
xmpp:make_error(IQ, Error)
end.
-spec process_pubsub(iq()) -> iq().
process_pubsub(#iq{to = To} = IQ) ->
Host = To#jid.lserver,
ServerHost = ejabberd_router:host_of_route(Host),
Access = config(ServerHost, access),
case iq_pubsub(Host, Access, IQ) of
{result, IQRes} ->
xmpp:make_iq_result(IQ, IQRes);
{error, Error} ->
xmpp:make_error(IQ, Error)
end.
-spec process_pubsub_owner(iq()) -> iq().
process_pubsub_owner(#iq{to = To} = IQ) ->
Host = To#jid.lserver,
case iq_pubsub_owner(Host, IQ) of
{result, IQRes} ->
xmpp:make_iq_result(IQ, IQRes);
{error, Error} ->
xmpp:make_error(IQ, Error)
end.
-spec process_vcard(iq()) -> iq().
process_vcard(#iq{type = get, lang = Lang} = IQ) ->
xmpp:make_iq_result(IQ, iq_get_vcard(Lang));
process_vcard(#iq{type = set, lang = Lang} = IQ) ->
Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)).
-spec process_commands(iq()) -> iq().
process_commands(#iq{type = set, to = To, from = From,
sub_els = [#adhoc_command{} = Request]} = IQ) ->
Host = To#jid.lserver,
ServerHost = ejabberd_router:host_of_route(Host),
Plugins = config(ServerHost, plugins),
Access = config(ServerHost, access),
case adhoc_request(Host, ServerHost, From, Request, Access, Plugins) of
{error, Error} ->
xmpp:make_error(IQ, Error);
Response ->
xmpp:make_iq_result(
IQ, xmpp_util:make_adhoc_response(Request, Response))
end;
process_commands(#iq{type = get, lang = Lang} = IQ) ->
Txt = <<"Value 'get' of 'type' attribute is not allowed">>,
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)).
-spec do_route(binary(), stanza()) -> ok.
do_route(Host, Packet) ->
To = xmpp:get_to(Packet),
case To of
#jid{luser = <<>>, lresource = <<>>} ->
case Packet of
#message{type = T} when T /= error ->
case find_authorization_response(Packet) of
undefined ->
ok;
{error, Err} ->
ejabberd_router:route_error(Packet, Err);
AuthResponse ->
handle_authorization_response(
Host, Packet, AuthResponse)
end;
_ ->
Err = xmpp:err_service_unavailable(),
ejabberd_router:route_error(Packet, Err)
end;
_ ->
Err = xmpp:err_item_not_found(),
ejabberd_router:route_error(Packet, Err)
end.
-spec command_disco_info(binary(), binary(), jid()) -> {result, disco_info()}.
command_disco_info(_Host, ?NS_COMMANDS, _From) ->
{result, #disco_info{identities = [#identity{category = <<"automation">>,
type = <<"command-list">>}]}};
command_disco_info(_Host, ?NS_PUBSUB_GET_PENDING, _From) ->
{result, #disco_info{identities = [#identity{category = <<"automation">>,
type = <<"command-node">>}],
features = [?NS_COMMANDS]}}.
-spec node_disco_info(binary(), binary(), jid()) -> {result, disco_info()} |
{error, stanza_error()}.
node_disco_info(Host, Node, From) ->
node_disco_info(Host, Node, From, true, true).
-spec node_disco_info(binary(), binary(), jid(), boolean(), boolean()) ->
{result, disco_info()} | {error, stanza_error()}.
node_disco_info(Host, Node, _From, _Identity, _Features) ->
Action =
fun(#pubsub_node{id = Nidx, type = Type, options = Options}) ->
NodeType = case get_option(Options, node_type) of
collection -> <<"collection">>;
_ -> <<"leaf">>
end,
Affs = case node_call(Host, Type, get_node_affiliations, [Nidx]) of
{result, As} -> As;
_ -> []
end,
Subs = case node_call(Host, Type, get_node_subscriptions, [Nidx]) of
{result, Ss} -> Ss;
_ -> []
end,
Meta = [{title, get_option(Options, title, <<>>)},
{description, get_option(Options, description, <<>>)},
{owner, [jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= owner]},
{publisher, [jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= publisher]},
{num_subscribers, length(Subs)}],
XData = #xdata{type = result,
fields = pubsub_meta_data:encode(Meta)},
Is = [#identity{category = <<"pubsub">>, type = NodeType}],
Fs = [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, Type)]],
{result, #disco_info{identities = Is, features = Fs, xdata = [XData]}}
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Result}} -> {result, Result};
Other -> Other
end.
-spec iq_disco_info(binary(), binary(), binary(), jid(), binary())
-> {result, disco_info()} | {error, stanza_error()}.
iq_disco_info(ServerHost, Host, SNode, From, Lang) ->
[Node | _] = case SNode of
<<>> -> [<<>>];
_ -> str:tokens(SNode, <<"!">>)
end,
case Node of
<<>> ->
Name = gen_mod:get_module_opt(ServerHost, ?MODULE, name),
{result,
#disco_info{
identities = [#identity{
category = <<"pubsub">>,
type = <<"service">>,
name = translate:translate(Lang, Name)}],
features = [?NS_DISCO_INFO,
?NS_DISCO_ITEMS,
?NS_PUBSUB,
?NS_COMMANDS,
?NS_VCARD |
[feature(F) || F <- features(Host, Node)]]}};
?NS_COMMANDS ->
command_disco_info(Host, Node, From);
?NS_PUBSUB_GET_PENDING ->
command_disco_info(Host, Node, From);
_ ->
node_disco_info(Host, Node, From)
end.
-spec iq_disco_items(host(), binary(), jid(), undefined | rsm_set()) ->
{result, disco_items()} | {error, stanza_error()}.
iq_disco_items(Host, <<>>, From, _RSM) ->
Items =
lists:map(
fun(#pubsub_node{nodeid = {_, SubNode}, options = Options}) ->
case get_option(Options, title) of
false ->
#disco_item{jid = jid:make(Host),
node = SubNode};
Title ->
#disco_item{jid = jid:make(Host),
name = Title,
node = SubNode}
end
end, tree_action(Host, get_subnodes, [Host, <<>>, From])),
{result, #disco_items{items = Items}};
iq_disco_items(Host, ?NS_COMMANDS, _From, _RSM) ->
{result,
#disco_items{items = [#disco_item{jid = jid:make(Host),
node = ?NS_PUBSUB_GET_PENDING,
name = <<"Get Pending">>}]}};
iq_disco_items(_Host, ?NS_PUBSUB_GET_PENDING, _From, _RSM) ->
{result, #disco_items{}};
iq_disco_items(Host, Item, From, RSM) ->
case str:tokens(Item, <<"!">>) of
[_Node, _ItemId] ->
{result, #disco_items{}};
[Node] ->
Action = fun (#pubsub_node{id = Nidx, type = Type, options = Options, owners = O}) ->
Owners = node_owners_call(Host, Type, Nidx, O),
{NodeItems, RsmOut} = case get_allowed_items_call(Host, Nidx,
From, Type, Options, Owners, RSM)
of
{result, R} -> R;
_ -> {[], undefined}
end,
Nodes = lists:map(
fun(#pubsub_node{nodeid = {_, SubNode}, options = SubOptions}) ->
case get_option(SubOptions, title) of
false ->
#disco_item{jid = jid:make(Host),
node = SubNode};
Title ->
#disco_item{jid = jid:make(Host),
name = Title,
node = SubNode}
end
end, tree_call(Host, get_subnodes, [Host, Node, From])),
Items = lists:map(
fun(#pubsub_item{itemid = {RN, _}}) ->
{result, Name} = node_call(Host, Type, get_item_name, [Host, Node, RN]),
#disco_item{jid = jid:make(Host), name = Name}
end, NodeItems),
{result,
#disco_items{items = Nodes ++ Items,
rsm = RsmOut}}
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Result}} -> {result, Result};
Other -> Other
end
end.
-spec iq_sm(iq()) -> iq().
iq_sm(#iq{to = To, sub_els = [SubEl]} = IQ) ->
LOwner = jid:tolower(jid:remove_resource(To)),
Res = case xmpp:get_ns(SubEl) of
?NS_PUBSUB ->
iq_pubsub(LOwner, all, IQ);
?NS_PUBSUB_OWNER ->
iq_pubsub_owner(LOwner, IQ)
end,
case Res of
{result, IQRes} ->
xmpp:make_iq_result(IQ, IQRes);
{error, Error} ->
xmpp:make_error(IQ, Error)
end.
-spec iq_get_vcard(binary()) -> vcard_temp().
iq_get_vcard(Lang) ->
Desc = translate:translate(Lang, <<"ejabberd Publish-Subscribe module">>),
#vcard_temp{fn = <<"ejabberd/mod_pubsub">>,
url = ?EJABBERD_URI,
desc = <<Desc/binary, $\n, ?COPYRIGHT>>}.
-spec iq_pubsub(binary() | ljid(), atom(), iq()) ->
{result, pubsub()} | {error, stanza_error()}.
iq_pubsub(Host, Access, #iq{from = From, type = IQType, lang = Lang,
sub_els = [SubEl]}) ->
case {IQType, SubEl} of
{set, #pubsub{create = Node, configure = Configure,
_ = undefined}} when is_binary(Node) ->
ServerHost = serverhost(Host),
Plugins = config(ServerHost, plugins),
Config = case Configure of
{_, XData} -> decode_node_config(XData, Host, Lang);
undefined -> []
end,
Type = hd(Plugins),
case Config of
{error, _} = Err ->
Err;
_ ->
create_node(Host, ServerHost, Node, From, Type, Access, Config)
end;
{set, #pubsub{publish = #ps_publish{node = Node, items = Items},
publish_options = XData, configure = _, _ = undefined}} ->
ServerHost = serverhost(Host),
case Items of
[#ps_item{id = ItemId, sub_els = Payload}] ->
case decode_publish_options(XData, Lang) of
{error, _} = Err ->
Err;
PubOpts ->
publish_item(Host, ServerHost, Node, From, ItemId,
Payload, PubOpts, Access)
end;
[] ->
{error, extended_error(xmpp:err_bad_request(), err_item_required())};
_ ->
{error, extended_error(xmpp:err_bad_request(), err_invalid_payload())}
end;
{set, #pubsub{retract = #ps_retract{node = Node, notify = Notify, items = Items},
_ = undefined}} ->
case Items of
[#ps_item{id = ItemId}] ->
if ItemId /= <<>> ->
delete_item(Host, Node, From, ItemId, Notify);
true ->
{error, extended_error(xmpp:err_bad_request(),
err_item_required())}
end;
[] ->
{error, extended_error(xmpp:err_bad_request(), err_item_required())};
_ ->
{error, extended_error(xmpp:err_bad_request(), err_invalid_payload())}
end;
{set, #pubsub{subscribe = #ps_subscribe{node = Node, jid = JID},
options = Options, _ = undefined}} ->
Config = case Options of
#ps_options{xdata = XData} ->
decode_subscribe_options(XData, Lang);
_ ->
[]
end,
case Config of
{error, _} = Err ->
Err;
_ ->
subscribe_node(Host, Node, From, JID, Config)
end;
{set, #pubsub{unsubscribe = #ps_unsubscribe{node = Node, jid = JID, subid = SubId},
_ = undefined}} ->
unsubscribe_node(Host, Node, From, JID, SubId);
{get, #pubsub{items = #ps_items{node = Node,
max_items = MaxItems,
subid = SubId,
items = Items},
rsm = RSM, _ = undefined}} ->
ItemIds = [ItemId || #ps_item{id = ItemId} <- Items, ItemId /= <<>>],
get_items(Host, Node, From, SubId, MaxItems, ItemIds, RSM);
{get, #pubsub{subscriptions = {Node, _}, _ = undefined}} ->
Plugins = config(serverhost(Host), plugins),
get_subscriptions(Host, Node, From, Plugins);
{get, #pubsub{affiliations = {Node, _}, _ = undefined}} ->
Plugins = config(serverhost(Host), plugins),
get_affiliations(Host, Node, From, Plugins);
{get, #pubsub{options = #ps_options{node = Node, subid = SubId, jid = JID},
_ = undefined}} ->
get_options(Host, Node, JID, SubId, Lang);
{set, #pubsub{options = #ps_options{node = Node, subid = SubId,
jid = JID, xdata = XData},
_ = undefined}} ->
case decode_subscribe_options(XData, Lang) of
{error, _} = Err ->
Err;
Config ->
set_options(Host, Node, JID, SubId, Config)
end;
{set, #pubsub{}} ->
{error, xmpp:err_bad_request()};
_ ->
{error, xmpp:err_feature_not_implemented()}
end.
-spec iq_pubsub_owner(binary() | ljid(), iq()) -> {result, pubsub_owner() | undefined} |
{error, stanza_error()}.
iq_pubsub_owner(Host, #iq{type = IQType, from = From,
lang = Lang, sub_els = [SubEl]}) ->
case {IQType, SubEl} of
{get, #pubsub_owner{configure = {Node, undefined}, _ = undefined}} ->
ServerHost = serverhost(Host),
get_configure(Host, ServerHost, Node, From, Lang);
{set, #pubsub_owner{configure = {Node, XData}, _ = undefined}} ->
case XData of
undefined ->
{error, xmpp:err_bad_request(<<"No data form found">>, Lang)};
#xdata{type = cancel} ->
{result, #pubsub_owner{}};
#xdata{type = submit} ->
case decode_node_config(XData, Host, Lang) of
{error, _} = Err ->
Err;
Config ->
set_configure(Host, Node, From, Config, Lang)
end;
#xdata{} ->
{error, xmpp:err_bad_request(<<"Incorrect data form">>, Lang)}
end;
{get, #pubsub_owner{default = {Node, undefined}, _ = undefined}} ->
get_default(Host, Node, From, Lang);
{set, #pubsub_owner{delete = {Node, _}, _ = undefined}} ->
delete_node(Host, Node, From);
{set, #pubsub_owner{purge = Node, _ = undefined}} when Node /= undefined ->
purge_node(Host, Node, From);
{get, #pubsub_owner{subscriptions = {Node, []}, _ = undefined}} ->
get_subscriptions(Host, Node, From);
{set, #pubsub_owner{subscriptions = {Node, Subs}, _ = undefined}} ->
set_subscriptions(Host, Node, From, Subs);
{get, #pubsub_owner{affiliations = {Node, []}, _ = undefined}} ->
get_affiliations(Host, Node, From);
{set, #pubsub_owner{affiliations = {Node, Affs}, _ = undefined}} ->
set_affiliations(Host, Node, From, Affs);
{_, #pubsub_owner{}} ->
{error, xmpp:err_bad_request()};
_ ->
{error, xmpp:err_feature_not_implemented()}
end.
-spec adhoc_request(binary(), binary(), jid(), adhoc_command(),
atom(), [binary()]) -> adhoc_command() | {error, stanza_error()}.
adhoc_request(Host, _ServerHost, Owner,
#adhoc_command{node = ?NS_PUBSUB_GET_PENDING, lang = Lang,
action = execute, xdata = undefined},
_Access, Plugins) ->
send_pending_node_form(Host, Owner, Lang, Plugins);
adhoc_request(Host, _ServerHost, Owner,
#adhoc_command{node = ?NS_PUBSUB_GET_PENDING, lang = Lang,
action = execute, xdata = #xdata{} = XData} = Request,
_Access, _Plugins) ->
case decode_get_pending(XData, Lang) of
{error, _} = Err ->
Err;
Config ->
Node = proplists:get_value(node, Config),
case send_pending_auth_events(Host, Node, Owner, Lang) of
ok ->
xmpp_util:make_adhoc_response(
Request, #adhoc_command{action = completed});
Err ->
Err
end
end;
adhoc_request(_Host, _ServerHost, _Owner,
#adhoc_command{action = cancel}, _Access, _Plugins) ->
#adhoc_command{status = canceled};
adhoc_request(_Host, _ServerHost, _Owner, Other, _Access, _Plugins) ->
?DEBUG("Couldn't process ad hoc command:~n~p", [Other]),
{error, xmpp:err_item_not_found()}.
-spec send_pending_node_form(binary(), jid(), binary(),
[binary()]) -> adhoc_command() | {error, stanza_error()}.
send_pending_node_form(Host, Owner, Lang, Plugins) ->
Filter = fun (Type) ->
lists:member(<<"get-pending">>, plugin_features(Host, Type))
end,
case lists:filter(Filter, Plugins) of
[] ->
Err = extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('get-pending')),
{error, Err};
Ps ->
case get_pending_nodes(Host, Owner, Ps) of
{ok, Nodes} ->
XForm = #xdata{type = form,
fields = pubsub_get_pending:encode(
[{node, Nodes}], Lang)},
#adhoc_command{status = executing, action = execute,
xdata = XForm};
Err ->
Err
end
end.
-spec get_pending_nodes(binary(), jid(), [binary()]) -> {ok, [binary()]} |
{error, stanza_error()}.
get_pending_nodes(Host, Owner, Plugins) ->
Tr = fun (Type) ->
case node_call(Host, Type, get_pending_nodes, [Host, Owner]) of
{result, Nodes} -> Nodes;
_ -> []
end
end,
Action = fun() -> {result, lists:flatmap(Tr, Plugins)} end,
case transaction(Host, Action, sync_dirty) of
{result, Res} -> {ok, Res};
Err -> Err
end.
%% @doc <p>Send a subscription approval form to Owner for all pending
%% subscriptions on Host and Node.</p>
-spec send_pending_auth_events(binary(), binary(), jid(),
binary()) -> adhoc_command() | {error, stanza_error()}.
send_pending_auth_events(Host, Node, Owner, Lang) ->
?DEBUG("Sending pending auth events for ~s on ~s:~s",
[jid:encode(Owner), Host, Node]),
Action =
fun(#pubsub_node{id = Nidx, type = Type}) ->
case lists:member(<<"get-pending">>, plugin_features(Host, Type)) of
true ->
case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of
{result, owner} ->
node_call(Host, Type, get_node_subscriptions, [Nidx]);
_ ->
{error, xmpp:err_forbidden(
<<"Owner privileges required">>, Lang)}
end;
false ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('get-pending'))}
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {N, Subs}} ->
lists:foreach(
fun({J, pending, _SubId}) -> send_authorization_request(N, jid:make(J));
({J, pending}) -> send_authorization_request(N, jid:make(J));
(_) -> ok
end, Subs),
#adhoc_command{};
Err ->
Err
end.
%%% authorization handling
-spec send_authorization_request(#pubsub_node{}, jid()) -> ok.
send_authorization_request(#pubsub_node{nodeid = {Host, Node},
type = Type, id = Nidx, owners = O},
Subscriber) ->
%% TODO: pass lang to this function
Lang = <<"en">>,
Fs = pubsub_subscribe_authorization:encode(
[{node, Node},
{subscriber_jid, Subscriber},
{allow, false}],
Lang),
X = #xdata{type = form,
title = translate:translate(
Lang, <<"PubSub subscriber request">>),
instructions = [translate:translate(
Lang,
<<"Choose whether to approve this entity's "
"subscription.">>)],
fields = Fs},
Stanza = #message{from = service_jid(Host), sub_els = [X]},
lists:foreach(
fun (Owner) ->
ejabberd_router:route(xmpp:set_to(Stanza, jid:make(Owner)))
end, node_owners_action(Host, Type, Nidx, O)).
-spec find_authorization_response(message()) -> undefined |
pubsub_subscribe_authorization:result() |
{error, stanza_error()}.
find_authorization_response(Packet) ->
case xmpp:get_subtag(Packet, #xdata{type = form}) of
#xdata{type = cancel} ->
undefined;
#xdata{type = submit, fields = Fs} ->
try pubsub_subscribe_authorization:decode(Fs) of
Result -> Result
catch _:{pubsub_subscribe_authorization, Why} ->
Lang = xmpp:get_lang(Packet),
Txt = pubsub_subscribe_authorization:format_error(Why),
{error, xmpp:err_bad_request(Txt, Lang)}
end;
#xdata{} ->
{error, xmpp:err_bad_request()};
false ->
undefined
end.
%% @doc Send a message to JID with the supplied Subscription
-spec send_authorization_approval(binary(), jid(), binary(), subscribed | none) -> ok.
send_authorization_approval(Host, JID, SNode, Subscription) ->
Event = #ps_event{subscription =
#ps_subscription{jid = JID,
node = SNode,
type = Subscription}},
Stanza = #message{from = service_jid(Host), to = JID, sub_els = [Event]},
ejabberd_router:route(Stanza).
-spec handle_authorization_response(binary(), message(),
pubsub_subscribe_authorization:result()) -> ok.
handle_authorization_response(Host, #message{from = From} = Packet, Response) ->
Node = proplists:get_value(node, Response),
Subscriber = proplists:get_value(subscriber_jid, Response),
Allow = proplists:get_value(allow, Response),
Lang = xmpp:get_lang(Packet),
FromLJID = jid:tolower(jid:remove_resource(From)),
Action =
fun(#pubsub_node{type = Type, id = Nidx, owners = O}) ->
Owners = node_owners_call(Host, Type, Nidx, O),
case lists:member(FromLJID, Owners) of
true ->
{result, Subs} = node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]),
update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs);
false ->
{error, xmpp:err_forbidden(<<"Owner privileges required">>, Lang)}
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{error, Error} ->
ejabberd_router:route_error(Packet, Error);
{result, {_, _NewSubscription}} ->
%% XXX: notify about subscription state change, section 12.11
ok;
_ ->
Err = xmpp:err_internal_server_error(),
ejabberd_router:route_error(Packet, Err)
end.
-spec update_auth(binary(), binary(), _, _, jid() | error, boolean(), _) ->
{result, ok} | {error, stanza_error()}.
update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) ->
Sub= lists:filter(fun
({pending, _}) -> true;
(_) -> false
end,
Subs),
case Sub of
[{pending, SubId}|_] ->
NewSub = case Allow of
true -> subscribed;
false -> none
end,
node_call(Host, Type, set_subscriptions, [Nidx, Subscriber, NewSub, SubId]),
send_authorization_approval(Host, Subscriber, Node, NewSub),
{result, ok};
_ ->
Txt = <<"No pending subscriptions found">>,
{error, xmpp:err_unexpected_request(Txt, ?MYLANG)}
end.
%% @doc <p>Create new pubsub nodes</p>
%%<p>In addition to method-specific error conditions, there are several general reasons why the node creation request might fail:</p>
%%<ul>
%%<li>The service does not support node creation.</li>
%%<li>Only entities that are registered with the service are allowed to create nodes but the requesting entity is not registered.</li>
%%<li>The requesting entity does not have sufficient privileges to create nodes.</li>
%%<li>The requested Node already exists.</li>
%%<li>The request did not include a Node and "instant nodes" are not supported.</li>
%%</ul>
%%<p>ote: node creation is a particular case, error return code is evaluated at many places:</p>
%%<ul>
%%<li>iq_pubsub checks if service supports node creation (type exists)</li>
%%<li>create_node checks if instant nodes are supported</li>
%%<li>create_node asks node plugin if entity have sufficient privilege</li>
%%<li>nodetree create_node checks if nodeid already exists</li>
%%<li>node plugin create_node just sets default affiliation/subscription</li>
%%</ul>
-spec create_node(host(), binary(), binary(), jid(),
binary()) -> {result, pubsub()} | {error, stanza_error()}.
create_node(Host, ServerHost, Node, Owner, Type) ->
create_node(Host, ServerHost, Node, Owner, Type, all, []).
-spec create_node(host(), binary(), binary(), jid(), binary(),
atom(), [{binary(), [binary()]}]) -> {result, pubsub()} | {error, stanza_error()}.
create_node(Host, ServerHost, <<>>, Owner, Type, Access, Configuration) ->
case lists:member(<<"instant-nodes">>, plugin_features(Host, Type)) of
true ->
Node = randoms:get_string(),
case create_node(Host, ServerHost, Node, Owner, Type, Access, Configuration) of
{result, _} ->
{result, #pubsub{create = Node}};
Error ->
Error
end;
false ->
{error, extended_error(xmpp:err_not_acceptable(), err_nodeid_required())}
end;
create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) ->
Type = select_type(ServerHost, Host, Node, GivenType),
NodeOptions = merge_config(Configuration, node_options(Host, Type)),
CreateNode =
fun() ->
Parent = case node_call(Host, Type, node_to_path, [Node]) of
{result, [Node]} ->
<<>>;
{result, Path} ->
element(2, node_call(Host, Type, path_to_node,
[lists:sublist(Path, length(Path)-1)]))
end,
Parents = case Parent of
<<>> -> [];
_ -> [Parent]
end,
case node_call(Host, Type, create_node_permission,
[Host, ServerHost, Node, Parent, Owner, Access]) of
{result, true} ->
case tree_call(Host, create_node,
[Host, Node, Type, Owner, NodeOptions, Parents])
of
{ok, Nidx} ->
SubsByDepth = get_node_subs_by_depth(Host, Node, Owner),
case node_call(Host, Type, create_node, [Nidx, Owner]) of
{result, Result} -> {result, {Nidx, SubsByDepth, Result}};
Error -> Error
end;
{error, {virtual, Nidx}} ->
case node_call(Host, Type, create_node, [Nidx, Owner]) of
{result, Result} -> {result, {Nidx, [], Result}};
Error -> Error
end;
Error ->
Error
end;
_ ->
Txt = <<"You're not allowed to create nodes">>,
{error, xmpp:err_forbidden(Txt, ?MYLANG)}
end
end,
Reply = #pubsub{create = Node},
case transaction(Host, CreateNode, transaction) of
{result, {Nidx, SubsByDepth, {Result, broadcast}}} ->
broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth),
ejabberd_hooks:run(pubsub_create_node, ServerHost,
[ServerHost, Host, Node, Nidx, NodeOptions]),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
{result, {Nidx, _SubsByDepth, Result}} ->
ejabberd_hooks:run(pubsub_create_node, ServerHost,
[ServerHost, Host, Node, Nidx, NodeOptions]),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
Error ->
%% in case we change transaction to sync_dirty...
%% node_call(Host, Type, delete_node, [Host, Node]),
%% tree_call(Host, delete_node, [Host, Node]),
Error
end.
%% @doc <p>Delete specified node and all childs.</p>
%%<p>There are several reasons why the node deletion request might fail:</p>
%%<ul>
%%<li>The requesting entity does not have sufficient privileges to delete the node.</li>
%%<li>The node is the root collection node, which cannot be deleted.</li>
%%<li>The specified node does not exist.</li>
%%</ul>
-spec delete_node(host(), binary(), jid()) -> {result, pubsub_owner()} | {error, stanza_error()}.
delete_node(_Host, <<>>, _Owner) ->
{error, xmpp:err_not_allowed(<<"No node specified">>, ?MYLANG)};
delete_node(Host, Node, Owner) ->
Action = fun (#pubsub_node{type = Type, id = Nidx}) ->
case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of
{result, owner} ->
SubsByDepth = get_node_subs_by_depth(Host, Node, service_jid(Host)),
Removed = tree_call(Host, delete_node, [Host, Node]),
case node_call(Host, Type, delete_node, [Removed]) of
{result, Res} -> {result, {SubsByDepth, Res}};
Error -> Error
end;
_ ->
{error, xmpp:err_forbidden(<<"Owner privileges required">>, ?MYLANG)}
end
end,
Reply = undefined,
ServerHost = serverhost(Host),
case transaction(Host, Node, Action, transaction) of
{result, {_, {SubsByDepth, {Result, broadcast, Removed}}}} ->
lists:foreach(fun ({RNode, _RSubs}) ->
{RH, RN} = RNode#pubsub_node.nodeid,
RNidx = RNode#pubsub_node.id,
RType = RNode#pubsub_node.type,
ROptions = RNode#pubsub_node.options,
unset_cached_item(RH, RNidx),
broadcast_removed_node(RH, RN, RNidx, RType, ROptions, SubsByDepth),
ejabberd_hooks:run(pubsub_delete_node,
ServerHost,
[ServerHost, RH, RN, RNidx])
end,
Removed),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
{result, {_, {_, {Result, Removed}}}} ->
lists:foreach(fun ({RNode, _RSubs}) ->
{RH, RN} = RNode#pubsub_node.nodeid,
RNidx = RNode#pubsub_node.id,
unset_cached_item(RH, RNidx),
ejabberd_hooks:run(pubsub_delete_node,
ServerHost,
[ServerHost, RH, RN, RNidx])
end,
Removed),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
{result, {TNode, {_, Result}}} ->
Nidx = TNode#pubsub_node.id,
unset_cached_item(Host, Nidx),
ejabberd_hooks:run(pubsub_delete_node, ServerHost,
[ServerHost, Host, Node, Nidx]),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
Error ->
Error
end.
%% @see node_hometree:subscribe_node/5
%% @doc <p>Accepts or rejects subcription requests on a PubSub node.</p>
%%<p>There are several reasons why the subscription request might fail:</p>
%%<ul>
%%<li>The bare JID portions of the JIDs do not match.</li>
%%<li>The node has an access model of "presence" and the requesting entity is not subscribed to the owner's presence.</li>
%%<li>The node has an access model of "roster" and the requesting entity is not in one of the authorized roster groups.</li>
%%<li>The node has an access model of "whitelist" and the requesting entity is not on the whitelist.</li>
%%<li>The service requires payment for subscriptions to the node.</li>
%%<li>The requesting entity is anonymous and the service does not allow anonymous entities to subscribe.</li>
%%<li>The requesting entity has a pending subscription.</li>
%%<li>The requesting entity is blocked from subscribing (e.g., because having an affiliation of outcast).</li>
%%<li>The node does not support subscriptions.</li>
%%<li>The node does not exist.</li>
%%</ul>
-spec subscribe_node(host(), binary(), jid(), jid(), [{binary(), [binary()]}]) ->
{result, pubsub()} | {error, stanza_error()}.
subscribe_node(Host, Node, From, JID, Configuration) ->
SubModule = subscription_plugin(Host),
SubOpts = case SubModule:parse_options_xform(Configuration) of
{result, GoodSubOpts} -> GoodSubOpts;
_ -> invalid
end,
Subscriber = jid:tolower(JID),
Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx, owners = O}) ->
Features = plugin_features(Host, 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, []),
CanSubscribe = case get_max_subscriptions_node(Host) of
Max when is_integer(Max) ->
case node_call(Host, Type, get_node_subscriptions, [Nidx]) of
{result, NodeSubs} ->
SubsNum = lists:foldl(
fun ({_, subscribed, _}, Acc) -> Acc+1;
(_, Acc) -> Acc
end, 0, NodeSubs),
SubsNum < Max;
_ ->
true
end;
_ ->
true
end,
if not SubscribeFeature ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('subscribe'))};
not SubscribeConfig ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('subscribe'))};
HasOptions andalso not OptionsFeature ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('subscription-options'))};
SubOpts == invalid ->
{error, extended_error(xmpp:err_bad_request(),
err_invalid_options())};
not CanSubscribe ->
%% fallback to closest XEP compatible result, assume we are not allowed to subscribe
{error, extended_error(xmpp:err_not_allowed(),
err_closed_node())};
true ->
Owners = node_owners_call(Host, Type, Nidx, O),
{PS, RG} = get_presence_and_roster_permissions(Host, Subscriber,
Owners, AccessModel, AllowedGroups),
node_call(Host, Type, subscribe_node,
[Nidx, From, Subscriber, AccessModel,
SendLast, PS, RG, SubOpts])
end
end,
Reply = fun (Subscription) ->
Sub = case Subscription of
{subscribed, SubId} ->
#ps_subscription{jid = JID, type = subscribed, subid = SubId};
Other ->
#ps_subscription{jid = JID, type = Other}
end,
#pubsub{subscription = Sub#ps_subscription{node = Node}}
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {TNode, {Result, subscribed, SubId, send_last}}} ->
Nidx = TNode#pubsub_node.id,
Type = TNode#pubsub_node.type,
Options = TNode#pubsub_node.options,
send_items(Host, Node, Nidx, Type, Options, Subscriber, 1),
ServerHost = serverhost(Host),
ejabberd_hooks:run(pubsub_subscribe_node, ServerHost,
[ServerHost, Host, Node, Subscriber, SubId]),
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}} ->
{result, Result};
Error -> Error
end.
%% @doc <p>Unsubscribe <tt>JID</tt> from the <tt>Node</tt>.</p>
%%<p>There are several reasons why the unsubscribe request might fail:</p>
%%<ul>
%%<li>The requesting entity has multiple subscriptions to the node but does not specify a subscription ID.</li>
%%<li>The request does not specify an existing subscriber.</li>
%%<li>The requesting entity does not have sufficient privileges to unsubscribe the specified JID.</li>
%%<li>The node does not exist.</li>
%%<li>The request specifies a subscription ID that is not valid or current.</li>
%%</ul>
-spec unsubscribe_node(host(), binary(), jid(), jid(), binary()) ->
{result, undefined} | {error, stanza_error()}.
unsubscribe_node(Host, Node, From, JID, SubId) ->
Subscriber = jid:tolower(JID),
Action = fun (#pubsub_node{type = Type, id = Nidx}) ->
node_call(Host, Type, unsubscribe_node, [Nidx, From, Subscriber, SubId])
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, default}} ->
ServerHost = serverhost(Host),
ejabberd_hooks:run(pubsub_unsubscribe_node, ServerHost,
[ServerHost, Host, Node, Subscriber, SubId]),
{result, undefined};
Error -> Error
end.
%% @doc <p>Publish item to a PubSub node.</p>
%% <p>The permission to publish an item must be verified by the plugin implementation.</p>
%%<p>There are several reasons why the publish request might fail:</p>
%%<ul>
%%<li>The requesting entity does not have sufficient privileges to publish.</li>
%%<li>The node does not support item publication.</li>
%%<li>The node does not exist.</li>
%%<li>The payload size exceeds a service-defined limit.</li>
%%<li>The item contains more than one payload element or the namespace of the root payload element does not match the configured namespace for the node.</li>
%%<li>The request does not match the node configuration.</li>
%%</ul>
-spec publish_item(host(), binary(), binary(), jid(), binary(),
[xmlel()]) -> {result, pubsub()} | {error, stanza_error()}.
publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) ->
publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, [], all).
publish_item(Host, ServerHost, Node, Publisher, <<>>, Payload, PubOpts, Access) ->
publish_item(Host, ServerHost, Node, Publisher, uniqid(), Payload, PubOpts, Access);
publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access) ->
Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) ->
Features = plugin_features(Host, Type),
PublishFeature = lists:member(<<"publish">>, Features),
PublishModel = get_option(Options, publish_model),
DeliverPayloads = get_option(Options, deliver_payloads),
PersistItems = get_option(Options, persist_items),
MaxItems = max_items(Host, Options),
PayloadCount = payload_xmlelements(Payload),
PayloadSize = byte_size(term_to_binary(Payload)) - 2,
PayloadMaxSize = get_option(Options, max_payload_size),
PreconditionsMet = preconditions_met(PubOpts, Options),
if not PublishFeature ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported(publish))};
not PreconditionsMet ->
{error, extended_error(xmpp:err_conflict(),
err_precondition_not_met())};
PayloadSize > PayloadMaxSize ->
{error, extended_error(xmpp:err_not_acceptable(),
err_payload_too_big())};
(PayloadCount == 0) and (Payload == []) ->
{error, extended_error(xmpp:err_bad_request(),
err_payload_required())};
(PayloadCount > 1) or (PayloadCount == 0) ->
{error, extended_error(xmpp:err_bad_request(),
err_invalid_payload())};
(DeliverPayloads == false) and (PersistItems == false) and
(PayloadSize > 0) ->
{error, extended_error(xmpp:err_bad_request(),
err_item_forbidden())};
((DeliverPayloads == true) or (PersistItems == true)) and (PayloadSize == 0) ->
{error, extended_error(xmpp:err_bad_request(),
err_item_required())};
true ->
node_call(Host, Type, publish_item,
[Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, PubOpts])
end
end,
Reply = #pubsub{publish = #ps_publish{node = Node,
items = [#ps_item{id = ItemId}]}},
case transaction(Host, Node, Action, sync_dirty) of
{result, {TNode, {Result, Broadcast, Removed}}} ->
Nidx = TNode#pubsub_node.id,
Type = TNode#pubsub_node.type,
Options = TNode#pubsub_node.options,
BrPayload = case Broadcast of
broadcast -> Payload;
PluginPayload -> PluginPayload
end,
set_cached_item(Host, Nidx, ItemId, Publisher, BrPayload),
case get_option(Options, deliver_notifications) of
true ->
broadcast_publish_item(Host, Node, Nidx, Type, Options, ItemId,
Publisher, BrPayload, Removed);
false ->
ok
end,
ejabberd_hooks:run(pubsub_publish_item, ServerHost,
[ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
{result, {TNode, {default, Removed}}} ->
Nidx = TNode#pubsub_node.id,
Type = TNode#pubsub_node.type,
Options = TNode#pubsub_node.options,
broadcast_retract_items(Host, Node, Nidx, Type, Options, Removed),
set_cached_item(Host, Nidx, ItemId, Publisher, Payload),
{result, Reply};
{result, {TNode, {Result, Removed}}} ->
Nidx = TNode#pubsub_node.id,
Type = TNode#pubsub_node.type,
Options = TNode#pubsub_node.options,
broadcast_retract_items(Host, Node, Nidx, Type, Options, Removed),
set_cached_item(Host, Nidx, ItemId, Publisher, Payload),
{result, Result};
{result, {_, default}} ->
{result, Reply};
{result, {_, Result}} ->
{result, Result};
{error, #stanza_error{reason = 'item-not-found'}} ->
Type = select_type(ServerHost, Host, Node),
case lists:member(<<"auto-create">>, plugin_features(Host, Type)) of
true ->
case create_node(Host, ServerHost, Node, Publisher, Type, Access, PubOpts) of
{result, #pubsub{create = NewNode}} ->
publish_item(Host, ServerHost, NewNode, Publisher, ItemId,
Payload, PubOpts, Access);
_ ->
{error, xmpp:err_item_not_found()}
end;
false ->
Txt = <<"Automatic node creation is not enabled">>,
{error, xmpp:err_item_not_found(Txt, ?MYLANG)}
end;
Error ->
Error
end.
%% @doc <p>Delete item from a PubSub node.</p>
%% <p>The permission to delete an item must be verified by the plugin implementation.</p>
%%<p>There are several reasons why the item retraction request might fail:</p>
%%<ul>
%%<li>The publisher does not have sufficient privileges to delete the requested item.</li>
%%<li>The node or item does not exist.</li>
%%<li>The request does not specify a node.</li>
%%<li>The request does not include an <item/> element or the <item/> element does not specify an ItemId.</li>
%%<li>The node does not support persistent items.</li>
%%<li>The service does not support the deletion of items.</li>
%%</ul>
-spec delete_item(host(), binary(), jid(), binary()) -> {result, undefined} |
{error, stanza_error()}.
delete_item(Host, Node, Publisher, ItemId) ->
delete_item(Host, Node, Publisher, ItemId, false).
delete_item(_, <<>>, _, _, _) ->
{error, extended_error(xmpp:err_bad_request(), err_nodeid_required())};
delete_item(Host, Node, Publisher, ItemId, ForceNotify) ->
Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) ->
Features = plugin_features(Host, 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 ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('persistent-items'))};
not DeleteFeature ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('delete-items'))};
true ->
node_call(Host, Type, delete_item, [Nidx, Publisher, PublishModel, ItemId])
end
end,
Reply = undefined,
case transaction(Host, Node, Action, sync_dirty) of
{result, {TNode, {Result, broadcast}}} ->
Nidx = TNode#pubsub_node.id,
Type = TNode#pubsub_node.type,
Options = TNode#pubsub_node.options,
broadcast_retract_items(Host, Node, Nidx, Type, Options, [ItemId], ForceNotify),
case get_cached_item(Host, Nidx) of
#pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx);
_ -> ok
end,
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
{result, {_, default}} ->
{result, Reply};
{result, {_, Result}} ->
{result, Result};
Error ->
Error
end.
%% @doc <p>Delete all items of specified node owned by JID.</p>
%%<p>There are several reasons why the node purge request might fail:</p>
%%<ul>
%%<li>The node or service does not support node purging.</li>
%%<li>The requesting entity does not have sufficient privileges to purge the node.</li>
%%<li>The node is not configured to persist items.</li>
%%<li>The specified node does not exist.</li>
%%</ul>
-spec purge_node(mod_pubsub:host(), binary(), jid()) -> {result, undefined} |
{error, stanza_error()}.
purge_node(Host, Node, Owner) ->
Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) ->
Features = plugin_features(Host, Type),
PurgeFeature = lists:member(<<"purge-nodes">>, Features),
PersistentFeature = lists:member(<<"persistent-items">>, Features),
PersistentConfig = get_option(Options, persist_items),
if not PurgeFeature ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('purge-nodes'))};
not PersistentFeature ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('persistent-items'))};
not PersistentConfig ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('persistent-items'))};
true -> node_call(Host, Type, purge_node, [Nidx, Owner])
end
end,
Reply = undefined,
case transaction(Host, Node, Action, sync_dirty) of
{result, {TNode, {Result, broadcast}}} ->
Nidx = TNode#pubsub_node.id,
Type = TNode#pubsub_node.type,
Options = TNode#pubsub_node.options,
broadcast_purge_node(Host, Node, Nidx, Type, Options),
unset_cached_item(Host, Nidx),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
{result, {_, default}} ->
{result, Reply};
{result, {_, Result}} ->
{result, Result};
Error ->
Error
end.
%% @doc <p>Return the items of a given node.</p>
%% <p>The number of items to return is limited by MaxItems.</p>
%% <p>The permission are not checked in this function.</p>
-spec get_items(host(), binary(), jid(), binary(),
binary(), [binary()], undefined | rsm_set()) ->
{result, pubsub()} | {error, stanza_error()}.
get_items(Host, Node, From, SubId, MaxItems, ItemIds, undefined)
when MaxItems =/= undefined ->
get_items(Host, Node, From, SubId, MaxItems, ItemIds,
#rsm_set{max = MaxItems, before = <<>>});
get_items(Host, Node, From, SubId, _MaxItems, ItemIds, RSM) ->
Action =
fun(#pubsub_node{options = Options, type = Type,
id = Nidx, owners = O}) ->
Features = plugin_features(Host, 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, []),
if not RetreiveFeature ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('retrieve-items'))};
not PersistentFeature ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('persistent-items'))};
true ->
Owners = node_owners_call(Host, Type, Nidx, O),
{PS, RG} = get_presence_and_roster_permissions(
Host, From, Owners, AccessModel, AllowedGroups),
case ItemIds of
[ItemId] ->
node_call(Host, Type, get_item,
[Nidx, ItemId, From, AccessModel, PS, RG, undefined]);
_ ->
node_call(Host, Type, get_items,
[Nidx, From, AccessModel, PS, RG, SubId, RSM])
end
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,
{result,
#pubsub{items = #ps_items{node = Node,
items = itemsEls(SendItems)},
rsm = RsmOut}};
{result, {_, Item}} ->
{result,
#pubsub{items = #ps_items{node = Node,
items = itemsEls([Item])}}};
Error ->
Error
end.
get_items(Host, Node) ->
Action = fun (#pubsub_node{type = Type, id = Nidx}) ->
node_call(Host, Type, get_items, [Nidx, service_jid(Host), undefined])
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 = Nidx}) ->
node_call(Host, Type, get_item, [Nidx, ItemId])
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Items}} -> Items;
Error -> Error
end.
get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) ->
case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, undefined) of
{result, {Items, _RSM}} -> {result, Items};
Error -> Error
end.
get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, RSM) ->
AccessModel = get_option(Options, access_model),
AllowedGroups = get_option(Options, roster_groups_allowed, []),
{PS, RG} = get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups),
node_call(Host, Type, get_items, [Nidx, From, AccessModel, PS, RG, undefined, RSM]).
get_last_items(Host, Type, Nidx, LJID, 1) ->
case get_cached_item(Host, Nidx) of
undefined ->
case node_action(Host, Type, get_last_items, [Nidx, LJID, 1]) of
{result, Items} -> Items;
_ -> []
end;
LastItem ->
[LastItem]
end;
get_last_items(Host, Type, Nidx, LJID, Count) when Count > 1 ->
case node_action(Host, Type, get_last_items, [Nidx, LJID, Count]) of
{result, Items} -> Items;
_ -> []
end;
get_last_items(_Host, _Type, _Nidx, _LJID, _Count) ->
[].
%% @doc <p>Return the list of affiliations as an XMPP response.</p>
-spec get_affiliations(host(), binary(), jid(), [binary()]) ->
{result, pubsub()} | {error, stanza_error()}.
get_affiliations(Host, Node, JID, Plugins) when is_list(Plugins) ->
Result =
lists:foldl(
fun(Type, {Status, Acc}) ->
Features = plugin_features(Host, Type),
RetrieveFeature = lists:member(<<"retrieve-affiliations">>, Features),
if not RetrieveFeature ->
{{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('retrieve-affiliations'))},
Acc};
true ->
{result, Affs} = node_action(Host, Type,
get_entity_affiliations,
[Host, JID]),
{Status, [Affs | Acc]}
end
end,
{ok, []}, Plugins),
case Result of
{ok, Affs} ->
Entities = lists:flatmap(
fun({_, none}) ->
[];
({#pubsub_node{nodeid = {_, NodeId}}, Aff}) ->
if (Node == <<>>) or (Node == NodeId) ->
[#ps_affiliation{node = NodeId,
type = Aff}];
true ->
[]
end;
(_) ->
[]
end, lists:usort(lists:flatten(Affs))),
{result, #pubsub{affiliations = {<<>>, Entities}}};
{Error, _} ->
Error
end.
-spec get_affiliations(host(), binary(), jid()) ->
{result, pubsub_owner()} | {error, stanza_error()}.
get_affiliations(Host, Node, JID) ->
Action =
fun(#pubsub_node{type = Type, id = Nidx}) ->
Features = plugin_features(Host, Type),
RetrieveFeature = lists:member(<<"modify-affiliations">>, Features),
{result, Affiliation} = node_call(Host, Type, get_affiliation, [Nidx, JID]),
if not RetrieveFeature ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('modify-affiliations'))};
Affiliation /= owner ->
{error, xmpp:err_forbidden(<<"Owner privileges required">>, ?MYLANG)};
true ->
node_call(Host, Type, get_node_affiliations, [Nidx])
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, []}} ->
{error, xmpp:err_item_not_found()};
{result, {_, Affs}} ->
Entities = lists:flatmap(
fun({_, none}) ->
[];
({AJID, Aff}) ->
[#ps_affiliation{jid = AJID, type = Aff}]
end, Affs),
{result, #pubsub_owner{affiliations = {Node, Entities}}};
Error ->
Error
end.
-spec set_affiliations(host(), binary(), jid(), [ps_affiliation()]) ->
{result, undefined} | {error, stanza_error()}.
set_affiliations(Host, Node, From, Affs) ->
Owner = jid:tolower(jid:remove_resource(From)),
Action =
fun(#pubsub_node{type = Type, id = Nidx, owners = O} = N) ->
Owners = node_owners_call(Host, Type, Nidx, O),
case lists:member(Owner, Owners) of
true ->
OwnerJID = jid:make(Owner),
FilteredAffs =
case Owners of
[Owner] ->
[Aff || Aff <- Affs,
Aff#ps_affiliation.jid /= OwnerJID];
_ ->
Affs
end,
lists:foreach(
fun(#ps_affiliation{jid = JID, type = Affiliation}) ->
node_call(Host, Type, set_affiliation, [Nidx, JID, Affiliation]),
case Affiliation of
owner ->
NewOwner = jid:tolower(jid:remove_resource(JID)),
NewOwners = [NewOwner | Owners],
tree_call(Host,
set_node,
[N#pubsub_node{owners = NewOwners}]);
none ->
OldOwner = jid:tolower(jid:remove_resource(JID)),
case lists:member(OldOwner, Owners) of
true ->
NewOwners = Owners -- [OldOwner],
tree_call(Host,
set_node,
[N#pubsub_node{owners = NewOwners}]);
_ ->
ok
end;
_ ->
ok
end
end, FilteredAffs),
{result, undefined};
_ ->
{error, xmpp:err_forbidden(
<<"Owner privileges required">>, ?MYLANG)}
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Result}} -> {result, Result};
Other -> Other
end.
-spec get_options(binary(), binary(), jid(), binary(), binary()) ->
{result, xdata()} | {error, stanza_error()}.
get_options(Host, Node, JID, SubId, Lang) ->
Action = fun (#pubsub_node{type = Type, id = Nidx}) ->
case lists:member(<<"subscription-options">>, plugin_features(Host, Type)) of
true ->
get_options_helper(Host, JID, Lang, Node, Nidx, SubId, Type);
false ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('subscription-options'))}
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_Node, XForm}} -> {result, XForm};
Error -> Error
end.
-spec get_options_helper(binary(), jid(), binary(), binary(), _, binary(),
binary()) -> {result, pubsub()} | {error, stanza_error()}.
get_options_helper(Host, JID, Lang, Node, Nidx, SubId, Type) ->
Subscriber = jid:tolower(JID),
{result, Subs} = node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]),
SubIds = [Id || {Sub, Id} <- Subs, Sub == subscribed],
case {SubId, SubIds} of
{_, []} ->
{error, extended_error(xmpp:err_not_acceptable(),
err_not_subscribed())};
{<<>>, [SID]} ->
read_sub(Host, Node, Nidx, Subscriber, SID, Lang);
{<<>>, _} ->
{error, extended_error(xmpp:err_not_acceptable(),
err_subid_required())};
{_, _} ->
ValidSubId = lists:member(SubId, SubIds),
if ValidSubId ->
read_sub(Host, Node, Nidx, Subscriber, SubId, Lang);
true ->
{error, extended_error(xmpp:err_not_acceptable(),
err_invalid_subid())}
end
end.
-spec read_sub(binary(), binary(), nodeIdx(), ljid(), binary(), binary()) -> {result, pubsub()}.
read_sub(Host, Node, Nidx, Subscriber, SubId, Lang) ->
SubModule = subscription_plugin(Host),
XData = case SubModule:get_subscription(Subscriber, Nidx, SubId) of
{error, notfound} ->
undefined;
{result, #pubsub_subscription{options = Options}} ->
{result, X} = SubModule:get_options_xform(Lang, Options),
X
end,
{result, #pubsub{options = #ps_options{jid = jid:make(Subscriber),
subid = SubId,
node = Node,
xdata = XData}}}.
-spec set_options(binary(), binary(), jid(), binary(),
[{binary(), [binary()]}]) ->
{result, undefined} | {error, stanza_error()}.
set_options(Host, Node, JID, SubId, Configuration) ->
Action = fun (#pubsub_node{type = Type, id = Nidx}) ->
case lists:member(<<"subscription-options">>, plugin_features(Host, Type)) of
true ->
set_options_helper(Host, Configuration, JID, Nidx, SubId, Type);
false ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('subscription-options'))}
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_Node, Result}} -> {result, Result};
Error -> Error
end.
-spec set_options_helper(binary(), [{binary(), [binary()]}], jid(),
nodeIdx(), binary(), binary()) ->
{result, undefined} | {error, stanza_error()}.
set_options_helper(Host, Configuration, JID, Nidx, SubId, Type) ->
SubModule = subscription_plugin(Host),
SubOpts = case SubModule:parse_options_xform(Configuration) of
{result, GoodSubOpts} -> GoodSubOpts;
_ -> invalid
end,
Subscriber = jid:tolower(JID),
{result, Subs} = node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]),
SubIds = [Id || {Sub, Id} <- Subs, Sub == subscribed],
case {SubId, SubIds} of
{_, []} ->
{error, extended_error(xmpp:err_not_acceptable(), err_not_subscribed())};
{<<>>, [SID]} ->
write_sub(Host, Nidx, Subscriber, SID, SubOpts);
{<<>>, _} ->
{error, extended_error(xmpp:err_not_acceptable(), err_subid_required())};
{_, _} ->
write_sub(Host, Nidx, Subscriber, SubId, SubOpts)
end.
-spec write_sub(binary(), nodeIdx(), ljid(), binary(), _) -> {result, undefined} |
{error, stanza_error()}.
write_sub(_Host, _Nidx, _Subscriber, _SubId, invalid) ->
{error, extended_error(xmpp:err_bad_request(), err_invalid_options())};
write_sub(_Host, _Nidx, _Subscriber, _SubId, []) ->
{result, undefined};
write_sub(Host, Nidx, Subscriber, SubId, Options) ->
SubModule = subscription_plugin(Host),
case SubModule:set_subscription(Subscriber, Nidx, SubId, Options) of
{result, _} -> {result, undefined};
{error, _} -> {error, extended_error(xmpp:err_not_acceptable(),
err_invalid_subid())}
end.
%% @doc <p>Return the list of subscriptions as an XMPP response.</p>
-spec get_subscriptions(host(), binary(), jid(), [binary()]) ->
{result, pubsub()} | {error, stanza_error()}.
get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) ->
Result = lists:foldl(fun (Type, {Status, Acc}) ->
Features = plugin_features(Host, Type),
RetrieveFeature = lists:member(<<"retrieve-subscriptions">>, Features),
if not RetrieveFeature ->
{{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('retrieve-subscriptions'))},
Acc};
true ->
Subscriber = jid:remove_resource(JID),
{result, Subs} = node_action(Host, Type,
get_entity_subscriptions,
[Host, Subscriber]),
{Status, [Subs | Acc]}
end
end,
{ok, []}, Plugins),
case Result of
{ok, Subs} ->
Entities = lists:flatmap(fun
({#pubsub_node{nodeid = {_, SubsNode}}, Sub}) ->
case Node of
<<>> ->
[#ps_subscription{node = SubsNode, type = Sub}];
SubsNode ->
[#ps_subscription{type = Sub}];
_ ->
[]
end;
({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubId, SubJID}) ->
case Node of
<<>> ->
[#ps_subscription{jid = SubJID,
subid = SubId,
type = Sub,
node = SubsNode}];
SubsNode ->
[#ps_subscription{jid = SubJID,
subid = SubId,
type = Sub}];
_ ->
[]
end;
({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubJID}) ->
case Node of
<<>> ->
[#ps_subscription{jid = SubJID,
type = Sub,
node = SubsNode}];
SubsNode ->
[#ps_subscription{jid = SubJID, type = Sub}];
_ ->
[]
end
end,
lists:usort(lists:flatten(Subs))),
{result, #pubsub{subscriptions = {<<>>, Entities}}};
{Error, _} ->
Error
end.
-spec get_subscriptions(host(), binary(), jid()) -> {result, pubsub_owner()} |
{error, stanza_error()}.
get_subscriptions(Host, Node, JID) ->
Action = fun (#pubsub_node{type = Type, id = Nidx}) ->
Features = plugin_features(Host, Type),
RetrieveFeature = lists:member(<<"manage-subscriptions">>, Features),
{result, Affiliation} = node_call(Host, Type, get_affiliation, [Nidx, JID]),
if not RetrieveFeature ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('manage-subscriptions'))};
Affiliation /= owner ->
{error, xmpp:err_forbidden(<<"Owner privileges required">>, ?MYLANG)};
true ->
node_call(Host, Type, get_node_subscriptions, [Nidx])
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Subs}} ->
Entities =
lists:flatmap(
fun({_, none}) ->
[];
({_, pending, _}) ->
[];
({AJID, Sub}) ->
[#ps_subscription{jid = AJID, type = Sub}];
({AJID, Sub, SubId}) ->
[#ps_subscription{jid = AJID, type = Sub, subid = SubId}]
end, Subs),
{result, #pubsub_owner{subscriptions = {Node, Entities}}};
Error ->
Error
end.
get_subscriptions_for_send_last(Host, PType, sql, JID, LJID, BJID) ->
{result, Subs} = node_action(Host, PType,
get_entity_subscriptions_for_send_last,
[Host, JID]),
[{Node, SubId, SubJID}
|| {Node, Sub, SubId, SubJID} <- Subs,
Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID)];
% sql version already filter result by on_sub_and_presence
get_subscriptions_for_send_last(Host, PType, _, JID, LJID, BJID) ->
{result, Subs} = node_action(Host, PType,
get_entity_subscriptions,
[Host, JID]),
[{Node, SubId, SubJID}
|| {Node, Sub, SubId, SubJID} <- Subs,
Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID),
match_option(Node, send_last_published_item, on_sub_and_presence)].
-spec set_subscriptions(host(), binary(), jid(), [ps_subscription()]) ->
{result, undefined} | {error, stanza_error()}.
set_subscriptions(Host, Node, From, Entities) ->
Owner = jid:tolower(jid:remove_resource(From)),
Notify = fun(#ps_subscription{jid = JID, type = Sub}) ->
Stanza = #message{
from = service_jid(Host),
to = JID,
sub_els = [#ps_event{
subscription = #ps_subscription{
jid = JID,
type = Sub,
node = Node}}]},
ejabberd_router:route(Stanza)
end,
Action =
fun(#pubsub_node{type = Type, id = Nidx, owners = O}) ->
Owners = node_owners_call(Host, Type, Nidx, O),
case lists:member(Owner, Owners) of
true ->
Result =
lists:foldl(
fun(_, {error, _} = Err) ->
Err;
(#ps_subscription{jid = JID, type = Sub,
subid = SubId} = Entity, _) ->
case node_call(Host, Type,
set_subscriptions,
[Nidx, JID, Sub, SubId]) of
{error, _} = Err ->
Err;
_ ->
Notify(Entity)
end
end, ok, Entities),
case Result of
ok -> {result, undefined};
{error, _} = Err -> Err
end;
_ ->
{error, xmpp:err_forbidden(
<<"Owner privileges required">>, ?MYLANG)}
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Result}} -> {result, Result};
Other -> Other
end.
-spec get_presence_and_roster_permissions(
host(), ljid(), [ljid()], accessModel(),
[binary()]) -> {boolean(), boolean()}.
get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups) ->
if (AccessModel == presence) or (AccessModel == roster) ->
case Host of
{User, Server, _} ->
get_roster_info(User, Server, From, AllowedGroups);
_ ->
[{OUser, OServer, _} | _] = Owners,
get_roster_info(OUser, OServer, From, AllowedGroups)
end;
true ->
{true, true}
end.
get_roster_info(_, _, {<<>>, <<>>, _}, _) ->
{false, false};
get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, AllowedGroups) ->
LJID = {SubscriberUser, SubscriberServer, <<>>},
{Subscription, _Ask, Groups} = ejabberd_hooks:run_fold(roster_get_jid_info,
OwnerServer, {none, none, []},
[OwnerUser, OwnerServer, LJID]),
PresenceSubscription = Subscription == both orelse
Subscription == from orelse
{OwnerUser, OwnerServer} == {SubscriberUser, SubscriberServer},
RosterGroup = lists:any(fun (Group) ->
lists:member(Group, AllowedGroups)
end,
Groups),
{PresenceSubscription, RosterGroup};
get_roster_info(OwnerUser, OwnerServer, JID, AllowedGroups) ->
get_roster_info(OwnerUser, OwnerServer, jid:tolower(JID), AllowedGroups).
-spec preconditions_met(pubsub_publish_options:result(),
pubsub_node_config:result()) -> boolean().
preconditions_met(PubOpts, NodeOpts) ->
lists:all(fun(Opt) -> lists:member(Opt, NodeOpts) end, PubOpts).
-spec service_jid(jid() | ljid() | binary()) -> jid().
service_jid(#jid{} = Jid) -> Jid;
service_jid({U, S, R}) -> jid:make(U, S, R);
service_jid(Host) -> jid:make(Host).
%% @spec (LJID, NotifyType, Depth, NodeOptions, SubOptions) -> boolean()
%% LJID = jid()
%% NotifyType = items | nodes
%% Depth = integer()
%% NodeOptions = [{atom(), term()}]
%% SubOptions = [{atom(), term()}]
%% @doc <p>Check if a notification must be delivered or not based on
%% node and subscription options.</p>
is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) ->
sub_to_deliver(LJID, NotifyType, Depth, SubOptions)
andalso node_to_deliver(LJID, NodeOptions).
sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) ->
lists:all(fun (Option) ->
sub_option_can_deliver(NotifyType, Depth, Option)
end,
SubOptions).
node_to_deliver(LJID, NodeOptions) ->
presence_can_deliver(LJID, get_option(NodeOptions, presence_based_delivery)).
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}) -> p1_time_compat:timestamp() < When;
sub_option_can_deliver(_, _, _) -> true.
-spec presence_can_deliver(ljid(), boolean()) -> boolean().
presence_can_deliver(_, false) ->
true;
presence_can_deliver({User, Server, Resource}, true) ->
case ejabberd_sm:get_user_present_resources(User, Server) of
[] ->
false;
Ss ->
lists:foldl(fun
(_, true) ->
true;
({_, R}, _Acc) ->
case Resource of
<<>> -> true;
R -> true;
_ -> false
end
end,
false, Ss)
end.
-spec state_can_deliver(ljid(), subOptions() | []) -> [ljid()].
state_can_deliver({U, S, R}, []) -> [{U, S, R}];
state_can_deliver({U, S, R}, SubOptions) ->
case lists:keysearch(show_values, 1, SubOptions) of
%% If not in suboptions, item can be delivered, case doesn't apply
false -> [{U, S, R}];
%% If in a suboptions ...
{_, {_, ShowValues}} ->
Resources = case R of
%% If the subscriber JID is a bare one, get all its resources
<<>> -> user_resources(U, S);
%% If the subscriber JID is a full one, use its resource
R -> [R]
end,
lists:foldl(fun (Resource, Acc) ->
get_resource_state({U, S, Resource}, ShowValues, Acc)
end,
[], Resources)
end.
-spec get_resource_state(ljid(), [binary()], [ljid()]) -> [ljid()].
get_resource_state({U, S, R}, ShowValues, JIDs) ->
case ejabberd_sm:get_session_pid(U, S, R) of
none ->
%% If no PID, item can be delivered
lists:append([{U, S, R}], JIDs);
Pid ->
Show = case ejabberd_c2s:get_presence(Pid) of
#presence{type = unavailable} -> <<"unavailable">>;
#presence{show = undefined} -> <<"online">>;
#presence{show = S} -> atom_to_binary(S, latin1)
end,
case lists:member(Show, ShowValues) of
%% If yes, item can be delivered
true -> lists:append([{U, S, R}], JIDs);
%% If no, item can't be delivered
false -> JIDs
end
end.
-spec payload_xmlelements([xmlel()]) -> non_neg_integer().
payload_xmlelements(Payload) ->
payload_xmlelements(Payload, 0).
payload_xmlelements([], Count) -> Count;
payload_xmlelements([#xmlel{} | Tail], Count) ->
payload_xmlelements(Tail, Count + 1);
payload_xmlelements([_ | Tail], Count) ->
payload_xmlelements(Tail, Count).
items_event_stanza(Node, Options, Items) ->
MoreEls = case Items of
[LastItem] ->
{ModifNow, ModifUSR} = LastItem#pubsub_item.modification,
[#delay{stamp = ModifNow, from = jid:make(ModifUSR)}];
_ ->
[]
end,
BaseStanza = #message{
sub_els = [#ps_event{items = #ps_items{
node = Node,
items = itemsEls(Items)}}
| MoreEls]},
NotificationType = get_option(Options, notification_type, headline),
add_message_type(BaseStanza, NotificationType).
%%%%%% broadcast functions
broadcast_publish_item(Host, Node, Nidx, Type, NodeOptions, ItemId, From, Payload, Removed) ->
case get_collection_subscriptions(Host, Node) of
SubsByDepth when is_list(SubsByDepth) ->
EventItem0 = case get_option(NodeOptions, deliver_payloads) of
true -> #ps_item{sub_els = Payload, id = ItemId};
false -> #ps_item{id = ItemId}
end,
EventItem = case get_option(NodeOptions, itemreply, none) of
owner -> %% owner not supported
EventItem0;
publisher ->
EventItem0#ps_item{
publisher = jid:encode(From)};
none ->
EventItem0
end,
Stanza = #message{
sub_els =
[#ps_event{items =
#ps_items{node = Node,
items = [EventItem]}}]},
broadcast_stanza(Host, From, Node, Nidx, Type,
NodeOptions, SubsByDepth, items, Stanza, true),
case Removed of
[] ->
ok;
_ ->
case get_option(NodeOptions, notify_retract) of
true ->
RetractStanza = #message{
sub_els =
[#ps_event{
items = #ps_items{
node = Node,
retract = Removed}}]},
broadcast_stanza(Host, Node, Nidx, Type,
NodeOptions, SubsByDepth,
items, RetractStanza, true);
_ ->
ok
end
end,
{result, true};
_ ->
{result, false}
end.
broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds) ->
broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds, false).
broadcast_retract_items(_Host, _Node, _Nidx, _Type, _NodeOptions, [], _ForceNotify) ->
{result, false};
broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds, ForceNotify) ->
case (get_option(NodeOptions, notify_retract) or ForceNotify) of
true ->
case get_collection_subscriptions(Host, Node) of
SubsByDepth when is_list(SubsByDepth) ->
Stanza = #message{
sub_els =
[#ps_event{
items = #ps_items{
node = Node,
retract = ItemIds}}]},
broadcast_stanza(Host, Node, Nidx, Type,
NodeOptions, SubsByDepth, items, Stanza, true),
{result, true};
_ ->
{result, false}
end;
_ ->
{result, false}
end.
broadcast_purge_node(Host, Node, Nidx, Type, NodeOptions) ->
case get_option(NodeOptions, notify_retract) of
true ->
case get_collection_subscriptions(Host, Node) of
SubsByDepth when is_list(SubsByDepth) ->
Stanza = #message{sub_els = [#ps_event{purge = Node}]},
broadcast_stanza(Host, Node, Nidx, Type,
NodeOptions, SubsByDepth, nodes, Stanza, false),
{result, true};
_ ->
{result, false}
end;
_ ->
{result, false}
end.
broadcast_removed_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth) ->
case get_option(NodeOptions, notify_delete) of
true ->
case SubsByDepth of
[] ->
{result, false};
_ ->
Stanza = #message{sub_els = [#ps_event{delete = {Node, <<>>}}]},
broadcast_stanza(Host, Node, Nidx, Type,
NodeOptions, SubsByDepth, nodes, Stanza, false),
{result, true}
end;
_ ->
{result, false}
end.
broadcast_created_node(_, _, _, _, _, []) ->
{result, false};
broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth) ->
Stanza = #message{sub_els = [#ps_event{create = Node}]},
broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, nodes, Stanza, true),
{result, true}.
broadcast_config_notification(Host, Node, Nidx, Type, NodeOptions, Lang) ->
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 ->
#xdata{type = result,
fields = get_configure_xfields(
Type, NodeOptions, Lang, [])};
false ->
undefined
end,
Stanza = #message{
sub_els = [#ps_event{
configuration = {Node, Content}}]},
broadcast_stanza(Host, Node, Nidx, Type,
NodeOptions, SubsByDepth, nodes, Stanza, false),
{result, true};
_ ->
{result, false}
end;
_ ->
{result, false}
end.
get_collection_subscriptions(Host, Node) ->
Action = fun() ->
{result, get_node_subs_by_depth(Host, Node, service_jid(Host))}
end,
case transaction(Host, Action, sync_dirty) of
{result, CollSubs} -> CollSubs;
_ -> []
end.
get_node_subs_by_depth(Host, Node, From) ->
ParentTree = tree_call(Host, get_parentnodes_tree, [Host, Node, From]),
[{Depth, [{N, get_node_subs(Host, N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree].
get_node_subs(Host, #pubsub_node{type = Type, id = Nidx}) ->
WithOptions = lists:member(<<"subscription-options">>, plugin_features(Host, Type)),
case node_call(Host, Type, get_node_subscriptions, [Nidx]) of
{result, Subs} -> get_options_for_subs(Host, Nidx, Subs, WithOptions);
Other -> Other
end.
get_options_for_subs(_Host, _Nidx, Subs, false) ->
lists:foldl(fun({JID, subscribed, SubID}, Acc) ->
[{JID, SubID, []} | Acc];
(_, Acc) ->
Acc
end, [], Subs);
get_options_for_subs(Host, Nidx, Subs, true) ->
SubModule = subscription_plugin(Host),
lists:foldl(fun({JID, subscribed, SubID}, Acc) ->
case SubModule:get_subscription(JID, Nidx, SubID) of
#pubsub_subscription{options = Options} -> [{JID, SubID, Options} | Acc];
{error, notfound} -> [{JID, SubID, []} | Acc]
end;
(_, Acc) ->
Acc
end, [], Subs).
broadcast_stanza(Host, _Node, _Nidx, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) ->
NotificationType = get_option(NodeOptions, notification_type, headline),
BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but useful
Stanza = add_message_type(
xmpp:set_from(BaseStanza, service_jid(Host)),
NotificationType),
%% Handles explicit subscriptions
SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth),
lists:foreach(fun ({LJID, _NodeName, SubIDs}) ->
LJIDs = case BroadcastAll of
true ->
{U, S, _} = LJID,
[{U, S, R} || R <- user_resources(U, S)];
false ->
[LJID]
end,
%% Determine if the stanza should have SHIM ('SubID' and 'name') headers
StanzaToSend = case {SHIM, SubIDs} of
{false, _} ->
Stanza;
%% If there's only one SubID, don't add it
{true, [_]} ->
Stanza;
{true, SubIDs} ->
add_shim_headers(Stanza, subid_shim(SubIDs))
end,
lists:foreach(fun(To) ->
ejabberd_router:route(
xmpp:set_to(StanzaToSend, jid:make(To)))
end, LJIDs)
end, SubIDsByJID).
broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) ->
broadcast_stanza({LUser, LServer, <<>>}, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM),
%% Handles implicit presence subscriptions
SenderResource = user_resource(LUser, LServer, LResource),
NotificationType = get_option(NodeOptions, notification_type, headline),
%% 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
FromBareJid = xmpp:set_from(BaseStanza, jid:make(LUser, LServer)),
Stanza = add_extended_headers(
add_message_type(FromBareJid, NotificationType),
extended_headers([Publisher])),
ejabberd_sm:route(jid:make(LUser, LServer, SenderResource),
{pep_message, <<((Node))/binary, "+notify">>, Stanza}),
ejabberd_router:route(xmpp:set_to(Stanza, jid:make(LUser, LServer)));
broadcast_stanza(Host, _Publisher, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) ->
broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM).
-spec c2s_handle_info(ejabberd_c2s:state(), term()) -> ejabberd_c2s:state().
c2s_handle_info(#{lserver := LServer} = C2SState,
{pep_message, Feature, Packet}) ->
[maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet)
|| {USR, Caps} <- mod_caps:list_features(C2SState)],
{stop, C2SState};
c2s_handle_info(#{lserver := LServer} = C2SState,
{pep_message, Feature, Packet, USR}) ->
case mod_caps:get_user_caps(USR, C2SState) of
{ok, Caps} -> maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet);
error -> ok
end,
{stop, C2SState};
c2s_handle_info(C2SState, _) ->
C2SState.
send_items(Host, Node, Nidx, Type, Options, LJID, Number) ->
send_items(Host, Node, Nidx, Type, Options, Host, LJID, LJID, Number).
send_items(Host, Node, Nidx, Type, Options, Publisher, SubLJID, ToLJID, Number) ->
case get_last_items(Host, Type, Nidx, SubLJID, Number) of
[] ->
ok;
Items ->
Stanza = items_event_stanza(Node, Options, Items),
send_stanza(Publisher, ToLJID, Node, Stanza)
end.
send_stanza({LUser, LServer, _} = Publisher, USR, Node, BaseStanza) ->
Stanza = xmpp:set_from(BaseStanza, jid:make(LUser, LServer)),
USRs = case USR of
{PUser, PServer, <<>>} ->
[{PUser, PServer, PRessource}
|| PRessource <- user_resources(PUser, PServer)];
_ ->
[USR]
end,
[ejabberd_sm:route(jid:make(Publisher),
{pep_message, <<((Node))/binary, "+notify">>,
add_extended_headers(
Stanza, extended_headers([Publisher])),
To}) || To <- USRs];
send_stanza(Host, USR, _Node, Stanza) ->
ejabberd_router:route(
xmpp:set_from_to(Stanza, service_jid(Host), jid:make(USR))).
maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet) ->
Features = mod_caps:get_features(LServer, Caps),
case lists:member(Feature, Features) of
true ->
ejabberd_router:route(xmpp:set_to(Packet, jid:make(USR)));
false ->
ok
end.
send_last_items(JID) ->
ServerHost = JID#jid.lserver,
Host = host(ServerHost),
DBType = config(ServerHost, db_type),
LJID = jid:tolower(JID),
BJID = jid:remove_resource(LJID),
lists:foreach(
fun(PType) ->
Subs = get_subscriptions_for_send_last(Host, PType, DBType, JID, LJID, BJID),
lists:foreach(
fun({#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx,
options = Options}, _, SubJID})
when Type == PType->
send_items(Host, Node, Nidx, PType, Options, Host, SubJID, LJID, 1);
(_) ->
ok
end,
lists:usort(Subs))
end, config(ServerHost, plugins)).
% pep_from_offline hack can not work anymore, as sender c2s does not
% exists when sender is offline, so we can't get match receiver caps
% does it make sens to send PEP from an offline contact anyway ?
% case config(ServerHost, ignore_pep_from_offline) of
% false ->
% Roster = ejabberd_hooks:run_fold(roster_get, ServerHost, [],
% [{JID#jid.luser, ServerHost}]),
% lists:foreach(
% fun(#roster{jid = {U, S, R}, subscription = Sub})
% when Sub == both orelse Sub == from,
% S == ServerHost ->
% case user_resources(U, S) of
% [] -> send_last_pep(jid:make(U, S, R), JID);
% _ -> ok %% this is already handled by presence probe
% end;
% (_) ->
% ok %% we can not do anything in any cases
% end, Roster);
% true ->
% ok
% end.
send_last_pep(From, To) ->
ServerHost = From#jid.lserver,
Host = host(ServerHost),
Publisher = jid:tolower(From),
Owner = jid:remove_resource(Publisher),
lists:foreach(
fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}) ->
case match_option(Options, send_last_published_item, on_sub_and_presence) of
true ->
LJID = jid:tolower(To),
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, Nidx, Type, Options, Publisher, LJID, LJID, 1);
true -> ok
end;
_ ->
ok
end
end,
tree_action(Host, get_nodes, [Owner, From])).
subscribed_nodes_by_jid(NotifyType, SubsByDepth) ->
NodesToDeliver = fun (Depth, Node, Subs, Acc) ->
NodeName = case Node#pubsub_node.nodeid of
{_, N} -> N;
Other -> Other
end,
NodeOptions = Node#pubsub_node.options,
lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) ->
case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of
true ->
case state_can_deliver(LJID, SubOptions) of
[] -> {JIDs, Recipients};
[LJID] -> {JIDs, [{LJID, NodeName, [SubID]} | Recipients]};
JIDsToDeliver ->
lists:foldl(
fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) ->
case lists:member(JIDToDeliver, JIDs) of
%% check if the JIDs co-accumulator contains the Subscription Jid,
false ->
%% - if not,
%% - add the Jid to JIDs list co-accumulator ;
%% - create a tuple of the Jid, Nidx, and SubID (as list),
%% and add the tuple to the Recipients list co-accumulator
{[JIDToDeliver | JIDsAcc],
[{JIDToDeliver, NodeName, [SubID]}
| RecipientsAcc]};
true ->
%% - if the JIDs co-accumulator contains the Jid
%% get the tuple containing the Jid from the Recipient list co-accumulator
{_, {JIDToDeliver, NodeName1, SubIDs}} =
lists:keysearch(JIDToDeliver, 1, RecipientsAcc),
%% delete the tuple from the Recipients list
% v1 : Recipients1 = lists:keydelete(LJID, 1, Recipients),
% v2 : Recipients1 = lists:keyreplace(LJID, 1, Recipients, {LJID, Nidx1, [SubID | SubIDs]}),
%% add the SubID to the SubIDs list in the tuple,
%% and add the tuple back to the Recipients list co-accumulator
% v1.1 : {JIDs, lists:append(Recipients1, [{LJID, Nidx1, lists:append(SubIDs, [SubID])}])}
% v1.2 : {JIDs, [{LJID, Nidx1, [SubID | SubIDs]} | Recipients1]}
% v2: {JIDs, Recipients1}
{JIDsAcc,
lists:keyreplace(JIDToDeliver, 1,
RecipientsAcc,
{JIDToDeliver, NodeName1,
[SubID | SubIDs]})}
end
end, {JIDs, Recipients}, JIDsToDeliver)
end;
false ->
{JIDs, Recipients}
end
end, Acc, Subs)
end,
DepthsToDeliver = fun({Depth, SubsByNode}, Acc1) ->
lists:foldl(fun({Node, Subs}, Acc2) ->
NodesToDeliver(Depth, Node, Subs, Acc2)
end, Acc1, SubsByNode)
end,
{_, JIDSubs} = lists:foldl(DepthsToDeliver, {[], []}, SubsByDepth),
JIDSubs.
-spec user_resources(binary(), binary()) -> [binary()].
user_resources(User, Server) ->
ejabberd_sm:get_user_resources(User, Server).
-spec user_resource(binary(), binary(), binary()) -> binary().
user_resource(User, Server, <<>>) ->
case user_resources(User, Server) of
[R | _] -> R;
_ -> <<>>
end;
user_resource(_, _, Resource) ->
Resource.
%%%%%%% Configuration handling
-spec get_configure(host(), binary(), binary(), jid(),
binary()) -> {error, stanza_error()} | {result, pubsub_owner()}.
get_configure(Host, ServerHost, Node, From, Lang) ->
Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) ->
case node_call(Host, Type, get_affiliation, [Nidx, From]) of
{result, owner} ->
Groups = ejabberd_hooks:run_fold(roster_groups, ServerHost, [], [ServerHost]),
Fs = get_configure_xfields(Type, Options, Lang, Groups),
{result, #pubsub_owner{
configure =
{Node, #xdata{type = form, fields = Fs}}}};
_ ->
{error, xmpp:err_forbidden(<<"Owner privileges required">>, Lang)}
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Result}} -> {result, Result};
Other -> Other
end.
-spec get_default(host(), binary(), jid(), binary()) -> {result, pubsub_owner()}.
get_default(Host, Node, _From, Lang) ->
Type = select_type(serverhost(Host), Host, Node),
Options = node_options(Host, Type),
Fs = get_configure_xfields(Type, Options, Lang, []),
{result, #pubsub_owner{default = {<<>>, #xdata{type = form, fields = Fs}}}}.
-spec match_option(#pubsub_node{} | [{atom(), any()}], atom(), any()) -> boolean().
match_option(Node, Var, Val) when is_record(Node, pubsub_node) ->
match_option(Node#pubsub_node.options, Var, Val);
match_option(Options, Var, Val) when is_list(Options) ->
get_option(Options, Var) == Val;
match_option(_, _, _) ->
false.
-spec get_option([{atom(), any()}], atom()) -> any().
get_option([], _) -> false;
get_option(Options, Var) -> get_option(Options, Var, false).
-spec get_option([{atom(), any()}], atom(), any()) -> any().
get_option(Options, Var, Def) ->
case lists:keysearch(Var, 1, Options) of
{value, {_Val, Ret}} -> Ret;
_ -> Def
end.
-spec node_options(host(), binary()) -> [{atom(), any()}].
node_options(Host, Type) ->
DefaultOpts = node_plugin_options(Host, Type),
case config(Host, plugins) of
[Type|_] -> config(Host, default_node_config, DefaultOpts);
_ -> DefaultOpts
end.
-spec node_plugin_options(host(), binary()) -> [{atom(), any()}].
node_plugin_options(Host, Type) ->
Module = plugin(Host, Type),
case catch Module:options() of
{'EXIT', {undef, _}} ->
DefaultModule = plugin(Host, ?STDNODE),
DefaultModule:options();
Result ->
Result
end.
-spec node_owners_action(host(), binary(), nodeIdx(), [ljid()]) -> [ljid()].
node_owners_action(Host, Type, Nidx, []) ->
case node_action(Host, Type, get_node_affiliations, [Nidx]) of
{result, Affs} -> [LJID || {LJID, Aff} <- Affs, Aff =:= owner];
_ -> []
end;
node_owners_action(_Host, _Type, _Nidx, Owners) ->
Owners.
-spec node_owners_call(host(), binary(), nodeIdx(), [ljid()]) -> [ljid()].
node_owners_call(Host, Type, Nidx, []) ->
case node_call(Host, Type, get_node_affiliations, [Nidx]) of
{result, Affs} -> [LJID || {LJID, Aff} <- Affs, Aff =:= owner];
_ -> []
end;
node_owners_call(_Host, _Type, _Nidx, Owners) ->
Owners.
%% @spec (Host, Options) -> MaxItems
%% Host = host()
%% Options = [Option]
%% Option = {Key::atom(), Value::term()}
%% MaxItems = integer() | unlimited
%% @doc <p>Return the maximum number of items for a given node.</p>
%% <p>Unlimited means that there is no limit in the number of items that can
%% be stored.</p>
-spec max_items(host(), [{atom(), any()}]) -> non_neg_integer().
max_items(Host, Options) ->
case get_option(Options, persist_items) of
true ->
case get_option(Options, max_items) of
I when is_integer(I), I < 0 -> 0;
I when is_integer(I) -> I;
_ -> ?MAXITEMS
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.
-spec get_configure_xfields(_, pubsub_node_config:result(),
binary(), [binary()]) -> [xdata_field()].
get_configure_xfields(_Type, Options, Lang, Groups) ->
pubsub_node_config:encode(
lists:map(
fun({roster_groups_allowed, Value}) ->
{roster_groups_allowed, Value, Groups};
(Opt) ->
Opt
end, Options),
Lang).
%%<p>There are several reasons why the node configuration request might fail:</p>
%%<ul>
%%<li>The service does not support node configuration.</li>
%%<li>The requesting entity does not have sufficient privileges to configure the node.</li>
%%<li>The request did not specify a node.</li>
%%<li>The node has no configuration options.</li>
%%<li>The specified node does not exist.</li>
%%</ul>
-spec set_configure(host(), binary(), jid(), [{binary(), [binary()]}],
binary()) -> {result, undefined} | {error, stanza_error()}.
set_configure(_Host, <<>>, _From, _Config, _Lang) ->
{error, extended_error(xmpp:err_bad_request(), err_nodeid_required())};
set_configure(Host, Node, From, Config, Lang) ->
Action =
fun(#pubsub_node{options = Options, type = Type, id = Nidx} = N) ->
case node_call(Host, Type, get_affiliation, [Nidx, From]) of
{result, owner} ->
OldOpts = case Options of
[] -> node_options(Host, Type);
_ -> Options
end,
NewOpts = merge_config(Config, OldOpts),
case tree_call(Host,
set_node,
[N#pubsub_node{options = NewOpts}]) of
{result, Nidx} -> {result, NewOpts};
ok -> {result, NewOpts};
Err -> Err
end;
_ ->
{error, xmpp:err_forbidden(
<<"Owner privileges required">>, Lang)}
end
end,
case transaction(Host, Node, Action, transaction) of
{result, {TNode, Options}} ->
Nidx = TNode#pubsub_node.id,
Type = TNode#pubsub_node.type,
broadcast_config_notification(Host, Node, Nidx, Type, Options, Lang),
{result, undefined};
Other ->
Other
end.
-spec merge_config([proplists:property()], [proplists:property()]) -> [proplists:property()].
merge_config(CustomConfig, DefaultConfig) ->
lists:foldl(
fun({Opt, Val}, Acc) ->
lists:keystore(Opt, 1, Acc, {Opt, Val})
end, DefaultConfig, CustomConfig).
-spec decode_node_config(undefined | xdata(), binary(), binary()) ->
pubsub_node_config:result() |
{error, stanza_error()}.
decode_node_config(undefined, _, _) ->
[];
decode_node_config(#xdata{fields = Fs}, Host, Lang) ->
try
Config = pubsub_node_config:decode(Fs),
Max = get_max_items_node(Host),
case {check_opt_range(max_items, Config, Max),
check_opt_range(max_payload_size, Config, ?MAX_PAYLOAD_SIZE)} of
{true, true} ->
Config;
{true, false} ->
erlang:error(
{pubsub_node_config,
{bad_var_value, <<"pubsub#max_payload_size">>,
?NS_PUBSUB_NODE_CONFIG}});
{false, _} ->
erlang:error(
{pubsub_node_config,
{bad_var_value, <<"pubsub#max_items">>,
?NS_PUBSUB_NODE_CONFIG}})
end
catch _:{pubsub_node_config, Why} ->
Txt = pubsub_node_config:format_error(Why),
{error, xmpp:err_resource_constraint(Txt, Lang)}
end.
-spec decode_subscribe_options(undefined | xdata(), binary()) ->
pubsub_subscribe_options:result() |
{error, stanza_error()}.
decode_subscribe_options(undefined, _) ->
[];
decode_subscribe_options(#xdata{fields = Fs}, Lang) ->
try pubsub_subscribe_options:decode(Fs)
catch _:{pubsub_subscribe_options, Why} ->
Txt = pubsub_subscribe_options:format_error(Why),
{error, xmpp:err_resource_constraint(Txt, Lang)}
end.
-spec decode_publish_options(undefined | xdata(), binary()) ->
pubsub_publish_options:result() |
{error, stanza_error()}.
decode_publish_options(undefined, _) ->
[];
decode_publish_options(#xdata{fields = Fs}, Lang) ->
try pubsub_publish_options:decode(Fs)
catch _:{pubsub_publish_options, Why} ->
Txt = pubsub_publish_options:format_error(Why),
{error, xmpp:err_resource_constraint(Txt, Lang)}
end.
-spec decode_get_pending(xdata(), binary()) ->
pubsub_get_pending:result() |
{error, stanza_error()}.
decode_get_pending(#xdata{fields = Fs}, Lang) ->
try pubsub_get_pending:decode(Fs)
catch _:{pubsub_get_pending, Why} ->
Txt = pubsub_get_pending:format_error(Why),
{error, xmpp:err_resource_constraint(Txt, Lang)}
end;
decode_get_pending(undefined, Lang) ->
{error, xmpp:err_bad_request(<<"No data form found">>, Lang)}.
-spec check_opt_range(atom(), [proplists:property()], non_neg_integer()) -> boolean().
check_opt_range(_Opt, _Opts, undefined) ->
true;
check_opt_range(Opt, Opts, Max) ->
Val = proplists:get_value(Opt, Opts, Max),
Val =< Max.
-spec get_max_items_node(host()) -> undefined | non_neg_integer().
get_max_items_node(Host) ->
config(Host, max_items_node, undefined).
-spec get_max_subscriptions_node(host()) -> undefined | non_neg_integer().
get_max_subscriptions_node(Host) ->
config(Host, max_subscriptions_node, undefined).
%%%% last item cache handling
-spec is_last_item_cache_enabled(host()) -> boolean().
is_last_item_cache_enabled(Host) ->
config(Host, last_item_cache, false).
-spec set_cached_item(host(), nodeIdx(), binary(), binary(), [xmlel()]) -> ok.
set_cached_item({_, ServerHost, _}, Nidx, ItemId, Publisher, Payload) ->
set_cached_item(ServerHost, Nidx, ItemId, Publisher, Payload);
set_cached_item(Host, Nidx, ItemId, Publisher, Payload) ->
case is_last_item_cache_enabled(Host) of
true ->
Stamp = {p1_time_compat:timestamp(), jid:tolower(jid:remove_resource(Publisher))},
Item = #pubsub_last_item{nodeid = {Host, Nidx},
itemid = ItemId,
creation = Stamp,
payload = Payload},
mnesia:dirty_write(Item);
_ ->
ok
end.
-spec unset_cached_item(host(), nodeIdx()) -> ok.
unset_cached_item({_, ServerHost, _}, Nidx) ->
unset_cached_item(ServerHost, Nidx);
unset_cached_item(Host, Nidx) ->
case is_last_item_cache_enabled(Host) of
true -> mnesia:dirty_delete({pubsub_last_item, {Host, Nidx}});
_ -> ok
end.
-spec get_cached_item(host(), nodeIdx()) -> undefined | pubsubItem().
get_cached_item({_, ServerHost, _}, Nidx) ->
get_cached_item(ServerHost, Nidx);
get_cached_item(Host, Nidx) ->
case is_last_item_cache_enabled(Host) of
true ->
case mnesia:dirty_read({pubsub_last_item, {Host, Nidx}}) of
[#pubsub_last_item{itemid = ItemId, creation = Creation, payload = Payload}] ->
#pubsub_item{itemid = {ItemId, Nidx},
payload = Payload, creation = Creation,
modification = Creation};
_ ->
undefined
end;
_ ->
undefined
end.
%%%% plugin handling
-spec host(binary()) -> binary().
host(ServerHost) ->
config(ServerHost, host, <<"pubsub.", ServerHost/binary>>).
-spec serverhost(host()) -> binary().
serverhost({_U, ServerHost, _R})->
serverhost(ServerHost);
serverhost(Host) ->
ejabberd_router:host_of_route(Host).
-spec tree(host()) -> atom().
tree(Host) ->
case config(Host, nodetree) of
undefined -> tree(Host, ?STDTREE);
Tree -> Tree
end.
-spec tree(host(), binary()) -> atom().
tree(_Host, <<"virtual">>) ->
nodetree_virtual; % special case, virtual does not use any backend
tree(Host, Name) ->
submodule(Host, <<"nodetree">>, Name).
-spec plugin(host(), binary()) -> atom().
plugin(Host, Name) ->
submodule(Host, <<"node">>, Name).
-spec plugins(host()) -> [binary()].
plugins(Host) ->
case config(Host, plugins) of
undefined -> [?STDNODE];
[] -> [?STDNODE];
Plugins -> Plugins
end.
-spec subscription_plugin(host()) -> atom().
subscription_plugin(Host) ->
submodule(Host, <<"pubsub">>, <<"subscription">>).
-spec submodule(host(), binary(), binary()) -> atom().
submodule(Host, Type, Name) ->
case gen_mod:get_module_opt(serverhost(Host), ?MODULE, db_type) of
mnesia -> ejabberd:module_name([<<"pubsub">>, Type, Name]);
Db -> ejabberd:module_name([<<"pubsub">>, Type, Name, misc:atom_to_binary(Db)])
end.
-spec config(binary(), any()) -> any().
config(ServerHost, Key) ->
config(ServerHost, Key, undefined).
-spec config(host(), any(), any()) -> any().
config({_User, Host, _Resource}, Key, Default) ->
config(Host, Key, Default);
config(ServerHost, Key, Default) ->
case catch ets:lookup(gen_mod:get_module_proc(ServerHost, config), Key) of
[{Key, Value}] -> Value;
_ -> Default
end.
-spec select_type(binary(), host(), binary(), binary()) -> binary().
select_type(ServerHost, {_User, _Server, _Resource}, Node, _Type) ->
case config(ServerHost, pep_mapping) of
undefined -> ?PEPNODE;
Mapping -> proplists:get_value(Node, Mapping, ?PEPNODE)
end;
select_type(ServerHost, _Host, _Node, Type) ->
case config(ServerHost, plugins) of
undefined ->
Type;
Plugins ->
case lists:member(Type, Plugins) of
true -> Type;
false -> hd(Plugins)
end
end.
-spec select_type(binary(), host(), binary()) -> binary().
select_type(ServerHost, Host, Node) ->
select_type(ServerHost, Host, Node, hd(plugins(Host))).
-spec feature(binary()) -> binary().
feature(<<"rsm">>) -> ?NS_RSM;
feature(Feature) -> <<(?NS_PUBSUB)/binary, "#", Feature/binary>>.
-spec features() -> [binary()].
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
<<"access-whitelist">>, % OPTIONAL
<<"collections">>, % RECOMMENDED
<<"config-node">>, % RECOMMENDED
<<"create-and-configure">>, % RECOMMENDED
<<"item-ids">>, % RECOMMENDED
<<"last-published">>, % RECOMMENDED
<<"member-affiliation">>, % RECOMMENDED
<<"presence-notifications">>, % OPTIONAL
<<"presence-subscribe">>, % RECOMMENDED
<<"publisher-affiliation">>, % RECOMMENDED
<<"publish-only-affiliation">>, % OPTIONAL
<<"publish-options">>, % OPTIONAL
<<"retrieve-default">>,
<<"shim">>]. % RECOMMENDED
% see plugin "retrieve-items", % RECOMMENDED
% see plugin "retrieve-subscriptions", % RECOMMENDED
% see plugin "subscribe", % REQUIRED
% see plugin "subscription-options", % OPTIONAL
% see plugin "subscription-notifications" % OPTIONAL
-spec plugin_features(binary(), binary()) -> [binary()].
plugin_features(Host, Type) ->
Module = plugin(Host, Type),
case catch Module:features() of
{'EXIT', {undef, _}} -> [];
Result -> Result
end.
-spec features(binary(), binary()) -> [binary()].
features(Host, <<>>) ->
lists:usort(lists:foldl(fun (Plugin, Acc) ->
Acc ++ plugin_features(Host, Plugin)
end,
features(), plugins(Host)));
features(Host, Node) when is_binary(Node) ->
Action = fun (#pubsub_node{type = Type}) ->
{result, plugin_features(Host, Type)}
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, Features} -> lists:usort(features() ++ Features);
_ -> features()
end.
%% @doc <p>node tree plugin call.</p>
tree_call({_User, Server, _Resource}, Function, Args) ->
tree_call(Server, Function, Args);
tree_call(Host, Function, Args) ->
Tree = tree(Host),
?DEBUG("tree_call apply(~s, ~s, ~p) @ ~s", [Tree, Function, Args, Host]),
catch apply(Tree, Function, Args).
tree_action(Host, Function, Args) ->
?DEBUG("tree_action ~p ~p ~p", [Host, Function, Args]),
ServerHost = serverhost(Host),
Fun = fun () -> tree_call(Host, Function, Args) end,
case gen_mod:get_module_opt(ServerHost, ?MODULE, db_type) of
mnesia ->
catch mnesia:sync_dirty(Fun);
sql ->
case catch ejabberd_sql:sql_bloc(ServerHost, Fun) of
{atomic, Result} ->
Result;
{aborted, Reason} ->
?ERROR_MSG("transaction return internal error: ~p~n", [{aborted, Reason}]),
ErrTxt = <<"Database failure">>,
{error, xmpp:err_internal_server_error(ErrTxt, ?MYLANG)}
end;
Other ->
case catch Fun() of
{'EXIT', _} ->
?ERROR_MSG("unsupported backend: ~p~n", [Other]),
ErrTxt = <<"Database failure">>,
{error, xmpp:err_internal_server_error(ErrTxt, ?MYLANG)};
Result ->
Result
end
end.
%% @doc <p>node plugin call.</p>
node_call(Host, Type, Function, Args) ->
?DEBUG("node_call ~p ~p ~p", [Type, Function, Args]),
Module = plugin(Host, Type),
case 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(Host, ?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(Host, Type, Function, Args)
end,
sync_dirty).
%% @doc <p>plugin transaction handling.</p>
transaction(Host, Node, Action, Trans) ->
transaction(Host, fun () ->
case tree_call(Host, get_node, [Host, Node]) of
N when is_record(N, pubsub_node) ->
case Action(N) of
{result, Result} -> {result, {N, Result}};
{atomic, {result, Result}} -> {result, {N, Result}};
Other -> Other
end;
Error ->
Error
end
end,
Trans).
transaction(Host, Fun, Trans) ->
ServerHost = serverhost(Host),
DBType = gen_mod:get_module_opt(ServerHost, ?MODULE, db_type),
Retry = case DBType of
sql -> 2;
_ -> 1
end,
transaction_retry(Host, ServerHost, Fun, Trans, DBType, Retry).
transaction_retry(_Host, _ServerHost, _Fun, _Trans, _DBType, 0) ->
{error, xmpp:err_internal_server_error(<<"Database failure">>, ?MYLANG)};
transaction_retry(Host, ServerHost, Fun, Trans, DBType, Count) ->
Res = case DBType of
mnesia ->
catch mnesia:Trans(Fun);
sql ->
SqlFun = case Trans of
transaction -> sql_transaction;
_ -> sql_bloc
end,
catch ejabberd_sql:SqlFun(ServerHost, Fun);
_ ->
catch Fun()
end,
case Res 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, xmpp:err_internal_server_error(<<"Database failure">>, ?MYLANG)};
{'EXIT', {timeout, _} = Reason} ->
?ERROR_MSG("transaction return internal error: ~p~n", [Reason]),
transaction_retry(Host, ServerHost, Fun, Trans, DBType, Count - 1);
{'EXIT', Reason} ->
?ERROR_MSG("transaction return internal error: ~p~n", [{'EXIT', Reason}]),
{error, xmpp:err_internal_server_error(<<"Database failure">>, ?MYLANG)};
Other ->
?ERROR_MSG("transaction return internal error: ~p~n", [Other]),
{error, xmpp:err_internal_server_error(<<"Database failure">>, ?MYLANG)}
end.
%%%% helpers
%% Add pubsub-specific error element
-spec extended_error(stanza_error(), ps_error()) -> stanza_error().
extended_error(StanzaErr, PubSubErr) ->
StanzaErr#stanza_error{sub_els = [PubSubErr]}.
-spec err_closed_node() -> ps_error().
err_closed_node() ->
#ps_error{type = 'closed-node'}.
-spec err_configuration_required() -> ps_error().
err_configuration_required() ->
#ps_error{type = 'configuration-required'}.
-spec err_invalid_jid() -> ps_error().
err_invalid_jid() ->
#ps_error{type = 'invalid-jid'}.
-spec err_invalid_options() -> ps_error().
err_invalid_options() ->
#ps_error{type = 'invalid-options'}.
-spec err_invalid_payload() -> ps_error().
err_invalid_payload() ->
#ps_error{type = 'invalid-payload'}.
-spec err_invalid_subid() -> ps_error().
err_invalid_subid() ->
#ps_error{type = 'invalid-subid'}.
-spec err_item_forbidden() -> ps_error().
err_item_forbidden() ->
#ps_error{type = 'item-forbidden'}.
-spec err_item_required() -> ps_error().
err_item_required() ->
#ps_error{type = 'item-required'}.
-spec err_jid_required() -> ps_error().
err_jid_required() ->
#ps_error{type = 'jid-required'}.
-spec err_max_items_exceeded() -> ps_error().
err_max_items_exceeded() ->
#ps_error{type = 'max-items-exceeded'}.
-spec err_max_nodes_exceeded() -> ps_error().
err_max_nodes_exceeded() ->
#ps_error{type = 'max-nodes-exceeded'}.
-spec err_nodeid_required() -> ps_error().
err_nodeid_required() ->
#ps_error{type = 'nodeid-required'}.
-spec err_not_in_roster_group() -> ps_error().
err_not_in_roster_group() ->
#ps_error{type = 'not-in-roster-group'}.
-spec err_not_subscribed() -> ps_error().
err_not_subscribed() ->
#ps_error{type = 'not-subscribed'}.
-spec err_payload_too_big() -> ps_error().
err_payload_too_big() ->
#ps_error{type = 'payload-too-big'}.
-spec err_payload_required() -> ps_error().
err_payload_required() ->
#ps_error{type = 'payload-required'}.
-spec err_pending_subscription() -> ps_error().
err_pending_subscription() ->
#ps_error{type = 'pending-subscription'}.
-spec err_precondition_not_met() -> ps_error().
err_precondition_not_met() ->
#ps_error{type = 'precondition-not-met'}.
-spec err_presence_subscription_required() -> ps_error().
err_presence_subscription_required() ->
#ps_error{type = 'presence-subscription-required'}.
-spec err_subid_required() -> ps_error().
err_subid_required() ->
#ps_error{type = 'subid-required'}.
-spec err_too_many_subscriptions() -> ps_error().
err_too_many_subscriptions() ->
#ps_error{type = 'too-many-subscriptions'}.
-spec err_unsupported(ps_feature()) -> ps_error().
err_unsupported(Feature) ->
#ps_error{type = 'unsupported', feature = Feature}.
-spec err_unsupported_access_model() -> ps_error().
err_unsupported_access_model() ->
#ps_error{type = 'unsupported-access-model'}.
-spec uniqid() -> mod_pubsub:itemId().
uniqid() ->
{T1, T2, T3} = p1_time_compat:timestamp(),
(str:format("~.16B~.16B~.16B", [T1, T2, T3])).
-spec itemsEls([#pubsub_item{}]) -> [ps_item()].
itemsEls(Items) ->
[#ps_item{id = ItemId, sub_els = Payload}
|| #pubsub_item{itemid = {ItemId, _}, payload = Payload} <- Items].
-spec add_message_type(message(), message_type()) -> message().
add_message_type(#message{} = Message, Type) ->
Message#message{type = Type}.
%% Place of <headers/> changed at the bottom of the stanza
%% cf. http://xmpp.org/extensions/xep-0060.html#publisher-publish-success-subid
%%
%% "[SHIM Headers] SHOULD be included after the event notification information
%% (i.e., as the last child of the <message/> stanza)".
-spec add_shim_headers(stanza(), [{binary(), binary()}]) -> stanza().
add_shim_headers(Stanza, Headers) ->
xmpp:set_subtag(Stanza, #shim{headers = Headers}).
-spec add_extended_headers(stanza(), [address()]) -> stanza().
add_extended_headers(Stanza, Addrs) ->
xmpp:set_subtag(Stanza, #addresses{list = Addrs}).
-spec subid_shim([binary()]) -> [{binary(), binary()}].
subid_shim(SubIds) ->
[{<<"SubId">>, SubId} || SubId <- SubIds].
%% The argument is a list of Jids because this function could be used
%% with the 'pubsub#replyto' (type=jid-multi) node configuration.
-spec extended_headers([jid()]) -> [address()].
extended_headers(Jids) ->
[#address{type = replyto, jid = Jid} || Jid <- Jids].
-spec purge_offline(ljid()) -> ok.
purge_offline(LJID) ->
Host = host(element(2, LJID)),
Plugins = plugins(Host),
Result = lists:foldl(fun (Type, {Status, Acc}) ->
Features = plugin_features(Host, Type),
case lists:member(<<"retrieve-affiliations">>, plugin_features(Host, Type)) of
false ->
{{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('retrieve-affiliations'))},
Acc};
true ->
Items = lists:member(<<"retract-items">>, Features)
andalso lists:member(<<"persistent-items">>, Features),
if Items ->
{result, Affs} = node_action(Host, Type,
get_entity_affiliations, [Host, LJID]),
{Status, [Affs | Acc]};
true ->
{Status, Acc}
end
end
end,
{ok, []}, Plugins),
case Result of
{ok, Affs} ->
lists:foreach(
fun ({Node, Affiliation}) ->
Options = Node#pubsub_node.options,
Publisher = lists:member(Affiliation, [owner,publisher,publish_only]),
Open = (get_option(Options, publish_model) == open),
Purge = (get_option(Options, purge_offline)
andalso get_option(Options, persist_items)),
if (Publisher or Open) and Purge ->
purge_offline(Host, LJID, Node);
true ->
ok
end
end, lists:usort(lists:flatten(Affs)));
{Error, _} ->
?ERROR_MSG("can not purge offline: ~p", [Error])
end.
-spec purge_offline(host(), ljid(), binary()) -> ok | {error, stanza_error()}.
purge_offline(Host, LJID, Node) ->
Nidx = Node#pubsub_node.id,
Type = Node#pubsub_node.type,
Options = Node#pubsub_node.options,
case node_action(Host, Type, get_items, [Nidx, service_jid(Host), undefined]) of
{result, {[], _}} ->
ok;
{result, {Items, _}} ->
{User, Server, Resource} = LJID,
PublishModel = get_option(Options, publish_model),
ForceNotify = get_option(Options, notify_retract),
{_, NodeId} = Node#pubsub_node.nodeid,
lists:foreach(fun
(#pubsub_item{itemid = {ItemId, _}, modification = {_, {U, S, R}}})
when (U == User) and (S == Server) and (R == Resource) ->
case node_action(Host, Type, delete_item, [Nidx, {U, S, <<>>}, PublishModel, ItemId]) of
{result, {_, broadcast}} ->
broadcast_retract_items(Host, NodeId, Nidx, Type, Options, [ItemId], ForceNotify),
case get_cached_item(Host, Nidx) of
#pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx);
_ -> ok
end;
{result, _} ->
ok;
Error ->
Error
end;
(_) ->
true
end, Items);
Error ->
Error
end.
mod_opt_type(access_createnode) -> fun acl:access_rules_validator/1;
mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
mod_opt_type(name) -> fun iolist_to_binary/1;
mod_opt_type(host) -> fun iolist_to_binary/1;
mod_opt_type(hosts) ->
fun (L) -> lists:map(fun iolist_to_binary/1, L) end;
mod_opt_type(ignore_pep_from_offline) ->
fun (A) when is_boolean(A) -> A end;
mod_opt_type(last_item_cache) ->
fun (A) when is_boolean(A) -> A end;
mod_opt_type(max_items_node) ->
fun (A) when is_integer(A) andalso A >= 0 -> A end;
mod_opt_type(max_subscriptions_node) ->
fun(A) when is_integer(A) andalso A >= 0 -> A;
(undefined) -> undefined
end;
mod_opt_type(default_node_config) ->
fun (A) when is_list(A) -> A end;
mod_opt_type(nodetree) ->
fun (A) when is_binary(A) -> A end;
mod_opt_type(pep_mapping) ->
fun (A) when is_list(A) -> A end;
mod_opt_type(plugins) ->
fun (A) when is_list(A) -> A end.
mod_options(Host) ->
[{access_createnode, all},
{db_type, ejabberd_config:default_db(Host, ?MODULE)},
{host, <<"pubsub.@HOST@">>},
{hosts, []},
{name, ?T("Publish-Subscribe")},
{ignore_pep_from_offline, true},
{last_item_cache, false},
{max_items_node, ?MAXITEMS},
{nodetree, ?STDTREE},
{pep_mapping, []},
{plugins, [?STDNODE]},
{max_subscriptions_node, undefined},
{default_node_config, []}].