diff --git a/src/mod_pubsub/mod_pubsub_odbc.erl b/src/mod_pubsub/mod_pubsub_odbc.erl index b96834fba..8342edf66 100644 --- a/src/mod_pubsub/mod_pubsub_odbc.erl +++ b/src/mod_pubsub/mod_pubsub_odbc.erl @@ -2213,7 +2213,7 @@ get_item(Host, Node, ItemId) -> send_items(Host, Node, NodeId, Type, LJID, last) -> Stanza = case get_cached_item(Host, NodeId) of undefined -> - % special ODBC optimization, works only with node_flat_odbc, node_flat_odbc and node_pep_odbc + % special ODBC optimization, works only with node_hometree_odbc, node_flat_odbc and node_pep_odbc case node_action(Host, Type, get_last_items, [NodeId, LJID, 1]) of {result, [LastItem]} -> {ModifNow, ModifLjid} = LastItem#pubsub_item.modification, diff --git a/src/mod_pubsub/node_flat.erl b/src/mod_pubsub/node_flat.erl index 2ca038168..985bc9421 100644 --- a/src/mod_pubsub/node_flat.erl +++ b/src/mod_pubsub/node_flat.erl @@ -962,12 +962,19 @@ get_item_name(_Host, _Node, Id) -> Id. node_to_path(Node) -> - [list_to_binary(Item) || Item <- string:tokens(binary_to_list(Node), "/")]. + [Node]. path_to_node([]) -> <<>>; path_to_node(Path) -> - list_to_binary(string:join([""|[binary_to_list(Item) || Item <- Path]], "/")). + case Path of + % default slot + [Node] -> Node; + % handle old possible entries, used when migrating database content to new format + [Node|_] when is_list(Node) -> list_to_binary(string:join([""|Path], "/")); + % default case (used by PEP for example) + _ -> list_to_binary(Path) + end. %% @spec (Affiliation, Subscription) -> true | false %% Affiliation = owner | member | publisher | outcast | none diff --git a/src/mod_pubsub/node_flat_odbc.erl b/src/mod_pubsub/node_flat_odbc.erl index 087103d7b..f259260b6 100644 --- a/src/mod_pubsub/node_flat_odbc.erl +++ b/src/mod_pubsub/node_flat_odbc.erl @@ -15,6 +15,7 @@ %%% All Rights Reserved.'' %%% This software is copyright 2006-2010, ProcessOne. %%% +%%% %%% @copyright 2006-2010 ProcessOne %%% @author Christophe Romain %%% [http://www.process-one.net/] @@ -22,11 +23,29 @@ %%% @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_flat_odbc). -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). -include_lib("exmpp/include/exmpp.hrl"). +-include("jlib.hrl"). %% for rsm_in and rsm_out records definitions + +-define(PUBSUB, mod_pubsub_odbc). -behaviour(gen_pubsub_node). @@ -47,6 +66,7 @@ get_affiliation/2, set_affiliation/3, get_entity_subscriptions/2, + get_entity_subscriptions_for_send_last/2, get_node_subscriptions/1, get_subscriptions/2, set_subscriptions/4, @@ -60,18 +80,62 @@ get_item/2, set_item/1, get_item_name/3, - get_last_items/3, - node_to_path/1, - path_to_node/1 + get_last_items/3, + path_to_node/1, + node_to_path/1 ]). +-export([ + decode_jid/1, + decode_node/1, + decode_affiliation/1, + decode_subscriptions/1, + encode_jid/1, + encode_affiliation/1, + encode_subscriptions/1 + ]). +%% ================ +%% API definition +%% ================ -init(Host, ServerHost, Opts) -> - node_flat_odbc:init(Host, ServerHost, Opts). +%% @spec (Host, ServerHost, Opts) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = mod_pubsub: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) -> + pubsub_subscription_odbc:init(), + ok. -terminate(Host, ServerHost) -> - node_flat_odbc:terminate(Host, ServerHost). +%% @spec (Host, ServerHost) -> any() +%% Host = mod_pubsub:host() +%% ServerHost = 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() -> [{deliver_payloads, true}, {notify_config, false}, @@ -93,103 +157,1212 @@ options() -> {odbc, true}, {rsm, true}]. +%% @spec () -> [] +%% @doc Returns the node features features() -> - node_flat_odbc:features(). + ["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", + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-items", + "retrieve-subscriptions", + "subscribe", + "subscription-notifications", + "subscription-options", + "rsm" + ]. -%% use same code as node_flat_odbc, but do not limite node to -%% the home/localhost/user/... hierarchy -%% any node is allowed -create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> - LOwner = jlib:short_prepd_jid(Owner), - Allowed = case LOwner of - {undefined, Host, undefined} -> - true; % pubsub service always allowed - _ -> - {LU, LS, LR} = LOwner, - acl:match_rule(ServerHost, Access, exmpp_jid:make(LU, LS, LR)) =:= allow - end, - {result, Allowed}. +%% @spec (Host, ServerHost, Node, ParentNode, Owner, Access) -> bool() +%% Host = mod_pubsub:host() +%% ServerHost = mod_pubsub:host() +%% Node = mod_pubsub:pubsubNode() +%% ParentNode = 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, ServerHost, Node, ParentNode, Owner, Access) -> +%% node_default:check_create_user_permission(Host, ServerHost, Node, ParentNode, Owner, Access).'''

+create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). +%% @spec (NodeId, Owner) -> +%% {result, Result} | exit +%% NodeId = mod_pubsub:pubsubNodeId() +%% Owner = mod_pubsub:jid() +%% @doc

create_node(NodeId, Owner) -> - node_flat_odbc:create_node(NodeId, Owner). + OwnerKey = jlib:short_prepd_bare_jid(Owner), + State = #pubsub_state{stateid = {OwnerKey, NodeId}, affiliation = owner}, + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values(", state_to_raw(NodeId, State), ");"]), + {result, {default, broadcast}}. +%% @spec (Removed) -> ok +%% Removed = [mod_pubsub:pubsubNode()] +%% @doc

purge items of deleted nodes after effective deletion.

delete_node(Removed) -> - node_flat_odbc:delete_node(Removed). +%% pablo: TODO, this is present on trunk/node_flat but not on trunk/node_flat_odbc. +%% check what is the desired behaviour +%% Tr = fun(#pubsub_state{stateid = {J, _}, subscriptions = Ss}) -> +%% lists:map(fun(S) -> +%% {J, S} +%% end, Ss) +%% end, + Reply = lists:map( + fun(#pubsub_node{id = NodeId} = PubsubNode) -> + Subscriptions = case catch ejabberd_odbc:sql_query_t( + ["select jid, subscriptions " + "from pubsub_state " + "where nodeid='", NodeId, ";"]) of + {selected, ["jid", "subscriptions"], RItems} -> + lists:map(fun({SJID, Subscriptions}) -> + {decode_jid(SJID), decode_subscriptions(Subscriptions)} + end, RItems); + _ -> + [] + end, + %% state and item remove already done thanks to DELETE CASCADE + %% but here we get nothing in States, making notify_retract unavailable ! + %% TODO, remove DELETE CASCADE from schema + {PubsubNode, Subscriptions} + end, Removed), + {result, {default, broadcast, Reply}}. -subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - node_flat_odbc:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). +%% @spec (NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> +%% {error, Reason} | {result, Result} +%% @doc

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(NodeId, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + SubKey = jlib:short_prepd_jid(Subscriber), + GenKey = jlib:short_prepd_bare_jid(SubKey), + Authorized = (jlib:short_prepd_bare_jid(Sender) == GenKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), + Whitelisted = lists:member(Affiliation, [member, publisher, owner]), + PendingSubscription = lists:any(fun({pending, _}) -> true; + (_) -> false + end, Subscriptions), + if + not Authorized -> + %% JIDs do not match + {error, ?ERR_EXTENDED('bad-request', "invalid-jid")}; + Affiliation == outcast -> + %% Requesting entity is blocked + {error, 'forbidden'}; + PendingSubscription -> + %% 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")}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + %%ForbiddenAnonymous -> + %% % Requesting entity is anonymous + %% {error, 'forbidden'}; + true -> + case pubsub_subscription_odbc:subscribe_node(Subscriber, NodeId, Options) of + {result, SubId} -> + NewSub = case AccessModel of + authorize -> pending; + _ -> subscribed + end, + update_subscription(NodeId, SubKey, [{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; + _ -> + {error, 'internal-server-error'} + end + end. -unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> - node_flat_odbc:unsubscribe_node(NodeId, Sender, Subscriber, SubID). +%% @spec (NodeId, Sender, Subscriber, SubId) -> +%% {error, Reason} | {result, []} +%% NodeId = mod_pubsub:pubsubNodeId() +%% Sender = mod_pubsub:jid() +%% Subscriber = mod_pubsub:jid() +%% SubId = mod_pubsub:subid() +%% Reason = mod_pubsub:stanzaError() +%% @doc

Unsubscribe the Subscriber from the Node.

+unsubscribe_node(NodeId, Sender, Subscriber, SubId) -> + SubKey = jlib:short_prepd_jid(Subscriber), + GenKey = jlib:short_prepd_bare_jid(SubKey), + Authorized = (jlib:short_prepd_bare_jid(Sender) == GenKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, SubKey), + SubIdExists = case SubId of + [] -> false; + List when is_list(List) -> true; + _ -> false + 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 + Subscriptions == [] -> + {error, ?ERR_EXTENDED('unexpected-request', "not-subscribed")}; + %% Subid supplied, so use that. + SubIdExists -> + Sub = first_in_list(fun(S) -> + case S of + {_Sub, SubId} -> true; + _ -> false + end + end, Subscriptions), + case Sub of + {value, S} -> + delete_subscription(SubKey, NodeId, S, Affiliation, Subscriptions), + {result, default}; + false -> + {error, ?ERR_EXTENDED('unexpected-request', "not-subscribed")} + end; + %% Asking to remove all subscriptions to the given node + SubId == all -> + [delete_subscription(SubKey, NodeId, S, Affiliation, Subscriptions) || S <- Subscriptions], + {result, default}; + %% No subid supplied, but there's only one matching + %% subscription, so use that. + length(Subscriptions) == 1 -> + delete_subscription(SubKey, NodeId, hd(Subscriptions), Affiliation, Subscriptions), + {result, default}; + true -> + {error, ?ERR_EXTENDED('bad-request', "subid-required")} + end. -publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> - node_flat_odbc:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). +delete_subscription(SubKey, NodeId, {Subscription, SubId}, Affiliation, Subscriptions) -> + NewSubs = Subscriptions -- [{Subscription, SubId}], + pubsub_subscription_odbc:unsubscribe_node(SubKey, NodeId, SubId), + case {Affiliation, NewSubs} of + {none, []} -> + % Just a regular subscriber, and this is final item, so + % delete the state. + del_state(NodeId, SubKey); + _ -> + update_subscription(NodeId, SubKey, NewSubs) + end. + +%% @spec (NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload) -> +%% {true, PubsubItem} | {result, Reply} +%% NodeId = mod_pubsub:pubsubNodeId() +%% 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 selected behaviour depends on the return parameter: +%%

+%%

+%%

In the default plugin module, the record is unchanged.

+publish_item(NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload) -> + SubKey = jlib:short_prepd_jid(Publisher), + GenKey = jlib:short_prepd_bare_jid(Publisher), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), + Subscribed = case PublishModel of + subscribers -> is_subscribed(Subscriptions); + _ -> undefined + end, + if + not ((PublishModel == open) + or ((PublishModel == publishers) + and ((Affiliation == owner) or (Affiliation == publisher))) + or (Subscribed == true)) -> + %% Entity does not have sufficient privileges to publish to node + {error, 'forbidden'}; + true -> + %% TODO: check creation, presence, roster + if MaxItems > 0 -> + %% Note: this works cause set_item tries an update before + %% the insert, and the update just ignore creation field. + PubId = {now(), SubKey}, + set_item(#pubsub_item{itemid = {ItemId, NodeId}, + creation = {now(), GenKey}, + modification = PubId, + payload = Payload}), + Items = [ItemId | itemids(NodeId, GenKey) -- [ItemId]], + {result, {_, OI}} = remove_extra_items(NodeId, MaxItems, Items), + %% set new item list use useless + {result, {default, broadcast, OI}}; + true -> + {result, {default, broadcast, []}} + end + end. + +%% @spec (NodeId, MaxItems, ItemIds) -> {NewItemIds,OldItemIds} +%% NodeId = mod_pubsub:pubsubNodeId() +%% 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(NodeId, MaxItems, ItemIds) -> +%% node_default:remove_extra_items(NodeId, MaxItems, ItemIds).'''

+remove_extra_items(_NodeId, unlimited, ItemIds) -> + {result, {ItemIds, []}}; remove_extra_items(NodeId, MaxItems, ItemIds) -> - node_flat_odbc:remove_extra_items(NodeId, MaxItems, ItemIds). + NewItems = lists:sublist(ItemIds, MaxItems), + OldItems = lists:nthtail(length(NewItems), ItemIds), + %% Remove extra items: + del_items(NodeId, OldItems), + %% Return the new items list: + {result, {NewItems, OldItems}}. +%% @spec (NodeId, Publisher, PublishModel, ItemId) -> +%% {error, Reason::stanzaError()} | +%% {result, []} +%% NodeId = mod_pubsub:pubsubNodeId() +%% Publisher = mod_pubsub:jid() +%% PublishModel = atom() +%% ItemId = string() +%% @doc

Triggers item deletion.

+%%

Default plugin: The user performing the deletion must be the node owner +%% or a publisher, or PublishModel being open.

delete_item(NodeId, Publisher, PublishModel, ItemId) -> - node_flat_odbc:delete_item(NodeId, Publisher, PublishModel, ItemId). + GenKey = jlib:short_prepd_bare_jid(Publisher), + {result, Affiliation} = get_affiliation(NodeId, GenKey), + Allowed = (Affiliation == publisher) orelse (Affiliation == owner) + orelse (PublishModel == open) + orelse case get_item(NodeId, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}}} -> true; + _ -> false + end, + if + not Allowed -> + %% Requesting entity does not have sufficient privileges + {error, 'forbidden'}; + true -> + case del_item(NodeId, ItemId) of + {updated, 1} -> + %% set new item list use useless + {result, {default, broadcast}}; + _ -> + %% Non-existent node or item + {error, 'item-not-found'} + end + end. +%% @spec (NodeId, Owner) -> +%% {error, Reason::stanzaError()} | +%% {result, {default, broadcast}} +%% NodeId = mod_pubsub:pubsubNodeId() +%% Owner = mod_pubsub:jid() purge_node(NodeId, Owner) -> - node_flat_odbc:purge_node(NodeId, Owner). + GenKey = jlib:short_prepd_bare_jid(Owner), + GenState = get_state(NodeId, GenKey), + case GenState of + #pubsub_state{affiliation = owner} -> + {result, States} = get_states(NodeId), + lists:foreach( + fun(#pubsub_state{items = []}) -> ok; + (#pubsub_state{items = Items}) -> del_items(NodeId, Items) + end, States), + {result, {default, broadcast}}; + _ -> + %% Entity is not owner + {error, '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) -> - node_flat_odbc:get_entity_affiliations(Host, Owner). + GenKey = jlib:short_prepd_bare_jid(Owner), + H = ?PUBSUB:escape(Host), + J = encode_jid(GenKey), + Reply = case catch ejabberd_odbc:sql_query_t( + ["select node, type, i.nodeid, affiliation " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid " + "and jid='", J, "' " + "and host='", H, "';"]) of + {selected, ["node", "type", "nodeid", "affiliation"], RItems} -> + lists:map(fun({N, T, I, A}) -> + Node = nodetree_tree_odbc:raw_to_node(Host, {N, "", T, I}), + {Node, decode_affiliation(A)} + end, RItems); + _ -> + [] + end, + {result, Reply}. get_node_affiliations(NodeId) -> - node_flat_odbc:get_node_affiliations(NodeId). + Reply = case catch ejabberd_odbc:sql_query_t( + ["select jid, affiliation " + "from pubsub_state " + "where nodeid='", NodeId, "';"]) of + {selected, ["jid", "affiliation"], RItems} -> + lists:map(fun({J, A}) -> {decode_jid(J), decode_affiliation(A)} end, RItems); + _ -> + [] + end, + {result, Reply}. get_affiliation(NodeId, Owner) -> - node_flat_odbc:get_affiliation(NodeId, Owner). + GenKey = jlib:short_prepd_bare_jid(Owner), + J = encode_jid(GenKey), + Reply = case catch ejabberd_odbc:sql_query_t( + ["select affiliation from pubsub_state " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {selected, ["affiliation"], [{A}]} -> decode_affiliation(A); + _ -> none + end, + {result, Reply}. -set_affiliation(NodeId, Owner, Affiliation) -> - node_flat_odbc:set_affiliation(NodeId, Owner, Affiliation). +set_affiliation(NodeId, Owner, Affiliation) when ?IS_JID(Owner)-> + GenKey = jlib:short_prepd_bare_jid(Owner), + {_, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey), + case {Affiliation, Subscriptions} of + {none, none} -> + del_state(NodeId, GenKey); + _ -> + update_affiliation(NodeId, GenKey, Affiliation) + end. +%% @spec (Host, Owner) -> [{Node,Subscription}] +%% Host = host() +%% Owner = 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) -> - node_flat_odbc:get_entity_subscriptions(Host, Owner). + SubKey = jlib:short_prepd_jid(Owner), + GenKey = jlib:short_prepd_bare_jid(Owner), + H = ?PUBSUB:escape(Host), + SJ = encode_jid(SubKey), + GJ = encode_jid(GenKey), + Query = case SubKey of + GenKey -> + ["select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid " + "and jid like '", GJ, "%' " + "and host='", H, "';"]; + _ -> + ["select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid " + "and jid in ('", SJ, "', '", GJ, "') " + "and host='", H, "';"] + end, + Reply = case catch ejabberd_odbc:sql_query_t(Query) of + {selected, ["node", "type", "nodeid", "jid", "subscriptions"], RItems} -> + lists:foldl(fun({N, T, I, J, S}, Acc) -> + Node = nodetree_tree_odbc:raw_to_node(Host, {N, "", T, I}), + Jid = decode_jid(J), + case decode_subscriptions(S) of + [] -> + [{Node, none, Jid}|Acc]; + Subs -> + lists:foldl(fun({Sub, SubId}, Acc2) -> [{Node, Sub, SubId, Jid}|Acc2]; + (Sub, Acc2) -> [{Node, Sub, Jid}|Acc2] + end, Acc, Subs) + end + end, [], RItems); + _ -> + [] + end, + {result, Reply}. +%% do the same as get_entity_subscriptions but filter result only to +%% nodes having send_last_published_item=on_sub_and_presence +%% as this call avoid seeking node, it must return node and type as well +get_entity_subscriptions_for_send_last(Host, Owner) -> + SubKey = jlib:short_prepd_jid(Owner), + GenKey = jlib:short_prepd_bare_jid(Owner), + H = ?PUBSUB:escape(Host), + SJ = encode_jid(SubKey), + GJ = encode_jid(GenKey), + Query = case SubKey of + GenKey -> + ["select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid " + "and name='send_last_published_item' and val='on_sub_and_presence' " + "and jid like '", GJ, "%' " + "and host='", H, "';"]; + _ -> + ["select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid " + "and name='send_last_published_item' and val='on_sub_and_presence' " + "and jid in ('", SJ, "', '", GJ, "') " + "and host='", H, "';"] + end, + Reply = case catch ejabberd_odbc:sql_query_t(Query) of + {selected, ["node", "type", "nodeid", "jid", "subscriptions"], RItems} -> + lists:foldl(fun({N, T, I, J, S}, Acc) -> + Node = nodetree_tree_odbc:raw_to_node(Host, {N, "", T, I}), + Jid = decode_jid(J), + case decode_subscriptions(S) of + [] -> + [{Node, none, Jid}|Acc]; + Subs -> + lists:foldl(fun({Sub, SubId}, Acc2) -> [{Node, Sub, SubId, Jid}|Acc2]; + (Sub, Acc2) -> [{Node, Sub, Jid}|Acc2] + end, Acc, Subs) + end + end, [], RItems); + _ -> + [] + end, + {result, Reply}. get_node_subscriptions(NodeId) -> - node_flat_odbc:get_node_subscriptions(NodeId). + Reply = case catch ejabberd_odbc:sql_query_t( + ["select jid, subscriptions " + "from pubsub_state " + "where nodeid='", NodeId, "';"]) of + {selected, ["jid", "subscriptions"], RItems} -> + lists:foldl(fun({J, S}, Acc) -> + Jid = decode_jid(J), + case decode_subscriptions(S) of + [] -> + [{Jid, none}|Acc]; + Subs -> + lists:foldl(fun({Sub, SubId}, Acc2) -> [{Jid, Sub, SubId}|Acc2]; + (Sub, Acc2) -> [{Jid, Sub}|Acc2] + end, Acc, Subs) + end + end, [], RItems); + _ -> + [] + end, + {result, Reply}. + get_subscriptions(NodeId, Owner) -> - node_flat_odbc:get_subscriptions(NodeId, Owner). + SubKey = jlib:short_prepd_jid(Owner), + J = encode_jid(SubKey), + Reply = case catch ejabberd_odbc:sql_query_t( + ["select subscriptions from pubsub_state " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {selected, ["subscriptions"], [{S}]} -> decode_subscriptions(S); + _ -> [] + end, + {result, Reply}. set_subscriptions(NodeId, Owner, Subscription, SubId) -> - node_flat_odbc:set_subscriptions(NodeId, Owner, Subscription, SubId). + SubKey = jlib:short_prepd_jid(Owner), + SubState = get_state_without_itemids(NodeId, SubKey), + case {SubId, SubState#pubsub_state.subscriptions} of + {_, []} -> + case Subscription of + none -> {error, ?ERR_EXTENDED('bad_request', "not-subscribed")}; + _ -> new_subscription(NodeId, Owner, Subscription, SubState) + end; + {"", [{_, SID}]} -> + case Subscription of + none -> unsub_with_subid(NodeId, SID, SubState); + _ -> replace_subscription({Subscription, SID}, SubState) + end; + {"", [_|_]} -> + {error, ?ERR_EXTENDED('bad_request', "subid-required")}; + _ -> + case Subscription of + none -> unsub_with_subid(NodeId, 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(NodeId, Owner, Subscription, SubState) -> + case pubsub_subscription_odbc:subscribe_node(Owner, NodeId, []) of + {result, SubId} -> + Subscriptions = SubState#pubsub_state.subscriptions, + set_state(SubState#pubsub_state{subscriptions = [{Subscription, SubId} | Subscriptions]}), + {Subscription, SubId}; + _ -> + {error, 'internal-server-error'} + end. + +unsub_with_subid(NodeId, SubId, SubState) -> + pubsub_subscription_odbc:unsubscribe_node(SubState#pubsub_state.stateid, + NodeId, SubId), + NewSubs = lists:filter(fun ({_, SID}) -> SubId =/= SID end, + SubState#pubsub_state.subscriptions), + case {NewSubs, SubState#pubsub_state.affiliation} of + {[], none} -> + del_state(NodeId, element(1, SubState#pubsub_state.stateid)); + _ -> + set_state(SubState#pubsub_state{subscriptions = NewSubs}) + end. + +%% @spec (Host, Owner) -> {result, [Node]} | {error, Reason} +%% Host = host() +%% Owner = jid() +%% Node = pubsubNode() +%% @doc

Returns a list of Owner's nodes on Host with pending +%% subscriptions.

get_pending_nodes(Host, Owner) -> - node_flat_odbc:get_pending_nodes(Host, Owner). + GenKey = jlib:short_prepd_bare_jid(Owner), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, + affiliation = owner, + _ = '_'}), + NodeIDs = [ID || #pubsub_state{stateid = {_, ID}} <- States], + NodeTree = case catch ets:lookup(gen_mod:get_module_proc(Host, config), nodetree) of + [{nodetree, N}] -> N; + _ -> nodetree_tree_odbc + end, + Reply = mnesia:foldl(fun(#pubsub_state{stateid = {_, NID}} = S, Acc) -> + case lists:member(NID, NodeIDs) 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. + +%% @spec (NodeId) -> {result, [State] | []} +%% NodeId = mod_pubsub:pubsubNodeId() +%% State = mod_pubsub:pubsubState() +%% @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(NodeId) -> +%% node_default:get_states(NodeId).'''

get_states(NodeId) -> - node_flat_odbc:get_states(NodeId). + case catch ejabberd_odbc:sql_query_t( + ["select jid, affiliation, subscriptions " + "from pubsub_state " + "where nodeid='", NodeId, "';"]) of + {selected, ["jid", "affiliation", "subscriptions"], RItems} -> + {result, lists:map(fun({SJID, Affiliation, Subscriptions}) -> + #pubsub_state{stateid = {decode_jid(SJID), NodeId}, + items = itemids(NodeId, SJID), + affiliation = decode_affiliation(Affiliation), + subscriptions = decode_subscriptions(Subscriptions)} + end, RItems)}; + _ -> + {result, []} + end. + +%% @spec (NodeId, JID) -> [State] | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% JID = mod_pubsub:jid() +%% State = mod_pubsub:pubsubItems() +%% @doc

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

get_state(NodeId, JID) -> - node_flat_odbc:get_state(NodeId, JID). + State = get_state_without_itemids(NodeId, JID), + {SJID, _} = State#pubsub_state.stateid, + State#pubsub_state{items = itemids(NodeId, SJID)}. +get_state_without_itemids(NodeId, JID) -> + J = encode_jid(JID), + case catch ejabberd_odbc:sql_query_t( + ["select jid, affiliation, subscriptions " + "from pubsub_state " + "where jid='", J, "' " + "and nodeid='", NodeId, "';"]) of + {selected, ["jid", "affiliation", "subscriptions"], [{SJID, Affiliation, Subscriptions}]} -> + #pubsub_state{stateid = {decode_jid(SJID), NodeId}, + affiliation = decode_affiliation(Affiliation), + subscriptions = decode_subscriptions(Subscriptions)}; + _ -> + #pubsub_state{stateid={JID, NodeId}} + end. -set_state(State) -> - node_flat_odbc:set_state(State). +%% @spec (State) -> ok | {error, Reason::stanzaError()} +%% State = mod_pubsub:pubsubStates() +%% @doc

Write a state into database.

+set_state(State) when is_record(State, pubsub_state) -> + {_, NodeId} = State#pubsub_state.stateid, + set_state(NodeId, State). +set_state(NodeId, State) -> + %% NOTE: in odbc version, as we do not handle item list, + %% we just need to update affiliation and subscription + %% cause {JID,NodeId} is the key. if it does not exists, then we insert it. + %% MySQL can be optimized using INSERT ... ON DUPLICATE KEY as well + {JID, _} = State#pubsub_state.stateid, + J = encode_jid(JID), + S = encode_subscriptions(State#pubsub_state.subscriptions), + A = encode_affiliation(State#pubsub_state.affiliation), + case catch ejabberd_odbc:sql_query_t( + ["update pubsub_state " + "set subscriptions='", S, "', affiliation='", A, "' " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {updated, 1} -> + ok; + _ -> + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values('", NodeId, "', '", J, "', '", A, "', '", S, "');"]) + end, + {result, []}. -get_items(NodeId, From) -> - node_flat_odbc:get_items(NodeId, From). +%% @spec (NodeId, JID) -> ok | {error, Reason::stanzaError()} +%% NodeId = mod_pubsub:pubsubNodeId() +%% @doc

Delete a state from database.

+del_state(NodeId, JID) -> + J = encode_jid(JID), + catch ejabberd_odbc:sql_query_t( + ["delete from pubsub_state " + "where jid='", J, "' " + "and nodeid='", NodeId, "';"]), + ok. +%% @spec (NodeId, From) -> {[Items], RsmOut} | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% 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(NodeId, From) -> +%% node_default:get_items(NodeId, From).'''

+get_items(NodeId, _From) -> + case catch ejabberd_odbc:sql_query_t( + ["select itemid, publisher, creation, modification, payload " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "order by modification desc;"]) of + {selected, ["itemid", "publisher", "creation", "modification", "payload"], RItems} -> + {result, lists:map(fun(RItem) -> raw_to_item(NodeId, RItem) end, RItems)}; + _ -> + {result, []} + end. +get_items(NodeId, From, none) -> + MaxItems = case catch ejabberd_odbc:sql_query_t( + ["select val from pubsub_node_option " + "where nodeid='", NodeId, "' " + "and name='max_items';"]) of + {selected, ["val"], [{Value}]} -> + Tokens = element(2, erl_scan:string(Value++".")), + element(2, erl_parse:parse_term(Tokens)); + _ -> + ?MAXITEMS + end, + get_items(NodeId, From, #rsm_in{max=MaxItems}); +get_items(NodeId, _From, #rsm_in{max=M, direction=Direction, id=I, index=IncIndex})-> + Max = ?PUBSUB:escape(i2l(M)), + + {Way, Order} = case Direction of + aft -> {"<", "desc"}; + before when I == [] -> {"is not", "asc"}; + before -> {">", "asc"}; + _ when IncIndex =/= undefined -> {"<", "desc"}; % using index + _ -> {"is not", "desc"}% Can be better + end, + [AttrName, Id] = case I of + undefined when IncIndex =/= undefined -> + case catch ejabberd_odbc:sql_query_t( + ["select modification from pubsub_item pi " + "where exists ( " + "select count(*) as count1 " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "and modification > pi.modification " + "having count1 = ",?PUBSUB:escape(i2l(IncIndex))," );"]) of + {selected, [_], [{O}]} -> ["modification", "'"++O++"'"]; + _ -> ["modification", "null"] + end; + undefined -> ["modification", "null"]; + [] -> ["modification", "null"]; + I -> [A, B] = string:tokens(?PUBSUB:escape(i2l(I)), "@"), + [A, "'"++B++"'"] + end, + Count= case catch ejabberd_odbc:sql_query_t( + ["select count(*) " + "from pubsub_item " + "where nodeid='", NodeId, "';"]) of + {selected, [_], [{C}]} -> C; + _ -> "0" + end, + + case catch ejabberd_odbc:sql_query_t( + ["select itemid, publisher, creation, modification, payload " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "and ", AttrName," ", Way, " ", Id, " " + "order by ", AttrName," ", Order," limit ", i2l(Max)," ;"]) of + {selected, ["itemid", "publisher", "creation", "modification", "payload"], RItems} -> + case length(RItems) of + 0 -> {result, {[], #rsm_out{count=Count}}}; + _ -> + {_, _, _, F, _} = hd(RItems), + Index = case catch ejabberd_odbc:sql_query_t( + ["select count(*) " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "and ", AttrName," > '", F, "';"]) of + %{selected, [_], [{C}, {In}]} -> [string:strip(C, both, $"), string:strip(In, both, $")]; + {selected, [_], [{In}]} -> In; + _ -> "0" + end, + %{F, _} = string:to_integer(FStr), + {_, _, _, L, _} = lists:last(RItems), + RsmOut = #rsm_out{count=Count, index=Index, first="modification@"++F, last="modification@"++i2l(L)}, + {result, {lists:map(fun(RItem) -> raw_to_item(NodeId, RItem) end, RItems), RsmOut}} + end; + _ -> + {result, {[], none}} + end. get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_flat_odbc:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). + get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, none). +get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> + SubKey = jlib:short_prepd_jid(JID), + GenKey = jlib:short_prepd_bare_jid(JID), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), + 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('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) and (not Whitelisted) -> + %% Node has authorize access model + {error, 'forbidden'}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_items(NodeId, JID, RSM) + end. +get_last_items(NodeId, _From, Count) -> + case catch ejabberd_odbc:sql_query_t( + ["select itemid, publisher, creation, modification, payload " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "order by modification desc limit ", i2l(Count), ";"]) of + {selected, ["itemid", "publisher", "creation", "modification", "payload"], RItems} -> + {result, lists:map(fun(RItem) -> raw_to_item(NodeId, RItem) end, RItems)}; + _ -> + {result, []} + end. + +%% @spec (NodeId, ItemId) -> [Item] | [] +%% NodeId = mod_pubsub:pubsubNodeId() +%% ItemId = string() +%% Item = mod_pubsub:pubsubItems() +%% @doc

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

get_item(NodeId, ItemId) -> - node_flat_odbc:get_item(NodeId, ItemId). + I = ?PUBSUB:escape(ItemId), + case catch ejabberd_odbc:sql_query_t( + ["select itemid, publisher, creation, modification, payload " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "and itemid='", I,"';"]) of + {selected, ["itemid", "publisher", "creation", "modification", "payload"], [RItem]} -> + {result, raw_to_item(NodeId, RItem)}; + _ -> + {error, 'item-not-found'} + end. -get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_flat_odbc:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> + SubKey = jlib:short_prepd_jid(JID), + GenKey = jlib:short_prepd_bare_jid(JID), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), + 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('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) and (not Whitelisted) -> + %% Node has authorize access model + {error, 'forbidden'}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_item(NodeId, ItemId) + end. +%% @spec (Item) -> ok | {error, Reason::stanzaError()} +%% Item = mod_pubsub:pubsubItems() +%% @doc

Write an item into database.

set_item(Item) -> - node_flat_odbc:set_item(Item). + {ItemId, NodeId} = Item#pubsub_item.itemid, + I = ?PUBSUB:escape(ItemId), + {C, _} = Item#pubsub_item.creation, + {M, JID} = Item#pubsub_item.modification, + P = encode_jid(JID), + Payload = Item#pubsub_item.payload, + XML = ?PUBSUB:escape(lists:flatten(lists:map(fun(X) -> xml:element_to_string(X) end, Payload))), + S = fun({T1, T2, T3}) -> + lists:flatten([i2l(T1, 6), ":", i2l(T2, 6), ":", i2l(T3, 6)]) + end, + case catch ejabberd_odbc:sql_query_t( + ["update pubsub_item " + "set publisher='", P, "', modification='", S(M), "', payload='", XML, "' " + "where nodeid='", NodeId, "' and itemid='", I, "';"]) of + {updated, 1} -> + ok; + _ -> + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_item " + "(nodeid, itemid, publisher, creation, modification, payload) " + "values('", NodeId, "', '", I, "', '", P, "', '", S(C), "', '", S(M), "', '", XML, "');"]) + end, + {result, []}. -get_item_name(Host, Node, Id) -> - node_flat_odbc:get_item_name(Host, Node, Id). +%% @spec (NodeId, ItemId) -> ok | {error, Reason::stanzaError()} +%% NodeId = mod_pubsub:pubsubNodeId() +%% ItemId = string() +%% @doc

Delete an item from database.

+del_item(NodeId, ItemId) -> + I = ?PUBSUB:escape(ItemId), + catch ejabberd_odbc:sql_query_t( + ["delete from pubsub_item " + "where itemid='", I, "' " + "and nodeid='", NodeId, "';"]). +del_items(_, []) -> + ok; +del_items(NodeId, [ItemId]) -> + del_item(NodeId, ItemId); +del_items(NodeId, ItemIds) -> + I = string:join([["'", ?PUBSUB:escape(X), "'"] || X <- ItemIds], ","), + catch ejabberd_odbc:sql_query_t( + ["delete from pubsub_item " + "where itemid in (", I, ") " + "and nodeid='", NodeId, "';"]). -get_last_items(NodeId, From, Count) -> - node_flat_odbc:get_last_items(NodeId, From, Count). +%% @doc

Return the name of the node if known: Default is to return +%% node id.

+get_item_name(_Host, _Node, Id) -> + Id. + +%% @spec (Affiliation, Subscription) -> true | false +%% Affiliation = owner | member | publisher | outcast | none +%% Subscription = subscribed | none +%% @doc Determines if the combination of Affiliation and Subscribed +%% are allowed to get items from a node. +can_fetch_item(owner, _) -> true; +can_fetch_item(member, _) -> true; +can_fetch_item(publisher, _) -> true; +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). + +%% Returns the first item where Pred() is true in List +first_in_list(_Pred, []) -> + false; +first_in_list(Pred, [H | T]) -> + case Pred(H) of + true -> {value, H}; + _ -> first_in_list(Pred, T) + end. + +itemids(NodeId, {U, S, R}) -> + itemids(NodeId, encode_jid({U, S, R})); +itemids(NodeId, SJID) -> + case catch ejabberd_odbc:sql_query_t( + ["select itemid " + "from pubsub_item " + "where nodeid='", NodeId, "' " + "and publisher like '", SJID, "%' " + "order by modification desc;"]) of + {selected, ["itemid"], RItems} -> + lists:map(fun({ItemId}) -> ItemId end, RItems); + _ -> + [] + end. + +select_affiliation_subscriptions(NodeId, JID) -> + J = encode_jid(JID), + case catch ejabberd_odbc:sql_query_t( + ["select affiliation,subscriptions from pubsub_state " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {selected, ["affiliation", "subscriptions"], [{A, S}]} -> + {decode_affiliation(A), decode_subscriptions(S)}; + _ -> + {none, []} + end. +select_affiliation_subscriptions(NodeId, JID, JID) -> + select_affiliation_subscriptions(NodeId, JID); +select_affiliation_subscriptions(NodeId, GenKey, SubKey) -> + {result, Affiliation} = get_affiliation(NodeId, GenKey), + {result, Subscriptions} = get_subscriptions(NodeId, SubKey), + {Affiliation, Subscriptions}. + +update_affiliation(NodeId, JID, Affiliation) -> + J = encode_jid(JID), + A = encode_affiliation(Affiliation), + case catch ejabberd_odbc:sql_query_t( + ["update pubsub_state " + "set affiliation='", A, "' " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {updated, 1} -> + ok; + _ -> + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values('", NodeId, "', '", J, "', '", A, "', '');"]) + end. + +update_subscription(NodeId, JID, Subscription) -> + J = encode_jid(JID), + S = encode_subscriptions(Subscription), + case catch ejabberd_odbc:sql_query_t( + ["update pubsub_state " + "set subscriptions='", S, "' " + "where nodeid='", NodeId, "' and jid='", J, "';"]) of + {updated, 1} -> + ok; + _ -> + catch ejabberd_odbc:sql_query_t( + ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values('", NodeId, "', '", J, "', 'n', '", S, "');"]) + end. + +decode_jid(SJID) -> jlib:short_prepd_jid(jlib:string_to_jid(SJID)). + +decode_node(N) -> ?PUBSUB:string_to_node(N). + +decode_affiliation("o") -> owner; +decode_affiliation("p") -> publisher; +decode_affiliation("m") -> member; +decode_affiliation("c") -> outcast; +decode_affiliation(_) -> none. + +decode_subscription("s") -> subscribed; +decode_subscription("p") -> pending; +decode_subscription("u") -> unconfigured; +decode_subscription(_) -> none. +decode_subscriptions(Subscriptions) -> + lists:foldl(fun(Subscription, Acc) -> + case string:tokens(Subscription, ":") of + [S, SubId] -> [{decode_subscription(S), SubId}|Acc]; + _ -> Acc + end + end, [], string:tokens(Subscriptions, ",")). + +encode_jid(JID) -> ?PUBSUB:escape(jlib:jid_to_string(JID)). + +encode_affiliation(owner) -> "o"; +encode_affiliation(publisher) -> "p"; +encode_affiliation(member) -> "m"; +encode_affiliation(outcast) -> "c"; +encode_affiliation(_) -> "n". + +encode_subscription(subscribed) -> "s"; +encode_subscription(pending) -> "p"; +encode_subscription(unconfigured) -> "u"; +encode_subscription(_) -> "n". +encode_subscriptions(Subscriptions) -> + string:join(lists:map(fun({S, SubId}) -> + encode_subscription(S)++":"++SubId + end, Subscriptions), ","). + +%%% record getter/setter + +state_to_raw(NodeId, State) -> + {JID, _} = State#pubsub_state.stateid, + J = encode_jid(JID), + A = encode_affiliation(State#pubsub_state.affiliation), + S = encode_subscriptions(State#pubsub_state.subscriptions), + ["'", NodeId, "', '", J, "', '", A, "', '", S, "'"]. + +raw_to_item(NodeId, {ItemId, SJID, Creation, Modification, XML}) -> + JID = decode_jid(SJID), + ToTime = fun(Str) -> + [T1,T2,T3] = string:tokens(Str, ":"), + {l2i(T1), l2i(T2), l2i(T3)} + end, + Payload = case exmpp_xmlstream:parse_element(XML) of + {error, _Reason} -> []; + [El] -> [El] + end, + #pubsub_item{itemid = {ItemId, NodeId}, + creation={ToTime(Creation), JID}, + modification={ToTime(Modification), JID}, + payload = Payload}. + +l2i(L) when is_list(L) -> list_to_integer(L); +l2i(I) when is_integer(I) -> I. +i2l(I) when is_integer(I) -> integer_to_list(I); +i2l(L) when is_list(L) -> L. +i2l(I, N) when is_integer(I) -> i2l(i2l(I), N); +i2l(L, N) when is_list(L) -> + case length(L) of + N -> L; + C when C > N -> L; + _ -> i2l([$0|L], N) + end. node_to_path(Node) -> node_flat:node_to_path(Node). diff --git a/src/mod_pubsub/node_hometree.erl b/src/mod_pubsub/node_hometree.erl index a5bf4da01..069f38599 100644 --- a/src/mod_pubsub/node_hometree.erl +++ b/src/mod_pubsub/node_hometree.erl @@ -210,14 +210,7 @@ get_item_name(Host, Node, Id) -> node_flat:get_item_name(Host, Node, Id). node_to_path(Node) -> - [Node]. + [list_to_binary(Item) || Item <- string:tokens(binary_to_list(Node), "/")]. path_to_node(Path) -> - case Path of - % default slot - [Node] -> Node; - % handle old possible entries, used when migrating database content to new format - [Node|_] when is_list(Node) -> list_to_binary(string:join([""|Path], "/")); - % default case (used by PEP for example) - _ -> list_to_binary(Path) - end. + list_to_binary(string:join([""|[binary_to_list(Item) || Item <- Path]], "/")). diff --git a/src/mod_pubsub/node_hometree_odbc.erl b/src/mod_pubsub/node_hometree_odbc.erl index 35004f4d3..2dd0503c5 100644 --- a/src/mod_pubsub/node_hometree_odbc.erl +++ b/src/mod_pubsub/node_hometree_odbc.erl @@ -15,7 +15,6 @@ %%% All Rights Reserved.'' %%% This software is copyright 2006-2010, ProcessOne. %%% -%%% %%% @copyright 2006-2010 ProcessOne %%% @author Christophe Romain %%% [http://www.process-one.net/] @@ -23,29 +22,11 @@ %%% @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_hometree_odbc). -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). -include_lib("exmpp/include/exmpp.hrl"). --include("jlib.hrl"). %% for rsm_in and rsm_out records definitions - --define(PUBSUB, mod_pubsub_odbc). -behaviour(gen_pubsub_node). @@ -66,7 +47,6 @@ get_affiliation/2, set_affiliation/3, get_entity_subscriptions/2, - get_entity_subscriptions_for_send_last/2, get_node_subscriptions/1, get_subscriptions/2, set_subscriptions/4, @@ -80,62 +60,18 @@ get_item/2, set_item/1, get_item_name/3, - get_last_items/3, - path_to_node/1, - node_to_path/1 + get_last_items/3, + node_to_path/1, + path_to_node/1 ]). --export([ - decode_jid/1, - decode_node/1, - decode_affiliation/1, - decode_subscriptions/1, - encode_jid/1, - encode_affiliation/1, - encode_subscriptions/1 - ]). -%% ================ -%% API definition -%% ================ -%% @spec (Host, ServerHost, Opts) -> any() -%% Host = mod_pubsub:host() -%% ServerHost = mod_pubsub: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) -> - pubsub_subscription_odbc:init(), - ok. +init(Host, ServerHost, Opts) -> + node_flat_odbc:init(Host, ServerHost, Opts). -%% @spec (Host, ServerHost) -> any() -%% Host = mod_pubsub:host() -%% ServerHost = host() -%% @doc

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

-terminate(_Host, _ServerHost) -> - ok. +terminate(Host, ServerHost) -> + node_flat_odbc:terminate(Host, ServerHost). -%% @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() -> [{deliver_payloads, true}, {notify_config, false}, @@ -157,1231 +93,98 @@ options() -> {odbc, true}, {rsm, true}]. -%% @spec () -> [] -%% @doc Returns the node features features() -> - ["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", - "purge-nodes", - "retract-items", - "retrieve-affiliations", - "retrieve-items", - "retrieve-subscriptions", - "subscribe", - "subscription-notifications", - "subscription-options", - "rsm" - ]. + node_flat_odbc:features(). -%% @spec (Host, ServerHost, Node, ParentNode, Owner, Access) -> bool() -%% Host = mod_pubsub:host() -%% ServerHost = mod_pubsub:host() -%% Node = mod_pubsub:pubsubNode() -%% ParentNode = 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, ServerHost, Node, ParentNode, Owner, Access) -> -%% node_default:check_create_user_permission(Host, ServerHost, Node, ParentNode, Owner, Access).'''

-create_node_permission(Host, ServerHost, Node, _ParentNode, Owner, Access) -> - LOwner = jlib:short_prepd_jid(Owner), - Allowed = case LOwner of - {undefined, BHost, undefined} -> - list_to_binary(Host) == BHost; % pubsub service always allowed - _ -> - {LU, LS, LR} = LOwner, - case acl:match_rule(ServerHost, Access, exmpp_jid:make(LU, LS, LR)) of - allow -> - case node_to_path(Node) of - [<<"home">>, LS, LU | _] -> true; - _ -> false - end; - _ -> - false - end - end, - {result, Allowed}. +%% use same code as node_flat_odbc, but do not limite node to +%% the home/localhost/user/... hierarchy +%% any node is allowed +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). -%% @spec (NodeId, Owner) -> -%% {result, Result} | exit -%% NodeId = mod_pubsub:pubsubNodeId() -%% Owner = mod_pubsub:jid() -%% @doc

create_node(NodeId, Owner) -> - OwnerKey = jlib:short_prepd_bare_jid(Owner), - State = #pubsub_state{stateid = {OwnerKey, NodeId}, affiliation = owner}, - catch ejabberd_odbc:sql_query_t( - ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " - "values(", state_to_raw(NodeId, State), ");"]), - {result, {default, broadcast}}. + node_flat_odbc:create_node(NodeId, Owner). -%% @spec (Removed) -> ok -%% Removed = [mod_pubsub:pubsubNode()] -%% @doc

purge items of deleted nodes after effective deletion.

delete_node(Removed) -> -%% pablo: TODO, this is present on trunk/node_flat but not on trunk/node_flat_odbc. -%% check what is the desired behaviour -%% Tr = fun(#pubsub_state{stateid = {J, _}, subscriptions = Ss}) -> -%% lists:map(fun(S) -> -%% {J, S} -%% end, Ss) -%% end, - Reply = lists:map( - fun(#pubsub_node{id = NodeId} = PubsubNode) -> - Subscriptions = case catch ejabberd_odbc:sql_query_t( - ["select jid, subscriptions " - "from pubsub_state " - "where nodeid='", NodeId, ";"]) of - {selected, ["jid", "subscriptions"], RItems} -> - lists:map(fun({SJID, Subscriptions}) -> - {decode_jid(SJID), decode_subscriptions(Subscriptions)} - end, RItems); - _ -> - [] - end, - %% state and item remove already done thanks to DELETE CASCADE - %% but here we get nothing in States, making notify_retract unavailable ! - %% TODO, remove DELETE CASCADE from schema - {PubsubNode, Subscriptions} - end, Removed), - {result, {default, broadcast, Reply}}. + node_flat_odbc:delete_node(Removed). -%% @spec (NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> -%% {error, Reason} | {result, Result} -%% @doc

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(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - SubKey = jlib:short_prepd_jid(Subscriber), - GenKey = jlib:short_prepd_bare_jid(SubKey), - Authorized = (jlib:short_prepd_bare_jid(Sender) == GenKey), - {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), - Whitelisted = lists:member(Affiliation, [member, publisher, owner]), - PendingSubscription = lists:any(fun({pending, _}) -> true; - (_) -> false - end, Subscriptions), - if - not Authorized -> - %% JIDs do not match - {error, ?ERR_EXTENDED('bad-request', "invalid-jid")}; - Affiliation == outcast -> - %% Requesting entity is blocked - {error, 'forbidden'}; - PendingSubscription -> - %% 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")}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - %%ForbiddenAnonymous -> - %% % Requesting entity is anonymous - %% {error, 'forbidden'}; - true -> - case pubsub_subscription_odbc:subscribe_node(Subscriber, NodeId, Options) of - {result, SubId} -> - NewSub = case AccessModel of - authorize -> pending; - _ -> subscribed - end, - update_subscription(NodeId, SubKey, [{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; - _ -> - {error, 'internal-server-error'} - end - end. +subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> + node_flat_odbc:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). -%% @spec (NodeId, Sender, Subscriber, SubId) -> -%% {error, Reason} | {result, []} -%% NodeId = mod_pubsub:pubsubNodeId() -%% Sender = mod_pubsub:jid() -%% Subscriber = mod_pubsub:jid() -%% SubId = mod_pubsub:subid() -%% Reason = mod_pubsub:stanzaError() -%% @doc

Unsubscribe the Subscriber from the Node.

-unsubscribe_node(NodeId, Sender, Subscriber, SubId) -> - SubKey = jlib:short_prepd_jid(Subscriber), - GenKey = jlib:short_prepd_bare_jid(SubKey), - Authorized = (jlib:short_prepd_bare_jid(Sender) == GenKey), - {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, SubKey), - SubIdExists = case SubId of - [] -> false; - List when is_list(List) -> true; - _ -> false - 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 - Subscriptions == [] -> - {error, ?ERR_EXTENDED('unexpected-request', "not-subscribed")}; - %% Subid supplied, so use that. - SubIdExists -> - Sub = first_in_list(fun(S) -> - case S of - {_Sub, SubId} -> true; - _ -> false - end - end, Subscriptions), - case Sub of - {value, S} -> - delete_subscription(SubKey, NodeId, S, Affiliation, Subscriptions), - {result, default}; - false -> - {error, ?ERR_EXTENDED('unexpected-request', "not-subscribed")} - end; - %% Asking to remove all subscriptions to the given node - SubId == all -> - [delete_subscription(SubKey, NodeId, S, Affiliation, Subscriptions) || S <- Subscriptions], - {result, default}; - %% No subid supplied, but there's only one matching - %% subscription, so use that. - length(Subscriptions) == 1 -> - delete_subscription(SubKey, NodeId, hd(Subscriptions), Affiliation, Subscriptions), - {result, default}; - true -> - {error, ?ERR_EXTENDED('bad-request', "subid-required")} - end. +unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> + node_flat_odbc:unsubscribe_node(NodeId, Sender, Subscriber, SubID). -delete_subscription(SubKey, NodeId, {Subscription, SubId}, Affiliation, Subscriptions) -> - NewSubs = Subscriptions -- [{Subscription, SubId}], - pubsub_subscription_odbc:unsubscribe_node(SubKey, NodeId, SubId), - case {Affiliation, NewSubs} of - {none, []} -> - % Just a regular subscriber, and this is final item, so - % delete the state. - del_state(NodeId, SubKey); - _ -> - update_subscription(NodeId, SubKey, NewSubs) - end. +publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> + node_flat_odbc:publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload). - -%% @spec (NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload) -> -%% {true, PubsubItem} | {result, Reply} -%% NodeId = mod_pubsub:pubsubNodeId() -%% 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 selected behaviour depends on the return parameter: -%%

-%%

-%%

In the default plugin module, the record is unchanged.

-publish_item(NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload) -> - SubKey = jlib:short_prepd_jid(Publisher), - GenKey = jlib:short_prepd_bare_jid(Publisher), - {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), - Subscribed = case PublishModel of - subscribers -> is_subscribed(Subscriptions); - _ -> undefined - end, - if - not ((PublishModel == open) - or ((PublishModel == publishers) - and ((Affiliation == owner) or (Affiliation == publisher))) - or (Subscribed == true)) -> - %% Entity does not have sufficient privileges to publish to node - {error, 'forbidden'}; - true -> - %% TODO: check creation, presence, roster - if MaxItems > 0 -> - %% Note: this works cause set_item tries an update before - %% the insert, and the update just ignore creation field. - PubId = {now(), SubKey}, - set_item(#pubsub_item{itemid = {ItemId, NodeId}, - creation = {now(), GenKey}, - modification = PubId, - payload = Payload}), - Items = [ItemId | itemids(NodeId, GenKey) -- [ItemId]], - {result, {_, OI}} = remove_extra_items(NodeId, MaxItems, Items), - %% set new item list use useless - {result, {default, broadcast, OI}}; - true -> - {result, {default, broadcast, []}} - end - end. - -%% @spec (NodeId, MaxItems, ItemIds) -> {NewItemIds,OldItemIds} -%% NodeId = mod_pubsub:pubsubNodeId() -%% 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(NodeId, MaxItems, ItemIds) -> -%% node_default:remove_extra_items(NodeId, MaxItems, ItemIds).'''

-remove_extra_items(_NodeId, unlimited, ItemIds) -> - {result, {ItemIds, []}}; remove_extra_items(NodeId, MaxItems, ItemIds) -> - NewItems = lists:sublist(ItemIds, MaxItems), - OldItems = lists:nthtail(length(NewItems), ItemIds), - %% Remove extra items: - del_items(NodeId, OldItems), - %% Return the new items list: - {result, {NewItems, OldItems}}. + node_flat_odbc:remove_extra_items(NodeId, MaxItems, ItemIds). -%% @spec (NodeId, Publisher, PublishModel, ItemId) -> -%% {error, Reason::stanzaError()} | -%% {result, []} -%% NodeId = mod_pubsub:pubsubNodeId() -%% Publisher = mod_pubsub:jid() -%% PublishModel = atom() -%% ItemId = string() -%% @doc

Triggers item deletion.

-%%

Default plugin: The user performing the deletion must be the node owner -%% or a publisher, or PublishModel being open.

delete_item(NodeId, Publisher, PublishModel, ItemId) -> - GenKey = jlib:short_prepd_bare_jid(Publisher), - {result, Affiliation} = get_affiliation(NodeId, GenKey), - Allowed = (Affiliation == publisher) orelse (Affiliation == owner) - orelse (PublishModel == open) - orelse case get_item(NodeId, ItemId) of - {result, #pubsub_item{creation = {_, GenKey}}} -> true; - _ -> false - end, - if - not Allowed -> - %% Requesting entity does not have sufficient privileges - {error, 'forbidden'}; - true -> - case del_item(NodeId, ItemId) of - {updated, 1} -> - %% set new item list use useless - {result, {default, broadcast}}; - _ -> - %% Non-existent node or item - {error, 'item-not-found'} - end - end. + node_flat_odbc:delete_item(NodeId, Publisher, PublishModel, ItemId). -%% @spec (NodeId, Owner) -> -%% {error, Reason::stanzaError()} | -%% {result, {default, broadcast}} -%% NodeId = mod_pubsub:pubsubNodeId() -%% Owner = mod_pubsub:jid() purge_node(NodeId, Owner) -> - GenKey = jlib:short_prepd_bare_jid(Owner), - GenState = get_state(NodeId, GenKey), - case GenState of - #pubsub_state{affiliation = owner} -> - {result, States} = get_states(NodeId), - lists:foreach( - fun(#pubsub_state{items = []}) -> ok; - (#pubsub_state{items = Items}) -> del_items(NodeId, Items) - end, States), - {result, {default, broadcast}}; - _ -> - %% Entity is not owner - {error, 'forbidden'} - end. + node_flat_odbc:purge_node(NodeId, Owner). -%% @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) -> - GenKey = jlib:short_prepd_bare_jid(Owner), - H = ?PUBSUB:escape(Host), - J = encode_jid(GenKey), - Reply = case catch ejabberd_odbc:sql_query_t( - ["select node, type, i.nodeid, affiliation " - "from pubsub_state i, pubsub_node n " - "where i.nodeid = n.nodeid " - "and jid='", J, "' " - "and host='", H, "';"]) of - {selected, ["node", "type", "nodeid", "affiliation"], RItems} -> - lists:map(fun({N, T, I, A}) -> - Node = nodetree_tree_odbc:raw_to_node(Host, {N, "", T, I}), - {Node, decode_affiliation(A)} - end, RItems); - _ -> - [] - end, - {result, Reply}. + node_flat_odbc:get_entity_affiliations(Host, Owner). get_node_affiliations(NodeId) -> - Reply = case catch ejabberd_odbc:sql_query_t( - ["select jid, affiliation " - "from pubsub_state " - "where nodeid='", NodeId, "';"]) of - {selected, ["jid", "affiliation"], RItems} -> - lists:map(fun({J, A}) -> {decode_jid(J), decode_affiliation(A)} end, RItems); - _ -> - [] - end, - {result, Reply}. + node_flat_odbc:get_node_affiliations(NodeId). get_affiliation(NodeId, Owner) -> - GenKey = jlib:short_prepd_bare_jid(Owner), - J = encode_jid(GenKey), - Reply = case catch ejabberd_odbc:sql_query_t( - ["select affiliation from pubsub_state " - "where nodeid='", NodeId, "' and jid='", J, "';"]) of - {selected, ["affiliation"], [{A}]} -> decode_affiliation(A); - _ -> none - end, - {result, Reply}. + node_flat_odbc:get_affiliation(NodeId, Owner). -set_affiliation(NodeId, Owner, Affiliation) when ?IS_JID(Owner)-> - GenKey = jlib:short_prepd_bare_jid(Owner), - {_, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey), - case {Affiliation, Subscriptions} of - {none, none} -> - del_state(NodeId, GenKey); - _ -> - update_affiliation(NodeId, GenKey, Affiliation) - end. +set_affiliation(NodeId, Owner, Affiliation) -> + node_flat_odbc:set_affiliation(NodeId, Owner, Affiliation). -%% @spec (Host, Owner) -> [{Node,Subscription}] -%% Host = host() -%% Owner = 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) -> - SubKey = jlib:short_prepd_jid(Owner), - GenKey = jlib:short_prepd_bare_jid(Owner), - H = ?PUBSUB:escape(Host), - SJ = encode_jid(SubKey), - GJ = encode_jid(GenKey), - Query = case SubKey of - GenKey -> - ["select node, type, i.nodeid, jid, subscriptions " - "from pubsub_state i, pubsub_node n " - "where i.nodeid = n.nodeid " - "and jid like '", GJ, "%' " - "and host='", H, "';"]; - _ -> - ["select node, type, i.nodeid, jid, subscriptions " - "from pubsub_state i, pubsub_node n " - "where i.nodeid = n.nodeid " - "and jid in ('", SJ, "', '", GJ, "') " - "and host='", H, "';"] - end, - Reply = case catch ejabberd_odbc:sql_query_t(Query) of - {selected, ["node", "type", "nodeid", "jid", "subscriptions"], RItems} -> - lists:foldl(fun({N, T, I, J, S}, Acc) -> - Node = nodetree_tree_odbc:raw_to_node(Host, {N, "", T, I}), - Jid = decode_jid(J), - case decode_subscriptions(S) of - [] -> - [{Node, none, Jid}|Acc]; - Subs -> - lists:foldl(fun({Sub, SubId}, Acc2) -> [{Node, Sub, SubId, Jid}|Acc2]; - (Sub, Acc2) -> [{Node, Sub, Jid}|Acc2] - end, Acc, Subs) - end - end, [], RItems); - _ -> - [] - end, - {result, Reply}. -%% do the same as get_entity_subscriptions but filter result only to -%% nodes having send_last_published_item=on_sub_and_presence -%% as this call avoid seeking node, it must return node and type as well -get_entity_subscriptions_for_send_last(Host, Owner) -> - SubKey = jlib:short_prepd_jid(Owner), - GenKey = jlib:short_prepd_bare_jid(Owner), - H = ?PUBSUB:escape(Host), - SJ = encode_jid(SubKey), - GJ = encode_jid(GenKey), - Query = case SubKey of - GenKey -> - ["select node, type, i.nodeid, jid, subscriptions " - "from pubsub_state i, pubsub_node n, pubsub_node_option o " - "where i.nodeid = n.nodeid and n.nodeid = o.nodeid " - "and name='send_last_published_item' and val='on_sub_and_presence' " - "and jid like '", GJ, "%' " - "and host='", H, "';"]; - _ -> - ["select node, type, i.nodeid, jid, subscriptions " - "from pubsub_state i, pubsub_node n, pubsub_node_option o " - "where i.nodeid = n.nodeid and n.nodeid = o.nodeid " - "and name='send_last_published_item' and val='on_sub_and_presence' " - "and jid in ('", SJ, "', '", GJ, "') " - "and host='", H, "';"] - end, - Reply = case catch ejabberd_odbc:sql_query_t(Query) of - {selected, ["node", "type", "nodeid", "jid", "subscriptions"], RItems} -> - lists:foldl(fun({N, T, I, J, S}, Acc) -> - Node = nodetree_tree_odbc:raw_to_node(Host, {N, "", T, I}), - Jid = decode_jid(J), - case decode_subscriptions(S) of - [] -> - [{Node, none, Jid}|Acc]; - Subs -> - lists:foldl(fun({Sub, SubId}, Acc2) -> [{Node, Sub, SubId, Jid}|Acc2]; - (Sub, Acc2) -> [{Node, Sub, Jid}|Acc2] - end, Acc, Subs) - end - end, [], RItems); - _ -> - [] - end, - {result, Reply}. + node_flat_odbc:get_entity_subscriptions(Host, Owner). get_node_subscriptions(NodeId) -> - Reply = case catch ejabberd_odbc:sql_query_t( - ["select jid, subscriptions " - "from pubsub_state " - "where nodeid='", NodeId, "';"]) of - {selected, ["jid", "subscriptions"], RItems} -> - lists:foldl(fun({J, S}, Acc) -> - Jid = decode_jid(J), - case decode_subscriptions(S) of - [] -> - [{Jid, none}|Acc]; - Subs -> - lists:foldl(fun({Sub, SubId}, Acc2) -> [{Jid, Sub, SubId}|Acc2]; - (Sub, Acc2) -> [{Jid, Sub}|Acc2] - end, Acc, Subs) - end - end, [], RItems); - _ -> - [] - end, - {result, Reply}. - + node_flat_odbc:get_node_subscriptions(NodeId). get_subscriptions(NodeId, Owner) -> - SubKey = jlib:short_prepd_jid(Owner), - J = encode_jid(SubKey), - Reply = case catch ejabberd_odbc:sql_query_t( - ["select subscriptions from pubsub_state " - "where nodeid='", NodeId, "' and jid='", J, "';"]) of - {selected, ["subscriptions"], [{S}]} -> decode_subscriptions(S); - _ -> [] - end, - {result, Reply}. + node_flat_odbc:get_subscriptions(NodeId, Owner). set_subscriptions(NodeId, Owner, Subscription, SubId) -> - SubKey = jlib:short_prepd_jid(Owner), - SubState = get_state_without_itemids(NodeId, SubKey), - case {SubId, SubState#pubsub_state.subscriptions} of - {_, []} -> - case Subscription of - none -> {error, ?ERR_EXTENDED('bad_request', "not-subscribed")}; - _ -> new_subscription(NodeId, Owner, Subscription, SubState) - end; - {"", [{_, SID}]} -> - case Subscription of - none -> unsub_with_subid(NodeId, SID, SubState); - _ -> replace_subscription({Subscription, SID}, SubState) - end; - {"", [_|_]} -> - {error, ?ERR_EXTENDED('bad_request', "subid-required")}; - _ -> - case Subscription of - none -> unsub_with_subid(NodeId, SubId, SubState); - _ -> replace_subscription({Subscription, SubId}, SubState) - end - end. + node_flat_odbc:set_subscriptions(NodeId, Owner, Subscription, SubId). -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(NodeId, Owner, Subscription, SubState) -> - case pubsub_subscription_odbc:subscribe_node(Owner, NodeId, []) of - {result, SubId} -> - Subscriptions = SubState#pubsub_state.subscriptions, - set_state(SubState#pubsub_state{subscriptions = [{Subscription, SubId} | Subscriptions]}), - {Subscription, SubId}; - _ -> - {error, 'internal-server-error'} - end. - -unsub_with_subid(NodeId, SubId, SubState) -> - pubsub_subscription_odbc:unsubscribe_node(SubState#pubsub_state.stateid, - NodeId, SubId), - NewSubs = lists:filter(fun ({_, SID}) -> SubId =/= SID end, - SubState#pubsub_state.subscriptions), - case {NewSubs, SubState#pubsub_state.affiliation} of - {[], none} -> - del_state(NodeId, element(1, SubState#pubsub_state.stateid)); - _ -> - set_state(SubState#pubsub_state{subscriptions = NewSubs}) - end. - -%% @spec (Host, Owner) -> {result, [Node]} | {error, Reason} -%% Host = host() -%% Owner = jid() -%% Node = pubsubNode() -%% @doc

Returns a list of Owner's nodes on Host with pending -%% subscriptions.

get_pending_nodes(Host, Owner) -> - KenKey = jlib:short_prepd_bare_jid(Owner), - States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, - affiliation = owner, - _ = '_'}), - NodeIDs = [ID || #pubsub_state{stateid = {_, ID}} <- States], - NodeTree = case catch ets:lookup(gen_mod:get_module_proc(Host, config), nodetree) of - [{nodetree, N}] -> N; - _ -> nodetree_tree_odbc - end, - Reply = mnesia:foldl(fun(#pubsub_state{stateid = {_, NID}} = S, Acc) -> - case lists:member(NID, NodeIDs) of - true -> - case get_nodes_helper(NodeTree, S) of - {value, Node} -> [Node | Acc]; - false -> Acc - end; - false -> - Acc - end - end, [], pubsub_state), - {result, Reply}. + node_flat_odbc:get_pending_nodes(Host, Owner). -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. - -%% @spec (NodeId) -> {result, [State] | []} -%% NodeId = mod_pubsub:pubsubNodeId() -%% State = mod_pubsub:pubsubState() -%% @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(NodeId) -> -%% node_default:get_states(NodeId).'''

get_states(NodeId) -> - case catch ejabberd_odbc:sql_query_t( - ["select jid, affiliation, subscriptions " - "from pubsub_state " - "where nodeid='", NodeId, "';"]) of - {selected, ["jid", "affiliation", "subscriptions"], RItems} -> - {result, lists:map(fun({SJID, Affiliation, Subscriptions}) -> - #pubsub_state{stateid = {decode_jid(SJID), NodeId}, - items = itemids(NodeId, SJID), - affiliation = decode_affiliation(Affiliation), - subscriptions = decode_subscriptions(Subscriptions)} - end, RItems)}; - _ -> - {result, []} - end. + node_flat_odbc:get_states(NodeId). - -%% @spec (NodeId, JID) -> [State] | [] -%% NodeId = mod_pubsub:pubsubNodeId() -%% JID = mod_pubsub:jid() -%% State = mod_pubsub:pubsubItems() -%% @doc

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

get_state(NodeId, JID) -> - State = get_state_without_itemids(NodeId, JID), - {SJID, _} = State#pubsub_state.stateid, - State#pubsub_state{items = itemids(NodeId, SJID)}. -get_state_without_itemids(NodeId, JID) -> - J = encode_jid(JID), - case catch ejabberd_odbc:sql_query_t( - ["select jid, affiliation, subscriptions " - "from pubsub_state " - "where jid='", J, "' " - "and nodeid='", NodeId, "';"]) of - {selected, ["jid", "affiliation", "subscriptions"], [{SJID, Affiliation, Subscriptions}]} -> - #pubsub_state{stateid = {decode_jid(SJID), NodeId}, - affiliation = decode_affiliation(Affiliation), - subscriptions = decode_subscriptions(Subscriptions)}; - _ -> - #pubsub_state{stateid={JID, NodeId}} - end. + node_flat_odbc:get_state(NodeId, JID). -%% @spec (State) -> ok | {error, Reason::stanzaError()} -%% State = mod_pubsub:pubsubStates() -%% @doc

Write a state into database.

-set_state(State) when is_record(State, pubsub_state) -> - {_, NodeId} = State#pubsub_state.stateid, - set_state(NodeId, State). -set_state(NodeId, State) -> - %% NOTE: in odbc version, as we do not handle item list, - %% we just need to update affiliation and subscription - %% cause {JID,NodeId} is the key. if it does not exists, then we insert it. - %% MySQL can be optimized using INSERT ... ON DUPLICATE KEY as well - {JID, _} = State#pubsub_state.stateid, - J = encode_jid(JID), - S = encode_subscriptions(State#pubsub_state.subscriptions), - A = encode_affiliation(State#pubsub_state.affiliation), - case catch ejabberd_odbc:sql_query_t( - ["update pubsub_state " - "set subscriptions='", S, "', affiliation='", A, "' " - "where nodeid='", NodeId, "' and jid='", J, "';"]) of - {updated, 1} -> - ok; - _ -> - catch ejabberd_odbc:sql_query_t( - ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " - "values('", NodeId, "', '", J, "', '", A, "', '", S, "');"]) - end, - {result, []}. +set_state(State) -> + node_flat_odbc:set_state(State). -%% @spec (NodeId, JID) -> ok | {error, Reason::stanzaError()} -%% NodeId = mod_pubsub:pubsubNodeId() -%% @doc

Delete a state from database.

-del_state(NodeId, JID) -> - J = encode_jid(JID), - catch ejabberd_odbc:sql_query_t( - ["delete from pubsub_state " - "where jid='", J, "' " - "and nodeid='", NodeId, "';"]), - ok. +get_items(NodeId, From) -> + node_flat_odbc:get_items(NodeId, From). -%% @spec (NodeId, From) -> {[Items], RsmOut} | [] -%% NodeId = mod_pubsub:pubsubNodeId() -%% 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(NodeId, From) -> -%% node_default:get_items(NodeId, From).'''

-get_items(NodeId, _From) -> - case catch ejabberd_odbc:sql_query_t( - ["select itemid, publisher, creation, modification, payload " - "from pubsub_item " - "where nodeid='", NodeId, "' " - "order by modification desc;"]) of - {selected, ["itemid", "publisher", "creation", "modification", "payload"], RItems} -> - {result, lists:map(fun(RItem) -> raw_to_item(NodeId, RItem) end, RItems)}; - _ -> - {result, []} - end. -get_items(NodeId, From, none) -> - MaxItems = case catch ejabberd_odbc:sql_query_t( - ["select val from pubsub_node_option " - "where nodeid='", NodeId, "' " - "and name='max_items';"]) of - {selected, ["val"], [{Value}]} -> - Tokens = element(2, erl_scan:string(Value++".")), - element(2, erl_parse:parse_term(Tokens)); - _ -> - ?MAXITEMS - end, - get_items(NodeId, From, #rsm_in{max=MaxItems}); -get_items(NodeId, _From, #rsm_in{max=M, direction=Direction, id=I, index=IncIndex})-> - Max = ?PUBSUB:escape(i2l(M)), - - {Way, Order} = case Direction of - aft -> {"<", "desc"}; - before when I == [] -> {"is not", "asc"}; - before -> {">", "asc"}; - _ when IncIndex =/= undefined -> {"<", "desc"}; % using index - _ -> {"is not", "desc"}% Can be better - end, - [AttrName, Id] = case I of - undefined when IncIndex =/= undefined -> - case catch ejabberd_odbc:sql_query_t( - ["select modification from pubsub_item pi " - "where exists ( " - "select count(*) as count1 " - "from pubsub_item " - "where nodeid='", NodeId, "' " - "and modification > pi.modification " - "having count1 = ",?PUBSUB:escape(i2l(IncIndex))," );"]) of - {selected, [_], [{O}]} -> ["modification", "'"++O++"'"]; - _ -> ["modification", "null"] - end; - undefined -> ["modification", "null"]; - [] -> ["modification", "null"]; - I -> [A, B] = string:tokens(?PUBSUB:escape(i2l(I)), "@"), - [A, "'"++B++"'"] - end, - Count= case catch ejabberd_odbc:sql_query_t( - ["select count(*) " - "from pubsub_item " - "where nodeid='", NodeId, "';"]) of - {selected, [_], [{C}]} -> C; - _ -> "0" - end, - - case catch ejabberd_odbc:sql_query_t( - ["select itemid, publisher, creation, modification, payload " - "from pubsub_item " - "where nodeid='", NodeId, "' " - "and ", AttrName," ", Way, " ", Id, " " - "order by ", AttrName," ", Order," limit ", i2l(Max)," ;"]) of - {selected, ["itemid", "publisher", "creation", "modification", "payload"], RItems} -> - case length(RItems) of - 0 -> {result, {[], #rsm_out{count=Count}}}; - _ -> - {_, _, _, F, _} = hd(RItems), - Index = case catch ejabberd_odbc:sql_query_t( - ["select count(*) " - "from pubsub_item " - "where nodeid='", NodeId, "' " - "and ", AttrName," > '", F, "';"]) of - %{selected, [_], [{C}, {In}]} -> [string:strip(C, both, $"), string:strip(In, both, $")]; - {selected, [_], [{In}]} -> In; - _ -> "0" - end, - %{F, _} = string:to_integer(FStr), - {_, _, _, L, _} = lists:last(RItems), - RsmOut = #rsm_out{count=Count, index=Index, first="modification@"++F, last="modification@"++i2l(L)}, - {result, {lists:map(fun(RItem) -> raw_to_item(NodeId, RItem) end, RItems), RsmOut}} - end; - _ -> - {result, {[], none}} - end. get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, none). -get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> - SubKey = jlib:short_prepd_jid(JID), - GenKey = jlib:short_prepd_bare_jid(JID), - {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), - 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('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) and (not Whitelisted) -> - %% Node has authorize access model - {error, 'forbidden'}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> - get_items(NodeId, JID, RSM) - end. + node_flat_odbc:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). -get_last_items(NodeId, _From, Count) -> - case catch ejabberd_odbc:sql_query_t( - ["select itemid, publisher, creation, modification, payload " - "from pubsub_item " - "where nodeid='", NodeId, "' " - "order by modification desc limit ", i2l(Count), ";"]) of - {selected, ["itemid", "publisher", "creation", "modification", "payload"], RItems} -> - {result, lists:map(fun(RItem) -> raw_to_item(NodeId, RItem) end, RItems)}; - _ -> - {result, []} - end. - -%% @spec (NodeId, ItemId) -> [Item] | [] -%% NodeId = mod_pubsub:pubsubNodeId() -%% ItemId = string() -%% Item = mod_pubsub:pubsubItems() -%% @doc

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

get_item(NodeId, ItemId) -> - I = ?PUBSUB:escape(ItemId), - case catch ejabberd_odbc:sql_query_t( - ["select itemid, publisher, creation, modification, payload " - "from pubsub_item " - "where nodeid='", NodeId, "' " - "and itemid='", I,"';"]) of - {selected, ["itemid", "publisher", "creation", "modification", "payload"], [RItem]} -> - {result, raw_to_item(NodeId, RItem)}; - _ -> - {error, 'item-not-found'} - end. + node_flat_odbc:get_item(NodeId, ItemId). -get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> - SubKey = jlib:short_prepd_jid(JID), - GenKey = jlib:short_prepd_bare_jid(JID), - {Affiliation, Subscriptions} = select_affiliation_subscriptions(NodeId, GenKey, SubKey), - 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('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) and (not Whitelisted) -> - %% Node has authorize access model - {error, 'forbidden'}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> - get_item(NodeId, ItemId) - end. +get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_flat_odbc:get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). -%% @spec (Item) -> ok | {error, Reason::stanzaError()} -%% Item = mod_pubsub:pubsubItems() -%% @doc

Write an item into database.

set_item(Item) -> - {ItemId, NodeId} = Item#pubsub_item.itemid, - I = ?PUBSUB:escape(ItemId), - {C, _} = Item#pubsub_item.creation, - {M, JID} = Item#pubsub_item.modification, - P = encode_jid(JID), - Payload = Item#pubsub_item.payload, - XML = ?PUBSUB:escape(lists:flatten(lists:map(fun(X) -> xml:element_to_string(X) end, Payload))), - S = fun({T1, T2, T3}) -> - lists:flatten([i2l(T1, 6), ":", i2l(T2, 6), ":", i2l(T3, 6)]) - end, - case catch ejabberd_odbc:sql_query_t( - ["update pubsub_item " - "set publisher='", P, "', modification='", S(M), "', payload='", XML, "' " - "where nodeid='", NodeId, "' and itemid='", I, "';"]) of - {updated, 1} -> - ok; - _ -> - catch ejabberd_odbc:sql_query_t( - ["insert into pubsub_item " - "(nodeid, itemid, publisher, creation, modification, payload) " - "values('", NodeId, "', '", I, "', '", P, "', '", S(C), "', '", S(M), "', '", XML, "');"]) - end, - {result, []}. + node_flat_odbc:set_item(Item). -%% @spec (NodeId, ItemId) -> ok | {error, Reason::stanzaError()} -%% NodeId = mod_pubsub:pubsubNodeId() -%% ItemId = string() -%% @doc

Delete an item from database.

-del_item(NodeId, ItemId) -> - I = ?PUBSUB:escape(ItemId), - catch ejabberd_odbc:sql_query_t( - ["delete from pubsub_item " - "where itemid='", I, "' " - "and nodeid='", NodeId, "';"]). -del_items(_, []) -> - ok; -del_items(NodeId, [ItemId]) -> - del_item(NodeId, ItemId); -del_items(NodeId, ItemIds) -> - I = string:join([["'", ?PUBSUB:escape(X), "'"] || X <- ItemIds], ","), - catch ejabberd_odbc:sql_query_t( - ["delete from pubsub_item " - "where itemid in (", I, ") " - "and nodeid='", NodeId, "';"]). +get_item_name(Host, Node, Id) -> + node_flat_odbc:get_item_name(Host, Node, Id). -%% @doc

Return the name of the node if known: Default is to return -%% node id.

-get_item_name(_Host, _Node, Id) -> - Id. - -%% @spec (Affiliation, Subscription) -> true | false -%% Affiliation = owner | member | publisher | outcast | none -%% Subscription = subscribed | none -%% @doc Determines if the combination of Affiliation and Subscribed -%% are allowed to get items from a node. -can_fetch_item(owner, _) -> true; -can_fetch_item(member, _) -> true; -can_fetch_item(publisher, _) -> true; -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). - -%% Returns the first item where Pred() is true in List -first_in_list(_Pred, []) -> - false; -first_in_list(Pred, [H | T]) -> - case Pred(H) of - true -> {value, H}; - _ -> first_in_list(Pred, T) - end. - -itemids(NodeId, {U, S, R}) -> - itemids(NodeId, encode_jid({U, S, R})); -itemids(NodeId, SJID) -> - case catch ejabberd_odbc:sql_query_t( - ["select itemid " - "from pubsub_item " - "where nodeid='", NodeId, "' " - "and publisher like '", SJID, "%' " - "order by modification desc;"]) of - {selected, ["itemid"], RItems} -> - lists:map(fun({ItemId}) -> ItemId end, RItems); - _ -> - [] - end. - -select_affiliation_subscriptions(NodeId, JID) -> - J = encode_jid(JID), - case catch ejabberd_odbc:sql_query_t( - ["select affiliation,subscriptions from pubsub_state " - "where nodeid='", NodeId, "' and jid='", J, "';"]) of - {selected, ["affiliation", "subscriptions"], [{A, S}]} -> - {decode_affiliation(A), decode_subscriptions(S)}; - _ -> - {none, []} - end. -select_affiliation_subscriptions(NodeId, JID, JID) -> - select_affiliation_subscriptions(NodeId, JID); -select_affiliation_subscriptions(NodeId, GenKey, SubKey) -> - {result, Affiliation} = get_affiliation(NodeId, GenKey), - {result, Subscriptions} = get_subscriptions(NodeId, SubKey), - {Affiliation, Subscriptions}. - -update_affiliation(NodeId, JID, Affiliation) -> - J = encode_jid(JID), - A = encode_affiliation(Affiliation), - case catch ejabberd_odbc:sql_query_t( - ["update pubsub_state " - "set affiliation='", A, "' " - "where nodeid='", NodeId, "' and jid='", J, "';"]) of - {updated, 1} -> - ok; - _ -> - catch ejabberd_odbc:sql_query_t( - ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " - "values('", NodeId, "', '", J, "', '", A, "', '');"]) - end. - -update_subscription(NodeId, JID, Subscription) -> - J = encode_jid(JID), - S = encode_subscriptions(Subscription), - case catch ejabberd_odbc:sql_query_t( - ["update pubsub_state " - "set subscriptions='", S, "' " - "where nodeid='", NodeId, "' and jid='", J, "';"]) of - {updated, 1} -> - ok; - _ -> - catch ejabberd_odbc:sql_query_t( - ["insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " - "values('", NodeId, "', '", J, "', 'n', '", S, "');"]) - end. - -decode_jid(SJID) -> jlib:short_prepd_jid(jlib:string_to_jid(SJID)). - -decode_node(N) -> ?PUBSUB:string_to_node(N). - -decode_affiliation("o") -> owner; -decode_affiliation("p") -> publisher; -decode_affiliation("m") -> member; -decode_affiliation("c") -> outcast; -decode_affiliation(_) -> none. - -decode_subscription("s") -> subscribed; -decode_subscription("p") -> pending; -decode_subscription("u") -> unconfigured; -decode_subscription(_) -> none. -decode_subscriptions(Subscriptions) -> - lists:foldl(fun(Subscription, Acc) -> - case string:tokens(Subscription, ":") of - [S, SubId] -> [{decode_subscription(S), SubId}|Acc]; - _ -> Acc - end - end, [], string:tokens(Subscriptions, ",")). - -encode_jid(JID) -> ?PUBSUB:escape(jlib:jid_to_string(JID)). - -encode_affiliation(owner) -> "o"; -encode_affiliation(publisher) -> "p"; -encode_affiliation(member) -> "m"; -encode_affiliation(outcast) -> "c"; -encode_affiliation(_) -> "n". - -encode_subscription(subscribed) -> "s"; -encode_subscription(pending) -> "p"; -encode_subscription(unconfigured) -> "u"; -encode_subscription(_) -> "n". -encode_subscriptions(Subscriptions) -> - string:join(lists:map(fun({S, SubId}) -> - encode_subscription(S)++":"++SubId - end, Subscriptions), ","). - -%%% record getter/setter - -state_to_raw(NodeId, State) -> - {JID, _} = State#pubsub_state.stateid, - J = encode_jid(JID), - A = encode_affiliation(State#pubsub_state.affiliation), - S = encode_subscriptions(State#pubsub_state.subscriptions), - ["'", NodeId, "', '", J, "', '", A, "', '", S, "'"]. - -raw_to_item(NodeId, {ItemId, SJID, Creation, Modification, XML}) -> - JID = decode_jid(SJID), - ToTime = fun(Str) -> - [T1,T2,T3] = string:tokens(Str, ":"), - {l2i(T1), l2i(T2), l2i(T3)} - end, - Payload = case exmpp_xmlstream:parse_element(XML) of - {error, _Reason} -> []; - [El] -> [El] - end, - #pubsub_item{itemid = {ItemId, NodeId}, - creation={ToTime(Creation), JID}, - modification={ToTime(Modification), JID}, - payload = Payload}. - -l2i(L) when is_list(L) -> list_to_integer(L); -l2i(I) when is_integer(I) -> I. -i2l(I) when is_integer(I) -> integer_to_list(I); -i2l(L) when is_list(L) -> L. -i2l(I, N) when is_integer(I) -> i2l(i2l(I), N); -i2l(L, N) when is_list(L) -> - case length(L) of - N -> L; - C when C > N -> L; - _ -> i2l([$0|L], N) - end. +get_last_items(NodeId, From, Count) -> + node_flat_odbc:get_last_items(NodeId, From, Count). node_to_path(Node) -> - node_flat:node_to_path(Node). + node_hometree:node_to_path(Node). path_to_node(Path) -> - node_flat:path_to_node(Path). + node_hometree:path_to_node(Path). diff --git a/src/mod_pubsub/pubsub_odbc.patch b/src/mod_pubsub/pubsub_odbc.patch index c62721242..ca617b9f3 100644 --- a/src/mod_pubsub/pubsub_odbc.patch +++ b/src/mod_pubsub/pubsub_odbc.patch @@ -1,5 +1,5 @@ ---- mod_pubsub.erl 2010-05-17 11:02:22.000000000 +0200 -+++ mod_pubsub_odbc.erl 2010-05-17 11:02:08.000000000 +0200 +--- mod_pubsub.erl 2010-05-17 22:05:12.000000000 +0200 ++++ mod_pubsub_odbc.erl 2010-05-18 17:28:09.000000000 +0200 @@ -42,7 +42,7 @@ %%% 6.2.3.1, 6.2.3.5, and 6.3. For information on subscription leases see %%% XEP-0060 section 12.18.