summaryrefslogtreecommitdiff
path: root/src/node_flat.erl
diff options
context:
space:
mode:
authorChristophe Romain <christophe.romain@process-one.net>2015-07-22 10:37:26 +0200
committerChristophe Romain <christophe.romain@process-one.net>2015-07-22 10:37:26 +0200
commit311fedaa125ede17349ade740c3cb9ff33ac4109 (patch)
tree5f2116cb90517bff79803a2111094a7292f3bf0e /src/node_flat.erl
parentDo not init nodes from mod_pubsub (#609) (diff)
Let flat be default plugin (#609)
Diffstat (limited to 'src/node_flat.erl')
-rw-r--r--src/node_flat.erl769
1 files changed, 716 insertions, 53 deletions
diff --git a/src/node_flat.erl b/src/node_flat.erl
index e2615438..736bbdfe 100644
--- a/src/node_flat.erl
+++ b/src/node_flat.erl
@@ -24,6 +24,12 @@
%%% @end
%%% ====================================================================
+%%% @doc The module <strong>{@module}</strong> is the default PubSub plugin.
+%%% <p>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.</p>
+%%% <p>PubSub plugin nodes are using the {@link gen_node} behaviour.</p>
+
-module(node_flat).
-behaviour(gen_pubsub_node).
-author('christophe.romain@process-one.net').
@@ -40,9 +46,10 @@
get_entity_subscriptions/2, get_node_subscriptions/1,
get_subscriptions/2, set_subscriptions/4,
get_pending_nodes/2, get_states/1, get_state/2,
- set_state/1, get_items/7, get_items/3, get_item/7,
+ set_state/1, get_items/6, get_items/2,
+ get_items/7, get_items/3, get_item/7,
get_item/2, set_item/1, get_item_name/3, node_to_path/1,
- path_to_node/1]).
+ path_to_node/1, can_fetch_item/2, is_subscribed/1]).
init(_Host, _ServerHost, _Opts) ->
pubsub_subscription:init(),
@@ -60,18 +67,55 @@ init(_Host, _ServerHost, _Opts) ->
end,
ok.
-terminate(Host, ServerHost) ->
- node_hometree:terminate(Host, ServerHost).
+terminate(_Host, _ServerHost) ->
+ ok.
options() ->
- node_hometree:options().
+ [{deliver_payloads, true},
+ {notify_config, false},
+ {notify_delete, false},
+ {notify_retract, true},
+ {purge_offline, false},
+ {persist_items, true},
+ {max_items, ?MAXITEMS},
+ {subscribe, true},
+ {access_model, open},
+ {roster_groups_allowed, []},
+ {publish_model, publishers},
+ {notification_type, headline},
+ {max_payload_size, ?MAX_PAYLOAD_SIZE},
+ {send_last_published_item, on_sub_and_presence},
+ {deliver_notifications, true},
+ {presence_based_delivery, false}].
features() ->
- node_hometree:features().
-
-%% use same code as node_hometree, but do not limite node to
-%% the home/localhost/user/... hierarchy
-%% any node is allowed
+ [<<"create-nodes">>,
+ <<"auto-create">>,
+ <<"access-authorize">>,
+ <<"delete-nodes">>,
+ <<"delete-items">>,
+ <<"get-pending">>,
+ <<"instant-nodes">>,
+ <<"manage-subscriptions">>,
+ <<"modify-affiliations">>,
+ <<"multi-subscribe">>,
+ <<"outcast-affiliation">>,
+ <<"persistent-items">>,
+ <<"publish">>,
+ <<"publish-only-affiliation">>,
+ <<"purge-nodes">>,
+ <<"retract-items">>,
+ <<"retrieve-affiliations">>,
+ <<"retrieve-items">>,
+ <<"retrieve-subscriptions">>,
+ <<"subscribe">>,
+ <<"subscription-notifications">>,
+ <<"subscription-options">>].
+
+%% @doc Checks if the current user has the permission to create the requested node
+%% <p>In flat node, any unused node name is allowed. The access parameter is also
+%% checked. This parameter depends on the value of the
+%% <tt>access_createnode</tt> ACL value in ejabberd config file.</p>
create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) ->
LOwner = jlib:jid_tolower(Owner),
Allowed = case LOwner of
@@ -83,88 +127,684 @@ create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) ->
{result, Allowed}.
create_node(Nidx, Owner) ->
- node_hometree:create_node(Nidx, Owner).
-
-delete_node(Removed) ->
- node_hometree:delete_node(Removed).
-
+ OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)),
+ set_state(#pubsub_state{stateid = {OwnerKey, Nidx},
+ affiliation = owner}),
+ {result, {default, broadcast}}.
+
+delete_node(Nodes) ->
+ Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Ss}) ->
+ lists:map(fun (S) -> {J, S} end, Ss)
+ end,
+ Reply = lists:map(fun (#pubsub_node{id = Nidx} = PubsubNode) ->
+ {result, States} = get_states(Nidx),
+ lists:foreach(fun (#pubsub_state{stateid = {LJID, _}, items = Items}) ->
+ del_items(Nidx, Items),
+ del_state(Nidx, LJID)
+ end, States),
+ {PubsubNode, lists:flatmap(Tr, States)}
+ end, Nodes),
+ {result, {default, broadcast, Reply}}.
+
+%% @doc <p>Accepts or rejects subcription requests on a PubSub node.</p>
+%% <p>The mechanism works as follow:
+%% <ul>
+%% <li>The main PubSub module prepares the subscription and passes the
+%% result of the preparation as a record.</li>
+%% <li>This function gets the prepared record and several other parameters and
+%% can decide to:<ul>
+%% <li>reject the subscription;</li>
+%% <li>allow it as is, letting the main module perform the database
+%% persistance;</li>
+%% <li>allow it, modifying the record. The main module will store the
+%% modified record;</li>
+%% <li>allow it, but perform the needed persistance operations.</li></ul>
+%% </li></ul></p>
+%% <p>The selected behaviour depends on the return parameter:
+%% <ul>
+%% <li><tt>{error, Reason}</tt>: an IQ error result will be returned. No
+%% subscription will actually be performed.</li>
+%% <li><tt>true</tt>: Subscribe operation is allowed, based on the
+%% unmodified record passed in parameter <tt>SubscribeResult</tt>. If this
+%% parameter contains an error, no subscription will be performed.</li>
+%% <li><tt>{true, PubsubState}</tt>: Subscribe operation is allowed, but
+%% the {@link mod_pubsub:pubsubState()} record returned replaces the value
+%% passed in parameter <tt>SubscribeResult</tt>.</li>
+%% <li><tt>{true, done}</tt>: 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.</li></ul>
+%% </p>
+%% <p>In the default plugin module, the record is unchanged.</p>
subscribe_node(Nidx, Sender, Subscriber, AccessModel,
SendLast, PresenceSubscription, RosterGroup, Options) ->
- node_hometree:subscribe_node(Nidx, Sender, Subscriber,
- AccessModel, SendLast, PresenceSubscription,
- RosterGroup, Options).
+ SubKey = jlib:jid_tolower(Subscriber),
+ GenKey = jlib:jid_remove_resource(SubKey),
+ Authorized = jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == GenKey,
+ GenState = get_state(Nidx, GenKey),
+ SubState = case SubKey of
+ GenKey -> GenState;
+ _ -> get_state(Nidx, SubKey)
+ end,
+ Affiliation = GenState#pubsub_state.affiliation,
+ Subscriptions = SubState#pubsub_state.subscriptions,
+ Whitelisted = lists:member(Affiliation, [member, publisher, owner]),
+ PendingSubscription = lists:any(fun
+ ({pending, _}) -> true;
+ (_) -> false
+ end,
+ Subscriptions),
+ Owner = Affiliation == owner,
+ if not Authorized ->
+ {error,
+ ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"invalid-jid">>)};
+ (Affiliation == outcast) or (Affiliation == publish_only) ->
+ {error, ?ERR_FORBIDDEN};
+ PendingSubscription ->
+ {error,
+ ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"pending-subscription">>)};
+ (AccessModel == presence) and (not PresenceSubscription) and (not Owner) ->
+ {error,
+ ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)};
+ (AccessModel == roster) and (not RosterGroup) and (not Owner) ->
+ {error,
+ ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)};
+ (AccessModel == whitelist) and (not Whitelisted) and (not Owner) ->
+ {error,
+ ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)};
+ %%MustPay ->
+ %% % Payment is required for a subscription
+ %% {error, ?ERR_PAYMENT_REQUIRED};
+ %%ForbiddenAnonymous ->
+ %% % Requesting entity is anonymous
+ %% {error, ?ERR_FORBIDDEN};
+ true ->
+ SubId = pubsub_subscription:add_subscription(Subscriber, Nidx, Options),
+ NewSub = case AccessModel of
+ authorize -> pending;
+ _ -> subscribed
+ end,
+ set_state(SubState#pubsub_state{subscriptions =
+ [{NewSub, SubId} | Subscriptions]}),
+ case {NewSub, SendLast} of
+ {subscribed, never} ->
+ {result, {default, subscribed, SubId}};
+ {subscribed, _} ->
+ {result, {default, subscribed, SubId, send_last}};
+ {_, _} ->
+ {result, {default, pending, SubId}}
+ end
+ end.
+%% @doc <p>Unsubscribe the <tt>Subscriber</tt> from the <tt>Node</tt>.</p>
unsubscribe_node(Nidx, Sender, Subscriber, SubId) ->
- node_hometree:unsubscribe_node(Nidx, Sender, Subscriber, SubId).
+ SubKey = jlib:jid_tolower(Subscriber),
+ GenKey = jlib:jid_remove_resource(SubKey),
+ Authorized = jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == GenKey,
+ GenState = get_state(Nidx, GenKey),
+ SubState = case SubKey of
+ GenKey -> GenState;
+ _ -> get_state(Nidx, SubKey)
+ end,
+ Subscriptions = lists:filter(fun
+ ({_Sub, _SubId}) -> true;
+ (_SubId) -> false
+ end,
+ SubState#pubsub_state.subscriptions),
+ SubIdExists = case SubId of
+ <<>> -> false;
+ Binary when is_binary(Binary) -> true;
+ _ -> false
+ end,
+ if
+ %% Requesting entity is prohibited from unsubscribing entity
+ not Authorized ->
+ {error, ?ERR_FORBIDDEN};
+ %% 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
+ Subscriptions == [] ->
+ {error,
+ ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), <<"not-subscribed">>)};
+ %% Subid supplied, so use that.
+ SubIdExists ->
+ Sub = first_in_list(fun
+ ({_, S}) when S == SubId -> true;
+ (_) -> false
+ end,
+ SubState#pubsub_state.subscriptions),
+ case Sub of
+ {value, S} ->
+ delete_subscriptions(SubKey, Nidx, [S], SubState),
+ {result, default};
+ false ->
+ {error,
+ ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), <<"not-subscribed">>)}
+ end;
+ %% Asking to remove all subscriptions to the given node
+ SubId == all ->
+ delete_subscriptions(SubKey, Nidx, Subscriptions, SubState),
+ {result, default};
+ %% No subid supplied, but there's only one matching subscription
+ length(Subscriptions) == 1 ->
+ delete_subscriptions(SubKey, Nidx, Subscriptions, SubState),
+ {result, default};
+ %% No subid and more than one possible subscription match.
+ true ->
+ {error,
+ ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)}
+ end.
-publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) ->
- node_hometree:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload).
+delete_subscriptions(SubKey, Nidx, Subscriptions, SubState) ->
+ NewSubs = lists:foldl(fun ({Subscription, SubId}, Acc) ->
+ pubsub_subscription:delete_subscription(SubKey, Nidx, SubId),
+ Acc -- [{Subscription, SubId}]
+ end, SubState#pubsub_state.subscriptions, Subscriptions),
+ case {SubState#pubsub_state.affiliation, NewSubs} of
+ {none, []} -> del_state(Nidx, SubKey);
+ _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs})
+ end.
-remove_extra_items(Nidx, MaxItems, ItemIds) ->
- node_hometree:remove_extra_items(Nidx, MaxItems, ItemIds).
+%% @doc <p>Publishes the item passed as parameter.</p>
+%% <p>The mechanism works as follow:
+%% <ul>
+%% <li>The main PubSub module prepares the item to publish and passes the
+%% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.</li>
+%% <li>This function gets the prepared record and several other parameters and can decide to:<ul>
+%% <li>reject the publication;</li>
+%% <li>allow the publication as is, letting the main module perform the database persistance;</li>
+%% <li>allow the publication, modifying the record. The main module will store the modified record;</li>
+%% <li>allow it, but perform the needed persistance operations.</li></ul>
+%% </li></ul></p>
+%% <p>The selected behaviour depends on the return parameter:
+%% <ul>
+%% <li><tt>{error, Reason}</tt>: an iq error result will be return. No
+%% publication is actually performed.</li>
+%% <li><tt>true</tt>: Publication operation is allowed, based on the
+%% unmodified record passed in parameter <tt>Item</tt>. If the <tt>Item</tt>
+%% parameter contains an error, no subscription will actually be
+%% performed.</li>
+%% <li><tt>{true, Item}</tt>: Publication operation is allowed, but the
+%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed
+%% in parameter <tt>Item</tt>. The persistance will be performed by the main
+%% module.</li>
+%% <li><tt>{true, done}</tt>: 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.</li></ul>
+%% </p>
+%% <p>In the default plugin module, the record is unchanged.</p>
+publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload) ->
+ SubKey = jlib:jid_tolower(Publisher),
+ GenKey = jlib:jid_remove_resource(SubKey),
+ GenState = get_state(Nidx, GenKey),
+ SubState = case SubKey of
+ GenKey -> GenState;
+ _ -> get_state(Nidx, SubKey)
+ end,
+ Affiliation = GenState#pubsub_state.affiliation,
+ Subscribed = case PublishModel of
+ subscribers -> is_subscribed(GenState#pubsub_state.subscriptions) orelse
+ is_subscribed(SubState#pubsub_state.subscriptions);
+ _ -> undefined
+ end,
+ if not ((PublishModel == open) or
+ (PublishModel == publishers) and
+ ((Affiliation == owner)
+ or (Affiliation == publisher)
+ or (Affiliation == publish_only))
+ or (Subscribed == true)) ->
+ {error, ?ERR_FORBIDDEN};
+ true ->
+ if MaxItems > 0 ->
+ Now = now(),
+ PubId = {Now, SubKey},
+ Item = case get_item(Nidx, ItemId) of
+ {result, OldItem} ->
+ OldItem#pubsub_item{modification = PubId,
+ payload = Payload};
+ _ ->
+ #pubsub_item{itemid = {ItemId, Nidx},
+ creation = {Now, GenKey},
+ modification = PubId,
+ payload = Payload}
+ end,
+ Items = [ItemId | GenState#pubsub_state.items -- [ItemId]],
+ {result, {NI, OI}} = remove_extra_items(Nidx, MaxItems, Items),
+ set_item(Item),
+ set_state(GenState#pubsub_state{items = NI}),
+ {result, {default, broadcast, OI}};
+ true ->
+ {result, {default, broadcast, []}}
+ end
+ end.
+%% @doc <p>This function is used to remove extra items, most notably when the
+%% maximum number of items has been reached.</p>
+%% <p>This function is used internally by the core PubSub module, as no
+%% permission check is performed.</p>
+%% <p>In the default plugin module, the oldest items are removed, but other
+%% rules can be used.</p>
+%% <p>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(Nidx, MaxItems, ItemIds) ->
+%% node_default:remove_extra_items(Nidx, MaxItems, ItemIds).'''</p>
+remove_extra_items(_Nidx, unlimited, ItemIds) ->
+ {result, {ItemIds, []}};
+remove_extra_items(Nidx, MaxItems, ItemIds) ->
+ NewItems = lists:sublist(ItemIds, MaxItems),
+ OldItems = lists:nthtail(length(NewItems), ItemIds),
+ del_items(Nidx, OldItems),
+ {result, {NewItems, OldItems}}.
+
+%% @doc <p>Triggers item deletion.</p>
+%% <p>Default plugin: The user performing the deletion must be the node owner
+%% or a publisher, or PublishModel being open.</p>
delete_item(Nidx, Publisher, PublishModel, ItemId) ->
- node_hometree:delete_item(Nidx, Publisher, PublishModel, ItemId).
+ SubKey = jlib:jid_tolower(Publisher),
+ GenKey = jlib:jid_remove_resource(SubKey),
+ GenState = get_state(Nidx, GenKey),
+ #pubsub_state{affiliation = Affiliation, items = Items} = GenState,
+ Allowed = Affiliation == publisher orelse
+ Affiliation == owner orelse
+ PublishModel == open orelse
+ case get_item(Nidx, ItemId) of
+ {result, #pubsub_item{creation = {_, GenKey}}} -> true;
+ _ -> false
+ end,
+ if not Allowed ->
+ {error, ?ERR_FORBIDDEN};
+ true ->
+ case lists:member(ItemId, Items) of
+ true ->
+ del_item(Nidx, ItemId),
+ set_state(GenState#pubsub_state{items = lists:delete(ItemId, Items)}),
+ {result, {default, broadcast}};
+ false ->
+ case Affiliation of
+ owner ->
+ {result, States} = get_states(Nidx),
+ lists:foldl(fun
+ (#pubsub_state{items = PI} = S, Res) ->
+ case lists:member(ItemId, PI) of
+ true ->
+ Nitems = lists:delete(ItemId, PI),
+ del_item(Nidx, ItemId),
+ set_state(S#pubsub_state{items = Nitems}),
+ {result, {default, broadcast}};
+ false ->
+ Res
+ end;
+ (_, Res) ->
+ Res
+ end,
+ {error, ?ERR_ITEM_NOT_FOUND}, States);
+ _ ->
+ {error, ?ERR_ITEM_NOT_FOUND}
+ end
+ end
+ end.
purge_node(Nidx, Owner) ->
- node_hometree:purge_node(Nidx, Owner).
+ SubKey = jlib:jid_tolower(Owner),
+ GenKey = jlib:jid_remove_resource(SubKey),
+ GenState = get_state(Nidx, GenKey),
+ case GenState of
+ #pubsub_state{affiliation = owner} ->
+ {result, States} = get_states(Nidx),
+ lists:foreach(fun
+ (#pubsub_state{items = []}) ->
+ ok;
+ (#pubsub_state{items = Items} = S) ->
+ del_items(Nidx, Items),
+ set_state(S#pubsub_state{items = []})
+ end,
+ States),
+ {result, {default, broadcast}};
+ _ ->
+ {error, ?ERR_FORBIDDEN}
+ end.
+%% @doc <p>Return the current affiliations for the given user</p>
+%% <p>The default module reads affiliations in the main Mnesia
+%% <tt>pubsub_state</tt> 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
+%% <tt>pubsub_state</tt> table.</p>
get_entity_affiliations(Host, Owner) ->
- node_hometree:get_entity_affiliations(Host, Owner).
+ SubKey = jlib:jid_tolower(Owner),
+ GenKey = jlib:jid_remove_resource(SubKey),
+ States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}),
+ NodeTree = mod_pubsub:tree(Host),
+ Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) ->
+ case NodeTree:get_node(N) of
+ #pubsub_node{nodeid = {Host, _}} = Node -> [{Node, A} | Acc];
+ _ -> Acc
+ end
+ end,
+ [], States),
+ {result, Reply}.
get_node_affiliations(Nidx) ->
- node_hometree:get_node_affiliations(Nidx).
+ {result, States} = get_states(Nidx),
+ Tr = fun (#pubsub_state{stateid = {J, _}, affiliation = A}) -> {J, A} end,
+ {result, lists:map(Tr, States)}.
get_affiliation(Nidx, Owner) ->
- node_hometree:get_affiliation(Nidx, Owner).
+ SubKey = jlib:jid_tolower(Owner),
+ GenKey = jlib:jid_remove_resource(SubKey),
+ #pubsub_state{affiliation = Affiliation} = get_state(Nidx, GenKey),
+ {result, Affiliation}.
set_affiliation(Nidx, Owner, Affiliation) ->
- node_hometree:set_affiliation(Nidx, Owner, Affiliation).
+ SubKey = jlib:jid_tolower(Owner),
+ GenKey = jlib:jid_remove_resource(SubKey),
+ GenState = get_state(Nidx, GenKey),
+ case {Affiliation, GenState#pubsub_state.subscriptions} of
+ {none, []} -> del_state(Nidx, GenKey);
+ _ -> set_state(GenState#pubsub_state{affiliation = Affiliation})
+ end.
+%% @doc <p>Return the current subscriptions for the given user</p>
+%% <p>The default module reads subscriptions in the main Mnesia
+%% <tt>pubsub_state</tt> 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
+%% <tt>pubsub_state</tt> table.</p>
get_entity_subscriptions(Host, Owner) ->
- node_hometree:get_entity_subscriptions(Host, Owner).
+ {U, D, _} = SubKey = jlib:jid_tolower(Owner),
+ GenKey = jlib:jid_remove_resource(SubKey),
+ States = case SubKey of
+ GenKey ->
+ mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'});
+ _ ->
+ mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'})
+ ++
+ mnesia:match_object(#pubsub_state{stateid = {SubKey, '_'}, _ = '_'})
+ end,
+ NodeTree = mod_pubsub:tree(Host),
+ Reply = lists:foldl(fun (#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) ->
+ case NodeTree:get_node(N) of
+ #pubsub_node{nodeid = {Host, _}} = Node ->
+ lists:foldl(fun ({Sub, SubId}, Acc2) ->
+ [{Node, Sub, SubId, J} | Acc2]
+ end,
+ Acc, Ss);
+ _ ->
+ Acc
+ end
+ end,
+ [], States),
+ {result, Reply}.
get_node_subscriptions(Nidx) ->
- node_hometree:get_node_subscriptions(Nidx).
+ {result, States} = get_states(Nidx),
+ Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Subscriptions}) ->
+ case Subscriptions of
+ [_ | _] ->
+ lists:foldl(fun ({S, SubId}, Acc) ->
+ [{J, S, SubId} | Acc]
+ end,
+ [], Subscriptions);
+ [] ->
+ [];
+ _ ->
+ [{J, none}]
+ end
+ end,
+ {result, lists:flatmap(Tr, States)}.
get_subscriptions(Nidx, Owner) ->
- node_hometree:get_subscriptions(Nidx, Owner).
+ SubKey = jlib:jid_tolower(Owner),
+ SubState = get_state(Nidx, SubKey),
+ {result, SubState#pubsub_state.subscriptions}.
set_subscriptions(Nidx, Owner, Subscription, SubId) ->
- node_hometree:set_subscriptions(Nidx, Owner, Subscription, SubId).
+ SubKey = jlib:jid_tolower(Owner),
+ SubState = get_state(Nidx, SubKey),
+ case {SubId, SubState#pubsub_state.subscriptions} of
+ {_, []} ->
+ case Subscription of
+ none ->
+ {error,
+ ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"not-subscribed">>)};
+ _ ->
+ new_subscription(Nidx, Owner, Subscription, SubState)
+ end;
+ {<<>>, [{_, SID}]} ->
+ case Subscription of
+ none -> unsub_with_subid(Nidx, SID, SubState);
+ _ -> replace_subscription({Subscription, SID}, SubState)
+ end;
+ {<<>>, [_ | _]} ->
+ {error,
+ ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)};
+ _ ->
+ case Subscription of
+ none -> unsub_with_subid(Nidx, SubId, SubState);
+ _ -> replace_subscription({Subscription, SubId}, SubState)
+ end
+ end.
+replace_subscription(NewSub, SubState) ->
+ NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []),
+ set_state(SubState#pubsub_state{subscriptions = NewSubs}).
+
+replace_subscription(_, [], Acc) -> Acc;
+replace_subscription({Sub, SubId}, [{_, SubId} | T], Acc) ->
+ replace_subscription({Sub, SubId}, T, [{Sub, SubId} | Acc]).
+
+new_subscription(Nidx, Owner, Sub, SubState) ->
+ SubId = pubsub_subscription:add_subscription(Owner, Nidx, []),
+ Subs = SubState#pubsub_state.subscriptions,
+ set_state(SubState#pubsub_state{subscriptions = [{Sub, SubId} | Subs]}),
+ {Sub, SubId}.
+
+unsub_with_subid(Nidx, SubId, #pubsub_state{stateid = {Entity, _}} = SubState) ->
+ pubsub_subscription:delete_subscription(SubState#pubsub_state.stateid, Nidx, SubId),
+ NewSubs = [{S, Sid}
+ || {S, Sid} <- SubState#pubsub_state.subscriptions,
+ SubId =/= Sid],
+ case {NewSubs, SubState#pubsub_state.affiliation} of
+ {[], none} -> del_state(Nidx, Entity);
+ _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs})
+ end.
+
+%% @doc <p>Returns a list of Owner's nodes on Host with pending
+%% subscriptions.</p>
get_pending_nodes(Host, Owner) ->
- node_hometree:get_pending_nodes(Host, Owner).
+ GenKey = jlib:jid_remove_resource(jlib:jid_tolower(Owner)),
+ States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'},
+ affiliation = owner,
+ _ = '_'}),
+ NodeIdxs = [Nidx || #pubsub_state{stateid = {_, Nidx}} <- States],
+ NodeTree = mod_pubsub:tree(Host),
+ Reply = mnesia:foldl(fun (#pubsub_state{stateid = {_, Nidx}} = S, Acc) ->
+ case lists:member(Nidx, NodeIdxs) of
+ true ->
+ case get_nodes_helper(NodeTree, S) of
+ {value, Node} -> [Node | Acc];
+ false -> Acc
+ end;
+ false ->
+ Acc
+ end
+ end,
+ [], pubsub_state),
+ {result, Reply}.
+
+get_nodes_helper(NodeTree, #pubsub_state{stateid = {_, N}, subscriptions = Subs}) ->
+ HasPending = fun
+ ({pending, _}) -> true;
+ (pending) -> true;
+ (_) -> false
+ end,
+ case lists:any(HasPending, Subs) of
+ true ->
+ case NodeTree:get_node(N) of
+ #pubsub_node{nodeid = {_, Node}} -> {value, Node};
+ _ -> false
+ end;
+ false ->
+ false
+ end.
+%% @doc Returns the list of stored states for a given node.
+%% <p>For the default PubSub module, states are stored in Mnesia database.</p>
+%% <p>We can consider that the pubsub_state table have been created by the main
+%% mod_pubsub module.</p>
+%% <p>PubSub plugins can store the states where they wants (for example in a
+%% relational database).</p>
+%% <p>If a PubSub plugin wants to delegate the states storage to the default node,
+%% they can implement this function like this:
+%% ```get_states(Nidx) ->
+%% node_default:get_states(Nidx).'''</p>
get_states(Nidx) ->
- node_hometree:get_states(Nidx).
+ States = case catch mnesia:match_object(
+ #pubsub_state{stateid = {'_', Nidx}, _ = '_'}) of
+ List when is_list(List) -> List;
+ _ -> []
+ end,
+ {result, States}.
+
+%% @doc <p>Returns a state (one state list), given its reference.</p>
+get_state(Nidx, Key) ->
+ StateId = {Key, Nidx},
+ case catch mnesia:read({pubsub_state, StateId}) of
+ [State] when is_record(State, pubsub_state) -> State;
+ _ -> #pubsub_state{stateid = StateId}
+ end.
-get_state(Nidx, JID) ->
- node_hometree:get_state(Nidx, JID).
+%% @doc <p>Write a state into database.</p>
+set_state(State) when is_record(State, pubsub_state) ->
+ mnesia:write(State).
+%set_state(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}.
+
+%% @doc <p>Delete a state from database.</p>
+del_state(Nidx, Key) ->
+ mnesia:delete({pubsub_state, {Key, Nidx}}).
+
+%% @doc Returns the list of stored items for a given node.
+%% <p>For the default PubSub module, items are stored in Mnesia database.</p>
+%% <p>We can consider that the pubsub_item table have been created by the main
+%% mod_pubsub module.</p>
+%% <p>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.</p>
+%% <p>If a PubSub plugin wants to delegate the item storage to the default node,
+%% they can implement this function like this:
+%% ```get_items(Nidx, From) ->
+%% node_default:get_items(Nidx, From).'''</p>
+get_items(Nidx, From) ->
+ get_items(Nidx, From, none).
+get_items(Nidx, _From, _RSM) ->
+ Items = mnesia:match_object(#pubsub_item{itemid = {'_', Nidx}, _ = '_'}),
+ {result, {lists:reverse(lists:keysort(#pubsub_item.modification, Items)), none}}.
+
+get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) ->
+ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, none).
+get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, _RSM) ->
+ SubKey = jlib:jid_tolower(JID),
+ GenKey = jlib:jid_remove_resource(SubKey),
+ GenState = get_state(Nidx, GenKey),
+ SubState = get_state(Nidx, SubKey),
+ Affiliation = GenState#pubsub_state.affiliation,
+ BareSubscriptions = GenState#pubsub_state.subscriptions,
+ FullSubscriptions = SubState#pubsub_state.subscriptions,
+ Whitelisted = can_fetch_item(Affiliation, BareSubscriptions) orelse
+ can_fetch_item(Affiliation, FullSubscriptions),
+ 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) or (Affiliation == publish_only) ->
+ {error, ?ERR_FORBIDDEN};
+ (AccessModel == presence) and not PresenceSubscription ->
+ {error,
+ ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)};
+ (AccessModel == roster) and not RosterGroup ->
+ {error,
+ ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)};
+ (AccessModel == whitelist) and not Whitelisted ->
+ {error,
+ ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)};
+ (AccessModel == authorize) and not Whitelisted ->
+ {error, ?ERR_FORBIDDEN};
+ %%MustPay ->
+ %% % Payment is required for a subscription
+ %% {error, ?ERR_PAYMENT_REQUIRED};
+ true ->
+ get_items(Nidx, JID)
+ end.
-set_state(State) ->
- node_hometree:set_state(State).
+%% @doc <p>Returns an item (one item list), given its reference.</p>
-get_items(Nidx, From, RSM) ->
- node_hometree:get_items(Nidx, From, RSM).
+get_item(Nidx, ItemId) ->
+ case mnesia:read({pubsub_item, {ItemId, Nidx}}) of
+ [Item] when is_record(Item, pubsub_item) -> {result, Item};
+ _ -> {error, ?ERR_ITEM_NOT_FOUND}
+ end.
-get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) ->
- node_hometree:get_items(Nidx, JID, AccessModel,
- PresenceSubscription, RosterGroup, SubId, RSM).
+get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) ->
+ SubKey = jlib:jid_tolower(JID),
+ GenKey = jlib:jid_remove_resource(SubKey),
+ GenState = get_state(Nidx, GenKey),
+ Affiliation = GenState#pubsub_state.affiliation,
+ Subscriptions = GenState#pubsub_state.subscriptions,
+ Whitelisted = can_fetch_item(Affiliation, Subscriptions),
+ 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) or (Affiliation == publish_only) ->
+ {error, ?ERR_FORBIDDEN};
+ (AccessModel == presence) and not PresenceSubscription ->
+ {error,
+ ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)};
+ (AccessModel == roster) and not RosterGroup ->
+ {error,
+ ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)};
+ (AccessModel == whitelist) and not Whitelisted ->
+ {error,
+ ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)};
+ (AccessModel == authorize) and not Whitelisted ->
+ {error, ?ERR_FORBIDDEN};
+ %%MustPay ->
+ %% % Payment is required for a subscription
+ %% {error, ?ERR_PAYMENT_REQUIRED};
+ true ->
+ get_item(Nidx, ItemId)
+ end.
-get_item(Nidx, ItemId) ->
- node_hometree:get_item(Nidx, ItemId).
+%% @doc <p>Write an item into database.</p>
+set_item(Item) when is_record(Item, pubsub_item) ->
+ mnesia:write(Item).
+%set_item(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}.
-get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) ->
- node_hometree:get_item(Nidx, ItemId, JID, AccessModel,
- PresenceSubscription, RosterGroup, SubId).
+%% @doc <p>Delete an item from database.</p>
+del_item(Nidx, ItemId) ->
+ mnesia:delete({pubsub_item, {ItemId, Nidx}}).
-set_item(Item) ->
- node_hometree:set_item(Item).
+del_items(Nidx, ItemIds) ->
+ lists:foreach(fun (ItemId) -> del_item(Nidx, ItemId)
+ end,
+ ItemIds).
-get_item_name(Host, Node, Id) ->
- node_hometree:get_item_name(Host, Node, Id).
+get_item_name(_Host, _Node, Id) ->
+ Id.
+%% @doc <p>Return the path of the node. In flat it's just node id.</p>
node_to_path(Node) ->
[(Node)].
@@ -178,3 +818,26 @@ path_to_node(Path) ->
% default case (used by PEP for example)
_ -> iolist_to_binary(Path)
end.
+
+can_fetch_item(owner, _) -> true;
+can_fetch_item(member, _) -> true;
+can_fetch_item(publisher, _) -> true;
+can_fetch_item(publish_only, _) -> false;
+can_fetch_item(outcast, _) -> false;
+can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions).
+%can_fetch_item(_Affiliation, _Subscription) -> false.
+
+is_subscribed(Subscriptions) ->
+ lists:any(fun
+ ({subscribed, _SubId}) -> true;
+ (_) -> false
+ end,
+ Subscriptions).
+
+first_in_list(_Pred, []) ->
+ false;
+first_in_list(Pred, [H | T]) ->
+ case Pred(H) of
+ true -> {value, H};
+ _ -> first_in_list(Pred, T)
+ end.