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);
......
/** /**
* $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
......
...@@ -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>
This diff is collapsed.
...@@ -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