%%% ====================================================================
%%% ``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) -> SubscriberKey = jlib:jid_tolower(jlib:jid_remove_resource(Subscriber)), Authorized = (jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == SubscriberKey), State = get_state(Host, Node, SubscriberKey), #pubsub_state{affiliation = Affiliation, subscription = Subscription} = State, if not Authorized -> %% JIDs do not match {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "invalid-jid")}; Affiliation == outcast -> %% Requesting entity is blocked {error, ?ERR_FORBIDDEN}; Subscription == pending -> %% Requesting entity has pending subscription {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "pending-subscription")}; (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; %%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() %% @docUnsubscribe the Subscriber from the Node.
unsubscribe_node(Host, Node, Sender, Subscriber, _SubId) -> SubscriberKey = jlib:jid_tolower(jlib:jid_remove_resource(Subscriber)), Authorized = (jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == SubscriberKey), State = get_state(Host, Node, SubscriberKey), 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) and (State#pubsub_state.affiliation =/= owner) -> {error, ?ERR_FORBIDDEN}; %% Was just subscriber, remove the record State#pubsub_state.affiliation == none -> del_state(State#pubsub_state.stateid), {result, default}; true -> set_state(State#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) -> PublisherKey = jlib:jid_tolower(jlib:jid_remove_resource(Publisher)), State = get_state(Host, Node, PublisherKey), #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 {result, OldItem} -> OldItem#pubsub_item{modification = PubId, payload = Payload}; _ -> #pubsub_item{itemid = {ItemId, {Host, Node}}, creation = PubId, modification = PubId, payload = Payload} end, Items = [ItemId | State#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(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()] %% @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) -> PublisherKey = jlib:jid_tolower(jlib:jid_remove_resource(Publisher)), State = get_state(Host, Node, PublisherKey), #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, _} -> del_item(Host, Node, ItemId), 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 (Host, Node, Owner) -> %% {error, Reason::stanzaError()} | %% {result, []} %% Host = mod_pubsub:host() %% Node = mod_pubsub:pubsubNode() %% Owner = mod_pubsub:jid() %% @docPurge all node items.
%%Default plugin: The user performing the deletion must be the node owner.
purge_node(Host, Node, Owner) -> OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), case get_state(Host, Node, OwnerKey) of #pubsub_state{items = Items, affiliation = owner} -> del_items(Host, Node, Items), {result, {default, broadcast}}; _ -> %% Entity is not owner {error, ?ERR_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) -> 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)), State = get_state(Host, Node, OwnerKey), {result, State#pubsub_state.affiliation}. set_affiliation(Host, Node, Owner, Affiliation) -> OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), State = get_state(Host, Node, OwnerKey), set_state(State#pubsub_state{affiliation = Affiliation}), ok. %% @spec (Host) -> [{Node,Subscription}] %% Host = host() %% JID = mod_pubsub:jid() %% @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) -> 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)), State = get_state(Host, Node, OwnerKey), {result, State#pubsub_state.subscription}. set_subscription(Host, Node, Owner, Subscription) -> OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), State = get_state(Host, Node, OwnerKey), set_state(State#pubsub_state{subscription = Subscription}), 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, ?ERR_INTERNAL_SERVER_ERROR} %% State = mod_pubsub:pubsubStates() %% @docWrite a state into database.
set_state(State) when is_record(State, pubsub_state) -> mnesia:write(State); set_state(_) -> {error, ?ERR_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) -> %% node_default:get_items(Host, Node).'''
get_items(Host, Node) -> Items = mnesia:match_object( #pubsub_item{itemid = {'_', {Host, Node}}, _ = '_'}), {result, Items}. get_items(Host, Node, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> State = get_state(Host, Node, jlib:jid_tolower(jlib:jid_remove_resource(JID))), #pubsub_state{affiliation = Affiliation, subscription = Subscription} = State, Subscribed = not ((Subscription == none) or (Subscription == pending)), if %%SubID == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; %%InvalidSubID -> %% Entity is subscribed but specifies an invalid subscription ID %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; Affiliation == outcast -> %% Requesting entity is blocked {error, ?ERR_FORBIDDEN}; (AccessModel == open) and (not Subscribed) -> %% Entity is not subscribed {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "not-subscribed")}; (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}; true -> get_items(Host, Node) 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, ?ERR_ITEM_NOT_FOUND} end. get_item(Host, Node, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> State = get_state(Host, Node, jlib:jid_tolower(jlib:jid_remove_resource(JID))), #pubsub_state{affiliation = Affiliation, subscription = Subscription} = State, Subscribed = not ((Subscription == none) or (Subscription == pending)), if %%SubID == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; %%InvalidSubID -> %% Entity is subscribed but specifies an invalid subscription ID %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; Affiliation == outcast -> %% Requesting entity is blocked {error, ?ERR_FORBIDDEN}; (AccessModel == open) and (not Subscribed) -> %% Entity is not subscribed {error, ?ERR_EXTENDED(?ERR_NOT_AUTHORIZED, "not-subscribed")}; (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}; true -> get_item(Host, Node, ItemId) end. %% @spec (Item) -> ok | {error, ?ERR_INTERNAL_SERVER_ERROR} %% Item = mod_pubsub:pubsubItems() %% @docWrite a state into database.
set_item(Item) when is_record(Item, pubsub_item) -> mnesia:write(Item); set_item(_) -> {error, ?ERR_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).