mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-12 15:57:31 +01:00
112c609222
friendly (thanks to Badlop). * src/mod_irc/mod_irc.erl: Likewise. * src/mod_pubsub/mod_pubsub.erl: Likewise. SVN Revision: 620
1699 lines
47 KiB
Erlang
1699 lines
47 KiB
Erlang
%%%----------------------------------------------------------------------
|
|
%%% File : mod_pubsub.erl
|
|
%%% Author : Alexey Shchepin <alexey@sevcom.net>
|
|
%%% Purpose : Pub/sub support (JEP-0060)
|
|
%%% Created : 4 Jul 2003 by Alexey Shchepin <alexey@sevcom.net>
|
|
%%% Id : $Id$
|
|
%%%----------------------------------------------------------------------
|
|
|
|
-module(mod_pubsub).
|
|
-author('alexey@sevcom.net').
|
|
-vsn('$Revision$ ').
|
|
|
|
-behaviour(gen_server).
|
|
-behaviour(gen_mod).
|
|
|
|
%% API
|
|
-export([start_link/2,
|
|
start/2,
|
|
stop/1]).
|
|
|
|
-export([delete_item/3,
|
|
set_entities/4,
|
|
delete_node/2,
|
|
create_new_node/2,
|
|
subscribe_node/3,
|
|
get_node_config/4,
|
|
set_node_config/4]).
|
|
|
|
%% gen_server callbacks
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
|
terminate/2, code_change/3]).
|
|
|
|
-include("ejabberd.hrl").
|
|
-include("jlib.hrl").
|
|
|
|
-record(state, {host, server_host, access}).
|
|
|
|
-define(DICT, dict).
|
|
-define(MAXITEMS, 20).
|
|
-define(MAX_PAYLOAD_SIZE, 100000).
|
|
|
|
-record(pubsub_node, {host_node, host_parent, info}).
|
|
-record(nodeinfo, {items = [],
|
|
options = [],
|
|
entities = ?DICT:new()
|
|
}).
|
|
-record(entity, {affiliation = none,
|
|
subscription = none}).
|
|
-record(item, {id, publisher, payload}).
|
|
|
|
-define(PROCNAME, ejabberd_mod_pubsub).
|
|
-define(MYJID, #jid{user = "", server = Host, resource = "",
|
|
luser = "", lserver = Host, lresource = ""}).
|
|
|
|
%%====================================================================
|
|
%% API
|
|
%%====================================================================
|
|
%%--------------------------------------------------------------------
|
|
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
|
|
%% Description: Starts the server
|
|
%%--------------------------------------------------------------------
|
|
start_link(Host, Opts) ->
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
|
|
|
|
start(Host, Opts) ->
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
ChildSpec =
|
|
{Proc,
|
|
{?MODULE, start_link, [Host, Opts]},
|
|
temporary,
|
|
1000,
|
|
worker,
|
|
[?MODULE]},
|
|
supervisor:start_child(ejabberd_sup, ChildSpec).
|
|
|
|
stop(Host) ->
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
gen_server:call(Proc, stop),
|
|
supervisor:stop_child(ejabberd_sup, Proc).
|
|
|
|
delete_item(From, Node, ItemID) ->
|
|
delete_item(get_host(), From, Node, ItemID).
|
|
|
|
delete_node(From, Node) ->
|
|
delete_node(get_host(), From, Node).
|
|
|
|
create_new_node(Node, From) ->
|
|
create_new_node(get_host(), Node, From).
|
|
|
|
subscribe_node(From, JID, Node) ->
|
|
subscribe_node(get_host(), From, JID, Node).
|
|
|
|
set_node_config(From, Node, Els, Lang) ->
|
|
set_node_config(get_host(), From, Node, Els, Lang).
|
|
|
|
get_host() ->
|
|
ejabberd_mod_pubsub ! {get_host, self()},
|
|
receive
|
|
{pubsub_host, Host} ->
|
|
Host
|
|
after 5000 ->
|
|
timeout
|
|
end.
|
|
|
|
%%====================================================================
|
|
%% gen_server callbacks
|
|
%%====================================================================
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: init(Args) -> {ok, State} |
|
|
%% {ok, State, Timeout} |
|
|
%% ignore |
|
|
%% {stop, Reason}
|
|
%% Description: Initiates the server
|
|
%%--------------------------------------------------------------------
|
|
init([ServerHost, Opts]) ->
|
|
mnesia:create_table(pubsub_node,
|
|
[{disc_only_copies, [node()]},
|
|
{attributes, record_info(fields, pubsub_node)}]),
|
|
Host = gen_mod:get_opt(host, Opts, "pubsub." ++ ServerHost),
|
|
update_table(Host),
|
|
mnesia:add_table_index(pubsub_node, host_parent),
|
|
ServedHosts = gen_mod:get_opt(served_hosts, Opts, []),
|
|
Access = gen_mod:get_opt(access_createnode, Opts, all),
|
|
|
|
ejabberd_router:register_route(Host),
|
|
create_new_node(Host, ["pubsub"], ?MYJID),
|
|
create_new_node(Host, ["pubsub", "nodes"], ?MYJID),
|
|
create_new_node(Host, ["home"], ?MYJID),
|
|
create_new_node(Host, ["home", ServerHost], ?MYJID),
|
|
lists:foreach(fun(H) ->
|
|
create_new_node(Host, ["home", H], ?MYJID)
|
|
end, ServedHosts),
|
|
ets:new(gen_mod:get_module_proc(Host, pubsub_presence),
|
|
[set, named_table]),
|
|
{ok, #state{host = Host, server_host = ServerHost, access = Access}}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
|
|
%% {reply, Reply, State, Timeout} |
|
|
%% {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, Reply, State} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling call messages
|
|
%%--------------------------------------------------------------------
|
|
handle_call(stop, _From, State) ->
|
|
{stop, normal, ok, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: handle_cast(Msg, State) -> {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling cast messages
|
|
%%--------------------------------------------------------------------
|
|
handle_cast(_Msg, State) ->
|
|
{noreply, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: handle_info(Info, State) -> {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling all non call/cast messages
|
|
%%--------------------------------------------------------------------
|
|
handle_info({route, From, To, Packet},
|
|
#state{server_host = ServerHost, access = Access} = State) ->
|
|
case catch do_route(To#jid.lserver, ServerHost, Access, From, To, Packet) of
|
|
{'EXIT', Reason} ->
|
|
?ERROR_MSG("~p", [Reason]);
|
|
_ ->
|
|
ok
|
|
end,
|
|
{noreply, State};
|
|
handle_info(_Info, State) ->
|
|
{noreply, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: terminate(Reason, State) -> void()
|
|
%% Description: This function is called by a gen_server when it is about to
|
|
%% terminate. It should be the opposite of Module:init/1 and do any necessary
|
|
%% cleaning up. When it returns, the gen_server terminates with Reason.
|
|
%% The return value is ignored.
|
|
%%--------------------------------------------------------------------
|
|
terminate(_Reason, State) ->
|
|
ejabberd_router:unregister_route(State#state.host),
|
|
ok.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
|
|
%% Description: Convert process state when code is changed
|
|
%%--------------------------------------------------------------------
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
{ok, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%%% Internal functions
|
|
%%--------------------------------------------------------------------
|
|
do_route(Host, ServerHost, Access, From, To, Packet) ->
|
|
{xmlelement, Name, Attrs, Els} = Packet,
|
|
case To of
|
|
#jid{luser = "", lresource = ""} ->
|
|
case Name of
|
|
"iq" ->
|
|
case jlib:iq_query_info(Packet) of
|
|
#iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS,
|
|
sub_el = SubEl} = IQ ->
|
|
{xmlelement, _, QAttrs, _} = SubEl,
|
|
Node = xml:get_attr_s("node", QAttrs),
|
|
Res = IQ#iq{type = result,
|
|
sub_el = [{xmlelement, "query",
|
|
QAttrs,
|
|
iq_disco_info(Node)}]},
|
|
ejabberd_router:route(To,
|
|
From,
|
|
jlib:iq_to_xml(Res));
|
|
#iq{type = get, xmlns = ?NS_DISCO_ITEMS = XMLNS,
|
|
sub_el = SubEl} = IQ ->
|
|
{xmlelement, _, QAttrs, _} = SubEl,
|
|
Node = xml:get_attr_s("node", QAttrs),
|
|
Res =
|
|
case iq_disco_items(Host, From, Node) of
|
|
{result, IQRes} ->
|
|
jlib:iq_to_xml(
|
|
IQ#iq{type = result,
|
|
sub_el = [{xmlelement, "query",
|
|
QAttrs,
|
|
IQRes}]});
|
|
{error, Error} ->
|
|
jlib:make_error_reply(
|
|
Packet, Error)
|
|
end,
|
|
ejabberd_router:route(To, From, Res);
|
|
#iq{type = Type, xmlns = ?NS_PUBSUB = XMLNS,
|
|
sub_el = SubEl} = IQ ->
|
|
Res =
|
|
case iq_pubsub(Host, ServerHost, From, Type, SubEl, Access) of
|
|
{result, IQRes} ->
|
|
jlib:iq_to_xml(
|
|
IQ#iq{type = result,
|
|
sub_el = IQRes});
|
|
{error, Error} ->
|
|
jlib:make_error_reply(
|
|
Packet, Error)
|
|
end,
|
|
ejabberd_router:route(To, From, Res);
|
|
#iq{type = Type, xmlns = ?NS_PUBSUB_OWNER = XMLNS,
|
|
lang = Lang, sub_el = SubEl} = IQ ->
|
|
Res =
|
|
case iq_pubsub_owner(
|
|
Host, From, Type, Lang, SubEl) of
|
|
{result, IQRes} ->
|
|
jlib:iq_to_xml(
|
|
IQ#iq{type = result,
|
|
sub_el = IQRes});
|
|
{error, Error} ->
|
|
jlib:make_error_reply(
|
|
Packet, Error)
|
|
end,
|
|
ejabberd_router:route(To, From, Res);
|
|
#iq{type = get, xmlns = ?NS_VCARD = XMLNS,
|
|
lang = Lang, sub_el = SubEl} = IQ ->
|
|
Res = IQ#iq{type = result,
|
|
sub_el = [{xmlelement, "vCard",
|
|
[{"xmlns", XMLNS}],
|
|
iq_get_vcard(Lang)}]},
|
|
ejabberd_router:route(To,
|
|
From,
|
|
jlib:iq_to_xml(Res));
|
|
#iq{} ->
|
|
Err = jlib:make_error_reply(
|
|
Packet,
|
|
?ERR_FEATURE_NOT_IMPLEMENTED),
|
|
ejabberd_router:route(To, From, Err);
|
|
_ ->
|
|
ok
|
|
end;
|
|
"presence" ->
|
|
Type = xml:get_attr_s("type", Attrs),
|
|
if
|
|
(Type == "unavailable") or (Type == "error") ->
|
|
ets:delete(
|
|
gen_mod:get_module_proc(Host, pubsub_presence),
|
|
{From#jid.luser, From#jid.lserver});
|
|
true ->
|
|
ets:insert(
|
|
gen_mod:get_module_proc(Host, pubsub_presence),
|
|
{{From#jid.luser, From#jid.lserver}, []})
|
|
end,
|
|
ok;
|
|
_ ->
|
|
ok
|
|
end;
|
|
_ ->
|
|
case xml:get_attr_s("type", Attrs) of
|
|
"error" ->
|
|
ok;
|
|
"result" ->
|
|
ok;
|
|
_ ->
|
|
Err = jlib:make_error_reply(
|
|
Packet, ?ERR_ITEM_NOT_FOUND),
|
|
ejabberd_router:route(To, From, Err)
|
|
end
|
|
end.
|
|
|
|
|
|
|
|
node_to_string(Node) ->
|
|
string:strip(lists:flatten(lists:map(fun(S) -> [S, "/"] end, Node)),
|
|
right, $/).
|
|
|
|
|
|
iq_disco_info(SNode) ->
|
|
Node = string:tokens(SNode, "/"),
|
|
case Node of
|
|
[] ->
|
|
[{xmlelement, "identity",
|
|
[{"category", "pubsub"},
|
|
{"type", "generic"},
|
|
{"name", "Publish-Subscribe"}], []},
|
|
{xmlelement, "feature", [{"var", ?NS_PUBSUB}], []},
|
|
{xmlelement, "feature", [{"var", ?NS_PUBSUB_EVENT}], []},
|
|
{xmlelement, "feature", [{"var", ?NS_PUBSUB_OWNER}], []},
|
|
{xmlelement, "feature", [{"var", ?NS_VCARD}], []}];
|
|
_ ->
|
|
% TODO
|
|
[]
|
|
end.
|
|
|
|
iq_disco_items(Host, From, SNode) ->
|
|
{Node,ItemID} = case SNode of
|
|
[] ->
|
|
{[],none};
|
|
_ ->
|
|
Tokens = string:tokens(SNode, "!"),
|
|
NodeList = string:tokens(lists:nth(1, Tokens), "/"),
|
|
ItemName = case length(Tokens) of
|
|
2 -> lists:nth(2, Tokens);
|
|
_ -> none
|
|
end,
|
|
{NodeList, ItemName}
|
|
end,
|
|
NodeFull = string:tokens(SNode,"/"),
|
|
F = fun() ->
|
|
case mnesia:read({pubsub_node, {Host, Node}}) of
|
|
[#pubsub_node{info = Info}] ->
|
|
case ItemID of
|
|
none ->
|
|
SubNodes = mnesia:index_read(pubsub_node,
|
|
{Host, Node},
|
|
#pubsub_node.host_parent),
|
|
SubItems = lists:map(fun(#pubsub_node{host_node = {_, N}}) ->
|
|
SN = node_to_string(N),
|
|
{xmlelement, "item",
|
|
[{"jid", Host},
|
|
{"node", SN},
|
|
{"name", lists:last(N)}], []}
|
|
end, SubNodes),
|
|
SN = node_to_string(Node),
|
|
Items = lists:map(fun(#item{id = Name}) ->
|
|
RealName = case Name of
|
|
[] -> "item";
|
|
_ -> Name
|
|
end,
|
|
{xmlelement, "item",
|
|
[{"jid", Host},
|
|
{"node", SN ++ "!" ++ Name},
|
|
{"name", RealName}], []}
|
|
end, Info#nodeinfo.items),
|
|
SubItems ++ Items;
|
|
_ ->
|
|
[]
|
|
end;
|
|
[] ->
|
|
case Node of
|
|
[] ->
|
|
SubNodes = mnesia:index_read(
|
|
pubsub_node,
|
|
{Host, Node},
|
|
#pubsub_node.host_parent),
|
|
lists:map(
|
|
fun(#pubsub_node{host_node = {_, N}}) ->
|
|
SN = node_to_string(N),
|
|
{xmlelement, "item",
|
|
[{"jid", Host},
|
|
{"node", SN},
|
|
{"name", lists:last(N)}],
|
|
[]}
|
|
end, SubNodes) ;
|
|
_ ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end
|
|
end
|
|
end,
|
|
case mnesia:transaction(F) of
|
|
{atomic, {error, _} = Error} ->
|
|
Error;
|
|
{atomic, Res} ->
|
|
{result, Res};
|
|
_ ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end.
|
|
|
|
iq_get_vcard(Lang) ->
|
|
[{xmlelement, "FN", [],
|
|
[{xmlcdata, "ejabberd/mod_pubsub"}]},
|
|
{xmlelement, "URL", [],
|
|
[{xmlcdata,
|
|
"http://ejabberd.jabberstudio.org/"}]},
|
|
{xmlelement, "DESC", [],
|
|
[{xmlcdata, translate:translate(
|
|
Lang,
|
|
"ejabberd pub/sub module\n"
|
|
"Copyright (c) 2003-2006 Alexey Shchepin")}]}].
|
|
|
|
|
|
iq_pubsub(Host, ServerHost, From, Type, SubEl, Access) ->
|
|
{xmlelement, _, _, SubEls} = SubEl,
|
|
case xml:remove_cdata(SubEls) of
|
|
[{xmlelement, Name, Attrs, Els}] ->
|
|
SNode = xml:get_attr_s("node", Attrs),
|
|
Node = string:tokens(SNode, "/"),
|
|
case {Type, Name} of
|
|
{set, "create"} ->
|
|
create_new_node(Host, Node, From, ServerHost, Access);
|
|
{set, "publish"} ->
|
|
case xml:remove_cdata(Els) of
|
|
[{xmlelement, "item", ItemAttrs, Payload}] ->
|
|
ItemID = xml:get_attr_s("id", ItemAttrs),
|
|
publish_item(Host, From, Node, ItemID, Payload);
|
|
_ ->
|
|
{error, ?ERR_BAD_REQUEST}
|
|
end;
|
|
{set, "retract"} ->
|
|
case xml:remove_cdata(Els) of
|
|
[{xmlelement, "item", ItemAttrs, _}] ->
|
|
ItemID = xml:get_attr_s("id", ItemAttrs),
|
|
delete_item(Host, From, Node, ItemID);
|
|
_ ->
|
|
{error, ?ERR_BAD_REQUEST}
|
|
end;
|
|
{set, "subscribe"} ->
|
|
JID = xml:get_attr_s("jid", Attrs),
|
|
subscribe_node(Host, From, JID, Node);
|
|
{set, "unsubscribe"} ->
|
|
JID = xml:get_attr_s("jid", Attrs),
|
|
unsubscribe_node(Host, From, JID, Node);
|
|
{get, "items"} ->
|
|
MaxItems = xml:get_attr_s("max_items", Attrs),
|
|
get_items(Host, From, Node, MaxItems);
|
|
{set, "delete"} ->
|
|
delete_node(Host, From, Node);
|
|
{set, "purge"} ->
|
|
purge_node(Host, From, Node);
|
|
{get, "entities"} ->
|
|
get_entities(Host, From, Node);
|
|
{set, "entities"} ->
|
|
set_entities(Host, From, Node, xml:remove_cdata(Els));
|
|
{get, "affiliations"} ->
|
|
get_affiliations(Host, From);
|
|
_ ->
|
|
{error, ?ERR_FEATURE_NOT_IMPLEMENTED}
|
|
end;
|
|
_ ->
|
|
{error, ?ERR_BAD_REQUEST}
|
|
end.
|
|
|
|
|
|
-define(XFIELD(Type, Label, Var, Val),
|
|
{xmlelement, "field", [{"type", Type},
|
|
{"label", translate:translate(Lang, Label)},
|
|
{"var", Var}],
|
|
[{xmlelement, "value", [], [{xmlcdata, Val}]}]}).
|
|
|
|
-define(BOOLXFIELD(Label, Var, Val),
|
|
?XFIELD("boolean", Label, Var,
|
|
case Val of
|
|
true -> "1";
|
|
_ -> "0"
|
|
end)).
|
|
|
|
-define(STRINGXFIELD(Label, Var, Val),
|
|
?XFIELD("text-single", Label, Var, Val)).
|
|
|
|
-define(XFIELDOPT(Type, Label, Var, Val, Opts),
|
|
{xmlelement, "field", [{"type", Type},
|
|
{"label", translate:translate(Lang, Label)},
|
|
{"var", Var}],
|
|
lists:map(fun(Opt) ->
|
|
{xmlelement, "option", [],
|
|
[{xmlelement, "value", [],
|
|
[{xmlcdata, Opt}]}]}
|
|
end, Opts) ++
|
|
[{xmlelement, "value", [], [{xmlcdata, Val}]}]}).
|
|
|
|
-define(LISTXFIELD(Label, Var, Val, Opts),
|
|
?XFIELDOPT("list-single", Label, Var, Val, Opts)).
|
|
|
|
|
|
|
|
%% Create new pubsub nodes
|
|
%% This function is used during init to create the first bootstrap nodes
|
|
create_new_node(Host, Node, Owner) ->
|
|
%% This is the case use during "bootstrapping to create the initial
|
|
%% hierarchy. Should always be ... undefined,all
|
|
create_new_node(Host, Node, Owner, undefined, all).
|
|
create_new_node(Host, Node, Owner, ServerHost, Access) ->
|
|
case Node of
|
|
[] ->
|
|
{LOU, LOS, _} = jlib:jid_tolower(Owner),
|
|
HomeNode = ["home", LOS, LOU],
|
|
create_new_node(Host, HomeNode, Owner, ServerHost, Access),
|
|
NewNode = ["home", LOS, LOU, randoms:get_string()],
|
|
create_new_node(Host, NewNode, Owner, ServerHost, Access);
|
|
_ ->
|
|
LOwner = jlib:jid_tolower(jlib:jid_remove_resource(Owner)),
|
|
Parent = lists:sublist(Node, length(Node) - 1),
|
|
F = fun() ->
|
|
ParentExists = (Parent == []) orelse
|
|
case mnesia:read({pubsub_node, {Host, Parent}}) of
|
|
[_] ->
|
|
true;
|
|
[] ->
|
|
false
|
|
end,
|
|
case ParentExists of
|
|
false ->
|
|
{error, ?ERR_CONFLICT};
|
|
_ ->
|
|
case mnesia:read({pubsub_node, {Host, Node}}) of
|
|
[_] ->
|
|
{error, ?ERR_CONFLICT};
|
|
[] ->
|
|
Entities =
|
|
?DICT:store(
|
|
LOwner,
|
|
#entity{affiliation = owner,
|
|
subscription = none},
|
|
?DICT:new()),
|
|
mnesia:write(
|
|
#pubsub_node{host_node = {Host, Node},
|
|
host_parent = {Host, Parent},
|
|
info = #nodeinfo{
|
|
entities = Entities}}),
|
|
ok
|
|
end
|
|
end
|
|
end,
|
|
case check_create_permission(Host, Node, Owner, ServerHost, Access) of
|
|
true ->
|
|
case mnesia:transaction(F) of
|
|
{atomic, ok} ->
|
|
Lang = "",
|
|
broadcast_publish_item(
|
|
Host, ["pubsub", "nodes"], node_to_string(Node),
|
|
[{xmlelement, "x",
|
|
[{"xmlns", ?NS_XDATA},
|
|
{"type", "result"}],
|
|
[?XFIELD("hidden", "", "FORM_TYPE",
|
|
?NS_PUBSUB_NMI),
|
|
?XFIELD("jid-single", "Node Creator",
|
|
"creator",
|
|
jlib:jid_to_string(LOwner))]}]),
|
|
{result,
|
|
[{xmlelement, "pubsub",
|
|
[{"xmlns", ?NS_PUBSUB}],
|
|
[{xmlelement, "create",
|
|
[{"node", node_to_string(Node)}], []}]}]};
|
|
{atomic, {error, _} = Error} ->
|
|
Error;
|
|
_ ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end;
|
|
_ ->
|
|
{error, ?ERR_NOT_ALLOWED}
|
|
end
|
|
end.
|
|
|
|
|
|
publish_item(Host, JID, Node, ItemID, Payload) ->
|
|
ejabberd_hooks:run(pubsub_publish_item, Host,
|
|
[JID, ?MYJID, Node, ItemID, Payload]),
|
|
Publisher = jlib:jid_tolower(jlib:jid_remove_resource(JID)),
|
|
F = fun() ->
|
|
case mnesia:read({pubsub_node, {Host, Node}}) of
|
|
[#pubsub_node{info = Info} = N] ->
|
|
Affiliation = get_affiliation(Info, Publisher),
|
|
Subscription = get_subscription(Info, Publisher),
|
|
MaxSize = get_node_option(Info, max_payload_size),
|
|
Model = get_node_option(Info, publish_model),
|
|
Size = size(term_to_binary(Payload)),
|
|
if
|
|
((Model == open) or
|
|
((Model == publishers) and
|
|
((Affiliation == owner) or
|
|
(Affiliation == publisher))) or
|
|
((Model == subscribers) and
|
|
(Subscription == subscribed))) and
|
|
(Size =< MaxSize) ->
|
|
NewInfo =
|
|
insert_item(Info, ItemID,
|
|
Publisher, Payload),
|
|
mnesia:write(
|
|
N#pubsub_node{info = NewInfo}),
|
|
{result, []};
|
|
true ->
|
|
{error, ?ERR_NOT_ALLOWED}
|
|
end;
|
|
[] ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end
|
|
end,
|
|
case mnesia:transaction(F) of
|
|
{atomic, {error, _} = Error} ->
|
|
Error;
|
|
{atomic, {result, Res}} ->
|
|
broadcast_publish_item(Host, Node, ItemID, Payload),
|
|
{result, Res};
|
|
_ ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end.
|
|
|
|
|
|
delete_item(Host, JID, Node, ItemID) ->
|
|
Publisher = jlib:jid_tolower(jlib:jid_remove_resource(JID)),
|
|
F = fun() ->
|
|
case mnesia:read({pubsub_node, {Host, Node}}) of
|
|
[#pubsub_node{info = Info} = N] ->
|
|
case check_item_publisher(Info, ItemID, Publisher)
|
|
orelse
|
|
(get_affiliation(Info, Publisher) == owner) of
|
|
true ->
|
|
NewInfo =
|
|
remove_item(Info, ItemID),
|
|
mnesia:write(
|
|
N#pubsub_node{info = NewInfo}),
|
|
{result, []};
|
|
_ ->
|
|
{error, ?ERR_NOT_ALLOWED}
|
|
end;
|
|
[] ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end
|
|
end,
|
|
case mnesia:transaction(F) of
|
|
{atomic, {error, _} = Error} ->
|
|
Error;
|
|
{atomic, {result, Res}} ->
|
|
broadcast_retract_item(Host, Node, ItemID),
|
|
{result, Res};
|
|
_ ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end.
|
|
|
|
|
|
subscribe_node(Host, From, JID, Node) ->
|
|
Sender = jlib:jid_tolower(jlib:jid_remove_resource(From)),
|
|
SubscriberJID =
|
|
case jlib:string_to_jid(JID) of
|
|
error ->
|
|
{"", "", ""};
|
|
J ->
|
|
J
|
|
end,
|
|
Subscriber = jlib:jid_tolower(SubscriberJID),
|
|
SubscriberWithoutResource = jlib:jid_remove_resource(Subscriber),
|
|
F = fun() ->
|
|
case mnesia:read({pubsub_node, {Host, Node}}) of
|
|
[#pubsub_node{info = Info} = N] ->
|
|
Affiliation = get_affiliation(Info, Subscriber),
|
|
AllowSubscriptions = get_node_option(Info, subscribe),
|
|
if
|
|
AllowSubscriptions and
|
|
(Affiliation /= outcast) ->
|
|
NewInfo = add_subscriber(Info, Subscriber),
|
|
mnesia:write(N#pubsub_node{info = NewInfo}),
|
|
{result, [], Info};
|
|
true ->
|
|
{error, ?ERR_NOT_ALLOWED}
|
|
end;
|
|
[] ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end
|
|
end,
|
|
if
|
|
Sender == SubscriberWithoutResource ->
|
|
case mnesia:transaction(F) of
|
|
{atomic, {error, _} = Error} ->
|
|
Error;
|
|
{atomic, {result, Res, Info}} ->
|
|
case get_node_option(Info, send_item_subscribe) of
|
|
true ->
|
|
ItemsEls =
|
|
lists:map(
|
|
fun(#item{id = ItemID,
|
|
payload = Payload}) ->
|
|
ItemAttrs = case ItemID of
|
|
"" -> [];
|
|
_ -> [{"id", ItemID}]
|
|
end,
|
|
{xmlelement, "item",
|
|
ItemAttrs, Payload}
|
|
end, Info#nodeinfo.items),
|
|
Stanza =
|
|
{xmlelement, "message",
|
|
[],
|
|
[{xmlelement, "x",
|
|
[{"xmlns", ?NS_PUBSUB_EVENT}],
|
|
[{xmlelement, "items",
|
|
[{"node", node_to_string(Node)}],
|
|
ItemsEls}]}]},
|
|
ejabberd_router:route(
|
|
?MYJID, jlib:make_jid(Subscriber), Stanza);
|
|
false ->
|
|
ok
|
|
end,
|
|
{result, Res};
|
|
_ ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end;
|
|
true ->
|
|
{error, ?ERR_NOT_ALLOWED}
|
|
end.
|
|
|
|
|
|
unsubscribe_node(Host, From, JID, Node) ->
|
|
Sender = jlib:jid_tolower(jlib:jid_remove_resource(From)),
|
|
SubscriberJID =
|
|
case jlib:string_to_jid(JID) of
|
|
error ->
|
|
{"", "", ""};
|
|
J ->
|
|
J
|
|
end,
|
|
Subscriber = jlib:jid_tolower(SubscriberJID),
|
|
F = fun() ->
|
|
case mnesia:read({pubsub_node, {Host, Node}}) of
|
|
[#pubsub_node{info = Info} = N] ->
|
|
Subscription = get_subscription(Info, Subscriber),
|
|
if
|
|
Subscription /= none ->
|
|
NewInfo =
|
|
remove_subscriber(Info, Subscriber),
|
|
mnesia:write(
|
|
N#pubsub_node{info = NewInfo}),
|
|
{result, []};
|
|
true ->
|
|
{error, ?ERR_NOT_ALLOWED}
|
|
end;
|
|
[] ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end
|
|
end,
|
|
if
|
|
Sender == Subscriber ->
|
|
case mnesia:transaction(F) of
|
|
{atomic, {error, _} = Error} ->
|
|
Error;
|
|
{atomic, {result, Res}} ->
|
|
{result, Res};
|
|
_ ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end;
|
|
true ->
|
|
{error, ?ERR_NOT_ALLOWED}
|
|
end.
|
|
|
|
|
|
get_items(Host, JID, Node, SMaxItems) ->
|
|
MaxItems =
|
|
if
|
|
SMaxItems == "" ->
|
|
?MAXITEMS;
|
|
true ->
|
|
case catch list_to_integer(SMaxItems) of
|
|
{'EXIT', _} ->
|
|
{error, ?ERR_BAD_REQUEST};
|
|
Val ->
|
|
Val
|
|
end
|
|
end,
|
|
case MaxItems of
|
|
{error, _} = Error ->
|
|
Error;
|
|
_ ->
|
|
case catch mnesia:dirty_read(pubsub_node, {Host, Node}) of
|
|
[#pubsub_node{info = Info}] ->
|
|
Items = lists:sublist(Info#nodeinfo.items, MaxItems),
|
|
ItemsEls =
|
|
lists:map(
|
|
fun(#item{id = ItemID,
|
|
payload = Payload}) ->
|
|
ItemAttrs = case ItemID of
|
|
"" -> [];
|
|
_ -> [{"id", ItemID}]
|
|
end,
|
|
{xmlelement, "item", ItemAttrs, Payload}
|
|
end, Items),
|
|
{result, [{xmlelement, "pubsub",
|
|
[{"xmlns", ?NS_PUBSUB_EVENT}],
|
|
[{xmlelement, "items",
|
|
[{"node", node_to_string(Node)}],
|
|
ItemsEls}]}]};
|
|
_ ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end
|
|
end.
|
|
|
|
|
|
delete_node(Host, JID, Node) ->
|
|
Owner = jlib:jid_tolower(jlib:jid_remove_resource(JID)),
|
|
F = fun() ->
|
|
case mnesia:read({pubsub_node, {Host, Node}}) of
|
|
[#pubsub_node{info = Info}] ->
|
|
case get_affiliation(Info, Owner) of
|
|
owner ->
|
|
% TODO: don't iterate over entire table
|
|
Removed =
|
|
mnesia:foldl(
|
|
fun(#pubsub_node{host_node = {_, N},
|
|
info = NInfo}, Acc) ->
|
|
case lists:prefix(Node, N) of
|
|
true ->
|
|
[{N, NInfo} | Acc];
|
|
_ ->
|
|
Acc
|
|
end
|
|
end, [], pubsub_node),
|
|
lists:foreach(
|
|
fun({N, _}) ->
|
|
mnesia:delete({pubsub_node, {Host, N}})
|
|
end, Removed),
|
|
{removed, Removed};
|
|
_ ->
|
|
{error, ?ERR_NOT_ALLOWED}
|
|
end;
|
|
[] ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end
|
|
end,
|
|
case mnesia:transaction(F) of
|
|
{atomic, {error, _} = Error} ->
|
|
Error;
|
|
{atomic, {removed, Removed}} ->
|
|
broadcast_removed_node(Host, Removed),
|
|
Lang = "",
|
|
broadcast_retract_item(
|
|
Host, ["pubsub", "nodes"], node_to_string(Node)),
|
|
{result, []};
|
|
_ ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end.
|
|
|
|
|
|
purge_node(Host, JID, Node) ->
|
|
Owner = jlib:jid_tolower(jlib:jid_remove_resource(JID)),
|
|
F = fun() ->
|
|
case mnesia:read({pubsub_node, {Host, Node}}) of
|
|
[#pubsub_node{info = Info} = N] ->
|
|
case get_affiliation(Info, Owner) of
|
|
owner ->
|
|
NewInfo = Info#nodeinfo{items = []},
|
|
mnesia:write(
|
|
N#pubsub_node{info = NewInfo}),
|
|
{result, Info#nodeinfo.items, []};
|
|
_ ->
|
|
{error, ?ERR_NOT_ALLOWED}
|
|
end;
|
|
[] ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end
|
|
end,
|
|
case mnesia:transaction(F) of
|
|
{atomic, {error, _} = Error} ->
|
|
Error;
|
|
{atomic, {result, Items, Res}} ->
|
|
lists:foreach(
|
|
fun(#item{id = ItemID}) ->
|
|
broadcast_retract_item(Host, Node, ItemID)
|
|
end, Items),
|
|
{result, Res};
|
|
_ ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end.
|
|
|
|
|
|
get_entities(Host, OJID, Node) ->
|
|
Owner = jlib:jid_tolower(jlib:jid_remove_resource(OJID)),
|
|
case catch mnesia:dirty_read(pubsub_node, {Host, Node}) of
|
|
[#pubsub_node{info = Info}] ->
|
|
case get_affiliation(Info, Owner) of
|
|
owner ->
|
|
Entities = Info#nodeinfo.entities,
|
|
EntitiesEls =
|
|
?DICT:fold(
|
|
fun(JID,
|
|
#entity{affiliation = Affiliation,
|
|
subscription = Subscription},
|
|
Acc) ->
|
|
[{xmlelement, "entity",
|
|
[{"jid", jlib:jid_to_string(JID)},
|
|
{"affiliation",
|
|
affiliation_to_string(Affiliation)},
|
|
{"subscription",
|
|
subscription_to_string(Subscription)}],
|
|
[]} | Acc]
|
|
end, [], Entities),
|
|
{result, [{xmlelement, "pubsub",
|
|
[{"xmlns", ?NS_PUBSUB_EVENT}],
|
|
[{xmlelement, "entities",
|
|
[{"node", node_to_string(Node)}],
|
|
EntitiesEls}]}]};
|
|
_ ->
|
|
{error, ?ERR_NOT_ALLOWED}
|
|
end;
|
|
_ ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end.
|
|
|
|
|
|
set_entities(Host, OJID, Node, EntitiesEls) ->
|
|
Owner = jlib:jid_tolower(jlib:jid_remove_resource(OJID)),
|
|
Entities =
|
|
lists:foldl(
|
|
fun(El, Acc) ->
|
|
case Acc of
|
|
error ->
|
|
error;
|
|
_ ->
|
|
case El of
|
|
{xmlelement, "entity", Attrs, _} ->
|
|
JID = jlib:string_to_jid(
|
|
xml:get_attr_s("jid", Attrs)),
|
|
Affiliation =
|
|
case xml:get_attr_s("affiliation",
|
|
Attrs) of
|
|
"owner" -> owner;
|
|
"publisher" -> publisher;
|
|
"outcast" -> outcast;
|
|
"none" -> none;
|
|
_ -> false
|
|
end,
|
|
Subscription =
|
|
case xml:get_attr_s("subscription",
|
|
Attrs) of
|
|
"subscribed" -> subscribed;
|
|
"pending" -> pending;
|
|
"unconfigured" -> unconfigured;
|
|
"none" -> none;
|
|
_ -> false
|
|
end,
|
|
if
|
|
(JID == error) or
|
|
(Affiliation == false) or
|
|
(Subscription == false) ->
|
|
error;
|
|
true ->
|
|
[{jlib:jid_tolower(JID),
|
|
#entity{
|
|
affiliation = Affiliation,
|
|
subscription = Subscription}} |
|
|
Acc]
|
|
end
|
|
end
|
|
end
|
|
end, [], EntitiesEls),
|
|
case Entities of
|
|
error ->
|
|
{error, ?ERR_BAD_REQUEST};
|
|
_ ->
|
|
F = fun() ->
|
|
case mnesia:read({pubsub_node, {Host, Node}}) of
|
|
[#pubsub_node{info = Info} = N] ->
|
|
case get_affiliation(Info, Owner) of
|
|
owner ->
|
|
NewInfo =
|
|
set_info_entities(Info, Entities),
|
|
mnesia:write(
|
|
N#pubsub_node{info = NewInfo}),
|
|
{result, []};
|
|
_ ->
|
|
{error, ?ERR_NOT_ALLOWED}
|
|
end;
|
|
[] ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end
|
|
end,
|
|
case mnesia:transaction(F) of
|
|
{atomic, {error, _} = Error} ->
|
|
Error;
|
|
{atomic, {result, _}} ->
|
|
{result, []};
|
|
_ ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end
|
|
end.
|
|
|
|
|
|
get_affiliations(Host, JID) ->
|
|
LJID = jlib:jid_tolower(jlib:jid_remove_resource(JID)),
|
|
case catch mnesia:dirty_select(
|
|
pubsub_node,
|
|
[{#pubsub_node{_ = '_'},
|
|
[],
|
|
['$_']}]) of
|
|
{'EXIT', _} ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR};
|
|
Nodes ->
|
|
Entities =
|
|
lists:flatmap(
|
|
fun(#pubsub_node{host_node = {H, Node}, info = Info})
|
|
when H == Host ->
|
|
Affiliation = get_affiliation(Info, LJID),
|
|
Subscription = get_subscription(Info, LJID),
|
|
if
|
|
(Affiliation /= none) or
|
|
(Subscription /= none) ->
|
|
[{xmlelement, "entity",
|
|
[{"node", node_to_string(Node)},
|
|
{"jid", jlib:jid_to_string(JID)},
|
|
{"affiliation",
|
|
affiliation_to_string(Affiliation)},
|
|
{"subscription",
|
|
subscription_to_string(Subscription)}],
|
|
[]}];
|
|
true ->
|
|
[]
|
|
end;
|
|
(_) ->
|
|
[]
|
|
end, Nodes),
|
|
{result, [{xmlelement, "pubsub",
|
|
[{"xmlns", ?NS_PUBSUB_EVENT}],
|
|
[{xmlelement, "affiliations", [],
|
|
Entities}]}]}
|
|
end.
|
|
|
|
|
|
|
|
|
|
get_affiliation(#nodeinfo{entities = Entities}, JID) ->
|
|
LJID = jlib:jid_tolower(jlib:jid_remove_resource(JID)),
|
|
case ?DICT:find(LJID, Entities) of
|
|
{ok, #entity{affiliation = Affiliation}} ->
|
|
Affiliation;
|
|
_ ->
|
|
none
|
|
end.
|
|
|
|
get_subscription(#nodeinfo{entities = Entities}, JID) ->
|
|
LJID = jlib:jid_tolower(jlib:jid_remove_resource(JID)),
|
|
case ?DICT:find(LJID, Entities) of
|
|
{ok, #entity{subscription = Subscription}} ->
|
|
Subscription;
|
|
_ ->
|
|
none
|
|
end.
|
|
|
|
affiliation_to_string(Affiliation) ->
|
|
case Affiliation of
|
|
owner -> "owner";
|
|
publisher -> "publisher";
|
|
outcast -> "outcast";
|
|
_ -> "none"
|
|
end.
|
|
|
|
subscription_to_string(Subscription) ->
|
|
case Subscription of
|
|
subscribed -> "subscribed";
|
|
pending -> "pending";
|
|
unconfigured -> "unconfigured";
|
|
_ -> "none"
|
|
end.
|
|
|
|
|
|
check_create_permission(Host, Node, Owner, ServerHost, Access) ->
|
|
#jid{luser = User, lserver = Server, lresource = Resource} = Owner,
|
|
case acl:match_rule(ServerHost, Access, {User, Server, Resource}) of
|
|
allow ->
|
|
if Server == Host ->
|
|
true;
|
|
true ->
|
|
case Node of
|
|
["home", Server, User | _] ->
|
|
true;
|
|
_ ->
|
|
false
|
|
end
|
|
end;
|
|
_ ->
|
|
case Owner of
|
|
?MYJID ->
|
|
true;
|
|
_ ->
|
|
false
|
|
end
|
|
end.
|
|
|
|
insert_item(Info, ItemID, Publisher, Payload) ->
|
|
Items = Info#nodeinfo.items,
|
|
Items1 = lists:filter(fun(I) ->
|
|
I#item.id /= ItemID
|
|
end, Items),
|
|
Items2 = [#item{id = ItemID, publisher = Publisher, payload = Payload} |
|
|
Items1],
|
|
Items3 = lists:sublist(Items2, get_max_items(Info)),
|
|
Info#nodeinfo{items = Items3}.
|
|
|
|
remove_item(Info, ItemID) ->
|
|
Items = Info#nodeinfo.items,
|
|
Items1 = lists:filter(fun(I) ->
|
|
I#item.id /= ItemID
|
|
end, Items),
|
|
Info#nodeinfo{items = Items1}.
|
|
|
|
check_item_publisher(Info, ItemID, Publisher) ->
|
|
Items = Info#nodeinfo.items,
|
|
case lists:keysearch(ItemID, #item.id, Items) of
|
|
{value, #item{publisher = Publisher}} ->
|
|
true;
|
|
_ ->
|
|
false
|
|
end.
|
|
|
|
add_subscriber(Info, Subscriber) ->
|
|
Entities = Info#nodeinfo.entities,
|
|
case ?DICT:find(Subscriber, Entities) of
|
|
{ok, Entity} ->
|
|
Info#nodeinfo{
|
|
entities = ?DICT:store(Subscriber,
|
|
Entity#entity{subscription = subscribed},
|
|
Entities)};
|
|
_ ->
|
|
Info#nodeinfo{
|
|
entities = ?DICT:store(Subscriber,
|
|
#entity{subscription = subscribed},
|
|
Entities)}
|
|
end.
|
|
|
|
remove_subscriber(Info, Subscriber) ->
|
|
Entities = Info#nodeinfo.entities,
|
|
case ?DICT:find(Subscriber, Entities) of
|
|
{ok, #entity{affiliation = none}} ->
|
|
Info#nodeinfo{
|
|
entities = ?DICT:erase(Subscriber, Entities)};
|
|
{ok, Entity} ->
|
|
Info#nodeinfo{
|
|
entities = ?DICT:store(Subscriber,
|
|
Entity#entity{subscription = none},
|
|
Entities)};
|
|
_ ->
|
|
Info
|
|
end.
|
|
|
|
|
|
set_info_entities(Info, Entities) ->
|
|
NewEntities =
|
|
lists:foldl(
|
|
fun({JID, Ent}, Es) ->
|
|
case Ent of
|
|
#entity{affiliation = none, subscription = none} ->
|
|
?DICT:erase(JID, Es);
|
|
_ ->
|
|
?DICT:store(JID, Ent, Es)
|
|
end
|
|
end, Info#nodeinfo.entities, Entities),
|
|
Info#nodeinfo{entities = NewEntities}.
|
|
|
|
|
|
|
|
broadcast_publish_item(Host, Node, ItemID, Payload) ->
|
|
case catch mnesia:dirty_read(pubsub_node, {Host, Node}) of
|
|
[#pubsub_node{info = Info}] ->
|
|
?DICT:fold(
|
|
fun(JID, #entity{subscription = Subscription}, _) ->
|
|
Present = case get_node_option(
|
|
Info, presence_based_delivery) of
|
|
true ->
|
|
case ets:lookup(
|
|
gen_mod:get_module_proc(Host, pubsub_presence),
|
|
{element(1, JID),
|
|
element(2, JID)}) of
|
|
[_] ->
|
|
true;
|
|
[] ->
|
|
false
|
|
end;
|
|
false ->
|
|
true
|
|
end,
|
|
if
|
|
(Subscription /= none) and
|
|
(Subscription /= pending) and
|
|
Present ->
|
|
ItemAttrs = case ItemID of
|
|
"" -> [];
|
|
_ -> [{"id", ItemID}]
|
|
end,
|
|
Content = case get_node_option(
|
|
Info, deliver_payloads) of
|
|
true ->
|
|
Payload;
|
|
false ->
|
|
[]
|
|
end,
|
|
Stanza =
|
|
{xmlelement, "message", [],
|
|
[{xmlelement, "event",
|
|
[{"xmlns", ?NS_PUBSUB_EVENT}],
|
|
[{xmlelement, "items",
|
|
[{"node", node_to_string(Node)}],
|
|
[{xmlelement, "item",
|
|
ItemAttrs,
|
|
Content}]}]}]},
|
|
ejabberd_router:route(
|
|
?MYJID, jlib:make_jid(JID), Stanza);
|
|
true ->
|
|
ok
|
|
end
|
|
end, ok, Info#nodeinfo.entities);
|
|
_ ->
|
|
false
|
|
end.
|
|
|
|
|
|
broadcast_retract_item(Host, Node, ItemID) ->
|
|
case catch mnesia:dirty_read(pubsub_node, {Host, Node}) of
|
|
[#pubsub_node{info = Info}] ->
|
|
case get_node_option(Info, notify_retract) of
|
|
true ->
|
|
?DICT:fold(
|
|
fun(JID, #entity{subscription = Subscription}, _) ->
|
|
if
|
|
(Subscription /= none) and
|
|
(Subscription /= pending) ->
|
|
ItemAttrs = case ItemID of
|
|
"" -> [];
|
|
_ -> [{"id", ItemID}]
|
|
end,
|
|
Stanza =
|
|
{xmlelement, "message", [],
|
|
[{xmlelement, "x",
|
|
[{"xmlns", ?NS_PUBSUB_EVENT}],
|
|
[{xmlelement, "items",
|
|
[{"node", node_to_string(Node)}],
|
|
[{xmlelement, "retract",
|
|
ItemAttrs, []}]}]}]},
|
|
ejabberd_router:route(
|
|
?MYJID, jlib:make_jid(JID), Stanza);
|
|
true ->
|
|
ok
|
|
end
|
|
end, ok, Info#nodeinfo.entities);
|
|
false ->
|
|
ok
|
|
end;
|
|
_ ->
|
|
false
|
|
end.
|
|
|
|
|
|
broadcast_removed_node(Host, Removed) ->
|
|
lists:foreach(
|
|
fun({Node, Info}) ->
|
|
case get_node_option(Info, notify_delete) of
|
|
true ->
|
|
Entities = Info#nodeinfo.entities,
|
|
?DICT:fold(
|
|
fun(JID, #entity{subscription = Subscription}, _) ->
|
|
if
|
|
(Subscription /= none) and
|
|
(Subscription /= pending) ->
|
|
Stanza =
|
|
{xmlelement, "message", [],
|
|
[{xmlelement, "x",
|
|
[{"xmlns", ?NS_PUBSUB_EVENT}],
|
|
[{xmlelement, "delete",
|
|
[{"node", node_to_string(Node)}],
|
|
[]}]}]},
|
|
ejabberd_router:route(
|
|
?MYJID, jlib:make_jid(JID), Stanza);
|
|
true ->
|
|
ok
|
|
end
|
|
end, ok, Entities);
|
|
false ->
|
|
ok
|
|
end
|
|
end, Removed).
|
|
|
|
|
|
broadcast_config_notification(Host, Node, Lang) ->
|
|
case catch mnesia:dirty_read(pubsub_node, {Host, Node}) of
|
|
[#pubsub_node{info = Info}] ->
|
|
case get_node_option(Info, notify_config) of
|
|
true ->
|
|
?DICT:fold(
|
|
fun(JID, #entity{subscription = Subscription}, _) ->
|
|
Present = case get_node_option(
|
|
Info, presence_based_delivery) of
|
|
true ->
|
|
case ets:lookup(
|
|
gen_mod:get_module_proc(Host, pubsub_presence),
|
|
{element(1, JID),
|
|
element(2, JID)}) of
|
|
[_] ->
|
|
true;
|
|
[] ->
|
|
false
|
|
end;
|
|
false ->
|
|
true
|
|
end,
|
|
if
|
|
(Subscription /= none) and
|
|
(Subscription /= pending) and
|
|
Present ->
|
|
Fields = get_node_config_xfields(
|
|
Node, Info, Lang),
|
|
Content = case get_node_option(
|
|
Info, deliver_payloads) of
|
|
true ->
|
|
[{xmlelement, "x",
|
|
[{"xmlns", ?NS_XDATA},
|
|
{"type", "form"}],
|
|
Fields}];
|
|
false ->
|
|
[]
|
|
end,
|
|
Stanza =
|
|
{xmlelement, "message", [],
|
|
[{xmlelement, "x",
|
|
[{"xmlns", ?NS_PUBSUB_EVENT}],
|
|
[{xmlelement, "items",
|
|
[{"node", node_to_string(Node)}],
|
|
[{xmlelement, "item",
|
|
[{"id", "configuration"}],
|
|
Content}]}]}]},
|
|
ejabberd_router:route(
|
|
?MYJID, jlib:make_jid(JID), Stanza);
|
|
true ->
|
|
ok
|
|
end
|
|
end, ok, Info#nodeinfo.entities);
|
|
false ->
|
|
ok
|
|
end;
|
|
_ ->
|
|
false
|
|
end.
|
|
|
|
|
|
|
|
iq_pubsub_owner(Host, From, Type, Lang, SubEl) ->
|
|
{xmlelement, _, _, SubEls} = SubEl,
|
|
case xml:remove_cdata(SubEls) of
|
|
[{xmlelement, Name, Attrs, Els}] ->
|
|
SNode = xml:get_attr_s("node", Attrs),
|
|
Node = string:tokens(SNode, "/"),
|
|
case {Type, Name} of
|
|
{get, "configure"} ->
|
|
get_node_config(Host, From, Node, Lang);
|
|
{set, "configure"} ->
|
|
set_node_config(Host, From, Node, Els, Lang);
|
|
_ ->
|
|
{error, ?ERR_FEATURE_NOT_IMPLEMENTED}
|
|
end;
|
|
_ ->
|
|
{error, ?ERR_BAD_REQUEST}
|
|
end.
|
|
|
|
get_node_config(Host, From, Node, Lang) ->
|
|
case catch mnesia:dirty_read(pubsub_node, {Host, Node}) of
|
|
[#pubsub_node{info = Info}] ->
|
|
case get_affiliation(Info, From) of
|
|
owner ->
|
|
Fields = get_node_config_xfields(Node, Info, Lang),
|
|
{result, [{xmlelement, "pubsub",
|
|
[{"xmlns", ?NS_PUBSUB_OWNER}],
|
|
[{xmlelement, "configure",
|
|
[{"node", node_to_string(Node)}],
|
|
[{xmlelement, "x", [{"xmlns", ?NS_XDATA},
|
|
{"type", "form"}],
|
|
Fields}]}]}]};
|
|
_ ->
|
|
{error, ?ERR_NOT_AUTHORIZED}
|
|
end;
|
|
_ ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end.
|
|
|
|
% TODO: move to jlib.hrl
|
|
-define(NS_PUBSUB_NODE_CONFIG, "http://jabber.org/protocol/pubsub#node_config").
|
|
|
|
-define(BOOL_CONFIG_FIELD(Label, Var),
|
|
?BOOLXFIELD(Label, "pubsub#" ++ atom_to_list(Var),
|
|
get_node_option(Info, Var))).
|
|
|
|
-define(STRING_CONFIG_FIELD(Label, Var),
|
|
?STRINGXFIELD(Label, "pubsub#" ++ atom_to_list(Var),
|
|
get_node_option(Info, Var))).
|
|
|
|
-define(INTEGER_CONFIG_FIELD(Label, Var),
|
|
?STRINGXFIELD(Label, "pubsub#" ++ atom_to_list(Var),
|
|
integer_to_list(get_node_option(Info, Var)))).
|
|
|
|
-define(JLIST_CONFIG_FIELD(Label, Var, Opts),
|
|
?LISTXFIELD(Label, "pubsub#" ++ atom_to_list(Var),
|
|
jlib:jid_to_string(get_node_option(Info, Var)),
|
|
[jlib:jid_to_string(O) || O <- Opts])).
|
|
|
|
-define(ALIST_CONFIG_FIELD(Label, Var, Opts),
|
|
?LISTXFIELD(Label, "pubsub#" ++ atom_to_list(Var),
|
|
atom_to_list(get_node_option(Info, Var)),
|
|
[atom_to_list(O) || O <- Opts])).
|
|
|
|
|
|
-define(DEFAULT_OPTIONS,
|
|
[{deliver_payloads, true},
|
|
{notify_config, false},
|
|
{notify_delete, false},
|
|
{notify_retract, true},
|
|
{persist_items, true},
|
|
{max_items, ?MAXITEMS div 2},
|
|
{subscribe, true},
|
|
{subscription_model, open},
|
|
{publish_model, publishers},
|
|
{max_payload_size, ?MAX_PAYLOAD_SIZE},
|
|
{send_item_subscribe, false},
|
|
{presence_based_delivery, false}]).
|
|
|
|
get_node_option(Info, current_approver) ->
|
|
Default = hd(get_owners_jids(Info)),
|
|
Options = Info#nodeinfo.options,
|
|
element(
|
|
2, element(2, lists:keysearch(
|
|
current_approver, 1,
|
|
Options ++ [{current_approver, Default}])));
|
|
get_node_option(#nodeinfo{options = Options}, Var) ->
|
|
element(
|
|
2, element(2, lists:keysearch(Var, 1, Options ++ ?DEFAULT_OPTIONS))).
|
|
|
|
get_max_items(Info) ->
|
|
case get_node_option(Info, persist_items) of
|
|
true ->
|
|
get_node_option(Info, max_items);
|
|
false ->
|
|
0
|
|
end.
|
|
|
|
get_owners_jids(Info) ->
|
|
Entities = Info#nodeinfo.entities,
|
|
Owners =
|
|
?DICT:fold(
|
|
fun(JID,
|
|
#entity{affiliation = Affiliation,
|
|
subscription = Subscription},
|
|
Acc) ->
|
|
case Affiliation of
|
|
owner ->
|
|
[JID | Acc];
|
|
_ ->
|
|
Acc
|
|
end
|
|
end, [], Entities),
|
|
lists:sort(Owners).
|
|
|
|
|
|
get_node_config_xfields(Node, Info, Lang) ->
|
|
[?XFIELD("hidden", "", "FORM_TYPE", ?NS_PUBSUB_NODE_CONFIG),
|
|
?BOOL_CONFIG_FIELD("Deliver payloads with event notifications", deliver_payloads),
|
|
?BOOL_CONFIG_FIELD("Notify subscribers when the node configuration changes", notify_config),
|
|
?BOOL_CONFIG_FIELD("Notify subscribers when the node is deleted", notify_delete),
|
|
?BOOL_CONFIG_FIELD("Notify subscribers when items are removed from the node", notify_retract),
|
|
?BOOL_CONFIG_FIELD("Persist items to storage", persist_items),
|
|
?INTEGER_CONFIG_FIELD("Max # of items to persist", max_items),
|
|
?BOOL_CONFIG_FIELD("Whether to allow subscriptions", subscribe),
|
|
?ALIST_CONFIG_FIELD("Specify the subscriber model", subscription_model,
|
|
[open]),
|
|
?ALIST_CONFIG_FIELD("Specify the publisher model", publish_model,
|
|
[publishers, subscribers, open]),
|
|
?INTEGER_CONFIG_FIELD("Max payload size in bytes", max_payload_size),
|
|
?BOOL_CONFIG_FIELD("Send items to new subscribers", send_item_subscribe),
|
|
?BOOL_CONFIG_FIELD("Only deliver notifications to available users", presence_based_delivery),
|
|
?JLIST_CONFIG_FIELD("Specify the current subscription approver", current_approver,
|
|
get_owners_jids(Info))
|
|
].
|
|
|
|
|
|
set_node_config(Host, From, Node, Els, Lang) ->
|
|
case catch mnesia:dirty_read(pubsub_node, {Host, Node}) of
|
|
[#pubsub_node{info = Info} = N] ->
|
|
case get_affiliation(Info, From) of
|
|
owner ->
|
|
case xml:remove_cdata(Els) of
|
|
[{xmlelement, "x", _Attrs1, _Els1} = XEl] ->
|
|
case {xml:get_tag_attr_s("xmlns", XEl),
|
|
xml:get_tag_attr_s("type", XEl)} of
|
|
{?NS_XDATA, "cancel"} ->
|
|
{result, []};
|
|
{?NS_XDATA, "submit"} ->
|
|
CurOpts = Info#nodeinfo.options,
|
|
set_node_config1(
|
|
Host, From, Node, XEl, CurOpts, Lang);
|
|
_ ->
|
|
{error, ?ERR_BAD_REQUEST}
|
|
end;
|
|
_ ->
|
|
{error, ?ERR_BAD_REQUEST}
|
|
end;
|
|
_ ->
|
|
{error, ?ERR_NOT_AUTHORIZED}
|
|
end;
|
|
_ ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end.
|
|
|
|
|
|
set_node_config1(Host, From, Node, XEl, CurOpts, Lang) ->
|
|
XData = jlib:parse_xdata_submit(XEl),
|
|
case XData of
|
|
invalid ->
|
|
{error, ?ERR_BAD_REQUEST};
|
|
_ ->
|
|
case set_xoption(XData, CurOpts) of
|
|
NewOpts when is_list(NewOpts) ->
|
|
change_node_opts(Host, NewOpts, Node, Lang);
|
|
Err ->
|
|
Err
|
|
end
|
|
end.
|
|
|
|
add_opt(Key, Value, Opts) ->
|
|
Opts1 = lists:keydelete(Key, 1, Opts),
|
|
[{Key, Value} | Opts1].
|
|
|
|
|
|
-define(SET_BOOL_XOPT(Opt, Val),
|
|
case Val of
|
|
"0" -> set_xoption(Opts, add_opt(Opt, false, NewOpts));
|
|
"1" -> set_xoption(Opts, add_opt(Opt, true, NewOpts));
|
|
_ -> {error, ?ERR_BAD_REQUEST}
|
|
end).
|
|
|
|
-define(SET_STRING_XOPT(Opt, Val),
|
|
set_xoption(Opts, add_opt(Opt, Val, NewOpts))).
|
|
|
|
-define(SET_INTEGER_XOPT(Opt, Val, Min, Max),
|
|
case catch list_to_integer(Val) of
|
|
IVal when is_integer(IVal),
|
|
IVal >= Min,
|
|
IVal =< Max ->
|
|
set_xoption(Opts, add_opt(Opt, IVal, NewOpts));
|
|
_ ->
|
|
{error, ?ERR_BAD_REQUEST}
|
|
end).
|
|
|
|
-define(SET_ALIST_XOPT(Opt, Val, Vals),
|
|
case lists:member(Val, [atom_to_list(V) || V <- Vals]) of
|
|
true ->
|
|
set_xoption(Opts, add_opt(Opt, list_to_atom(Val), NewOpts));
|
|
false ->
|
|
{error, ?ERR_BAD_REQUEST}
|
|
end).
|
|
|
|
|
|
set_xoption([], NewOpts) ->
|
|
NewOpts;
|
|
set_xoption([{"FORM_TYPE", _} | Opts], NewOpts) ->
|
|
set_xoption(Opts, NewOpts);
|
|
set_xoption([{"pubsub#deliver_payloads", [Val]} | Opts], NewOpts) ->
|
|
?SET_BOOL_XOPT(deliver_payloads, Val);
|
|
set_xoption([{"pubsub#notify_config", [Val]} | Opts], NewOpts) ->
|
|
?SET_BOOL_XOPT(notify_config, Val);
|
|
set_xoption([{"pubsub#notify_delete", [Val]} | Opts], NewOpts) ->
|
|
?SET_BOOL_XOPT(notify_delete, Val);
|
|
set_xoption([{"pubsub#notify_retract", [Val]} | Opts], NewOpts) ->
|
|
?SET_BOOL_XOPT(notify_retract, Val);
|
|
set_xoption([{"pubsub#persist_items", [Val]} | Opts], NewOpts) ->
|
|
?SET_BOOL_XOPT(persist_items, Val);
|
|
set_xoption([{"pubsub#max_items", [Val]} | Opts], NewOpts) ->
|
|
?SET_INTEGER_XOPT(max_items, Val, 0, ?MAXITEMS);
|
|
set_xoption([{"pubsub#subscribe", [Val]} | Opts], NewOpts) ->
|
|
?SET_BOOL_XOPT(subscribe, Val);
|
|
set_xoption([{"pubsub#subscription_model", [Val]} | Opts], NewOpts) ->
|
|
?SET_ALIST_XOPT(subscription_model, Val, [open]);
|
|
set_xoption([{"pubsub#publish_model", [Val]} | Opts], NewOpts) ->
|
|
?SET_ALIST_XOPT(publish_model, Val, [publishers, subscribers, open]);
|
|
set_xoption([{"pubsub#max_payload_size", [Val]} | Opts], NewOpts) ->
|
|
?SET_INTEGER_XOPT(max_payload_size, Val, 0, ?MAX_PAYLOAD_SIZE);
|
|
set_xoption([{"pubsub#send_item_subscribe", [Val]} | Opts], NewOpts) ->
|
|
?SET_BOOL_XOPT(send_item_subscribe, Val);
|
|
set_xoption([{"pubsub#presence_based_delivery", [Val]} | Opts], NewOpts) ->
|
|
?SET_BOOL_XOPT(presence_based_delivery, Val);
|
|
set_xoption([{"pubsub#current_approver", _} | Opts], NewOpts) ->
|
|
% TODO
|
|
set_xoption(Opts, NewOpts);
|
|
%set_xoption([{"title", [Val]} | Opts], NewOpts) ->
|
|
% ?SET_STRING_XOPT(title, Val);
|
|
set_xoption([_ | _Opts], _NewOpts) ->
|
|
{error, ?ERR_BAD_REQUEST}.
|
|
|
|
|
|
change_node_opts(Host, NewOpts, Node, Lang) ->
|
|
F = fun() ->
|
|
case mnesia:read({pubsub_node, {Host, Node}}) of
|
|
[#pubsub_node{info = Info} = N] ->
|
|
NewInfo = Info#nodeinfo{options = NewOpts},
|
|
mnesia:write(
|
|
N#pubsub_node{info = NewInfo}),
|
|
{result, []};
|
|
[] ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end
|
|
end,
|
|
case mnesia:transaction(F) of
|
|
{atomic, {error, _} = Error} ->
|
|
Error;
|
|
{atomic, {result, Res}} ->
|
|
broadcast_config_notification(Host, Node, Lang),
|
|
{result, Res};
|
|
_ ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end.
|
|
|
|
|
|
|
|
|
|
|
|
find_my_host(LServer) ->
|
|
Parts = string:tokens(LServer, "."),
|
|
find_my_host(Parts, ?MYHOSTS).
|
|
|
|
find_my_host([], _Hosts) ->
|
|
?MYNAME;
|
|
find_my_host([_ | Tail] = Parts, Hosts) ->
|
|
Domain = parts_to_string(Parts),
|
|
case lists:member(Domain, Hosts) of
|
|
true ->
|
|
Domain;
|
|
false ->
|
|
find_my_host(Tail, Hosts)
|
|
end.
|
|
|
|
parts_to_string(Parts) ->
|
|
string:strip(lists:flatten(lists:map(fun(S) -> [S, $.] end, Parts)),
|
|
right, $.).
|
|
|
|
|
|
|
|
update_table(Host) ->
|
|
Fields = record_info(fields, pubsub_node),
|
|
case mnesia:table_info(pubsub_node, attributes) of
|
|
Fields ->
|
|
ok;
|
|
[node, parent, info] ->
|
|
?INFO_MSG("Converting pubsub_node table from "
|
|
"{node, parent, info} format", []),
|
|
{atomic, ok} = mnesia:create_table(
|
|
mod_pubsub_tmp_table,
|
|
[{disc_only_copies, [node()]},
|
|
{type, bag},
|
|
{local_content, true},
|
|
{record_name, pubsub_node},
|
|
{attributes, record_info(fields, pubsub_node)}]),
|
|
mnesia:del_table_index(pubsub_node, parent),
|
|
mnesia:transform_table(pubsub_node, ignore, Fields),
|
|
F1 = fun() ->
|
|
mnesia:write_lock_table(mod_pubsub_tmp_table),
|
|
mnesia:foldl(
|
|
fun(#pubsub_node{host_node = N,
|
|
host_parent = P} = R, _) ->
|
|
mnesia:dirty_write(
|
|
mod_pubsub_tmp_table,
|
|
R#pubsub_node{host_node = {Host, N},
|
|
host_parent = {Host, P}})
|
|
end, ok, pubsub_node)
|
|
end,
|
|
mnesia:transaction(F1),
|
|
mnesia:clear_table(pubsub_node),
|
|
F2 = fun() ->
|
|
mnesia:write_lock_table(pubsub_node),
|
|
mnesia:foldl(
|
|
fun(R, _) ->
|
|
mnesia:dirty_write(R)
|
|
end, ok, mod_pubsub_tmp_table)
|
|
end,
|
|
mnesia:transaction(F2),
|
|
mnesia:delete_table(mod_pubsub_tmp_table);
|
|
_ ->
|
|
?INFO_MSG("Recreating pubsub_node table", []),
|
|
mnesia:transform_table(pubsub_node, ignore, Fields)
|
|
end.
|
|
|
|
|
|
|
|
|