diff --git a/ChangeLog b/ChangeLog index fb216e24c..ec0a32436 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,32 +1,44 @@ +2007-12-01 Alexey Shchepin + + * src/mod_caps.erl: CAPS support (thanks to Magnus Henoch) + * src/ejabberd_local.erl: Support for IQ responses + * src/jlib.erl: Added iq_query_or_response_info/1 function + * src/jlib.hrl: Added NS_PUBSUB_ERRORS and NS_CAPS + + * src/mod_pubsub/Makefile.in: New pubsub+pep implementation + (thanks to Christophe Romain and Magnus Henoch) + * src/ejabberd_sm.erl: Added get_session_pid/3 function + * src/ejabberd_c2s.erl: Added get_subscribed_and_online/1 function + 2007-11-30 Mickael Remond - * src/odbc_queries.erl: Added a default define value so that we can - recompile the file manually with a simple erlc command. + * src/odbc_queries.erl: Added a default define value so that we + can recompile the file manually with a simple erlc command. 2007-11-29 Badlop - * src/mod_vcard.erl: Add type of x:data field to search - results (thanks to Robin Redeker) (EJAB-327) - * src/mod_vcard_ldap.erl: - * src/mod_vcard_odbc.erl: + * src/mod_vcard.erl: Add type of x:data field to search results + (thanks to Robin Redeker) (EJAB-327) + * src/mod_vcard_ldap.erl: Likewise + * src/mod_vcard_odbc.erl: Likewise * src/aclocal.m4: Fix autoconf caching for SSL libraries (thanks to Michael Shields) (EJAB-439) * src/configure.ac: Don't hardcode gcc and gcc options in Makefiles (thanks to Etan Reisner) (EJAB-436) - * src/Makefile.in: - * src/ejabberd_zlib/Makefile.in: - * src/eldap/Makefile.in: - * src/mod_irc/Makefile.in: - * src/mod_muc/Makefile.in: - * src/mod_proxy65/Makefile.in: - * src/mod_pubsub/Makefile.in: - * src/odbc/Makefile.in: - * src/pam/Makefile.in: - * src/stringprep/Makefile.in: - * src/tls/Makefile.in: - * src/web/Makefile.in: + * src/Makefile.in: Likewise + * src/ejabberd_zlib/Makefile.in: Likewise + * src/eldap/Makefile.in: Likewise + * src/mod_irc/Makefile.in: Likewise + * src/mod_muc/Makefile.in: Likewise + * src/mod_proxy65/Makefile.in: Likewise + * src/mod_pubsub/Makefile.in: Likewise + * src/odbc/Makefile.in: Likewise + * src/pam/Makefile.in: Likewise + * src/stringprep/Makefile.in: Likewise + * src/tls/Makefile.in: Likewise + * src/web/Makefile.in: Likewise * src/mod_muc/mod_muc_room.erl: Hide the option 'Make room moderated' because it isn't implemented, and set the default value diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index f60699727..725351950 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -18,7 +18,8 @@ send_text/2, send_element/2, socket_type/0, - get_presence/1]). + get_presence/1, + get_subscribed_and_online/1]). %% gen_fsm callbacks -export([init/1, @@ -39,6 +40,7 @@ -include("jlib.hrl"). -define(SETS, gb_sets). +-define(DICT, dict). -record(state, {socket, sockmod, @@ -60,6 +62,7 @@ pres_f = ?SETS:new(), pres_a = ?SETS:new(), pres_i = ?SETS:new(), + pres_available = ?DICT:new(), pres_last, pres_pri, pres_timestamp, pres_invis = false, @@ -173,6 +176,12 @@ init([{SockMod, Socket}, Opts]) -> shaper = Shaper, ip = IP}, ?C2S_OPEN_TIMEOUT}. +%% Return list of all available resources of contacts, +%% in form [{JID, Caps}]. +get_subscribed_and_online(FsmRef) -> + gen_fsm:sync_send_all_state_event( + FsmRef, get_subscribed_and_online, 1000). + %%---------------------------------------------------------------------- %% Func: StateName/2 @@ -572,7 +581,7 @@ wait_for_feature_request({xmlstreamelement, El}, StateData) -> case xml:get_subtag(El, "method") of false -> send_element(StateData, - {xmlelement, "failure", + {xmlelement, "failure", [{"xmlns", ?NS_COMPRESS}], [{xmlelement, "setup-failed", [], []}]}), fsm_next_state(wait_for_feature_request, StateData); @@ -964,6 +973,17 @@ handle_sync_event({get_presence}, _From, StateName, StateData) -> Reply = {User, Resource, Show, Status}, fsm_reply(Reply, StateName, StateData); +handle_sync_event(get_subscribed_and_online, _From, StateName, StateData) -> + Subscribed = StateData#state.pres_f, + Online = StateData#state.pres_available, + Pred = fun(User, _Caps) -> + ?SETS:is_element(jlib:jid_remove_resource(User), + Subscribed) orelse + ?SETS:is_element(User, Subscribed) + end, + SubscribedAndOnline = ?DICT:filter(Pred, Online), + {reply, ?DICT:to_list(SubscribedAndOnline), StateName, StateData}; + handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, fsm_reply(Reply, StateName, StateData). @@ -1054,32 +1074,42 @@ handle_info({route, From, To, Packet}, StateName, StateData) -> allow -> LFrom = jlib:jid_tolower(From), LBFrom = jlib:jid_remove_resource(LFrom), + %% Note contact availability + Caps = mod_caps:read_caps(Els), + mod_caps:note_caps(StateData#state.server, From, Caps), + NewAvailable = case xml:get_attr_s("type", Attrs) of + "unavailable" -> + ?DICT:erase(LFrom, StateData#state.pres_available); + _ -> + ?DICT:store(LFrom, Caps, StateData#state.pres_available) + end, + NewStateData = StateData#state{pres_available = NewAvailable}, case ?SETS:is_element( - LFrom, StateData#state.pres_a) orelse + LFrom, NewStateData#state.pres_a) orelse ?SETS:is_element( - LBFrom, StateData#state.pres_a) of + LBFrom, NewStateData#state.pres_a) of true -> - {true, Attrs, StateData}; + {true, Attrs, NewStateData}; false -> case ?SETS:is_element( - LFrom, StateData#state.pres_f) of + LFrom, NewStateData#state.pres_f) of true -> A = ?SETS:add_element( LFrom, - StateData#state.pres_a), + NewStateData#state.pres_a), {true, Attrs, - StateData#state{pres_a = A}}; + NewStateData#state{pres_a = A}}; false -> case ?SETS:is_element( - LBFrom, StateData#state.pres_f) of + LBFrom, NewStateData#state.pres_f) of true -> A = ?SETS:add_element( LBFrom, - StateData#state.pres_a), + NewStateData#state.pres_a), {true, Attrs, - StateData#state{pres_a = A}}; + NewStateData#state{pres_a = A}}; false -> - {true, Attrs, StateData} + {true, Attrs, NewStateData} end end end; diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index afb5332a7..37655f39b 100644 --- a/src/ejabberd_local.erl +++ b/src/ejabberd_local.erl @@ -18,6 +18,7 @@ -export([route/3, register_iq_handler/4, register_iq_handler/5, + register_iq_response_handler/4, unregister_iq_handler/2, refresh_iq_handlers/0, bounce_resource_packet/3 @@ -32,6 +33,8 @@ -record(state, {}). +-record(iq_response, {id, module, function}). + -define(IQTABLE, local_iqtable). %%==================================================================== @@ -68,13 +71,38 @@ process_iq(From, To, Packet) -> ejabberd_router:route(To, From, Err) end; reply -> - ok; + process_iq_reply(From, To, Packet); _ -> Err = jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST), ejabberd_router:route(To, From, Err), ok end. +process_iq_reply(From, To, Packet) -> + IQ = jlib:iq_query_or_response_info(Packet), + #iq{id = ID} = IQ, + case catch mnesia:dirty_read(iq_response, ID) of + [] -> + ok; + _ -> + F = fun() -> + case mnesia:read({iq_response, ID}) of + [] -> + nothing; + [#iq_response{module = Module, + function = Function}] -> + mnesia:delete({iq_response, ID}), + {Module, Function} + end + end, + case mnesia:transaction(F) of + {atomic, {Module, Function}} -> + Module:Function(From, To, IQ); + _ -> + ok + end + end. + route(From, To, Packet) -> case catch do_route(From, To, Packet) of {'EXIT', Reason} -> @@ -84,6 +112,9 @@ route(From, To, Packet) -> ok end. +register_iq_response_handler(Host, ID, Module, Fun) -> + ejabberd_local ! {register_iq_response_handler, Host, ID, Module, Fun}. + register_iq_handler(Host, XMLNS, Module, Fun) -> ejabberd_local ! {register_iq_handler, Host, XMLNS, Module, Fun}. @@ -120,6 +151,9 @@ init([]) -> ?MODULE, bounce_resource_packet, 100) end, ?MYHOSTS), catch ets:new(?IQTABLE, [named_table, public]), + mnesia:create_table(iq_response, + [{ram_copies, [node()]}, + {attributes, record_info(fields, iq_response)}]), {ok, #state{}}. %%-------------------------------------------------------------------- @@ -159,6 +193,9 @@ handle_info({route, From, To, Packet}, State) -> ok end, {noreply, State}; +handle_info({register_iq_response_handler, _Host, ID, Module, Function}, State) -> + mnesia:dirty_write(#iq_response{id = ID, module = Module, function = Function}), + {noreply, State}; handle_info({register_iq_handler, Host, XMLNS, Module, Function}, State) -> ets:insert(?IQTABLE, {{XMLNS, Host}, Module, Function}), catch mod_disco:register_feature(Host, XMLNS), diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 051d18bdf..0904b7198 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -28,6 +28,7 @@ register_iq_handler/5, unregister_iq_handler/2, ctl_process/2, + get_session_pid/3, get_user_ip/3 ]). @@ -74,7 +75,7 @@ open_session(SID, User, Server, Resource, IP) -> close_session(SID, User, Server, Resource) -> F = fun() -> mnesia:delete({session, SID}) - end, + end, mnesia:sync_dirty(F), JID = jlib:make_jid(User, Server, Resource), ejabberd_hooks:run(sm_remove_connection_hook, JID#jid.lserver, @@ -139,6 +140,15 @@ close_session_unset_presence(SID, User, Server, Resource, Status) -> ejabberd_hooks:run(unset_presence_hook, jlib:nameprep(Server), [User, Server, Resource, Status]). +get_session_pid(User, Server, Resource) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + LResource = jlib:resourceprep(Resource), + USR = {LUser, LServer, LResource}, + case catch mnesia:dirty_index_read(session, USR, #session.usr) of + [#session{sid = {_, Pid}}] -> Pid; + _ -> none + end. dirty_get_sessions_list() -> mnesia:dirty_select( @@ -315,7 +325,7 @@ clean_table_from_bad_node(Node) -> lists:foreach(fun(E) -> mnesia:delete({session, E#session.sid}) end, Es) - end, + end, mnesia:sync_dirty(F). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -368,7 +378,6 @@ do_route(From, To, Packet) -> {true, false} end, if Pass -> - LFrom = jlib:jid_tolower(From), PResources = get_user_present_resources( LUser, LServer), lists:foreach( @@ -377,7 +386,9 @@ do_route(From, To, Packet) -> From, jlib:jid_replace_resource(To, R), Packet) - end, PResources); + end, PResources), + ejabberd_hooks:run(incoming_presence_hook, LServer, + [From, To, Packet]); true -> ok end; @@ -649,4 +660,3 @@ update_tables() -> false -> ok end. - diff --git a/src/jlib.erl b/src/jlib.erl index 74369cf22..ac54c51a8 100644 --- a/src/jlib.erl +++ b/src/jlib.erl @@ -32,6 +32,7 @@ jid_replace_resource/2, get_iq_namespace/1, iq_query_info/1, + iq_query_or_response_info/1, is_iq_request_type/1, iq_to_xml/1, parse_xdata_submit/1, @@ -331,39 +332,66 @@ get_iq_namespace({xmlelement, Name, _Attrs, Els}) when Name == "iq" -> get_iq_namespace(_) -> "". -iq_query_info({xmlelement, Name, Attrs, Els}) when Name == "iq" -> +iq_query_info(El) -> + iq_info_internal(El, request). + +iq_query_or_response_info(El) -> + iq_info_internal(El, any). + +iq_info_internal({xmlelement, Name, Attrs, Els}, Filter) when Name == "iq" -> + %% Filter is either request or any. If it is request, any replies + %% are converted to the atom reply. ID = xml:get_attr_s("id", Attrs), Type = xml:get_attr_s("type", Attrs), Lang = xml:get_attr_s("xml:lang", Attrs), - Type1 = case Type of - "set" -> set; - "get" -> get; - "result" -> reply; - "error" -> reply; - _ -> invalid + {Type1, Class} = case Type of + "set" -> {set, request}; + "get" -> {get, request}; + "result" -> {result, reply}; + "error" -> {error, reply}; + _ -> {invalid, invalid} end, if - (Type1 /= invalid) and (Type1 /= reply) -> - case xml:remove_cdata(Els) of - [{xmlelement, Name2, Attrs2, Els2}] -> - XMLNS = xml:get_attr_s("xmlns", Attrs2), - if - XMLNS /= "" -> + Type1 == invalid -> + invalid; + Class == request; Filter == any -> + %% The iq record is a bit strange. The sub_el field is an + %% XML tuple for requests, but a list of XML tuples for + %% responses. + FilteredEls = xml:remove_cdata(Els), + {XMLNS, SubEl} = + case {Class, FilteredEls} of + {request, [{xmlelement, _Name2, Attrs2, _Els2}]} -> + {xml:get_attr_s("xmlns", Attrs2), + hd(FilteredEls)}; + {reply, _} -> + %% Find the namespace of the first non-error + %% element, if there is one. + NonErrorEls = [El || + {xmlelement, SubName, _, _} = El + <- FilteredEls, + SubName /= "error"], + {case NonErrorEls of + [NonErrorEl] -> xml:get_tag_attr_s("xmlns", NonErrorEl); + _ -> invalid + end, + FilteredEls}; + _ -> + {invalid, invalid} + end, + if XMLNS == "", Class == request -> + invalid; + true -> #iq{id = ID, type = Type1, xmlns = XMLNS, lang = Lang, - sub_el = {xmlelement, Name2, Attrs2, Els2}}; - true -> - invalid + sub_el = SubEl} end; - _ -> - invalid - end; - true -> - Type1 + Class == reply, Filter /= any -> + reply end; -iq_query_info(_) -> +iq_info_internal(_, _) -> not_iq. is_iq_request_type(set) -> true; diff --git a/src/jlib.hrl b/src/jlib.hrl index 8034698eb..b83625ee0 100644 --- a/src/jlib.hrl +++ b/src/jlib.hrl @@ -33,6 +33,7 @@ -define(NS_PUBSUB_EVENT, "http://jabber.org/protocol/pubsub#event"). -define(NS_PUBSUB_OWNER, "http://jabber.org/protocol/pubsub#owner"). -define(NS_PUBSUB_NMI, "http://jabber.org/protocol/pubsub#node-meta-info"). +-define(NS_PUBSUB_ERRORS,"http://jabber.org/protocol/pubsub#errors"). -define(NS_COMMANDS, "http://jabber.org/protocol/commands"). -define(NS_BYTESTREAMS, "http://jabber.org/protocol/bytestreams"). -define(NS_ADMIN, "http://jabber.org/protocol/admin"). @@ -55,6 +56,8 @@ -define(NS_COMPRESS, "http://jabber.org/protocol/compress"). +-define(NS_CAPS, "http://jabber.org/protocol/caps"). + % TODO: remove "code" attribute (currently it used for backward-compatibility) -define(STANZA_ERROR(Code, Type, Condition), {xmlelement, "error", diff --git a/src/mod_caps.erl b/src/mod_caps.erl new file mode 100644 index 000000000..66afd10ab --- /dev/null +++ b/src/mod_caps.erl @@ -0,0 +1,259 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_caps.erl +%%% Author : Magnus Henoch +%%% Purpose : Request and cache Entity Capabilities (XEP-0115) +%%% Created : 7 Oct 2006 by Magnus Henoch +%%%---------------------------------------------------------------------- + +-module(mod_caps). +-author('henoch@dtek.chalmers.se'). + +-behaviour(gen_server). +-behaviour(gen_mod). + +-export([read_caps/1, + note_caps/3, + get_features/2, + handle_disco_response/3]). + +%% gen_mod callbacks +-export([start/2, start_link/2, + stop/1]). + +%% gen_server callbacks +-export([init/1, + handle_info/2, + handle_call/3, + handle_cast/2, + terminate/2, + code_change/3 + ]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-define(PROCNAME, ejabberd_mod_caps). +-define(DICT, dict). + +-record(caps, {node, version, exts}). +-record(caps_features, {node_pair, features}). +-record(state, {host, + disco_requests = ?DICT:new(), + feature_queries = []}). + +%% read_caps takes a list of XML elements (the child elements of a +%% stanza) and returns an opaque value representing the +%% Entity Capabilities contained therein, or the atom nothing if no +%% capabilities are advertised. +read_caps([{xmlelement, "c", Attrs, _Els} | Tail]) -> + case xml:get_attr_s("xmlns", Attrs) of + ?NS_CAPS -> + Node = xml:get_attr_s("node", Attrs), + Version = xml:get_attr_s("ver", Attrs), + Exts = string:tokens(xml:get_attr_s("ext", Attrs), " "), + #caps{node = Node, version = Version, exts = Exts}; + _ -> + read_caps(Tail) + end; +read_caps([_ | Tail]) -> + read_caps(Tail); +read_caps([]) -> + nothing. + +%% note_caps should be called to make the module request disco +%% information. Host is the host that asks, From is the full JID that +%% sent the caps packet, and Caps is what read_caps returned. +note_caps(Host, From, Caps) -> + case Caps of + nothing -> ok; + _ -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:cast(Proc, {note_caps, From, Caps}) + end. + +%% get_features returns a list of features implied by the given caps +%% record (as extracted by read_caps). It may block, and may signal a +%% timeout error. +get_features(Host, Caps) -> + case Caps of + nothing -> []; + #caps{} -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, {get_features, Caps}) + end. + +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). + +start(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = + {Proc, + {?MODULE, start_link, [Host, Opts]}, + transient, + 1000, + worker, + [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:stop_child(ejabberd_sup, Proc). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +init([Host, _Opts]) -> + mnesia:create_table(caps_features, + [{ram_copies, [node()]}, + {attributes, record_info(fields, caps_features)}]), + mnesia:add_table_copy(caps_features, node(), ram_copies), + {ok, #state{host = Host}}. + +maybe_get_features(#caps{node = Node, version = Version, exts = Exts}) -> + SubNodes = [Version | Exts], + F = fun() -> + %% Make sure that we have all nodes we need to know. + %% If a single one is missing, we wait for more disco + %% responses. + lists:foldl(fun(SubNode, Acc) -> + case Acc of + fail -> fail; + _ -> + case mnesia:read({caps_features, {Node, SubNode}}) of + [] -> fail; + [#caps_features{features = Features}] -> Features ++ Acc + end + end + end, [], SubNodes) + end, + case mnesia:transaction(F) of + {atomic, fail} -> + wait; + {atomic, Features} -> + {ok, Features} + end. + +timestamp() -> + {MegaSecs, Secs, _MicroSecs} = now(), + MegaSecs * 1000000 + Secs. + +handle_call({get_features, Caps}, From, State) -> + case maybe_get_features(Caps) of + {ok, Features} -> + {reply, Features, State}; + wait -> + Timeout = timestamp() + 10, + FeatureQueries = State#state.feature_queries, + NewFeatureQueries = [{From, Caps, Timeout} | FeatureQueries], + NewState = State#state{feature_queries = NewFeatureQueries}, + {noreply, NewState} + end; + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}. + +handle_cast({note_caps, From, + #caps{node = Node, version = Version, exts = Exts}}, + #state{host = Host, disco_requests = Requests} = State) -> + %% XXX: this leads to race conditions where ejabberd will send + %% lots of caps disco requests. + SubNodes = [Version | Exts], + %% Now, find which of these are not already in the database. + Fun = fun() -> + lists:foldl(fun(SubNode, Acc) -> + case mnesia:read({caps_features, {Node, SubNode}}) of + [] -> + [SubNode | Acc]; + _ -> + Acc + end + end, [], SubNodes) + end, + case mnesia:transaction(Fun) of + {atomic, Missing} -> + %% For each unknown caps "subnode", we send a disco + %% request. + NewRequests = + lists:foldl( + fun(SubNode, Dict) -> + ID = randoms:get_string(), + Stanza = + {xmlelement, "iq", + [{"type", "get"}, + {"id", ID}], + [{xmlelement, "query", + [{"xmlns", ?NS_DISCO_INFO}, + {"node", Node ++ "#" ++ SubNode}], + []}]}, + ejabberd_local:register_iq_response_handler + (Host, ID, ?MODULE, handle_disco_response), + ejabberd_router:route(jlib:make_jid("", Host, ""), From, Stanza), + ?DICT:store(ID, {Node, SubNode}, Dict) + end, Requests, Missing), + {noreply, State#state{disco_requests = NewRequests}}; + Error -> + ?ERROR_MSG("Transaction failed: ~p", [Error]), + {noreply, State} + end; +handle_cast({disco_response, From, _To, + #iq{type = Type, id = ID, + sub_el = SubEls}}, + #state{disco_requests = Requests} = State) -> + case {Type, SubEls} of + {result, [{xmlelement, "query", Attrs, Els}]} -> + case ?DICT:find(ID, Requests) of + {ok, {Node, SubNode}} -> + Features = + lists:flatmap(fun({xmlelement, "feature", FAttrs, _}) -> + [xml:get_attr_s("var", FAttrs)]; + (_) -> + [] + end, Els), + mnesia:transaction( + fun() -> + mnesia:write(#caps_features{node_pair = {Node, SubNode}, + features = Features}) + end), + gen_server:cast(self(), visit_feature_queries); + error -> + ?ERROR_MSG("ID '~s' matches no query", [ID]) + end; + {result, _} -> + ?ERROR_MSG("Invalid IQ contents from ~s: ~p", [jlib:jid_to_string(From), SubEls]); + _ -> + %% Can't do anything about errors + ok + end, + NewRequests = ?DICT:erase(ID, Requests), + {noreply, State#state{disco_requests = NewRequests}}; +handle_cast(visit_feature_queries, #state{feature_queries = FeatureQueries} = State) -> + Timestamp = timestamp(), + NewFeatureQueries = + lists:foldl(fun({From, Caps, Timeout}, Acc) -> + case maybe_get_features(Caps) of + wait when Timeout < Timestamp -> [{From, Caps, Timeout} | Acc]; + wait -> Acc; + {ok, Features} -> + gen_server:reply(From, Features), + Acc + end + end, [], FeatureQueries), + {noreply, State#state{feature_queries = NewFeatureQueries}}. + +handle_disco_response(From, To, IQ) -> + #jid{lserver = Host} = To, + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:cast(Proc, {disco_response, From, To, IQ}). + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/src/mod_pubsub/Makefile.in b/src/mod_pubsub/Makefile.in index 95d0a131c..116ddbfbc 100644 --- a/src/mod_pubsub/Makefile.in +++ b/src/mod_pubsub/Makefile.in @@ -15,11 +15,18 @@ OUTDIR = .. EFLAGS = -I .. -pz .. # make debug=true to compile Erlang module with debug informations. ifdef debug - EFLAGS+=+debug_info +EFLAGS+=+debug_info endif OBJS = \ - $(OUTDIR)/mod_pubsub.beam + $(OUTDIR)/gen_pubsub_node.beam \ + $(OUTDIR)/gen_pubsub_nodetree.beam \ + $(OUTDIR)/nodetree_default.beam \ + $(OUTDIR)/nodetree_virtual.beam \ + $(OUTDIR)/mod_pubsub.beam \ + $(OUTDIR)/mod_pubsub_old.beam \ + $(OUTDIR)/node_default.beam \ + $(OUTDIR)/node_pep.beam all: $(OBJS) diff --git a/src/mod_pubsub/Makefile.win32 b/src/mod_pubsub/Makefile.win32 index bb44dd778..077b02899 100644 --- a/src/mod_pubsub/Makefile.win32 +++ b/src/mod_pubsub/Makefile.win32 @@ -5,12 +5,38 @@ OUTDIR = .. EFLAGS = -I .. -pz .. OBJS = \ - $(OUTDIR)\mod_pubsub.beam + $(OUTDIR)/gen_pubsub_node.beam \ + $(OUTDIR)/gen_pubsub_nodetree.beam \ + $(OUTDIR)/nodetree_default.beam \ + $(OUTDIR)/nodetree_virtual.beam \ + $(OUTDIR)/mod_pubsub.beam \ + $(OUTDIR)/mod_pubsub_old.beam \ + $(OUTDIR)/node_default.beam \ + $(OUTDIR)/node_pep.beam ALL : $(OBJS) CLEAN : -@erase $(OBJS) +$(OUTDIR)\gen_pubsub_node.beam : gen_pubsub_node.erl + erlc -W $(EFLAGS) -o $(OUTDIR) gen_pubsub_node.erl + +$(OUTDIR)\gen_pubsub_nodetree.beam : gen_pubsub_nodetree.erl + erlc -W $(EFLAGS) -o $(OUTDIR) gen_pubsub_nodetree.erl + $(OUTDIR)\mod_pubsub.beam : mod_pubsub.erl erlc -W $(EFLAGS) -o $(OUTDIR) mod_pubsub.erl + +$(OUTDIR)\nodetree_default.beam : nodetree_default.erl + erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_default.erl + +$(OUTDIR)\nodetree_virtual.beam : nodetree_virtual.erl + erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_virtual.erl + +$(OUTDIR)\node_default.beam : node_default.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_default.erl + +$(OUTDIR)\node_pep.beam : node_pep.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_pep.erl + diff --git a/src/mod_pubsub/gen_pubsub_node.erl b/src/mod_pubsub/gen_pubsub_node.erl new file mode 100644 index 000000000..33c27049f --- /dev/null +++ b/src/mod_pubsub/gen_pubsub_node.erl @@ -0,0 +1,55 @@ +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% @copyright 2006 Process-one +%%% @author Christophe Romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @private +%%% @doc

The module {@module} defines the PubSub node +%%% plugin behaviour. This behaviour is used to check that a PubSub plugin +%%% respects the current ejabberd PubSub plugin API.

+ +-module(gen_pubsub_node). + +-export([behaviour_info/1]). + +%% @spec (Query::atom()) -> Callbacks | atom() +%% Callbacks = [{Function,Arity}] +%% Function = atom() +%% Arity = integer() +%% @doc Behaviour definition +behaviour_info(callbacks) -> + [{init, 3}, + {terminate, 2}, + {options, 0}, + {features, 0}, + {create_node_permission, 6}, + {create_node, 3}, + {delete_node, 2}, + {purge_node, 3}, + {subscribe_node, 8}, + {unsubscribe_node, 5}, + {publish_item, 7}, + {delete_item, 4}, + {remove_extra_items, 4}, + {get_node_affiliations, 2}, + {get_entity_affiliations, 2}, + {get_affiliation, 3}, + {set_affiliation, 4}, + {get_node_subscriptions, 2}, + {get_entity_subscriptions, 2}, + {get_subscription, 3}, + {set_subscription, 4}, + {get_states, 2}, + {get_state, 3}, + {set_state, 1}, + {get_items, 2}, + {get_item, 3}, + {set_item, 1} + ]; +behaviour_info(_Other) -> + undefined. diff --git a/src/mod_pubsub/gen_pubsub_nodetree.erl b/src/mod_pubsub/gen_pubsub_nodetree.erl new file mode 100644 index 000000000..7529850ef --- /dev/null +++ b/src/mod_pubsub/gen_pubsub_nodetree.erl @@ -0,0 +1,40 @@ +%%% ==================================================================== +%%% This software is copyright 2006, Process-one. +%%% +%%% $Id: gen_pubsub_nodetree.erl 100 2007-11-15 13:04:44Z mremond $ +%%% +%%% @copyright 2006 Process-one +%%% @author Christophe Romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @private +%%% @doc

The module {@module} defines the PubSub node +%%% tree plugin behaviour. This behaviour is used to check that a PubSub +%%% node tree plugin respects the current ejabberd PubSub plugin API.

+ +-module(gen_pubsub_nodetree). + +-export([behaviour_info/1]). + +%% @spec (Query::atom()) -> Callbacks | atom() +%% Callbacks = [{Function,Arity}] +%% Function = atom() +%% Arity = integer() +%% @doc Behaviour definition +behaviour_info(callbacks) -> + [{init, 3}, + {terminate, 2}, + {options, 0}, + {set_node, 1}, + {get_node, 2}, + {get_nodes, 1}, + {get_subnodes, 2}, + {get_subnodes_tree, 2}, + {create_node, 5}, + {delete_node, 2} + ]; +behaviour_info(_Other) -> + undefined. diff --git a/src/mod_pubsub/mod_pubsub.erl b/src/mod_pubsub/mod_pubsub.erl index 82a0bace7..c218e1711 100644 --- a/src/mod_pubsub/mod_pubsub.erl +++ b/src/mod_pubsub/mod_pubsub.erl @@ -1,55 +1,103 @@ -%%%---------------------------------------------------------------------- -%%% File : mod_pubsub.erl -%%% Author : Alexey Shchepin -%%% Purpose : Pub/sub support (JEP-0060) -%%% Created : 4 Jul 2003 by Alexey Shchepin -%%%---------------------------------------------------------------------- +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% @copyright 2007 Process-one +%%% @author Christophe Romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + + +%%% @doc The module {@module} is the core of the PubSub +%%% extension. It relies on PubSub plugins for a large part of its functions. +%%% +%%% @headerfile "pubsub.hrl" +%%% +%%% @reference See XEP-0060: Pubsub for +%%% the latest version of the PubSub specification. +%%% This module uses version 1.10 of the specification as a base. +%%% Most of the specification is implemented. +%%% Code is derivated from the original pubsub v1.7, functions concerning config may be rewritten. +%%% Code also inspired from the original PEP patch by Magnus Henoch + +%%% TODO +%%% plugin: generate Reply (do not use broadcast atom anymore) +%%% to be implemented: deliver_notifications -module(mod_pubsub). --author('alexey@sevcom.net'). +-version('1.10-01'). -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"). +-include("pubsub.hrl"). --record(state, {host, server_host, access}). +-define(STDTREE, "default"). +-define(STDNODE, "default"). --define(DICT, dict). --define(MAXITEMS, 20). --define(MAX_PAYLOAD_SIZE, 100000). +%% exports for hooks +-export([set_presence/4, + unset_presence/4, + incoming_presence/3, + disco_local_identity/5, + disco_sm_identity/5, + disco_sm_features/5, + disco_sm_items/5 + ]). +%% exported iq handlers +-export([iq_local/3, + iq_sm/3 + ]). --record(pubsub_node, {host_node, host_parent, info}). --record(pubsub_presence, {key, presence}). --record(nodeinfo, {items = [], - options = [], - entities = ?DICT:new() - }). --record(entity, {affiliation = none, - subscription = none}). --record(item, {id, publisher, payload}). +%% exports for console debug manual use +-export([create_node/5, + delete_node/3, + subscribe_node/4, + unsubscribe_node/5, + publish_item/6, + delete_item/4, + get_configure/4, + set_configure/5, + get_items/2, + node_action/3, + node_action/4 + ]). + +%% general helpers for plugins +-export([node_to_string/1, + string_to_node/1, + subscription_to_string/1, + affiliation_to_string/1, + string_to_subscription/1, + string_to_affiliation/1, + extended_error/2, + extended_error/3 + ]). + +%% API and gen_server callbacks +-export([start_link/2, + start/2, + stop/1, + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3 + ]). -define(PROCNAME, ejabberd_mod_pubsub). --define(MYJID, #jid{user = "", server = Host, resource = "", - luser = "", lserver = Host, lresource = ""}). +-define(PLUGIN_PREFIX, "node_"). +-define(TREE_PREFIX, "nodetree_"). + +-record(state, {server_host, + host, + access, + nodetree = ?STDTREE, + plugins = [?STDNODE]}). %%==================================================================== %% API @@ -64,13 +112,9 @@ start_link(Host, Opts) -> start(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = - {Proc, - {?MODULE, start_link, [Host, Opts]}, - temporary, - 1000, - worker, - [?MODULE]}, + ChildSpec = {Proc, + {?MODULE, start_link, [Host, Opts]}, + transient, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> @@ -78,30 +122,6 @@ stop(Host) -> gen_server:call(Proc, stop), supervisor:delete_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 %%==================================================================== @@ -114,30 +134,256 @@ get_host() -> %% Description: Initiates the server %%-------------------------------------------------------------------- init([ServerHost, Opts]) -> - mnesia:create_table(pubsub_node, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, pubsub_node)}]), + ?INFO_MSG("pubsub init ~p ~p",[ServerHost,Opts]), mnesia:create_table(pubsub_presence, - [{ram_copies, [node()]}, + [{disc_copies, [node()]}, {attributes, record_info(fields, pubsub_presence)}]), Host = gen_mod:get_opt_host(ServerHost, Opts, "pubsub.@HOST@"), - 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), - + mod_disco:register_feature(ServerHost, ?NS_PUBSUB), + ejabberd_hooks:add(disco_local_identity, ServerHost, + ?MODULE, disco_local_identity, 75), + ejabberd_hooks:add(disco_sm_identity, ServerHost, + ?MODULE, disco_sm_identity, 75), + ejabberd_hooks:add(disco_sm_features, ServerHost, + ?MODULE, disco_sm_features, 75), + ejabberd_hooks:add(disco_sm_items, ServerHost, + ?MODULE, disco_sm_items, 75), + ejabberd_hooks:add(incoming_presence_hook, ServerHost, + ?MODULE, incoming_presence, 50), + %%ejabberd_hooks:add(set_presence_hook, ServerHost, ?MODULE, set_presence, 50), + %%ejabberd_hooks:add(unset_presence_hook, ServerHost, ?MODULE, unset_presence, 50), + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), + lists:foreach( + fun({NS,Mod,Fun}) -> + gen_iq_handler:add_iq_handler( + Mod, ServerHost, NS, ?MODULE, Fun, IQDisc) + end, + [{?NS_PUBSUB, ejabberd_local, iq_local}, + {?NS_PUBSUB_OWNER, ejabberd_local, iq_local}, + {?NS_PUBSUB, ejabberd_sm, iq_sm}, + {?NS_PUBSUB_OWNER, ejabberd_sm, iq_sm}]), ejabberd_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), - {ok, #state{host = Host, server_host = ServerHost, access = Access}}. + {Plugins, NodeTree} = init_plugins(Host, ServerHost, Opts), + update_database(Host), + ets:new(gen_mod:get_module_proc(Host, pubsub_state), [set, named_table]), + ets:insert(gen_mod:get_module_proc(Host, pubsub_state), {nodetree, NodeTree}), + ets:insert(gen_mod:get_module_proc(Host, pubsub_state), {plugins, Plugins}), + ets:new(gen_mod:get_module_proc(ServerHost, pubsub_state), [set, named_table]), + ets:insert(gen_mod:get_module_proc(ServerHost, pubsub_state), {nodetree, NodeTree}), + ets:insert(gen_mod:get_module_proc(ServerHost, pubsub_state), {plugins, Plugins}), + init_nodes(Host, ServerHost, ServedHosts), + {ok, #state{host = Host, + server_host = ServerHost, + access = Access, + nodetree = NodeTree, + plugins = Plugins}}. + +%% @spec (Host, Opts) -> Plugins +%% Host = mod_pubsub:host() Opts = [{Key,Value}] +%% ServerHost = host() +%% Key = atom() +%% Value = term() +%% Plugins = [Plugin::string()] +%% @doc Call the init/1 function for each plugin declared in the config file. +%% The default plugin module is implicit. +%%

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

+%%

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

+%%

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

+init_plugins(Host, ServerHost, Opts) -> + TreePlugin = list_to_atom(?TREE_PREFIX ++ + gen_mod:get_opt(nodetree, Opts, ?STDTREE)), + ?INFO_MSG("** tree plugin is ~p",[TreePlugin]), + TreePlugin:init(Host, ServerHost, Opts), + Plugins = lists:usort(gen_mod:get_opt(plugins, Opts, []) ++ [?STDNODE]), + lists:foreach(fun(Name) -> + ?INFO_MSG("** init ~s plugin~n",[Name]), + Plugin = list_to_atom(?PLUGIN_PREFIX ++ Name), + Plugin:init(Host, ServerHost, Opts) + end, Plugins), + {Plugins, TreePlugin}. + +terminate_plugins(Host, ServerHost, Plugins, TreePlugin) -> + lists:foreach(fun(Name) -> + ?INFO_MSG("** terminate ~s plugin~n",[Name]), + Plugin = list_to_atom(?PLUGIN_PREFIX++Name), + Plugin:terminate(Host, ServerHost) + end, Plugins), + TreePlugin:terminate(Host, ServerHost), + ok. + +init_nodes(Host, ServerHost, ServedHosts) -> + create_node(Host, ServerHost, ["pubsub"], ?PUBSUB_JID, ?STDNODE), + create_node(Host, ServerHost, ["pubsub", "nodes"], ?PUBSUB_JID, ?STDNODE), + create_node(Host, ServerHost, ["home"], ?PUBSUB_JID, ?STDNODE), + lists:foreach( + fun(H) -> + create_node(Host, ServerHost, ["home", H], ?PUBSUB_JID, ?STDNODE) + end, [ServerHost | ServedHosts]), + ok. + +update_database(Host) -> + case catch mnesia:table_info(pubsub_node, attributes) of + [host_node, host_parent, info] -> + ?INFO_MSG("upgrade pubsub tables",[]), + F = fun() -> + NewRecords = + lists:foldl( + fun({pubsub_node, NodeId, ParentId, {nodeinfo, Items, Options, Entities}}, RecList) -> + ItemsList = + lists:foldl( + fun({item, IID, Publisher, Payload}, Acc) -> + C = {Publisher, unknown}, + M = {Publisher, now()}, + mnesia:write( + #pubsub_item{itemid = {IID, NodeId}, + creation = C, + modification = M, + payload = Payload}), + [{Publisher, IID} | Acc] + end, [], Items), + Owners = + dict:fold( + fun(JID, {entity, Aff, Sub}, Acc) -> + UsrItems = + lists:foldl( + fun({P, I}, IAcc) -> + case P of + JID -> [I | IAcc]; + _ -> IAcc + end + end, [], ItemsList), + mnesia:write( + #pubsub_state{stateid = {JID, NodeId}, + items = UsrItems, + affiliation = Aff, + subscription = Sub}), + case Aff of + owner -> [JID | Acc]; + _ -> Acc + end + end, [], Entities), + mnesia:delete({pubsub_node, NodeId}), + [#pubsub_node{nodeid = NodeId, + parentid = ParentId, + owners = Owners, + options = Options} | + RecList] + end, [], + mnesia:match_object( + {pubsub_node, {Host, '_'}, '_', '_'})), + mnesia:delete_table(pubsub_node), + mnesia:create_table(pubsub_node, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_node)}, + {index, [type, parentid]}]), + lists:foreach(fun(Record) -> + mnesia:write(Record) + end, NewRecords) + end, + mnesia:transaction(F); + _ -> + ok + end. + +%% ------- +%% hooks handling functions +%% + +disco_local_identity(Acc, _From, _To, [], _Lang) -> + Acc ++ [{xmlelement, "identity", [{"category", "pubsub"}, {"type", "service"}], []}, + {xmlelement, "identity", [{"category", "pubsub"}, {"type", "pep"}], []} ]; +disco_local_identity(Acc, _From, _To, _Node, _Lang) -> + Acc. + +disco_sm_identity(Acc, _From, _To, [], _Lang) -> + Acc ++ [ + {xmlelement, "identity", [{"category", "pubsub"}, {"type", "service"}], []}, + {xmlelement, "identity", [{"category", "pubsub"}, {"type", "pep"}], []} ]; +disco_sm_identity(Acc, From, To, Node, _Lang) -> + LOwner = jlib:jid_tolower(jlib:jid_remove_resource(To)), + Acc ++ case node_disco_identity(LOwner, From, Node) of + {result, I} -> I; + _ -> [] + end. + +disco_sm_features(Acc, _From, _To, [], _Lang) -> + Acc; +disco_sm_features(Acc, From, To, Node, _Lang) -> + LOwner = jlib:jid_tolower(jlib:jid_remove_resource(To)), + Features = node_disco_features(LOwner, From, Node), + case {Acc, Features} of + {{result, AccFeatures}, {result, AddFeatures}} -> + {result, AccFeatures++AddFeatures}; + {_, {result, AddFeatures}} -> + {result, AddFeatures}; + {_, _} -> + Acc + end. + +disco_sm_items(Acc, _From, To, [], _Lang) -> + %% TODO, use iq_disco_items(Host, [], From) + Host = To#jid.lserver, + LJID = jlib:jid_tolower(jlib:jid_remove_resource(To)), + case tree_action(Host, get_nodes, [Host]) of + [] -> + Acc; + Nodes -> + Items = case Acc of + {result, I} -> I; + _ -> [] + end, + NodeItems = lists:map( + fun(Node) -> + {xmlelement, "item", + [{"jid", jlib:jid_to_string(LJID)}, + {"node", node_to_string(Node)}], + []} + end, Nodes), + {result, NodeItems ++ Items} + end; + +disco_sm_items(Acc, _From, To, Node, _Lang) -> + %% TODO, use iq_disco_items(Host, Node, From) + Host = To#jid.lserver, + LJID = jlib:jid_tolower(jlib:jid_remove_resource(To)), + case tree_action(Host, get_items, [Host, Node]) of + [] -> + Acc; + AllItems -> + Items = case Acc of + {result, I} -> I; + _ -> [] + end, + NodeItems = lists:map( + fun(#pubsub_item{itemid = Id}) -> + %% "jid" is required by XEP-0030, and + %% "node" is forbidden by XEP-0060. + {xmlelement, "item", + [{"jid", jlib:jid_to_string(LJID)}, + {"name", Id}], + []} + end, AllItems), + {result, NodeItems ++ Items} + end. + + +set_presence(User, Server, Resource, Presence) -> + Proc = gen_mod:get_module_proc(Server, ?PROCNAME), + gen_server:cast(Proc, {set_presence, User, Server, Resource, Presence}). +unset_presence(User, Server, Resource, Status) -> + Proc = gen_mod:get_module_proc(Server, ?PROCNAME), + gen_server:cast(Proc, {unset_presence, User, Server, Resource, Status}). +incoming_presence(From, #jid{lserver = Host} = To, Packet) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:cast(Proc, {presence, From, To, Packet}). %%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% Function: +%% handle_call(Request, From, State) -> {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | @@ -145,6 +391,13 @@ init([ServerHost, Opts]) -> %% {stop, Reason, State} %% Description: Handling call messages %%-------------------------------------------------------------------- +%% @private +handle_call(server_host, _From, State) -> + {reply, State#state.server_host, State}; +handle_call(plugins, _From, State) -> + {reply, State#state.plugins, State}; +handle_call(nodetree, _From, State) -> + {reply, State#state.nodetree, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}. @@ -154,6 +407,142 @@ handle_call(stop, _From, State) -> %% {stop, Reason, State} %% Description: Handling cast messages %%-------------------------------------------------------------------- +%% @private +handle_cast({presence, From, To, Packet}, State) -> + Plugins = State#state.plugins, + Key = {To#jid.lserver, From#jid.luser, From#jid.lserver}, + Record = #pubsub_presence{key = Key, resource = From#jid.lresource}, + Priority = case xml:get_subtag(Packet, "priority") of + false -> 0; + SubEl -> + case catch list_to_integer(xml:get_tag_cdata(SubEl)) of + P when is_integer(P) -> P; + _ -> 0 + end + end, + PresenceType = case xml:get_tag_attr_s("type", Packet) of + "" -> + if Priority < 0 -> unavailable; + true -> available + end; + "unavailable" -> unavailable; + "error" -> unavailable; + _ -> none + end, + PreviouslyAvailable = + lists:member(From#jid.lresource, + lists:map(fun(#pubsub_presence{resource = R}) -> R end, + case catch mnesia:dirty_read(pubsub_presence, Key) of + Result when is_list(Result) -> Result; + _ -> [] + end)), + case PresenceType of + available -> mnesia:dirty_write(Record); + unavailable -> mnesia:dirty_delete_object(Record); + _ -> ok + end, + if PreviouslyAvailable == false, + PresenceType == available -> + %% A new resource is available. Loop through all nodes + %% and see if the contact is subscribed, and if so, and if + %% the node is so configured, send the last item. + Host = From#jid.lserver, + JID = jlib:jid_tolower(From), + lists:foreach( + fun(Type) -> + {result, Subscriptions} = node_action(Type, get_entity_subscriptions, [Host, From]), + lists:foreach( + fun({Node, Subscription}) -> + Options = node_options(Type), + SendLast = get_option(Options, send_last_published_item), + if + Subscription /= none, Subscription /= pending, SendLast == on_sub_and_presence -> + send_last_item(Host, Node, JID); + SendLast == on_sub_and_presence -> + AccessModel = get_option(Options, access_model), + AllowedGroups = get_option(Options, roster_groups_allowed), + {PresenceSubscription, RosterGroup} = get_roster_info( + To#jid.luser, To#jid.lserver, JID, AllowedGroups), + Features = case catch mod_caps:get_features(Host, mod_caps:read_caps(element(4, Packet))) of + F when is_list(F) -> F; + _ -> [] + end, + case lists:member(Node ++ "+notify", Features) of + true -> + MaySubscribe = + case AccessModel of + open -> true; + presence -> PresenceSubscription; + whitelist -> false; % subscribers are added manually + authorize -> false; % likewise + roster -> RosterGroup + end, + if MaySubscribe -> + send_last_item( + Host, Node, JID); + true -> + ok + end; + false -> + ok + end; + true -> + ok + end; + (_) -> + ok + end, Subscriptions) + end, Plugins), + {noreply, State}; + true -> + {noreply, State} + end; + +handle_cast({set_presence, _User, _Server, _Resource, _Presence}, State) -> + {noreply, State}; +handle_cast({unset_presence, _User, _Server, _Resource, _Status}, State) -> + {noreply, State}; +% Owner = jlib:jid_tolower(jlib:jid_remove_resource(#jid{luser = User, lserver = Server, lresource = Resource})), +% JID = User ++ "@" ++ Server ++ "/" ++ Resource, +% F = fun() -> +% case mnesia:dirty_match_object(#pubsub_state{stateid = {Owner, '_'}, _ = '_'}) of +% [] -> +% {error, ?ERR_ITEM_NOT_FOUND}; +% States -> +% {result, lists:foldl( +% fun(#pubsub_state{stateid = {_, {Host, Node}}, items = Items} = S, ItemsList) -> +% %% To make this code work for nodetree_default system, we must be sure node type is correct +% %% this can be done asking a match {Host, Node} when Type=this_module_type +% %% case mnesia:dirty_match_object(#pubsub_node{nodeid={Host, Node}, type=virtual, _ = '_'}) +% %% this call should be handled by the nodetree plugin +% %% but at this stage we don't know what is the actual nodetree plugin +% %% this must be implemented into the API, mod_pubsub telling us what is the nodetree plugin +% NewItems = lists:filter(fun(ItemID) -> ItemID /= JID end, Items), +% if NewItems /= Items -> +% mnesia:dirty_write(S#pubsub_state{items = NewItems}), +% Key = {JID, {Host, Node}}, +% mnesia:dirty_delete({pubsub_item, Key}), +% ItemsList ++ [Key]; +% true -> +% ItemsList +% end +% end, [], States)} +% end +% end, +% case catch mnesia:sync_dirty(F) of +% {'EXIT', Reason} -> +% {error, Reason}; +% {error, Reason} -> +% {error, Reason}; +% {result, Items} -> +% lists:foreach(fun({ItemID, {Host, Node}}) -> +% mod_pubsub:broadcast_retract_item(Host, Node, ItemID) +% end, Items), +% {result, []}; +% _ -> +% {error, ?ERR_INTERNAL_SERVER_ERROR} +% end. + handle_cast(_Msg, State) -> {noreply, State}. @@ -163,13 +552,14 @@ handle_cast(_Msg, State) -> %% {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 +%% @private +handle_info({route, From, To, Packet}, + #state{server_host = ServerHost, + access = Access, + plugins = Plugins} = State) -> + case catch do_route(ServerHost, Access, Plugins, To#jid.lserver, From, To, Packet) of + {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); + _ -> ok end, {noreply, State}; handle_info(_Info, State) -> @@ -182,87 +572,108 @@ handle_info(_Info, State) -> %% 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), +%% @private +terminate(_Reason, #state{host = Host, + server_host = ServerHost, + nodetree = TreePlugin, + plugins = Plugins}) -> + terminate_plugins(Host, ServerHost, Plugins, TreePlugin), + ejabberd_router:unregister_route(Host), + ejabberd_hooks:delete(disco_local_identity, ServerHost, + ?MODULE, disco_local_identity, 75), + ejabberd_hooks:delete(disco_sm_identity, ServerHost, + ?MODULE, disco_sm_identity, 75), + ejabberd_hooks:delete(disco_sm_features, ServerHost, + ?MODULE, disco_sm_features, 75), + ejabberd_hooks:delete(disco_sm_items, ServerHost, + ?MODULE, disco_sm_items, 75), + ejabberd_hooks:delete(incoming_presence_hook, ServerHost, + ?MODULE, incoming_presence, 50), + %%ejabberd_hooks:delete(set_presence_hook, ServerHost, ?MODULE, set_presence, 50), + %%ejabberd_hooks:delete(unset_presence_hook, ServerHost, ?MODULE, unset_presence, 50), + lists:foreach(fun({NS,Mod}) -> + gen_iq_handler:remove_iq_handler(Mod, ServerHost, NS) + end, [{?NS_PUBSUB, ejabberd_local}, + {?NS_PUBSUB_OWNER, ejabberd_local}, + {?NS_PUBSUB, ejabberd_sm}, + {?NS_PUBSUB_OWNER, ejabberd_sm}]), + mod_disco:unregister_feature(ServerHost, ?NS_PUBSUB), ok. %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} %% Description: Convert process state when code is changed %%-------------------------------------------------------------------- +%% @private code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -do_route(Host, ServerHost, Access, From, To, Packet) -> - {xmlelement, Name, Attrs, Els} = Packet, +do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> + {xmlelement, Name, Attrs, _Els} = Packet, case To of #jid{luser = "", lresource = ""} -> case Name of "iq" -> case jlib:iq_query_info(Packet) of - #iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS, + #iq{type = get, xmlns = ?NS_DISCO_INFO, sub_el = SubEl, lang = Lang} = 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, Lang)}]}, - ejabberd_router:route(To, - From, - jlib:iq_to_xml(Res)); - #iq{type = get, xmlns = ?NS_DISCO_ITEMS = XMLNS, + Res = case iq_disco_info(Host, Node, From, Lang) of + {result, IQRes} -> + jlib:iq_to_xml( + IQ#iq{type = result, + sub_el = [{xmlelement, "query", + QAttrs, IQRes}]}); + {error, Error} -> + jlib:make_error_reply(Packet, Error) + end, + ejabberd_router:route(To, From, Res); + #iq{type = get, xmlns = ?NS_DISCO_ITEMS, sub_el = SubEl} = IQ -> {xmlelement, _, QAttrs, _} = SubEl, Node = xml:get_attr_s("node", QAttrs), - 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, + Res = case iq_disco_items(Host, Node, From) 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, + #iq{type = IQType, xmlns = ?NS_PUBSUB, lang = Lang, sub_el = SubEl} = IQ -> Res = - case iq_pubsub_owner( - Host, From, Type, Lang, SubEl) of + case iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) of {result, IQRes} -> jlib:iq_to_xml( IQ#iq{type = result, sub_el = IQRes}); {error, Error} -> - jlib:make_error_reply( - Packet, Error) + jlib:make_error_reply(Packet, Error) + end, + ejabberd_router:route(To, From, Res); + #iq{type = IQType, xmlns = ?NS_PUBSUB_OWNER, + lang = Lang, sub_el = SubEl} = IQ -> + Res = + case iq_pubsub_owner(Host, From, IQType, SubEl, Lang) of + {result, IQRes} -> + jlib:iq_to_xml( + IQ#iq{type = result, + sub_el = IQRes}); + {error, Error} -> + jlib:make_error_reply(Packet, Error) end, ejabberd_router:route(To, From, Res); #iq{type = get, xmlns = ?NS_VCARD = XMLNS, - lang = Lang, sub_el = SubEl} = IQ -> + lang = Lang, sub_el = _SubEl} = IQ -> Res = IQ#iq{type = result, - sub_el = [{xmlelement, "vCard", - [{"xmlns", XMLNS}], + sub_el = [{xmlelement, "vCard", [{"xmlns", XMLNS}], iq_get_vcard(Lang)}]}, ejabberd_router:route(To, From, @@ -276,14 +687,23 @@ do_route(Host, ServerHost, Access, From, To, Packet) -> ok end; "presence" -> - Type = xml:get_attr_s("type", Attrs), - if - (Type == "unavailable") or (Type == "error") -> - mnesia:dirty_delete(pubsub_presence, {Host, From#jid.luser, From#jid.lserver}); - true -> - mnesia:dirty_write(#pubsub_presence{key={Host, From#jid.luser, From#jid.lserver}, presence=[]}) - end, + incoming_presence(From, To, Packet), ok; + "message" -> + case xml:get_attr_s("type", Attrs) of + "error" -> + ok; + _ -> + case find_authorization_response(Packet) of + none -> + ok; + invalid -> + ejabberd_router:route(To, From, + jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST)); + XFields -> + handle_authorization_response(Host, From, To, Packet, XFields) + end + end; _ -> ok end; @@ -294,173 +714,412 @@ do_route(Host, ServerHost, Access, From, To, Packet) -> "result" -> ok; _ -> - Err = jlib:make_error_reply( - Packet, ?ERR_ITEM_NOT_FOUND), + Err = jlib:make_error_reply(Packet, ?ERR_ITEM_NOT_FOUND), ejabberd_router:route(To, From, Err) end end. +node_disco_info(Host, Node, From) -> + node_disco_info(Host, Node, From, true, true). +node_disco_identity(Host, Node, From) -> + node_disco_info(Host, Node, From, true, false). +node_disco_features(Host, Node, From) -> + node_disco_info(Host, Node, From, false, true). +node_disco_info(Host, Node, From, Identity, Features) -> + Action = + fun(#pubsub_node{type = Type}) -> + I = case Identity of + false -> + []; + true -> + Types = + case tree_call(Host, get_subnodes, [Host, Node]) of + [] -> + ["leaf"]; %% No sub-nodes: it's a leaf node + _ -> + case node_call(Type, get_items, [Host, Node]) of + {result, []} -> ["collection"]; + {result, _} -> ["leaf", "collection"]; + _ -> [] + end + end, + lists:map(fun(T) -> + {xmlelement, "identity", [{"category", "pubsub"}, + {"type", T}], []} + end, Types) + end, + F = case Features of + false -> + []; + true -> + [{xmlelement, "feature", [{"var", ?NS_PUBSUB}], []} | + lists:map(fun(T) -> + {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++T}], []} + end, features(Type))] + end, + %% TODO: add meta-data info (spec section 5.4) + {result, I ++ F} + end, + transaction(Host, Node, Action, sync_dirty). - -node_to_string(Node) -> - string:strip(lists:flatten(lists:map(fun(S) -> [S, "/"] end, Node)), - right, $/). - - -iq_disco_info(SNode, Lang) -> - Node = string:tokens(SNode, "/"), +iq_disco_info(Host, SNode, From, Lang) -> + Node = string_to_node(SNode), case Node of [] -> - [{xmlelement, "identity", - [{"category", "pubsub"}, - {"type", "generic"}, - {"name", translate:translate(Lang, "Publish-Subscribe")}], []}, - {xmlelement, "feature", [{"var", ?NS_PUBSUB}], []}, - {xmlelement, "feature", [{"var", ?NS_PUBSUB_EVENT}], []}, - {xmlelement, "feature", [{"var", ?NS_PUBSUB_OWNER}], []}, - {xmlelement, "feature", [{"var", ?NS_VCARD}], []}]; + {result, + [{xmlelement, "identity", + [{"category", "pubsub"}, + {"type", "service"}, + {"name", translate:translate(Lang, "Publish-Subscribe")}], []}, + {xmlelement, "feature", [{"var", ?NS_PUBSUB}], []}, + {xmlelement, "feature", [{"var", ?NS_VCARD}], []}] ++ + lists:map(fun(Feature) -> + {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++Feature}], []} + end, features(Host, Node))}; _ -> - % TODO - [] + node_disco_info(Host, Node, From) 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 +iq_disco_items(Host, [], _From) -> + {result, lists:map( + fun(#pubsub_node{nodeid = {_, SubNode}}) -> + SN = node_to_string(SubNode), + RN = lists:last(SubNode), + %% remove name attribute + {xmlelement, "item", [{"jid", Host}, + {"node", SN}, + {"name", RN}], []} + end, tree_action(Host, get_subnodes, [Host, []]))}; +iq_disco_items(Host, Item, _From) -> + case string:tokens(Item, "!") of + [_SNode, _ItemID] -> + {result, []}; + [SNode] -> + Node = string_to_node(SNode), + %% Note: Multiple Node Discovery not supported (mask on pubsub#type) + %% TODO this code is also back-compatible with pubsub v1.8 (for client issue) + %% TODO make it pubsub v1.10 compliant (this breaks client compatibility) + %% TODO That is, remove name attribute + Action = + fun(#pubsub_node{type = Type}) -> + NodeItems = case node_call(Type, get_items, [Host, Node]) of + {result, I} -> I; + _ -> [] + end, + Nodes = lists:map( + fun(#pubsub_node{nodeid = {_, SubNode}}) -> + SN = node_to_string(SubNode), + RN = lists:last(SubNode), + {xmlelement, "item", [{"jid", Host}, {"node", SN}, {"name", RN}], []} + end, tree_call(Host, get_subnodes, [Host, Node])), + Items = lists:map( + fun(#pubsub_item{itemid = {RN, _}}) -> + SN = node_to_string(Node) ++ "!" ++ RN, + {xmlelement, "item", [{"jid", Host}, {"node", SN}, {"name", RN}], []} + end, NodeItems), + {result, Nodes ++ Items} 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} + transaction(Host, Node, Action, sync_dirty) + end. + +iq_local(From, To, #iq{type = Type, + sub_el = SubEl, + xmlns = XMLNS, + lang = Lang} = IQ) -> + ServerHost = To#jid.lserver, + %% Accept IQs to server only from our own users. + if + From#jid.lserver /= ServerHost -> + IQ#iq{type = error, sub_el = [?ERR_FORBIDDEN, SubEl]}; + true -> + LOwner = jlib:jid_tolower(jlib:jid_remove_resource(From)), + Res = case XMLNS of + ?NS_PUBSUB -> iq_pubsub(LOwner, ServerHost, From, Type, SubEl, Lang); + ?NS_PUBSUB_OWNER -> iq_pubsub_owner(LOwner, From, Type, SubEl, Lang) + end, + case Res of + {result, IQRes} -> IQ#iq{type = result, sub_el = IQRes}; + {error, Error} -> IQ#iq{type = error, sub_el = [Error, SubEl]} + end + end. + +iq_sm(From, To, #iq{type = Type, sub_el = SubEl, xmlns = XMLNS, lang = Lang} = IQ) -> + ServerHost = To#jid.lserver, + LOwner = jlib:jid_tolower(jlib:jid_remove_resource(To)), + Res = case XMLNS of + ?NS_PUBSUB -> iq_pubsub(LOwner, ServerHost, From, Type, SubEl, Lang); + ?NS_PUBSUB_OWNER -> iq_pubsub_owner(LOwner, From, Type, SubEl, Lang) + end, + case Res of + {result, IQRes} -> IQ#iq{type = result, sub_el = IQRes}; + {error, Error} -> IQ#iq{type = error, sub_el = [Error, SubEl]} end. iq_get_vcard(Lang) -> - [{xmlelement, "FN", [], - [{xmlcdata, "ejabberd/mod_pubsub"}]}, - {xmlelement, "URL", [], - [{xmlcdata, ?EJABBERD_URI}]}, + [{xmlelement, "FN", [], [{xmlcdata, "ejabberd/mod_pubsub"}]}, + {xmlelement, "URL", [], [{xmlcdata, "http://www.process-one.net/en/projects/ejabberd/"}]}, {xmlelement, "DESC", [], - [{xmlcdata, translate:translate( - Lang, - "ejabberd pub/sub module\n" - "Copyright (c) 2003-2007 Alexey Shchepin")}]}]. + [{xmlcdata, + translate:translate(Lang, + "ejabberd Publish-Subscribe module\n" + "Copyright (c) 2004-2007 Process-One")}]}]. +iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang) -> + Plugins = case ets:lookup(gen_mod:get_module_proc(ServerHost, pubsub_state), plugins) of + [{plugins, PL}] -> PL; + _ -> [?STDNODE] + end, + iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, all, Plugins). -iq_pubsub(Host, ServerHost, From, Type, SubEl, Access) -> +iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) -> {xmlelement, _, _, SubEls} = SubEl, - case xml:remove_cdata(SubEls) of + WithoutCdata = xml:remove_cdata(SubEls), + Configuration = lists:filter(fun({xmlelement, Name, _, _}) -> + Name == "configure" + end, WithoutCdata), + Action = WithoutCdata -- Configuration, + case Action of [{xmlelement, Name, Attrs, Els}] -> - SNode = xml:get_attr_s("node", Attrs), - Node = string:tokens(SNode, "/"), - case {Type, Name} of + Node = case Host of + {_, _, _} -> xml:get_attr_s("node", Attrs); + _ -> string_to_node(xml:get_attr_s("node", Attrs)) + end, + case {IQType, Name} of {set, "create"} -> - create_new_node(Host, Node, From, ServerHost, Access); + case Configuration of + [{xmlelement, "configure", _, Config}] -> + %% Get the type of the node + Type = case xml:get_attr_s("type", Attrs) of + [] -> ?STDNODE; + T -> T + end, + %% we use Plugins list matching because we do not want to allocate + %% atoms for non existing type, this prevent atom allocation overflow + case lists:member(Type, Plugins) of + false -> + {error, extended_error( + ?ERR_FEATURE_NOT_IMPLEMENTED, + unsupported, "create-nodes")}; + true -> + create_node(Host, ServerHost, Node, From, + Type, Access, Config) + end; + _ -> + %% this breaks backward compatibility! + %% can not create node without + %% but this is the new spec anyway + ?INFO_MSG("Node ~p ; invalid configuration: ~p", [Node, Configuration]), + {error, ?ERR_BAD_REQUEST} + end; {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); + ItemId = xml:get_attr_s("id", ItemAttrs), + publish_item(Host, ServerHost, Node, From, ItemId, Payload); + [] -> + %% Publisher attempts to publish to persistent node with no item + {error, extended_error(?ERR_BAD_REQUEST, + "item-required")}; _ -> - {error, ?ERR_BAD_REQUEST} + %% Entity attempts to publish item with multiple payload elements or namespace does not match + {error, extended_error(?ERR_BAD_REQUEST, + "invalid-payload")} end; {set, "retract"} -> + ForceNotify = case xml:get_attr_s("notify", Attrs) of + "1" -> true; + "true" -> true; + _ -> false + end, case xml:remove_cdata(Els) of [{xmlelement, "item", ItemAttrs, _}] -> - ItemID = xml:get_attr_s("id", ItemAttrs), - delete_item(Host, From, Node, ItemID); + ItemId = xml:get_attr_s("id", ItemAttrs), + delete_item(Host, Node, From, ItemId, ForceNotify); _ -> - {error, ?ERR_BAD_REQUEST} + %% Request does not specify an item + {error, extended_error(?ERR_BAD_REQUEST, + "item-required")} end; {set, "subscribe"} -> JID = xml:get_attr_s("jid", Attrs), - subscribe_node(Host, From, JID, Node); + subscribe_node(Host, Node, From, JID); {set, "unsubscribe"} -> JID = xml:get_attr_s("jid", Attrs), - unsubscribe_node(Host, From, JID, Node); + SubId = xml:get_attr_s("subid", Attrs), + unsubscribe_node(Host, Node, From, JID, SubId); {get, "items"} -> MaxItems = xml:get_attr_s("max_items", Attrs), - get_items(Host, 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_items(Host, Node, From, MaxItems); + {get, "subscriptions"} -> + get_subscriptions(Host, From, Plugins); {get, "affiliations"} -> - get_affiliations(Host, From); + get_affiliations(Host, From, Plugins); _ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} end; _ -> + ?INFO_MSG("Too many actions: ~p", [Action]), {error, ?ERR_BAD_REQUEST} end. +iq_pubsub_owner(Host, From, IQType, SubEl, Lang) -> + {xmlelement, _, _, SubEls} = SubEl, + Action = xml:remove_cdata(SubEls), + case Action of + [{xmlelement, Name, Attrs, Els}] -> + Node = case Host of + {_, _, _} -> xml:get_attr_s("node", Attrs); + _ -> string_to_node(xml:get_attr_s("node", Attrs)) + end, + case {IQType, Name} of + {get, "configure"} -> + get_configure(Host, Node, From, Lang); + {set, "configure"} -> + set_configure(Host, Node, From, Els, Lang); + {get, "default"} -> + get_default(Host, Node, From, Lang); + {set, "delete"} -> + delete_node(Host, Node, From); + {set, "purge"} -> + purge_node(Host, Node, From); + {get, "subscriptions"} -> + get_subscriptions(Host, Node, From); + {set, "subscriptions"} -> + set_subscriptions(Host, Node, From, xml:remove_cdata(Els)); + {get, "affiliations"} -> + get_affiliations(Host, Node, From); + {set, "affiliations"} -> + set_affiliations(Host, Node, From, xml:remove_cdata(Els)); + _ -> + {error, ?ERR_FEATURE_NOT_IMPLEMENTED} + end; + _ -> + ?INFO_MSG("Too many actions: ~p", [Action]), + {error, ?ERR_BAD_REQUEST} + end. + +%%% authorization handling + +send_authorization_request(Host, Node, Subscriber) -> + Lang = "en", %% TODO fix + Stanza = {xmlelement, "message", + [], + [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], + [{xmlelement, "title", [], + [{xmlcdata, translate:translate(Lang, "PubSub subscriber request")}]}, + {xmlelement, "instructions", [], + [{xmlcdata, translate:translate(Lang, "Choose whether to approve this entity's subscription.")}]}, + {xmlelement, "field", + [{"var", "FORM_TYPE"}, {"type", "hidden"}], + [{xmlelement, "value", [], [{xmlcdata, ?NS_PUBSUB_SUB_AUTH}]}]}, + {xmlelement, "field", + [{"var", "pubsub#node"}, {"type", "text-single"}, + {"label", translate:translate(Lang, "Node ID")}], + [{xmlelement, "value", [], + [{xmlcdata, node_to_string(Node)}]}]}, + {xmlelement, "field", [{"var", "pubsub#subscriber_jid"}, + {"type", "jid-single"}, + {"label", translate:translate(Lang, "Subscriber Address")}], + [{xmlelement, "value", [], + [{xmlcdata, jlib:jid_to_string(Subscriber)}]}]}, + {xmlelement, "field", + [{"var", "pubsub#allow"}, + {"type", "boolean"}, + {"label", translate:translate(Lang, "Allow this JID to subscribe to this pubsub node?")}], + [{xmlelement, "value", [], [{xmlcdata, "false"}]}]}]}]}, + case tree_action(Host, get_node, [Host, Node]) of + #pubsub_node{owners = Owners} -> + lists:foreach( + fun(Owner) -> + ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(Owner), + Stanza) + end, Owners), + ok; + _ -> + ok + end. + +find_authorization_response(Packet) -> + {xmlelement, _Name, _Attrs, Els} = Packet, + XData1 = lists:map(fun({xmlelement, "x", XAttrs, _} = XEl) -> + case xml:get_attr_s("xmlns", XAttrs) of + ?NS_XDATA -> + case xml:get_attr_s("type", XAttrs) of + "cancel" -> + none; + _ -> + jlib:parse_xdata_submit(XEl) + end; + _ -> + none + end; + (_) -> + none + end, xml:remove_cdata(Els)), + XData = lists:filter(fun(E) -> E /= none end, XData1), + case XData of + [invalid] -> invalid; + [] -> none; + [XFields] when is_list(XFields) -> + case lists:keysearch("FORM_TYPE", 1, XFields) of + {value, {_, ?NS_PUBSUB_SUB_AUTH}} -> + XFields; + _ -> + invalid + end + end. + +handle_authorization_response(Host, From, To, Packet, XFields) -> + case {lists:keysearch("pubsub#node", 1, XFields), + lists:keysearch("pubsub#subscriber_jid", 1, XFields), + lists:keysearch("pubsub#allow", 1, XFields)} of + {{value, {_, SNode}}, {value, {_, SSubscriber}}, + {value, {_, SAllow}}} -> + Node = case Host of + {_, _, _} -> [SNode]; + _ -> string:tokens(SNode, "/") + end, + Subscriber = jlib:string_to_jid(SSubscriber), + Allow = case SAllow of + "1" -> true; + "true" -> true; + _ -> false + end, + Action = fun(#pubsub_node{type = Type, + options = Options, + owners = Owners}) -> + IsApprover = lists:member(jlib:jid_tolower(jlib:jid_remove_resource(From)), Owners), + Subscription = node_call(Type, get_subscription, [Host, Node, Subscriber]), + if + not IsApprover -> + {error, ?ERR_FORBIDDEN}; + Subscription /= pending -> + {error, ?ERR_UNEXPECTED_REQUEST}; + true -> + NewSubscription = case Allow of + true -> subscribed; + false -> none + end, + node_call(Type, set_subscription, [Host, Node, Subscriber, NewSubscription]) + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {error, Error} -> + ejabberd_router:route(To, From, + jlib:make_error_reply(Packet, Error)); + {result, _NewSubscription} -> + %% XXX: notify about subscription state change, section 12.11 + ok; + _ -> + ejabberd_router:route( + To, From, + jlib:make_error_reply(Packet, ?ERR_INTERNAL_SERVER_ERROR)) + end; + _ -> + ejabberd_router:route( + To, From, + jlib:make_error_reply(Packet, ?ERR_NOT_ACCEPTABLE)) + end. -define(XFIELD(Type, Label, Var, Val), {xmlelement, "field", [{"type", Type}, @@ -492,430 +1151,669 @@ iq_pubsub(Host, ServerHost, From, Type, SubEl, Access) -> -define(LISTXFIELD(Label, Var, Val, Opts), ?XFIELDOPT("list-single", Label, Var, Val, Opts)). +%% @spec (Host::host(), ServerHost::host(), Node::pubsubNode(), Owner::jid(), NodeType::nodeType()) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% @doc

Create new pubsub nodes

+%%

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

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

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

+%%
    +%%
  • iq_pubsub checks if service supports node creation (type exists) +%%
  • create_node checks if instant nodes are supported +%%
  • create_node asks node plugin if entity have sufficient privilege +%%
  • nodetree create_node checks if nodeid already exists +%%
  • node plugin create_node just sets default affiliation/subscription +%%
+create_node(Host, ServerHost, Node, Owner, Type) -> + create_node(Host, ServerHost, Node, Owner, Type, all, []). - -%% Create 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 - [] -> +create_node(Host, ServerHost, [], Owner, Type, Access, Configuration) -> + case lists:member("instant-nodes", features(Type)) of + true -> {LOU, LOS, _} = jlib:jid_tolower(Owner), HomeNode = ["home", LOS, LOU], - create_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}; + create_node(Host, ServerHost, + HomeNode, Owner, Type, Access, Configuration), + NewNode = HomeNode ++ [randoms:get_string()], + case create_node(Host, ServerHost, + NewNode, Owner, Type, Access, Configuration) of + {result, []} -> + {result, + [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "create", [{"node", node_to_string(NewNode)}], []}]}]}; + Error -> Error + end; + false -> + %% Service does not support instant nodes + {error, extended_error(?ERR_NOT_ACCEPTABLE, "nodeid-required")} + end; +create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> + Type = case Host of + {_User, _Server, _Resource} -> "pep"; + _ -> GivenType + end, + Parent = lists:sublist(Node, length(Node) - 1), + %% TODO, check/set node_type = Type + ParseOptions = case xml:remove_cdata(Configuration) of + [] -> + {result, node_options(Type)}; + [{xmlelement, "x", _Attrs, _SubEls} = XEl] -> + case jlib:parse_xdata_submit(XEl) of + invalid -> + {error, ?ERR_BAD_REQUEST}; + XData -> + case set_xoption(XData, node_options(Type)) of + NewOpts when is_list(NewOpts) -> + {result, NewOpts}; + Err -> + Err + end + end; + _ -> + ?INFO_MSG("Node ~p; bad configuration: ~p", [Node, Configuration]), + {error, ?ERR_BAD_REQUEST} + end, + case ParseOptions of + {result, NodeOptions} -> + CreateNode = + fun() -> + case node_call(Type, create_node_permission, [Host, ServerHost, Node, Parent, Owner, Access]) of + {result, true} -> + case tree_call(Host, create_node, [Host, Node, Type, Owner, NodeOptions]) of + ok -> + node_call(Type, create_node, [Host, Node, Owner]); + {error, ?ERR_CONFLICT} -> + case ets:lookup(gen_mod:get_module_proc(ServerHost, pubsub_state), nodetree) of + [{nodetree, nodetree_virtual}] -> node_call(Type, create_node, [Host, Node, Owner]); + _ -> {error, ?ERR_CONFLICT} + end; + Error -> + Error + end; _ -> - 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 + {error, ?ERR_FORBIDDEN} 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} + Reply = [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "create", [{"node", node_to_string(Node)}], + []}]}], + case transaction(CreateNode, transaction) of + {error, Error} -> + %% in case we change transaction to sync_dirty... + %%node_action: + %% node_call(Type, delete_node, [Host, Node]), + %% tree_call(Host, delete_node, [Host, Node]), + {error, Error}; + {result, {Result, broadcast}} -> + %%Lang = "en", %% TODO: fix + %%OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + %%broadcast_publish_item(Host, Node, uniqid(), Owner, + %% [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}], + %% [?XFIELD("hidden", "", "FORM_TYPE", ?NS_PUBSUB_NMI), + %% ?XFIELD("jid-single", "Node Creator", "creator", jlib:jid_to_string(OwnerKey))]}]), + %% todo publish_item(Host, ServerHost, ["pubsub", "nodes"], node_to_string(Node)), + case Result of + default -> {result, Reply}; + _ -> {result, Result} 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} + {result, default} -> + {result, Reply}; + {result, Result} -> + {result, Result} end; - true -> - {error, ?ERR_NOT_ALLOWED} + Error -> + Error 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} +%% @spec (Host, Node, Owner) -> +%% {error, Reason} | {result, []} +%% Host = host() +%% Node = pubsubNode() +%% Owner = jid() +%% Reason = stanzaError() +%% @doc

Delete specified node and all childs.

+%%

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

+%%
    +%%
  • The requesting entity does not have sufficient privileges to delete the node. +%%
  • The node is the root collection node, which cannot be deleted. +%%
  • The specified node does not exist. +%%
+delete_node(_Host, [], _Owner) -> + %% Node is the root + {error, ?ERR_NOT_ALLOWED}; +delete_node(Host, Node, Owner) -> + Action = fun(#pubsub_node{type = Type}) -> + case node_call(Type, get_affiliation, [Host, Node, Owner]) of + {result, owner} -> + Removed = tree_call(Host, delete_node, [Host, Node]), + node_call(Type, delete_node, [Host, Removed]); + _ -> + %% Entity is not an owner + {error, ?ERR_FORBIDDEN} + end + end, + Reply = [], + case transaction(Host, Node, Action, transaction) of + {error, Error} -> + {error, Error}; + {result, {Result, broadcast, Removed}} -> + broadcast_removed_node(Host, Removed), + %%broadcast_retract_item(Host, ["pubsub", "nodes"], node_to_string(Node)), + case Result of + default -> {result, Reply}; + _ -> {result, Result} end; - true -> - {error, ?ERR_NOT_ALLOWED} + {result, {Result, Removed}} -> + broadcast_removed_node(Host, Removed), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, default} -> + {result, Reply}; + {result, Result} -> + {result, Result} end. +%% @spec (Host, Node, From, JID) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% Host = host() +%% Node = pubsubNode() +%% From = jid() +%% JID = jid() +%% @doc

Accepts or rejects subcription requests on a PubSub node.

+%% @see node_default:subscribe_node/5 +%%

There are several reasons why the subscription request might fail:

+%%
    +%%
  • The bare JID portions of the JIDs do not match. +%%
  • The node has an access model of "presence" and the requesting entity is not subscribed to the owner's presence. +%%
  • The node has an access model of "roster" and the requesting entity is not in one of the authorized roster groups. +%%
  • The node has an access model of "whitelist" and the requesting entity is not on the whitelist. +%%
  • The service requires payment for subscriptions to the node. +%%
  • The requesting entity is anonymous and the service does not allow anonymous entities to subscribe. +%%
  • The requesting entity has a pending subscription. +%%
  • The requesting entity is blocked from subscribing (e.g., because having an affiliation of outcast). +%%
  • The node does not support subscriptions. +%%
  • The node does not exist. +%%
+subscribe_node(Host, Node, From, JID) -> + Subscriber = case jlib:string_to_jid(JID) of + error -> {"", "", ""}; + J -> jlib:jid_tolower(J) + end, + SubId = uniqid(), + Action = fun(#pubsub_node{options = Options, type = Type}) -> + Features = features(Type), + SubscribeFeature = lists:member("subscribe", Features), + SubscribeConfig = get_option(Options, subscribe), + AccessModel = get_option(Options, access_model), + SendLast = get_option(Options, send_last_published_item), + AllowedGroups = get_option(Options, roster_groups_allowed), + {PresenceSubscription, RosterGroup} = + case Host of + {OUser, OServer, _} -> + get_roster_info(OUser, OServer, + Subscriber, AllowedGroups); + _ -> + {true, true} + end, + if + not SubscribeFeature -> + %% Node does not support subscriptions + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "subscribe")}; + not SubscribeConfig -> + %% Node does not support subscriptions + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "subscribe")}; + true -> + node_call(Type, subscribe_node, + [Host, Node, From, Subscriber, + AccessModel, SendLast, + PresenceSubscription, RosterGroup]) + end + end, + Reply = fun(Subscription) -> + %% TODO, this is subscription-notification, should depends on node features + Fields = + [{"node", node_to_string(Node)}, + {"jid", jlib:jid_to_string(Subscriber)}, + {"subscription", subscription_to_string(Subscription)}], + case Subscription of + subscribed -> + [{xmlelement, "subscription", + Fields ++ [{"subid", SubId}], []}]; + _ -> + [{xmlelement, "subscription", Fields, []}] + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {error, Error} -> + {error, Error}; + {result, {Result, subscribed, send_last}} -> + %% TODO was send_last_published_item + send_all_items(Host, Node, Subscriber), + case Result of + default -> {result, Reply(subscribed)}; + _ -> {result, Result} + end; + {result, {Result, Subscription}} -> + case Subscription of + pending -> send_authorization_request(Host, Node, Subscriber); + _ -> ok + end, + case Result of + default -> {result, Reply(Subscription)}; + _ -> {result, Result} + end; + {result, Result} -> + %% this case should never occure anyway + {result, Result} + end. -get_items(Host, JID, Node, SMaxItems) -> +%% @spec (Host, Noce, From, JID) -> {error, Reason} | {result, []} +%% Host = host() +%% Node = pubsubNode() +%% From = jid() +%% JID = string() +%% SubId = string() +%% Reason = stanzaError() +%% @doc

Unsubscribe JID from the Node.

+%%

There are several reasons why the unsubscribe request might fail:

+%%
    +%%
  • The requesting entity has multiple subscriptions to the node but does not specify a subscription ID. +%%
  • The request does not specify an existing subscriber. +%%
  • The requesting entity does not have sufficient privileges to unsubscribe the specified JID. +%%
  • The node does not exist. +%%
  • The request specifies a subscription ID that is not valid or current. +%% +unsubscribe_node(Host, Node, From, JID, SubId) -> + Subscriber = case jlib:string_to_jid(JID) of + error -> {"", "", ""}; + J -> jlib:jid_tolower(J) + end, + case node_action(Host, Node, unsubscribe_node, + [Host, Node, From, Subscriber, SubId]) of + {error, Error} -> + {error, Error}; + {result, default} -> + {result, []}; + {result, Result} -> + {result, Result} + end. + +%% @spec (Host::host(), ServerHost::host(), JID::jid(), Node::pubsubNode(), ItemId::string(), Payload::term()) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% @doc

    Publish item to a PubSub node.

    +%%

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

    +%%

    There are several reasons why the publish request might fail:

    +%%
      +%%
    • The requesting entity does not have sufficient privileges to publish. +%%
    • The node does not support item publication. +%%
    • The node does not exist. +%%
    • The payload size exceeds a service-defined limit. +%%
    • The item contains more than one payload element or the namespace of the root payload element does not match the configured namespace for the node. +%%
    • The request does not match the node configuration. +%%
    +publish_item(Host, ServerHost, Node, Publisher, "", Payload) -> + %% if publisher does not specify an ItemId, the service MUST generate the ItemId + publish_item(Host, ServerHost, Node, Publisher, uniqid(), Payload); +publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) -> + Action = fun(#pubsub_node{options = Options, type = Type}) -> + Features = features(Type), + PublishFeature = lists:member("publish", Features), + Model = get_option(Options, publish_model), + MaxItems = max_items(Options), + PayloadSize = size(term_to_binary(Payload)), + PayloadMaxSize = get_option(Options, max_payload_size), + if + not PublishFeature -> + %% Node does not support item publication + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "publish")}; + PayloadSize > PayloadMaxSize -> + %% Entity attempts to publish very large payload + {error, extended_error(?ERR_NOT_ACCEPTABLE, "payload-too-big")}; + %%?? -> iq_pubsub just does that matchs + %% % Entity attempts to publish item with multiple payload elements or namespace does not match + %% {error, extended_error(?ERR_BAD_REQUEST, "invalid-payload")}; + %% % Publisher attempts to publish to persistent node with no item + %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; + Payload == "" -> + %% Publisher attempts to publish to payload node with no payload + {error, extended_error(?ERR_BAD_REQUEST, "payload-required")}; + %%?? -> + %% % Publisher attempts to publish to transient notification node with item + %% {error, extended_error(?ERR_BAD_REQUEST, "item-forbidden")}; + true -> + node_call(Type, publish_item, [Host, Node, Publisher, Model, MaxItems, ItemId, Payload]) + end + end, + %%ejabberd_hooks:run(pubsub_publish_item, Host, [Host, Node, JID, ?PUBSUB_JID, ItemId, Payload]), + Reply = [], + case transaction(Host, Node, Action, sync_dirty) of + {error, ?ERR_ITEM_NOT_FOUND} -> + %% handles auto-create feature + %% for automatic node creation. we'll take the default node type: + %% first listed into the plugins configuration option + Type = case ets:lookup(gen_mod:get_module_proc(ServerHost, pubsub_state), plugins) of + [{plugins, PL}] -> hd(PL); + _ -> ?STDNODE + end, + case lists:member("auto-create", features(Type)) of + true -> + case create_node(Host, ServerHost, Node, Publisher, Type) of + {result, [CreateRes]} -> + %% Ugly hack + SNewNode = xml:get_path_s( + CreateRes, + [{elem, "create"}, + {attr, "node"}]), + NewNode = string_to_node(SNewNode), + if + NewNode /= Node -> + publish_item(Host, ServerHost, NewNode, + Publisher, ItemId, Payload); + true -> + {error, ?ERR_ITEM_NOT_FOUND} + end; + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end; + false -> + {error, ?ERR_ITEM_NOT_FOUND} + end; + {error, Reason} -> + {error, Reason}; + {result, {Result, broadcast, Removed}} -> + lists:foreach(fun(OldItem) -> + broadcast_retract_item(Host, Node, OldItem) + end, Removed), + broadcast_publish_item(Host, Node, ItemId, jlib:jid_tolower(Publisher), Payload), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, default, Removed} -> + lists:foreach(fun(OldItem) -> + broadcast_retract_item(Host, Node, OldItem) + end, Removed), + {result, Reply}; + {result, Result, Removed} -> + lists:foreach(fun(OldItem) -> + broadcast_retract_item(Host, Node, OldItem) + end, Removed), + {result, Result}; + {result, default} -> + {result, Reply}; + {result, Result} -> + {result, Result} + end. + +%% @spec (Host::host(), JID::jid(), Node::pubsubNode(), ItemId::string()) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% @doc

    Delete item from a PubSub node.

    +%%

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

    +%%

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

    +%%
      +%%
    • The publisher does not have sufficient privileges to delete the requested item. +%%
    • The node or item does not exist. +%%
    • The request does not specify a node. +%%
    • The request does not include an element or the element does not specify an ItemId. +%%
    • The node does not support persistent items. +%%
    • The service does not support the deletion of items. +%%
    +delete_item(Host, Node, Publisher, ItemId) -> + delete_item(Host, Node, Publisher, ItemId, false). +delete_item(_, "", _, _, _) -> + %% Request does not specify a node + {error, extended_error(?ERR_BAD_REQUEST, "node-required")}; +delete_item(Host, Node, Publisher, ItemId, ForceNotify) -> + Action = fun(#pubsub_node{type = Type}) -> + Features = features(Type), + PersistentFeature = lists:member("persistent-items", Features), + DeleteFeature = lists:member("delete-nodes", Features), + if + %%?? -> iq_pubsub just does that matchs + %% %% Request does not specify an item + %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; + not PersistentFeature -> + %% Node does not support persistent items + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "persistent-items")}; + not DeleteFeature -> + %% Service does not support item deletion + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "delete-nodes")}; + true -> + node_call(Type, delete_item, [Host, Node, Publisher, ItemId]) + end + end, + Reply = [], + case transaction(Host, Node, Action, sync_dirty) of + {error, Reason} -> + {error, Reason}; + {result, {Result, broadcast}} -> + broadcast_retract_item(Host, Node, ItemId, ForceNotify), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, default} -> + {result, Reply}; + {result, Result} -> + {result, Result} + end. + +%% @spec (Host, JID, Node) -> +%% {error, Reason} | {result, []} +%% Host = host() +%% Node = pubsubNode() +%% JID = jid() +%% Reason = stanzaError() +%% @doc

    Delete all items of specified node owned by JID.

    +%%

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

    +%%
      +%%
    • The node or service does not support node purging. +%%
    • The requesting entity does not have sufficient privileges to purge the node. +%%
    • The node is not configured to persist items. +%%
    • The specified node does not exist. +%%
    +purge_node(Host, Node, Owner) -> + Action = fun(#pubsub_node{type = Type, options = Options}) -> + Features = features(Type), + PurgeFeature = lists:member("purge-nodes", Features), + PersistentFeature = lists:member("persistent-items", Features), + PersistentConfig = get_option(Options, persist_items), + if + not PurgeFeature -> + %% Service does not support node purging + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "purge-nodes")}; + not PersistentFeature -> + %% Node does not support persistent items + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "persistent-items")}; + not PersistentConfig -> + %% Node is not configured for persistent items + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "persistent-items")}; + true -> + node_call(Type, purge_node, [Host, Node, Owner]) + end + end, + Reply = [], + case transaction(Host, Node, Action, sync_dirty) of + {error, Reason} -> + {error, Reason}; + {result, {Result, broadcast}} -> + broadcast_purge_node(Host, Node), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, default} -> + {result, Reply}; + {result, Result} -> + {result, Result} + end. + +%% @doc

    Return the items of a given node.

    +%%

    The number of items to return is limited by MaxItems.

    +%%

    The permission are not checked in this function.

    +%% @todo We probably need to check that the user doing the query has the right +%% to read the items. +get_items(Host, Node, _JID, SMaxItems) -> MaxItems = if - SMaxItems == "" -> - ?MAXITEMS; + SMaxItems == "" -> ?MAXITEMS; true -> case catch list_to_integer(SMaxItems) of - {'EXIT', _} -> - {error, ?ERR_BAD_REQUEST}; - Val -> - Val + {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; + Val -> Val end end, case MaxItems of - {error, _} = Error -> - Error; + {error, 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} + case get_items(Host, Node) of + [] -> + {error, ?ERR_ITEM_NOT_FOUND}; + Items -> + %% Generate the XML response (Item list), limiting the + %% number of items sent to MaxItems: + ItemsEls = lists:map( + fun(#pubsub_item{itemid = {ItemId, _}, + payload = Payload}) -> + ItemAttrs = case ItemId of + "" -> []; + _ -> [{"id", ItemId}] + end, + {xmlelement, "item", ItemAttrs, Payload} + end, lists:sublist(Items, MaxItems)), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_EVENT}], + [{xmlelement, "items", [{"node", node_to_string(Node)}], + ItemsEls}]}]} 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} +get_items(Host, Node) -> + case node_action(Host, Node, get_items, [Host, Node]) of + {result, Items} -> Items; + _ -> [] end. +%% @spec (Host, Node, LJID) -> any() +%% Host = host() +%% Node = pubsubNode() +%% LJID = {U,S,""} +%% @doc

    Resend the items of a node to the user.

    +send_all_items(Host, Node, LJID) -> + send_items(Host, Node, LJID, all). -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} +send_last_item(Host, Node, LJID) -> + send_items(Host, Node, LJID, last). + +%% TODO use cache-last-item feature +send_items(Host, Node, LJID, Number) -> + Items = get_items(Host, Node), + ToSend = case Number of + last -> lists:sublist(Items, 1); + all -> Items; + N when N > 0 -> lists:sublist(Items, N); + _ -> Items + end, + ItemsEls = lists:map( + fun(#pubsub_item{itemid = {ItemId, _}, payload = Payload}) -> + ItemAttrs = case ItemId of + "" -> []; + _ -> [{"id", ItemId}] + end, + {xmlelement, "item", ItemAttrs, Payload} + end, ToSend), + Stanza = {xmlelement, "message", [], + [{xmlelement, "x", [{"xmlns", ?NS_PUBSUB_EVENT}], + [{xmlelement, "items", [{"node", node_to_string(Node)}], + ItemsEls}]}]}, + ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(LJID), Stanza). + +%% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response} +%% Host = host() +%% JID = jid() +%% Plugins = [Plugin::string()] +%% Reason = stanzaError() +%% Response = [pubsubIQResponse()] +%% @doc

    Return the list of affiliations as an XMPP response.

    +get_affiliations(Host, JID, Plugins) when is_list(Plugins) -> + Result = lists:foldl( + fun(Type, {Status, Acc}) -> + Features = features(Type), + RetrieveFeature = lists:member("retrieve-affiliations", Features), + if + not RetrieveFeature -> + %% Service does not support retreive affiliatons + {{error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "retrieve-affiliations")}, Acc}; + true -> + {result, Affiliations} = node_action(Type, get_entity_affiliations, [Host, JID]), + {Status, [Affiliations|Acc]} + end + end, {ok, []}, Plugins), + case Result of + {ok, Affiliations} -> + Entities = lists:flatmap( + fun({_, none}) -> []; + ({Node, Affiliation}) -> + [{xmlelement, "affiliation", + [{"node", node_to_string(Node)}, + {"affiliation", affiliation_to_string(Affiliation)}], + []}] + end, lists:flatten(Affiliations)), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "affiliations", [], + Entities}]}]}; + {Error, _} -> + Error + end; +get_affiliations(Host, Node, JID) -> + Action = fun(#pubsub_node{type = Type}) -> + Features = features(Type), + RetrieveFeature = lists:member("modify-affiliations", Features), + Affiliation = node_call(Type, get_affiliation, [Host, Node, JID]), + if + not RetrieveFeature -> + %% Service does not support modify affiliations + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "modify-affiliations")}; + Affiliation /= {result, owner} -> + %% Entity is not an owner + {error, ?ERR_FORBIDDEN}; + true -> + node_call(Type, get_node_affiliations, [Host, Node]) + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {error, Reason} -> + {error, Reason}; + {result, []} -> + {error, ?ERR_ITEM_NOT_FOUND}; + {result, Affiliations} -> + Entities = lists:flatmap( + fun({_, none}) -> []; + ({AJID, Affiliation}) -> + [{xmlelement, "affiliation", + [{"jid", jlib:jid_to_string(AJID)}, + {"affiliation", affiliation_to_string(Affiliation)}], + []}] + end, Affiliations), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_OWNER}], + [{xmlelement, "affiliations", [{"node", node_to_string(Node)}], + Entities}]}]} 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)), +set_affiliations(Host, Node, From, EntitiesEls) -> + Owner = jlib:jid_tolower(jlib:jid_remove_resource(From)), Entities = lists:foldl( fun(El, Acc) -> @@ -924,38 +1822,17 @@ set_entities(Host, OJID, Node, EntitiesEls) -> error; _ -> case El of - {xmlelement, "entity", Attrs, _} -> + {xmlelement, "affiliation", 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, + Affiliation = string_to_affiliation( + xml:get_attr_s("affiliation", Attrs)), if (JID == error) or - (Affiliation == false) or - (Subscription == false) -> + (Affiliation == false) -> error; true -> - [{jlib:jid_tolower(JID), - #entity{ - affiliation = Affiliation, - subscription = Subscription}} | - Acc] + [{jlib:jid_tolower(JID), Affiliation} | Acc] end end end @@ -964,568 +1841,743 @@ set_entities(Host, OJID, Node, EntitiesEls) -> 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 + Action = fun(#pubsub_node{type = Type, owners = Owners}) -> + case lists:member(Owner, Owners) of + true -> + lists:foreach( + fun({JID, Affiliation}) -> + node_call( + Type, set_affiliation, + [Host, Node, JID, Affiliation]) + end, Entities), + {result, []}; + _ -> + {error, ?ERR_NOT_ALLOWED} + end + end, + transaction(Host, Node, Action, sync_dirty) 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", [], +%% @spec (Host, JID, Plugins) -> {error, Reason} | {result, Response} +%% Host = host() +%% JID = jid() +%% Plugins = [Plugin::string()] +%% Reason = stanzaError() +%% Response = [pubsubIQResponse()] +%% @doc

    Return the list of subscriptions as an XMPP response.

    +get_subscriptions(Host, JID, Plugins) when is_list(Plugins) -> + Result = lists:foldl( + fun(Type, {Status, Acc}) -> + Features = features(Type), + RetrieveFeature = lists:member("retrieve-subscriptions", Features), + if + not RetrieveFeature -> + %% Service does not support retreive subscriptions + {{error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "retrieve-subscriptions")}, Acc}; + true -> + {result, Subscriptions} = node_action(Type, get_entity_subscriptions, [Host, JID]), + {Status, [Subscriptions|Acc]} + end + end, {ok, []}, Plugins), + case Result of + {ok, Subscriptions} -> + Entities = lists:flatmap( + fun({_, none}) -> []; + ({Node, Subscription}) -> + [{xmlelement, "subscription", + [{"node", node_to_string(Node)}, + {"subscription", subscription_to_string(Subscription)}], + []}]; + ({_, none, _}) -> []; + ({Node, Subscription, SubJID}) -> + [{xmlelement, "subscription", + [{"node", node_to_string(Node)}, + {"jid", jlib:jid_to_string(SubJID)}, + {"subscription", subscription_to_string(Subscription)}], + []}] + end, lists:flatten(Subscriptions)), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}], + [{xmlelement, "subscriptions", [], + Entities}]}]}; + {Error, _} -> + Error + end; +get_subscriptions(Host, Node, JID) -> + Action = fun(#pubsub_node{type = Type}) -> + Features = features(Type), + RetrieveFeature = lists:member("manage-subscriptions", Features), + Affiliation = node_call(Type, get_affiliation, [Host, Node, JID]), + if + not RetrieveFeature -> + %% Service does not support manage subscriptions + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, "manage-affiliations")}; + Affiliation /= {result, owner} -> + % Entity is not an owner + {error, ?ERR_FORBIDDEN}; + true -> + node_call(Type, get_node_subscriptions, [Host, Node]) + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {error, Reason} -> + {error, Reason}; + {result, []} -> + {error, ?ERR_ITEM_NOT_FOUND}; + {result, Subscriptions} -> + Entities = lists:flatmap( + fun({_, none}) -> []; + ({AJID, Subscription}) -> + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(AJID)}, + {"subscription", subscription_to_string(Subscription)}], + []}]; + ({AJID, Subscription, SubId}) -> + [{xmlelement, "subscription", + [{"jid", jlib:jid_to_string(AJID)}, + {"subscription", subscription_to_string(Subscription)}, + {"subid", SubId}], + []}] + end, Subscriptions), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_OWNER}], + [{xmlelement, "subscriptions", [{"node", node_to_string(Node)}], 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 = +set_subscriptions(Host, Node, From, EntitiesEls) -> + Owner = jlib:jid_tolower(jlib:jid_remove_resource(From)), + Entities = lists:foldl( - fun({JID, Ent}, Es) -> - case Ent of - #entity{affiliation = none, subscription = none} -> - ?DICT:erase(JID, Es); + fun(El, Acc) -> + case Acc of + error -> + error; _ -> - ?DICT:store(JID, Ent, Es) + case El of + {xmlelement, "subscriptions", Attrs, _} -> + JID = jlib:string_to_jid( + xml:get_attr_s("jid", Attrs)), + Subscription = string_to_subscription( + xml:get_attr_s("subscription", Attrs)), + if + (JID == error) or + (Subscription == false) -> + error; + true -> + [{jlib:jid_tolower(JID), Subscription} | Acc] + end + end end - end, Info#nodeinfo.entities, Entities), - Info#nodeinfo{entities = NewEntities}. + end, [], EntitiesEls), + case Entities of + error -> + {error, ?ERR_BAD_REQUEST}; + _ -> + Action = fun(#pubsub_node{type = Type, owners = Owners}) -> + case lists:member(Owner, Owners) of + true -> + lists:foreach(fun({JID, Subscription}) -> + node_call(Type, set_subscription, [Host, Node, JID, Subscription]) + end, Entities), + {result, []}; + _ -> + {error, ?ERR_NOT_ALLOWED} + end + end, + transaction(Host, Node, Action, sync_dirty) + end. +% @spec (OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, AllowedGroups) +get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, AllowedGroups) -> + {Subscription, Groups} = + ejabberd_hooks:run_fold( + roster_get_jid_info, OwnerServer, + {none, []}, + [OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, ""}]), + PresenceSubscription = (Subscription == both) orelse (Subscription == from), + RosterGroup = lists:any(fun(Group) -> + lists:member(Group, AllowedGroups) + end, Groups), + {PresenceSubscription, RosterGroup}. -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 mnesia:dirty_read(pubsub_presence, {Host, element(1, JID), element(2, JID)}) of - [_] -> true; - [] -> false - end; - false -> - true +%% @spec (AffiliationStr) -> Affiliation +%% AffiliationStr = string() +%% Affiliation = atom() +%% @doc

    Convert an affiliation type from string to atom.

    +string_to_affiliation("owner") -> owner; +string_to_affiliation("publisher") -> publisher; +string_to_affiliation("outcast") -> outcast; +string_to_affiliation("none") -> none; +string_to_affiliation(_) -> false. + +%% @spec (SubscriptionStr) -> Subscription +%% SubscriptionStr = string() +%% Subscription = atom() +%% @doc

    Convert a subscription type from string to atom.

    +string_to_subscription("subscribed") -> subscribed; +string_to_subscription("pending") -> pending; +string_to_subscription("unconfigured") -> unconfigured; +string_to_subscription("none") -> none; +string_to_subscription(_) -> false. + +%% @spec (Affiliation) -> AffiliationStr +%% Affiliation = atom() +%% AffiliationStr = string() +%% @doc

    Convert an affiliation type from atom to string.

    +affiliation_to_string(owner) -> "owner"; +affiliation_to_string(publisher) -> "publisher"; +affiliation_to_string(outcast) -> "outcast"; +affiliation_to_string(_) -> "none". + +%% @spec (Subscription) -> SubscriptionStr +%% Subscription = atom() +%% SubscriptionStr = string() +%% @doc

    Convert a subscription type from atom to string.

    +subscription_to_string(subscribed) -> "subscribed"; +subscription_to_string(pending) -> "pending"; +subscription_to_string(unconfigured) -> "unconfigured"; +subscription_to_string(_) -> "none". + +%% @spec (Node) -> NodeStr +%% Node = pubsubNode() +%% NodeStr = string() +%% @doc

    Convert a node type from pubsubNode to string.

    +node_to_string([]) -> "/"; +node_to_string(Node) -> + case Node of + [[_ | _] | _] -> string:strip(lists:flatten(["/", lists:map(fun(S) -> [S, "/"] end, Node)]), right, $/); + [Head | _] when is_integer(Head) -> Node + end. +string_to_node(SNode) -> + string:tokens(SNode, "/"). + + +%%%%%% broadcast functions + +broadcast_publish_item(Host, Node, ItemId, From, Payload) -> + Action = + fun(#pubsub_node{options = Options, type = Type}) -> + case node_call(Type, get_states, [Host, Node]) of + {error, _} -> {result, false}; + {result, []} -> {result, false}; + {result, States} -> + PresenceDelivery = get_option(Options, presence_based_delivery), + DeliverPayloads = get_option(Options, deliver_payloads), + BroadcastAll = get_option(Options, broadcast_all_resources), + lists:foreach( + fun(#pubsub_state{stateid = {JID, _}, + subscription = Subscription}) -> + ToBeSent = + case PresenceDelivery of + true -> + case mnesia:dirty_read( + pubsub_presence, + {Host, + element(1, JID), + element(2, JID)}) of + [_] -> true; + [] -> false + end; + false -> + true + end, + if + (Subscription /= none) and + (Subscription /= pending) and + ToBeSent -> + ItemAttrs = case ItemId of + "" -> []; + _ -> [{"id", ItemId}] + end, + Content = case DeliverPayloads of + true -> Payload; + false -> [] + end, + DestJIDs = case BroadcastAll of + true -> ejabberd_sm:get_user_resources(element(1, JID), element(2, JID)); + false -> [JID] + end, + Stanza = + {xmlelement, "message", [], + [{xmlelement, "event", + [{"xmlns", ?NS_PUBSUB_EVENT}], + [{xmlelement, "items", [{"node", node_to_string(Node)}], + [{xmlelement, "item", ItemAttrs, Content}]}]}]}, + lists:foreach( + fun(DestJID) -> + ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(DestJID), Stanza) + end, DestJIDs), + broadcast_by_caps(Host, Node, Type, Stanza), + true; + true -> + false + end + end, States), + {result, true} + end + end, + transaction(Host, Node, Action, sync_dirty). + +broadcast_retract_item(Host, Node, ItemId) -> + broadcast_retract_item(Host, Node, ItemId, false). +broadcast_retract_item(Host, Node, ItemId, ForceNotify) -> + Action = + fun(#pubsub_node{options = Options, type = Type}) -> + case node_call(Type, get_states, [Host, Node]) of + {error, _} -> {result, false}; + {result, []} -> {result, false}; + {result, States} -> + Notify = case ForceNotify of + true -> true; + _ -> get_option(Options, notify_retract) end, - if - (Subscription /= none) and - (Subscription /= pending) and - Present -> - ItemAttrs = case ItemID of - "" -> []; - _ -> [{"id", ItemID}] - end, - Content = case get_node_option( - Info, deliver_payloads) of + case Notify of + true -> + lists:foreach( + fun(#pubsub_state{stateid = {JID, _}, + 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(?PUBSUB_JID, jlib:make_jid(JID), Stanza), + broadcast_by_caps(Host, Node, Type, Stanza), + true; 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. + false + end + end, States), + {result, true}; + false -> + {result, false} + end + end + end, + transaction(Host, Node, Action, sync_dirty). +broadcast_purge_node(Host, Node) -> + Action = + fun(#pubsub_node{options = Options, type = Type}) -> + case node_call(Type, get_states, [Host, Node]) of + {error, _} -> {result, false}; + {result, []} -> {result, false}; + {result, States} -> + case get_option(Options, notify_retract) of + true -> + lists:foreach( + fun(#pubsub_state{stateid = {JID,_}, + subscription = Subscription}) -> + if (Subscription /= none) and + (Subscription /= pending) -> + Stanza = {xmlelement, "message", [], + [{xmlelement, "event", + [{"xmlns", ?NS_PUBSUB_EVENT}], + [{xmlelement, "purge", [{"node", node_to_string(Node)}], + []}]}]}, + ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(JID), Stanza), + broadcast_by_caps(Host, Node, Type, Stanza), + true; + true -> + false + end + end, States), + {result, true}; + false -> + {result, false} + end + end + end, + transaction(Host, Node, Action, sync_dirty). 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 + fun(Node) -> + Action = + fun(#pubsub_node{options = Options, type = Type}) -> + case get_option(Options, notify_delete) of + true -> + case node_call(Type, get_states, [Host, Node]) of + {result, States} -> + lists:foreach( + fun(#pubsub_state{stateid = {JID, _}, + subscription = Subscription}) -> + if (Subscription /= none) and + (Subscription /= pending) -> + Stanza = {xmlelement, "message", [], + [{xmlelement, "event", [{"xmlns", ?NS_PUBSUB_EVENT}], + [{xmlelement, "delete", [{"node", node_to_string(Node)}], + []}]}]}, + ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(JID), Stanza), + broadcast_by_caps(Host, Node, Type, Stanza), + true; + true -> + false + end + end, States), + {result, true}; + _ -> + {result, false} + end; + _ -> + {result, false} + end + end, + transaction(Host, Node, Action, sync_dirty) 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 + Action = + fun(#pubsub_node{options = Options, owners = Owners, type = Type}) -> + case node_call(Type, get_states, [Host, Node]) of + {error, _} -> {result, false}; + {result, []} -> {result, false}; + {result, States} -> + case get_option(Options, notify_config) of + true -> + lists:foreach( + fun(#pubsub_state{stateid = {JID, _}, + subscription = Subscription}) -> + ToBeSent = case get_option(Options, presence_based_delivery) of + true -> + case mnesia:dirty_read(pubsub_presence, + {Host, element(1, JID), element(2, JID)}) of + [_] -> true; + [] -> false + end; + false -> + true + end, + if (Subscription /= none) and + (Subscription /= pending) and + ToBeSent -> + Content = case get_option(Options, deliver_payloads) of + true -> + [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], + get_configure_xfields(Type, Options, Lang, Owners)}]; + false -> + [] + end, + Stanza = {xmlelement, "message", [], + [{xmlelement, "x", [{"xmlns", ?NS_PUBSUB_EVENT}], + [{xmlelement, "items", [{"node", node_to_string(Node)}], + [{xmlelement, "item", [{"id", "configuration"}], + Content}]}]}]}, + ejabberd_router:route(?PUBSUB_JID, jlib:make_jid(JID), Stanza), + broadcast_by_caps(Host, Node, Type, Stanza), + true; true -> - case mnesia:dirty_read(pubsub_presence, {Host, 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 -> + false + end + end, States), + {result, true}; + _ -> + {result, false} + end + end + end, + transaction(Host, Node, Action, dirty_sync). + +%TODO: simplify broadcast_* using a generic function like that: +%broadcast(Host, Node, Fun) -> +% transaction(fun() -> +% case tree_call(Host, get_node, [Host, Node]) of +% #pubsub_node{options = Options, owners = Owners, type = Type} -> +% case node_call(Type, get_states, [Host, Node]) of +% {error, _} -> {result, false}; +% {result, []} -> {result, false}; +% {result, States} -> +% lists:foreach(fun(#pubsub_state{stateid = {JID,_}, subscription = Subscription}) -> +% Fun(Host, Node, Options, Owners, JID, Subscription) +% end, States), +% {result, true} +% end; +% Other -> +% Other +% end +% end, sync_dirty). + + +%% broadcast Stanza to all contacts of the user that are advertising +%% interest in this kind of Node. +broadcast_by_caps({LUser, LServer, LResource}, Node, Type, Stanza) -> + ?DEBUG("looking for pid of ~p@~p/~p", [LUser, LServer, LResource]), + %% We need to know the resource, so we can ask for presence data. + SenderResources = ejabberd_sm:get_user_resources(LUser, LServer), + SenderResource = case LResource of + "" -> + %% If we don't know the resource, just pick one. + case SenderResources of + [R|_] -> + R; + [] -> + ?ERROR_MSG("~p@~p is offline; can't deliver ~p to contacts", [LUser, LServer, Stanza]), + "" + end; + _ -> + LResource + end, + case ejabberd_sm:get_session_pid(LUser, LServer, SenderResource) of + C2SPid when is_pid(C2SPid) -> + %% set the from address on the notification to the bare JID of the account owner + %% Also, add "replyto" if entity has presence subscription to the account owner + %% See XEP-0163 1.1 section 4.3.1 + Sender = jlib:make_jid(LUser, LServer, ""), + ReplyTo = jlib:make_jid(LUser, LServer, SenderResource), % This has to be used + case catch ejabberd_c2s:get_subscribed_and_online(C2SPid) of + ContactsWithCaps when is_list(ContactsWithCaps) -> + ?DEBUG("found contacts with caps: ~p", [ContactsWithCaps]), + LookingFor = Node ++ "+notify", + lists:foreach( + fun({JID, Caps}) -> + case catch mod_caps:get_features(?MYNAME, Caps) of + Features when is_list(Features) -> + case lists:member(LookingFor, Features) of + true -> + To = jlib:make_jid(JID), + ejabberd_router:route(Sender, To, Stanza); + _ -> + ok + end; + _ -> + %% couldn't get entity capabilities. + %% nothing to do about that... + ok + end + end, ContactsWithCaps); + _ -> ok - end; + end, + %% also send a notification to any + %% of the account owner's available resources. + %% See: XEP-0163 1.1 section 3 + lists:foreach(fun(Resource) -> + To = jlib:make_jid(LUser, LServer, Resource), + ejabberd_router:route(Sender, To, Stanza) + end, SenderResources), + ok; _ -> - false + ok + end; +broadcast_by_caps(_, _, _, _) -> + ok. + +%%%%%%% Configuration handling + +%%

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

    +%%
      +%%
    • The service does not support node configuration. +%%
    • The service does not support retrieval of default node configuration. +%%
    +get_configure(Host, Node, From, Lang) -> + Action = + fun(#pubsub_node{options = Options, owners = Owners, type = Type}) -> + case node_call(Type, get_affiliation, [Host, Node, From]) of + {result, owner} -> + {result, + [{xmlelement, "pubsub", + [{"xmlns", ?NS_PUBSUB_OWNER}], + [{xmlelement, "configure", + [{"node", node_to_string(Node)}], + [{xmlelement, "x", + [{"xmlns", ?NS_XDATA}, {"type", "form"}], + get_configure_xfields(Type, Options, Lang, Owners) + }]}]}]}; + _ -> + {error, ?ERR_FORBIDDEN} + end + end, + transaction(Host, Node, Action, sync_dirty). + +get_default(Host, Node, From, Lang) -> + Action = + fun(#pubsub_node{owners = Owners, type = Type}) -> + case node_call(Type, get_affiliation, [Host, Node, From]) of + {result, owner} -> + Options = node_options(Type), + {result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB_OWNER}], + [{xmlelement, "default", [], + [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], + get_configure_xfields(Type, Options, Lang, Owners) + }]}]}]}; + _ -> + {error, ?ERR_FORBIDDEN} + end + end, + transaction(Host, Node, Action, sync_dirty). + +%% Get node option +%% The result depend of the node type plugin system. +get_option([], _) -> false; +get_option(Options, Var) -> + case lists:keysearch(Var, 1, Options) of + {value, {_Val, Ret}} -> Ret; + _ -> 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} +%% Get default options from the module plugin. +node_options(Type) -> + Module = list_to_atom(?PLUGIN_PREFIX ++ Type), + case catch Module:options() of + {'EXIT',{undef,_}} -> + DefaultModule = list_to_atom(?PLUGIN_PREFIX++?STDNODE), + DefaultModule:options(); + Result -> + Result end. -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} +%% @spec (Options) -> MaxItems +%% Options = [Option] +%% Option = {Key::atom(), Value::term()} +%% MaxItems = integer() | unlimited +%% @doc

    Return the maximum number of items for a given node.

    +%%

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

    +%% @todo In practice, the current data structure means that we cannot manage +%% millions of items on a given node. This should be addressed in a new +%% version. +max_items(Options) -> + case get_option(Options, persist_items) of + true -> + case get_option(Options, max_items) of + false -> unlimited; + Result when (Result < 0) -> 0; + Result -> Result end; - _ -> - {error, ?ERR_ITEM_NOT_FOUND} + false -> + case get_option(Options, send_last_published_item) of + never -> 0; + _ -> 1 + end 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))). + get_option(Options, Var))). -define(STRING_CONFIG_FIELD(Label, Var), ?STRINGXFIELD(Label, "pubsub#" ++ atom_to_list(Var), - get_node_option(Info, Var))). + get_option(Options, Var))). -define(INTEGER_CONFIG_FIELD(Label, Var), ?STRINGXFIELD(Label, "pubsub#" ++ atom_to_list(Var), - integer_to_list(get_node_option(Info, Var)))). + integer_to_list(get_option(Options, Var)))). -define(JLIST_CONFIG_FIELD(Label, Var, Opts), ?LISTXFIELD(Label, "pubsub#" ++ atom_to_list(Var), - jlib:jid_to_string(get_node_option(Info, Var)), + jlib:jid_to_string(get_option(Options, Var)), [jlib:jid_to_string(O) || O <- Opts])). -define(ALIST_CONFIG_FIELD(Label, Var, Opts), ?LISTXFIELD(Label, "pubsub#" ++ atom_to_list(Var), - atom_to_list(get_node_option(Info, Var)), + atom_to_list(get_option(Options, 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) -> +get_configure_xfields(Type, Options, Owners, Lang) -> [?XFIELD("hidden", "", "FORM_TYPE", ?NS_PUBSUB_NODE_CONFIG), ?BOOL_CONFIG_FIELD("Deliver payloads with event notifications", deliver_payloads), + ?BOOL_CONFIG_FIELD("Deliver event notifications", deliver_notifications), ?BOOL_CONFIG_FIELD("Notify subscribers when the node configuration changes", notify_config), ?BOOL_CONFIG_FIELD("Notify subscribers when the node is deleted", notify_delete), ?BOOL_CONFIG_FIELD("Notify subscribers when items are removed from the node", notify_retract), ?BOOL_CONFIG_FIELD("Persist items to storage", persist_items), ?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 access model", access_model, + [open, authorize, presence, roster, whitelist]), + %% XXX: change to list-multi, include current roster groups as options + {xmlelement, "field", [{"type", "text-multi"}, + {"label", translate:translate(Lang, "Roster groups that may subscribe (if access model is roster)")}, + {"var", "pubsub#roster_groups_allowed"}], + [{xmlelement, "value", [], [{xmlcdata, Value}]} || + Value <- get_option(Options, roster_groups_allowed)]}, ?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)) + ?ALIST_CONFIG_FIELD("When to send the last published item", send_last_published_item, + [never, on_sub, on_sub_and_presence]), + ?BOOL_CONFIG_FIELD("Only deliver notifications to available users", presence_based_delivery) ]. - -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} +%%

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

    +%%
      +%%
    • The service does not support node configuration. +%%
    • The requesting entity does not have sufficient privileges to configure the node. +%%
    • The request did not specify a node. +%%
    • The node has no configuration options. +%%
    • The specified node does not exist. +%%
    +set_configure(Host, Node, From, Els, Lang) -> + case xml:remove_cdata(Els) of + [{xmlelement, "x", _Attrs1, _Els1} = XEl] -> + case {xml:get_tag_attr_s("xmlns", XEl), xml:get_tag_attr_s("type", XEl)} of + {?NS_XDATA, "cancel"} -> + {result, []}; + {?NS_XDATA, "submit"} -> + Action = + fun(#pubsub_node{options = Options, type = Type}=N) -> + case node_call(Type, get_affiliation, + [Host, Node, From]) of + {result, owner} -> + case jlib:parse_xdata_submit(XEl) of + invalid -> + {error, ?ERR_BAD_REQUEST}; + XData -> + OldOpts = case Options of + [] -> node_options(Type); + _ -> Options + end, + case set_xoption(XData, OldOpts) of + NewOpts when is_list(NewOpts) -> + tree_call(Host, set_node, + [N#pubsub_node{options = NewOpts}]), + {result, ok}; + Err -> + Err + end + end; + _ -> + {error, ?ERR_FORBIDDEN} + end + end, + case transaction(Host, Node, Action, transaction) of + {result, ok} -> + broadcast_config_notification(Host, Node, Lang), + {result, []}; + Other -> + Other end; _ -> - {error, ?ERR_NOT_AUTHORIZED} + {error, ?ERR_BAD_REQUEST} 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 + {error, ?ERR_BAD_REQUEST} end. add_opt(Key, Value, Opts) -> Opts1 = lists:keydelete(Key, 1, Opts), [{Key, Value} | Opts1]. - -define(SET_BOOL_XOPT(Opt, Val), - 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} + BoolVal = case Val of + "0" -> false; + "1" -> true; + "false" -> false; + "true" -> true; + _ -> error + end, + case BoolVal of + error -> {error, ?ERR_NOT_ACCEPTABLE}; + _ -> set_xoption(Opts, add_opt(Opt, false, NewOpts)) end). -define(SET_STRING_XOPT(Opt, Val), @@ -1534,28 +2586,32 @@ add_opt(Key, Value, Opts) -> -define(SET_INTEGER_XOPT(Opt, Val, Min, Max), case catch list_to_integer(Val) of IVal when is_integer(IVal), - IVal >= Min, - IVal =< Max -> + IVal >= Min, + IVal =< Max -> set_xoption(Opts, add_opt(Opt, IVal, NewOpts)); _ -> - {error, ?ERR_BAD_REQUEST} + {error, ?ERR_NOT_ACCEPTABLE} end). -define(SET_ALIST_XOPT(Opt, Val, Vals), case lists:member(Val, [atom_to_list(V) || V <- Vals]) of - true -> - set_xoption(Opts, add_opt(Opt, list_to_atom(Val), NewOpts)); - false -> - {error, ?ERR_BAD_REQUEST} + true -> set_xoption(Opts, add_opt(Opt, list_to_atom(Val), NewOpts)); + false -> {error, ?ERR_NOT_ACCEPTABLE} end). +-define(SET_LIST_XOPT(Opt, Val), + set_xoption(Opts, add_opt(Opt, list_to_atom(Val), NewOpts))). set_xoption([], NewOpts) -> NewOpts; set_xoption([{"FORM_TYPE", _} | Opts], NewOpts) -> set_xoption(Opts, NewOpts); +set_xoption([{"pubsub#roster_groups_allowed", Value} | Opts], NewOpts) -> + ?SET_LIST_XOPT(roster_groups_allowed, Value); set_xoption([{"pubsub#deliver_payloads", [Val]} | Opts], NewOpts) -> ?SET_BOOL_XOPT(deliver_payloads, Val); +set_xoption([{"pubsub#deliver_notifications", [Val]} | Opts], NewOpts) -> + ?SET_BOOL_XOPT(deliver_notifications, Val); set_xoption([{"pubsub#notify_config", [Val]} | Opts], NewOpts) -> ?SET_BOOL_XOPT(notify_config, Val); set_xoption([{"pubsub#notify_delete", [Val]} | Opts], NewOpts) -> @@ -1568,116 +2624,167 @@ 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#access_model", [Val]} | Opts], NewOpts) -> + ?SET_ALIST_XOPT(access_model, Val, [open, authorize, presence, roster, whitelist]); set_xoption([{"pubsub#publish_model", [Val]} | Opts], NewOpts) -> ?SET_ALIST_XOPT(publish_model, Val, [publishers, subscribers, open]); +set_xoption([{"pubsub#node_type", [Val]} | Opts], NewOpts) -> + ?SET_ALIST_XOPT(node_type, Val, [leaf, collection]); set_xoption([{"pubsub#max_payload_size", [Val]} | Opts], NewOpts) -> ?SET_INTEGER_XOPT(max_payload_size, Val, 0, ?MAX_PAYLOAD_SIZE); -set_xoption([{"pubsub#send_item_subscribe", [Val]} | Opts], NewOpts) -> - ?SET_BOOL_XOPT(send_item_subscribe, Val); +set_xoption([{"pubsub#send_last_published_item", [Val]} | Opts], NewOpts) -> + ?SET_ALIST_XOPT(send_last_published_item, Val, [never, on_sub, on_sub_and_presence]); set_xoption([{"pubsub#presence_based_delivery", [Val]} | Opts], NewOpts) -> ?SET_BOOL_XOPT(presence_based_delivery, Val); -set_xoption([{"pubsub#current_approver", _} | Opts], NewOpts) -> - % TODO - set_xoption(Opts, NewOpts); -%set_xoption([{"title", [Val]} | Opts], NewOpts) -> -% ?SET_STRING_XOPT(title, Val); +set_xoption([{"pubsub#title", Value} | Opts], NewOpts) -> + ?SET_STRING_XOPT(title, Value); +set_xoption([{"pubsub#type", Value} | Opts], NewOpts) -> + ?SET_STRING_XOPT(type, Value); +set_xoption([{"pubsub#body_xslt", Value} | Opts], NewOpts) -> + ?SET_STRING_XOPT(body_xslt, Value); set_xoption([_ | _Opts], _NewOpts) -> - {error, ?ERR_BAD_REQUEST}. + {error, ?ERR_NOT_ACCEPTABLE}. +%%%% plugin handling -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}; - _ -> +features() -> + [ + %"access-authorize", % OPTIONAL + "access-open", % OPTIONAL this relates to access_model option in node_default + %"access-presence", % OPTIONAL + %"access-roster", % OPTIONAL + %"access-whitelist", % OPTIONAL + % see plugin "auto-create", % OPTIONAL + % see plugin "auto-subscribe", % RECOMMENDED + "collections", % RECOMMENDED + "config-node", % RECOMMENDED + "create-and-configure", % RECOMMENDED + % see plugin "create-nodes", % RECOMMENDED + %TODO "delete-any", % OPTIONAL + % see plugin "delete-nodes", % RECOMMENDED + % see plugin "filtered-notifications", % RECOMMENDED + %TODO "get-pending", % OPTIONAL + % see plugin "instant-nodes", % RECOMMENDED + %TODO "item-ids", % RECOMMENDED + "last-published", % RECOMMENDED + %TODO "cache-last-item", + %TODO "leased-subscription", % OPTIONAL + % see plugin "manage-subscriptions", % OPTIONAL + %TODO "member-affiliation", % RECOMMENDED + %TODO "meta-data", % RECOMMENDED + % see plugin "modify-affiliations", % OPTIONAL + %TODO "multi-collection", % OPTIONAL + %TODO "multi-subscribe", % OPTIONAL + % see plugin "outcast-affiliation", % RECOMMENDED + % see plugin "persistent-items", % RECOMMENDED + "presence-notifications", % OPTIONAL + "presence-subscribe", % RECOMMENDED + % see plugin "publish", % REQUIRED + %TODO "publish-options", % OPTIONAL + "publisher-affiliation", % RECOMMENDED + % see plugin "purge-nodes", % OPTIONAL + % see plugin "retract-items", % OPTIONAL + % see plugin "retrieve-affiliations", % RECOMMENDED + "retrieve-default" % RECOMMENDED + % see plugin "retrieve-items", % RECOMMENDED + % see plugin "retrieve-subscriptions", % RECOMMENDED + % see plugin "subscribe", % REQUIRED + %TODO "subscription-options", % OPTIONAL + % see plugin "subscription-notifications" % OPTIONAL + ]. +features(Type) -> + Module = list_to_atom(?PLUGIN_PREFIX++Type), + features() ++ case catch Module:features() of + {'EXIT', {undef, _}} -> []; + Result -> Result + end. +features(Host, []) -> + features(?STDNODE); +features(Host, Node) -> + {result, Features} = node_action(Host, Node, features, []), + features() ++ Features. + +%% @doc

    node tree plugin call.

    +tree_call({_User, Server, _Resource}, Function, Args) -> + tree_call(Server, Function, Args); +tree_call(Host, Function, Args) -> + Module = case ets:lookup(gen_mod:get_module_proc(Host, pubsub_state), + nodetree) of + [{nodetree, N}] -> N; + _ -> list_to_atom(?TREE_PREFIX ++ ?STDNODE) + end, + catch apply(Module, Function, Args). +tree_action(Host, Function, Args) -> + Fun = fun() -> tree_call(Host, Function, Args) end, + catch mnesia:sync_dirty(Fun). + +%% @doc

    node plugin call.

    +node_call(Type, Function, Args) -> + Module = list_to_atom(?PLUGIN_PREFIX++Type), + case catch apply(Module, Function, Args) of + {result, Result} -> {result, Result}; + {error, Error} -> {error, Error}; + {'EXIT', {undef, Undefined}} -> + case Type of + ?STDNODE -> {error, {undef, Undefined}}; + _ -> node_call(?STDNODE, Function, Args) + end; + {'EXIT', Reason} -> {error, Reason}; + Result -> {result, Result} %% any other return value is forced as result + end. + +node_action(Type, Function, Args) -> + transaction(fun() -> + node_call(Type, Function, Args) + end, sync_dirty). +node_action(Host, Node, Function, Args) -> + transaction(fun() -> + case tree_call(Host, get_node, [Host, Node]) of + #pubsub_node{type=Type} -> node_call(Type, Function, Args); + Other -> Other + end + end, sync_dirty). + +%% @doc

    plugin transaction handling.

    +transaction(Host, Node, Action, Trans) -> + transaction(fun() -> + case tree_call(Host, get_node, [Host, Node]) of + Record when is_record(Record, pubsub_node) -> Action(Record); + Other -> Other + end + end, Trans). + +transaction(Fun, Trans) -> + case catch mnesia:Trans(Fun) of + {result, Result} -> {result, Result}; + {error, Error} -> {error, Error}; + {atomic, {result, Result}} -> {result, Result}; + {atomic, {error, Error}} -> {error, Error}; + {aborted, Reason} -> + ?ERROR_MSG("transaction return internal error: ~p~n", [{aborted, Reason}]), + {error, ?ERR_INTERNAL_SERVER_ERROR}; + {'EXIT', Reason} -> + ?ERROR_MSG("transaction return internal error: ~p~n", [{'EXIT', Reason}]), + {error, ?ERR_INTERNAL_SERVER_ERROR}; + Other -> + ?ERROR_MSG("transaction return internal error: ~p~n", [Other]), {error, ?ERR_INTERNAL_SERVER_ERROR} end. +%%%% helpers +%% Add pubsub-specific error element +extended_error(Error, Ext) -> + extended_error(Error, Ext, [{"xmlns", ?NS_PUBSUB_ERRORS}]). +extended_error(Error, unsupported, Feature) -> + extended_error(Error, "unsupported", + [{"xmlns", ?NS_PUBSUB_ERRORS}, + {"feature", Feature}]); +extended_error({xmlelement, Error, Attrs, SubEls}, Ext, ExtAttrs) -> + {xmlelement, Error, Attrs, + lists:reverse([{xmlelement, Ext, ExtAttrs, []} | SubEls])}. - - -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. - - - - +%% Give a uniq identifier +uniqid() -> + {T1, T2, T3} = now(), + lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). diff --git a/src/mod_pubsub/node.template b/src/mod_pubsub/node.template new file mode 100644 index 000000000..8e514a888 --- /dev/null +++ b/src/mod_pubsub/node.template @@ -0,0 +1,163 @@ +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% @copyright 2007 Process-one +%%% @author Christophe romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(__TO_BE_DEFINED__). +-author(__TO_BE_DEFINED__). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% Note on function definition +%% included is all defined plugin function +%% it's possible not to define some function at all +%% in that case, warning will be generated at compilation +%% and function call will fail, +%% then mod_pubsub will call function from node_default +%% (this makes code cleaner, but execution a little bit longer) + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/3, + delete_node/2, + purge_node/3, + subscribe_node/8, + unsubscribe_node/5, + publish_item/7, + delete_item/4, + remove_extra_items/4, + get_entity_affiliations/2, + get_node_affiliations/2, + get_affiliation/3, + set_affiliation/4, + get_entity_subscriptions/2, + get_node_subscriptions/2, + get_subscription/3, + set_subscription/4, + get_states/2, + get_state/3, + set_state/1, + get_items/2, + get_item/3, + set_item/1 + ]). + + +init(Host, ServerHost, Opts) -> + node_default:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_default:terminate(Host, ServerHost). + +options() -> + [{node_type, __TO_BE_DEFINED__}, + {deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {persist_items, true}, + {max_items, ?MAXITEMS div 2}, + {subscribe, true}, + {access_model, open}, + {access_roster_groups, []}, + {publish_model, publishers}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, never}, + {deliver_notifications, true}, + {presence_based_delivery, false}]. + +features() -> + ["create-nodes", + "delete-nodes", + "instant-nodes", + "item-ids", + "outcast-affiliation", + "persistent-items", + "publish", + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", + "retrieve-subscriptions", + "subscribe", + "subscription-notifications" + ]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_default:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Host, Node, Owner) -> + node_default:create_node(Host, Node, Owner). + +delete_node(Host, Removed) -> + node_default:delete_node(Host, Removed). + +subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> + node_default:subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). + +unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> + node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID). + +publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> + node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(Host, Node, MaxItems, ItemIds) -> + node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). + +delete_item(Host, Node, JID, ItemId) -> + node_default:delete_item(Host, Node, JID, ItemId). + +purge_node(Host, Node, Owner) -> + node_default:purge_node(Host, Node, Owner). + +get_entity_affiliations(Host, Owner) -> + node_default:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Host, Node) -> + node_default:get_node_affiliations(Host, Node). + +get_affiliation(Host, Node, Owner) -> + node_default:get_affiliation(Host, Node, Owner). + +set_affiliation(Host, Node, Owner, Affiliation) -> + node_default:set_affiliation(Host, Node, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_default:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Host, Node) -> + node_default:get_node_subscriptions(Host, Node). + +get_subscription(Host, Node, Owner) -> + node_default:get_subscription(Host, Node, Owner). + +set_subscription(Host, Node, Owner, Subscription) -> + node_default:set_subscription(Host, Node, Owner, Subscription). + +get_states(Host, Node) -> + node_default:get_states(Host, Node). + +get_state(Host, Node, JID) -> + node_default:get_state(Host, Node, JID). + +set_state(State) -> + node_default:set_state(State). + +get_items(Host, Node) -> + node_default:get_items(Host, Node). + +get_item(Host, Node, ItemId) -> + node_default:get_items(Host, Node, ItemId). + +set_item(Item) -> + node_default:set_item(Item). diff --git a/src/mod_pubsub/node_buddy.erl b/src/mod_pubsub/node_buddy.erl new file mode 100644 index 000000000..2d240e140 --- /dev/null +++ b/src/mod_pubsub/node_buddy.erl @@ -0,0 +1,163 @@ +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% @copyright 2007 Process-one +%%% @author Christophe romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(node_buddy). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% Note on function definition +%% included is all defined plugin function +%% it's possible not to define some function at all +%% in that case, warning will be generated at compilation +%% and function call will fail, +%% then mod_pubsub will call function from node_default +%% (this makes code cleaner, but execution a little bit longer) + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/3, + delete_node/2, + purge_node/3, + subscribe_node/8, + unsubscribe_node/5, + publish_item/7, + delete_item/4, + remove_extra_items/4, + get_entity_affiliations/2, + get_node_affiliations/2, + get_affiliation/3, + set_affiliation/4, + get_entity_subscriptions/2, + get_node_subscriptions/2, + get_subscription/3, + set_subscription/4, + get_states/2, + get_state/3, + set_state/1, + get_items/2, + get_item/3, + set_item/1 + ]). + + +init(Host, ServerHost, Opts) -> + node_default:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_default:terminate(Host, ServerHost). + +options() -> + [{node_type, buddy}, + {deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {persist_items, true}, + {max_items, ?MAXITEMS div 2}, + {subscribe, true}, + {access_model, presence}, + {access_roster_groups, []}, + {publish_model, publishers}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, never}, + {deliver_notifications, true}, + {presence_based_delivery, false}]. + +features() -> + ["create-nodes", + "delete-nodes", + "instant-nodes", + "item-ids", + "outcast-affiliation", + "persistent-items", + "publish", + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", + "retrieve-subscriptions", + "subscribe", + "subscription-notifications" + ]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_default:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Host, Node, Owner) -> + node_default:create_node(Host, Node, Owner). + +delete_node(Host, Removed) -> + node_default:delete_node(Host, Removed). + +subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> + node_default:subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). + +unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> + node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID). + +publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> + node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(Host, Node, MaxItems, ItemIds) -> + node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). + +delete_item(Host, Node, JID, ItemId) -> + node_default:delete_item(Host, Node, JID, ItemId). + +purge_node(Host, Node, Owner) -> + node_default:purge_node(Host, Node, Owner). + +get_entity_affiliations(Host, Owner) -> + node_default:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Host, Node) -> + node_default:get_node_affiliations(Host, Node). + +get_affiliation(Host, Node, Owner) -> + node_default:get_affiliation(Host, Node, Owner). + +set_affiliation(Host, Node, Owner, Affiliation) -> + node_default:set_affiliation(Host, Node, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_default:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Host, Node) -> + node_default:get_node_subscriptions(Host, Node). + +get_subscription(Host, Node, Owner) -> + node_default:get_subscription(Host, Node, Owner). + +set_subscription(Host, Node, Owner, Subscription) -> + node_default:set_subscription(Host, Node, Owner, Subscription). + +get_states(Host, Node) -> + node_default:get_states(Host, Node). + +get_state(Host, Node, JID) -> + node_default:get_state(Host, Node, JID). + +set_state(State) -> + node_default:set_state(State). + +get_items(Host, Node) -> + node_default:get_items(Host, Node). + +get_item(Host, Node, ItemId) -> + node_default:get_items(Host, Node, ItemId). + +set_item(Item) -> + node_default:set_item(Item). diff --git a/src/mod_pubsub/node_club.erl b/src/mod_pubsub/node_club.erl new file mode 100644 index 000000000..a0ea2b071 --- /dev/null +++ b/src/mod_pubsub/node_club.erl @@ -0,0 +1,163 @@ +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% @copyright 2007 Process-one +%%% @author Christophe romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(node_club). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% Note on function definition +%% included is all defined plugin function +%% it's possible not to define some function at all +%% in that case, warning will be generated at compilation +%% and function call will fail, +%% then mod_pubsub will call function from node_default +%% (this makes code cleaner, but execution a little bit longer) + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/3, + delete_node/2, + purge_node/3, + subscribe_node/8, + unsubscribe_node/5, + publish_item/7, + delete_item/4, + remove_extra_items/4, + get_entity_affiliations/2, + get_node_affiliations/2, + get_affiliation/3, + set_affiliation/4, + get_entity_subscriptions/2, + get_node_subscriptions/2, + get_subscription/3, + set_subscription/4, + get_states/2, + get_state/3, + set_state/1, + get_items/2, + get_item/3, + set_item/1 + ]). + + +init(Host, ServerHost, Opts) -> + node_default:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_default:terminate(Host, ServerHost). + +options() -> + [{node_type, club}, + {deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {persist_items, true}, + {max_items, ?MAXITEMS div 2}, + {subscribe, true}, + {access_model, authorize}, + {access_roster_groups, []}, + {publish_model, publishers}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, never}, + {deliver_notifications, true}, + {presence_based_delivery, false}]. + +features() -> + ["create-nodes", + "delete-nodes", + "instant-nodes", + "item-ids", + "outcast-affiliation", + "persistent-items", + "publish", + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", + "retrieve-subscriptions", + "subscribe", + "subscription-notifications" + ]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_default:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Host, Node, Owner) -> + node_default:create_node(Host, Node, Owner). + +delete_node(Host, Removed) -> + node_default:delete_node(Host, Removed). + +subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> + node_default:subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). + +unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> + node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID). + +publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> + node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(Host, Node, MaxItems, ItemIds) -> + node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). + +delete_item(Host, Node, JID, ItemId) -> + node_default:delete_item(Host, Node, JID, ItemId). + +purge_node(Host, Node, Owner) -> + node_default:purge_node(Host, Node, Owner). + +get_entity_affiliations(Host, Owner) -> + node_default:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Host, Node) -> + node_default:get_node_affiliations(Host, Node). + +get_affiliation(Host, Node, Owner) -> + node_default:get_affiliation(Host, Node, Owner). + +set_affiliation(Host, Node, Owner, Affiliation) -> + node_default:set_affiliation(Host, Node, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_default:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Host, Node) -> + node_default:get_node_subscriptions(Host, Node). + +get_subscription(Host, Node, Owner) -> + node_default:get_subscription(Host, Node, Owner). + +set_subscription(Host, Node, Owner, Subscription) -> + node_default:set_subscription(Host, Node, Owner, Subscription). + +get_states(Host, Node) -> + node_default:get_states(Host, Node). + +get_state(Host, Node, JID) -> + node_default:get_state(Host, Node, JID). + +set_state(State) -> + node_default:set_state(State). + +get_items(Host, Node) -> + node_default:get_items(Host, Node). + +get_item(Host, Node, ItemId) -> + node_default:get_items(Host, Node, ItemId). + +set_item(Item) -> + node_default:set_item(Item). diff --git a/src/mod_pubsub/node_default.erl b/src/mod_pubsub/node_default.erl new file mode 100644 index 000000000..02a6983f1 --- /dev/null +++ b/src/mod_pubsub/node_default.erl @@ -0,0 +1,712 @@ +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% @copyright 2007 Process-one +%%% @author Christophe Romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @todo The item table should be handled by the plugin, but plugin that do +%%% not want to manage it should be able to use the default behaviour. +%%% @todo Plugin modules should be able to register to receive presence update +%%% send to pubsub. + +%%% @doc The module {@module} is the default PubSub plugin. +%%%

    It is used as a default for all unknown PubSub node type. It can serve +%%% as a developer basis and reference to build its own custom pubsub node +%%% types.

    +%%%

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

    +%%%

    The API isn't stabilized yet. The pubsub plugin +%%% development is still a work in progress. However, the system is already +%%% useable and useful as is. Please, send us comments, feedback and +%%% improvements.

    + +-module(node_default). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/3, + delete_node/2, + purge_node/3, + subscribe_node/8, + unsubscribe_node/5, + publish_item/7, + delete_item/4, + remove_extra_items/4, + get_entity_affiliations/2, + get_node_affiliations/2, + get_affiliation/3, + set_affiliation/4, + get_entity_subscriptions/2, + get_node_subscriptions/2, + get_subscription/3, + set_subscription/4, + get_states/2, + get_state/3, + set_state/1, + get_items/2, + get_item/3, + set_item/1 + ]). + +%% ================ +%% API definition +%% ================ + +%% @spec (Host) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = host() +%% Opts = list() +%% @doc

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

    +%%

    This function is mainly used to trigger the setup task necessary for the +%% plugin. It can be used for example by the developer to create the specific +%% module database schema if it does not exists yet.

    +init(_Host, _ServerHost, _Opts) -> + mnesia:create_table(pubsub_state, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_state)}]), + StatesFields = record_info(fields, pubsub_state), + case mnesia:table_info(pubsub_state, attributes) of + StatesFields -> ok; + _ -> + mnesia:transform_table(pubsub_state, ignore, StatesFields) + end, + mnesia:create_table(pubsub_item, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, pubsub_item)}]), + ItemsFields = record_info(fields, pubsub_item), + case mnesia:table_info(pubsub_item, attributes) of + ItemsFields -> ok; + _ -> + mnesia:transform_table(pubsub_item, ignore, ItemsFields) + end, + ok. + +%% @spec (Host) -> any() +%% Host = mod_pubsub:host() +%% @doc

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

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

    Example of function return value:

    +%% ``` +%% [{deliver_payloads, true}, +%% {notify_config, false}, +%% {notify_delete, false}, +%% {notify_retract, true}, +%% {persist_items, true}, +%% {max_items, 10}, +%% {subscribe, true}, +%% {access_model, open}, +%% {publish_model, publishers}, +%% {max_payload_size, 100000}, +%% {send_last_published_item, never}, +%% {presence_based_delivery, false}]''' +options() -> + [{node_type, default}, + {deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {persist_items, true}, + {max_items, ?MAXITEMS div 2}, + {subscribe, true}, + {access_model, open}, + {access_roster_groups, []}, + {publish_model, publishers}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, never}, + {deliver_notifications, true}, + {presence_based_delivery, false}]. + +%% @spec () -> [] +%% @doc Returns the node features +features() -> + ["create-nodes", + "auto-create", + "delete-nodes", + "instant-nodes", + "item-ids", + "manage-subscriptions", + "outcast-affiliation", + "persistent-items", + "publish", + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", + "retrieve-subscriptions", + "subscribe", + "subscription-notifications" + ]. + +%% @spec (Host, Node, Owner, Access) -> bool() +%% Host = mod_pubsub:host() +%% ServerHost = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% Owner = mod_pubsub:jid() +%% Access = all | atom() +%% @doc Checks if the current user has the permission to create the requested node +%%

    In {@link node_default}, the permission is decided by the place in the +%% hierarchy where the user is creating the node. The access parameter is also +%% checked in the default module. This parameter depends on the value of the +%% access_createnode ACL value in ejabberd config file.

    +%%

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

    +%%

    PubSub plugins can redefine the PubSub node creation rights as they +%% which. They can simply delegate this check to the {@link node_default} +%% module by implementing this function like this: +%% ```check_create_user_permission(Host, Node, Owner, Access) -> +%% node_default:check_create_user_permission(Host, Node, Owner, Access).'''

    +create_node_permission(Host, ServerHost, Node, _ParentNode, Owner, Access) -> + LOwner = jlib:jid_tolower(Owner), + {User, Server, _Resource} = LOwner, + Allowed = case acl:match_rule(ServerHost, Access, LOwner) of + allow -> + if Server == Host -> %% Server == ServerHost ?? + true; + true -> + case Node of + ["home", Server, User | _] -> true; + _ -> false + end + end; + _ -> + case Owner of + ?PUBSUB_JID -> true; + _ -> false + end + end, + ChildOK = true, %% TODO test with ParentNode + {result, Allowed and ChildOK}. + +%% @spec (Host, Node, Owner) -> +%% {result, Result} | exit +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% Owner = mod_pubsub:jid() +%% @doc

    +create_node(Host, Node, Owner) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + mnesia:write(#pubsub_state{stateid = {OwnerKey, {Host, Node}}, + affiliation = owner, subscription = none}), + {result, {default, broadcast}}. + + +%% @spec (Host, Removed) -> ok +%% Host = mod_pubsub:host() +%% Removed = [mod_pubsub:pubsubNode()] +%% @doc

    purge items of deleted nodes after effective deletion.

    +delete_node(Host, Removed) -> + lists:foreach( + fun(Node) -> + lists:foreach( + fun(#pubsub_state{stateid = StateId, items = Items}) -> + lists:foreach( + fun(ItemId) -> + mnesia:delete( + {pubsub_item, {ItemId, {Host, Node}}}) + end, Items), + mnesia:delete({pubsub_state, StateId}) + end, + mnesia:match_object( + #pubsub_state{stateid = {'_', {Host, Node}}, _ = '_'})) + end, Removed), + {result, {default, broadcast, Removed}}. + +%% @spec (Host, Node, Sender, Subscriber, AccessModel, SendLast) -> +%% {error, Reason} | {result, Result} +%% @doc

    Accepts or rejects subcription requests on a PubSub node.

    +%%

    The mechanism works as follow: +%%

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

    +%%

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

      +%%
    • {error, Reason}: an IQ error result will be returned. No +%% subscription will actually be performed.
    • +%%
    • true: Subscribe operation is allowed, based on the +%% unmodified record passed in parameter SubscribeResult. If this +%% parameter contains an error, no subscription will be performed.
    • +%%
    • {true, PubsubState}: Subscribe operation is allowed, but +%% the {@link mod_pubsub:pubsubState()} record returned replaces the value +%% passed in parameter SubscribeResult.
    • +%%
    • {true, done}: Subscribe operation is allowed, but the +%% {@link mod_pubsub:pubsubState()} will be considered as already stored and +%% no further persistance operation will be performed. This case is used, +%% when the plugin module is doing the persistance by itself or when it want +%% to completly disable persistance.
    +%%

    +%%

    In the default plugin module, the record is unchanged.

    +subscribe_node(Host, Node, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup) -> + SenderKey = jlib:jid_tolower(Sender), + Authorized = (jlib:jid_remove_resource(SenderKey) == jlib:jid_remove_resource(Subscriber)), + % TODO add some acl check for Authorized ? + State = case get_state(Host, Node, Subscriber) of + {error, ?ERR_ITEM_NOT_FOUND} -> + #pubsub_state{stateid = {Subscriber, {Host, Node}}}; % TODO: bug on Key ? + {result, S} -> S + end, + #pubsub_state{affiliation = Affiliation, + subscription = Subscription} = State, + if + not Authorized -> + %% JIDs do not match + {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "invalid-jid")}; + Subscription == pending -> + %% Requesting entity has pending subscription + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "pending-subscription")}; + Affiliation == outcast -> + %% Requesting entity is blocked + {error, ?ERR_FORBIDDEN}; + (AccessModel == presence) and (not PresenceSubscription) -> + %% Entity is not authorized to create a subscription (presence subscription required) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "presence-subscription-required")}; + (AccessModel == roster) and (not RosterGroup) -> + %% Entity is not authorized to create a subscription (not in roster group) + {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "not-in-roster-group")}; + (AccessModel == whitelist) -> % TODO: to be done + %% Node has whitelist access model + {error, ?ERR_EXTENDED(?ERR_NOT_ALLOWED, "closed-node")}; + (AccessModel == authorize) -> % TODO: to be done + %% Node has authorize access model + {error, ?ERR_FORBIDDEN}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + %%ForbiddenAnonymous -> + %% % Requesting entity is anonymous + %% {error, ?ERR_FORBIDDEN}; + true -> + NewSubscription = + if + AccessModel == authorize -> + pending; + %%TODO Affiliation == none -> ? + %%NeedConfiguration -> + %% unconfigured + true -> + subscribed + end, + set_state(State#pubsub_state{subscription = NewSubscription}), + case NewSubscription of + subscribed -> + case SendLast of + never -> {result, {default, NewSubscription}}; + _ -> {result, {default, NewSubscription, send_last}} + end; + _ -> + {result, {default, NewSubscription}} + end + end. + +%% @spec (Host, Node, Sender, Subscriber, SubID) -> +%% {error, Reason} | {result, []} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% Sender = mod_pubsub:jid() +%% Subscriber = mod_pubsub:jid() +%% SubID = string() +%% Reason = mod_pubsub:stanzaError() +%% @doc

    Unsubscribe the Subscriber from the Node.

    +unsubscribe_node(Host, Node, Sender, Subscriber, _SubId) -> + SenderKey = jlib:jid_tolower(Sender), + Match = jlib:jid_remove_resource(SenderKey) == jlib:jid_remove_resource(Subscriber), + Authorized = case Match of + true -> + true; + false -> + case get_state(Host, Node, SenderKey) of % TODO: bug on Key ? + {result, #pubsub_state{affiliation=owner}} -> true; + _ -> false + end + end, + case get_state(Host, Node, Subscriber) of + {error, ?ERR_ITEM_NOT_FOUND} -> + %% Requesting entity is not a subscriber + {error, ?ERR_EXTENDED(?ERR_UNEXPECTED_REQUEST, "not-subscribed")}; + {result, State} -> + if + %% Entity did not specify SubID + %%SubID == "", ?? -> + %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %% Invalid subscription identifier + %%InvalidSubID -> + %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + %% Requesting entity is not a subscriber + State#pubsub_state.subscription == none -> + {error, ?ERR_EXTENDED(?ERR_UNEXPECTED_REQUEST, "not-subscribed")}; + %% Requesting entity is prohibited from unsubscribing entity + not Authorized -> + {error, ?ERR_FORBIDDEN}; + true -> + set_state(State#pubsub_state{subscription = none}), + {result, default} + end + end. + +%% @spec (Host, Node, Publisher, PublishModel, MaxItems, ItemId, Payload) -> +%% {true, PubsubItem} | {result, Reply} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% Publisher = mod_pubsub:jid() +%% PublishModel = atom() +%% MaxItems = integer() +%% ItemId = string() +%% Payload = term() +%% @doc

    Publishes the item passed as parameter.

    +%%

    The mechanism works as follow: +%%

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

    +%%

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

      +%%
    • {error, Reason}: an iq error result will be return. No +%% publication is actually performed.
    • +%%
    • true: Publication operation is allowed, based on the +%% unmodified record passed in parameter Item. If the Item +%% parameter contains an error, no subscription will actually be +%% performed.
    • +%%
    • {true, Item}: Publication operation is allowed, but the +%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed +%% in parameter Item. The persistance will be performed by the main +%% module.
    • +%%
    • {true, done}: Publication operation is allowed, but the +%% {@link mod_pubsub:pubsubItem()} will be considered as already stored and +%% no further persistance operation will be performed. This case is used, +%% when the plugin module is doing the persistance by itself or when it want +%% to completly disable persistance.
    +%%

    +%%

    In the default plugin module, the record is unchanged.

    +publish_item(Host, Node, Publisher, PublishModel, MaxItems, ItemId, Payload) -> + PublisherKey = jlib:jid_tolower(jlib:jid_remove_resource(Publisher)), + State = case get_state(Host, Node, PublisherKey) of + {error, ?ERR_ITEM_NOT_FOUND} -> #pubsub_state{stateid={PublisherKey, {Host, Node}}}; + {result, S} -> S + end, + #pubsub_state{affiliation = Affiliation, + subscription = Subscription} = State, + if + not ((PublishModel == open) + or ((PublishModel == publishers) + and ((Affiliation == owner) or (Affiliation == publisher))) + or ((PublishModel == subscribers) + and (Subscription == subscribed))) -> + %% Entity does not have sufficient privileges to publish to node + {error, ?ERR_FORBIDDEN}; + true -> + PubId = {PublisherKey, now()}, + Item = case get_item(Host, Node, ItemId) of + {error, ?ERR_ITEM_NOT_FOUND} -> + #pubsub_item{itemid = {ItemId, {Host, Node}}, + creation = PubId, + modification = PubId, + payload = Payload}; + {result, OldItem} -> + OldItem#pubsub_item{modification = PubId, + payload = Payload} + end, + Items = [ItemId | State#pubsub_state.items], + {result, {NI, OI}} = remove_extra_items( + Host, Node, MaxItems, Items), + set_item(Item), + set_state(State#pubsub_state{items = NI}), + {result, {default, broadcast, OI}} + end. + +%% @spec (Host, Node, MaxItems, ItemIds) -> {NewItemIds,OldItemIds} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% MaxItems = integer() | unlimited +%% ItemIds = [ItemId::string()] +%% NewItemIds = [ItemId::string()] +%% @doc

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

    +%%

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

    +%%

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

    +%%

    If another PubSub plugin wants to delegate the item removal (and if the +%% plugin is using the default pubsub storage), it can implements this function like this: +%% ```remove_extra_items(Host, Node, MaxItems, ItemIds) -> +%% node_default:remove_extra_items(Host, Node, MaxItems, ItemIds).'''

    +remove_extra_items(_Host, _Node, unlimited, ItemIds) -> + {result, {ItemIds, []}}; +remove_extra_items(Host, Node, MaxItems, ItemIds) -> + NewItems = lists:sublist(ItemIds, MaxItems), + OldItems = lists:nthtail(length(NewItems), ItemIds), + %% Remove extra items: + lists:foreach(fun(ItemId) -> + mnesia:delete({pubsub_item, {ItemId, {Host, Node}}}) + end, OldItems), + %% Return the new items list: + {result, {NewItems, OldItems}}. + +%% @spec (Host, Node, JID, ItemId) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% JID = mod_pubsub:jid() +%% ItemId = string() +%% @doc

    Triggers item deletion.

    +%%

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

    +delete_item(Host, Node, Publisher, ItemId) -> + PublisherKey = jlib:jid_tolower(jlib:jid_remove_resource(Publisher)), + State = case get_state(Host, Node, PublisherKey) of + {error, ?ERR_ITEM_NOT_FOUND} -> + #pubsub_state{stateid = {PublisherKey, {Host, Node}}}; + {result, S} -> + S + end, + #pubsub_state{affiliation = Affiliation, items = Items} = State, + Allowed = (Affiliation == publisher) orelse (Affiliation == owner) + orelse case get_item(Host, Node, ItemId) of + {result, #pubsub_item{creation = {PublisherKey, _}}} -> true; + _ -> false + end, + if + not Allowed -> + %% Requesting entity does not have sufficient privileges + {error, ?ERR_FORBIDDEN}; + true -> + case get_item(Host, Node, ItemId) of + {result, _} -> + mnesia:delete({pubsub_item, {ItemId, {Host, Node}}}), + NewItems = lists:delete(ItemId, Items), + set_state(State#pubsub_state{items = NewItems}), + {result, {default, broadcast}}; + _ -> + %% Non-existent node or item + {error, ?ERR_ITEM_NOT_FOUND} + end + end. + +%% @spec (TODO) +purge_node(Host, Node, Owner) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + case get_state(Host, Node, OwnerKey) of + {error, ?ERR_ITEM_NOT_FOUND} -> + %% This should not append (case node does not exists) + {error, ?ERR_ITEM_NOT_FOUND}; + {result, #pubsub_state{items = Items, affiliation = owner}} -> + lists:foreach(fun(ItemId) -> + mnesia:delete({pubsub_item, {ItemId, {Host, Node}}}) + end, Items), + {result, {default, broadcast}}; + _ -> + %% Entity is not an owner + {error, ?ERR_FORBIDDEN} + end. + +%% @spec (Host, JID) -> [{Node,Affiliation}] +%% Host = host() +%% JID = mod_pubsub:jid() +%% @doc

    Return the current affiliations for the given user

    +%%

    The default module reads affiliations in the main Mnesia +%% pubsub_state table. If a plugin stores its data in the same +%% table, it should return an empty list, as the affiliation will be read by +%% the default PubSub module. Otherwise, it should return its own affiliation, +%% that will be added to the affiliation stored in the main +%% pubsub_state table.

    +get_entity_affiliations(Host, Owner) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + States = mnesia:match_object( + #pubsub_state{stateid = {OwnerKey, {Host, '_'}}, + _ = '_'}), + Tr = fun(#pubsub_state{stateid = {_, {_, N}}, affiliation = A}) -> + {N, A} + end, + {result, lists:map(Tr, States)}. + +get_node_affiliations(Host, Node) -> + States = mnesia:match_object( + #pubsub_state{stateid = {'_', {Host, Node}}, + _ = '_'}), + Tr = fun(#pubsub_state{stateid = {J, {_, _}}, affiliation = A}) -> + {J, A} + end, + {result, lists:map(Tr, States)}. + +get_affiliation(Host, Node, Owner) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + Affiliation = case get_state(Host, Node, OwnerKey) of + {result, #pubsub_state{affiliation = A}} -> A; + _ -> unknown + end, + {result, Affiliation}. + +set_affiliation(Host, Node, Owner, Affiliation) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + Record = case get_state(Host, Node, OwnerKey) of + {error, ?ERR_ITEM_NOT_FOUND} -> + #pubsub_state{stateid = {OwnerKey, {Host, Node}}, + affiliation = Affiliation}; + {result, State} -> + State#pubsub_state{affiliation = Affiliation} + end, + set_state(Record), + ok. + +%% @spec (Host) -> [{Node,Subscription}] +%% Host = host() +%% JID = mod_pubsub:jid() +%% @doc

    Return the current subscriptions for the given user

    +%%

    The default module reads subscriptions in the main Mnesia +%% pubsub_state table. If a plugin stores its data in the same +%% table, it should return an empty list, as the affiliation will be read by +%% the default PubSub module. Otherwise, it should return its own affiliation, +%% that will be added to the affiliation stored in the main +%% pubsub_state table.

    +get_entity_subscriptions(Host, Owner) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + States = mnesia:match_object( + #pubsub_state{stateid = {OwnerKey, {Host, '_'}}, + _ = '_'}), + Tr = fun(#pubsub_state{stateid = {_, {_, N}}, subscription = S}) -> + {N, S} + end, + {result, lists:map(Tr, States)}. + +get_node_subscriptions(Host, Node) -> + States = mnesia:match_object( + #pubsub_state{stateid = {'_', {Host, Node}}, + _ = '_'}), + Tr = fun(#pubsub_state{stateid = {J, {_, _}}, subscription = S}) -> + {J, S} + end, + {result, lists:map(Tr, States)}. + +get_subscription(Host, Node, Owner) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + Subscription = case get_state(Host, Node, OwnerKey) of + {result, #pubsub_state{subscription = S}} -> S; + _ -> unknown + end, + {result, Subscription}. + +set_subscription(Host, Node, Owner, Subscription) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + Record = case get_state(Host, Node, OwnerKey) of + {error, ?ERR_ITEM_NOT_FOUND} -> + #pubsub_state{stateid = {OwnerKey, {Host, Node}}, + subscription = Subscription}; + {result, State} -> + State#pubsub_state{subscription = Subscription} + end, + set_state(Record), + ok. + +%% @spec (Host, Node) -> [States] | [] +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% Item = mod_pubsub:pubsubItems() +%% @doc Returns the list of stored states for a given node. +%%

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

    +%%

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

    +%%

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

    +%%

    If a PubSub plugin wants to delegate the states storage to the default node, +%% they can implement this function like this: +%% ```get_states(Host, Node) -> +%% node_default:get_states(Host, Node).'''

    +get_states(Host, Node) -> + States = mnesia:match_object( + #pubsub_state{stateid = {'_', {Host, Node}}, _ = '_'}), + {result, States}. + +%% @spec (JID, Host, Node) -> [State] | [] +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% JID = mod_pubsub:jid() +%% State = mod_pubsub:pubsubItems() +%% @doc

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

    +get_state(Host, Node, JID) -> + case mnesia:read({pubsub_state, {JID, {Host, Node}}}) of + [State] when is_record(State, pubsub_state) -> + {result, State}; + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end. + +%% @spec (State) -> ok | {error, ?ERR_INTERNAL_SERVER_ERROR} +%% State = mod_pubsub:pubsubStates() +%% @doc

    Write a state into database.

    +set_state(State) when is_record(State, pubsub_state) -> + mnesia:write(State); +set_state(_) -> + {error, ?ERR_INTERNAL_SERVER_ERROR}. + +%% @spec (Host, Node) -> [Items] | [] +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% Items = mod_pubsub:pubsubItems() +%% @doc Returns the list of stored items for a given node. +%%

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

    +%%

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

    +%%

    PubSub plugins can store the items where they wants (for example in a +%% relational database), or they can even decide not to persist any items.

    +%%

    If a PubSub plugin wants to delegate the item storage to the default node, +%% they can implement this function like this: +%% ```get_items(Host, Node) -> +%% node_default:get_items(Host, Node).'''

    +get_items(Host, Node) -> + Items = mnesia:match_object( + #pubsub_item{itemid = {'_', {Host, Node}}, _ = '_'}), + {result, Items}. + +%% @spec (Host, Node, ItemId) -> [Item] | [] +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% ItemId = string() +%% Item = mod_pubsub:pubsubItems() +%% @doc

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

    +get_item(Host, Node, ItemId) -> + case mnesia:read({pubsub_item, {ItemId, {Host, Node}}}) of + [Item] when is_record(Item, pubsub_item) -> + {result, Item}; + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end. + +%% @spec (Item) -> ok | {error, ?ERR_INTERNAL_SERVER_ERROR} +%% Item = mod_pubsub:pubsubItems() +%% @doc

    Write a state into database.

    +set_item(Item) when is_record(Item, pubsub_item) -> + mnesia:write(Item); +set_item(_) -> + {error, ?ERR_INTERNAL_SERVER_ERROR}. diff --git a/src/mod_pubsub/node_dispatch.erl b/src/mod_pubsub/node_dispatch.erl new file mode 100644 index 000000000..e0b9d0277 --- /dev/null +++ b/src/mod_pubsub/node_dispatch.erl @@ -0,0 +1,166 @@ +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% @copyright 2007 Process-one +%%% @author Christophe romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(node_dispatch). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%%% @doc

    The {@module} module is a PubSub plugin whose +%%% goal is to republished each published item to all its children.

    +%%%

    Users cannot subscribe to this node, but are supposed to subscribe to +%%% its children.

    +%%% This module can not work with virtual nodetree + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/3, + delete_node/2, + purge_node/3, + subscribe_node/8, + unsubscribe_node/5, + publish_item/7, + delete_item/4, + remove_extra_items/4, + get_entity_affiliations/2, + get_node_affiliations/2, + get_affiliation/3, + set_affiliation/4, + get_entity_subscriptions/2, + get_node_subscriptions/2, + get_subscription/3, + set_subscription/4, + get_states/2, + get_state/3, + set_state/1, + get_items/2, + get_item/3, + set_item/1 + ]). + + +init(Host, ServerHost, Opts) -> + node_default:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_default:terminate(Host, ServerHost). + +options() -> + [{node_type, dispatch}, + {deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {persist_items, true}, + {max_items, ?MAXITEMS div 2}, + {subscribe, true}, + {access_model, open}, + {access_roster_groups, []}, + {publish_model, publishers}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, never}, + {deliver_notifications, true}, + {presence_based_delivery, false}]. + +features() -> + ["create-nodes", + "delete-nodes", + "instant-nodes", + "item-ids", + "outcast-affiliation", + "persistent-items", + "publish", + %%"purge-nodes", + %%"retract-items", + %%"retrieve-affiliations", + "retrieve-items", + %%"retrieve-subscriptions", + %%"subscribe", + %%"subscription-notifications", + ]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_default:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Host, Node, Owner) -> + node_default:create_node(Host, Node, Owner). + +delete_node(Host, Removed) -> + node_default:delete_node(Host, Removed). + +subscribe_node(_Host, _Node, _Sender, _Subscriber, _AccessModel, + _SendLast, _PresenceSubscription, _RosterGroup) -> + {error, ?ERR_FORBIDDEN}. + +unsubscribe_node(_Host, _Node, _Sender, _Subscriber, _SubID) -> + {error, ?ERR_FORBIDDEN}. + +publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> + lists:foreach(fun(SubNode) -> + node_default:publish_item( + Host, SubNode, Publisher, Model, + MaxItems, ItemId, Payload) + end, nodetree_default:get_subnodes(Host, Node)). + +remove_extra_items(_Host, _Node, _MaxItems, ItemIds) -> + {result, {ItemsIds, []}}. + +delete_item(_Host, _Node, _JID, _ItemId) -> + {error, ?ERR_ITEM_NOT_FOUND}. + +purge_node(_Host, _Node, _Owner) -> + {error, ?ERR_FORBIDDEN}. + +get_entity_affiliations(_Host, _Owner) -> + {result, []}. + +get_node_affiliations(_Host, _Node) -> + {result, []}. + +get_affiliation(_Host, _Node, _Owner) -> + {result, []}. + +set_affiliation(Host, Node, Owner, Affiliation) -> + node_default:set_affiliation(Host, Node, Owner, Affiliation). + +get_entity_subscriptions(_Host, _Owner) -> + {result, []}. + +get_node_subscriptions(_Host, _Node) -> + {result, []}. + +get_subscription(_Host, _Node, _Owner) -> + {result, []}. + +set_subscription(Host, Node, Owner, Subscription) -> + node_default:set_subscription(Host, Node, Owner, Subscription). + +get_states(Host, Node) -> + node_default:get_states(Host, Node). + +get_state(Host, Node, JID) -> + node_default:get_state(Host, Node, JID). + +set_state(State) -> + node_default:set_state(State). + +get_items(Host, Node) -> + node_default:get_items(Host, Node). + +get_item(Host, Node, ItemId) -> + node_default:get_items(Host, Node, ItemId). + +set_item(Item) -> + node_default:set_item(Item). diff --git a/src/mod_pubsub/node_pep.erl b/src/mod_pubsub/node_pep.erl new file mode 100644 index 000000000..2ec568f11 --- /dev/null +++ b/src/mod_pubsub/node_pep.erl @@ -0,0 +1,173 @@ +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% @copyright 2007 Process-one +%%% @author Christophe Romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @doc The module {@module} is the pep PubSub plugin. +%%%

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

    + +-module(node_pep). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/3, + delete_node/2, + purge_node/3, + subscribe_node/8, + unsubscribe_node/5, + publish_item/7, + delete_item/4, + remove_extra_items/4, + get_entity_affiliations/2, + get_node_affiliations/2, + get_affiliation/3, + set_affiliation/4, + get_entity_subscriptions/2, + get_node_subscriptions/2, + get_subscription/3, + set_subscription/4, + get_states/2, + get_state/3, + set_state/1, + get_items/2, + get_item/3, + set_item/1 + ]). + +init(Host, ServerHost, Opts) -> + node_default:init(Host, ServerHost, Opts), + ok. + +terminate(Host, ServerHost) -> + node_default:terminate(Host, ServerHost), + ok. + +options() -> + [{node_type, pep}, + {deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, false}, + {persist_items, false}, + {max_items, ?MAXITEMS div 2}, + {subscribe, true}, + {access_model, presence}, + {access_roster_groups, []}, + {publish_model, publishers}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, true}]. + +features() -> + ["create-nodes", %* + "auto-create", %* + "delete-nodes", %* + "item-ids", + "outcast-affiliation", + "persistent-items", + "publish", %* + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", %* + "retrieve-subscriptions", + "subscribe", %* + "auto-subscribe", %* + "filtered-notifications" %* + ]. + +create_node_permission(_Host, _ServerHost, _Node, _ParentNode, _Owner, _Access) -> + %% TODO may we check bare JID match ? + {result, true}. + +create_node(Host, Node, Owner) -> + case node_default:create_node(Host, Node, Owner) of + {result, _} -> {result, []}; + Error -> Error + end. + +delete_node(Host, Removed) -> + case node_default:delete_node(Host, Removed) of + {result, {_, _, Removed}} -> {result, {[], Removed}}; + Error -> Error + end. + +subscribe_node(Host, Node, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup) -> + node_default:subscribe_node( + Host, Node, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup). + +unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> + case node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID) of + {error, Error} -> {error, Error}; + {result, _} -> {result, []} + end. + +publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> + node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(Host, Node, MaxItems, ItemIds) -> + node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). + +delete_item(Host, Node, JID, ItemId) -> + node_default:delete_item(Host, Node, JID, ItemId). + +purge_node(Host, Node, Owner) -> + node_default:purge_node(Host, Node, Owner). + +get_entity_affiliations(Host, Owner) -> + node_default:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Host, Node) -> + node_default:get_node_affiliations(Host, Node). + +get_affiliation(Host, Node, Owner) -> + node_default:get_affiliation(Host, Node, Owner). + +set_affiliation(Host, Node, Owner, Affiliation) -> + node_default:set_affiliation(Host, Node, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_default:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Host, Node) -> + node_default:get_node_subscriptions(Host, Node). + +get_subscription(Host, Node, Owner) -> + node_default:get_subscription(Host, Node, Owner). + +set_subscription(Host, Node, Owner, Subscription) -> + node_default:set_subscription(Host, Node, Owner, Subscription). + +get_states(Host, Node) -> + node_default:get_states(Host, Node). + +get_state(Host, Node, JID) -> + node_default:get_state(Host, Node, JID). + +set_state(State) -> + node_default:set_state(State). + +get_items(Host, Node) -> + node_default:get_items(Host, Node). + +get_item(Host, Node, ItemId) -> + node_default:get_items(Host, Node, ItemId). + +set_item(Item) -> + node_default:set_item(Item). diff --git a/src/mod_pubsub/node_private.erl b/src/mod_pubsub/node_private.erl new file mode 100644 index 000000000..9b17cb294 --- /dev/null +++ b/src/mod_pubsub/node_private.erl @@ -0,0 +1,166 @@ +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% @copyright 2007 Process-one +%%% @author Christophe romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(node_private). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% Note on function definition +%% included is all defined plugin function +%% it's possible not to define some function at all +%% in that case, warning will be generated at compilation +%% and function call will fail, +%% then mod_pubsub will call function from node_default +%% (this makes code cleaner, but execution a little bit longer) + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/3, + delete_node/2, + purge_node/3, + subscribe_node/8, + unsubscribe_node/5, + publish_item/7, + delete_item/4, + remove_extra_items/4, + get_entity_affiliations/2, + get_node_affiliations/2, + get_affiliation/3, + set_affiliation/4, + get_entity_subscriptions/2, + get_node_subscriptions/2, + get_subscription/3, + set_subscription/4, + get_states/2, + get_state/3, + set_state/1, + get_items/2, + get_item/3, + set_item/1 + ]). + + +init(Host, ServerHost, Opts) -> + node_default:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_default:terminate(Host, ServerHost). + +options() -> + [{node_type, private}, + {deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {persist_items, true}, + {max_items, ?MAXITEMS div 2}, + {subscribe, true}, + {access_model, whitelist}, + {access_roster_groups, []}, + {publish_model, publishers}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, never}, + {deliver_notifications, false}, + {presence_based_delivery, false}]. + +features() -> + ["create-nodes", + "delete-nodes", + "instant-nodes", + "item-ids", + "outcast-affiliation", + "persistent-items", + "publish", + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", + "retrieve-subscriptions", + "subscribe", + "subscription-notifications" + ]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_default:create_node_permission(Host, ServerHost, Node, ParentNode, + Owner, Access). + +create_node(Host, Node, Owner) -> + node_default:create_node(Host, Node, Owner). + +delete_node(Host, Removed) -> + node_default:delete_node(Host, Removed). + +subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup) -> + node_default:subscribe_node(Host, Node, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup). + +unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> + node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID). + +publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> + node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(Host, Node, MaxItems, ItemIds) -> + node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). + +delete_item(Host, Node, JID, ItemId) -> + node_default:delete_item(Host, Node, JID, ItemId). + +purge_node(Host, Node, Owner) -> + node_default:purge_node(Host, Node, Owner). + +get_entity_affiliations(Host, Owner) -> + node_default:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Host, Node) -> + node_default:get_node_affiliations(Host, Node). + +get_affiliation(Host, Node, Owner) -> + node_default:get_affiliation(Host, Node, Owner). + +set_affiliation(Host, Node, Owner, Affiliation) -> + node_default:set_affiliation(Host, Node, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_default:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Host, Node) -> + node_default:get_node_subscriptions(Host, Node). + +get_subscription(Host, Node, Owner) -> + node_default:get_subscription(Host, Node, Owner). + +set_subscription(Host, Node, Owner, Subscription) -> + node_default:set_subscription(Host, Node, Owner, Subscription). + +get_states(Host, Node) -> + node_default:get_states(Host, Node). + +get_state(Host, Node, JID) -> + node_default:get_state(Host, Node, JID). + +set_state(State) -> + node_default:set_state(State). + +get_items(Host, Node) -> + node_default:get_items(Host, Node). + +get_item(Host, Node, ItemId) -> + node_default:get_items(Host, Node, ItemId). + +set_item(Item) -> + node_default:set_item(Item). diff --git a/src/mod_pubsub/node_public.erl b/src/mod_pubsub/node_public.erl new file mode 100644 index 000000000..73e78ee31 --- /dev/null +++ b/src/mod_pubsub/node_public.erl @@ -0,0 +1,165 @@ +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% $Id: node_public.erl 100 2007-11-15 13:04:44Z mremond $ +%%% +%%% @copyright 2007 Process-one +%%% @author Christophe romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(node_public). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% Note on function definition +%% included is all defined plugin function +%% it's possible not to define some function at all +%% in that case, warning will be generated at compilation +%% and function call will fail, +%% then mod_pubsub will call function from node_default +%% (this makes code cleaner, but execution a little bit longer) + +%% API definition +-export([init/3, terminate/2, + options/0, features/0, + create_node_permission/6, + create_node/3, + delete_node/2, + purge_node/3, + subscribe_node/8, + unsubscribe_node/5, + publish_item/7, + delete_item/4, + remove_extra_items/4, + get_entity_affiliations/2, + get_node_affiliations/2, + get_affiliation/3, + set_affiliation/4, + get_entity_subscriptions/2, + get_node_subscriptions/2, + get_subscription/3, + set_subscription/4, + get_states/2, + get_state/3, + set_state/1, + get_items/2, + get_item/3, + set_item/1 + ]). + + +init(Host, ServerHost, Opts) -> + node_default:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_default:terminate(Host, ServerHost). + +options() -> + [{node_type, public}, + {deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {persist_items, true}, + {max_items, ?MAXITEMS div 2}, + {subscribe, true}, + {access_model, open}, + {access_roster_groups, []}, + {publish_model, publishers}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, never}, + {deliver_notifications, true}, + {presence_based_delivery, false}]. + +features() -> + ["create-nodes", + "delete-nodes", + "instant-nodes", + "item-ids", + "outcast-affiliation", + "persistent-items", + "publish", + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", + "retrieve-subscriptions", + "subscribe", + "subscription-notifications" + ]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_default:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Host, Node, Owner) -> + node_default:create_node(Host, Node, Owner). + +delete_node(Host, Removed) -> + node_default:delete_node(Host, Removed). + +subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> + node_default:subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). + +unsubscribe_node(Host, Node, Sender, Subscriber, SubID) -> + node_default:unsubscribe_node(Host, Node, Sender, Subscriber, SubID). + +publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload) -> + node_default:publish_item(Host, Node, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(Host, Node, MaxItems, ItemIds) -> + node_default:remove_extra_items(Host, Node, MaxItems, ItemIds). + +delete_item(Host, Node, JID, ItemId) -> + node_default:delete_item(Host, Node, JID, ItemId). + +purge_node(Host, Node, Owner) -> + node_default:purge_node(Host, Node, Owner). + +get_entity_affiliations(Host, Owner) -> + node_default:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Host, Node) -> + node_default:get_node_affiliations(Host, Node). + +get_affiliation(Host, Node, Owner) -> + node_default:get_affiliation(Host, Node, Owner). + +set_affiliation(Host, Node, Owner, Affiliation) -> + node_default:set_affiliation(Host, Node, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_default:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Host, Node) -> + node_default:get_node_subscriptions(Host, Node). + +get_subscription(Host, Node, Owner) -> + node_default:get_subscription(Host, Node, Owner). + +set_subscription(Host, Node, Owner, Subscription) -> + node_default:set_subscription(Host, Node, Owner, Subscription). + +get_states(Host, Node) -> + node_default:get_states(Host, Node). + +get_state(Host, Node, JID) -> + node_default:get_state(Host, Node, JID). + +set_state(State) -> + node_default:set_state(State). + +get_items(Host, Node) -> + node_default:get_items(Host, Node). + +get_item(Host, Node, ItemId) -> + node_default:get_items(Host, Node, ItemId). + +set_item(Item) -> + node_default:set_item(Item). diff --git a/src/mod_pubsub/nodetree_default.erl b/src/mod_pubsub/nodetree_default.erl new file mode 100644 index 000000000..3d05b5b24 --- /dev/null +++ b/src/mod_pubsub/nodetree_default.erl @@ -0,0 +1,164 @@ +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% @copyright 2007 Process-one +%%% @author Christophe Romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @doc The module {@module} is the default PubSub node tree plugin. +%%%

    It is used as a default for all unknown PubSub node type. It can serve +%%% as a developer basis and reference to build its own custom pubsub node tree +%%% types.

    +%%%

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

    +%%%

    The API isn't stabilized yet. The pubsub plugin +%%% development is still a work in progress. However, the system is already +%%% useable and useful as is. Please, send us comments, feedback and +%%% improvements.

    + +-module(nodetree_default). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_nodetree). + +-export([init/3, + terminate/2, + options/0, + set_node/1, + get_node/2, + get_nodes/1, + get_subnodes/2, + get_subnodes_tree/2, + create_node/5, + delete_node/2 + ]). + + +%% ================ +%% API definition +%% ================ + +%% @spec (Host) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = host() +%% Opts = list() +%% @doc

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

    +%%

    This function is mainly used to trigger the setup task necessary for the +%% plugin. It can be used for example by the developer to create the specific +%% module database schema if it does not exists yet.

    +init(_Host, _ServerHost, _Opts) -> + mnesia:create_table(pubsub_node, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_node)}, + {index, [type,parentid]}]), + NodesFields = record_info(fields, pubsub_node), + case mnesia:table_info(pubsub_node, attributes) of + NodesFields -> ok; + _ -> mnesia:transform_table(pubsub_node, ignore, NodesFields) + end, + ok. +terminate(_Host, _ServerHost) -> + ok. + +%% @spec () -> [Option] +%% Option = mod_pubsub:nodetreeOption() +%% @doc Returns the default pubsub node tree options. +options() -> + [{virtual_tree, false}]. + +%% @spec (NodeRecord) -> ok | {error, Reason} +%% Record = mod_pubsub:pubsub_node() +set_node(Record) when is_record(Record, pubsub_node) -> + mnesia:write(Record); +set_node(_) -> + {error, ?ERR_INTERNAL_SERVER_ERROR}. + +%% @spec (Host, Node) -> pubsubNode() | {error, Reason} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +get_node(Host, Node) -> + case catch mnesia:read({pubsub_node, {Host, Node}}) of + [Record] when is_record(Record, pubsub_node) -> Record; + [] -> {error, ?ERR_ITEM_NOT_FOUND}; + Error -> Error + end. + +%% @spec (Key) -> [pubsubNode()] | {error, Reason} +%% Key = mod_pubsub:host() | mod_pubsub:jid() +get_nodes(Key) -> + mnesia:match_object(#pubsub_node{nodeid = {Key, '_'}, _ = '_'}). + +%% @spec (Host, Index) -> [pubsubNode()] | {error, Reason} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +get_subnodes(Host, Node) -> + mnesia:index_read(pubsub_node, {Host, Node}, #pubsub_node.parentid). + +%% @spec (Host, Index) -> [pubsubNode()] | {error, Reason} +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +get_subnodes_tree(Host, Node) -> + mnesia:foldl(fun(#pubsub_node{nodeid = {H, N}}, Acc) -> + case lists:prefix(Node, N) and (H == Host) of + true -> [N | Acc]; + _ -> Acc + end + end, [], pubsub_node). + +%% @spec (Key, Node, Type, Owner, Options) -> ok | {error, Reason} +%% Key = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +%% NodeType = mod_pubsub:nodeType() +%% Owner = mod_pubsub:jid() +%% Options = list() +create_node(Key, Node, Type, Owner, Options) -> + OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + case mnesia:read({pubsub_node, {Key, Node}}) of + [] -> + Parent = lists:sublist(Node, length(Node) - 1), + ParentExists = + case Key of + {_U, _S, _R} -> + %% This is special case for PEP handling + %% PEP does not uses hierarchy + true; + _ -> + (Parent == []) orelse + case mnesia:read({pubsub_node, {Key, Parent}}) of + [] -> false; + _ -> true + end + end, + case ParentExists of + true -> + %% Service requires registration + %%{error, ?ERR_REGISTRATION_REQUIRED}; + mnesia:write(#pubsub_node{nodeid = {Key, Node}, + parentid = {Key, Parent}, + type = Type, + owners = [OwnerKey], + options = Options}); + false -> + %% Requesting entity is prohibited from creating nodes + {error, ?ERR_FORBIDDEN} + end; + _ -> + %% NodeID already exists + {error, ?ERR_CONFLICT} + end. + +%% @spec (Key, Node) -> [mod_pubsub:node()] +%% Key = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +delete_node(Key, Node) -> + Removed = get_subnodes_tree(Key, Node), + lists:foreach(fun(N) -> + mnesia:delete({pubsub_node, {Key, N}}) + end, Removed), + Removed. diff --git a/src/mod_pubsub/nodetree_virtual.erl b/src/mod_pubsub/nodetree_virtual.erl new file mode 100644 index 000000000..d3f3de851 --- /dev/null +++ b/src/mod_pubsub/nodetree_virtual.erl @@ -0,0 +1,119 @@ +%%% ==================================================================== +%%% This software is copyright 2007, Process-one. +%%% +%%% @copyright 2007 Process-one +%%% @author Christophe Romain +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @doc The module {@module} is the PubSub node tree plugin that +%%% allow virtual nodes handling. +%%%

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

    +%%%

    The API isn't stabilized yet. The pubsub plugin +%%% development is still a work in progress. However, the system is already +%%% useable and useful as is. Please, send us comments, feedback and +%%% improvements.

    + +-module(nodetree_virtual). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_nodetree). + +-export([init/3, + terminate/2, + options/0, + set_node/1, + get_node/2, + get_nodes/1, + get_subnodes/2, + get_subnodes_tree/2, + create_node/5, + delete_node/2 + ]). + +%% ================ +%% API definition +%% ================ + +%% @spec (Host) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = host() +%% Opts = list() +%% @doc

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

    +%%

    This function is mainly used to trigger the setup task necessary for the +%% plugin. It can be used for example by the developer to create the specific +%% module database schema if it does not exists yet.

    +init(_Host, _ServerHost, _Opts) -> + ok. +terminate(_Host, _ServerHost) -> + ok. + +%% @spec () -> [Option] +%% Option = mod_pubsub:nodetreeOption() +%% @doc

    Returns the default pubsub node tree options.

    +options() -> + [{virtual_tree, true}]. + +%% @spec (NodeRecord) -> ok | {error, Reason} +%% NodeRecord = mod_pubsub:pubsub_node() +%% @doc

    No node record is stored on database. Just do nothing.

    +set_node(_NodeRecord) -> + ok. + +%% @spec (Host, Node) -> pubsubNode() +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% @doc

    Virtual node tree does not handle a node database. Any node is considered +%% as existing. Node record contains default values.

    +get_node(Host, Node) -> + #pubsub_node{nodeid = {Host, Node}}. + +%% @spec (Key) -> [pubsubNode()] +%% Host = mod_pubsub:host() | mod_pubsub:jid() +%% @doc

    Virtual node tree does not handle a node database. Any node is considered +%% as existing. Nodes list can not be determined.

    +get_nodes(_Key) -> + []. + +%% @spec (Host, Index) -> [pubsubNode()] +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% @doc

    Virtual node tree does not handle parent/child. Child list is empty.

    +get_subnodes(_Host, _Node) -> + []. + +%% @spec (Host, Index) -> [pubsubNode()] +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% @doc

    Virtual node tree does not handle parent/child. Child list is empty.

    +get_subnodes_tree(_Host, _Node) -> + []. + +%% @spec (Host, Node, Type, Owner, Options) -> ok +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% Type = mod_pubsub:nodeType() +%% Owner = mod_pubsub:jid() +%% Options = list() +%% @doc

    No node record is stored on database. Any valid node +%% is considered as already created.

    +%%

    default allowed nodes: /home/host/user/any/node/name

    +create_node(_Host, Node, _Type, {UserName, UserHost, _}, _Options) -> + case Node of + ["home", UserHost, UserName | _] -> {error, ?ERR_CONFLICT}; + _ -> {error, ?ERR_NOT_ALLOWED} + end. + +%% @spec (Host, Node) -> [mod_pubsub:node()] +%% Host = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% @doc

    Virtual node tree does not handle parent/child. +%% node deletion just affects the corresponding node.

    +delete_node(_Host, Node) -> + [Node]. diff --git a/src/mod_pubsub/pubsub.hrl b/src/mod_pubsub/pubsub.hrl new file mode 100644 index 000000000..e5bbe45f6 --- /dev/null +++ b/src/mod_pubsub/pubsub.hrl @@ -0,0 +1,119 @@ +%%% ==================================================================== +%%% This software is copyright 2006, Process-one. +%%% +%%% This file contains pubsub types definition. +%%% ==================================================================== + +%% ------------------------------- +%% Pubsub constants +-define(PUBSUB_JID, {jid, "", Host, "", "", Host, ""}). +-define(ERR_EXTENDED(E,C), mod_pubsub:extended_error(E,C)). + +% TODO: move to jlib.hrl +-define(NS_PUBSUB_NODE_CONFIG, "http://jabber.org/protocol/pubsub#node_config"). +-define(NS_PUBSUB_SUB_AUTH, "http://jabber.org/protocol/pubsub#subscribe_authorization"). + +%% this is currently a hard limit. +%% Would be nice to have it configurable. +-define(MAXITEMS, 20). +-define(MAX_PAYLOAD_SIZE, 60000). + +%% ------------------------------- +%% Pubsub types + +%%% @type host() = string(). +%%%

    host is the name of the PubSub service. For example, it can be +%%% "pubsub.localhost".

    + +%%% @type pubsubNode() = [string()]. +%%%

    A node is defined by a list of its ancestors. The last element is the name +%%% of the current node. For example: +%%% ```["home", "localhost", "cromain", "node1"]'''

    + +%%% @type stanzaError() = #xmlelement{}. +%%% Example: +%%% ```{xmlelement, "error", +%%% [{"code", Code}, {"type", Type}], +%%% [{xmlelement, Condition, [{"xmlns", ?NS_STANZAS}], []}]}''' + +%%% @type pubsubIQResponse() = #xmlelement{}. +%%% Example: +%%% ```{xmlelement, "pubsub", +%%% [{"xmlns", ?NS_PUBSUB_EVENT}], +%%% [{xmlelement, "affiliations", [], +%%% []}]}''' + +%%% @type nodeOption() = {Option::atom(), Value::term()}. +%%% Example: +%%% ```{deliver_payloads, true}''' + +%%% @type nodeType() = string(). +%%%

    The nodeType is a string containing the name of the PubSub +%%% plugin to use to manage a given node. For example, it can be +%%% "default", "collection" or "blog".

    + +%%% @type jid() = #jid{ +%%% user = string(), +%%% server = string(), +%%% resource = string(), +%%% luser = string(), +%%% lserver = string(), +%%% lresource = string()}. + +%%% @type usr() = {User::string(), Server::string(), Resource::string()}. + +%%% @type affiliation() = none | owner | publisher | outcast. +%%% @type subscription() = none | pending | unconfigured | subscribed. + +%%% @type pubsubNode() = #pubsub_node{ +%%% nodeid = {Host::host(), Node::pubsubNode()}, +%%% parentid = {Host::host(), Node::pubsubNode()}, +%%% type = nodeType(), +%%% owners = [usr()], +%%% options = [nodeOption()]}. +%%%

    This is the format of the nodes table. The type of the table +%%% is: set,ram/disc.

    +%%%

    The parentid and type fields are indexed.

    +-record(pubsub_node, {nodeid, + parentid = {}, + type = "", + owners = [], + options = [] + }). + +%%% @type pubsubState() = #pubsub_state{ +%%% stateid = {jid(), {Host::host(), Node::pubsubNode()}}, +%%% items = [ItemId::string()], +%%% affiliation = affiliation(), +%%% subscription = subscription()}. +%%%

    This is the format of the affiliations table. The type of the +%%% table is: set,ram/disc.

    +-record(pubsub_state, {stateid, + items = [], + affiliation = none, + subscription = none +}). + +%% @type pubsubItem() = #pubsub_item{ +%% itemid = {ItemId::string(), {Host::host(),Node::pubsubNode()}}, +%% creation = {JID::jid(), now()}, +%% modification = {JID::jid(), now()}, +%% payload = XMLContent::string()}. +%%%

    This is the format of the published items table. The type of the +%%% table is: set,disc,fragmented.

    +-record(pubsub_item, {itemid, + creation = {unknown,unknown}, + modification = {unknown,unknown}, + payload = [] + }). + + +%% @type pubsubPresence() = #pubsub_presence{ +%% key = {Host::host(), User::string(), Server::string()}, +%% presence = list(). +%%%

    This is the format of the published presence table. The type of the +%%% table is: set,ram.

    +-record(pubsub_presence, {key, + resource + }). +