# HG changeset patch # User Matthew Wild # Date 1526469261 -3600 # Node ID c2b99fa134b3d8d2007833ad561d1bcc8bc2784d # Parent 8da11142fabfebcb65c4e9387bda15e1daf6f714 MUC: Import revised, more comprehensive patch for 8da11142fabf (#345) diff -r 8da11142fabf -r c2b99fa134b3 plugins/muc/muc.lib.lua --- a/plugins/muc/muc.lib.lua Sat Mar 18 18:47:28 2017 +0100 +++ b/plugins/muc/muc.lib.lua Wed May 16 12:14:21 2018 +0100 @@ -804,15 +804,19 @@ local role = current_nick and self._occupants[current_nick].role or self:get_default_role(affiliation); if type == "set" then local at_least_one_item_provided = false; + local callback = function() origin.send(st.reply(stanza)); end + -- Gather all changes to affiliations and roles + local jid_affiliation = {}; + local jidnick_role = {}; for item in stanza.tags[1]:childtags("item") do at_least_one_item_provided = true; - local callback = function() origin.send(st.reply(stanza)); end if item.attr.jid then -- Validate provided JID item.attr.jid = jid_prep(item.attr.jid); if not item.attr.jid then origin.send(st.error_reply(stanza, "modify", "jid-malformed")); + return; end end if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation @@ -822,13 +826,12 @@ local nick = self._jid_nick[item.attr.jid]; if nick then item.attr.nick = select(3, jid_split(nick)); end end + local reason = item.tags[1] and item.tags[1].name == "reason" and #item.tags[1] == 1 and item.tags[1][1]; if item.attr.affiliation and item.attr.jid and not item.attr.role then - local success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, callback, reason); - if not success then origin.send(st.error_reply(stanza, errtype, err)); end + jid_affiliation[item.attr.jid] = { ["affiliation"] = item.attr.affiliation, ["reason"] = reason }; elseif item.attr.role and item.attr.nick and not item.attr.affiliation then - local success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, callback, reason); - if not success then origin.send(st.error_reply(stanza, errtype, err)); end + jidnick_role[item.attr.jid.."/"..item.attr.nick] = { ["role"] = item.attr.role, ["reason"] = reason }; else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; @@ -838,6 +841,36 @@ if not at_least_one_item_provided then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; + else + local can_set_affiliations, errtype_aff, err_aff = self:can_set_affiliations(actor, jid_affiliation) + local can_set_roles, errtype_role, err_role = self:can_set_roles(actor, jidnick_role) + + if can_set_affiliations and can_set_roles then + local nb_affiliation_changes = 0; + for _ in pairs(jid_affiliation) do nb_affiliation_changes = nb_affiliation_changes + 1; end + local nb_role_changes = 0; + for _ in pairs(jidnick_role) do nb_role_changes = nb_role_changes + 1; end + + if nb_affiliation_changes > 0 and nb_role_changes > 0 then + origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + if nb_affiliation_changes > 0 then + self:set_affiliations(actor, jid_affiliation, callback); + end + if nb_role_changes > 0 then + self:set_roles(actor, jidnick_role, callback); + end + else + if not can_set_affiliations then + origin.send(st.error_reply(stanza, errtype_aff, err_aff)); + elseif not can_set_roles then + origin.send(st.error_reply(stanza, errtype_role, err_role)); + else + origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + + return; + end end elseif type == "get" then local item = stanza.tags[1].tags[1]; @@ -1009,82 +1042,125 @@ if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned return result; end -function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) - jid = jid_bare(jid); - if affiliation == "none" then affiliation = nil; end - if affiliation and affiliation ~= "outcast" and affiliation ~= "owner" and affiliation ~= "admin" and affiliation ~= "member" then - return nil, "modify", "not-acceptable"; +--- Checks whether the given affiliation changes in jid_affiliation can be applied by actor. +-- Note: Empty tables can always be applied and won't have any effect. +function room_mt:can_set_affiliations(actor, jid_affiliation) + local actor_affiliation; + if actor ~= true then + actor_affiliation = self:get_affiliation(actor); end - if actor ~= true then - local actor_affiliation = self:get_affiliation(actor); + + -- First let's see if there are any problems with the affiliations given + -- in jid_affiliation + for jid, value in pairs(jid_affiliation) do + local affiliation = value["affiliation"]; + + jid = jid_bare(jid); + if affiliation == "none" then affiliation = nil; end + if affiliation and affiliation ~= "outcast" and affiliation ~= "owner" and affiliation ~= "admin" and affiliation ~= "member" then + return false, "modify", "not-acceptable"; + end + local target_affiliation = self:get_affiliation(jid); - if target_affiliation == affiliation then -- no change, shortcut - if callback then callback(); end - return true; - end - if actor_affiliation ~= "owner" then - if affiliation == "owner" or affiliation == "admin" or actor_affiliation ~= "admin" or target_affiliation == "owner" or target_affiliation == "admin" then - return nil, "cancel", "not-allowed"; - end - elseif target_affiliation == "owner" and jid_bare(actor) == jid then -- self change - local is_last = true; - for j, aff in pairs(self._affiliations) do if j ~= jid and aff == "owner" then is_last = false; break; end end - if is_last then - return nil, "cancel", "conflict"; + if target_affiliation == affiliation then + -- no change, no error checking necessary + else + if actor ~= true and actor_affiliation ~= "owner" then + if affiliation == "owner" or affiliation == "admin" or actor_affiliation ~= "admin" or target_affiliation == "owner" or target_affiliation == "admin" then + return false, "cancel", "not-allowed"; + end + elseif target_affiliation == "owner" and jid_bare(actor) == jid then -- self change + local is_last = true; + for j, aff in pairs(self._affiliations) do if j ~= jid and aff == "owner" then is_last = false; break; end end + if is_last then + return false, "cancel", "conflict"; + end end end end - self._affiliations[jid] = affiliation; - local role = self:get_default_role(affiliation); - local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) - :tag("item", {affiliation=affiliation or "none", role=role or "none"}) - :tag("reason"):text(reason or ""):up() - :up(); - local presence_type = nil; - if not role then -- getting kicked - presence_type = "unavailable"; - if affiliation == "outcast" then - x:tag("status", {code="301"}):up(); -- banned - else - x:tag("status", {code="321"}):up(); -- affiliation change - end + + return true; +end +--- Updates the room affiliations by applying the ones given here. +-- Takes the affiliations given in jid_affiliation and applies them to +-- the room, overwriting a potentially existing affiliation for any given +-- jid. +-- @param jid_affiliation A table associating a jid with a table consisting +-- of two subkeys: `affilation` and `reason`. The jids +-- within must not be malformed. +function room_mt:set_affiliations(actor, jid_affiliation, callback) + local can_set, err_type, err_condition = self:can_set_affiliations(actor, jid_affiliation) + + if not can_set then + return false, err_type, err_condition; end -- Your own presence should have status 110 - local self_x = st.clone(x); - self_x:tag("status", {code="110"}); local modified_nicks = {}; - for nick, occupant in pairs(self._occupants) do - if jid_bare(occupant.jid) == jid then - if not role then -- getting kicked - self._occupants[nick] = nil; + local nb_modified_nicks = 0; + -- Now we can be sure that jid_affiliation causes no problems + -- We can actually set them + for jid, value in pairs(jid_affiliation) do + local affiliation = value["affiliation"]; + local reason = value["reason"]; + + self._affiliations[jid] = affiliation; + local role = self:get_default_role(affiliation); + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) + :tag("item", {affiliation=affiliation or "none", role=role or "none"}) + :tag("reason"):text(reason or ""):up() + :up(); + local self_x = st.clone(x); + self_x:tag("status", {code="110"}); + local presence_type = nil; + if not role then -- getting kicked + presence_type = "unavailable"; + if affiliation == "outcast" then + x:tag("status", {code="301"}):up(); -- banned else - occupant.affiliation, occupant.role = affiliation, role; + x:tag("status", {code="321"}):up(); -- affiliation change end - for jid,pres in pairs(occupant.sessions) do -- remove for all sessions of the nick - if not role then self._jid_nick[jid] = nil; end - local p = st.clone(pres); - p.attr.from = nick; - p.attr.type = presence_type; - p.attr.to = jid; - if occupant.jid == jid then + end + for nick, occupant in pairs(self._occupants) do + if jid_bare(occupant.jid) == jid then + if not role then -- getting kicked + self._occupants[nick] = nil; + else + occupant.affiliation, occupant.role = affiliation, role; + end + for jid,pres in pairs(occupant.sessions) do -- remove for all sessions of the nick + if not role then self._jid_nick[jid] = nil; end + local p = st.clone(pres); + p.attr.from = nick; + p.attr.type = presence_type; + p.attr.to = jid; + self:_route_stanza(p); + if occupant.jid == jid then -- Broadcast this presence to everyone else later, with the public variant - local bp = st.clone(p); - bp:add_child(x); - modified_nicks[nick] = bp; + local bp = st.clone(p); + bp:add_child(x); + modified_nicks[nick] = bp; + nb_modified_nicks = nb_modified_nicks + 1; + end + p:add_child(self_x); + self:_route_stanza(p); end - p:add_child(self_x); - self:_route_stanza(p); end end end - if self.save then self:save(); end - if callback then callback(); end + + if nb_modified_nicks > 0 then + if self.save then self:save(); end + if callback then callback(); end + end for nick,p in pairs(modified_nicks) do p.attr.from = nick; self:broadcast_except_nick(p, nick); end return true; end +function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) + return self.set_affiliations(actor, { [jid] = { ["affiliation"] = affiliation, ["reason"] = reason } }, callback) +end function room_mt:get_role(nick) local session = self._occupants[nick]; @@ -1108,45 +1184,81 @@ end return nil, "cancel", "not-allowed"; end -function room_mt:set_role(actor, occupant_jid, role, callback, reason) - if role == "none" then role = nil; end - if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end - local allowed, err_type, err_condition = self:can_set_role(actor, occupant_jid, role); - if not allowed then return allowed, err_type, err_condition; end - local occupant = self._occupants[occupant_jid]; - local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) - :tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"}) - :tag("reason"):text(reason or ""):up() - :up(); - local presence_type = nil; - if not role then -- kick - presence_type = "unavailable"; - self._occupants[occupant_jid] = nil; - for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick - self._jid_nick[jid] = nil; + +--- Checks whether the given role changes in jidnick_role can be applied by actor. +-- Note: Empty tables can always be applied and won't have any effect. +function room_mt:can_set_roles(actor, jidnick_role) + for jidnick, role in pairs(jidnick_role) do + if role == "none" then role = nil; end + if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return false, "modify", "not-acceptable"; end + local can_set, err_type, err_condition = self:can_set_role(actor, jidnick, role) + if not can_set then + return false, err_type, err_condition; end - x:tag("status", {code = "307"}):up(); - else - occupant.role = role; end - local self_x = st.clone(x); - self_x:tag("status", {code = "110"}):up(); - local bp; - for jid,pres in pairs(occupant.sessions) do -- send to all sessions of the nick - local p = st.clone(pres); - p.attr.from = occupant_jid; - p.attr.type = presence_type; - p.attr.to = jid; - if occupant.jid == jid then - bp = st.clone(p); - bp:add_child(x); + + return true; +end + +--- Updates the room roles by applying the ones given here. +-- Takes the roles given in jidnick_role and applies them to +-- the room, overwriting a potentially existing role for any given +-- jid. +-- @param jidnick_role A table associating a jid/nick with a table consisting +-- of two subkeys: `role` and `reason`. The jids within +-- must not be malformed. +function room_mt:set_roles(actor, jidnick_role, callback) + local allowed, err_type, err_condition = self:can_set_roles(actor, jidnick_role); + if not allowed then return allowed, err_type, err_condition; end + + local modified_nicks = {}; + local nb_modified_nicks = 0; + for jidnick, value in pairs(jidnick_role) do + local occupant_jid = jidnick; + local role = value["role"]; + local reason = value["reason"]; + + local occupant = self._occupants[occupant_jid]; + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) + :tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"}) + :tag("reason"):text(reason or ""):up() + :up(); + local presence_type = nil; + if not role then -- kick + presence_type = "unavailable"; + self._occupants[occupant_jid] = nil; + for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick + self._jid_nick[jid] = nil; + end + x:tag("status", {code = "307"}):up(); + else + occupant.role = role; end - p:add_child(self_x); - self:_route_stanza(p); + local self_x = st.clone(x); + self_x:tag("status", {code = "110"}):up(); + local bp; + for jid,pres in pairs(occupant.sessions) do -- send to all sessions of the nick + local p = st.clone(pres); + p.attr.from = occupant_jid; + p.attr.type = presence_type; + p.attr.to = jid; + self:_route_stanza(p); + if occupant.jid == jid then + bp = st.clone(p); + bp:add_child(x); + modified_nicks[occupant_jid] = p; + nb_modified_nicks = nb_modified_nicks + 1; + end + p:add_child(self_x); + self:route_stanza(p); + end end - if callback then callback(); end - if bp then - self:broadcast_except_nick(bp, occupant_jid); + + if nb_modified_nicks > 0 then + if callback then callback(); end + end + for nick,p in pairs(modified_nicks) do + self:broadcast_except_nick(p, nick); end return true; end