Commit 822abb01 authored by Tom Evans's avatar Tom Evans

OF-179: Add group features for MUC administration

Introduces MUC group affiliation feature; allow users to join rooms
based
on group membership; distribute presence updates for MUC affiliation
changes due to group membership events.
parent d551da38
......@@ -11,7 +11,7 @@ commons-lang.jar | 2.3
commons-logging.jar | Jetty 5.1.10 | Apache 2.0
commons-el.jar | Jetty 6.0.1 (1.0) | 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)
concurrentlinkedhashmap-lru | concurrentlinkedhashmap-lru-1.0_jdk5 | Apache 2.0
dbutil.jar | Jive Code, no release version. | GPL
......
......@@ -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-occupants=Room Occupants
sidebar.muc-room-occupants.descr=Click to view the room's occupants
sidebar.muc-room-affiliations=User Permissions
sidebar.muc-room-affiliations.descr=Click to edit user permissions
sidebar.muc-room-affiliations=Access Controls
sidebar.muc-room-affiliations.descr=Click to edit room access controls (affiliations)
sidebar.muc-room-delete=Delete Room
sidebar.muc-room-delete.descr=Click to delete the room
sidebar.muc-room-create=Create New Room
......@@ -932,17 +932,20 @@ groupchat.history.settings.save=Save Settings
groupchat.admins.title=Group Chat Administrators
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.
groupchat.admins.user_added=User added to the list successfully.
groupchat.admins.error_adding=Error adding the user. Please verify the JID is correct.
groupchat.admins.user_removed=User removed from the list successfully.
groupchat.admins.user_added=User/Group added to the list successfully.
groupchat.admins.error_adding=Error adding the user/group. Please verify the JID is correct.
groupchat.admins.user_removed=User/Group removed from the list successfully.
groupchat.admins.legend=Administrators
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.add=Add
groupchat.admins.no_admins=No administrators specified, use the form above to add one.
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
......@@ -1172,18 +1175,21 @@ logviewer.enabled=Enabled
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.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.add_user=User added successfully.
muc.create.permission.user_removed=User removed successfully.
muc.create.permission.add_user=User/Group added successfully.
muc.create.permission.user_removed=User/Group removed successfully.
muc.create.permission.policy=Permission Policy
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.allowed_users=Allowed Users
muc.create.permission.specific_created=Only specific users/groups can create a chat room.
muc.create.permission.allowed_users=Allowed Users/Groups
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.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
......@@ -1206,29 +1212,33 @@ muc.default.settings.update=Settings updated successfully.
# 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 \
the the room
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 \
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 \
banned from the room.
muc.room.affiliations.error_removing_user=Error removing the user. The room must have at least one owner.
muc.room.affiliations.error_banning_user=Error banning the user. Owners or Administratos cannot be banned.
muc.room.affiliations.error_adding_user=Error adding the user. Please verify the JID is correct.
muc.room.affiliations.user_added=User added successfully.
muc.room.affiliations.user_removed=User removed successfully.
muc.room.affiliations.permission=User Permissions
the room when it is configured as members-only. Outcasts are users who have been \
banned from the room.</p><p>Select one or more groups, or optionally provide a user JID using \
the form below. After selecting the appropriate affiliation (Owner/Admin/Member/Outcast), \
click the &quot;Add&quot; button to apply the change.
muc.room.affiliations.error_removing_user=Error removing the user/group. The room must have at least one owner.
muc.room.affiliations.error_banning_user=Error banning the user/group. Owners or Administratos cannot be banned.
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.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_group=Select Group (one or more):
muc.room.affiliations.owner=Owner
muc.room.affiliations.admin=Admin
muc.room.affiliations.member=Member
muc.room.affiliations.outcast=Outcast
muc.room.affiliations.user=User
muc.room.affiliations.group=Group
muc.room.affiliations.room_owner=Room Owners
muc.room.affiliations.no_users=No Users
muc.room.affiliations.confirm_removed=Are you sure you want to remove this user from the list?
muc.room.affiliations.no_users=No Users or Groups
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_member=Room Members
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;
import java.io.ObjectOutput;
import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
......@@ -34,11 +35,11 @@ import java.util.Set;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.util.PersistableMap;
import org.jivesoftware.util.cache.CacheSizes;
import org.jivesoftware.util.cache.Cacheable;
import org.jivesoftware.util.cache.CannotCalculateSizeException;
import org.jivesoftware.util.cache.ExternalizableUtil;
import org.jivesoftware.util.PersistableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
......@@ -61,6 +62,7 @@ public class Group implements Cacheable, Externalizable {
private transient GroupProvider provider;
private transient GroupManager groupManager;
private transient PersistableMap<String, String> properties;
private transient GroupJID jid;
private String name;
private String description;
......@@ -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'.
*
......@@ -155,13 +174,16 @@ public class Group implements Cacheable, Externalizable {
}
try {
String originalName = this.name;
GroupJID originalJID = getJID();
provider.setName(originalName, name);
this.name = name;
this.jid = null; // rebuilt when needed
// Fire event.
Map<String, Object> params = new HashMap<String, Object>();
params.put("type", "nameModified");
params.put("originalValue", originalName);
params.put("originalJID", originalJID);
GroupEventDispatcher.dispatchEvent(this, GroupEventDispatcher.EventType.group_modified,
params);
}
......@@ -232,6 +254,17 @@ public class Group implements Cacheable, Externalizable {
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.
*
......@@ -471,4 +504,41 @@ public class Group implements Cacheable, Externalizable {
ExternalizableUtil.getInstance().readSerializableCollection(in, members, 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;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import org.jivesoftware.openfire.XMPPServer;
......@@ -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.
*
......@@ -313,22 +324,24 @@ public class GroupManager {
* Returns a Group by name.
*
* @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
* @throws GroupNotFoundException if the group does not exist.
*/
public Group getGroup(String name, boolean forceLookup) throws GroupNotFoundException {
Group group = null;
if (!forceLookup) {
if (forceLookup) {
groupCache.remove(name);
} else {
group = groupCache.get(name);
}
// If ID wan't found in cache, load it up and put it there.
if (group == null) {
synchronized (name.intern()) {
group = groupCache.get(name);
// If group wan't found in cache, load it up and put it there.
if (group == null) {
group = provider.getGroup(name);
groupCache.put(name, group);
group = provider.getGroup(name);
groupCache.put(name, group);
}
}
}
......
......@@ -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
* the same permissions as a room owner.
*
* @return a list of bare JIDs.
* @return a list of user/group JIDs.
*/
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
* 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);
/**
* 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.
*
* @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);
......@@ -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
* anyone can create a room.
*
* @return a list of bare JIDs.
* @return a list of user/group JIDs.
*/
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);
/**
* Adds new users to the list of JIDs that are allowed to create MUC rooms.
* @param userJIDs collection of bare JIDs if users to add to list.
* Adds new users/groups to the list of JIDs that are allowed to create MUC rooms.
* @param userJIDs collection of JIDs for users/groups to add to list.
*/
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);
/**
* 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);
......
/**
* $RCSfile$
* $Revision: 1623 $
* $Date: 2005-07-12 18:40:57 -0300 (Tue, 12 Jul 2005) $
*
......@@ -22,20 +21,21 @@ package org.jivesoftware.openfire.muc.spi;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
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.ConflictException;
import org.jivesoftware.openfire.muc.ForbiddenException;
import org.jivesoftware.openfire.muc.MUCRole;
import org.jivesoftware.openfire.muc.cluster.RoomUpdatedEvent;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.cache.CacheFactory;
......@@ -223,7 +223,8 @@ public class IQOwnerHandler {
// XEP-0045: "Affiliations are granted, revoked, and
// maintained based on the user's bare JID, (...)"
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 {
// XEP-0045: "Affiliations are granted, revoked, and
// maintained based on the user's bare JID, (...)"
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()) {
throw new ConflictException();
}
......@@ -394,7 +396,10 @@ public class IQOwnerHandler {
ownersToRemove.removeAll(admins);
ownersToRemove.removeAll(owners);
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 {
adminsToRemove.removeAll(admins);
adminsToRemove.removeAll(owners);
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 {
field = configurationForm.getField("muc#roomconfig_roomadmins");
field.clearValues();
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.clearValues();
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
......
......@@ -173,8 +173,8 @@ public class LocalMUCRole implements MUCRole {
throw new NotAllowedException();
}
}
// A moderator cannot be kicked from a room
if (MUCRole.Role.moderator == role && MUCRole.Role.none == newRole) {
// 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 && MUCRole.Affiliation.none != affiliation) {
throw new NotAllowedException();
}
// 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;
import java.util.Queue;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
......@@ -46,6 +45,9 @@ import org.jivesoftware.openfire.disco.DiscoItem;
import org.jivesoftware.openfire.disco.DiscoItemsProvider;
import org.jivesoftware.openfire.disco.DiscoServerItem;
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.MUCEventDelegate;
import org.jivesoftware.openfire.muc.MUCEventDispatcher;
......@@ -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
* 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
* 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.
......@@ -550,9 +552,9 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
// The room does not exist so check for creation permissions
// Room creation is always allowed for sysadmin
final JID bareJID = userjid.asBareJID();
if (isRoomCreationRestricted() && !sysadmins.contains(bareJID)) {
if (isRoomCreationRestricted() && !sysadmins.includes(bareJID)) {
// 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
// an exception
throw new NotAllowedException();
......@@ -822,15 +824,27 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
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) {
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.
ArrayList<JID> tempList = new ArrayList<JID>(sysadmins);
Collections.sort(tempList);
sysadmins = new CopyOnWriteArrayList<JID>(tempList);
sysadmins = new ConcurrentGroupList<JID>(tempList);
// Update the config.
String[] jids = new String[sysadmins.size()];
......@@ -917,7 +931,10 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
for(JID userJID: userJIDs) {
// 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
listChanged |= allowedToCreate.add(userJID);
if (!allowedToCreate.contains(userJID)) {
allowedToCreate.add(userJID);
listChanged = true;
}
}
// if nothing was added, there's nothing to update
......@@ -925,7 +942,7 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
// CopyOnWriteArray does not allow sorting, so do sorting in temp list.
List<JID> tempList = new ArrayList<JID>(allowedToCreate);
Collections.sort(tempList);
allowedToCreate = new CopyOnWriteArrayList<JID>(tempList);
allowedToCreate = new ConcurrentGroupList<JID>(tempList);
// Update the config.
MUCPersistenceManager.setProperty(chatServiceName, "create.jid", fromCollection(allowedToCreate));
}
......@@ -985,7 +1002,8 @@ public class MultiUserChatServiceImpl implements Component, MultiUserChatService
continue;
}
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) {
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
continue;
}
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) {
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;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.apache.commons.codec.binary.Base32;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -55,6 +56,9 @@ public class StringUtils {
private static final char[] LT_ENCODE = "&lt;".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() {
// Not instantiable.
}
......@@ -569,6 +573,58 @@ public class StringUtils {
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
* BreakIterator.wordInstance().<p>
......@@ -1070,6 +1126,25 @@ public class StringUtils {
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
* 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 @@
--%>
<%@ page import="org.jivesoftware.util.*,
org.jivesoftware.openfire.group.Group,
org.jivesoftware.openfire.group.GroupJID,
java.util.*,
org.xmpp.packet.*,
org.jivesoftware.openfire.muc.MultiUserChatService"
errorPage="error.jsp"
%>
<%@ 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/fmt_rt" prefix="fmt" %>
......@@ -33,6 +36,7 @@
<% // Get parameters
String userJID = ParamUtils.getParameter(request,"userJID");
String[] groupNames = ParamUtils.getParameters(request, "groupNames");
boolean add = request.getParameter("add") != null;
boolean save = request.getParameter("save") != null;
boolean success = request.getParameter("success") != null;
......@@ -78,11 +82,29 @@
}
}
JID bareJID = null;
List<JID> allowedJIDs = new ArrayList<JID>();
try {
if (userJID != null && userJID.trim().length() > 0) {
// do validation
bareJID = new JID(userJID.trim()).asBareJID();
String allowedJID;
// 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");
......@@ -91,16 +113,16 @@
if (errors.size() == 0) {
// Handle an add
if (add) {
mucService.addUserAllowedToCreate(bareJID);
// Log the event
webManager.logEvent("added MUC room creation permission to "+userJID+" for service "+mucname, null);
response.sendRedirect("muc-create-permission.jsp?addsuccess=true&mucname="+URLEncoder.encode(mucname, "UTF-8"));
return;
mucService.addUsersAllowedToCreate(allowedJIDs);
// Log the event
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"));
return;
}
if (delete) {
// Remove the user from the allowed list
mucService.removeUserAllowedToCreate(bareJID);
mucService.removeUserAllowedToCreate(GroupJID.fromString(userJID));
// Log the event
webManager.logEvent("removed MUC room creation permission from "+userJID+" for service "+mucname, null);
// done, return
......@@ -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>
</p>
<% if (errors.size() > 0) { %>
<% if (errors.size() > 0) {
if (delete) {
userJID = null; // mask group jid on error
}
%>
<div class="jive-error">
<table cellpadding="0" cellspacing="0" border="0">
......@@ -210,6 +236,17 @@
<fmt:message key="muc.create.permission.allowed_users" />
</div>
<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>
<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 : "") %>"
......@@ -221,7 +258,7 @@
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<thead>
<tr>
<th width="99%">User</th>
<th width="99%">User/Group</th>
<th width="1%">Remove</th>
</tr>
</thead>
......@@ -237,10 +274,18 @@
<% } %>
<% for (JID jid : mucService.getUsersAllowedToCreate()) {
boolean isGroup = GroupJID.isGroup(jid);
String jidDisplay = isGroup ? ((GroupJID)jid).getGroupName() : jid.toString();
%>
<tr>
<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 width="1%" align="center">
<a href="muc-create-permission.jsp?userJID=<%= jid.toString() %>&delete=true&mucname=<%= URLEncoder.encode(mucname, "UTF-8") %>"
......@@ -263,3 +308,4 @@
</body>
</html>
This diff is collapsed.
......@@ -18,12 +18,15 @@
--%>
<%@ page import="org.jivesoftware.util.*,
org.jivesoftware.openfire.group.Group,
org.jivesoftware.openfire.group.GroupJID,
java.util.*,
org.xmpp.packet.*,
org.jivesoftware.openfire.muc.MultiUserChatService"
errorPage="error.jsp"
%>
<%@ 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/fmt" prefix="fmt"%>
......@@ -33,6 +36,7 @@
<% // Get parameters
String userJID = ParamUtils.getParameter(request,"userJID");
String[] groupNames = ParamUtils.getParameters(request, "groupNames");
boolean add = request.getParameter("add") != null;
boolean delete = ParamUtils.getBooleanParameter(request,"delete");
String mucname = ParamUtils.getParameter(request,"mucname");
......@@ -48,26 +52,46 @@
// Handle a save
Map<String,String> errors = new HashMap<String,String>();
JID bareJID = null;
List<JID> allowedJIDs = new ArrayList<JID>();
try {
// do validation
bareJID = new JID(userJID).asBareJID();
} catch (IllegalArgumentException e) {
if (userJID != null && userJID.trim().length() > 0) {
String allowedJID;
// 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");
}
if (errors.size() == 0) {
if (add) {
mucService.addSysadmin(bareJID);
mucService.addSysadmins(allowedJIDs);
// 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"));
return;
}
if (delete) {
// Remove the user from the list of system administrators
mucService.removeSysadmin(bareJID);
mucService.removeSysadmin(GroupJID.fromString(userJID));
// Log the event
webManager.logEvent("removed muc sysadmin "+userJID+" for service "+mucname, null);
// done, return
......@@ -117,7 +141,11 @@
</table>
</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">
<table cellpadding="0" cellspacing="0" border="0">
......@@ -140,6 +168,16 @@
<fmt:message key="groupchat.admins.legend" />
</div>
<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>
<input type="text" name="userJID" size="30" maxlength="100" value="<%= (userJID != null ? StringUtils.escapeForXML(userJID) : "") %>"
id="userJIDtf">
......@@ -165,16 +203,23 @@
<% } %>
<% for (JID user : mucService.getSysadmins()) {
String username = JID.unescapeNode(user.getNode());
String userDisplay = username + '@' + user.getDomain();
<% for (JID jid : mucService.getSysadmins()) {
boolean isGroup = GroupJID.isGroup(jid);
String jidDisplay = isGroup ? ((GroupJID)jid).getGroupName() : jid.toString();
%>
<tr>
<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 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" />"
onclick="return confirm('<fmt:message key="groupchat.admins.dialog.text" />');"
><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