util.pluginloader: Return full file path from internal file loader on success, not...
[prosody.git] / util / pubsub.lua
index 9cb5c7ec1035aaa771413c64a4762da80d36eca2..69e79acc2894d52ab45c90f91f516768d0c7c86b 100644 (file)
+module("pubsub", package.seeall);
 
-local ipairs, pairs, setmetatable, type = 
-        ipairs, pairs, setmetatable, type;
+local service = {};
+local service_mt = { __index = service };
 
-module "pubsub"
+local default_config = {
+       broadcaster = function () end;
+       get_affiliation = function () end;
+       capabilities = {};
+};
 
-local pubsub_node_mt = { __index = _M };
+function new(config)
+       config = config or {};
+       return setmetatable({
+               config = setmetatable(config, { __index = default_config });
+               affiliations = {};
+               subscriptions = {};
+               nodes = {};
+       }, service_mt);
+end
 
-function new_node(name)
-       return setmetatable({ name = name, subscribers = {} }, pubsub_node_mt);
+function service:jids_equal(jid1, jid2)
+       local normalize = self.config.normalize_jid;
+       return normalize(jid1) == normalize(jid2);
 end
 
-function set_subscribers(node, subscribers_list, list_type)
-       local subscribers = node.subscribers;
+function service:may(node, actor, action)
+       if actor == true then return true; end
+       
+       local node_obj = self.nodes[node];
+       local node_aff = node_obj and node_obj.affiliations[actor];
+       local service_aff = self.affiliations[actor]
+                        or self.config.get_affiliation(actor, node, action)
+                        or "none";
+       
+       -- Check if node allows/forbids it
+       local node_capabilities = node_obj and node_obj.capabilities;
+       if node_capabilities then
+               local caps = node_capabilities[node_aff or service_aff];
+               if caps then
+                       local can = caps[action];
+                       if can ~= nil then
+                               return can;
+                       end
+               end
+       end
+       
+       -- Check service-wide capabilities instead
+       local service_capabilities = self.config.capabilities;
+       local caps = service_capabilities[node_aff or service_aff];
+       if caps then
+               local can = caps[action];
+               if can ~= nil then
+                       return can;
+               end
+       end
        
-       if list_type == "array" then
-               for _, jid in ipairs(subscribers_list) do
-                       if not subscribers[jid] then
-                               node:add_subscriber(jid);
+       return false;
+end
+
+function service:set_affiliation(node, actor, jid, affiliation)
+       -- Access checking
+       if not self:may(node, actor, "set_affiliation") then
+               return false, "forbidden";
+       end
+       --
+       local node_obj = self.nodes[node];
+       if not node_obj then
+               return false, "item-not-found";
+       end
+       node_obj.affiliations[jid] = affiliation;
+       local _, jid_sub = self:get_subscription(node, true, jid);
+       if not jid_sub and not self:may(node, jid, "be_unsubscribed") then
+               local ok, err = self:add_subscription(node, true, jid);
+               if not ok then
+                       return ok, err;
+               end
+       elseif jid_sub and not self:may(node, jid, "be_subscribed") then
+               local ok, err = self:add_subscription(node, true, jid);
+               if not ok then
+                       return ok, err;
+               end
+       end
+       return true;
+end
+
+function service:add_subscription(node, actor, jid, options)
+       -- Access checking
+       local cap;
+       if actor == true or jid == actor or self:jids_equal(actor, jid) then
+               cap = "subscribe";
+       else
+               cap = "subscribe_other";
+       end
+       if not self:may(node, actor, cap) then
+               return false, "forbidden";
+       end
+       if not self:may(node, jid, "be_subscribed") then
+               return false, "forbidden";
+       end
+       --
+       local node_obj = self.nodes[node];
+       if not node_obj then
+               if not self.config.autocreate_on_subscribe then
+                       return false, "item-not-found";
+               else
+                       local ok, err = self:create(node, actor);
+                       if not ok then
+                               return ok, err;
                        end
+                       node_obj = self.nodes[node];
+               end
+       end
+       node_obj.subscribers[jid] = options or true;
+       local normal_jid = self.config.normalize_jid(jid);
+       local subs = self.subscriptions[normal_jid];
+       if subs then
+               if not subs[jid] then
+                       subs[jid] = { [node] = true };
+               else
+                       subs[jid][node] = true;
                end
-       elseif (not list_type) or list_type == "set" then
-               for jid in pairs(subscribers_list) do
-                       if type(jid) == "string" then
-                               node:add_subscriber(jid);
+       else
+               self.subscriptions[normal_jid] = { [jid] = { [node] = true } };
+       end
+       return true;
+end
+
+function service:remove_subscription(node, actor, jid)
+       -- Access checking
+       local cap;
+       if actor == true or jid == actor or self:jids_equal(actor, jid) then
+               cap = "unsubscribe";
+       else
+               cap = "unsubscribe_other";
+       end
+       if not self:may(node, actor, cap) then
+               return false, "forbidden";
+       end
+       if not self:may(node, jid, "be_unsubscribed") then
+               return false, "forbidden";
+       end
+       --
+       local node_obj = self.nodes[node];
+       if not node_obj then
+               return false, "item-not-found";
+       end
+       if not node_obj.subscribers[jid] then
+               return false, "not-subscribed";
+       end
+       node_obj.subscribers[jid] = nil;
+       local normal_jid = self.config.normalize_jid(jid);
+       local subs = self.subscriptions[normal_jid];
+       if subs then
+               local jid_subs = subs[jid];
+               if jid_subs then
+                       jid_subs[node] = nil;
+                       if next(jid_subs) == nil then
+                               subs[jid] = nil;
                        end
                end
+               if next(subs) == nil then
+                       self.subscriptions[normal_jid] = nil;
+               end
        end
+       return true;
 end
 
-function get_subscribers(node)
-       return node.subscribers;
+function service:get_subscription(node, actor, jid)
+       -- Access checking
+       local cap;
+       if actor == true or jid == actor or self:jids_equal(actor, jid) then
+               cap = "get_subscription";
+       else
+               cap = "get_subscription_other";
+       end
+       if not self:may(node, actor, cap) then
+               return false, "forbidden";
+       end
+       --
+       local node_obj = self.nodes[node];
+       if not node_obj then
+               return false, "item-not-found";
+       end
+       return true, node_obj.subscribers[jid];
+end
+
+function service:create(node, actor)
+       -- Access checking
+       if not self:may(node, actor, "create") then
+               return false, "forbidden";
+       end
+       --
+       if self.nodes[node] then
+               return false, "conflict";
+       end
+       
+       self.nodes[node] = {
+               name = node;
+               subscribers = {};
+               config = {};
+               data = {};
+               affiliations = {};
+       };
+       local ok, err = self:set_affiliation(node, true, actor, "owner");
+       if not ok then
+               self.nodes[node] = nil;
+       end
+       return ok, err;
+end
+
+function service:publish(node, actor, id, item)
+       -- Access checking
+       if not self:may(node, actor, "publish") then
+               return false, "forbidden";
+       end
+       --
+       local node_obj = self.nodes[node];
+       if not node_obj then
+               if not self.config.autocreate_on_publish then
+                       return false, "item-not-found";
+               end
+               local ok, err = self:create(node, actor);
+               if not ok then
+                       return ok, err;
+               end
+               node_obj = self.nodes[node];
+       end
+       node_obj.data[id] = item;
+       self.config.broadcaster(node, node_obj.subscribers, item);
+       return true;
+end
+
+function service:retract(node, actor, id, retract)
+       -- Access checking
+       if not self:may(node, actor, "retract") then
+               return false, "forbidden";
+       end
+       --
+       local node_obj = self.nodes[node];
+       if (not node_obj) or (not node_obj.data[id]) then
+               return false, "item-not-found";
+       end
+       node_obj.data[id] = nil;
+       if retract then
+               self.config.broadcaster(node, node_obj.subscribers, retract);
+       end
+       return true
+end
+
+function service:get_items(node, actor, id)
+       -- Access checking
+       if not self:may(node, actor, "get_items") then
+               return false, "forbidden";
+       end
+       --
+       local node_obj = self.nodes[node];
+       if not node_obj then
+               return false, "item-not-found";
+       end
+       if id then -- Restrict results to a single specific item
+               return true, { [id] = node_obj.data[id] };
+       else
+               return true, node_obj.data;
+       end
 end
 
-function publish(node, item, dispatcher, data)
-       local subscribers = node.subscribers;
-       for i = 1,#subscribers do
-               item.attr.to = subscribers[i];
-               dispatcher(data, item);
+function service:get_nodes(actor)
+       -- Access checking
+       if not self:may(nil, actor, "get_nodes") then
+               return false, "forbidden";
        end
+       --
+       return true, self.nodes;
 end
 
-function add_subscriber(node, jid)
-       local subscribers = node.subscribers;
-       if not subscribers[jid] then
-               local space = #subscribers;
-               subscribers[space] = jid;
-               subscribers[jid] = space;
+function service:get_subscriptions(node, actor, jid)
+       -- Access checking
+       local cap;
+       if actor == true or jid == actor or self:jids_equal(actor, jid) then
+               cap = "get_subscriptions";
+       else
+               cap = "get_subscriptions_other";
+       end
+       if not self:may(node, actor, cap) then
+               return false, "forbidden";
+       end
+       --
+       local node_obj;
+       if node then
+               node_obj = self.nodes[node];
+               if not node_obj then
+                       return false, "item-not-found";
+               end
        end
+       local normal_jid = self.config.normalize_jid(jid);
+       local subs = self.subscriptions[normal_jid];
+       -- We return the subscription object from the node to save
+       -- a get_subscription() call for each node.
+       local ret = {};
+       if subs then
+               for jid, subscribed_nodes in pairs(subs) do
+                       if node then -- Return only subscriptions to this node
+                               if subscribed_nodes[node] then
+                                       ret[#ret+1] = {
+                                               node = subscribed_node;
+                                               jid = jid;
+                                               subscription = node_obj.subscribers[jid];
+                                       };
+                               end
+                       else -- Return subscriptions to all nodes
+                               local nodes = self.nodes;
+                               for subscribed_node in pairs(subscribed_nodes) do
+                                       ret[#ret+1] = {
+                                               node = subscribed_node;
+                                               jid = jid;
+                                               subscription = nodes[subscribed_node].subscribers[jid];
+                                       };
+                               end
+                       end
+               end
+       end
+       return true, ret;
 end
 
-function remove_subscriber(node, jid)
-       local subscribers = node.subscribers;
-       if subscribers[jid] then
-               subscribers[subscribers[jid]] = nil;
-               subscribers[jid] = nil;
+-- Access models only affect 'none' affiliation caps, service/default access level...
+function service:set_node_capabilities(node, actor, capabilities)
+       -- Access checking
+       if not self:may(node, actor, "configure") then
+               return false, "forbidden";
+       end
+       --
+       local node_obj = self.nodes[node];
+       if not node_obj then
+               return false, "item-not-found";
        end
+       node_obj.capabilities = capabilities;
+       return true;
 end
 
 return _M;