%%% ====================================================================
%%% ``The contents of this file are subject to the Erlang Public License,
%%% Version 1.1, (the "License"); you may not use this file except in
%%% compliance with the License. You should have received a copy of the
%%% Erlang Public License along with this software. If not, it can be
%%% retrieved via the world wide web at http://www.erlang.org/.
%%%
%%% Software distributed under the License is distributed on an "AS IS"
%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%%% the License for the specific language governing rights and limitations
%%% under the License.
%%%
%%% The Initial Developer of the Original Code is ProcessOne.
%%% Portions created by ProcessOne are Copyright 2006-2008, ProcessOne
%%% All Rights Reserved.''
%%% This software is copyright 2006-2008, ProcessOne.
%%%
%%%
%%% @copyright 2006-2008 ProcessOne
%%% @author Christophe Romain 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. 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. Called during pubsub modules termination. Any pubsub plugin must
%% implement this function. It can return anything. Example of function return value: 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).''' purge items of deleted nodes after effective deletion. Accepts or rejects subcription requests on a PubSub node. The mechanism works as follow:
%%
%%
%%
%%
The selected behaviour depends on the return parameter: %%
In the default plugin module, the record is unchanged.
subscribe_node(Host, Node, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> SubKey = jlib:short_prepd_jid(Subscriber), GenKey = jlib:short_prepd_bare_jid(SubKey), Authorized = (jlib:short_prepd_bare_jid(Sender) == GenKey), GenState = get_state(Host, Node, GenKey), SubState = case SubKey of GenKey -> GenState; _ -> get_state(Host, Node, SubKey) end, Affiliation = GenState#pubsub_state.affiliation, Whitelisted = lists:member(Affiliation, [member, publisher, owner]), if not Authorized -> %% JIDs do not match {error, ?ERR_EXTENDED('bad-request', "invalid-jid")}; Affiliation == outcast -> %% Requesting entity is blocked {error, 'forbidden'}; SubState#pubsub_state.subscription == pending -> %% Requesting entity has pending subscription {error, ?ERR_EXTENDED('not-authorized', "pending-subscription")}; (AccessModel == presence) and (not PresenceSubscription) -> %% Entity is not authorized to create a subscription (presence subscription required) {error, ?ERR_EXTENDED('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('not-authorized', "not-in-roster-group")}; (AccessModel == whitelist) and (not Whitelisted) -> %% Node has whitelist access model and entity lacks required affiliation {error, ?ERR_EXTENDED('not-allowed', "closed-node")}; (AccessModel == authorize) -> % TODO: to be done %% Node has authorize access model {error, 'forbidden'}; %%MustPay -> %% % Payment is required for a subscription %% {error, ?ERR_PAYMENT_REQUIRED}; %%ForbiddenAnonymous -> %% % Requesting entity is anonymous %% {error, 'forbidden'}; true -> NewSubscription = if AccessModel == authorize -> pending; %%NeedConfiguration -> %% unconfigured true -> subscribed end, set_state(SubState#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() %% @docUnsubscribe the Subscriber from the Node.
unsubscribe_node(Host, Node, Sender, Subscriber, _SubId) -> SubKey = jlib:short_prepd_jid(Subscriber), GenKey = jlib:short_prepd_bare_jid(SubKey), Authorized = (jlib:short_prepd_bare_jid(Sender) == GenKey), GenState = get_state(Host, Node, GenKey), SubState = case SubKey of GenKey -> GenState; _ -> get_state(Host, Node, SubKey) end, if %% Requesting entity is prohibited from unsubscribing entity not Authorized -> {error, 'forbidden'}; %% Entity did not specify SubID %%SubID == "", ?? -> %% {error, ?ERR_EXTENDED('bad-request', "subid-required")}; %% Invalid subscription identifier %%InvalidSubID -> %% {error, ?ERR_EXTENDED('not-acceptable', "invalid-subid")}; %% Requesting entity is not a subscriber SubState#pubsub_state.subscription == none -> {error, ?ERR_EXTENDED('unexpected-request', "not-subscribed")}; %% Was just subscriber, remove the record SubState#pubsub_state.affiliation == none -> del_state(SubState#pubsub_state.stateid), {result, default}; true -> set_state(SubState#pubsub_state{subscription = none}), {result, default} 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() %% @docPublishes the item passed as parameter.
%%The mechanism works as follow: %%
The selected behaviour depends on the return parameter: %%
In the default plugin module, the record is unchanged.
publish_item(Host, Node, Publisher, PublishModel, MaxItems, ItemId, Payload) -> SubKey = jlib:short_prepd_jid(Publisher), GenKey = jlib:short_prepd_bare_jid(SubKey), GenState = get_state(Host, Node, GenKey), SubState = case SubKey of GenKey -> GenState; _ -> get_state(Host, Node, SubKey) end, Affiliation = GenState#pubsub_state.affiliation, Subscription = SubState#pubsub_state.subscription, 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, 'forbidden'}; true -> PubId = {SubKey, now()}, %% TODO, uses {now(),PublisherKey} for sorting (EJAB-824) %% TODO: check creation, presence, roster (EJAB-663) Item = case get_item(Host, Node, ItemId) of {result, OldItem} -> OldItem#pubsub_item{modification = PubId, payload = Payload}; _ -> #pubsub_item{itemid = {ItemId, {Host, Node}}, creation = {GenKey, now()}, modification = PubId, payload = Payload} end, Items = [ItemId | GenState#pubsub_state.items--[ItemId]], {result, {NI, OI}} = remove_extra_items( Host, Node, MaxItems, Items), if MaxItems > 0 -> set_item(Item); true -> ok end, set_state(GenState#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()] %% @docThis 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: del_items(Host, Node, 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() %% @docTriggers item deletion.
%%Default plugin: The user performing the deletion must be the node owner %% or a publisher.
delete_item(Host, Node, Publisher, ItemId) -> GenKey = jlib:short_prepd_bare_jid(Publisher), GenState = get_state(Host, Node, GenKey), #pubsub_state{affiliation = Affiliation, items = Items} = GenState, Allowed = (Affiliation == publisher) orelse (Affiliation == owner) orelse case get_item(Host, Node, ItemId) of {result, #pubsub_item{creation = {GenKey, _}}} -> true; _ -> false end, if not Allowed -> %% Requesting entity does not have sufficient privileges {error, 'forbidden'}; true -> case get_item(Host, Node, ItemId) of {result, _} -> del_item(Host, Node, ItemId), NewItems = lists:delete(ItemId, Items), set_state(GenState#pubsub_state{items = NewItems}), {result, {default, broadcast}}; _ -> %% Non-existent node or item {error, 'item-not-found'} end end. %% @spec (Host, Node, Owner) -> %% {error, Reason::stanzaError()} | %% {result, {default, broadcast}} %% Host = mod_pubsub:host() %% Node = mod_pubsub:pubsubNode() %% Owner = mod_pubsub:jid() purge_node(Host, Node, Owner) -> GenKey = jlib:short_prepd_bare_jid(Owner), GenState = get_state(Host, Node, GenKey), case GenState of #pubsub_state{items = Items, affiliation = owner} -> del_items(Host, Node, Items), {result, {default, broadcast}}; _ -> %% Entity is not owner {error, 'forbidden'} end. %% @spec (Host, JID) -> [{Node,Affiliation}] %% Host = host() %% JID = mod_pubsub:jid() %% @docReturn 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) -> GenKey = jlib:short_prepd_bare_jid(Owner), States = mnesia:match_object( #pubsub_state{stateid = {GenKey, {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) -> GenKey = jlib:short_prepd_bare_jid(Owner), GenState = get_state(Host, Node, GenKey), {result, GenState#pubsub_state.affiliation}. set_affiliation(Host, Node, Owner, Affiliation) -> GenKey = jlib:short_prepd_bare_jid(Owner), GenState = get_state(Host, Node, GenKey), case {Affiliation, GenState#pubsub_state.subscription} of {none, none} -> del_state(GenState#pubsub_state.stateid); _ -> set_state(GenState#pubsub_state{affiliation = Affiliation}) end, ok. %% @spec (Host, Owner) -> [{Node,Subscription}] %% Host = host() %% Owner = mod_pubsub:jid() %% Node = mod_pubsub:pubsubNode() %% @docReturn 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) -> States = case jlib:short_prepd_bare_jid(Owner) of {U, D, ""} -> mnesia:match_object( #pubsub_state{stateid = {{U, D, '_'}, {Host, '_'}}, _ = '_'}); {U, D, R} -> mnesia:match_object( #pubsub_state{stateid = {{U, D, ""}, {Host, '_'}}, _ = '_'}) ++ mnesia:match_object( #pubsub_state{stateid = {{U, D, R}, {Host, '_'}}, _ = '_'}) end, Tr = fun(#pubsub_state{stateid = {J, {_, N}}, subscription = S}) -> {N, S, J} 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) -> SubKey = jlib:short_prepd_jid(Owner), SubState = get_state(Host, Node, SubKey), {result, SubState#pubsub_state.subscription}. set_subscription(Host, Node, Owner, Subscription) -> SubKey = jlib:short_prepd_jid(Owner), SubState = get_state(Host, Node, SubKey), case {Subscription, SubState#pubsub_state.affiliation} of {none, none} -> del_state(SubState#pubsub_state.stateid); _ -> set_state(SubState#pubsub_state{subscription = Subscription}) end, 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() %% @docReturns a state (one state list), given its reference.
get_state(Host, Node, JID) -> StateId = {JID, {Host, Node}}, case mnesia:read({pubsub_state, StateId}) of [State] when is_record(State, pubsub_state) -> State; _ -> #pubsub_state{stateid=StateId} end. %% @spec (State) -> ok | {error, Reason::stanzaError()} %% State = mod_pubsub:pubsubStates() %% @docWrite a state into database.
set_state(State) when is_record(State, pubsub_state) -> mnesia:write(State); set_state(_) -> {error, 'internal-server-error'}. %% @spec (StateId) -> ok | {error, Reason::stanzaError()} %% StateId = mod_pubsub:pubsubStateId() %% @docDelete a state from database.
del_state(StateId) -> mnesia:delete({pubsub_state, StateId}). %% @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, From) -> %% node_default:get_items(Host, Node, From).'''
get_items(Host, Node, _From) -> Items = mnesia:match_object( #pubsub_item{itemid = {'_', {Host, Node}}, _ = '_'}), {result, Items}. get_items(Host, Node, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> GenKey = jlib:short_prepd_bare_jid(JID), GenState = get_state(Host, Node, GenKey), Affiliation = GenState#pubsub_state.affiliation, Whitelisted = lists:member(Affiliation, [member, publisher, owner]), if %%SubID == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, ?ERR_EXTENDED('bad-request', "subid-required")}; %%InvalidSubID -> %% Entity is subscribed but specifies an invalid subscription ID %{error, ?ERR_EXTENDED('not-acceptable', "invalid-subid")}; Affiliation == outcast -> %% Requesting entity is blocked {error, 'forbidden'}; (AccessModel == presence) and (not PresenceSubscription) -> %% Entity is not authorized to create a subscription (presence subscription required) {error, ?ERR_EXTENDED('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('not-authorized', "not-in-roster-group")}; (AccessModel == whitelist) and (not Whitelisted) -> %% Node has whitelist access model and entity lacks required affiliation {error, ?ERR_EXTENDED('not-allowed', "closed-node")}; (AccessModel == authorize) -> % TODO: to be done %% Node has authorize access model {error, 'forbidden'}; %%MustPay -> %% % Payment is required for a subscription %% {error, ?ERR_PAYMENT_REQUIRED}; true -> get_items(Host, Node, JID) end. %% @spec (Host, Node, ItemId) -> [Item] | [] %% Host = mod_pubsub:host() %% Node = mod_pubsub:pubsubNode() %% ItemId = string() %% Item = mod_pubsub:pubsubItems() %% @docReturns 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, 'item-not-found'} end. get_item(Host, Node, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> GenKey = jlib:short_prepd_bare_jid(JID), GenState = get_state(Host, Node, GenKey), Affiliation = GenState#pubsub_state.affiliation, Whitelisted = lists:member(Affiliation, [member, publisher, owner]), if %%SubID == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, ?ERR_EXTENDED('bad-request', "subid-required")}; %%InvalidSubID -> %% Entity is subscribed but specifies an invalid subscription ID %{error, ?ERR_EXTENDED('not-acceptable', "invalid-subid")}; Affiliation == outcast -> %% Requesting entity is blocked {error, 'forbidden'}; (AccessModel == presence) and (not PresenceSubscription) -> %% Entity is not authorized to create a subscription (presence subscription required) {error, ?ERR_EXTENDED('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('not-authorized', "not-in-roster-group")}; (AccessModel == whitelist) and (not Whitelisted) -> %% Node has whitelist access model and entity lacks required affiliation {error, ?ERR_EXTENDED('not-allowed', "closed-node")}; (AccessModel == authorize) -> % TODO: to be done %% Node has authorize access model {error, 'forbidden'}; %%MustPay -> %% % Payment is required for a subscription %% {error, ?ERR_PAYMENT_REQUIRED}; true -> get_item(Host, Node, ItemId) end. %% @spec (Item) -> ok | {error, Reason::stanzaError()} %% Item = mod_pubsub:pubsubItems() %% @docWrite a state into database.
set_item(Item) when is_record(Item, pubsub_item) -> mnesia:write(Item); set_item(_) -> {error, 'internal-server-error'}. %% @spec (ItemId) -> ok | {error, Reason::stanzaError()} %% Host = mod_pubsub:host() %% Node = mod_pubsub:pubsubNode() %% ItemId = string() %% @docDelete an item from database.
del_item(Host, Node, ItemId) -> mnesia:delete({pubsub_item, {ItemId, {Host, Node}}}). del_items(Host, Node, ItemIds) -> lists:foreach(fun(ItemId) -> del_item(Host, Node, ItemId) end, ItemIds). %% @docReturn the name of the node if known: Default is to return %% node id.
get_item_name(_Host, _Node, Id) -> Id.