24
1
mirror of https://github.com/processone/ejabberd.git synced 2024-06-02 21:17:12 +02:00
xmpp.chapril.org-ejabberd/src/mod_pubsub.erl
Holger Weiss 8d5025076f PubSub: Add delete_old_pubsub_items command
Add a command for keeping only the specified number of items on each
node and removing all older items.  This might be especially useful if
nodes may be configured to have no 'max_items' limit.

Thanks to Ammonit Measurement GmbH for sponsoring this work.
2021-08-22 12:44:50 +02:00

4462 lines
157 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-2021 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("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("pubsub.hrl").
-include("mod_roster.hrl").
-include("translate.hrl").
-include("ejabberd_stacktrace.hrl").
-include("ejabberd_commands.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, publish_item/8,
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, mod_doc/0,
terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1]).
%% ejabberd commands
-export([get_commands_spec/0, delete_old_items/1]).
-export([route/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()|unlimited,
max_subscriptions_node :: non_neg_integer()|undefined,
default_node_config :: [{atom(), binary()|boolean()|integer()|atom()}],
nodetree :: binary(),
plugins :: [binary(),...],
db_type :: atom()
}
).
-type subs_by_depth() :: [{integer(), [{#pubsub_node{}, [{ljid(), subId(), subOptions()}]}]}].
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|_]) ->
process_flag(trap_exit, true),
Opts = gen_mod:get_module_opts(ServerHost, ?MODULE),
Hosts = gen_mod:get_opt_hosts(Opts),
Access = mod_pubsub_opt:access_createnode(Opts),
PepOffline = mod_pubsub_opt:ignore_pep_from_offline(Opts),
LastItemCache = mod_pubsub_opt:last_item_cache(Opts),
MaxItemsNode = mod_pubsub_opt:max_items_node(Opts),
MaxSubsNode = mod_pubsub_opt:max_subscriptions_node(Opts),
ejabberd_mnesia:create(?MODULE, pubsub_last_item,
[{ram_copies, [node()]},
{attributes, record_info(fields, pubsub_last_item)}]),
DBMod = gen_mod:db_mod(Opts, ?MODULE),
AllPlugins =
lists:flatmap(
fun(Host) ->
DBMod:init(Host, ServerHost, Opts),
ejabberd_router:register_route(
Host, ServerHost, {apply, ?MODULE, route}),
{Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts),
DefaultModule = plugin(Host, hd(Plugins)),
DefaultNodeCfg = merge_config(
[mod_pubsub_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,
ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
NodeTree = config(ServerHost, nodetree),
Plugins = config(ServerHost, plugins),
PepMapping = config(ServerHost, pep_mapping),
DBType = mod_pubsub_opt:db_type(ServerHost),
{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, Opts) ->
[Host|_] = gen_mod:get_opt_hosts(Opts),
Plugins = mod_pubsub_opt:plugins(Opts),
Db = mod_pubsub_opt:db_type(Opts),
lists:flatmap(
fun(Name) ->
Plugin = plugin(Db, 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, mod_pubsub_opt:nodetree(Opts)),
TreePlugin:init(Host, ServerHost, Opts),
Plugins = mod_pubsub_opt:plugins(Opts),
PepMapping = mod_pubsub_opt:pep_mapping(Opts),
PluginsOK = lists:foldl(
fun (Name, Acc) ->
Plugin = plugin(Host, Name),
apply(Plugin, init, [Host, ServerHost, Opts]),
[Name | Acc]
end,
[], Plugins),
{lists:reverse(PluginsOK), TreePlugin, PepMapping}.
terminate_plugins(Host, ServerHost, Plugins, TreePlugin) ->
lists:foreach(
fun (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(host(), 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) ->
MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)),
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() ->
case tree_call(Host, get_nodes, [Host, MaxNodes]) of
Nodes when is_list(Nodes) ->
{result, lists:foldl(Action, [], Nodes)};
Error ->
Error
end
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, Features);
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, Features);
caps_add(_From, _To, _Features) ->
ok.
-spec caps_update(jid(), jid(), [binary()]) -> ok.
caps_update(From, To, Features) ->
send_last_pep(To, From, Features).
-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 resources
ok;
presence_probe(#jid{lserver = S} = From, #jid{lserver = S} = To, _Pid) ->
send_last_pep(To, From, unknown);
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}) ->
if From#jid.lserver == To#jid.lserver ->
send_last_pep(jid:remove_resource(From), To, unknown);
true ->
ok
end;
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.
-spec unsubscribe_user(jid(), jid()) -> ok.
unsubscribe_user(Entity, Owner) ->
lists:foreach(
fun(ServerHost) ->
unsubscribe_user(ServerHost, Entity, Owner)
end,
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]))).
-spec unsubscribe_user(binary(), jid(), jid()) -> ok.
unsubscribe_user(Host, Entity, Owner) ->
BJID = jid:tolower(jid:remove_resource(Owner)),
lists:foreach(
fun (PType) ->
case node_action(Host, PType,
get_entity_subscriptions,
[Host, Entity]) of
{result, Subs} ->
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);
_ ->
ok
end
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>>,
lists:foreach(
fun(PType) ->
case node_action(Host, PType,
get_entity_subscriptions,
[Host, Entity]) of
{result, Subs} ->
lists:foreach(
fun({#pubsub_node{id = Nidx}, _, _, JID}) ->
node_action(Host, PType,
unsubscribe_node,
[Nidx, Entity, JID, all]);
(_) ->
ok
end, Subs),
case node_action(Host, PType,
get_entity_affiliations,
[Host, Entity]) of
{result, Affs} ->
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}, _}) ->
case node_action(Host, PType,
get_state,
[Nidx, jid:tolower(Entity)]) of
{result, #pubsub_state{items = ItemIds}} ->
node_action(Host, PType,
remove_extra_items,
[Nidx, 0, ItemIds]),
node_action(Host, PType,
set_affiliation,
[Nidx, Entity, none]);
_ ->
ok
end
end, Affs);
_ ->
ok
end;
_ ->
ok
end
end, plugins(Host)).
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};
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({route, Packet}, State) ->
try route(Packet)
catch ?EX_RULE(Class, Reason, St) ->
StackTrace = ?EX_STACK(St),
?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts",
[xmpp:pp(Packet),
misc:format_exception(2, Class, Reason, StackTrace)])
end,
{noreply, State};
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
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),
case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of
false ->
ejabberd_commands:unregister_commands(get_commands_spec());
true ->
ok
end.
%%--------------------------------------------------------------------
%% 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 = ?T("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 = ?T("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, to = To, lang = Lang} = IQ) ->
ServerHost = ejabberd_router:host_of_route(To#jid.lserver),
xmpp:make_iq_result(IQ, iq_get_vcard(ServerHost, Lang));
process_vcard(#iq{type = set, lang = Lang} = IQ) ->
Txt = ?T("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 = ?T("Value 'get' of 'type' attribute is not allowed"),
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)).
-spec route(stanza()) -> ok.
route(#iq{to = To} = IQ) when To#jid.lresource == <<"">> ->
ejabberd_router:process_iq(IQ);
route(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(
To#jid.lserver, 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]},
{access_model, get_option(Options, access_model, open)},
{publish_model, get_option(Options, publish_model, publishers)},
{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 = mod_pubsub_opt:name(ServerHost),
{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) ->
MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)),
case tree_action(Host, get_subnodes, [Host, <<>>, MaxNodes]) of
{error, #stanza_error{}} = Err ->
Err;
Nodes when is_list(Nodes) ->
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, Nodes),
{result, #disco_items{items = Items}}
end;
iq_disco_items(Host, ?NS_COMMANDS, _From, _RSM) ->
{result,
#disco_items{items = [#disco_item{jid = jid:make(Host),
node = ?NS_PUBSUB_GET_PENDING,
name = ?T("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] ->
MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)),
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,
case tree_call(Host, get_subnodes, [Host, Node, MaxNodes]) of
SubNodes when is_list(SubNodes) ->
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, SubNodes),
Items = lists:flatmap(
fun(#pubsub_item{itemid = {RN, _}}) ->
case node_call(Host, Type, get_item_name, [Host, Node, RN]) of
{result, Name} ->
[#disco_item{jid = jid:make(Host), name = Name}];
_ ->
[]
end
end, NodeItems),
{result, #disco_items{items = Nodes ++ Items,
rsm = RsmOut}};
Error ->
Error
end
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(), binary()) -> vcard_temp().
iq_get_vcard(ServerHost, Lang) ->
case mod_pubsub_opt:vcard(ServerHost) of
undefined ->
Desc = misc:get_descr(Lang, ?T("ejabberd Publish-Subscribe module")),
#vcard_temp{fn = <<"ejabberd/mod_pubsub">>,
url = ejabberd_config:get_uri(),
desc = Desc};
VCard ->
VCard
end.
-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;
[] ->
publish_item(Host, ServerHost, Node, From, <<>>, [], [], Access);
_ ->
{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, jid = undefined, node = <<>>} ->
decode_subscribe_options(XData, Lang);
#ps_options{xdata = _XData, jid = #jid{}} ->
Txt = ?T("Attribute 'jid' is not allowed here"),
{error, xmpp:err_bad_request(Txt, Lang)};
#ps_options{xdata = _XData} ->
Txt = ?T("Attribute 'node' is not allowed here"),
{error, xmpp:err_bad_request(Txt, 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);
{_, #pubsub{options = #ps_options{jid = undefined}, _ = undefined}} ->
{error, extended_error(xmpp:err_bad_request(), err_jid_required())};
{_, #pubsub{options = #ps_options{node = <<>>}, _ = undefined}} ->
{error, extended_error(xmpp:err_bad_request(), err_nodeid_required())};
{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(?T("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(?T("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{status = 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} ->
Form = [{node, <<>>, lists:zip(Nodes, Nodes)}],
XForm = #xdata{type = form,
fields = pubsub_get_pending:encode(Form, 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()) -> ok | {error, stanza_error()}.
send_pending_auth_events(Host, Node, Owner, Lang) ->
?DEBUG("Sending pending auth events for ~ts on ~ts:~ts",
[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(
?T("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);
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, ?T("PubSub subscriber request")),
instructions = [translate:translate(
Lang,
?T("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 ->
case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of
{result, Subs} ->
update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs);
{error, _} = Err ->
Err
end;
false ->
{error, xmpp:err_forbidden(?T("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
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 = ?T("No pending subscriptions found"),
{error, xmpp:err_unexpected_request(Txt, ejabberd_option:language())}
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 = p1_rand: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(
[node_config(Node, ServerHost),
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} ->
case get_node_subs_by_depth(Host, Node, Owner) of
{result, SubsByDepth} ->
case node_call(Host, Type, create_node, [Nidx, Owner]) of
{result, Result} -> {result, {Nidx, SubsByDepth, Result}};
Error -> Error
end;
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;
{result, _} ->
Txt = ?T("You're not allowed to create nodes"),
{error, xmpp:err_forbidden(Txt, ejabberd_option:language())};
Err ->
Err
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 children.</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(?T("No node specified"), ejabberd_option:language())};
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} ->
case get_node_subs_by_depth(Host, Node, service_jid(Host)) of
{result, SubsByDepth} ->
case tree_call(Host, delete_node, [Host, Node]) of
Removed when is_list(Removed) ->
case node_call(Host, Type, delete_node, [Removed]) of
{result, Res} -> {result, {SubsByDepth, Res}};
Error -> Error
end;
Error ->
Error
end;
Error ->
Error
end;
{result, _} ->
Lang = ejabberd_option:language(),
{error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)};
Error ->
Error
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, JID,
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, last),
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, JID),
{result, Reply(pending)};
{result, {TNode, {Result, pending}}} ->
send_authorization_request(TNode, JID),
{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())};
(DeliverPayloads or PersistItems) and (PayloadCount == 0) ->
{error, extended_error(xmpp:err_bad_request(),
err_item_required())};
(DeliverPayloads or PersistItems) and (PayloadCount > 1) ->
{error, extended_error(xmpp:err_bad_request(),
err_invalid_payload())};
(not (DeliverPayloads or PersistItems)) and (PayloadCount > 0) ->
{error, extended_error(xmpp:err_bad_request(),
err_item_forbidden())};
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 = ?T("Automatic node creation is not enabled"),
{error, xmpp:err_item_not_found(Txt, ejabberd_option:language())}
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 matches
%% %% 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, transaction) 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(),
undefined | non_neg_integer(), [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] ->
NotFound = xmpp:err_item_not_found(),
case node_call(Host, Type, get_item,
[Nidx, ItemId, From, AccessModel, PS, RG, undefined])
of
{error, NotFound} -> {result, {[], undefined}};
Result -> Result
end;
_ ->
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, {TNode, {Items, RsmOut}}} ->
SendItems = case ItemIds of
[] ->
Items;
_ ->
lists:filter(
fun(#pubsub_item{itemid = {ItemId, _}}) ->
lists:member(ItemId, ItemIds)
end, Items)
end,
Options = TNode#pubsub_node.options,
{result, #pubsub{items = items_els(Node, Options, SendItems),
rsm = RsmOut}};
{result, {TNode, Item}} ->
Options = TNode#pubsub_node.options,
{result, #pubsub{items = items_els(Node, Options, [Item])}};
Error ->
Error
end.
%% Seems like this function broken
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.
%% This function is broken too?
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.
-spec get_allowed_items_call(host(), nodeIdx(), jid(),
binary(), nodeOptions(), [ljid()]) -> {result, [#pubsub_item{}]} |
{error, stanza_error()}.
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.
-spec get_allowed_items_call(host(), nodeIdx(), jid(),
binary(), nodeOptions(), [ljid()],
undefined | rsm_set()) ->
{result, {[#pubsub_item{}], undefined | rsm_set()}} |
{error, stanza_error()}.
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]).
-spec get_last_items(host(), binary(), nodeIdx(), ljid(), last | integer()) -> [#pubsub_item{}].
get_last_items(Host, Type, Nidx, LJID, last) ->
% hack to handle section 6.1.7 of XEP-0060
get_last_items(Host, Type, Nidx, LJID, 1);
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) ->
[].
-spec get_only_item(host(), binary(), nodeIdx(), ljid()) -> [#pubsub_item{}].
get_only_item(Host, Type, Nidx, LJID) ->
case get_cached_item(Host, Nidx) of
undefined ->
case node_action(Host, Type, get_only_item, [Nidx, LJID]) of
{result, Items} when length(Items) < 2 ->
Items;
{result, Items} ->
[hd(lists:keysort(#pubsub_item.modification, Items))];
_ -> []
end;
LastItem ->
[LastItem]
end.
%% @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 ->
case node_action(Host, Type,
get_entity_affiliations,
[Host, JID]) of
{result, Affs} ->
{Status, [Affs | Acc]};
{error, _} = Err ->
{Err, Acc}
end
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(?T("Owner privileges required"), ejabberd_option:language())};
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(
?T("Owner privileges required"), ejabberd_option:language())}
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),
case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of
{result, Subs} ->
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;
{error, _} = Error ->
Error
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),
case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of
{result, Subs} ->
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;
{error, _} = Err ->
Err
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),
case node_action(Host, Type,
get_entity_subscriptions,
[Host, Subscriber]) of
{result, Subs} ->
{Status, [Subs | Acc]};
{error, _} = Err ->
{Err, Acc}
end
end
end, {ok, []}, Plugins),
case Result of
{ok, Subs} ->
Entities = lists:flatmap(fun
({#pubsub_node{nodeid = {_, SubsNode}}, Sub}) ->
case Node of
<<>> ->
[#ps_subscription{jid = jid:remove_resource(JID),
node = SubsNode, type = Sub}];
SubsNode ->
[#ps_subscription{jid = jid:remove_resource(JID),
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),
case node_call(Host, Type, get_affiliation, [Nidx, JID]) of
{result, Affiliation} ->
if not RetrieveFeature ->
{error, extended_error(xmpp:err_feature_not_implemented(),
err_unsupported('manage-subscriptions'))};
Affiliation /= owner ->
Lang = ejabberd_option:language(),
{error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)};
true ->
node_call(Host, Type, get_node_subscriptions, [Nidx])
end;
Error ->
Error
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.
-spec get_subscriptions_for_send_last(host(), binary(), atom(), jid(), ljid(), ljid()) ->
[{#pubsub_node{}, subId(), ljid()}].
get_subscriptions_for_send_last(Host, PType, sql, JID, LJID, BJID) ->
case node_action(Host, PType,
get_entity_subscriptions_for_send_last,
[Host, JID]) of
{result, Subs} ->
[{Node, SubId, SubJID}
|| {Node, Sub, SubId, SubJID} <- Subs,
Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID)];
_ ->
[]
end;
%% sql version already filter result by on_sub_and_presence
get_subscriptions_for_send_last(Host, PType, _, JID, LJID, BJID) ->
case node_action(Host, PType,
get_entity_subscriptions,
[Host, JID]) of
{result, Subs} ->
[{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)];
_ ->
[]
end.
-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(
?T("Owner privileges required"), ejabberd_option:language())}
end
end,
case transaction(Host, Node, Action, sync_dirty) of
{result, {_, Result}} -> {result, Result};
Other -> Other
end.
-spec get_presence_and_roster_permissions(
host(), jid(), [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.
-spec get_roster_info(binary(), binary(), ljid() | jid(), [binary()]) -> {boolean(), boolean()}.
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).
%% @doc <p>Check if a notification must be delivered or not based on
%% node and subscription options.</p>
-spec is_to_deliver(ljid(), items | nodes, integer(), nodeOptions(), subOptions()) -> boolean().
is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) ->
sub_to_deliver(LJID, NotifyType, Depth, SubOptions)
andalso node_to_deliver(LJID, NodeOptions).
-spec sub_to_deliver(ljid(), items | nodes, integer(), subOptions()) -> boolean().
sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) ->
lists:all(fun (Option) ->
sub_option_can_deliver(NotifyType, Depth, Option)
end,
SubOptions).
-spec node_to_deliver(ljid(), nodeOptions()) -> boolean().
node_to_deliver(LJID, NodeOptions) ->
presence_can_deliver(LJID, get_option(NodeOptions, presence_based_delivery)).
-spec sub_option_can_deliver(items | nodes, integer(), _) -> boolean().
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}) -> erlang: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 = Sh} -> atom_to_binary(Sh, 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).
-spec payload_xmlelements([xmlel()], non_neg_integer()) -> non_neg_integer().
payload_xmlelements([], Count) -> Count;
payload_xmlelements([#xmlel{} | Tail], Count) ->
payload_xmlelements(Tail, Count + 1);
payload_xmlelements([_ | Tail], Count) ->
payload_xmlelements(Tail, Count).
-spec items_els(binary(), nodeOptions(), [#pubsub_item{}]) -> ps_items().
items_els(Node, Options, Items) ->
Els = case get_option(Options, itemreply) of
publisher ->
[#ps_item{id = ItemId, sub_els = Payload, publisher = jid:encode(USR)}
|| #pubsub_item{itemid = {ItemId, _}, payload = Payload, modification = {_, USR}}
<- Items];
_ ->
[#ps_item{id = ItemId, sub_els = Payload}
|| #pubsub_item{itemid = {ItemId, _}, payload = Payload}
<- Items]
end,
#ps_items{node = Node, items = Els}.
%%%%%% broadcast functions
-spec broadcast_publish_item(host(), binary(), nodeIdx(), binary(),
nodeOptions(), binary(), jid(), [xmlel()], _) ->
{result, boolean()}.
broadcast_publish_item(Host, Node, Nidx, Type, NodeOptions, ItemId, From, Payload, Removed) ->
case get_collection_subscriptions(Host, Node) of
{result, SubsByDepth} ->
ItemPublisher = case get_option(NodeOptions, itemreply) of
publisher -> jid:encode(From);
_ -> <<>>
end,
ItemPayload = case get_option(NodeOptions, deliver_payloads) of
true -> Payload;
false -> []
end,
ItemsEls = #ps_items{node = Node,
items = [#ps_item{id = ItemId,
publisher = ItemPublisher,
sub_els = ItemPayload}]},
Stanza = #message{ sub_els = [#ps_event{items = ItemsEls}]},
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.
-spec broadcast_retract_items(host(), binary(), nodeIdx(), binary(),
nodeOptions(), [itemId()]) -> {result, boolean()}.
broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds) ->
broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds, false).
-spec broadcast_retract_items(host(), binary(), nodeIdx(), binary(),
nodeOptions(), [itemId()], boolean()) -> {result, boolean()}.
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
{result, 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.
-spec broadcast_purge_node(host(), binary(), nodeIdx(), binary(), nodeOptions()) -> {result, boolean()}.
broadcast_purge_node(Host, Node, Nidx, Type, NodeOptions) ->
case get_option(NodeOptions, notify_retract) of
true ->
case get_collection_subscriptions(Host, Node) of
{result, 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.
-spec broadcast_removed_node(host(), binary(), nodeIdx(), binary(),
nodeOptions(), subs_by_depth()) -> {result, boolean()}.
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.
-spec broadcast_created_node(host(), binary(), nodeIdx(), binary(),
nodeOptions(), subs_by_depth()) -> {result, boolean()}.
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}.
-spec broadcast_config_notification(host(), binary(), nodeIdx(), binary(),
nodeOptions(), binary()) -> {result, boolean()}.
broadcast_config_notification(Host, Node, Nidx, Type, NodeOptions, Lang) ->
case get_option(NodeOptions, notify_config) of
true ->
case get_collection_subscriptions(Host, Node) of
{result, 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.
-spec get_collection_subscriptions(host(), nodeId()) -> {result, subs_by_depth()} |
{error, stanza_error()}.
get_collection_subscriptions(Host, Node) ->
Action = fun() -> get_node_subs_by_depth(Host, Node, service_jid(Host)) end,
transaction(Host, Action, sync_dirty).
-spec get_node_subs_by_depth(host(), nodeId(), jid()) -> {result, subs_by_depth()} |
{error, stanza_error()}.
get_node_subs_by_depth(Host, Node, From) ->
case tree_call(Host, get_parentnodes_tree, [Host, Node, From]) of
ParentTree when is_list(ParentTree) ->
{result,
lists:filtermap(
fun({Depth, Nodes}) ->
case lists:filtermap(
fun(N) ->
case get_node_subs(Host, N) of
{result, Result} -> {true, {N, Result}};
_ -> false
end
end, Nodes) of
[] -> false;
Subs -> {true, {Depth, Subs}}
end
end, ParentTree)};
Error ->
Error
end.
-spec get_node_subs(host(), #pubsub_node{}) -> {result, [{ljid(), subId(), subOptions()}]} |
{error, stanza_error()}.
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} -> {result, get_options_for_subs(Host, Nidx, Subs, WithOptions)};
Other -> Other
end.
-spec get_options_for_subs(host(), nodeIdx(),
[{ljid(), subscription(), subId()}],
boolean()) ->
[{ljid(), subId(), subOptions()}].
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).
-spec broadcast_stanza(host(), nodeId(), nodeIdx(), binary(),
nodeOptions(), subs_by_depth(),
items | nodes, stanza(), boolean()) -> ok.
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).
-spec broadcast_stanza(host(), jid(), nodeId(), nodeIdx(), binary(),
nodeOptions(), subs_by_depth(), items | nodes,
stanza(), boolean()) -> ok.
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
Owner = jid:make(LUser, LServer),
FromBareJid = xmpp:set_from(BaseStanza, Owner),
Stanza = add_extended_headers(
add_message_type(FromBareJid, NotificationType),
extended_headers([Publisher])),
Pred = fun(To) -> delivery_permitted(Owner, To, NodeOptions) end,
ejabberd_sm:route(jid:make(LUser, LServer, SenderResource),
{pep_message, <<((Node))/binary, "+notify">>, Stanza, Pred}),
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, Pred}) when is_function(Pred) ->
[maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet)
|| {USR, Caps} <- mod_caps:list_features(C2SState), Pred(USR)],
{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.
-spec send_items(host(), nodeId(), nodeIdx(), binary(),
nodeOptions(), ljid(), last | integer()) -> ok.
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) ->
Items = case max_items(Host, Options) of
1 ->
get_only_item(Host, Type, Nidx, SubLJID);
_ ->
get_last_items(Host, Type, Nidx, SubLJID, Number)
end,
case Items of
[] ->
ok;
Items ->
Delay = case Number of
last -> % handle section 6.1.7 of XEP-0060
[Last] = Items,
{Stamp, _USR} = Last#pubsub_item.modification,
[#delay{stamp = Stamp}];
_ ->
[]
end,
Stanza = #message{
sub_els = [#ps_event{items = items_els(Node, Options, Items)}
| Delay]},
NotificationType = get_option(Options, notification_type, headline),
send_stanza(Publisher, ToLJID, Node,
add_message_type(Stanza, NotificationType))
end.
-spec send_stanza(host(), ljid(), binary(), stanza()) -> ok.
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,
lists:foreach(
fun(To) ->
ejabberd_sm:route(
jid:make(Publisher),
{pep_message, <<((Node))/binary, "+notify">>,
add_extended_headers(
Stanza, extended_headers([jid:make(Publisher)])),
To})
end, USRs);
send_stanza(Host, USR, _Node, Stanza) ->
ejabberd_router:route(
xmpp:set_from_to(Stanza, service_jid(Host), jid:make(USR))).
-spec maybe_send_pep_stanza(binary(), ljid(), caps(), binary(), stanza()) -> ok.
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.
-spec send_last_items(jid()) -> ok.
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, Features) ->
ServerHost = From#jid.lserver,
Host = host(ServerHost),
Publisher = jid:tolower(From),
Owner = jid:remove_resource(Publisher),
NotifyNodes =
case Features of
_ when is_list(Features) ->
lists:filtermap(
fun(V) ->
Vs = byte_size(V) - 7,
case V of
<<NotNode:Vs/binary, "+notify">> ->
{true, NotNode};
_ ->
false
end
end, Features);
_ ->
unknown
end,
case tree_action(Host, get_nodes, [Owner, infinity]) of
Nodes when is_list(Nodes) ->
lists:foreach(
fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}) ->
MaybeNotify =
case NotifyNodes of
unknown -> true;
_ -> lists:member(Node, NotifyNodes)
end,
case MaybeNotify andalso match_option(Options, send_last_published_item, on_sub_and_presence) of
true ->
case delivery_permitted(From, To, Options) of
true ->
LJID = jid:tolower(To),
send_items(Owner, Node, Nidx, Type, Options,
Publisher, LJID, LJID, 1);
false ->
ok
end;
_ ->
ok
end
end, Nodes);
_ ->
ok
end.
-spec subscribed_nodes_by_jid(items | nodes, subs_by_depth()) -> [{ljid(), binary(), subId()}].
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 delivery_permitted(jid() | ljid(), jid() | ljid(), nodeOptions()) -> boolean().
delivery_permitted(From, To, Options) ->
LFrom = jid:tolower(From),
LTo = jid:tolower(To),
RecipientIsOwner = jid:remove_resource(LFrom) == jid:remove_resource(LTo),
%% TODO: Fix the 'whitelist'/'authorize' cases for last PEP notifications.
%% Currently, only node owners receive those.
case get_option(Options, access_model) of
open -> true;
presence -> true;
whitelist -> RecipientIsOwner;
authorize -> RecipientIsOwner;
roster ->
Grps = get_option(Options, roster_groups_allowed, []),
{LUser, LServer, _} = LFrom,
{_, IsInGrp} = get_roster_info(LUser, LServer, LTo, Grps),
IsInGrp
end.
-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}}}};
{result, _} ->
{error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)};
Error ->
Error
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.
node_config(Node, ServerHost) ->
Opts = mod_pubsub_opt:force_node_config(ServerHost),
node_config(Node, ServerHost, Opts).
node_config(Node, ServerHost, [{RE, Opts}|NodeOpts]) ->
case re:run(Node, RE) of
{match, _} ->
Opts;
nomatch ->
node_config(Node, ServerHost, NodeOpts)
end;
node_config(_, _, []) ->
[].
%% @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() | unlimited.
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;
_ -> get_max_items_node(Host)
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:filtermap(
fun({roster_groups_allowed, Value}) ->
{true, {roster_groups_allowed, Value, Groups}};
({sql, _}) -> false;
({rsm, _}) -> false;
(_) -> true
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(
[node_config(Node, serverhost(Host)),
Config, OldOpts]),
case tree_call(Host,
set_node,
[N#pubsub_node{options = NewOpts}]) of
{result, Nidx} -> {result, NewOpts};
ok -> {result, NewOpts};
Err -> Err
end;
{result, _} ->
{error, xmpp:err_forbidden(
?T("Owner privileges required"), Lang)};
Error ->
Error
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()].
merge_config(ListOfConfigs) ->
lists:ukeysort(1, lists:flatten(ListOfConfigs)).
-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.
-spec check_opt_range(atom(), [proplists:property()],
non_neg_integer() | unlimited | undefined) -> boolean().
check_opt_range(_Opt, _Opts, undefined) ->
true;
check_opt_range(_Opt, _Opts, unlimited) ->
true;
check_opt_range(Opt, Opts, Max) ->
case proplists:get_value(Opt, Opts, Max) of
max -> true;
Val -> Val =< Max
end.
-spec get_max_items_node(host()) -> undefined | unlimited | 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(), jid(), [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 = {erlang: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 | #pubsub_item{}.
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() | atom(), 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() | atom(), 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()) -> atom().
subscription_plugin(Host) ->
submodule(Host, <<"pubsub">>, <<"subscription">>).
-spec submodule(host() | atom(), binary(), binary()) -> atom().
submodule(Db, Type, Name) when is_atom(Db) ->
case Db of
mnesia -> ejabberd:module_name([<<"pubsub">>, Type, Name]);
_ -> ejabberd:module_name([<<"pubsub">>, Type, Name, misc:atom_to_binary(Db)])
end;
submodule(Host, Type, Name) ->
Db = mod_pubsub_opt:db_type(serverhost(Host)),
submodule(Db, Type, Name).
-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
<<"config-node-max">>,
<<"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(host(), 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>
-spec tree_call(host(), atom(), list()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any().
tree_call({_User, Server, _Resource}, Function, Args) ->
tree_call(Server, Function, Args);
tree_call(Host, Function, Args) ->
Tree = tree(Host),
?DEBUG("Tree_call apply(~ts, ~ts, ~p) @ ~ts", [Tree, Function, Args, Host]),
case apply(Tree, Function, Args) of
{error, #stanza_error{}} = Err ->
Err;
{error, {virtual, _}} = Err ->
Err;
{error, _} ->
ErrTxt = ?T("Database failure"),
Lang = ejabberd_option:language(),
{error, xmpp:err_internal_server_error(ErrTxt, Lang)};
Other ->
Other
end.
-spec tree_action(host(), atom(), list()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any().
tree_action(Host, Function, Args) ->
?DEBUG("Tree_action ~p ~p ~p", [Host, Function, Args]),
ServerHost = serverhost(Host),
DBType = mod_pubsub_opt:db_type(ServerHost),
Fun = fun () ->
try tree_call(Host, Function, Args)
catch ?EX_RULE(Class, Reason, St) when DBType == sql ->
StackTrace = ?EX_STACK(St),
ejabberd_sql:abort({exception, Class, Reason, StackTrace})
end
end,
Ret = case DBType of
mnesia ->
mnesia:sync_dirty(Fun);
sql ->
ejabberd_sql:sql_bloc(ServerHost, Fun);
_ ->
Fun()
end,
get_tree_action_result(Ret).
-spec get_tree_action_result(any()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any().
get_tree_action_result({atomic, Result}) ->
Result;
get_tree_action_result({aborted, {exception, Class, Reason, StackTrace}}) ->
?ERROR_MSG("Transaction aborted:~n** ~ts",
[misc:format_exception(2, Class, Reason, StackTrace)]),
get_tree_action_result({error, db_failure});
get_tree_action_result({aborted, Reason}) ->
?ERROR_MSG("Transaction aborted: ~p~n", [Reason]),
get_tree_action_result({error, db_failure});
get_tree_action_result({error, #stanza_error{}} = Err) ->
Err;
get_tree_action_result({error, {virtual, _}} = Err) ->
Err;
get_tree_action_result({error, _}) ->
ErrTxt = ?T("Database failure"),
Lang = ejabberd_option:language(),
{error, xmpp:err_internal_server_error(ErrTxt, Lang)};
get_tree_action_result(Other) ->
%% This is very risky, but tree plugins design is really bad
Other.
%% @doc <p>node plugin call.</p>
-spec node_call(host(), binary(), atom(), list()) -> {result, any()} | {error, stanza_error()}.
node_call(Host, Type, Function, Args) ->
?DEBUG("Node_call ~p ~p ~p", [Type, Function, Args]),
Module = plugin(Host, Type),
case erlang:function_exported(Module, Function, length(Args)) of
true ->
case apply(Module, Function, Args) of
{result, Result} ->
{result, Result};
#pubsub_state{} = Result ->
{result, Result};
{error, #stanza_error{}} = Err ->
Err;
{error, _} ->
ErrTxt = ?T("Database failure"),
Lang = ejabberd_option:language(),
{error, xmpp:err_internal_server_error(ErrTxt, Lang)}
end;
false when Type /= ?STDNODE ->
node_call(Host, ?STDNODE, Function, Args);
false ->
%% Let it crash with the stacktrace
apply(Module, Function, Args)
end.
-spec node_action(host(), binary(), atom(), list()) -> {result, any()} | {error, stanza_error()}.
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>
-spec transaction(host(), binary(), fun((#pubsub_node{}) -> _), transaction | sync_dirty) ->
{result, any()} | {error, stanza_error()}.
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).
-spec transaction(host(), fun(), transaction | sync_dirty) ->
{result, any()} | {error, stanza_error()}.
transaction(Host, Fun, Trans) ->
ServerHost = serverhost(Host),
DBType = mod_pubsub_opt:db_type(ServerHost),
do_transaction(ServerHost, Fun, Trans, DBType).
-spec do_transaction(binary(), fun(), transaction | sync_dirty, atom()) ->
{result, any()} | {error, stanza_error()}.
do_transaction(ServerHost, Fun, Trans, DBType) ->
F = fun() ->
try Fun()
catch ?EX_RULE(Class, Reason, St) when (DBType == mnesia andalso
Trans == transaction) orelse
DBType == sql ->
StackTrace = ?EX_STACK(St),
Ex = {exception, Class, Reason, StackTrace},
case DBType of
mnesia -> mnesia:abort(Ex);
sql -> ejabberd_sql:abort(Ex)
end
end
end,
Res = case DBType of
mnesia ->
mnesia:Trans(F);
sql ->
SqlFun = case Trans of
transaction -> sql_transaction;
_ -> sql_bloc
end,
ejabberd_sql:SqlFun(ServerHost, F);
_ ->
F()
end,
get_transaction_response(Res).
-spec get_transaction_response(any()) -> {result, any()} | {error, stanza_error()}.
get_transaction_response({result, _} = Result) ->
Result;
get_transaction_response({error, #stanza_error{}} = Err) ->
Err;
get_transaction_response({atomic, Result}) ->
get_transaction_response(Result);
get_transaction_response({aborted, Err}) ->
get_transaction_response(Err);
get_transaction_response({error, _}) ->
Lang = ejabberd_option:language(),
{error, xmpp:err_internal_server_error(?T("Database failure"), Lang)};
get_transaction_response({exception, Class, Reason, StackTrace}) ->
?ERROR_MSG("Transaction aborted:~n** ~ts",
[misc:format_exception(2, Class, Reason, StackTrace)]),
get_transaction_response({error, db_failure});
get_transaction_response(Err) ->
?ERROR_MSG("Transaction error: ~p", [Err]),
get_transaction_response({error, db_failure}).
%%%% 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} = erlang:timestamp(),
(str:format("~.16B~.16B~.16B", [T1, T2, T3])).
-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 ->
case node_action(Host, Type,
get_entity_affiliations, [Host, LJID]) of
{result, Affs} ->
{Status, [Affs | Acc]};
{error, _} = Err ->
{Err, Acc}
end;
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)));
_ ->
ok
end.
-spec purge_offline(host(), ljid(), #pubsub_node{}) -> 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;
_ ->
ok
end;
(_) ->
true
end, Items);
{error, #stanza_error{}} = Err ->
Err;
_ ->
Txt = ?T("Database failure"),
Lang = ejabberd_option:language(),
{error, xmpp:err_internal_server_error(Txt, Lang)}
end.
-spec delete_old_items(non_neg_integer()) -> ok | error.
delete_old_items(N) ->
Results = lists:flatmap(
fun(Host) ->
case tree_action(Host, get_all_nodes, [Host]) of
Nodes when is_list(Nodes) ->
lists:map(
fun(#pubsub_node{id = Nidx, type = Type}) ->
case node_action(Host, Type,
remove_extra_items,
[Nidx , N]) of
{result, _} ->
ok;
{error, _} ->
error
end
end, Nodes);
_ ->
error
end
end, ejabberd_option:hosts()),
case lists:member(error, Results) of
true ->
error;
false ->
ok
end.
-spec get_commands_spec() -> [ejabberd_commands()].
get_commands_spec() ->
[#ejabberd_commands{name = delete_old_pubsub_items, tags = [purge],
desc = "Keep only NUMBER of PubSub items per node",
module = ?MODULE, function = delete_old_items,
args_desc = ["Number of items to keep per node"],
args = [{number, integer}],
result = {res, rescode},
result_desc = "0 if command failed, 1 when succeeded",
args_example = [1000],
result_example = ok}].
-spec mod_opt_type(atom()) -> econf:validator().
mod_opt_type(access_createnode) ->
econf:acl();
mod_opt_type(name) ->
econf:binary();
mod_opt_type(ignore_pep_from_offline) ->
econf:bool();
mod_opt_type(last_item_cache) ->
econf:bool();
mod_opt_type(max_items_node) ->
econf:non_neg_int(unlimited);
mod_opt_type(max_nodes_discoitems) ->
econf:non_neg_int(infinity);
mod_opt_type(max_subscriptions_node) ->
econf:non_neg_int();
mod_opt_type(force_node_config) ->
econf:map(
econf:glob(),
econf:map(
econf:atom(),
econf:either(
econf:int(),
econf:atom()),
[{return, orddict}, unique]));
mod_opt_type(default_node_config) ->
econf:map(
econf:atom(),
econf:either(
econf:int(),
econf:atom()),
[unique]);
mod_opt_type(nodetree) ->
econf:binary();
mod_opt_type(pep_mapping) ->
econf:map(econf:binary(), econf:binary());
mod_opt_type(plugins) ->
econf:list(
econf:enum([<<"flat">>, <<"pep">>]),
[unique]);
mod_opt_type(host) ->
econf:host();
mod_opt_type(hosts) ->
econf:hosts();
mod_opt_type(db_type) ->
econf:db_type(?MODULE);
mod_opt_type(vcard) ->
econf:vcard_temp().
mod_options(Host) ->
[{access_createnode, all},
{db_type, ejabberd_config:default_db(Host, ?MODULE)},
{host, <<"pubsub.", Host/binary>>},
{hosts, []},
{name, ?T("Publish-Subscribe")},
{vcard, undefined},
{ignore_pep_from_offline, true},
{last_item_cache, false},
{max_items_node, ?MAXITEMS},
{max_nodes_discoitems, 100},
{nodetree, ?STDTREE},
{pep_mapping, []},
{plugins, [?STDNODE]},
{max_subscriptions_node, undefined},
{default_node_config, []},
{force_node_config, []}].
mod_doc() ->
#{desc =>
[?T("This module offers a service for "
"https://xmpp.org/extensions/xep-0060.html"
"[XEP-0060: Publish-Subscribe]. The functionality in "
"'mod_pubsub' can be extended using plugins. "
"The plugin that implements PEP "
"(https://xmpp.org/extensions/xep-0163.html"
"[XEP-0163: Personal Eventing via Pubsub]) "
"is enabled in the default ejabberd configuration file, "
"and it requires 'mod_caps'.")],
opts =>
[{access_createnode,
#{value => "AccessName",
desc =>
?T("This option restricts which users are allowed to "
"create pubsub nodes using 'acl' and 'access'. "
"By default any account in the local ejabberd server "
"is allowed to create pubsub nodes. "
"The default value is: 'all'.")}},
{db_type,
#{value => "mnesia | sql",
desc =>
?T("Same as top-level 'default_db' option, but applied to "
"this module only.")}},
{default_node_config,
#{value => "List of Key:Value",
desc =>
?T("To override default node configuration, regardless "
"of node plugin. Value is a list of key-value "
"definition. Node configuration still uses default "
"configuration defined by node plugin, and overrides "
"any items by value defined in this configurable list.")}},
{force_node_config,
#{value => "List of Node and the list of its Key:Value",
desc =>
?T("Define the configuration for given nodes. "
"The default value is: '[]'."),
example =>
["force_node_config:",
" ## Avoid buggy clients to make their bookmarks public",
" storage:bookmarks:",
" access_model: whitelist"]}},
{host,
#{desc => ?T("Deprecated. Use 'hosts' instead.")}},
{hosts,
#{value => ?T("[Host, ...]"),
desc =>
?T("This option defines the Jabber IDs of the service. "
"If the 'hosts' option is not specified, the only Jabber "
"ID will be the hostname of the virtual host with the "
"prefix \"pubsub.\". The keyword '@HOST@' is replaced "
"with the real virtual host name.")}},
{ignore_pep_from_offline,
#{value => "false | true",
desc =>
?T("To specify whether or not we should get last "
"published PEP items from users in our roster which "
"are offline when we connect. Value is 'true' or "
"'false'. If not defined, pubsub assumes true so we "
"only get last items of online contacts.")}},
{last_item_cache,
#{value => "false | true",
desc =>
?T("To specify whether or not pubsub should cache last "
"items. Value is 'true' or 'false'. If not defined, "
"pubsub does not cache last items. On systems with not"
" so many nodes, caching last items speeds up pubsub "
"and allows to raise user connection rate. The cost "
"is memory usage, as every item is stored in memory.")}},
{max_items_node,
#{value => "non_neg_integer() | infinity",
desc =>
?T("Define the maximum number of items that can be "
"stored in a node. Default value is: '10'.")}},
{max_nodes_discoitems,
#{value => "pos_integer() | infinity",
desc =>
?T("The maximum number of nodes to return in a "
"discoitem response. The default value is: '100'.")}},
{max_subscriptions_node,
#{value => "MaxSubs",
desc =>
?T("Define the maximum number of subscriptions managed "
"by a node. "
"Default value is no limitation: 'undefined'.")}},
{name,
#{value => ?T("Name"),
desc =>
?T("The value of the service name. This name is only visible "
"in some clients that support "
"https://xmpp.org/extensions/xep-0030.html"
"[XEP-0030: Service Discovery]. "
"The default is 'vCard User Search'.")}},
{nodetree,
#{value => "Nodetree",
desc =>
[?T("To specify which nodetree to use. If not defined, the "
"default pubsub nodetree is used: 'tree'. Only one "
"nodetree can be used per host, and is shared by all "
"node plugins."),
?T("- 'tree' nodetree store node configuration and "
"relations on the database. 'flat' nodes are stored "
"without any relationship, and 'hometree' nodes can "
"have child nodes."),
?T("- 'virtual' nodetree does not store nodes on database. "
"This saves resources on systems with tons of nodes. "
"If using the 'virtual' nodetree, you can only enable "
"those node plugins: '[flat, pep]' or '[flat]'; any "
"other plugins configuration will not work. Also, all "
"nodes will have the default configuration, and this "
"can not be changed. Using 'virtual' nodetree requires "
"to start from a clean database, it will not work if "
"you used the default 'tree' nodetree before.")]}},
{pep_mapping,
#{value => "List of Key:Value",
desc =>
?T("This allows to define a list of key-value to choose "
"defined node plugins on given PEP namespace. "
"The following example will use 'node_tune' instead of "
"'node_pep' for every PEP node with the tune namespace:"),
example =>
["modules:",
" ...",
" mod_pubsub:",
" pep_mapping:",
" http://jabber.org/protocol/tune: tune",
" ..."]
}},
{plugins,
#{value => "[Plugin, ...]",
desc => [?T("To specify which pubsub node plugins to use. "
"The first one in the list is used by default. "
"If this option is not defined, the default plugins "
"list is: '[flat]'. PubSub clients can define which "
"plugin to use when creating a node: "
"add 'type=\'plugin-name\'' attribute "
"to the 'create' stanza element."),
?T("- 'flat' plugin handles the default behaviour and "
"follows standard XEP-0060 implementation."),
?T("- 'pep' plugin adds extention to handle Personal "
"Eventing Protocol (XEP-0163) to the PubSub engine. "
"Adding pep allows to handle PEP automatically.")]}},
{vcard,
#{value => ?T("vCard"),
desc =>
?T("A custom vCard of the server that will be displayed by "
"some XMPP clients in Service Discovery. The value of "
"'vCard' is a YAML map constructed from an XML "
"representation of vCard. Since the representation has "
"no attributes, the mapping is straightforward."),
example =>
[{?T("The following XML representation of vCard:"),
["<vCard xmlns='vcard-temp'>",
" <FN>PubSub Service</FN>",
" <ADR>",
" <WORK/>",
" <STREET>Elm Street</STREET>",
" </ADR>",
"</vCard>"]},
{?T("will be translated to:"),
["vcard:",
" fn: PubSub Service",
" adr:",
" -",
" work: true",
" street: Elm Street"]}]}}
],
example =>
[{?T("Example of configuration that uses flat nodes as default, "
"and allows use of flat, hometree and pep nodes:"),
["modules:",
" ...",
" mod_pubsub:",
" access_createnode: pubsub_createnode",
" max_subscriptions_node: 100",
" default_node_config:",
" notification_type: normal",
" notify_retract: false",
" max_items: 4",
" plugins:",
" - flat",
" - pep",
" ..."]},
{?T("Using relational database requires using mod_pubsub with "
"db_type 'sql'. Only flat, hometree and pep plugins supports "
"SQL. The following example shows previous configuration "
"with SQL usage:"),
["modules:",
" ...",
" mod_pubsub:",
" db_type: sql",
" access_createnode: pubsub_createnode",
" ignore_pep_from_offline: true",
" last_item_cache: false",
" plugins:",
" - flat",
" - pep",
" ..."]}
]}.