mod_privacy: Fix selecting the top resource (fixes #694)
[prosody.git] / plugins / mod_privacy.lua
index ab1eb87062bd2b573110d32fb7ebbc9d9abb8014..f95dfa50a1043588f1a4201177378d96b448aa41 100644 (file)
@@ -1,31 +1,23 @@
 -- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- Copyright (C) 2009-2010 Matthew Wild
+-- Copyright (C) 2009-2010 Waqas Hussain
 -- Copyright (C) 2009 Thilo Cestonaro
 -- 
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
-local prosody = prosody;
+module:add_feature("jabber:iq:privacy");
+
 local st = require "util.stanza";
-local datamanager = require "util.datamanager";
-local bare_sessions, full_sessions = bare_sessions, full_sessions;
+local bare_sessions, full_sessions = prosody.bare_sessions, prosody.full_sessions;
 local util_Jid = require "util.jid";
 local jid_bare = util_Jid.bare;
-local jid_split = util_Jid.split;
+local jid_split, jid_join = util_Jid.split, util_Jid.join;
 local load_roster = require "core.rostermanager".load_roster;
 local to_number = tonumber;
 
-function findNamedList(privacy_lists, name)
-       if privacy_lists.lists then
-               for i=1,#privacy_lists.lists do
-                       if privacy_lists.lists[i].name == name then
-                               return i;
-                       end
-               end
-       end
-end
+local privacy_storage = module:open_store();
 
 function isListUsed(origin, name, privacy_lists)
        local user = bare_sessions[origin.username.."@"..origin.host];
@@ -53,62 +45,6 @@ function isAnotherSessionUsingDefaultList(origin)
        end
 end
 
-function sendUnavailable(origin, to, from)
---[[ example unavailable presence stanza
-<presence from="node@host/resource" type="unavailable" to="node@host" >
-       <status>Logged out</status>
-</presence>
-]]--
-       local presence = st.presence({from=from, type="unavailable"});
-       presence:tag("status"):text("Logged out");
-
-       local node, host = jid_bare(to);
-       local bare = node .. "@" .. host;
-       
-       local user = bare_sessions[bare];
-       if user then
-               for resource, session in pairs(user.sessions) do
-                       presence.attr.to = session.full_jid;
-                       module:log("debug", "send unavailable to: %s; from: %s", tostring(presence.attr.to), tostring(presence.attr.from));
-                       origin.send(presence);
-               end
-       end
-end
-
-function sendNeededUnavailablePersences(origin, listnameOrItem) -- TODO implement it correctly!
-       if type(listnameOrItem) == "string" then
-               local listname = listnameOrItem;
-               for _,list in ipairs(privacy_lists.lists) do
-                       if list.name == listname then
-                               for _,item in ipairs(list.items) do
-                                       sendNeededUnavailablePersences(origin, item);
-                               end
-                       end
-               end
-       elseif type(listnameOrItem) == "table" then
-               module:log("debug", "got an item, check whether to send unavailable presence stanza or not");
-               local item = listnameOrItem;
-
-               if item["presence-out"] == true then
-                       if item.type == "jid" then
-                               sendUnavailable(origin, item.value, origin.full_jid);
-                       elseif item.type == "group" then
-                       elseif item.type == "subscription" then
-                       elseif item.type == nil then
-                       end
-               elseif item["presence-in"] == true then
-                       if item.type == "jid" then
-                               sendUnavailable(origin, origin.full_jid, item.value);
-                       elseif item.type == "group" then
-                       elseif item.type == "subscription" then
-                       elseif item.type == nil then
-                       end
-               end
-       else
-               module:log("debug", "got unknown type: %s", type(listnameOrItem));
-       end
-end
-
 function declineList(privacy_lists, origin, stanza, which)
        if which == "default" then
                if isAnotherSessionUsingDefaultList(origin) then
@@ -126,70 +62,56 @@ function declineList(privacy_lists, origin, stanza, which)
 end
 
 function activateList(privacy_lists, origin, stanza, which, name)
-       local idx = findNamedList(privacy_lists, name);
+       local list = privacy_lists.lists[name];
 
-       if privacy_lists.default == nil then
-               privacy_lists.default = "";
-       end
-       if origin.activePrivacyList == nil then
-               origin.activePrivacyList = "";
-       end
-       
-       if which == "default" and idx ~= nil then
+       if which == "default" and list then
                if isAnotherSessionUsingDefaultList(origin) then
                        return {"cancel", "conflict", "Another session is online and using the default list."};
                end
                privacy_lists.default = name;
                origin.send(st.reply(stanza));
---[[
-               if origin.activePrivacyList == nil then
-                       sendNeededUnavailablePersences(origin, name);
-               end
-]]--
-       elseif which == "active" and idx ~= nil then
+       elseif which == "active" and list then
                origin.activePrivacyList = name;
                origin.send(st.reply(stanza));
-               -- sendNeededUnavailablePersences(origin, name);
+       elseif not list then
+               return {"cancel", "item-not-found", "No such list: "..name};
        else
-               return {"modify", "bad-request", "Either not active or default given or unknown list name specified."};
+               return {"modify", "bad-request", "No list chosen to be active or default."};
        end
        return true;
 end
 
 function deleteList(privacy_lists, origin, stanza, name)
-       local idx = findNamedList(privacy_lists, name);
+       local list = privacy_lists.lists[name];
 
-       if idx ~= nil then
+       if list then
                if isListUsed(origin, name, privacy_lists) then
                        return {"cancel", "conflict", "Another session is online and using the list which should be deleted."};
                end
                if privacy_lists.default == name then
-                       privacy_lists.default = "";
+                       privacy_lists.default = nil;
                end
                if origin.activePrivacyList == name then
-                       origin.activePrivacyList = "";
+                       origin.activePrivacyList = nil;
                end
-               table.remove(privacy_lists.lists, idx);
+               privacy_lists.lists[name] = nil;
                origin.send(st.reply(stanza));
                return true;
        end
        return {"modify", "bad-request", "Not existing list specifed to be deleted."};
 end
 
-function createOrReplaceList (privacy_lists, origin, stanza, name, entries, roster)
-       local idx = findNamedList(privacy_lists, name);
+function createOrReplaceList (privacy_lists, origin, stanza, name, entries)
        local bare_jid = origin.username.."@"..origin.host;
        
        if privacy_lists.lists == nil then
                privacy_lists.lists = {};
        end
 
-       if idx == nil then
-               idx = #privacy_lists.lists + 1;
-       end
+       local list = {};
+       privacy_lists.lists[name] = list;
 
        local orderCheck = {};
-       local list = {};
        list.name = name;
        list.items = {};
 
@@ -220,26 +142,7 @@ function createOrReplaceList (privacy_lists, origin, stanza, name, entries, rost
                        end
                end
                
-               if tmp.type == "group" then
-                       local found = false;
-                       local roster = load_roster(origin.username, origin.host);
-                       for jid,item in pairs(roster) do
-                               if item.groups ~= nil then
-                                       for group in pairs(item.groups) do
-                                               if group == tmp.value then
-                                                       found = true;
-                                                       break;
-                                               end
-                                       end
-                                       if found == true then
-                                               break;
-                                       end
-                               end
-                       end
-                       if found == false then
-                               return {"cancel", "item-not-found", "Specifed roster group not existing."};
-                       end
-               elseif tmp.type == "subscription" then
+               if tmp.type == "subscription" then
                        if      tmp.value ~= "both" and
                                tmp.value ~= "to" and
                                tmp.value ~= "from" and
@@ -251,20 +154,11 @@ function createOrReplaceList (privacy_lists, origin, stanza, name, entries, rost
                if tmp.action ~= "deny" and tmp.action ~= "allow" then
                        return {"cancel", "bad-request", "Action must be either deny or allow."};
                end
-               
---[[
-               if (privacy_lists.default == name and origin.activePrivacyList == nil) or origin.activePrivacyList == name then
-                       module:log("debug", "calling sendNeededUnavailablePresences!");
-                       -- item is valid and list is active, so send needed unavailable stanzas
-                       sendNeededUnavailablePersences(origin, tmp);
-               end
-]]--
                list.items[#list.items + 1] = tmp;
        end
        
-       table.sort(list, function(a, b) return a.order < b.order; end);
+       table.sort(list.items, function(a, b) return a.order < b.order; end);
 
-       privacy_lists.lists[idx] = list;
        origin.send(st.reply(stanza));
        if bare_sessions[bare_jid] ~= nil then
                local iq = st.iq ( { type = "set", id="push1" } );
@@ -286,17 +180,20 @@ function getList(privacy_lists, origin, stanza, name)
        reply:tag("query", {xmlns="jabber:iq:privacy"});
 
        if name == nil then
-               reply:tag("active", {name=origin.activePrivacyList or ""}):up();
-               reply:tag("default", {name=privacy_lists.default or ""}):up();
                if privacy_lists.lists then
-                       for _,list in ipairs(privacy_lists.lists) do
-                               reply:tag("list", {name=list.name}):up();
+                       if origin.activePrivacyList then
+                               reply:tag("active", {name=origin.activePrivacyList}):up();
+                       end
+                       if privacy_lists.default then
+                               reply:tag("default", {name=privacy_lists.default}):up();
+                       end
+                       for name,list in pairs(privacy_lists.lists) do
+                               reply:tag("list", {name=name}):up();
                        end
                end
        else
-               local idx = findNamedList(privacy_lists, name);
-               if idx ~= nil then
-                       local list = privacy_lists.lists[idx];
+               local list = privacy_lists.lists[name];
+               if list then
                        reply = reply:tag("list", {name=list.name});
                        for _,item in ipairs(list.items) do
                                reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order});
@@ -321,7 +218,16 @@ module:hook("iq/bare/jabber:iq:privacy:query", function(data)
        if stanza.attr.to == nil then -- only service requests to own bare JID
                local query = stanza.tags[1]; -- the query element
                local valid = false;
-               local privacy_lists = datamanager.load(origin.username, origin.host, "privacy") or {};
+               local privacy_lists = privacy_storage:get(origin.username) or { lists = {} };
+
+               if privacy_lists.lists[1] then -- Code to migrate from old privacy lists format, remove in 0.8
+                       module:log("info", "Upgrading format of stored privacy lists for %s@%s", origin.username, origin.host);
+                       local lists = privacy_lists.lists;
+                       for idx, list in ipairs(lists) do
+                               lists[list.name] = list;
+                               lists[idx] = nil;
+                       end
+               end
 
                if stanza.attr.type == "set" then
                        if #query.tags == 1 then --  the <query/> element MUST NOT include more than one child element
@@ -358,15 +264,16 @@ module:hook("iq/bare/jabber:iq:privacy:query", function(data)
                end
 
                if valid ~= true then
-                       if valid[0] == nil then
-                               valid[0] = "cancel";
-                       end
+                       valid = valid or { "cancel", "bad-request", "Couldn't understand request" };
                        if valid[1] == nil then
-                               valid[1] = "bad-request";
+                               valid[1] = "cancel";
+                       end
+                       if valid[2] == nil then
+                               valid[2] = "bad-request";
                        end
-                       origin.send(st.error_reply(stanza, valid[0], valid[1], valid[2]));
+                       origin.send(st.error_reply(stanza, valid[1], valid[2], valid[3]));
                else
-                       datamanager.store(origin.username, origin.host, "privacy", privacy_lists);
+                       privacy_storage:set(origin.username, privacy_lists);
                end
                return true;
        end
@@ -374,42 +281,33 @@ end);
 
 function checkIfNeedToBeBlocked(e, session)
        local origin, stanza = e.origin, e.stanza;
-       local privacy_lists = datamanager.load(session.username, session.host, "privacy") or {};
+       local privacy_lists = privacy_storage:get(session.username) or {};
        local bare_jid = session.username.."@"..session.host;
-       local to = stanza.attr.to;
+       local to = stanza.attr.to or bare_jid;
        local from = stanza.attr.from;
        
-       local to_user = bare_jid == jid_bare(to);
-       local from_user = bare_jid == jid_bare(from);
+       local is_to_user = bare_jid == jid_bare(to);
+       local is_from_user = bare_jid == jid_bare(from);
        
-       module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
+       --module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
        
        if privacy_lists.lists == nil or
-               (session.activePrivacyList == nil or session.activePrivacyList == "") and
-               (privacy_lists.default == nil     or privacy_lists.default == "")
+               not (session.activePrivacyList or privacy_lists.default)
        then
                return; -- Nothing to block, default is Allow all
        end
-       if from_user and to_user then
-               module:log("debug", "Not blocking communications between user's resources");
+       if is_from_user and is_to_user then
+               --module:log("debug", "Not blocking communications between user's resources");
                return; -- from one of a user's resource to another => HANDS OFF!
        end
        
-       local idx;
-       local list;
-       local item;
        local listname = session.activePrivacyList;
-       if listname == nil or listname == "" then
+       if listname == nil then
                listname = privacy_lists.default; -- no active list selected, use default list
        end
-       idx = findNamedList(privacy_lists, listname);
-       if idx == nil then
-               module:log("debug", "given privacy listname not found. name: %s", listname);
-               return;
-       end
-       list = privacy_lists.lists[idx];
-       if list == nil then
-               module:log("debug", "privacy list index wrong. index: %d", idx);
+       local list = privacy_lists.lists[listname];
+       if not list then -- should never happen
+               module:log("warn", "given privacy list not found. name: %s for user %s", listname, bare_jid);
                return;
        end
        for _,item in ipairs(list.items) do
@@ -418,8 +316,8 @@ function checkIfNeedToBeBlocked(e, session)
                if (
                        (stanza.name == "message" and item.message) or
                        (stanza.name == "iq" and item.iq) or
-                       (stanza.name == "presence" and to_user and item["presence-in"]) or
-                       (stanza.name == "presence" and from_user and item["presence-out"]) or
+                       (stanza.name == "presence" and is_to_user and item["presence-in"]) or
+                       (stanza.name == "presence" and is_from_user and item["presence-out"]) or
                        (item.message == false and item.iq == false and item["presence-in"] == false and item["presence-out"] == false)
                ) then
                        apply = true;
@@ -427,11 +325,11 @@ function checkIfNeedToBeBlocked(e, session)
                if apply then
                        local evilJid = {};
                        apply = false;
-                       if to_user then
-                               module:log("debug", "evil jid is (from): %s", from);
+                       if is_to_user then
+                               --module:log("debug", "evil jid is (from): %s", from);
                                evilJid.node, evilJid.host, evilJid.resource = jid_split(from);
                        else
-                               module:log("debug", "evil jid is (to): %s", to);
+                               --module:log("debug", "evil jid is (to): %s", to);
                                evilJid.node, evilJid.host, evilJid.resource = jid_split(to);
                        end
                        if      item.type == "jid" and
@@ -443,17 +341,22 @@ function checkIfNeedToBeBlocked(e, session)
                                block = (item.action == "deny");
                        elseif item.type == "group" then
                                local roster = load_roster(session.username, session.host);
-                               local groups = roster[evilJid.node .. "@" .. evilJid.host].groups;
-                               for group in pairs(groups) do
-                                       if group == item.value then
-                                               apply = true;
-                                               block = (item.action == "deny");
-                                               break;
+                               local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
+                               if roster_entry then
+                                       local groups = roster_entry.groups;
+                                       for group in pairs(groups) do
+                                               if group == item.value then
+                                                       apply = true;
+                                                       block = (item.action == "deny");
+                                                       break;
+                                               end
                                        end
                                end
-                       elseif item.type == "subscription" and evilJid.node ~= nil and evilJid.host ~= nil then -- we need a valid bare evil jid
+                       elseif item.type == "subscription" then -- we need a valid bare evil jid
                                local roster = load_roster(session.username, session.host);
-                               if roster[evilJid.node .. "@" .. evilJid.host].subscription == item.value then
+                               local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
+                               if (not(roster_entry) and item.value == "none")
+                                  or (roster_entry and roster_entry.subscription == item.value) then
                                        apply = true;
                                        block = (item.action == "deny");
                                end
@@ -464,6 +367,10 @@ function checkIfNeedToBeBlocked(e, session)
                end
                if apply then
                        if block then
+                               -- drop and not bounce groupchat messages, otherwise users will get kicked
+                               if stanza.attr.type == "groupchat" then
+                                       return true;
+                               end
                                module:log("debug", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
                                if stanza.name == "message" then
                                        origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
@@ -472,7 +379,7 @@ function checkIfNeedToBeBlocked(e, session)
                                end
                                return true; -- stanza blocked !
                        else
-                               module:log("debug", "stanza explicitly allowed!")
+                               --module:log("debug", "stanza explicitly allowed!")
                                return;
                        end
                end
@@ -488,10 +395,9 @@ function preCheckIncoming(e)
                end
                if resource == nil then
                        local prio = 0;
-                       local session_;
                        if bare_sessions[node.."@"..host] ~= nil then
                                for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do
-                                       if session_.priority ~= nil and session_.priority > prio then
+                                       if session_.priority ~= nil and session_.priority >= prio then
                                                session = session_;
                                                prio = session_.priority;
                                        end
@@ -503,7 +409,7 @@ function preCheckIncoming(e)
                if session ~= nil then
                        return checkIfNeedToBeBlocked(e, session);
                else
-                       module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource));
+                       --module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource));
                end
        end
 end
@@ -516,7 +422,9 @@ function preCheckOutgoing(e)
                        e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource;
                end
        end
-       return checkIfNeedToBeBlocked(e, session);
+       if session.username then -- FIXME do properly
+               return checkIfNeedToBeBlocked(e, session);
+       end
 end
 
 module:hook("pre-message/full", preCheckOutgoing, 500);