%%% ====================================================================
%%% ``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 Process-one.
%%% Portions created by Process-one are Copyright 2006-2008, Process-one
%%% All Rights Reserved.''
%%% This software is copyright 2006-2008, Process-one.
%%%
%%%
%%% @copyright 2006-2008 Process-one
%%% @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) -> 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() %% @docUnsubscribe 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() %% @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 = 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()] %% @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: 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() %% @docTriggers 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 (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) -> 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() %% @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)), 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, 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) -> 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() %% @docReturns 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, 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, ?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() %% @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. %% @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, ?ERR_INTERNAL_SERVER_ERROR}. %% @docReturn the name of the node if known: Default is to return %% node id.
get_item_name(_Host, _Node, Id) -> Id.