Commit 5bb42888 authored by daryl herzmann's avatar daryl herzmann

Merge pull request #100 from tevans/OF-179-PR

OF-179: Add group features for MUC administration
parents d551da38 822abb01
...@@ -11,7 +11,7 @@ commons-lang.jar | 2.3 ...@@ -11,7 +11,7 @@ commons-lang.jar | 2.3
commons-logging.jar | Jetty 5.1.10 | Apache 2.0 commons-logging.jar | Jetty 5.1.10 | Apache 2.0
commons-el.jar | Jetty 6.0.1 (1.0) | Apache 2.0 commons-el.jar | Jetty 6.0.1 (1.0) | Apache 2.0
commons-httpclient.jar | 3.1 | Apache 2.0 commons-httpclient.jar | 3.1 | Apache 2.0
commons-codec.jar | 1.3 | Apache 2.0 commons-codec.jar | 1.9 | Apache 2.0
dom4j.jar | 1.6.1 | BSD (http://www.dom4j.org/dom4j-1.6.1/license.html) dom4j.jar | 1.6.1 | BSD (http://www.dom4j.org/dom4j-1.6.1/license.html)
concurrentlinkedhashmap-lru | concurrentlinkedhashmap-lru-1.0_jdk5 | Apache 2.0 concurrentlinkedhashmap-lru | concurrentlinkedhashmap-lru-1.0_jdk5 | Apache 2.0
dbutil.jar | Jive Code, no release version. | GPL dbutil.jar | Jive Code, no release version. | GPL
......
...@@ -686,8 +686,8 @@ tab.tab-groupchat.descr=Click to manage group chat settings ...@@ -686,8 +686,8 @@ tab.tab-groupchat.descr=Click to manage group chat settings
sidebar.muc-room-edit-form.descr=Click to edit the room's configuration sidebar.muc-room-edit-form.descr=Click to edit the room's configuration
sidebar.muc-room-occupants=Room Occupants sidebar.muc-room-occupants=Room Occupants
sidebar.muc-room-occupants.descr=Click to view the room's occupants sidebar.muc-room-occupants.descr=Click to view the room's occupants
sidebar.muc-room-affiliations=User Permissions sidebar.muc-room-affiliations=Access Controls
sidebar.muc-room-affiliations.descr=Click to edit user permissions sidebar.muc-room-affiliations.descr=Click to edit room access controls (affiliations)
sidebar.muc-room-delete=Delete Room sidebar.muc-room-delete=Delete Room
sidebar.muc-room-delete.descr=Click to delete the room sidebar.muc-room-delete.descr=Click to delete the room
sidebar.muc-room-create=Create New Room sidebar.muc-room-create=Create New Room
...@@ -932,17 +932,20 @@ groupchat.history.settings.save=Save Settings ...@@ -932,17 +932,20 @@ groupchat.history.settings.save=Save Settings
groupchat.admins.title=Group Chat Administrators groupchat.admins.title=Group Chat Administrators
groupchat.admins.introduction=Below is the list of system administrators of the group chat service. System \ groupchat.admins.introduction=Below is the list of system administrators of the group chat service. System \
administrators can enter any group chat room and their permissions are the same as the room owner. administrators can enter any group chat room and their permissions are the same as the room owner.
groupchat.admins.user_added=User added to the list successfully. groupchat.admins.user_added=User/Group added to the list successfully.
groupchat.admins.error_adding=Error adding the user. Please verify the JID is correct. groupchat.admins.error_adding=Error adding the user/group. Please verify the JID is correct.
groupchat.admins.user_removed=User removed from the list successfully. groupchat.admins.user_removed=User/Group removed from the list successfully.
groupchat.admins.legend=Administrators groupchat.admins.legend=Administrators
groupchat.admins.label_add_admin=Add Administrator (JID): groupchat.admins.label_add_admin=Add Administrator (JID):
groupchat.admins.column_user=User groupchat.admins.column_user=User/Group
groupchat.admins.column_remove=Remove groupchat.admins.column_remove=Remove
groupchat.admins.add=Add groupchat.admins.add=Add
groupchat.admins.no_admins=No administrators specified, use the form above to add one. groupchat.admins.no_admins=No administrators specified, use the form above to add one.
groupchat.admins.dialog.title=Click to delete... groupchat.admins.dialog.title=Click to delete...
groupchat.admins.dialog.text=Are you sure you want to remove this user from the list? groupchat.admins.dialog.text=Are you sure you want to remove this user/group from the list?
groupchat.admins.add_group=Select Group (one or more):
groupchat.admins.group=Group
groupchat.admins.user=User
# Audit policy Page # Audit policy Page
...@@ -1172,18 +1175,21 @@ logviewer.enabled=Enabled ...@@ -1172,18 +1175,21 @@ logviewer.enabled=Enabled
muc.create.permission.title=Room Creation Permissions muc.create.permission.title=Room Creation Permissions
muc.create.permission.info=Use the form below to configure the policy for who can create group chat rooms. muc.create.permission.info=Use the form below to configure the policy for who can create group chat rooms.
muc.create.permission.error=Error adding the user. Please verify the JID is correct. muc.create.permission.error=Error adding the user/group. Please verify the JID is correct.
muc.create.permission.update=Settings updated successfully. muc.create.permission.update=Settings updated successfully.
muc.create.permission.add_user=User added successfully. muc.create.permission.add_user=User/Group added successfully.
muc.create.permission.user_removed=User removed successfully. muc.create.permission.user_removed=User/Group removed successfully.
muc.create.permission.policy=Permission Policy muc.create.permission.policy=Permission Policy
muc.create.permission.anyone_created=Anyone can create a chat room. muc.create.permission.anyone_created=Anyone can create a chat room.
muc.create.permission.specific_created=Only specific users can create a chat room. muc.create.permission.specific_created=Only specific users/groups can create a chat room.
muc.create.permission.allowed_users=Allowed Users muc.create.permission.allowed_users=Allowed Users/Groups
muc.create.permission.add_jid=Add User (JID): muc.create.permission.add_jid=Add User (JID):
muc.create.permission.add_group=Select Group (one or more):
muc.create.permission.no_allowed_users=No allowed users, use the form above to add one. muc.create.permission.no_allowed_users=No allowed users, use the form above to add one.
muc.create.permission.click_title=Click to delete... muc.create.permission.click_title=Click to delete...
muc.create.permission.confirm_remove=Are you sure you want to remove this user from the list? muc.create.permission.confirm_remove=Are you sure you want to remove this user/group from the list?
muc.create.permission.user=User
muc.create.permission.group=Group
# Muc default room settings Page # Muc default room settings Page
...@@ -1206,29 +1212,33 @@ muc.default.settings.update=Settings updated successfully. ...@@ -1206,29 +1212,33 @@ muc.default.settings.update=Settings updated successfully.
# Muc room affiliations Page # Muc room affiliations Page
muc.room.affiliations.title=User Permissions muc.room.affiliations.title=Room Affiliations
muc.room.affiliations.info=Below is the list of room owners, administrators, members and outcasts of \ muc.room.affiliations.info=Below is the list of room owners, administrators, members and outcasts of \
the the room the the room
muc.room.affiliations.info_detail=Room owners can alter the room configuration, grant ownership and \ muc.room.affiliations.info_detail=Room owners can alter the room configuration, grant ownership and \
administrative privileges to users and destroy the room. Room administrators can ban, grant \ administrative privileges to users and destroy the room. Room administrators can ban, grant \
membership and moderator privileges to users. Room members are the only allowed users to join \ membership and moderator privileges to users. Room members are the only allowed users to join \
the room when it is configured as members-only. Whilst room outcasts are users who have been \ the room when it is configured as members-only. Outcasts are users who have been \
banned from the room. banned from the room.</p><p>Select one or more groups, or optionally provide a user JID using \
muc.room.affiliations.error_removing_user=Error removing the user. The room must have at least one owner. the form below. After selecting the appropriate affiliation (Owner/Admin/Member/Outcast), \
muc.room.affiliations.error_banning_user=Error banning the user. Owners or Administratos cannot be banned. click the &quot;Add&quot; button to apply the change.
muc.room.affiliations.error_adding_user=Error adding the user. Please verify the JID is correct. muc.room.affiliations.error_removing_user=Error removing the user/group. The room must have at least one owner.
muc.room.affiliations.user_added=User added successfully. muc.room.affiliations.error_banning_user=Error banning the user/group. Owners or Administratos cannot be banned.
muc.room.affiliations.user_removed=User removed successfully. muc.room.affiliations.error_adding_user=Error adding the user/group. Please select a group or verify the user JID is correct.
muc.room.affiliations.permission=User Permissions muc.room.affiliations.user_added=User/Group added successfully.
muc.room.affiliations.user_removed=User/Group removed successfully.
muc.room.affiliations.permission=Room Affiliations
muc.room.affiliations.add_jid=Add User (JID): muc.room.affiliations.add_jid=Add User (JID):
muc.room.affiliations.add_group=Select Group (one or more):
muc.room.affiliations.owner=Owner muc.room.affiliations.owner=Owner
muc.room.affiliations.admin=Admin muc.room.affiliations.admin=Admin
muc.room.affiliations.member=Member muc.room.affiliations.member=Member
muc.room.affiliations.outcast=Outcast muc.room.affiliations.outcast=Outcast
muc.room.affiliations.user=User muc.room.affiliations.user=User
muc.room.affiliations.group=Group
muc.room.affiliations.room_owner=Room Owners muc.room.affiliations.room_owner=Room Owners
muc.room.affiliations.no_users=No Users muc.room.affiliations.no_users=No Users or Groups
muc.room.affiliations.confirm_removed=Are you sure you want to remove this user from the list? muc.room.affiliations.confirm_removed=Are you sure you want to remove this user/group from the list?
muc.room.affiliations.room_admin=Room Admins muc.room.affiliations.room_admin=Room Admins
muc.room.affiliations.room_member=Room Members muc.room.affiliations.room_member=Room Members
muc.room.affiliations.room_outcast=Room Outcasts muc.room.affiliations.room_outcast=Room Outcasts
......
package org.jivesoftware.openfire.event;
import java.util.Map;
import org.jivesoftware.openfire.group.Group;
/**
* An abstract adapter class for receiving group events.
* The methods in this class are empty. This class exists as convenience for creating listener objects.
*/
public class GroupEventAdapter implements GroupEventListener {
@Override
public void groupCreated(Group group, Map params) {
}
@Override
public void groupDeleting(Group group, Map params) {
}
@Override
public void groupModified(Group group, Map params) {
}
@Override
public void memberAdded(Group group, Map params) {
}
@Override
public void memberRemoved(Group group, Map params) {
}
@Override
public void adminAdded(Group group, Map params) {
}
@Override
public void adminRemoved(Group group, Map params) {
}
}
package org.jivesoftware.openfire.group;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.xmpp.packet.JID;
/**
* This list specifies additional methods that understand groups among
* the items in the list.
*
* @author Tom Evans
*/
public class ConcurrentGroupList<T> extends CopyOnWriteArrayList<T> implements GroupAwareList<T> {
private static final long serialVersionUID = -8884698048047935327L;
// This set is used to optimize group operations within this list.
// We only populate this set when it's needed to dereference the
// groups in the base list, but once it exists we keep it in sync
// via the various add/remove operations.
private transient Set<Group> groupsInList;
public ConcurrentGroupList() {
super();
}
public ConcurrentGroupList(Collection<? extends T> c) {
super(c);
}
/**
* Returns true if the list contains the given JID. If the JID
* is not found in the list, search the list for groups and look
* for the JID in each of the corresponding groups.
*
* @param value The target, presumably a JID
* @return True if the target is in the list, or in any groups in the list
*/
@Override
public boolean includes(Object value) {
boolean found = false;
if (contains(value)) {
found = true;
} else if (value instanceof JID) {
JID target = (JID) value;
Iterator<Group> iterator = getGroups().iterator();
while (!found && iterator.hasNext()) {
found = iterator.next().isUser(target);
}
}
return found;
}
/**
* Returns the groups that are implied (resolvable) from the items in the list.
*
* @return A Set containing the groups in the list
*/
@Override
public synchronized Set<Group> getGroups() {
if (groupsInList == null) {
groupsInList = new HashSet<Group>();
// add all the groups into the group set
Iterator<T> iterator = iterator();
while (iterator.hasNext()) {
T listItem = iterator.next();
Group group = Group.resolveFrom(listItem);
if (group != null) {
groupsInList.add(group);
};
}
}
return groupsInList;
}
/**
* This method is called from several of the mutators to keep
* the group set in sync with the full list.
*
* @param item The item to be added or removed if it is in the group set
* @param addOrRemove True to add, false to remove
* @return true if the given item is a group
*/
private synchronized boolean syncGroups(Object item, boolean addOrRemove) {
boolean result = false;
// only sync if the group list has been instantiated
if (groupsInList != null) {
Group group = Group.resolveFrom(item);
if (group != null) {
result = true;
if (addOrRemove == ADD) {
groupsInList.add(group);
} else if (addOrRemove == REMOVE) {
groupsInList.remove(group);
}
}
}
return result;
}
// below are overrides for the various mutators
@Override
public T set(int index, T element) {
T result = super.set(index, element);
syncGroups(element, ADD);
return result;
}
@Override
public boolean add(T e) {
boolean result = super.add(e);
syncGroups(e, ADD);
return result;
}
@Override
public void add(int index, T element) {
super.add(index, element);
syncGroups(element, ADD);
}
@Override
public T remove(int index) {
T result = super.remove(index);
syncGroups(result, REMOVE);
return result;
}
@Override
public boolean remove(Object o) {
boolean removed = super.remove(o);
if (removed) {
syncGroups(o, REMOVE);
}
return removed;
}
@Override
public boolean addIfAbsent(T e) {
boolean added = super.addIfAbsent(e);
if (added) {
syncGroups(e, ADD);
}
return added;
}
@Override
public boolean removeAll(Collection<?> c) {
boolean changed = super.removeAll(c);
if (changed) {
// drop the transient set, will be rebuilt when/if needed
synchronized(this) {
groupsInList = null;
}
}
return changed;
}
@Override
public boolean retainAll(Collection<?> c) {
boolean changed = super.retainAll(c);
if (changed) {
// drop the transient set, will be rebuilt when/if needed
synchronized(this) {
groupsInList = null;
}
}
return changed;
}
@Override
public int addAllAbsent(Collection<? extends T> c) {
int added = super.addAllAbsent(c);
if (added > 0) {
// drop the transient set, will be rebuilt when/if needed
synchronized(this) {
groupsInList = null;
}
}
return added;
}
@Override
public void clear() {
super.clear();
synchronized(this) {
groupsInList = null;
}
}
@Override
public boolean addAll(Collection<? extends T> c) {
boolean changed = super.addAll(c);
if (changed) {
// drop the transient set, will be rebuilt when/if needed
synchronized(this) {
groupsInList = null;
}
}
return changed;
}
@Override
public boolean addAll(int index, Collection<? extends T> c) {
boolean changed = super.addAll(index, c);
if (changed) {
// drop the transient set, will be rebuilt when/if needed
synchronized(this) {
groupsInList = null;
}
}
return changed;
}
private static final boolean ADD = true;
private static final boolean REMOVE = false;
}
package org.jivesoftware.openfire.group;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.xmpp.packet.JID;
/**
* This extension class provides additional methods that understand groups among
* the entries in the map.
*
* @author Tom Evans
*/
public class ConcurrentGroupMap<K, V> extends ConcurrentHashMap<K, V> implements GroupAwareMap<K, V> {
private static final long serialVersionUID = -2068242013524715293L;
// These sets are used to optimize group operations within this map.
// We only populate these sets when they are needed to dereference the
// groups in the base map, but once they exist we keep them in sync
// via the various put/remove operations.
private transient Set<Group> groupsFromKeys;
private transient Set<Group> groupsFromValues;
/**
* Returns true if the key list contains the given JID. If the JID
* is not found in the key list, search the key list for groups and
* look for the JID in each of the corresponding groups.
*
* @param o The target, presumably a JID
* @return True if the target is in the key list, or in any groups in the key list
*/
public boolean includesKey(Object key) {
boolean found = false;
if (containsKey(key)) {
found = true;
} else if (key instanceof JID) {
// look for group JIDs in the list of keys and dereference as needed
JID target = (JID) key;
Iterator<Group> iterator = getGroupsFromKeys().iterator();
while (!found && iterator.hasNext()) {
found = iterator.next().isUser(target);
}
}
return found;
}
/**
* Returns true if the map has an entry value matching the given JID. If the JID
* is not found explicitly, search the values for groups and search
* for the JID in each of the corresponding groups.
*
* @param value The target, presumably a JID
* @return True if the target is in the value set, or in any groups in the value set
*/
public boolean includesValue(Object value) {
boolean found = false;
if (containsValue(value)) {
found = true;
} else if (value instanceof JID) {
// look for group JIDs in the list of keys and dereference as needed
JID target = (JID) value;
Iterator<Group> iterator = getGroupsFromValues().iterator();
while (!found && iterator.hasNext()) {
found = iterator.next().isUser(target);
}
}
return found;
}
/**
* Returns the groups that are implied (resolvable) from the keys in the map.
*
* @return A Set containing the groups among the keys
*/
@Override
public synchronized Set<Group> getGroupsFromKeys() {
if (groupsFromKeys == null) {
groupsFromKeys = new HashSet<Group>();
// add all the groups into the group set
Iterator<K> iterator = keySet().iterator();
while (iterator.hasNext()) {
K key = iterator.next();
Group group = Group.resolveFrom(key);
if (group != null) {
groupsFromKeys.add(group);
};
}
}
return groupsFromKeys;
}
/**
* Returns the groups that are implied (resolvable) from the values in the map.
*
* @return A Set containing the groups among the values
*/
@Override
public synchronized Set<Group> getGroupsFromValues() {
if (groupsFromValues == null) {
groupsFromValues = new HashSet<Group>();
// add all the groups into the group set
Iterator<V> iterator = values().iterator();
while (iterator.hasNext()) {
V value = iterator.next();
Group group = Group.resolveFrom(value);
if (group != null) {
groupsFromValues.add(group);
};
}
}
return groupsFromValues;
}
/**
* This method is called from several of the mutators to keep
* the group set in sync with the keys in the map.
*
* @param item The item to be added or removed if it is in the group set
* @param keyOrValue True for keys, false for values
* @param addOrRemove True to add, false to remove
* @return true if the given item is a group
*/
private synchronized boolean syncGroups(Object item, boolean keyOrValue, boolean addOrRemove) {
boolean result = false;
Set<Group> groupSet = (keyOrValue == KEYS) ? groupsFromKeys : groupsFromValues;
// only sync if the group list has been instantiated
if (groupSet != null) {
Group group = Group.resolveFrom(item);
if (group != null) {
result = true;
if (addOrRemove == ADD) {
groupSet.add(group);
} else if (addOrRemove == REMOVE) {
groupSet.remove(group);
}
}
}
return result;
}
// below are overrides for the various mutators
@Override
public V put(K key, V value) {
V priorValue = super.put(key, value);
syncGroups(value, VALUES, ADD);
if (priorValue == null) {
syncGroups(key, KEYS, ADD);
} else {
syncGroups(priorValue, VALUES, REMOVE);
}
return priorValue;
}
@Override
public V putIfAbsent(K key, V value) {
V priorValue = super.putIfAbsent(key, value);
// if the map already contains the key, there was no change
if (!value.equals(priorValue)) {
syncGroups(value, VALUES, ADD);
if (priorValue == null) {
syncGroups(key, KEYS, ADD);
} else {
syncGroups(priorValue, VALUES, REMOVE);
}
}
return priorValue;
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
super.putAll(m);
// drop the transient sets; will be rebuilt when/if needed
synchronized(this) {
groupsFromKeys = null;
groupsFromValues = null;
}
}
@Override
public V remove(Object key) {
V priorValue = super.remove(key);
if (priorValue != null) {
syncGroups(key, KEYS, REMOVE);
syncGroups(priorValue, VALUES, REMOVE);
}
return priorValue;
}
@Override
public boolean remove(Object key, Object value) {
boolean removed = super.remove(key, value);
if (removed) {
syncGroups(key, KEYS, REMOVE);
syncGroups(value, VALUES, REMOVE);
}
return removed;
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
boolean replaced = super.replace(key, oldValue, newValue);
if (replaced) {
syncGroups(oldValue, VALUES, REMOVE);
syncGroups(newValue, VALUES, ADD);
}
return replaced;
}
@Override
public V replace(K key, V value) {
V priorValue = super.replace(key, value);
if (priorValue != null) {
syncGroups(value, VALUES, ADD);
syncGroups(priorValue, VALUES, REMOVE);
}
return priorValue;
}
@Override
public void clear() {
super.clear();
synchronized(this) {
groupsFromKeys = null;
groupsFromValues = null;
}
}
private static final boolean KEYS = true;
private static final boolean VALUES = false;
private static final boolean ADD = true;
private static final boolean REMOVE = false;
}
...@@ -26,6 +26,7 @@ import java.io.ObjectInput; ...@@ -26,6 +26,7 @@ import java.io.ObjectInput;
import java.io.ObjectOutput; import java.io.ObjectOutput;
import java.util.AbstractCollection; import java.util.AbstractCollection;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
...@@ -34,11 +35,11 @@ import java.util.Set; ...@@ -34,11 +35,11 @@ import java.util.Set;
import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.event.GroupEventDispatcher; import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.util.PersistableMap;
import org.jivesoftware.util.cache.CacheSizes; import org.jivesoftware.util.cache.CacheSizes;
import org.jivesoftware.util.cache.Cacheable; import org.jivesoftware.util.cache.Cacheable;
import org.jivesoftware.util.cache.CannotCalculateSizeException; import org.jivesoftware.util.cache.CannotCalculateSizeException;
import org.jivesoftware.util.cache.ExternalizableUtil; import org.jivesoftware.util.cache.ExternalizableUtil;
import org.jivesoftware.util.PersistableMap;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID; import org.xmpp.packet.JID;
...@@ -61,6 +62,7 @@ public class Group implements Cacheable, Externalizable { ...@@ -61,6 +62,7 @@ public class Group implements Cacheable, Externalizable {
private transient GroupProvider provider; private transient GroupProvider provider;
private transient GroupManager groupManager; private transient GroupManager groupManager;
private transient PersistableMap<String, String> properties; private transient PersistableMap<String, String> properties;
private transient GroupJID jid;
private String name; private String name;
private String description; private String description;
...@@ -132,6 +134,23 @@ public class Group implements Cacheable, Externalizable { ...@@ -132,6 +134,23 @@ public class Group implements Cacheable, Externalizable {
} }
} }
/**
* Returns a JID for the group based on the group name. This
* instance will be of class GroupJID to distinguish it from
* other types of JIDs in the system.
*
* This method is synchronized to ensure each group has only
* a single JID instance created via lazy instantiation.
*
* @return A JID for the group.
*/
public synchronized GroupJID getJID() {
if (jid == null) {
jid = new GroupJID(getName());
}
return jid;
}
/** /**
* Returns the name of the group. For example, 'XYZ Admins'. * Returns the name of the group. For example, 'XYZ Admins'.
* *
...@@ -155,13 +174,16 @@ public class Group implements Cacheable, Externalizable { ...@@ -155,13 +174,16 @@ public class Group implements Cacheable, Externalizable {
} }
try { try {
String originalName = this.name; String originalName = this.name;
GroupJID originalJID = getJID();
provider.setName(originalName, name); provider.setName(originalName, name);
this.name = name; this.name = name;
this.jid = null; // rebuilt when needed
// Fire event. // Fire event.
Map<String, Object> params = new HashMap<String, Object>(); Map<String, Object> params = new HashMap<String, Object>();
params.put("type", "nameModified"); params.put("type", "nameModified");
params.put("originalValue", originalName); params.put("originalValue", originalName);
params.put("originalJID", originalJID);
GroupEventDispatcher.dispatchEvent(this, GroupEventDispatcher.EventType.group_modified, GroupEventDispatcher.dispatchEvent(this, GroupEventDispatcher.EventType.group_modified,
params); params);
} }
...@@ -232,6 +254,17 @@ public class Group implements Cacheable, Externalizable { ...@@ -232,6 +254,17 @@ public class Group implements Cacheable, Externalizable {
return properties; return properties;
} }
/**
* Returns a Collection of everyone in the group.
*
* @return a read-only Collection of the group administrators + members.
*/
public Collection<JID> getAll() {
Set<JID> everybody = new HashSet<JID>(administrators);
everybody.addAll(members);
return Collections.unmodifiableSet(everybody);
}
/** /**
* Returns a Collection of the group administrators. * Returns a Collection of the group administrators.
* *
...@@ -471,4 +504,41 @@ public class Group implements Cacheable, Externalizable { ...@@ -471,4 +504,41 @@ public class Group implements Cacheable, Externalizable {
ExternalizableUtil.getInstance().readSerializableCollection(in, members, getClass().getClassLoader()); ExternalizableUtil.getInstance().readSerializableCollection(in, members, getClass().getClassLoader());
ExternalizableUtil.getInstance().readSerializableCollection(in, administrators, getClass().getClassLoader()); ExternalizableUtil.getInstance().readSerializableCollection(in, administrators, getClass().getClassLoader());
} }
/**
* Search for a JID within a group. If the given haystack is not resolvable
* to a group, this method returns false.
*
* @param needle A JID, possibly a member/admin of the given group
* @param haystack Presumably a Group, a Group name, or a JID that represents a Group
* @return true if the JID (needle) is found in the group (haystack)
*/
public static boolean search(JID needle, Object haystack) {
Group group = resolveFrom(haystack);
return (group != null && group.isUser(needle));
}
/**
* Attempt to resolve the given object into a Group.
*
* @param proxy Presumably a Group, a Group name, or a JID that represents a Group
* @return The corresponding group, or null if the proxy cannot be resolved as a group
*/
public static Group resolveFrom(Object proxy) {
Group result = null;
try {
GroupManager groupManger = GroupManager.getInstance();
if (proxy instanceof JID) {
result = groupManger.getGroup((JID)proxy);
} else if (proxy instanceof String) {
result = groupManger.getGroup((String)proxy);
} else if (proxy instanceof Group) {
result = (Group) proxy;
}
} catch (GroupNotFoundException gnfe) {
// ignore
}
return result;
}
} }
package org.jivesoftware.openfire.group;
import java.util.List;
import java.util.Set;
/**
* This list specifies additional methods that understand groups among
* the items in the list.
*
* @author Tom Evans
*/
public interface GroupAwareList<T> extends List<T> {
/**
* Returns true if the list contains the given JID. If the JID
* is not found explicitly, search the list for groups and look
* for the JID in each of the corresponding groups.
*
* @param o The target, presumably a JID
* @return True if the target is in the list, or in any groups in the list
*/
boolean includes(Object o);
/**
* Returns the groups that are implied (resolvable) from the items in the list.
*
* @return A Set containing the groups in the list
*/
Set<Group> getGroups();
}
package org.jivesoftware.openfire.group;
import java.util.Map;
import java.util.Set;
/**
* This map specifies additional methods that understand groups among
* the entries in the map.
*
* @author Tom Evans
*/
public interface GroupAwareMap<K, V> extends Map<K, V> {
/**
* Returns true if the map's keySet contains the given JID. If the JID
* is not found explicitly, search the keySet for groups and look
* for the JID in each of the corresponding groups.
*
* @param key The target, presumably a JID
* @return True if the target is in the key list, or in any groups in the key list
*/
public boolean includesKey(Object key);
/**
* Returns true if the map has a key referencing the given JID. If the JID
* is not found explicitly, search the values for groups and look
* for the JID in each of the corresponding groups.
*
* @param value The target, presumably a JID
* @return True if the target is in the key list, or in any groups in the key list
*/
public boolean includesValue(Object value);
/**
* Returns the groups that are implied (resolvable) from the keys in the map.
*
* @return A new Set containing the groups in the keySet
*/
Set<Group> getGroupsFromKeys();
/**
* Returns the groups that are implied (resolvable) from the values in the map.
*
* @return A new Set containing the groups among the mapped values
*/
Set<Group> getGroupsFromValues();
}
package org.jivesoftware.openfire.group;
import java.io.UnsupportedEncodingException;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
/**
* This class is designed to identify and manage custom JIDs
* that represent Groups (rather than Users or Components).
*
* The node for a GroupJID is the group name encoded as base32hex.
* This allows us to preserve special characters and upper/lower casing
* within the group name. The encoded group name is valid according to
* the RFC6122 rules for a valid node and does not require further
* JID escaping.
*
* We use an MD5 hash of the group name as the resource value to help
* distinguish Group JIDs from regular JIDs in the local domain when
* they are persisted in the DB or over the network.
*
* @author Tom Evans
*
*/
public class GroupJID extends JID {
private static final Logger Log = LoggerFactory.getLogger(GroupJID.class);
private static final long serialVersionUID = 5681300465012974014L;
private transient String groupName;
/**
* Construct a JID representing a Group.
*
* @param name A group name for the local domain
*/
public GroupJID(String name) {
super(encodeNode(name),
XMPPServer.getInstance().getServerInfo().getXMPPDomain(),
StringUtils.hash(name),
true);
groupName = name;
}
/**
* Construct a JID representing a Group from a regular JID. This constructor is
* private because it is used only from within this class after the source JID
* has been validated.
*
* @param source A full JID representing a group
* @see GroupJID#fromString
*/
private GroupJID(JID source) {
// skip stringprep for the new group JID, since it has already been parsed
super(source.getNode(), source.getDomain(), source.getResource(), true);
}
/**
* Returns the group name corresponding to this JID.
*
* @return The name for the corresponding group
*/
public String getGroupName() {
// lazy instantiation
if (groupName == null) {
groupName = decodeNode(getNode());
}
return groupName;
}
/**
* Override the base class implementation to retain the resource
* identifier for group JIDs.
*
* @return This JID, as a group JID
*/
@Override
public JID asBareJID() {
return this;
}
/**
* Override the base class implementation to retain the resource
* identifier for group JIDs.
*
* @return The full JID rendered as a string
*/
@Override
public String toBareJID() {
return this.toString();
}
@Override
public int compareTo(JID jid) {
// Comparison order is domain, node, resource.
int compare = getDomain().compareTo(jid.getDomain());
if (compare == 0) {
String otherNode = jid.getNode();
compare = otherNode == null ? 1 : getGroupName().compareTo(otherNode);
}
if (compare == 0) {
compare = jid.getResource() == null ? 0 : -1;
}
return compare;
}
/**
* Encode the given group name in base32hex (UTF-8). This encoding
* is valid according to the nodeprep profile of stringprep
* (RFC6122, Appendix A) and needs no further escaping.
*
* @param name A group name
* @return The encoded group name
*/
private static String encodeNode(String name) {
return StringUtils.encodeBase32(name);
}
/**
* Decode the given group name from base32hex (UTF-8).
*
* @param name A group name, encoded as base32hex
* @return The group name
*/
private static String decodeNode(String node) {
try {
return new String(StringUtils.decodeBase32(node), "UTF-8");
} catch (UnsupportedEncodingException uee) {
// this shouldn't happen, but ...
Log.error("Unexpected encoding exception", uee);
return null;
}
}
/**
* Check a JID to determine whether it represents a group. If the given
* JID is an instance of this class, it is a group JID. Otherwise,
* calculate the hash to determine whether the JID can be resolved to
* a group.
*
* @param jid A JID, possibly representing a group
* @return true if the given jid represents a group in the local domain
*/
public static boolean isGroup(JID jid) {
try {
return isGroup(jid, false);
} catch (GroupNotFoundException gnfe) {
// should not happen because we do not validate the group exists
Log.error("Unexpected group validation", gnfe);
return false;
}
}
/**
* Check a JID to determine whether it represents a group. If the given
* JID is an instance of this class, it is a group JID. Otherwise,
* calculate the hash to determine whether the JID can be resolved to
* a group. This method also optionally validates that the corresponding
* group actually exists in the local domain.
*
* @param jid A JID, possibly representing a group
* @param groupMustExist If true, validate that the corresponding group actually exists
* @return true if the given jid represents a group in the local domain
* @throws GroupNotFoundException The JID represents a group, but the group does not exist
*/
public static boolean isGroup(JID jid, boolean groupMustExist) throws GroupNotFoundException {
boolean isGroup = false;
String groupName = null, node = jid.getNode();
if (node != null) {
isGroup = (jid instanceof GroupJID) ? true :
jid.getResource() != null &&
StringUtils.isBase32(node) &&
StringUtils.hash(groupName = decodeNode(node)).equals(jid.getResource());
if (isGroup && groupMustExist) {
Log.debug("Validating group: " + jid);
if (XMPPServer.getInstance().isLocal(jid)) {
GroupManager.getInstance().getGroup(groupName);
} else {
isGroup = false; // not in the local domain
}
}
}
return isGroup;
}
/**
* Returns a JID from the given JID. If the JID represents a group,
* returns an instance of this class. Otherwise returns the given JID.
*
* @param jid A JID, possibly representing a group
* @return A new GroupJID if the given JID represents a group, or the given JID
*/
public static JID fromJID(JID jid) {
if (jid instanceof GroupJID || jid.getResource() == null || jid.getNode() == null) {
return jid;
} else {
return (isGroup(jid)) ? new GroupJID(jid) : jid;
}
}
/**
* Creates a JID from the given string. If the string represents a group,
* return an instance of this class. Otherwise returns a regular JID.
*
* @param jid A JID, possibly representing a group
* @return A JID with a type appropriate to its content
* @throws IllegalArgumentException the given string is not a valid JID
*/
public static JID fromString(String jid) {
Log.debug("Parsing JID from string: " + jid);
return fromJID(new JID(jid));
}
}
...@@ -22,7 +22,6 @@ package org.jivesoftware.openfire.group; ...@@ -22,7 +22,6 @@ package org.jivesoftware.openfire.group;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator;
import java.util.Map; import java.util.Map;
import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.XMPPServer;
...@@ -298,6 +297,18 @@ public class GroupManager { ...@@ -298,6 +297,18 @@ public class GroupManager {
} }
} }
/**
* Returns the corresponding group if the given JID represents a group.
*
* @param groupJID The JID for the group to retrieve
* @return The group corresponding to the JID, or null if the JID does not represent a group
* @throws GroupNotFoundException if the JID represents a group that does not exist
*/
public Group getGroup(JID jid) throws GroupNotFoundException {
JID groupJID = GroupJID.fromJID(jid);
return (groupJID instanceof GroupJID) ? getGroup(((GroupJID)groupJID).getGroupName()) : null;
}
/** /**
* Returns a Group by name. * Returns a Group by name.
* *
...@@ -313,22 +324,24 @@ public class GroupManager { ...@@ -313,22 +324,24 @@ public class GroupManager {
* Returns a Group by name. * Returns a Group by name.
* *
* @param name The name of the group to retrieve * @param name The name of the group to retrieve
* @param forceLookup Invalidate the group cache for this group
* @return The group corresponding to that name * @return The group corresponding to that name
* @throws GroupNotFoundException if the group does not exist. * @throws GroupNotFoundException if the group does not exist.
*/ */
public Group getGroup(String name, boolean forceLookup) throws GroupNotFoundException { public Group getGroup(String name, boolean forceLookup) throws GroupNotFoundException {
Group group = null; Group group = null;
if (!forceLookup) { if (forceLookup) {
groupCache.remove(name);
} else {
group = groupCache.get(name); group = groupCache.get(name);
} }
// If ID wan't found in cache, load it up and put it there. // If ID wan't found in cache, load it up and put it there.
if (group == null) { if (group == null) {
synchronized (name.intern()) { synchronized (name.intern()) {
group = groupCache.get(name); group = groupCache.get(name);
// If group wan't found in cache, load it up and put it there.
if (group == null) { if (group == null) {
group = provider.getGroup(name); group = provider.getGroup(name);
groupCache.put(name, group); groupCache.put(name, group);
} }
} }
} }
......
...@@ -60,22 +60,37 @@ public interface MultiUserChatService extends Component { ...@@ -60,22 +60,37 @@ public interface MultiUserChatService extends Component {
* Returns the collection of JIDs that are system administrators of the MUC service. A sysadmin has * Returns the collection of JIDs that are system administrators of the MUC service. A sysadmin has
* the same permissions as a room owner. * the same permissions as a room owner.
* *
* @return a list of bare JIDs. * @return a list of user/group JIDs.
*/ */
Collection<JID> getSysadmins(); Collection<JID> getSysadmins();
/**
* Validates the given JID as a MUC service administrator.
*
* @return true if the given JID is a MUC service administrator
*/
boolean isSysadmin(JID bareJID);
/** /**
* Adds a new system administrator of the MUC service. A sysadmin has the same permissions as * Adds a new system administrator of the MUC service. A sysadmin has the same permissions as
* a room owner. * a room owner.
* *
* @param userJID the bare JID of the new user to add as a system administrator. * @param userJID the bare JID of the new user/group to add as a system administrator.
*/ */
void addSysadmin(JID userJID); void addSysadmin(JID userJID);
/**
* Adds multiple system administrators for the MUC service. A sysadmin has the same permissions as
* a room owner.
*
* @param userJIDs the JIDs of the new users/groups to add as a system administrator.
*/
void addSysadmins(Collection<JID> userJIDs);
/** /**
* Removes a system administrator of the MUC service. * Removes a system administrator of the MUC service.
* *
* @param userJID the bare JID of the user to remove from the list. * @param userJID the bare JID of the user/group to remove from the list.
*/ */
void removeSysadmin(JID userJID); void removeSysadmin(JID userJID);
...@@ -99,34 +114,34 @@ public interface MultiUserChatService extends Component { ...@@ -99,34 +114,34 @@ public interface MultiUserChatService extends Component {
* Returns the collection of JIDs that are allowed to create MUC rooms. An empty list means that * Returns the collection of JIDs that are allowed to create MUC rooms. An empty list means that
* anyone can create a room. * anyone can create a room.
* *
* @return a list of bare JIDs. * @return a list of user/group JIDs.
*/ */
Collection<JID> getUsersAllowedToCreate(); Collection<JID> getUsersAllowedToCreate();
/** /**
* Adds a new user to the list of JIDs that are allowed to create MUC rooms. * Adds a new user/group to the list of JIDs that are allowed to create MUC rooms.
* *
* @param userJID the bare JID of the new user to add to list. * @param userJID the JID of the new user/group to add to list.
*/ */
void addUserAllowedToCreate(JID userJID); void addUserAllowedToCreate(JID userJID);
/** /**
* Adds new users to the list of JIDs that are allowed to create MUC rooms. * Adds new users/groups to the list of JIDs that are allowed to create MUC rooms.
* @param userJIDs collection of bare JIDs if users to add to list. * @param userJIDs collection of JIDs for users/groups to add to list.
*/ */
void addUsersAllowedToCreate(Collection<JID> userJIDs); void addUsersAllowedToCreate(Collection<JID> userJIDs);
/** /**
* Removes a user from list of JIDs that are allowed to create MUC rooms. * Removes a user/group from list of JIDs that are allowed to create MUC rooms.
* *
* @param userJID the bare JID of the user to remove from the list. * @param userJID the JID of the user/group to remove from the list.
*/ */
void removeUserAllowedToCreate(JID userJID); void removeUserAllowedToCreate(JID userJID);
/** /**
* Removes users from list of JIDs that are allowed to create MUC rooms. * Removes users/groups from list of JIDs that are allowed to create MUC rooms.
* *
* @param userJIDs collection of bare JIDs of users to remove from the list. * @param userJIDs collection of JIDs of users/groups to remove from the list.
*/ */
void removeUsersAllowedToCreate(Collection<JID> userJIDs); void removeUsersAllowedToCreate(Collection<JID> userJIDs);
......
...@@ -25,6 +25,10 @@ import java.util.List; ...@@ -25,6 +25,10 @@ import java.util.List;
import org.dom4j.Element; import org.dom4j.Element;
import org.jivesoftware.openfire.PacketRouter; import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupJID;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.openfire.muc.CannotBeInvitedException; import org.jivesoftware.openfire.muc.CannotBeInvitedException;
import org.jivesoftware.openfire.muc.ConflictException; import org.jivesoftware.openfire.muc.ConflictException;
import org.jivesoftware.openfire.muc.ForbiddenException; import org.jivesoftware.openfire.muc.ForbiddenException;
...@@ -32,6 +36,8 @@ import org.jivesoftware.openfire.muc.MUCRole; ...@@ -32,6 +36,8 @@ import org.jivesoftware.openfire.muc.MUCRole;
import org.jivesoftware.openfire.muc.NotAllowedException; import org.jivesoftware.openfire.muc.NotAllowedException;
import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ; import org.xmpp.packet.IQ;
import org.xmpp.packet.JID; import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError; import org.xmpp.packet.PacketError;
...@@ -47,6 +53,8 @@ import org.xmpp.packet.Presence; ...@@ -47,6 +53,8 @@ import org.xmpp.packet.Presence;
*/ */
public class IQAdminHandler { public class IQAdminHandler {
private static final Logger logger = LoggerFactory.getLogger(IQAdminHandler.class);
private final LocalMUCRoom room; private final LocalMUCRoom room;
private final PacketRouter router; private final PacketRouter router;
...@@ -152,9 +160,19 @@ public class IQAdminHandler { ...@@ -152,9 +160,19 @@ public class IQAdminHandler {
throw new ForbiddenException(); throw new ForbiddenException();
} }
for (JID jid : room.getOutcasts()) { for (JID jid : room.getOutcasts()) {
metaData = result.addElement("item", "http://jabber.org/protocol/muc#admin"); if (GroupJID.isGroup(jid)) {
metaData.addAttribute("affiliation", "outcast"); try {
metaData.addAttribute("jid", jid.toString()); // add each group member to the result (clients don't understand groups)
Group group = GroupManager.getInstance().getGroup(jid);
for (JID groupMember : group.getAll()) {
metaData = addAffiliationToResult(affiliation, result, groupMember);
}
} catch (GroupNotFoundException gnfe) {
logger.warn("Invalid group JID in the outcast list: " + jid);
}
} else {
metaData = addAffiliationToResult(affiliation, result, jid);
}
} }
} else if ("member".equals(affiliation)) { } else if ("member".equals(affiliation)) {
...@@ -166,18 +184,19 @@ public class IQAdminHandler { ...@@ -166,18 +184,19 @@ public class IQAdminHandler {
throw new ForbiddenException(); throw new ForbiddenException();
} }
for (JID jid : room.getMembers()) { for (JID jid : room.getMembers()) {
metaData = result.addElement("item", "http://jabber.org/protocol/muc#admin"); if (GroupJID.isGroup(jid)) {
metaData.addAttribute("affiliation", "member"); try {
metaData.addAttribute("jid", jid.toString()); // add each group member to the result (clients don't understand groups)
try { Group group = GroupManager.getInstance().getGroup(jid);
List<MUCRole> roles = room.getOccupantsByBareJID(jid); for (JID groupMember : group.getAll()) {
MUCRole role = roles.get(0); metaData = addAffiliationToResult(affiliation, result, groupMember);
metaData.addAttribute("role", role.getRole().toString()); }
metaData.addAttribute("nick", role.getNickname()); } catch (GroupNotFoundException gnfe) {
} logger.warn("Invalid group JID in the member list: " + jid);
catch (UserNotFoundException e) { }
// Do nothing } else {
} metaData = addAffiliationToResult(affiliation, result, jid);
}
} }
} else if ("moderator".equals(roleAttribute)) { } else if ("moderator".equals(roleAttribute)) {
// The client is requesting the list of moderators // The client is requesting the list of moderators
...@@ -206,41 +225,37 @@ public class IQAdminHandler { ...@@ -206,41 +225,37 @@ public class IQAdminHandler {
} }
} else if ("owner".equals(affiliation)) { } else if ("owner".equals(affiliation)) {
// The client is requesting the list of owners // The client is requesting the list of owners
Element ownerMetaData;
MUCRole role;
for (JID jid : room.getOwners()) { for (JID jid : room.getOwners()) {
ownerMetaData = result.addElement("item", "http://jabber.org/protocol/muc#admin"); if (GroupJID.isGroup(jid)) {
ownerMetaData.addAttribute("affiliation", "owner"); try {
ownerMetaData.addAttribute("jid", jid.toBareJID()); // add each group member to the result (clients don't understand groups)
// Add role and nick to the metadata if the user is in the room Group group = GroupManager.getInstance().getGroup(jid);
try { for (JID groupMember : group.getAll()) {
List<MUCRole> roles = room.getOccupantsByBareJID(jid); metaData = addAffiliationToResult(affiliation, result, groupMember);
role = roles.get(0); }
ownerMetaData.addAttribute("role", role.getRole().toString()); } catch (GroupNotFoundException gnfe) {
ownerMetaData.addAttribute("nick", role.getNickname()); logger.warn("Invalid group JID in the owner list: " + jid);
} }
catch (UserNotFoundException e) { } else {
// Do nothing metaData = addAffiliationToResult(affiliation, result, jid);
} }
} }
} else if ("admin".equals(affiliation)) { } else if ("admin".equals(affiliation)) {
// The client is requesting the list of admins // The client is requesting the list of admins
Element adminMetaData;
MUCRole role;
for (JID jid : room.getAdmins()) { for (JID jid : room.getAdmins()) {
adminMetaData = result.addElement("item", "http://jabber.org/protocol/muc#admin"); if (GroupJID.isGroup(jid)) {
adminMetaData.addAttribute("affiliation", "admin"); try {
adminMetaData.addAttribute("jid", jid.toBareJID()); // add each group member to the result (clients don't understand groups)
// Add role and nick to the metadata if the user is in the room Group group = GroupManager.getInstance().getGroup(jid);
try { for (JID groupMember : group.getAll()) {
List<MUCRole> roles = room.getOccupantsByBareJID(jid); metaData = addAffiliationToResult(affiliation, result, groupMember);
role = roles.get(0); }
adminMetaData.addAttribute("role", role.getRole().toString()); } catch (GroupNotFoundException gnfe) {
adminMetaData.addAttribute("nick", role.getNickname()); logger.warn("Invalid group JID in the admin list: " + jid);
} }
catch (UserNotFoundException e) { } else {
// Do nothing metaData = addAffiliationToResult(affiliation, result, jid);
} }
} }
} else { } else {
reply.setError(PacketError.Condition.bad_request); reply.setError(PacketError.Condition.bad_request);
...@@ -268,7 +283,8 @@ public class IQAdminHandler { ...@@ -268,7 +283,8 @@ public class IQAdminHandler {
// going to change a role or an affiliation // going to change a role or an affiliation
nick = item.attributeValue("nick"); nick = item.attributeValue("nick");
if (hasJID) { if (hasJID) {
jids.add(new JID(item.attributeValue("jid"))); // could be a group JID
jids.add(GroupJID.fromString(item.attributeValue("jid")));
} else { } else {
// Get the JID based on the requested nick // Get the JID based on the requested nick
for (MUCRole role : room.getOccupantsByNickname(nick)) { for (MUCRole role : room.getOccupantsByNickname(nick)) {
...@@ -300,9 +316,24 @@ public class IQAdminHandler { ...@@ -300,9 +316,24 @@ public class IQAdminHandler {
presences.addAll(room.addMember(jid, nick, senderRole)); presences.addAll(room.addMember(jid, nick, senderRole));
// If the user had an affiliation don't send an invitation. Otherwise // If the user had an affiliation don't send an invitation. Otherwise
// send an invitation if the room is members-only and skipping invites // send an invitation if the room is members-only and skipping invites
// are not disabled system-wide xmpp.muc.skipInvite // are not disabled system-wide xmpp.muc.skipInvite
if (!skipInvite && !hadAffiliation && room.isMembersOnly()) { if (!skipInvite && !hadAffiliation && room.isMembersOnly()) {
room.sendInvitation(jid, null, senderRole, null); List<JID> invitees = new ArrayList<JID>();
if (GroupJID.isGroup(jid)) {
try {
Group group = GroupManager.getInstance().getGroup(jid);
for (JID inGroup : group.getAll()) {
invitees.add(inGroup);
}
} catch (GroupNotFoundException gnfe) {
logger.error("Failed to send invitations for group members", gnfe);
}
} else {
invitees.add(jid);
}
for (JID invitee : invitees) {
room.sendInvitation(invitee, null, senderRole, null);
}
} }
} else if ("outcast".equals(target)) { } else if ("outcast".equals(target)) {
// Add the user as an outcast of the room based on the bare JID // Add the user as an outcast of the room based on the bare JID
...@@ -335,4 +366,20 @@ public class IQAdminHandler { ...@@ -335,4 +366,20 @@ public class IQAdminHandler {
} }
} }
} }
private Element addAffiliationToResult(String affiliation, Element parent, JID jid) {
Element result = parent.addElement("item", "http://jabber.org/protocol/muc#admin");
result.addAttribute("affiliation", affiliation);
result.addAttribute("jid", jid.toString());
try {
List<MUCRole> roles = room.getOccupantsByBareJID(jid);
MUCRole role = roles.get(0);
result.addAttribute("role", role.getRole().toString());
result.addAttribute("nick", role.getNickname());
}
catch (UserNotFoundException e) {
// the JID is note currently an occupant
}
return result;
}
} }
/** /**
* $RCSfile$
* $Revision: 1623 $ * $Revision: 1623 $
* $Date: 2005-07-12 18:40:57 -0300 (Tue, 12 Jul 2005) $ * $Date: 2005-07-12 18:40:57 -0300 (Tue, 12 Jul 2005) $
* *
...@@ -22,20 +21,21 @@ package org.jivesoftware.openfire.muc.spi; ...@@ -22,20 +21,21 @@ package org.jivesoftware.openfire.muc.spi;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import org.dom4j.DocumentHelper; import org.dom4j.DocumentHelper;
import org.dom4j.Element; import org.dom4j.Element;
import org.dom4j.QName; import org.dom4j.QName;
import org.jivesoftware.openfire.PacketRouter; import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupJID;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.openfire.muc.CannotBeInvitedException; import org.jivesoftware.openfire.muc.CannotBeInvitedException;
import org.jivesoftware.openfire.muc.ConflictException; import org.jivesoftware.openfire.muc.ConflictException;
import org.jivesoftware.openfire.muc.ForbiddenException; import org.jivesoftware.openfire.muc.ForbiddenException;
import org.jivesoftware.openfire.muc.MUCRole; import org.jivesoftware.openfire.muc.MUCRole;
import org.jivesoftware.openfire.muc.cluster.RoomUpdatedEvent; import org.jivesoftware.openfire.muc.cluster.RoomUpdatedEvent;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils; import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.cache.CacheFactory; import org.jivesoftware.util.cache.CacheFactory;
...@@ -223,7 +223,8 @@ public class IQOwnerHandler { ...@@ -223,7 +223,8 @@ public class IQOwnerHandler {
// XEP-0045: "Affiliations are granted, revoked, and // XEP-0045: "Affiliations are granted, revoked, and
// maintained based on the user's bare JID, (...)" // maintained based on the user's bare JID, (...)"
if (value != null && value.trim().length() != 0) { if (value != null && value.trim().length() != 0) {
admins.add(new JID(value.trim()).asBareJID()); // could be a group jid
admins.add(GroupJID.fromString((value.trim())).asBareJID());
} }
} }
} }
...@@ -237,12 +238,13 @@ public class IQOwnerHandler { ...@@ -237,12 +238,13 @@ public class IQOwnerHandler {
// XEP-0045: "Affiliations are granted, revoked, and // XEP-0045: "Affiliations are granted, revoked, and
// maintained based on the user's bare JID, (...)" // maintained based on the user's bare JID, (...)"
if (value != null && value.trim().length() != 0) { if (value != null && value.trim().length() != 0) {
owners.add(new JID(value.trim()).asBareJID()); // could be a group jid
owners.add(GroupJID.fromString((value.trim())).asBareJID());
} }
} }
} }
// Answer a conflic error if all the current owners will be removed // Answer a conflict error if all the current owners will be removed
if (ownersSent && owners.isEmpty()) { if (ownersSent && owners.isEmpty()) {
throw new ConflictException(); throw new ConflictException();
} }
...@@ -394,7 +396,10 @@ public class IQOwnerHandler { ...@@ -394,7 +396,10 @@ public class IQOwnerHandler {
ownersToRemove.removeAll(admins); ownersToRemove.removeAll(admins);
ownersToRemove.removeAll(owners); ownersToRemove.removeAll(owners);
for (JID jid : ownersToRemove) { for (JID jid : ownersToRemove) {
presences.addAll(room.addMember(jid, null, senderRole)); // ignore group jids
if (!GroupJID.isGroup(jid)) {
presences.addAll(room.addMember(jid, null, senderRole));
}
} }
} }
...@@ -405,7 +410,10 @@ public class IQOwnerHandler { ...@@ -405,7 +410,10 @@ public class IQOwnerHandler {
adminsToRemove.removeAll(admins); adminsToRemove.removeAll(admins);
adminsToRemove.removeAll(owners); adminsToRemove.removeAll(owners);
for (JID jid : adminsToRemove) { for (JID jid : adminsToRemove) {
presences.addAll(room.addMember(jid, null, senderRole)); // ignore group jids
if (!GroupJID.isGroup(jid)) {
presences.addAll(room.addMember(jid, null, senderRole));
}
} }
} }
...@@ -497,13 +505,37 @@ public class IQOwnerHandler { ...@@ -497,13 +505,37 @@ public class IQOwnerHandler {
field = configurationForm.getField("muc#roomconfig_roomadmins"); field = configurationForm.getField("muc#roomconfig_roomadmins");
field.clearValues(); field.clearValues();
for (JID jid : room.getAdmins()) { for (JID jid : room.getAdmins()) {
field.addValue(jid.toString()); if (GroupJID.isGroup(jid)) {
try {
// add each group member to the result (clients don't understand groups)
Group group = GroupManager.getInstance().getGroup(jid);
for (JID groupMember : group.getAll()) {
field.addValue(groupMember);
}
} catch (GroupNotFoundException gnfe) {
Log.warn("Invalid group JID in the member list: " + jid);
}
} else {
field.addValue(jid.toString());
}
} }
field = configurationForm.getField("muc#roomconfig_roomowners"); field = configurationForm.getField("muc#roomconfig_roomowners");
field.clearValues(); field.clearValues();
for (JID jid : room.getOwners()) { for (JID jid : room.getOwners()) {
field.addValue(jid.toString()); if (GroupJID.isGroup(jid)) {
try {
// add each group member to the result (clients don't understand groups)
Group group = GroupManager.getInstance().getGroup(jid);
for (JID groupMember : group.getAll()) {
field.addValue(groupMember);
}
} catch (GroupNotFoundException gnfe) {
Log.warn("Invalid group JID in the member list: " + jid);
}
} else {
field.addValue(jid.toString());
}
} }
// Remove the old element // Remove the old element
......
...@@ -173,8 +173,8 @@ public class LocalMUCRole implements MUCRole { ...@@ -173,8 +173,8 @@ public class LocalMUCRole implements MUCRole {
throw new NotAllowedException(); throw new NotAllowedException();
} }
} }
// A moderator cannot be kicked from a room // A moderator cannot be kicked from a room unless there has also been an affiliation change
if (MUCRole.Role.moderator == role && MUCRole.Role.none == newRole) { if (MUCRole.Role.moderator == role && MUCRole.Role.none == newRole && MUCRole.Affiliation.none != affiliation) {
throw new NotAllowedException(); throw new NotAllowedException();
} }
// TODO A moderator MUST NOT be able to revoke voice from a user whose affiliation is at or // TODO A moderator MUST NOT be able to revoke voice from a user whose affiliation is at or
......
...@@ -32,7 +32,6 @@ import java.util.Iterator; ...@@ -32,7 +32,6 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
...@@ -42,6 +41,16 @@ import org.jivesoftware.openfire.PacketRouter; ...@@ -42,6 +41,16 @@ import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.cluster.NodeID; import org.jivesoftware.openfire.cluster.NodeID;
import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.openfire.event.GroupEventListener;
import org.jivesoftware.openfire.group.ConcurrentGroupList;
import org.jivesoftware.openfire.group.ConcurrentGroupMap;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupAwareList;
import org.jivesoftware.openfire.group.GroupAwareMap;
import org.jivesoftware.openfire.group.GroupJID;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.openfire.muc.CannotBeInvitedException; import org.jivesoftware.openfire.muc.CannotBeInvitedException;
import org.jivesoftware.openfire.muc.ConflictException; import org.jivesoftware.openfire.muc.ConflictException;
import org.jivesoftware.openfire.muc.ForbiddenException; import org.jivesoftware.openfire.muc.ForbiddenException;
...@@ -97,7 +106,7 @@ import org.xmpp.packet.Presence; ...@@ -97,7 +106,7 @@ import org.xmpp.packet.Presence;
* *
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public class LocalMUCRoom implements MUCRoom { public class LocalMUCRoom implements MUCRoom, GroupEventListener {
private static final Logger Log = LoggerFactory.getLogger(LocalMUCRoom.class); private static final Logger Log = LoggerFactory.getLogger(LocalMUCRoom.class);
...@@ -172,22 +181,22 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -172,22 +181,22 @@ public class LocalMUCRoom implements MUCRoom {
/** /**
* List of chatroom's owner. The list contains only bare jid. * List of chatroom's owner. The list contains only bare jid.
*/ */
List<JID> owners = new CopyOnWriteArrayList<JID>(); GroupAwareList<JID> owners = new ConcurrentGroupList<JID>();
/** /**
* List of chatroom's admin. The list contains only bare jid. * List of chatroom's admin. The list contains only bare jid.
*/ */
List<JID> admins = new CopyOnWriteArrayList<JID>(); GroupAwareList<JID> admins = new ConcurrentGroupList<JID>();
/** /**
* List of chatroom's members. The list contains only bare jid, mapped to a nickname. * List of chatroom's members. The list contains only bare jid, mapped to a nickname.
*/ */
private Map<JID, String> members = new ConcurrentHashMap<JID,String>(); GroupAwareMap<JID, String> members = new ConcurrentGroupMap<JID,String>();
/** /**
* List of chatroom's outcast. The list contains only bare jid of not allowed users. * List of chatroom's outcast. The list contains only bare jid of not allowed users.
*/ */
private List<JID> outcasts = new CopyOnWriteArrayList<JID>(); private GroupAwareList<JID> outcasts = new ConcurrentGroupList<JID>();
/** /**
* The natural language name of the room. * The natural language name of the room.
...@@ -373,6 +382,7 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -373,6 +382,7 @@ public class LocalMUCRoom implements MUCRoom {
rolesToBroadcastPresence.add("moderator"); rolesToBroadcastPresence.add("moderator");
rolesToBroadcastPresence.add("participant"); rolesToBroadcastPresence.add("participant");
rolesToBroadcastPresence.add("visitor"); rolesToBroadcastPresence.add("visitor");
GroupEventDispatcher.addListener(this);
} }
public String getName() { public String getName() {
...@@ -502,16 +512,16 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -502,16 +512,16 @@ public class LocalMUCRoom implements MUCRoom {
public MUCRole.Affiliation getAffiliation(JID jid) { public MUCRole.Affiliation getAffiliation(JID jid) {
final JID bareJID = jid.asBareJID(); final JID bareJID = jid.asBareJID();
if (owners.contains(bareJID)) { if (owners.includes(bareJID)) {
return MUCRole.Affiliation.owner; return MUCRole.Affiliation.owner;
} }
else if (admins.contains(bareJID)) { else if (admins.includes(bareJID)) {
return MUCRole.Affiliation.admin; return MUCRole.Affiliation.admin;
} }
else if (members.containsKey(bareJID)) { else if (members.includesKey(bareJID)) {
return MUCRole.Affiliation.member; return MUCRole.Affiliation.member;
} }
else if (outcasts.contains(bareJID)) { else if (outcasts.includes(bareJID)) {
return MUCRole.Affiliation.outcast; return MUCRole.Affiliation.outcast;
} }
return MUCRole.Affiliation.none; return MUCRole.Affiliation.none;
...@@ -536,7 +546,7 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -536,7 +546,7 @@ public class LocalMUCRoom implements MUCRoom {
throw new ServiceUnavailableException(); throw new ServiceUnavailableException();
} }
final JID bareJID = user.getAddress().asBareJID(); final JID bareJID = user.getAddress().asBareJID();
boolean isOwner = owners.contains(bareJID); boolean isOwner = owners.includes(bareJID);
// If the room is locked and this user is not an owner raise a RoomLocked exception // If the room is locked and this user is not an owner raise a RoomLocked exception
if (isLocked()) { if (isLocked()) {
if (!isOwner) { if (!isOwner) {
...@@ -581,23 +591,29 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -581,23 +591,29 @@ public class LocalMUCRoom implements MUCRoom {
role = MUCRole.Role.moderator; role = MUCRole.Role.moderator;
affiliation = MUCRole.Affiliation.owner; affiliation = MUCRole.Affiliation.owner;
} }
else if (mucService.getSysadmins().contains(bareJID)) { else if (mucService.isSysadmin(bareJID)) {
// The user is a system administrator of the MUC service. Treat him as an owner // The user is a system administrator of the MUC service. Treat him as an owner
// although he won't appear in the list of owners // although he won't appear in the list of owners
role = MUCRole.Role.moderator; role = MUCRole.Role.moderator;
affiliation = MUCRole.Affiliation.owner; affiliation = MUCRole.Affiliation.owner;
} }
else if (admins.contains(bareJID)) { else if (admins.includes(bareJID)) {
// The user is an admin. Set the role and affiliation accordingly. // The user is an admin. Set the role and affiliation accordingly.
role = MUCRole.Role.moderator; role = MUCRole.Role.moderator;
affiliation = MUCRole.Affiliation.admin; affiliation = MUCRole.Affiliation.admin;
} }
else if (members.containsKey(bareJID)) { // explicit outcast status has higher precedence than member status
else if (outcasts.contains(bareJID)) {
// The user is an outcast. Raise a "Forbidden" exception.
throw new ForbiddenException();
}
else if (members.includesKey(bareJID)) {
// The user is a member. Set the role and affiliation accordingly. // The user is a member. Set the role and affiliation accordingly.
role = MUCRole.Role.participant; role = MUCRole.Role.participant;
affiliation = MUCRole.Affiliation.member; affiliation = MUCRole.Affiliation.member;
} }
else if (outcasts.contains(bareJID)) { // this checks if the user is an outcast implicitly (via a group)
else if (outcasts.includes(bareJID)) {
// The user is an outcast. Raise a "Forbidden" exception. // The user is an outcast. Raise a "Forbidden" exception.
throw new ForbiddenException(); throw new ForbiddenException();
} }
...@@ -681,8 +697,8 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -681,8 +697,8 @@ public class LocalMUCRoom implements MUCRoom {
* @return boolean * @return boolean
*/ */
private boolean canJoinRoom(LocalMUCUser user){ private boolean canJoinRoom(LocalMUCUser user){
boolean isOwner = owners.contains(user.getAddress().asBareJID()); boolean isOwner = owners.includes(user.getAddress().asBareJID());
boolean isAdmin = admins.contains(user.getAddress().asBareJID()); boolean isAdmin = admins.includes(user.getAddress().asBareJID());
return (!isDestroyed && (!hasOccupancyLimit() || isAdmin || isOwner || (getOccupantsCount() < getMaxUsers()))); return (!isDestroyed && (!hasOccupancyLimit() || isAdmin || isOwner || (getOccupantsCount() < getMaxUsers())));
} }
...@@ -1398,6 +1414,7 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -1398,6 +1414,7 @@ public class LocalMUCRoom implements MUCRoom {
} }
public List<Presence> addOwner(JID jid, MUCRole sendRole) throws ForbiddenException { public List<Presence> addOwner(JID jid, MUCRole sendRole) throws ForbiddenException {
final JID bareJID = jid.asBareJID(); final JID bareJID = jid.asBareJID();
lock.writeLock().lock(); lock.writeLock().lock();
try { try {
...@@ -1405,7 +1422,7 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -1405,7 +1422,7 @@ public class LocalMUCRoom implements MUCRoom {
if (MUCRole.Affiliation.owner != sendRole.getAffiliation()) { if (MUCRole.Affiliation.owner != sendRole.getAffiliation()) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
// Check if user is already an owner // Check if user is already an owner (explicitly)
if (owners.contains(bareJID)) { if (owners.contains(bareJID)) {
// Do nothing // Do nothing
return Collections.emptyList(); return Collections.emptyList();
...@@ -1434,15 +1451,10 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -1434,15 +1451,10 @@ public class LocalMUCRoom implements MUCRoom {
} }
// Update other cluster nodes with new affiliation // Update other cluster nodes with new affiliation
CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.owner)); CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.owner));
// Update the presence with the new affiliation and inform all occupants
try { // apply the affiliation change, assigning a new affiliation
return changeOccupantAffiliation(sendRole, jid, MUCRole.Affiliation.owner, // based on the group(s) of the affected user(s)
MUCRole.Role.moderator); return applyAffiliationChange(getRole(), bareJID, null);
}
catch (NotAllowedException e) {
// We should never receive this exception....in theory
return null;
}
} }
private boolean removeOwner(JID jid) { private boolean removeOwner(JID jid) {
...@@ -1491,15 +1503,10 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -1491,15 +1503,10 @@ public class LocalMUCRoom implements MUCRoom {
} }
// Update other cluster nodes with new affiliation // Update other cluster nodes with new affiliation
CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.admin)); CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.admin));
// Update the presence with the new affiliation and inform all occupants
try { // apply the affiliation change, assigning a new affiliation
return changeOccupantAffiliation(sendRole, jid, MUCRole.Affiliation.admin, // based on the group(s) of the affected user(s)
MUCRole.Role.moderator); return applyAffiliationChange(getRole(), bareJID, null);
}
catch (NotAllowedException e) {
// We should never receive this exception....in theory
return null;
}
} }
private boolean removeAdmin(JID bareJID) { private boolean removeAdmin(JID bareJID) {
...@@ -1537,6 +1544,11 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -1537,6 +1544,11 @@ public class LocalMUCRoom implements MUCRoom {
if (owners.contains(bareJID) && owners.size() == 1) { if (owners.contains(bareJID) && owners.size() == 1) {
throw new ConflictException(); throw new ConflictException();
} }
// Check if user is already an member
if (members.containsKey(bareJID)) {
// Do nothing
return Collections.emptyList();
}
// Associate the reserved nickname with the bareJID. If nickname is null then associate an // Associate the reserved nickname with the bareJID. If nickname is null then associate an
// empty string // empty string
members.put(bareJID, (nickname == null ? "" : nickname.toLowerCase())); members.put(bareJID, (nickname == null ? "" : nickname.toLowerCase()));
...@@ -1563,15 +1575,10 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -1563,15 +1575,10 @@ public class LocalMUCRoom implements MUCRoom {
} }
// Update other cluster nodes with new member // Update other cluster nodes with new member
CacheFactory.doClusterTask(new AddMember(this, jid.toBareJID(), (nickname == null ? "" : nickname))); CacheFactory.doClusterTask(new AddMember(this, jid.toBareJID(), (nickname == null ? "" : nickname)));
// Update the presence with the new affiliation and inform all occupants
try { // apply the affiliation change, assigning a new affiliation
return changeOccupantAffiliation(sendRole, jid, MUCRole.Affiliation.member, // based on the group(s) of the affected user(s)
MUCRole.Role.participant); return applyAffiliationChange(getRole(), bareJID, null);
}
catch (NotAllowedException e) {
// We should never receive this exception....in theory
return null;
}
} }
private boolean removeMember(JID jid) { private boolean removeMember(JID jid) {
...@@ -1623,45 +1630,23 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -1623,45 +1630,23 @@ public class LocalMUCRoom implements MUCRoom {
} }
// Update other cluster nodes with new affiliation // Update other cluster nodes with new affiliation
CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.outcast)); CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.outcast));
// Update the presence with the new affiliation and inform all occupants
// actorJID will be null if the room itself (ie. via admin console) made the request // apply the affiliation change, assigning a new affiliation
JID actorJID = senderRole.getUserAddress(); // based on the group(s) of the affected user(s)
List<Presence> updatedPresences = changeOccupantAffiliation(senderRole, return applyAffiliationChange(senderRole, bareJID, reason);
jid,
MUCRole.Affiliation.outcast,
MUCRole.Role.none);
Element frag;
// Add the status code and reason why the user was banned to the presences that will
// be sent to the room occupants (the banned user will not receive this presences)
for (Presence presence : updatedPresences) {
frag = presence.getChildElement("x", "http://jabber.org/protocol/muc#user");
// Add the status code 301 that indicates that the user was banned
frag.addElement("status").addAttribute("code", "301");
// Add the reason why the user was banned
if (reason != null && reason.trim().length() > 0) {
frag.element("item").addElement("reason").setText(reason);
}
// Remove the banned users from the room. If a user has joined the room from
// different client resources, he/she will be kicked from all the client resources
// Effectively kick the occupant from the room
kickPresence(presence, actorJID);
}
return updatedPresences;
} }
private boolean removeOutcast(JID bareJID) { private boolean removeOutcast(JID bareJID) {
return outcasts.remove( bareJID.asBareJID() ); return outcasts.remove( bareJID.asBareJID() );
} }
public List<Presence> addNone(JID jid, MUCRole senderRole) throws ForbiddenException, public List<Presence> addNone(JID jid, MUCRole senderRole) throws ForbiddenException, ConflictException {
ConflictException {
final JID bareJID = jid.asBareJID(); final JID bareJID = jid.asBareJID();
List<Presence> updatedPresences = Collections.emptyList(); MUCRole.Affiliation oldAffiliation = MUCRole.Affiliation.none;
boolean wasMember = false; boolean jidWasAffiliated = false;
lock.writeLock().lock(); lock.writeLock().lock();
try { try {
MUCRole.Affiliation oldAffiliation = MUCRole.Affiliation.none;
if (MUCRole.Affiliation.admin != senderRole.getAffiliation() if (MUCRole.Affiliation.admin != senderRole.getAffiliation()
&& MUCRole.Affiliation.owner != senderRole.getAffiliation()) { && MUCRole.Affiliation.owner != senderRole.getAffiliation()) {
throw new ForbiddenException(); throw new ForbiddenException();
...@@ -1670,16 +1655,18 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -1670,16 +1655,18 @@ public class LocalMUCRoom implements MUCRoom {
if (owners.contains(bareJID) && owners.size() == 1) { if (owners.contains(bareJID) && owners.size() == 1) {
throw new ConflictException(); throw new ConflictException();
} }
wasMember = members.containsKey(bareJID) || admins.contains(bareJID) || owners.contains(bareJID); // Remove the jid from ALL the affiliation lists
// Remove the user from ALL the affiliation lists
if (removeOwner(bareJID)) { if (removeOwner(bareJID)) {
oldAffiliation = MUCRole.Affiliation.owner; oldAffiliation = MUCRole.Affiliation.owner;
jidWasAffiliated = true;
} }
else if (removeAdmin(bareJID)) { else if (removeAdmin(bareJID)) {
oldAffiliation = MUCRole.Affiliation.admin; oldAffiliation = MUCRole.Affiliation.admin;
jidWasAffiliated = true;
} }
else if (removeMember(bareJID)) { else if (removeMember(bareJID)) {
oldAffiliation = MUCRole.Affiliation.member; oldAffiliation = MUCRole.Affiliation.member;
jidWasAffiliated = true;
} }
else if (removeOutcast(bareJID)) { else if (removeOutcast(bareJID)) {
oldAffiliation = MUCRole.Affiliation.outcast; oldAffiliation = MUCRole.Affiliation.outcast;
...@@ -1692,46 +1679,117 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -1692,46 +1679,117 @@ public class LocalMUCRoom implements MUCRoom {
} }
// Update other cluster nodes with new affiliation // Update other cluster nodes with new affiliation
CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.none)); CacheFactory.doClusterTask(new AddAffiliation(this, jid.toBareJID(), MUCRole.Affiliation.none));
// Update the presence with the new affiliation and inform all occupants
try { if (jidWasAffiliated) {
MUCRole.Role newRole; // apply the affiliation change, assigning a new affiliation
if (isMembersOnly() && wasMember) { // based on the group(s) of the affected user(s)
return applyAffiliationChange(senderRole, bareJID, null);
} else {
// no presence updates needed
return Collections.emptyList();
}
}
/**
* Evaluate the given JID to determine what the appropriate affiliation should be
* after a change has been made. Each affected user will be granted the highest
* affiliation they now possess, either explicitly or implicitly via membership
* in one or more groups. If the JID is a user, the effective affiliation is
* applied to each presence corresponding to that user. If the given JID is a group,
* each user in the group is evaluated to determine what their new affiliations will
* be. The returned presence updates will be broadcast to the occupants of the room.
*
* @param senderRole Typically the room itself, or an owner/admin
* @param affiliationJID The JID for the user or group that has been changed
* @param reason An optional reason to explain why a user was kicked from the room
* @return List of presence updates to be delivered to the room's occupants
*/
private List<Presence> applyAffiliationChange(MUCRole senderRole, final JID affiliationJID, String reason) {
// Update the presence(s) for the new affiliation and inform all occupants
List<JID> affectedOccupants = new ArrayList<JID>();
// first, determine which actual (user) JIDs are affected by the affiliation change
if (GroupJID.isGroup(affiliationJID)) {
try {
Group group = GroupManager.getInstance().getGroup(affiliationJID);
// check each occupant to see if they are in the group that was changed
// if so, calculate a new affiliation (if any) for the occupant(s)
for (JID groupMember : group.getAll()) {
if (occupantsByBareJID.containsKey(groupMember)) {
affectedOccupants.add(groupMember);
}
}
} catch (GroupNotFoundException gnfe) {
Log.error("Error updating group presences for " + affiliationJID , gnfe);
}
} else {
if (occupantsByBareJID.containsKey(affiliationJID)) {
affectedOccupants.add(affiliationJID);
}
}
// now update each of the affected occupants with a new role/affiliation
MUCRole.Role newRole;
MUCRole.Affiliation newAffiliation;
List<Presence> updatedPresences = new ArrayList<Presence>();
// new role/affiliation may be granted via group membership
for (JID occupantJID : affectedOccupants) {
Log.info("Applying affiliation change for " + occupantJID);
boolean kickMember = false, isOutcast = false;
if (owners.includes(occupantJID)) {
newRole = MUCRole.Role.moderator;
newAffiliation = MUCRole.Affiliation.owner;
}
else if (admins.includes(occupantJID)) {
newRole = MUCRole.Role.moderator;
newAffiliation = MUCRole.Affiliation.admin;
}
// outcast trumps member when an affiliation is changed
else if (outcasts.includes(occupantJID)) {
newAffiliation = MUCRole.Affiliation.none;
newRole = MUCRole.Role.none;
kickMember = true;
isOutcast = true;
}
else if (members.includesKey(occupantJID)) {
newRole = MUCRole.Role.participant;
newAffiliation = MUCRole.Affiliation.member;
}
else if (isMembersOnly()) {
newRole = MUCRole.Role.none; newRole = MUCRole.Role.none;
newAffiliation = MUCRole.Affiliation.none;
kickMember = true;
} }
else { else {
newRole = isModerated() ? MUCRole.Role.visitor : MUCRole.Role.participant; newRole = isModerated() ? MUCRole.Role.visitor : MUCRole.Role.participant;
newAffiliation = MUCRole.Affiliation.member;
} }
updatedPresences = changeOccupantAffiliation(senderRole, bareJID, MUCRole.Affiliation.none, newRole); Log.info("New affiliation: " + newAffiliation);
if (isMembersOnly() && wasMember) { try {
// If the room is members-only, remove the user from the room including a status List<Presence> thisOccupant = changeOccupantAffiliation(senderRole, occupantJID, newAffiliation, newRole);
// code of 321 to indicate that the user was removed because of an affiliation if (isMembersOnly() && kickMember) {
// change // If the room is members-only, remove the user from the room including a status
Element frag; // code of 321 to indicate that the user was removed because of an affiliation change
// Add the status code to the presences that will be sent to the room occupants // a status code of 301 indicates the user was removed as an outcast
for (Presence presence : updatedPresences) { for (Presence presence : thisOccupant) {
// Set the presence as an unavailable presence presence.setType(Presence.Type.unavailable);
presence.setType(Presence.Type.unavailable); presence.setStatus(null);
presence.setStatus(null); Element x = presence.getChildElement("x", "http://jabber.org/protocol/muc#user");
frag = presence.getChildElement("x", "http://jabber.org/protocol/muc#user"); if (reason != null && reason.trim().length() > 0) {
// Add the status code 321 that indicates that the user was removed because of x.element("item").addElement("reason").setText(reason);
// an affiliation change }
frag.addElement("status").addAttribute("code", "321"); x.addElement("status").addAttribute("code", isOutcast ? "301" : "321");
kickPresence(presence, senderRole.getUserAddress());
// Remove the ex-member from the room. If a user has joined the room from }
// different client resources, he/she will be kicked from all the client
// resources.
// Effectively kick the occupant from the room
JID actorJID = senderRole.getUserAddress();
kickPresence(presence, actorJID);
} }
} updatedPresences.addAll(thisOccupant);
} } catch (NotAllowedException e) {
catch (NotAllowedException e) { Log.error("Error updating presences for " + occupantJID, e);
// We should never receive this exception....in theory }
Log.error(e.getMessage(), e);
} }
return updatedPresences; return updatedPresences;
} }
public boolean isLocked() { public boolean isLocked() {
return lockedTime > 0; return lockedTime > 0;
...@@ -1826,32 +1884,32 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -1826,32 +1884,32 @@ public class LocalMUCRoom implements MUCRoom {
} }
public void affiliationAdded(AddAffiliation affiliation) { public void affiliationAdded(AddAffiliation affiliation) {
JID bareJID = affiliation.getBareJID(); JID affiliationJID = affiliation.getBareJID();
switch(affiliation.getAffiliation()) { switch(affiliation.getAffiliation()) {
case owner: case owner:
removeMember(bareJID); removeMember(affiliationJID);
removeAdmin(bareJID); removeAdmin(affiliationJID);
removeOutcast(bareJID); removeOutcast(affiliationJID);
owners.add(bareJID); owners.add(affiliationJID);
break; break;
case admin: case admin:
removeMember(bareJID); removeMember(affiliationJID);
removeOwner(bareJID); removeOwner(affiliationJID);
removeOutcast(bareJID); removeOutcast(affiliationJID);
admins.add(bareJID); admins.add(affiliationJID);
break; break;
case outcast: case outcast:
removeMember(bareJID); removeMember(affiliationJID);
removeAdmin(bareJID); removeAdmin(affiliationJID);
removeOwner(bareJID); removeOwner(affiliationJID);
outcasts.add(bareJID); outcasts.add(affiliationJID);
break; break;
case none: case none:
default: default:
removeMember(bareJID); removeMember(affiliationJID);
removeAdmin(bareJID); removeAdmin(affiliationJID);
removeOwner(bareJID); removeOwner(affiliationJID);
removeOutcast(bareJID); removeOutcast(affiliationJID);
break; break;
} }
} }
...@@ -2609,4 +2667,67 @@ public class LocalMUCRoom implements MUCRoom { ...@@ -2609,4 +2667,67 @@ public class LocalMUCRoom implements MUCRoom {
return false; return false;
return true; return true;
} }
// overrides for important Group events
@Override
public void groupDeleting(Group group, Map params) {
// remove the group from this room's affiliations
GroupJID groupJID = group.getJID();
try {
addNone(groupJID, getRole());
} catch (Exception ex) {
Log.error("Failed to remove deleted group from affiliation lists: " + groupJID, ex);
}
}
@Override
public void groupModified(Group group, Map params) {
// check the affiliation lists for the old group jid, replace with a new group jid
if ("nameModified".equals(params.get("type"))) {
GroupJID originalJID = (GroupJID) params.get("originalJID");
GroupJID newJID = group.getJID();
try {
if (owners.contains(originalJID)) {
addOwner(newJID, getRole());
} else if (admins.contains(originalJID)) {
addAdmin(newJID, getRole());
} else if (outcasts.contains(originalJID)) {
addOutcast(newJID, null, getRole());
} else if (members.containsKey(originalJID)) {
addMember(newJID, null, getRole());
}
addNone(originalJID, getRole());
} catch (Exception ex) {
Log.error("Failed to update group affiliation for " + newJID, ex);
}
}
}
@Override
public void memberAdded(Group group, Map params) {
applyAffiliationChange(getRole(), new JID((String)params.get("member")), null);
}
@Override
public void memberRemoved(Group group, Map params) {
applyAffiliationChange(getRole(), new JID((String)params.get("member")), null);
}
@Override
public void adminAdded(Group group, Map params) {
applyAffiliationChange(getRole(), new JID((String)params.get("admin")), null);
}
@Override
public void adminRemoved(Group group, Map params) {
applyAffiliationChange(getRole(), new JID((String)params.get("admin")), null);
}
@Override
public void groupCreated(Group group, Map params) {
// ignore
}
} }
...@@ -31,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap; ...@@ -31,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.jivesoftware.database.DbConnectionManager; import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.PacketRouter; import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.group.GroupJID;
import org.jivesoftware.openfire.muc.*; import org.jivesoftware.openfire.muc.*;
import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils; import org.jivesoftware.util.StringUtils;
...@@ -258,22 +259,23 @@ public class MUCPersistenceManager { ...@@ -258,22 +259,23 @@ public class MUCPersistenceManager {
pstmt.setLong(1, room.getID()); pstmt.setLong(1, room.getID());
rs = pstmt.executeQuery(); rs = pstmt.executeQuery();
while (rs.next()) { while (rs.next()) {
JID jid = new JID(rs.getString(1)); // might be a group JID
JID affiliationJID = GroupJID.fromString(rs.getString(1));
MUCRole.Affiliation affiliation = MUCRole.Affiliation.valueOf(rs.getInt(2)); MUCRole.Affiliation affiliation = MUCRole.Affiliation.valueOf(rs.getInt(2));
try { try {
switch (affiliation) { switch (affiliation) {
case owner: case owner:
room.addOwner(jid, room.getRole()); room.addOwner(affiliationJID, room.getRole());
break; break;
case admin: case admin:
room.addAdmin(jid, room.getRole()); room.addAdmin(affiliationJID, room.getRole());
break; break;
case outcast: case outcast:
room.addOutcast(jid, null, room.getRole()); room.addOutcast(affiliationJID, null, room.getRole());
break; break;
default: default:
Log.error("Unkown affiliation value " + affiliation + " for user " Log.error("Unkown affiliation value " + affiliation + " for user "
+ jid.toBareJID() + " in persistent room " + room.getID()); + affiliationJID.toBareJID() + " in persistent room " + room.getID());
} }
} }
catch (Exception e) { catch (Exception e) {
...@@ -287,7 +289,7 @@ public class MUCPersistenceManager { ...@@ -287,7 +289,7 @@ public class MUCPersistenceManager {
rs = pstmt.executeQuery(); rs = pstmt.executeQuery();
while (rs.next()) { while (rs.next()) {
try { try {
room.addMember(new JID(rs.getString(1)), rs.getString(2), room.getRole()); room.addMember(new JID(rs.getString(1)), rs.getString(2), room.getRole());
} }
catch (Exception e) { catch (Exception e) {
Log.error(e.getMessage(), e); Log.error(e.getMessage(), e);
...@@ -613,9 +615,10 @@ public class MUCPersistenceManager { ...@@ -613,9 +615,10 @@ public class MUCPersistenceManager {
final MUCRole.Affiliation affiliation = MUCRole.Affiliation.valueOf(resultSet.getInt(3)); final MUCRole.Affiliation affiliation = MUCRole.Affiliation.valueOf(resultSet.getInt(3));
final String jidValue = resultSet.getString(2); final String jidValue = resultSet.getString(2);
final JID jid; final JID affiliationJID;
try { try {
jid = new JID(jidValue); // might be a group JID
affiliationJID = GroupJID.fromString(jidValue);
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
Log.warn("An illegal JID ({}) was found in the database, " Log.warn("An illegal JID ({}) was found in the database, "
+ "while trying to load all affiliations for room " + "while trying to load all affiliations for room "
...@@ -627,16 +630,16 @@ public class MUCPersistenceManager { ...@@ -627,16 +630,16 @@ public class MUCPersistenceManager {
try { try {
switch (affiliation) { switch (affiliation) {
case owner: case owner:
room.addOwner(jid, room.getRole()); room.addOwner(affiliationJID, room.getRole());
break; break;
case admin: case admin:
room.addAdmin(jid, room.getRole()); room.addAdmin(affiliationJID, room.getRole());
break; break;
case outcast: case outcast:
room.addOutcast(jid, null, room.getRole()); room.addOutcast(affiliationJID, null, room.getRole());
break; break;
default: default:
Log.error("Unknown affiliation value " + affiliation + " for user " + jid + " in persistent room " + room.getID()); Log.error("Unknown affiliation value " + affiliation + " for user " + affiliationJID + " in persistent room " + room.getID());
} }
} catch (ForbiddenException e) { } catch (ForbiddenException e) {
Log.warn("An exception prevented affiliations to be added to the room with id " + roomID, e); Log.warn("An exception prevented affiliations to be added to the room with id " + roomID, e);
...@@ -659,6 +662,7 @@ public class MUCPersistenceManager { ...@@ -659,6 +662,7 @@ public class MUCPersistenceManager {
Connection connection = null; Connection connection = null;
PreparedStatement statement = null; PreparedStatement statement = null;
ResultSet resultSet = null; ResultSet resultSet = null;
JID affiliationJID = null;
try { try {
connection = DbConnectionManager.getConnection(); connection = DbConnectionManager.getConnection();
statement = connection.prepareStatement(LOAD_ALL_MEMBERS); statement = connection.prepareStatement(LOAD_ALL_MEMBERS);
...@@ -673,7 +677,9 @@ public class MUCPersistenceManager { ...@@ -673,7 +677,9 @@ public class MUCPersistenceManager {
continue; continue;
} }
try { try {
room.addMember(new JID(resultSet.getString(2)), resultSet.getString(3), room.getRole()); // might be a group JID
affiliationJID = GroupJID.fromString(resultSet.getString(2));
room.addMember(affiliationJID, resultSet.getString(3), room.getRole());
} catch (ForbiddenException e) { } catch (ForbiddenException e) {
Log.warn("Unable to add member to room.", e); Log.warn("Unable to add member to room.", e);
} catch (ConflictException e) { } catch (ConflictException e) {
...@@ -788,7 +794,7 @@ public class MUCPersistenceManager { ...@@ -788,7 +794,7 @@ public class MUCPersistenceManager {
public static void saveAffiliationToDB(MUCRoom room, JID jid, String nickname, public static void saveAffiliationToDB(MUCRoom room, JID jid, String nickname,
MUCRole.Affiliation newAffiliation, MUCRole.Affiliation oldAffiliation) MUCRole.Affiliation newAffiliation, MUCRole.Affiliation oldAffiliation)
{ {
final String bareJID = jid.toBareJID(); final String affiliationJid = jid.toBareJID();
if (!room.isPersistent() || !room.wasSavedToDB()) { if (!room.isPersistent() || !room.wasSavedToDB()) {
return; return;
} }
...@@ -801,7 +807,7 @@ public class MUCPersistenceManager { ...@@ -801,7 +807,7 @@ public class MUCPersistenceManager {
con = DbConnectionManager.getConnection(); con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(ADD_MEMBER); pstmt = con.prepareStatement(ADD_MEMBER);
pstmt.setLong(1, room.getID()); pstmt.setLong(1, room.getID());
pstmt.setString(2, bareJID); pstmt.setString(2, affiliationJid);
pstmt.setString(3, nickname); pstmt.setString(3, nickname);
pstmt.executeUpdate(); pstmt.executeUpdate();
} }
...@@ -820,7 +826,7 @@ public class MUCPersistenceManager { ...@@ -820,7 +826,7 @@ public class MUCPersistenceManager {
con = DbConnectionManager.getConnection(); con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(ADD_AFFILIATION); pstmt = con.prepareStatement(ADD_AFFILIATION);
pstmt.setLong(1, room.getID()); pstmt.setLong(1, room.getID());
pstmt.setString(2, bareJID); pstmt.setString(2, affiliationJid);
pstmt.setInt(3, newAffiliation.getValue()); pstmt.setInt(3, newAffiliation.getValue());
pstmt.executeUpdate(); pstmt.executeUpdate();
} }
...@@ -844,7 +850,7 @@ public class MUCPersistenceManager { ...@@ -844,7 +850,7 @@ public class MUCPersistenceManager {
pstmt = con.prepareStatement(UPDATE_MEMBER); pstmt = con.prepareStatement(UPDATE_MEMBER);
pstmt.setString(1, nickname); pstmt.setString(1, nickname);
pstmt.setLong(2, room.getID()); pstmt.setLong(2, room.getID());
pstmt.setString(3, bareJID); pstmt.setString(3, affiliationJid);
pstmt.executeUpdate(); pstmt.executeUpdate();
} }
catch (SQLException sqle) { catch (SQLException sqle) {
...@@ -863,14 +869,14 @@ public class MUCPersistenceManager { ...@@ -863,14 +869,14 @@ public class MUCPersistenceManager {
con = DbConnectionManager.getTransactionConnection(); con = DbConnectionManager.getTransactionConnection();
pstmt = con.prepareStatement(DELETE_AFFILIATION); pstmt = con.prepareStatement(DELETE_AFFILIATION);
pstmt.setLong(1, room.getID()); pstmt.setLong(1, room.getID());
pstmt.setString(2, bareJID); pstmt.setString(2, affiliationJid);
pstmt.executeUpdate(); pstmt.executeUpdate();
DbConnectionManager.fastcloseStmt(pstmt); DbConnectionManager.fastcloseStmt(pstmt);
// Add them as a member. // Add them as a member.
pstmt = con.prepareStatement(ADD_MEMBER); pstmt = con.prepareStatement(ADD_MEMBER);
pstmt.setLong(1, room.getID()); pstmt.setLong(1, room.getID());
pstmt.setString(2, bareJID); pstmt.setString(2, affiliationJid);
pstmt.setString(3, nickname); pstmt.setString(3, nickname);
pstmt.executeUpdate(); pstmt.executeUpdate();
} }
...@@ -891,13 +897,13 @@ public class MUCPersistenceManager { ...@@ -891,13 +897,13 @@ public class MUCPersistenceManager {
con = DbConnectionManager.getTransactionConnection(); con = DbConnectionManager.getTransactionConnection();
pstmt = con.prepareStatement(DELETE_MEMBER); pstmt = con.prepareStatement(DELETE_MEMBER);
pstmt.setLong(1, room.getID()); pstmt.setLong(1, room.getID());
pstmt.setString(2, bareJID); pstmt.setString(2, affiliationJid);
pstmt.executeUpdate(); pstmt.executeUpdate();
DbConnectionManager.fastcloseStmt(pstmt); DbConnectionManager.fastcloseStmt(pstmt);
pstmt = con.prepareStatement(ADD_AFFILIATION); pstmt = con.prepareStatement(ADD_AFFILIATION);
pstmt.setLong(1, room.getID()); pstmt.setLong(1, room.getID());
pstmt.setString(2, bareJID); pstmt.setString(2, affiliationJid);
pstmt.setInt(3, newAffiliation.getValue()); pstmt.setInt(3, newAffiliation.getValue());
pstmt.executeUpdate(); pstmt.executeUpdate();
} }
...@@ -919,7 +925,7 @@ public class MUCPersistenceManager { ...@@ -919,7 +925,7 @@ public class MUCPersistenceManager {
pstmt = con.prepareStatement(UPDATE_AFFILIATION); pstmt = con.prepareStatement(UPDATE_AFFILIATION);
pstmt.setInt(1, newAffiliation.getValue()); pstmt.setInt(1, newAffiliation.getValue());
pstmt.setLong(2, room.getID()); pstmt.setLong(2, room.getID());
pstmt.setString(3, bareJID); pstmt.setString(3, affiliationJid);
pstmt.executeUpdate(); pstmt.executeUpdate();
} }
catch (SQLException sqle) { catch (SQLException sqle) {
...@@ -942,7 +948,7 @@ public class MUCPersistenceManager { ...@@ -942,7 +948,7 @@ public class MUCPersistenceManager {
public static void removeAffiliationFromDB(MUCRoom room, JID jid, public static void removeAffiliationFromDB(MUCRoom room, JID jid,
MUCRole.Affiliation oldAffiliation) MUCRole.Affiliation oldAffiliation)
{ {
final String bareJID = jid.toBareJID(); final String affiliationJID = jid.toBareJID();
if (room.isPersistent() && room.wasSavedToDB()) { if (room.isPersistent() && room.wasSavedToDB()) {
if (MUCRole.Affiliation.member == oldAffiliation) { if (MUCRole.Affiliation.member == oldAffiliation) {
// Remove the user from the members table // Remove the user from the members table
...@@ -952,7 +958,7 @@ public class MUCPersistenceManager { ...@@ -952,7 +958,7 @@ public class MUCPersistenceManager {
con = DbConnectionManager.getConnection(); con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_MEMBER); pstmt = con.prepareStatement(DELETE_MEMBER);
pstmt.setLong(1, room.getID()); pstmt.setLong(1, room.getID());
pstmt.setString(2, bareJID); pstmt.setString(2, affiliationJID);
pstmt.executeUpdate(); pstmt.executeUpdate();
} }
catch (SQLException sqle) { catch (SQLException sqle) {
...@@ -970,7 +976,7 @@ public class MUCPersistenceManager { ...@@ -970,7 +976,7 @@ public class MUCPersistenceManager {
con = DbConnectionManager.getConnection(); con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_AFFILIATION); pstmt = con.prepareStatement(DELETE_AFFILIATION);
pstmt.setLong(1, room.getID()); pstmt.setLong(1, room.getID());
pstmt.setString(2, bareJID); pstmt.setString(2, affiliationJID);
pstmt.executeUpdate(); pstmt.executeUpdate();
} }
catch (SQLException sqle) { catch (SQLException sqle) {
...@@ -986,9 +992,9 @@ public class MUCPersistenceManager { ...@@ -986,9 +992,9 @@ public class MUCPersistenceManager {
/** /**
* Removes the affiliation of the user from the DB if ANY room that is persistent. * Removes the affiliation of the user from the DB if ANY room that is persistent.
* *
* @param bareJID The bareJID of the user to remove his affiliation from ALL persistent rooms. * @param affiliationJID The bareJID of the user to remove his affiliation from ALL persistent rooms.
*/ */
public static void removeAffiliationFromDB(JID bareJID) public static void removeAffiliationFromDB(JID affiliationJID)
{ {
Connection con = null; Connection con = null;
PreparedStatement pstmt = null; PreparedStatement pstmt = null;
...@@ -996,13 +1002,13 @@ public class MUCPersistenceManager { ...@@ -996,13 +1002,13 @@ public class MUCPersistenceManager {
con = DbConnectionManager.getConnection(); con = DbConnectionManager.getConnection();
// Remove the user from the members table // Remove the user from the members table
pstmt = con.prepareStatement(DELETE_USER_MEMBER); pstmt = con.prepareStatement(DELETE_USER_MEMBER);
pstmt.setString(1, bareJID.toBareJID()); pstmt.setString(1, affiliationJID.toBareJID());
pstmt.executeUpdate(); pstmt.executeUpdate();
DbConnectionManager.fastcloseStmt(pstmt); DbConnectionManager.fastcloseStmt(pstmt);
// Remove the user from the generic affiliations table // Remove the user from the generic affiliations table
pstmt = con.prepareStatement(DELETE_USER_MUCAFFILIATION); pstmt = con.prepareStatement(DELETE_USER_MUCAFFILIATION);
pstmt.setString(1, bareJID.toBareJID()); pstmt.setString(1, affiliationJID.toBareJID());
pstmt.executeUpdate(); pstmt.executeUpdate();
} }
catch (SQLException sqle) { catch (SQLException sqle) {
......
...@@ -30,7 +30,6 @@ import java.util.Map; ...@@ -30,7 +30,6 @@ import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
...@@ -46,6 +45,9 @@ import org.jivesoftware.openfire.disco.DiscoItem; ...@@ -46,6 +45,9 @@ import org.jivesoftware.openfire.disco.DiscoItem;
import org.jivesoftware.openfire.disco.DiscoItemsProvider; import org.jivesoftware.openfire.disco.DiscoItemsProvider;
import org.jivesoftware.openfire.disco.DiscoServerItem; import org.jivesoftware.openfire.disco.DiscoServerItem;
import org.jivesoftware.openfire.disco.ServerItemsProvider; import org.jivesoftware.openfire.disco.ServerItemsProvider;
import org.jivesoftware.openfire.group.ConcurrentGroupList;
import org.jivesoftware.openfire.group.GroupAwareList;
import org.jivesoftware.openfire.group.GroupJID;
import org.jivesoftware.openfire.muc.HistoryStrategy; import org.jivesoftware.openfire.muc.HistoryStrategy;
import org.jivesoftware.openfire.muc.MUCEventDelegate; import org.jivesoftware.openfire.muc.MUCEventDelegate;
import org.jivesoftware.openfire.muc.MUCEventDispatcher; import org.jivesoftware.openfire.muc.MUCEventDispatcher;
...@@ -190,15 +192,15 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService ...@@ -190,15 +192,15 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
/** /**
* Bare jids of users that are allowed to create MUC rooms. An empty list means that anyone can * Bare jids of users that are allowed to create MUC rooms. An empty list means that anyone can
* create a room. * create a room. Might also include group jids.
*/ */
private List<JID> allowedToCreate = new CopyOnWriteArrayList<JID>(); private GroupAwareList<JID> allowedToCreate = new ConcurrentGroupList<JID>();
/** /**
* Bare jids of users that are system administrators of the MUC service. A sysadmin has the same * Bare jids of users that are system administrators of the MUC service. A sysadmin has the same
* permissions as a room owner. * permissions as a room owner. Might also contain group jids.
*/ */
private List<JID> sysadmins = new CopyOnWriteArrayList<JID>(); private GroupAwareList<JID> sysadmins = new ConcurrentGroupList<JID>();
/** /**
* Queue that holds the messages to log for the rooms that need to log their conversations. * Queue that holds the messages to log for the rooms that need to log their conversations.
...@@ -550,9 +552,9 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService ...@@ -550,9 +552,9 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
// The room does not exist so check for creation permissions // The room does not exist so check for creation permissions
// Room creation is always allowed for sysadmin // Room creation is always allowed for sysadmin
final JID bareJID = userjid.asBareJID(); final JID bareJID = userjid.asBareJID();
if (isRoomCreationRestricted() && !sysadmins.contains(bareJID)) { if (isRoomCreationRestricted() && !sysadmins.includes(bareJID)) {
// The room creation is only allowed for certain JIDs // The room creation is only allowed for certain JIDs
if (!allowedToCreate.contains(bareJID)) { if (!allowedToCreate.includes(bareJID)) {
// The user is not in the list of allowed JIDs to create a room so raise // The user is not in the list of allowed JIDs to create a room so raise
// an exception // an exception
throw new NotAllowedException(); throw new NotAllowedException();
...@@ -822,15 +824,27 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService ...@@ -822,15 +824,27 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
return Collections.unmodifiableCollection(sysadmins); return Collections.unmodifiableCollection(sysadmins);
} }
public boolean isSysadmin(JID bareJID) {
return sysadmins.includes(bareJID);
}
public void addSysadmins(Collection<JID> userJIDs) {
for (JID userJID : userJIDs) {
addSysadmin(userJID);
}
}
public void addSysadmin(JID userJID) { public void addSysadmin(JID userJID) {
final JID bareJID = userJID.asBareJID(); final JID bareJID = userJID.asBareJID();
sysadmins.add(bareJID); if (!sysadmins.contains(userJID)) {
sysadmins.add(bareJID);
}
// CopyOnWriteArray does not allow sorting, so do sorting in temp list. // CopyOnWriteArray does not allow sorting, so do sorting in temp list.
ArrayList<JID> tempList = new ArrayList<JID>(sysadmins); ArrayList<JID> tempList = new ArrayList<JID>(sysadmins);
Collections.sort(tempList); Collections.sort(tempList);
sysadmins = new CopyOnWriteArrayList<JID>(tempList); sysadmins = new ConcurrentGroupList<JID>(tempList);
// Update the config. // Update the config.
String[] jids = new String[sysadmins.size()]; String[] jids = new String[sysadmins.size()];
...@@ -917,7 +931,10 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService ...@@ -917,7 +931,10 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
for(JID userJID: userJIDs) { for(JID userJID: userJIDs) {
// Update the list of allowed JIDs to create MUC rooms. Since we are updating the instance // Update the list of allowed JIDs to create MUC rooms. Since we are updating the instance
// variable there is no need to restart the service // variable there is no need to restart the service
listChanged |= allowedToCreate.add(userJID); if (!allowedToCreate.contains(userJID)) {
allowedToCreate.add(userJID);
listChanged = true;
}
} }
// if nothing was added, there's nothing to update // if nothing was added, there's nothing to update
...@@ -925,7 +942,7 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService ...@@ -925,7 +942,7 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
// CopyOnWriteArray does not allow sorting, so do sorting in temp list. // CopyOnWriteArray does not allow sorting, so do sorting in temp list.
List<JID> tempList = new ArrayList<JID>(allowedToCreate); List<JID> tempList = new ArrayList<JID>(allowedToCreate);
Collections.sort(tempList); Collections.sort(tempList);
allowedToCreate = new CopyOnWriteArrayList<JID>(tempList); allowedToCreate = new ConcurrentGroupList<JID>(tempList);
// Update the config. // Update the config.
MUCPersistenceManager.setProperty(chatServiceName, "create.jid", fromCollection(allowedToCreate)); MUCPersistenceManager.setProperty(chatServiceName, "create.jid", fromCollection(allowedToCreate));
} }
...@@ -985,7 +1002,8 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService ...@@ -985,7 +1002,8 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
continue; continue;
} }
try { try {
sysadmins.add(new JID(jid.trim().toLowerCase()).asBareJID()); // could be a group jid
sysadmins.add(GroupJID.fromString(jid.trim().toLowerCase()).asBareJID());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Log.warn("The 'sysadmin.jid' property contains a value that is not a valid JID. It is ignored. Offending value: '" + jid + "'.", e); Log.warn("The 'sysadmin.jid' property contains a value that is not a valid JID. It is ignored. Offending value: '" + jid + "'.", e);
} }
...@@ -1007,7 +1025,8 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService ...@@ -1007,7 +1025,8 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
continue; continue;
} }
try { try {
allowedToCreate.add(new JID(jid.trim().toLowerCase()).asBareJID()); // could be a group jid
allowedToCreate.add(GroupJID.fromString(jid.trim().toLowerCase()).asBareJID());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Log.warn("The 'create.jid' property contains a value that is not a valid JID. It is ignored. Offending value: '" + jid + "'.", e); Log.warn("The 'create.jid' property contains a value that is not a valid JID. It is ignored. Offending value: '" + jid + "'.", e);
} }
......
...@@ -39,6 +39,7 @@ import java.util.concurrent.ConcurrentHashMap; ...@@ -39,6 +39,7 @@ import java.util.concurrent.ConcurrentHashMap;
import javax.mail.internet.AddressException; import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress; import javax.mail.internet.InternetAddress;
import org.apache.commons.codec.binary.Base32;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
...@@ -55,6 +56,9 @@ public class StringUtils { ...@@ -55,6 +56,9 @@ public class StringUtils {
private static final char[] LT_ENCODE = "&lt;".toCharArray(); private static final char[] LT_ENCODE = "&lt;".toCharArray();
private static final char[] GT_ENCODE = "&gt;".toCharArray(); private static final char[] GT_ENCODE = "&gt;".toCharArray();
// docs indicate this class is thread safe
private static Base32 Base32Hex = new Base32(true);
private StringUtils() { private StringUtils() {
// Not instantiable. // Not instantiable.
} }
...@@ -569,6 +573,58 @@ public class StringUtils { ...@@ -569,6 +573,58 @@ public class StringUtils {
return Base64.decode(data); return Base64.decode(data);
} }
/**
* Encodes a String as a base32 String using the base32hex profile.
*
* @param data a String to encode.
* @return a base32 encoded String.
*/
public static String encodeBase32(String data) {
byte[] bytes = null;
try {
bytes = data == null ? null : data.getBytes("UTF-8");
}
catch (UnsupportedEncodingException uee) {
Log.error(uee.getMessage(), uee);
}
return encodeBase32(bytes);
}
/**
* Encodes a byte array into a base32 String using the base32hex profile.
* Implementation is case-insensitive and returns encoded strings in lower case.
*
* @param data a byte array to encode.
* @return a base32 encode String.
*/
public static String encodeBase32(byte[] data) {
return data == null ? null : Base32Hex.encodeAsString(data).toLowerCase();
}
/**
* Decodes a base32 String using the base32hex profile. Implementation
* is case-insensitive and converts the given string to upper case before
* decoding.
*
* @param data a base32 encoded String to decode.
* @return the decoded String.
*/
public static byte[] decodeBase32(String data) {
return data == null ? null : Base32Hex.decode(data.toUpperCase());
}
/**
* Validates a string to ensure all its bytes are in the Base32 alphabet.
* Implementation is case-insensitive and converts the given string to
* upper case before evaluating.
*
* @param data the string to test
* @return True if the given string can be decoded using Base32
*/
public static boolean isBase32(String data) {
return data == null ? false : Base32Hex.isInAlphabet(data.toUpperCase());
}
/** /**
* Converts a line of text into an array of lower case words using a * Converts a line of text into an array of lower case words using a
* BreakIterator.wordInstance().<p> * BreakIterator.wordInstance().<p>
...@@ -1070,6 +1126,25 @@ public class StringUtils { ...@@ -1070,6 +1126,25 @@ public class StringUtils {
return collection; return collection;
} }
/**
* Returns true if the given string is in the given array.
*
* @param array
* @param item
* @return true if the array contains the item
*/
public static boolean contains(String[] array, String item) {
if (array == null || array.length == 0 || item == null) {
return false;
}
for (String anArray : array) {
if (item.equals(anArray)) {
return true;
}
}
return false;
}
/** /**
* Abbreviates a string to a specified length and then adds an ellipsis * Abbreviates a string to a specified length and then adds an ellipsis
* if the input is greater than the maxWidth. Example input: * if the input is greater than the maxWidth. Example input:
......
package org.jivesoftware.openfire.group;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.UnsupportedEncodingException;
import org.jivesoftware.util.StringUtils;
import org.junit.Test;
import org.xmpp.packet.JID;
public class GroupJIDTest {
@Test
public void testBase32Encoding() throws UnsupportedEncodingException {
String testGroupName = "Test Group (1)";
String testDomainName = "localhost";
String testBase32GroupName = StringUtils.encodeBase32(testGroupName);
// no need for JID escaping
JID testJid = new JID(testBase32GroupName, testDomainName, null);
assertEquals(testBase32GroupName, testJid.getNode());
String testDecodedGroupName = new String(StringUtils.decodeBase32(testJid.getNode()));
assertEquals(testGroupName, testDecodedGroupName);
}
@Test
public void testBase32Alphabet() {
String testABC = "ABC";
assertTrue(StringUtils.isBase32(testABC));
String test123 = "123";
assertTrue(StringUtils.isBase32(test123));
// should be case insensitve
String testabc = "abc";
assertTrue(StringUtils.isBase32(testabc));
String testXYZ = "XYZ";
assertFalse(StringUtils.isBase32(testXYZ));
}
@Test
public void testParseGroupJIDFromString() {
String testGroupName = "Test Group (2); - now with *special* =characters= too!";
JID testJid = new JID(StringUtils.encodeBase32(testGroupName), "localhost", StringUtils.hash(testGroupName));
assertTrue(GroupJID.isGroup(testJid));
assertEquals(testGroupName, ((GroupJID)GroupJID.fromString(testJid.toString())).getGroupName());
}
}
...@@ -19,12 +19,15 @@ ...@@ -19,12 +19,15 @@
--%> --%>
<%@ page import="org.jivesoftware.util.*, <%@ page import="org.jivesoftware.util.*,
org.jivesoftware.openfire.group.Group,
org.jivesoftware.openfire.group.GroupJID,
java.util.*, java.util.*,
org.xmpp.packet.*, org.xmpp.packet.*,
org.jivesoftware.openfire.muc.MultiUserChatService" org.jivesoftware.openfire.muc.MultiUserChatService"
errorPage="error.jsp" errorPage="error.jsp"
%> %>
<%@ page import="java.net.URLEncoder" %> <%@ page import="java.net.URLEncoder" %>
<%@ page import="java.net.URLDecoder" %>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %> <%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt" %> <%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt" %>
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
<% // Get parameters <% // Get parameters
String userJID = ParamUtils.getParameter(request,"userJID"); String userJID = ParamUtils.getParameter(request,"userJID");
String[] groupNames = ParamUtils.getParameters(request, "groupNames");
boolean add = request.getParameter("add") != null; boolean add = request.getParameter("add") != null;
boolean save = request.getParameter("save") != null; boolean save = request.getParameter("save") != null;
boolean success = request.getParameter("success") != null; boolean success = request.getParameter("success") != null;
...@@ -78,11 +82,29 @@ ...@@ -78,11 +82,29 @@
} }
} }
JID bareJID = null; List<JID> allowedJIDs = new ArrayList<JID>();
try { try {
if (userJID != null && userJID.trim().length() > 0) { if (userJID != null && userJID.trim().length() > 0) {
// do validation String allowedJID;
bareJID = new JID(userJID.trim()).asBareJID(); // do validation; could be a group jid
if (userJID.indexOf('@') == -1) {
String username = JID.escapeNode(userJID);
String domain = webManager.getXMPPServer().getServerInfo().getXMPPDomain();
allowedJID = username + '@' + domain;
}
else {
String username = JID.escapeNode(userJID.substring(0, userJID.indexOf('@')));
String rest = userJID.substring(userJID.indexOf('@'), userJID.length());
allowedJID = username + rest.trim();
}
allowedJIDs.add(GroupJID.fromString(allowedJID.trim()).asBareJID());
}
if (groupNames != null) {
// create a group JID for each group
for (String groupName : groupNames) {
GroupJID groupJID = new GroupJID(URLDecoder.decode(groupName, "UTF-8"));
allowedJIDs.add(groupJID);
}
} }
} catch (java.lang.IllegalArgumentException ex) { } catch (java.lang.IllegalArgumentException ex) {
errors.put("userJID","userJID"); errors.put("userJID","userJID");
...@@ -91,16 +113,16 @@ ...@@ -91,16 +113,16 @@
if (errors.size() == 0) { if (errors.size() == 0) {
// Handle an add // Handle an add
if (add) { if (add) {
mucService.addUserAllowedToCreate(bareJID); mucService.addUsersAllowedToCreate(allowedJIDs);
// Log the event // Log the event
webManager.logEvent("added MUC room creation permission to "+userJID+" for service "+mucname, null); webManager.logEvent("updated MUC room creation permissions for service "+mucname, null);
response.sendRedirect("muc-create-permission.jsp?addsuccess=true&mucname="+URLEncoder.encode(mucname, "UTF-8")); response.sendRedirect("muc-create-permission.jsp?addsuccess=true&mucname="+URLEncoder.encode(mucname, "UTF-8"));
return; return;
} }
if (delete) { if (delete) {
// Remove the user from the allowed list // Remove the user from the allowed list
mucService.removeUserAllowedToCreate(bareJID); mucService.removeUserAllowedToCreate(GroupJID.fromString(userJID));
// Log the event // Log the event
webManager.logEvent("removed MUC room creation permission from "+userJID+" for service "+mucname, null); webManager.logEvent("removed MUC room creation permission from "+userJID+" for service "+mucname, null);
// done, return // done, return
...@@ -124,7 +146,11 @@ ...@@ -124,7 +146,11 @@
<fmt:message key="groupchat.service.settings_affect" /> <b><a href="muc-service-edit-form.jsp?mucname=<%= URLEncoder.encode(mucname, "UTF-8") %>"><%= StringUtils.escapeHTMLTags(mucname) %></a></b> <fmt:message key="groupchat.service.settings_affect" /> <b><a href="muc-service-edit-form.jsp?mucname=<%= URLEncoder.encode(mucname, "UTF-8") %>"><%= StringUtils.escapeHTMLTags(mucname) %></a></b>
</p> </p>
<% if (errors.size() > 0) { %> <% if (errors.size() > 0) {
if (delete) {
userJID = null; // mask group jid on error
}
%>
<div class="jive-error"> <div class="jive-error">
<table cellpadding="0" cellspacing="0" border="0"> <table cellpadding="0" cellspacing="0" border="0">
...@@ -210,6 +236,17 @@ ...@@ -210,6 +236,17 @@
<fmt:message key="muc.create.permission.allowed_users" /> <fmt:message key="muc.create.permission.allowed_users" />
</div> </div>
<div class="jive-contentBox"> <div class="jive-contentBox">
<p>
<label for="groupJIDs"><fmt:message key="muc.create.permission.add_group" /></label><br/>
<select name="groupNames" size="6" multiple style="width:400px;font-family:verdana,arial,helvetica,sans-serif;font-size:8pt;"
onclick="this.form.openPerms[1].checked=true;" id="groupJIDs">
<% for (Group g : webManager.getGroupManager().getGroups()) { %>
<option value="<%= URLEncoder.encode(g.getName(), "UTF-8") %>"
<%= (StringUtils.contains(groupNames, g.getName()) ? "selected" : "") %>
><%= StringUtils.escapeHTMLTags(g.getName()) %></option>
<% } %>
</select>
</p>
<p> <p>
<label for="userJIDtf"><fmt:message key="muc.create.permission.add_jid" /></label> <label for="userJIDtf"><fmt:message key="muc.create.permission.add_jid" /></label>
<input type="text" name="userJID" size="30" maxlength="100" value="<%= (userJID != null ? userJID : "") %>" <input type="text" name="userJID" size="30" maxlength="100" value="<%= (userJID != null ? userJID : "") %>"
...@@ -221,7 +258,7 @@ ...@@ -221,7 +258,7 @@
<table cellpadding="0" cellspacing="0" border="0" width="100%"> <table cellpadding="0" cellspacing="0" border="0" width="100%">
<thead> <thead>
<tr> <tr>
<th width="99%">User</th> <th width="99%">User/Group</th>
<th width="1%">Remove</th> <th width="1%">Remove</th>
</tr> </tr>
</thead> </thead>
...@@ -237,10 +274,18 @@ ...@@ -237,10 +274,18 @@
<% } %> <% } %>
<% for (JID jid : mucService.getUsersAllowedToCreate()) { <% for (JID jid : mucService.getUsersAllowedToCreate()) {
boolean isGroup = GroupJID.isGroup(jid);
String jidDisplay = isGroup ? ((GroupJID)jid).getGroupName() : jid.toString();
%> %>
<tr> <tr>
<td width="99%"> <td width="99%">
<%= jid.toString() %> <% if (isGroup) { %>
<img src="images/group.gif" width="16" height="16" align="top" title="<fmt:message key="muc.create.permission.group" />" alt="<fmt:message key="muc.create.permission.group" />"/>
<% } else { %>
<img src="images/user.gif" width="16" height="16" align="top" title="<fmt:message key="muc.create.permission.user" />" alt="<fmt:message key="muc.create.permission.user" />"/>
<% } %>
<a href="<%= isGroup ? "group-edit.jsp?group=" + URLEncoder.encode(jidDisplay) : "user-properties.jsp?username=" + URLEncoder.encode(jid.getNode()) %>">
<%= jidDisplay %></a>
</td> </td>
<td width="1%" align="center"> <td width="1%" align="center">
<a href="muc-create-permission.jsp?userJID=<%= jid.toString() %>&delete=true&mucname=<%= URLEncoder.encode(mucname, "UTF-8") %>" <a href="muc-create-permission.jsp?userJID=<%= jid.toString() %>&delete=true&mucname=<%= URLEncoder.encode(mucname, "UTF-8") %>"
...@@ -263,3 +308,4 @@ ...@@ -263,3 +308,4 @@
</body> </body>
</html> </html>
...@@ -22,6 +22,9 @@ ...@@ -22,6 +22,9 @@
org.jivesoftware.openfire.muc.ConflictException, org.jivesoftware.openfire.muc.ConflictException,
org.jivesoftware.openfire.muc.MUCRoom, org.jivesoftware.openfire.muc.MUCRoom,
org.jivesoftware.openfire.muc.NotAllowedException, org.jivesoftware.openfire.muc.NotAllowedException,
org.jivesoftware.openfire.group.Group,
org.jivesoftware.openfire.group.GroupJID,
org.jivesoftware.openfire.group.GroupManager,
org.jivesoftware.util.ParamUtils, org.jivesoftware.util.ParamUtils,
org.jivesoftware.util.StringUtils, org.jivesoftware.util.StringUtils,
org.xmpp.packet.IQ" org.xmpp.packet.IQ"
...@@ -29,6 +32,7 @@ ...@@ -29,6 +32,7 @@
%> %>
<%@ page import="org.xmpp.packet.JID" %> <%@ page import="org.xmpp.packet.JID" %>
<%@ page import="java.net.URLEncoder" %> <%@ page import="java.net.URLEncoder" %>
<%@ page import="java.net.URLDecoder" %>
<%@ page import="java.util.ArrayList" %> <%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.Collections" %> <%@ page import="java.util.Collections" %>
<%@ page import="java.util.HashMap" %> <%@ page import="java.util.HashMap" %>
...@@ -45,6 +49,7 @@ ...@@ -45,6 +49,7 @@
String affiliation = ParamUtils.getParameter(request,"affiliation"); String affiliation = ParamUtils.getParameter(request,"affiliation");
String userJID = ParamUtils.getParameter(request,"userJID"); String userJID = ParamUtils.getParameter(request,"userJID");
String roomName = roomJID.getNode(); String roomName = roomJID.getNode();
String[] groupNames = ParamUtils.getParameters(request, "groupNames");
boolean add = request.getParameter("add") != null; boolean add = request.getParameter("add") != null;
boolean addsuccess = request.getParameter("addsuccess") != null; boolean addsuccess = request.getParameter("addsuccess") != null;
...@@ -64,34 +69,47 @@ ...@@ -64,34 +69,47 @@
// Handle an add // Handle an add
if (add) { if (add) {
// do validation // do validation
if (userJID == null) { if (userJID == null && groupNames == null) {
errors.put("userJID","userJID"); errors.put("userJID","userJID");
} }
if (errors.size() == 0) { if (errors.size() == 0) {
try { try {
// Escape username ArrayList<String> memberJIDs = new ArrayList<String>();
if (userJID.indexOf('@') == -1) { if (userJID != null) {
String username = JID.escapeNode(userJID); // Escape username
String domain = webManager.getXMPPServer().getServerInfo().getXMPPDomain(); if (userJID.indexOf('@') == -1) {
userJID = username + '@' + domain; String username = JID.escapeNode(userJID);
} String domain = webManager.getXMPPServer().getServerInfo().getXMPPDomain();
else { userJID = username + '@' + domain;
String username = JID.escapeNode(userJID.substring(0, userJID.indexOf('@'))); }
String rest = userJID.substring(userJID.indexOf('@'), userJID.length()); else {
userJID = username + rest.trim(); String username = JID.escapeNode(userJID.substring(0, userJID.indexOf('@')));
} String rest = userJID.substring(userJID.indexOf('@'), userJID.length());
userJID = username + rest.trim();
}
memberJIDs.add(userJID);
}
if (groupNames != null) {
// create a group JID for each group
for (String groupName : groupNames) {
GroupJID groupJID = new GroupJID(URLDecoder.decode(groupName, "UTF-8"));
memberJIDs.add(groupJID.toString());
}
}
IQ iq = new IQ(IQ.Type.set); IQ iq = new IQ(IQ.Type.set);
Element frag = iq.setChildElement("query", "http://jabber.org/protocol/muc#admin"); Element frag = iq.setChildElement("query", "http://jabber.org/protocol/muc#admin");
Element item = frag.addElement("item"); for (String memberJID : memberJIDs){
item.addAttribute("affiliation", affiliation); Element item = frag.addElement("item");
item.addAttribute("jid", userJID); item.addAttribute("affiliation", affiliation);
item.addAttribute("jid", memberJID);
}
// Send the IQ packet that will modify the room's configuration // Send the IQ packet that will modify the room's configuration
room.getIQAdminHandler().handleIQ(iq, room.getRole()); room.getIQAdminHandler().handleIQ(iq, room.getRole());
// Log the event // Log the event
webManager.logEvent("set MUC affilation to "+affiliation+" for "+userJID+" in "+roomName, null); for (String memberJID : memberJIDs) {
webManager.logEvent("set MUC affilation to "+affiliation+" for "+memberJID+" in "+roomName, null);
}
// done, return // done, return
response.sendRedirect("muc-room-affiliations.jsp?addsuccess=true&roomJID="+URLEncoder.encode(roomJID.toBareJID(), "UTF-8")); response.sendRedirect("muc-room-affiliations.jsp?addsuccess=true&roomJID="+URLEncoder.encode(roomJID.toBareJID(), "UTF-8"));
return; return;
...@@ -103,7 +121,7 @@ ...@@ -103,7 +121,7 @@
errors.put("NotAllowedException","NotAllowedException"); errors.put("NotAllowedException","NotAllowedException");
} }
catch (CannotBeInvitedException e) { catch (CannotBeInvitedException e) {
errors.put("CannotBeInvitedException", "CannotBeInvitedExcpetion"); errors.put("CannotBeInvitedException", "CannotBeInvitedException");
} }
} }
} }
...@@ -126,8 +144,9 @@ ...@@ -126,8 +144,9 @@
errors.put("ConflictException","ConflictException"); errors.put("ConflictException","ConflictException");
} }
catch (CannotBeInvitedException e) { catch (CannotBeInvitedException e) {
errors.put("CannotBeInvitedException", "CannotBeInvitedExcpetion"); errors.put("CannotBeInvitedException", "CannotBeInvitedException");
} }
userJID = null; // hide group/user JID
} }
%> %>
...@@ -201,8 +220,18 @@ ...@@ -201,8 +220,18 @@
<legend><fmt:message key="muc.room.affiliations.permission" /></legend> <legend><fmt:message key="muc.room.affiliations.permission" /></legend>
<div> <div>
<p> <p>
<label for="groupJIDs"><fmt:message key="muc.room.affiliations.add_group" /></label><br/>
<select name="groupNames" size="6" multiple style="width:400px;font-family:verdana,arial,helvetica,sans-serif;font-size:8pt;" id="groupJIDs">
<% for (Group g : webManager.getGroupManager().getGroups()) { %>
<option value="<%= URLEncoder.encode(g.getName(), "UTF-8") %>"
<%= (StringUtils.contains(groupNames, g.getName()) ? "selected" : "") %>
><%= StringUtils.escapeHTMLTags(g.getName()) %></option>
<% } %>
</select>
</p>
<p>
<label for="memberJID"><fmt:message key="muc.room.affiliations.add_jid" /></label> <label for="memberJID"><fmt:message key="muc.room.affiliations.add_jid" /></label>
<input type="text" name="userJID" size="30" maxlength="100" value="<%= (userJID != null ? userJID : "") %>" id="memberJID"> <input type="text" name="userJID" size="30" maxlength="255" value="<%= (userJID != null ? userJID : "") %>" id="memberJID">
<select name="affiliation"> <select name="affiliation">
<option value="owner"><fmt:message key="muc.room.affiliations.owner" /></option> <option value="owner"><fmt:message key="muc.room.affiliations.owner" /></option>
<option value="admin"><fmt:message key="muc.room.affiliations.admin" /></option> <option value="admin"><fmt:message key="muc.room.affiliations.admin" /></option>
...@@ -237,13 +266,20 @@ ...@@ -237,13 +266,20 @@
ArrayList<JID> owners = new ArrayList<JID>(room.getOwners()); ArrayList<JID> owners = new ArrayList<JID>(room.getOwners());
Collections.sort(owners); Collections.sort(owners);
for (JID user : owners) { for (JID user : owners) {
boolean isGroup = GroupJID.isGroup(user);
String username = JID.unescapeNode(user.getNode()); String username = JID.unescapeNode(user.getNode());
String userDisplay = username + '@' + user.getDomain(); String userDisplay = isGroup ? ((GroupJID)user).getGroupName() : username + '@' + user.getDomain();
%> %>
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td> <td>
<%= StringUtils.escapeHTMLTags(userDisplay) %> <% if (isGroup) { %>
<img src="images/group.gif" width="16" height="16" align="top" title="<fmt:message key="muc.room.affiliations.group" />" alt="<fmt:message key="muc.room.affiliations.group" />"/>
<% } else { %>
<img src="images/user.gif" width="16" height="16" align="top" title="<fmt:message key="muc.room.affiliations.user" />" alt="<fmt:message key="muc.room.affiliations.user" />"/>
<% } %>
<a href="<%= isGroup ? "group-edit.jsp?group=" + URLEncoder.encode(userDisplay) : "user-properties.jsp?username=" + URLEncoder.encode(user.getNode()) %>">
<%= StringUtils.escapeHTMLTags(userDisplay) %></a>
</td> </td>
<td width="1%" align="center"> <td width="1%" align="center">
<a href="muc-room-affiliations.jsp?roomJID=<%= URLEncoder.encode(roomJID.toBareJID(), "UTF-8") %>&userJID=<%= URLEncoder.encode(user.toString()) %>&delete=true&affiliation=owner" <a href="muc-room-affiliations.jsp?roomJID=<%= URLEncoder.encode(roomJID.toBareJID(), "UTF-8") %>&userJID=<%= URLEncoder.encode(user.toString()) %>&delete=true&affiliation=owner"
...@@ -269,13 +305,20 @@ ...@@ -269,13 +305,20 @@
ArrayList<JID> admins = new ArrayList<JID>(room.getAdmins()); ArrayList<JID> admins = new ArrayList<JID>(room.getAdmins());
Collections.sort(admins); Collections.sort(admins);
for (JID user : admins) { for (JID user : admins) {
boolean isGroup = GroupJID.isGroup(user);
String username = JID.unescapeNode(user.getNode()); String username = JID.unescapeNode(user.getNode());
String userDisplay = username + '@' + user.getDomain(); String userDisplay = isGroup ? ((GroupJID)user).getGroupName() : username + '@' + user.getDomain();
%> %>
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td> <td>
<%= StringUtils.escapeHTMLTags(userDisplay) %> <% if (isGroup) { %>
<img src="images/group.gif" width="16" height="16" align="top" title="<fmt:message key="muc.room.affiliations.group" />" alt="<fmt:message key="muc.room.affiliations.group" />"/>
<% } else { %>
<img src="images/user.gif" width="16" height="16" align="top" title="<fmt:message key="muc.room.affiliations.user" />" alt="<fmt:message key="muc.room.affiliations.user" />"/>
<% } %>
<a href="<%= isGroup ? "group-edit.jsp?group=" + URLEncoder.encode(userDisplay) : "user-properties.jsp?username=" + URLEncoder.encode(user.getNode()) %>">
<%= StringUtils.escapeHTMLTags(userDisplay) %></a>
</td> </td>
<td width="1%" align="center"> <td width="1%" align="center">
<a href="muc-room-affiliations.jsp?roomJID=<%= URLEncoder.encode(roomJID.toBareJID(), "UTF-8") %>&userJID=<%= URLEncoder.encode(user.toString()) %>&delete=true&affiliation=admin" <a href="muc-room-affiliations.jsp?roomJID=<%= URLEncoder.encode(roomJID.toBareJID(), "UTF-8") %>&userJID=<%= URLEncoder.encode(user.toString()) %>&delete=true&affiliation=admin"
...@@ -301,15 +344,22 @@ ...@@ -301,15 +344,22 @@
ArrayList<JID> members = new ArrayList<JID>(room.getMembers()); ArrayList<JID> members = new ArrayList<JID>(room.getMembers());
Collections.sort(members); Collections.sort(members);
for (JID user : members) { for (JID user : members) {
boolean isGroup = GroupJID.isGroup(user);
String username = JID.unescapeNode(user.getNode()); String username = JID.unescapeNode(user.getNode());
String userDisplay = username + '@' + user.getDomain(); String userDisplay = isGroup ? ((GroupJID)user).getGroupName() : username + '@' + user.getDomain();
String nickname = room.getReservedNickname(user); String nickname = room.getReservedNickname(user);
nickname = (nickname == null ? "" : " (" + nickname + ")"); nickname = (nickname == null ? "" : " (" + nickname + ")");
%> %>
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td> <td>
<%= StringUtils.escapeHTMLTags(userDisplay) %><%= StringUtils.escapeHTMLTags(nickname) %> <% if (isGroup) { %>
<img src="images/group.gif" width="16" height="16" align="top" title="<fmt:message key="muc.room.affiliations.group" />" alt="<fmt:message key="muc.room.affiliations.group" />"/>
<% } else { %>
<img src="images/user.gif" width="16" height="16" align="top" title="<fmt:message key="muc.room.affiliations.user" />" alt="<fmt:message key="muc.room.affiliations.user" />"/>
<% } %>
<a href="<%= isGroup ? "group-edit.jsp?group=" + URLEncoder.encode(userDisplay) : "user-properties.jsp?username=" + URLEncoder.encode(user.getNode()) %>">
<%= StringUtils.escapeHTMLTags(userDisplay) %></a><%= StringUtils.escapeHTMLTags(nickname) %>
</td> </td>
<td width="1%" align="center"> <td width="1%" align="center">
<a href="muc-room-affiliations.jsp?roomJID=<%= URLEncoder.encode(roomJID.toBareJID(), "UTF-8") %>&userJID=<%= URLEncoder.encode(user.toString()) %>&delete=true&affiliation=member" <a href="muc-room-affiliations.jsp?roomJID=<%= URLEncoder.encode(roomJID.toBareJID(), "UTF-8") %>&userJID=<%= URLEncoder.encode(user.toString()) %>&delete=true&affiliation=member"
...@@ -335,13 +385,20 @@ ...@@ -335,13 +385,20 @@
ArrayList<JID> outcasts = new ArrayList<JID>(room.getOutcasts()); ArrayList<JID> outcasts = new ArrayList<JID>(room.getOutcasts());
Collections.sort(outcasts); Collections.sort(outcasts);
for (JID user : outcasts) { for (JID user : outcasts) {
boolean isGroup = GroupJID.isGroup(user);
String username = JID.unescapeNode(user.getNode()); String username = JID.unescapeNode(user.getNode());
String userDisplay = username + '@' + user.getDomain(); String userDisplay = isGroup ? ((GroupJID)user).getGroupName() : username + '@' + user.getDomain();
%> %>
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td> <td>
<%= StringUtils.escapeHTMLTags(userDisplay) %> <% if (isGroup) { %>
<img src="images/group.gif" width="16" height="16" align="top" title="<fmt:message key="muc.room.affiliations.group" />" alt="<fmt:message key="muc.room.affiliations.group" />"/>
<% } else { %>
<img src="images/user.gif" width="16" height="16" align="top" title="<fmt:message key="muc.room.affiliations.user" />" alt="<fmt:message key="muc.room.affiliations.user" />"/>
<% } %>
<a href="<%= isGroup ? "group-edit.jsp?group=" + URLEncoder.encode(userDisplay) : "user-properties.jsp?username=" + URLEncoder.encode(user.getNode()) %>">
<%= StringUtils.escapeHTMLTags(userDisplay) %></a>
</td> </td>
<td width="1%" align="center"> <td width="1%" align="center">
<a href="muc-room-affiliations.jsp?roomJID=<%= URLEncoder.encode(roomJID.toBareJID(), "UTF-8") %>&userJID=<%= URLEncoder.encode(user.toString()) %>&delete=true&affiliation=outcast" <a href="muc-room-affiliations.jsp?roomJID=<%= URLEncoder.encode(roomJID.toBareJID(), "UTF-8") %>&userJID=<%= URLEncoder.encode(user.toString()) %>&delete=true&affiliation=outcast"
......
...@@ -18,12 +18,15 @@ ...@@ -18,12 +18,15 @@
--%> --%>
<%@ page import="org.jivesoftware.util.*, <%@ page import="org.jivesoftware.util.*,
org.jivesoftware.openfire.group.Group,
org.jivesoftware.openfire.group.GroupJID,
java.util.*, java.util.*,
org.xmpp.packet.*, org.xmpp.packet.*,
org.jivesoftware.openfire.muc.MultiUserChatService" org.jivesoftware.openfire.muc.MultiUserChatService"
errorPage="error.jsp" errorPage="error.jsp"
%> %>
<%@ page import="java.net.URLEncoder" %> <%@ page import="java.net.URLEncoder" %>
<%@ page import="java.net.URLDecoder" %>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %> <%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jstl/fmt" prefix="fmt"%> <%@ taglib uri="http://java.sun.com/jstl/fmt" prefix="fmt"%>
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
<% // Get parameters <% // Get parameters
String userJID = ParamUtils.getParameter(request,"userJID"); String userJID = ParamUtils.getParameter(request,"userJID");
String[] groupNames = ParamUtils.getParameters(request, "groupNames");
boolean add = request.getParameter("add") != null; boolean add = request.getParameter("add") != null;
boolean delete = ParamUtils.getBooleanParameter(request,"delete"); boolean delete = ParamUtils.getBooleanParameter(request,"delete");
String mucname = ParamUtils.getParameter(request,"mucname"); String mucname = ParamUtils.getParameter(request,"mucname");
...@@ -48,26 +52,46 @@ ...@@ -48,26 +52,46 @@
// Handle a save // Handle a save
Map<String,String> errors = new HashMap<String,String>(); Map<String,String> errors = new HashMap<String,String>();
JID bareJID = null; List<JID> allowedJIDs = new ArrayList<JID>();
try { try {
// do validation if (userJID != null && userJID.trim().length() > 0) {
bareJID = new JID(userJID).asBareJID(); String allowedJID;
} catch (IllegalArgumentException e) { // do validation; could be a group jid
if (userJID.indexOf('@') == -1) {
String username = JID.escapeNode(userJID);
String domain = webManager.getXMPPServer().getServerInfo().getXMPPDomain();
allowedJID = username + '@' + domain;
}
else {
String username = JID.escapeNode(userJID.substring(0, userJID.indexOf('@')));
String rest = userJID.substring(userJID.indexOf('@'), userJID.length());
allowedJID = username + rest.trim();
}
allowedJIDs.add(GroupJID.fromString(allowedJID.trim()).asBareJID());
}
if (groupNames != null) {
// create a group JID for each group
for (String groupName : groupNames) {
GroupJID groupJID = new GroupJID(URLDecoder.decode(groupName, "UTF-8"));
allowedJIDs.add(groupJID);
}
}
} catch (java.lang.IllegalArgumentException ex) {
errors.put("userJID","userJID"); errors.put("userJID","userJID");
} }
if (errors.size() == 0) { if (errors.size() == 0) {
if (add) { if (add) {
mucService.addSysadmin(bareJID); mucService.addSysadmins(allowedJIDs);
// Log the event // Log the event
webManager.logEvent("added muc sysadmin "+userJID+" for service "+mucname, null); webManager.logEvent("added muc sysadmin permissions for service "+mucname, null);
response.sendRedirect("muc-sysadmins.jsp?addsuccess=true&mucname="+URLEncoder.encode(mucname, "UTF-8")); response.sendRedirect("muc-sysadmins.jsp?addsuccess=true&mucname="+URLEncoder.encode(mucname, "UTF-8"));
return; return;
} }
if (delete) { if (delete) {
// Remove the user from the list of system administrators // Remove the user from the list of system administrators
mucService.removeSysadmin(bareJID); mucService.removeSysadmin(GroupJID.fromString(userJID));
// Log the event // Log the event
webManager.logEvent("removed muc sysadmin "+userJID+" for service "+mucname, null); webManager.logEvent("removed muc sysadmin "+userJID+" for service "+mucname, null);
// done, return // done, return
...@@ -117,7 +141,11 @@ ...@@ -117,7 +141,11 @@
</table> </table>
</div><br> </div><br>
<% } else if (errors.size() > 0) { %> <% } else if (errors.size() > 0) {
if (delete) {
userJID = null; // mask group jid on error
}
%>
<div class="jive-error"> <div class="jive-error">
<table cellpadding="0" cellspacing="0" border="0"> <table cellpadding="0" cellspacing="0" border="0">
...@@ -140,6 +168,16 @@ ...@@ -140,6 +168,16 @@
<fmt:message key="groupchat.admins.legend" /> <fmt:message key="groupchat.admins.legend" />
</div> </div>
<div class="jive-contentBox"> <div class="jive-contentBox">
<p>
<label for="groupJIDs"><fmt:message key="groupchat.admins.add_group" /></label><br/>
<select name="groupNames" size="6" multiple style="width:400px;font-family:verdana,arial,helvetica,sans-serif;font-size:8pt;" id="groupJIDs">
<% for (Group g : webManager.getGroupManager().getGroups()) { %>
<option value="<%= URLEncoder.encode(g.getName(), "UTF-8") %>"
<%= (StringUtils.contains(groupNames, g.getName()) ? "selected" : "") %>
><%= StringUtils.escapeHTMLTags(g.getName()) %></option>
<% } %>
</select>
</p>
<label for="userJIDtf"><fmt:message key="groupchat.admins.label_add_admin" /></label> <label for="userJIDtf"><fmt:message key="groupchat.admins.label_add_admin" /></label>
<input type="text" name="userJID" size="30" maxlength="100" value="<%= (userJID != null ? StringUtils.escapeForXML(userJID) : "") %>" <input type="text" name="userJID" size="30" maxlength="100" value="<%= (userJID != null ? StringUtils.escapeForXML(userJID) : "") %>"
id="userJIDtf"> id="userJIDtf">
...@@ -165,16 +203,23 @@ ...@@ -165,16 +203,23 @@
<% } %> <% } %>
<% for (JID user : mucService.getSysadmins()) { <% for (JID jid : mucService.getSysadmins()) {
String username = JID.unescapeNode(user.getNode()); boolean isGroup = GroupJID.isGroup(jid);
String userDisplay = username + '@' + user.getDomain(); String jidDisplay = isGroup ? ((GroupJID)jid).getGroupName() : jid.toString();
%> %>
<tr> <tr>
<td width="99%"> <td width="99%">
<%= StringUtils.escapeHTMLTags(userDisplay) %> <% if (isGroup) { %>
<img src="images/group.gif" width="16" height="16" align="top" title="<fmt:message key="groupchat.admins.group" />" alt="<fmt:message key="groupchat.admins.group" />"/>
<% } else { %>
<img src="images/user.gif" width="16" height="16" align="top" title="<fmt:message key="groupchat.admins.user" />" alt="<fmt:message key="groupchat.admins.user" />"/>
<% } %>
<a href="<%= isGroup ? "group-edit.jsp?group=" + URLEncoder.encode(jidDisplay) : "user-properties.jsp?username=" + URLEncoder.encode(jid.getNode()) %>">
<%= jidDisplay %></a>
</td>
</td> </td>
<td width="1%" align="center"> <td width="1%" align="center">
<a href="muc-sysadmins.jsp?userJID=<%= URLEncoder.encode(user.toString()) %>&delete=true&mucname=<%= URLEncoder.encode(mucname, "UTF-8") %>" <a href="muc-sysadmins.jsp?userJID=<%= URLEncoder.encode(jid.toString()) %>&delete=true&mucname=<%= URLEncoder.encode(mucname, "UTF-8") %>"
title="<fmt:message key="groupchat.admins.dialog.title" />" title="<fmt:message key="groupchat.admins.dialog.title" />"
onclick="return confirm('<fmt:message key="groupchat.admins.dialog.text" />');" onclick="return confirm('<fmt:message key="groupchat.admins.dialog.text" />');"
><img src="images/delete-16x16.gif" width="16" height="16" border="0" alt=""></a> ><img src="images/delete-16x16.gif" width="16" height="16" border="0" alt=""></a>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment