Commit fa6beb91 authored by Guus der Kinderen's avatar Guus der Kinderen Committed by daryl herzmann

OF-210: Additional work. (#923)

* Support for Roster Versioning (without send the modifications via roster pushes)

* Roster versioning comparison clearing

* Implementation Note: This empty IQ-result is different from an empty <query/> element, thus disambiguating this usage from an empty roster.

* Avoid cache miss while updating roster

When the roster is updated via group renaming, group user adding or removing, the roster pushes only happen if there is a cache hit. If there is a cache miss (what can happen if the cache is full or if the admin cleaned up the cache) the user is not properly notified about the roster update. Thus only update rosters in memory can lead to this undesired behavior.

This commit avoids the use of the cache directly (where there can be a cache miss or a cache hit). It is using the method getRoster(username) that instantiante a new Roster in the case of a cache miss.

* Clarify the code

* OF-210: Base roster version on its hashCode.

This commit removes all fields from the Roster class that do not relate to its state
(replacing them with method variables - which seems harmless, as they're all final
singletons). This allows for an easy override of Object#hashCode() and equals().
These, in turn, are used to calculate the roster version from.

* Simplified loop

* Prevent potential NPEs.

* Log exceptions for exceptions that cannot happen.

If they cannot happen, we should scream murder if they do...

* OF-210: Roster versioning enabled by default.
parent 1494c6b2
......@@ -182,7 +182,23 @@ public class IQRosterHandler extends IQHandler implements ServerFeaturesProvider
Roster cachedRoster = userManager.getUser(sender.getNode()).getRoster();
if (IQ.Type.get == type) {
returnPacket = cachedRoster.getReset();
if (RosterManager.isRosterVersioningEnabled()) {
String clientVersion = packet.getChildElement().attributeValue("ver");
String latestVersion = String.valueOf( cachedRoster.hashCode() );
// Whether or not the roster has been modified since the version ID enumerated by the client, ...
if (!latestVersion.equals(clientVersion)) {
// ... the server MUST either return the complete roster
// (including a 'ver' attribute that signals the latest version)
returnPacket = cachedRoster.getReset();
returnPacket.getChildElement().addAttribute("ver", latestVersion );
} else {
// ... or return an empty IQ-result
returnPacket = new org.xmpp.packet.IQ();
}
} else {
returnPacket = cachedRoster.getReset();
}
returnPacket.setType(IQ.Type.result);
returnPacket.setTo(sender);
returnPacket.setID(packet.getID());
......@@ -343,4 +359,4 @@ public class IQRosterHandler extends IQHandler implements ServerFeaturesProvider
public Iterator<String> getFeatures() {
return Collections.singleton("jabber:iq:roster").iterator();
}
}
\ No newline at end of file
}
......@@ -16,27 +16,8 @@
package org.jivesoftware.openfire.roster;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.jivesoftware.database.JiveID;
import org.jivesoftware.openfire.PresenceManager;
import org.jivesoftware.openfire.RoutingTable;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.SharedGroupException;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.openfire.privacy.PrivacyList;
......@@ -56,6 +37,14 @@ import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Presence;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* <p>A roster is a list of users that the user wishes to know if they are online.</p>
* <p>Rosters are similar to buddy groups in popular IM clients. The Roster class is
......@@ -82,17 +71,7 @@ public class Roster implements Cacheable, Externalizable {
*/
protected ConcurrentMap<String, Set<String>> implicitFrom = new ConcurrentHashMap<>();
private RosterItemProvider rosterItemProvider;
private String username;
private SessionManager sessionManager;
private XMPPServer server = XMPPServer.getInstance();
private RoutingTable routingTable;
private PresenceManager presenceManager;
/**
* Note: Used only for shared groups logic.
*/
private RosterManager rosterManager;
/**
* Constructor added for Externalizable. Do not use this constructor.
......@@ -115,10 +94,8 @@ public class Roster implements Cacheable, Externalizable {
* @param username The username of the user that owns this roster
*/
Roster(String username) {
presenceManager = XMPPServer.getInstance().getPresenceManager();
rosterManager = XMPPServer.getInstance().getRosterManager();
sessionManager = SessionManager.getInstance();
routingTable = XMPPServer.getInstance().getRoutingTable();
final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager();
this.username = username;
// Get the shared groups of this user
......@@ -126,8 +103,7 @@ public class Roster implements Cacheable, Externalizable {
//Collection<Group> userGroups = GroupManager.getInstance().getGroups(getUserJID());
// Add RosterItems that belong to the personal roster
rosterItemProvider = RosterManager.getRosterItemProvider();
Iterator<RosterItem> items = rosterItemProvider.getItems(username);
Iterator<RosterItem> items = RosterManager.getRosterItemProvider().getItems(username);
while (items.hasNext()) {
RosterItem item = items.next();
// Check if the item (i.e. contact) belongs to a shared group of the user. Add the
......@@ -327,14 +303,14 @@ public class Roster implements Cacheable, Externalizable {
Collection<Group> groupsWithProp = GroupManager
.getInstance()
.search("sharedRoster.displayName", groupDisplayName);
Iterator<Group> itr = groupsWithProp.iterator();
while(itr.hasNext()) {
Group group = itr.next();
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if(showInRoster != null && !showInRoster.equals("nobody")) {
throw new SharedGroupException("Cannot add an item to a shared group");
}
}
for ( Group group : groupsWithProp )
{
String showInRoster = group.getProperties().get( "sharedRoster.showInRoster" );
if ( showInRoster != null && !showInRoster.equals( "nobody" ) )
{
throw new SharedGroupException( "Cannot add an item to a shared group" );
}
}
}
}
org.xmpp.packet.Roster roster = new org.xmpp.packet.Roster();
......@@ -348,7 +324,7 @@ public class Roster implements Cacheable, Externalizable {
// Check if we need to make the new roster item persistent
if (persistent) {
rosterItem = rosterItemProvider.createItem(username, rosterItem);
rosterItem = RosterManager.getRosterItemProvider().createItem(username, rosterItem);
}
if (push) {
......@@ -403,16 +379,17 @@ public class Roster implements Cacheable, Externalizable {
}
}
try {
rosterItemProvider.createItem(username, item);
RosterManager.getRosterItemProvider().createItem(username, item);
} catch (UserAlreadyExistsException e) {
// Do nothing. We shouldn't be here.
Log.warn( "Unexpected error while updating roster item for user '{}'!", username, e);
}
} else {
// Item is not persistent and it does not belong to a shared contact so do nothing
}
} else {
// Update the backend data store
rosterItemProvider.updateItem(username, item);
RosterManager.getRosterItemProvider().updateItem(username, item);
}
// broadcast roster update
// Do not push items with a state of "None + Pending In"
......@@ -460,19 +437,19 @@ public class Roster implements Cacheable, Externalizable {
// Cancel any existing presence subscription between the user and the contact
if (subType == RosterItem.SUB_TO || subType == RosterItem.SUB_BOTH) {
Presence presence = new Presence();
presence.setFrom(server.createJID(username, null));
presence.setFrom(XMPPServer.getInstance().createJID(username, null));
presence.setTo(itemToRemove.getJid());
presence.setType(Presence.Type.unsubscribe);
server.getPacketRouter().route(presence);
XMPPServer.getInstance().getPacketRouter().route(presence);
}
// cancel any existing presence subscription between the contact and the user
if (subType == RosterItem.SUB_FROM || subType == RosterItem.SUB_BOTH) {
Presence presence = new Presence();
presence.setFrom(server.createJID(username, null));
presence.setFrom(XMPPServer.getInstance().createJID(username, null));
presence.setTo(itemToRemove.getJid());
presence.setType(Presence.Type.unsubscribed);
server.getPacketRouter().route(presence);
XMPPServer.getInstance().getPacketRouter().route(presence);
}
// If removing the user was successful, remove the user from the subscriber list:
......@@ -483,7 +460,7 @@ public class Roster implements Cacheable, Externalizable {
// belong to shared groups won't be persistent
if (item.getID() > 0) {
// If removing the user was successful, remove the user from the backend store
rosterItemProvider.deleteItem(username, item.getID());
RosterManager.getRosterItemProvider().deleteItem(username, item.getID());
}
// Broadcast the update to the user
......@@ -503,12 +480,12 @@ public class Roster implements Cacheable, Externalizable {
if (item != null) {
implicitFrom.remove(user.toBareJID());
// If the contact being removed is not a local user then ACK unsubscription
if (!server.isLocal(user)) {
if (!XMPPServer.getInstance().isLocal(user)) {
Presence presence = new Presence();
presence.setFrom(server.createJID(username, null));
presence.setFrom(XMPPServer.getInstance().createJID(username, null));
presence.setTo(user);
presence.setType(Presence.Type.unsubscribed);
server.getPacketRouter().route(presence);
XMPPServer.getInstance().getPacketRouter().route(presence);
}
// Fire event indicating that a roster item has been deleted
RosterEventDispatcher.contactDeleted(this, item);
......@@ -583,6 +560,8 @@ public class Roster implements Cacheable, Externalizable {
* @param packet The presence packet to broadcast
*/
public void broadcastPresence(Presence packet) {
final RoutingTable routingTable = XMPPServer.getInstance().getRoutingTable();
if (routingTable == null) {
return;
}
......@@ -592,7 +571,7 @@ public class Roster implements Cacheable, Externalizable {
if (from != null) {
// Try to use the active list of the session. If none was found then try to use
// the default privacy list of the session
ClientSession session = sessionManager.getSession(from);
ClientSession session = SessionManager.getInstance().getSession(from);
if (session != null) {
list = session.getActiveList();
list = list == null ? session.getDefaultList() : list;
......@@ -646,7 +625,7 @@ public class Roster implements Cacheable, Externalizable {
}
if (from != null) {
// Broadcast presence to other user's resources
sessionManager.broadcastPresenceToOtherResources(from, packet);
SessionManager.getInstance().broadcastPresenceToOtherResources(from, packet);
}
}
......@@ -658,6 +637,7 @@ public class Roster implements Cacheable, Externalizable {
* @return the list of users that belong ONLY to a shared group of this user.
*/
private Map<JID, List<Group>> getSharedUsers(Collection<Group> sharedGroups) {
final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager();
// Get the users to process from the shared groups. Users that belong to different groups
// will have one entry in the map associated with all the groups
Map<JID, List<Group>> sharedGroupUsers = new HashMap<>();
......@@ -684,12 +664,15 @@ public class Roster implements Cacheable, Externalizable {
}
private void broadcast(org.xmpp.packet.Roster roster) {
JID recipient = server.createJID(username, null, true);
JID recipient = XMPPServer.getInstance().createJID(username, null, true);
roster.setTo(recipient);
if (sessionManager == null) {
sessionManager = SessionManager.getInstance();
// When roster versioning is enabled, the server MUST include
// the updated roster version with each roster push.
if (RosterManager.isRosterVersioningEnabled()) {
roster.getChildElement().addAttribute("ver", String.valueOf( roster.hashCode() ) );
}
sessionManager.userBroadcast(username, roster);
SessionManager.getInstance().userBroadcast(username, roster);
}
/**
......@@ -729,7 +712,8 @@ public class Roster implements Cacheable, Externalizable {
* Sends a presence probe to the probee for each connected resource of this user.
*/
private void probePresence(JID probee) {
for (ClientSession session : sessionManager.getSessions(username)) {
final PresenceManager presenceManager = XMPPServer.getInstance().getPresenceManager();
for (ClientSession session : SessionManager.getInstance().getSessions(username)) {
presenceManager.probePresence(session.getAddress(), probee);
}
}
......@@ -765,8 +749,8 @@ public class Roster implements Cacheable, Externalizable {
* @param addedUser the contact to update in the roster.
*/
void addSharedUser(Group group, JID addedUser) {
boolean newItem = false;
RosterItem item = null;
boolean newItem;
RosterItem item;
try {
// Get the RosterItem for the *local* user to add
item = getRosterItem(addedUser);
......@@ -789,6 +773,7 @@ public class Roster implements Cacheable, Externalizable {
Log.error("Group (" + group.getName() + ") includes non-existent username (" +
addedUser +
")");
return;
}
}
......@@ -806,6 +791,7 @@ public class Roster implements Cacheable, Externalizable {
sharedGroups.add(group);
// Set subscription type to BOTH if the roster user belongs to a shared group
// that is mutually visible with a shared group of the new roster item
final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager();
if (rosterManager.hasMutualVisibility(getUsername(), userGroups, addedUser, sharedGroups)) {
item.setSubStatus(RosterItem.SUB_BOTH);
}
......@@ -873,8 +859,8 @@ public class Roster implements Cacheable, Externalizable {
* @param groups the groups where the contact is a member
*/
void addSharedUser(JID addedUser, Collection<Group> groups, Group addedGroup) {
boolean newItem = false;
RosterItem item = null;
boolean newItem;
RosterItem item;
try {
// Get the RosterItem for the *local* user to add
item = getRosterItem(addedUser);
......@@ -891,12 +877,14 @@ public class Roster implements Cacheable, Externalizable {
newItem = true;
} catch (UserNotFoundException ex) {
Log.error("Couldn't find a user with username (" + addedUser + ")");
return;
}
}
// Update the subscription of the item **based on the item groups**
Collection<Group> userGroups = GroupManager.getInstance().getGroups(getUserJID());
// Set subscription type to BOTH if the roster user belongs to a shared group
// that is mutually visible with a shared group of the new roster item
final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager();
if (rosterManager.hasMutualVisibility(getUsername(), userGroups, addedUser, groups)) {
item.setSubStatus(RosterItem.SUB_BOTH);
for (Group group : groups) {
......@@ -1012,6 +1000,7 @@ public class Roster implements Cacheable, Externalizable {
sharedGroups.addAll(item.getSharedGroups());
// Set subscription type to BOTH if the roster user belongs to a shared group
// that is mutually visible with a shared group of the new roster item
final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager();
if (rosterManager.hasMutualVisibility(getUsername(), userGroups, deletedUser,
sharedGroups)) {
item.setSubStatus(RosterItem.SUB_BOTH);
......@@ -1032,13 +1021,16 @@ public class Roster implements Cacheable, Externalizable {
}
} catch (SharedGroupException e) {
// Do nothing. Checkings are disabled so this exception should never happen.
Log.error( "Unexpected error while deleting user '{}' from shared group '{}'!", deletedUser, sharedGroup, e );
} catch (UserNotFoundException e) {
// Do nothing since the contact does not exist in the user's roster. (strange case!)
Log.warn( "Unexpected error while deleting user '{}' from shared group '{}'!", deletedUser, sharedGroup, e );
}
}
void deleteSharedUser(JID deletedUser, Group deletedGroup) {
try {
final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager();
// Get the RosterItem for the *local* user to remove
RosterItem item = getRosterItem(deletedUser);
int groupSize = item.getSharedGroups().size() + item.getInvisibleSharedGroups().size();
......@@ -1098,8 +1090,10 @@ public class Roster implements Cacheable, Externalizable {
}
} catch (SharedGroupException e) {
// Do nothing. Checkings are disabled so this exception should never happen.
Log.error( "Unexpected error while deleting user '{}' from shared group '{}'!", deletedUser, deletedGroup, e);
} catch (UserNotFoundException e) {
// Do nothing since the contact does not exist in the user's roster. (strange case!)
Log.warn( "Unexpected error while deleting user '{}' from shared group '{}'!", deletedUser, deletedGroup, e);
}
}
......@@ -1119,10 +1113,11 @@ public class Roster implements Cacheable, Externalizable {
try {
// Get the RosterItem for the *local* user to add
item = getRosterItem(user);
// Brodcast to all the user resources of the updated roster item
// Broadcast to all the user resources of the updated roster item
broadcast(item, true);
} catch (UserNotFoundException e) {
// Do nothing since the contact does not exist in the user's roster. (strange case!)
Log.warn( "Unexpected error while broadcasting shared group rename for user '{}'!", user, e);
}
}
}
......@@ -1131,6 +1126,40 @@ public class Roster implements Cacheable, Externalizable {
return XMPPServer.getInstance().createJID(getUsername(), null, true);
}
@Override
public boolean equals( Object o )
{
if ( this == o )
{
return true;
}
if ( o == null || getClass() != o.getClass() )
{
return false;
}
final Roster roster = (Roster) o;
if ( !rosterItems.equals( roster.rosterItems ) )
{
return false;
}
if ( !implicitFrom.equals( roster.implicitFrom ) )
{
return false;
}
return username.equals( roster.username );
}
@Override
public int hashCode()
{
int result = rosterItems.hashCode();
result = 31 * result + implicitFrom.hashCode();
result = 31 * result + username.hashCode();
return result;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeSafeUTF(out, username);
......@@ -1140,12 +1169,6 @@ public class Roster implements Cacheable, Externalizable {
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
presenceManager = XMPPServer.getInstance().getPresenceManager();
rosterManager = XMPPServer.getInstance().getRosterManager();
sessionManager = SessionManager.getInstance();
rosterItemProvider = RosterManager.getRosterItemProvider();
routingTable = XMPPServer.getInstance().getRoutingTable();
username = ExternalizableUtil.getInstance().readSafeUTF(in);
ExternalizableUtil.getInstance().readExternalizableMap(in, rosterItems, getClass().getClassLoader());
ExternalizableUtil.getInstance().readStringsMap(in, implicitFrom);
......
/*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.openfire.roster;
import org.jivesoftware.openfire.RoutingTable;
import org.jivesoftware.openfire.SharedGroupException;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.openfire.event.GroupEventListener;
import org.jivesoftware.openfire.event.UserEventDispatcher;
import org.jivesoftware.openfire.event.UserEventListener;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
import org.xmpp.packet.Presence;
import java.util.*;
/**
* A simple service that allows components to retrieve a roster based solely on the ID
* of the owner. Users have convenience methods for obtaining a roster associated with
* the owner. However there are many components that need to retrieve the roster
* based solely on the generic ID owner key. This interface defines a service that can
* do that. This allows classes that generically manage resource for resource owners
* (such as presence updates) to generically offer their services without knowing or
* caring if the roster owner is a user, chatbot, etc.
*
* @author Iain Shigeoka
*/
public class RosterManager extends BasicModule implements GroupEventListener, UserEventListener {
private static final Logger Log = LoggerFactory.getLogger(RosterManager.class);
private Cache<String, Roster> rosterCache = null;
private XMPPServer server;
private RoutingTable routingTable;
private RosterItemProvider provider;
/**
* Returns true if the roster service is enabled. When disabled it is not possible to
* retrieve users rosters or broadcast presence packets to roster contacts.
*
* @return true if the roster service is enabled.
*/
public static boolean isRosterServiceEnabled() {
return JiveGlobals.getBooleanProperty("xmpp.client.roster.active", true);
}
public RosterManager() {
super("Roster Manager");
rosterCache = CacheFactory.createCache("Roster");
initProvider();
PropertyEventDispatcher.addListener(new PropertyEventListener() {
@Override
public void propertySet(String property, Map params) {
if (property.equals("provider.roster.className")) {
initProvider();
}
}
@Override
public void propertyDeleted(String property, Map params) {}
@Override
public void xmlPropertySet(String property, Map params) {}
@Override
public void xmlPropertyDeleted(String property, Map params) {}
});
}
/**
* Returns the roster for the given username.
*
* @param username the username to search for.
* @return the roster associated with the ID.
* @throws org.jivesoftware.openfire.user.UserNotFoundException if the ID does not correspond
* to a known entity on the server.
*/
public Roster getRoster(String username) throws UserNotFoundException {
Roster roster = rosterCache.get(username);
if (roster == null) {
// Synchronize using a unique key so that other threads loading the User
// and not the Roster cannot produce a deadlock
synchronized ((username + " ro").intern()) {
roster = rosterCache.get(username);
if (roster == null) {
// Not in cache so load a new one:
roster = new Roster(username);
rosterCache.put(username, roster);
}
}
}
return roster;
}
/**
* Removes the entire roster of a given user. This is necessary when a user
* account is being deleted from the server.
*
* @param user the user.
*/
public void deleteRoster(JID user) {
if (!server.isLocal(user)) {
// Ignore request if user is not a local user
return;
}
try {
String username = user.getNode();
// Get the roster of the deleted user
Roster roster = getRoster(username);
// Remove each roster item from the user's roster
for (RosterItem item : roster.getRosterItems()) {
try {
roster.deleteRosterItem(item.getJid(), false);
}
catch (SharedGroupException e) {
// Do nothing. We shouldn't have this exception since we disabled the checkings
}
}
// Remove the cached roster from memory
rosterCache.remove(username);
// Get the rosters that have a reference to the deleted user
Iterator<String> usernames = provider.getUsernames(user.toBareJID());
while (usernames.hasNext()) {
username = usernames.next();
try {
// Get the roster that has a reference to the deleted user
roster = getRoster(username);
// Remove the deleted user reference from this roster
roster.deleteRosterItem(user, false);
}
catch (SharedGroupException e) {
// Do nothing. We shouldn't have this exception since we disabled the checkings
}
catch (UserNotFoundException e) {
// Do nothing.
}
}
}
catch (UnsupportedOperationException | UserNotFoundException e) {
// Do nothing
}
}
/**
* Returns a collection with all the groups that the user may include in his roster. The
* following criteria will be used to select the groups: 1) Groups that are configured so that
* everybody can include in his roster, 2) Groups that are configured so that its users may
* include the group in their rosters and the user is a group user of the group and 3) User
* belongs to a Group that may see a Group that whose members may include the Group in their
* rosters.
*
* @param username the username of the user to return his shared groups.
* @return a collection with all the groups that the user may include in his roster.
*/
public Collection<Group> getSharedGroups(String username) {
Collection<Group> answer = new HashSet<>();
Collection<Group> groups = GroupManager.getInstance().getSharedGroups(username);
for (Group group : groups) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("onlyGroup".equals(showInRoster)) {
if (group.isUser(username)) {
// The user belongs to the group so add the group to the answer
answer.add(group);
}
else {
// Check if the user belongs to a group that may see this group
Collection<Group> groupList = parseGroups(group.getProperties().get("sharedRoster.groupList"));
for (Group groupInList : groupList) {
if (groupInList.isUser(username)) {
answer.add(group);
}
}
}
}
else if ("everybody".equals(showInRoster)) {
// Anyone can see this group so add the group to the answer
answer.add(group);
}
}
return answer;
}
/**
* Returns the list of shared groups whose visibility is public.
*
* @return the list of shared groups whose visibility is public.
*/
public Collection<Group> getPublicSharedGroups() {
return GroupManager.getInstance().getPublicSharedGroups();
}
/**
* Returns a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*
* @param groupNames a comma delimited string with group names.
* @return a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*/
private Collection<Group> parseGroups(String groupNames) {
Collection<Group> answer = new HashSet<>();
for (String groupName : parseGroupNames(groupNames)) {
try {
answer.add(GroupManager.getInstance().getGroup(groupName));
}
catch (GroupNotFoundException e) {
// Do nothing. Silently ignore the invalid reference to the group
}
}
return answer;
}
/**
* Returns a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*
* @param groupNames a comma delimited string with group names.
* @return a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*/
private static Collection<String> parseGroupNames(String groupNames) {
Collection<String> answer = new HashSet<>();
if (groupNames != null) {
StringTokenizer tokenizer = new StringTokenizer(groupNames, ",");
while (tokenizer.hasMoreTokens()) {
answer.add(tokenizer.nextToken());
}
}
return answer;
}
@Override
public void groupCreated(Group group, Map params) {
//Do nothing
}
@Override
public void groupDeleting(Group group, Map params) {
// Get group members
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Get users whose roster will be updated
Collection<JID> affectedUsers = getAffectedUsers(group);
// Iterate on group members and update rosters of affected users
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
}
@Override
public void groupModified(Group group, Map params) {
// Do nothing if no group property has been modified
if ("propertyDeleted".equals(params.get("type"))) {
return;
}
String keyChanged = (String) params.get("propertyKey");
String originalValue = (String) params.get("originalValue");
if ("sharedRoster.showInRoster".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.showInRoster");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Get the users of the group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Get the users whose roster will be affected
Collection<JID> affectedUsers = getAffectedUsers(group, originalValue,
group.getProperties().get("sharedRoster.groupList"));
// Remove the group members from the affected rosters
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
// Simulate that the group users has been added to the group. This will cause to push
// roster items to the "affected" users for the group users
for (JID user : users) {
groupUserAdded(group, user);
}
}
else if ("sharedRoster.groupList".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.groupList");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Get the users of the group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Get the users whose roster will be affected
Collection<JID> affectedUsers = getAffectedUsers(group,
group.getProperties().get("sharedRoster.showInRoster"), originalValue);
// Remove the group members from the affected rosters
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
// Simulate that the group users has been added to the group. This will cause to push
// roster items to the "affected" users for the group users
for (JID user : users) {
groupUserAdded(group, user);
}
}
else if ("sharedRoster.displayName".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.displayName");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Do nothing if the group is not being shown in users' rosters
if (!isSharedGroup(group)) {
return;
}
// Get all the affected users
Collection<JID> users = getAffectedUsers(group);
// Iterate on all the affected users and update their rosters
for (JID updatedUser : users) {
// Get the roster to update.
Roster roster = null;
if (server.isLocal(updatedUser)) {
roster = rosterCache.get(updatedUser.getNode());
}
if (roster != null) {
// Update the roster with the new group display name
roster.shareGroupRenamed(users);
}
}
}
}
@Override
public void initialize(XMPPServer server) {
super.initialize(server);
this.server = server;
this.routingTable = server.getRoutingTable();
RosterEventDispatcher.addListener(new RosterEventListener() {
@Override
public void rosterLoaded(Roster roster) {
// Do nothing
}
@Override
public boolean addingContact(Roster roster, RosterItem item, boolean persistent) {
// Do nothing
return true;
}
@Override
public void contactAdded(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
@Override
public void contactUpdated(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
@Override
public void contactDeleted(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
});
}
/**
* Returns true if the specified Group may be included in a user roster. The decision is made
* based on the group properties that are configurable through the Admin Console.
*
* @param group the group to check if it may be considered a shared group.
* @return true if the specified Group may be included in a user roster.
*/
public static boolean isSharedGroup(Group group) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("onlyGroup".equals(showInRoster) || "everybody".equals(showInRoster)) {
return true;
}
return false;
}
/**
* Returns true if the specified Group may be seen by all users in the system. The decision
* is made based on the group properties that are configurable through the Admin Console.
*
* @param group the group to check if it may be seen by all users in the system.
* @return true if the specified Group may be seen by all users in the system.
*/
public static boolean isPublicSharedGroup(Group group) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("everybody".equals(showInRoster)) {
return true;
}
return false;
}
@Override
public void memberAdded(Group group, Map params) {
JID addedUser = new JID((String) params.get("member"));
// Do nothing if the user was an admin that became a member
if (group.getAdmins().contains(addedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserAdded(visibleGroup, users, addedUser);
}
}
else {
groupUserAdded(group, addedUser);
}
}
@Override
public void memberRemoved(Group group, Map params) {
String member = (String) params.get("member");
if (member == null) {
return;
}
JID deletedUser = new JID(member);
// Do nothing if the user is still an admin
if (group.getAdmins().contains(deletedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserDeleted(visibleGroup, users, deletedUser);
}
}
else {
groupUserDeleted(group, deletedUser);
}
}
@Override
public void adminAdded(Group group, Map params) {
JID addedUser = new JID((String) params.get("admin"));
// Do nothing if the user was a member that became an admin
if (group.getMembers().contains(addedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserAdded(visibleGroup, users, addedUser);
}
}
else {
groupUserAdded(group, addedUser);
}
}
@Override
public void adminRemoved(Group group, Map params) {
JID deletedUser = new JID((String) params.get("admin"));
// Do nothing if the user is still a member
if (group.getMembers().contains(deletedUser)) {
return;
}
// Do nothing if the group is not being shown in group members' rosters
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserDeleted(visibleGroup, users, deletedUser);
}
}
else {
groupUserDeleted(group, deletedUser);
}
}
/**
* A new user has been created so members of public shared groups need to have
* their rosters updated. Members of public shared groups need to have a roster
* item with subscription FROM for the new user since the new user can see them.
*
* @param newUser the newly created user.
* @param params event parameters.
*/
@Override
public void userCreated(User newUser, Map<String,Object> params) {
JID newUserJID = server.createJID(newUser.getUsername(), null);
// Shared public groups that are public should have a presence subscription
// of type FROM for the new user
for (Group group : getPublicSharedGroups()) {
// Get group members of public group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Update the roster of each group member to include a subscription of type FROM
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.addSharedUser(group, newUserJID);
}
if (!server.isLocal(userToUpdate)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(newUserJID, userToUpdate, true);
}
}
}
}
@Override
public void userDeleting(User user, Map<String,Object> params) {
// Shared public groups that have a presence subscription of type FROM
// for the deleted user should no longer have a reference to the deleted user
JID userJID = server.createJID(user.getUsername(), null);
// Shared public groups that are public should have a presence subscription
// of type FROM for the new user
for (Group group : getPublicSharedGroups()) {
// Get group members of public group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Update the roster of each group member to include a subscription of type FROM
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.deleteSharedUser(group, userJID);
}
if (!server.isLocal(userToUpdate)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userJID, userToUpdate, false);
}
}
}
deleteRoster(userJID);
}
@Override
public void userModified(User user, Map<String,Object> params) {
if ("nameModified".equals(params.get("type"))) {
for (Group group : getSharedGroups(user.getUsername())) {
ArrayList<JID> groupUsers = new ArrayList<>();
groupUsers.addAll(group.getAdmins());
groupUsers.addAll(group.getMembers());
for (JID groupUser : groupUsers) {
rosterCache.remove(groupUser.getNode());
}
}
}
}
/**
* Notification that a Group user has been added. Update the group users' roster accordingly.
*
* @param group the group where the user was added.
* @param addedUser the username of the user that has been added to the group.
*/
private void groupUserAdded(Group group, JID addedUser) {
groupUserAdded(group, getAffectedUsers(group), addedUser);
}
/**
* Notification that a Group user has been added. Update the group users' roster accordingly.
*
* @param group the group where the user was added.
* @param users the users to update their rosters
* @param addedUser the username of the user that has been added to the group.
*/
private void groupUserAdded(Group group, Collection<JID> users, JID addedUser) {
// Get the roster of the added user.
Roster addedUserRoster = null;
if (server.isLocal(addedUser)) {
addedUserRoster = rosterCache.get(addedUser.getNode());
}
// Iterate on all the affected users and update their rosters
for (JID userToUpdate : users) {
if (!addedUser.equals(userToUpdate)) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.addSharedUser(group, addedUser);
}
// Check if the roster is still not in memory
if (addedUserRoster == null && server.isLocal(addedUser)) {
addedUserRoster =
rosterCache.get(addedUser.getNode());
}
// Update the roster of the newly added group user.
if (addedUserRoster != null) {
Collection<Group> groups = GroupManager.getInstance().getGroups(userToUpdate);
addedUserRoster.addSharedUser(userToUpdate, groups, group);
}
if (!server.isLocal(addedUser)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userToUpdate, addedUser, true);
}
if (!server.isLocal(userToUpdate)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(addedUser, userToUpdate, true);
}
}
}
}
/**
* Notification that a Group user has been deleted. Update the group users' roster accordingly.
*
* @param group the group from where the user was deleted.
* @param deletedUser the username of the user that has been deleted from the group.
*/
private void groupUserDeleted(Group group, JID deletedUser) {
groupUserDeleted(group, getAffectedUsers(group), deletedUser);
}
/**
* Notification that a Group user has been deleted. Update the group users' roster accordingly.
*
* @param group the group from where the user was deleted.
* @param users the users to update their rosters
* @param deletedUser the username of the user that has been deleted from the group.
*/
private void groupUserDeleted(Group group, Collection<JID> users, JID deletedUser) {
// Get the roster of the deleted user.
Roster deletedUserRoster = null;
if (server.isLocal(deletedUser)) {
deletedUserRoster = rosterCache.get(deletedUser.getNode());
}
// Iterate on all the affected users and update their rosters
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.deleteSharedUser(group, deletedUser);
}
// Check if the roster is still not in memory
if (deletedUserRoster == null && server.isLocal(deletedUser)) {
deletedUserRoster =
rosterCache.get(deletedUser.getNode());
}
// Update the roster of the newly deleted group user.
if (deletedUserRoster != null) {
deletedUserRoster.deleteSharedUser(userToUpdate, group);
}
if (!server.isLocal(deletedUser)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userToUpdate, deletedUser, false);
}
if (!server.isLocal(userToUpdate)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(deletedUser, userToUpdate, false);
}
}
}
private void sendSubscribeRequest(JID sender, JID recipient, boolean isSubscribe) {
Presence presence = new Presence();
presence.setFrom(sender);
presence.setTo(recipient);
if (isSubscribe) {
presence.setType(Presence.Type.subscribe);
}
else {
presence.setType(Presence.Type.unsubscribe);
}
routingTable.routePacket(recipient, presence, false);
}
private Collection<Group> getVisibleGroups(Group groupToCheck) {
return GroupManager.getInstance().getVisibleGroups(groupToCheck);
}
/**
* Returns true if a given group is visible to a given user. That means, if the user can
* see the group in his roster.
*
* @param group the group to check if the user can see.
* @param user the JID of the user to check if he may see the group.
* @return true if a given group is visible to a given user.
*/
public boolean isGroupVisible(Group group, JID user) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("everybody".equals(showInRoster)) {
return true;
}
else if ("onlyGroup".equals(showInRoster)) {
if (group.isUser(user)) {
return true;
}
// Check if the user belongs to a group that may see this group
Collection<Group> groupList = parseGroups(group.getProperties().get(
"sharedRoster.groupList"));
for (Group groupInList : groupList) {
if (groupInList.isUser(user)) {
return true;
}
}
}
return false;
}
/**
* Returns all the users that are related to a shared group. This is the logic that we are
* using: 1) If the group visiblity is configured as "Everybody" then all users in the system or
* all logged users in the system will be returned (configurable thorugh the "filterOffline"
* flag), 2) if the group visiblity is configured as "onlyGroup" then all the group users will
* be included in the answer and 3) if the group visiblity is configured as "onlyGroup" and
* the group allows other groups to include the group in the groups users' roster then all
* the users of the allowed groups will be included in the answer.
*/
private Collection<JID> getAffectedUsers(Group group) {
return getAffectedUsers(group, group.getProperties().get("sharedRoster.showInRoster"),
group.getProperties().get("sharedRoster.groupList"));
}
/**
* This method is similar to {@link #getAffectedUsers(Group)} except that it receives
* some group properties. The group properties are passed as parameters since the called of this
* method may want to obtain the related users of the group based in some properties values.
*
* This is useful when the group is being edited and some properties has changed and we need to
* obtain the related users of the group based on the previous group state.
*/
private Collection<JID> getAffectedUsers(Group group, String showInRoster, String groupNames) {
// Answer an empty collection if the group is not being shown in users' rosters
if (!"onlyGroup".equals(showInRoster) && !"everybody".equals(showInRoster)) {
return new ArrayList<>();
}
// Add the users of the group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Check if anyone can see this shared group
if ("everybody".equals(showInRoster)) {
// Add all users in the system
for (String username : UserManager.getInstance().getUsernames()) {
users.add(server.createJID(username, null, true));
}
// Add all logged users. We don't need to add all users in the system since only the
// logged ones will be affected.
//users.addAll(SessionManager.getInstance().getSessionUsers());
}
else {
// Add the users that may see the group
Collection<Group> groupList = parseGroups(groupNames);
for (Group groupInList : groupList) {
users.addAll(groupInList.getMembers());
users.addAll(groupInList.getAdmins());
}
}
return users;
}
Collection<JID> getSharedUsersForRoster(Group group, Roster roster) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
String groupNames = group.getProperties().get("sharedRoster.groupList");
// Answer an empty collection if the group is not being shown in users' rosters
if (!"onlyGroup".equals(showInRoster) && !"everybody".equals(showInRoster)) {
return new ArrayList<>();
}
// Add the users of the group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// If the user of the roster belongs to the shared group then we should return
// users that need to be in the roster with subscription "from"
if (group.isUser(roster.getUsername())) {
// Check if anyone can see this shared group
if ("everybody".equals(showInRoster)) {
// Add all users in the system
for (String username : UserManager.getInstance().getUsernames()) {
users.add(server.createJID(username, null, true));
}
}
else {
// Add the users that may see the group
Collection<Group> groupList = parseGroups(groupNames);
for (Group groupInList : groupList) {
users.addAll(groupInList.getMembers());
users.addAll(groupInList.getAdmins());
}
}
}
return users;
}
/**
* Returns true if a group in the first collection may mutually see a group of the
* second collection. More precisely, return true if both collections contain a public
* group (i.e. anybody can see the group) or if both collection have a group that may see
* each other and the users are members of those groups or if one group is public and the
* other group allowed the public group to see it.
*
* @param user the name of the user associated to the first collection of groups. This is always a local user.
* @param groups a collection of groups to check against the other collection of groups.
* @param otherUser the JID of the user associated to the second collection of groups.
* @param otherGroups the other collection of groups to check against the first collection.
* @return true if a group in the first collection may mutually see a group of the
* second collection.
*/
boolean hasMutualVisibility(String user, Collection<Group> groups, JID otherUser,
Collection<Group> otherGroups) {
for (Group group : groups) {
for (Group otherGroup : otherGroups) {
// Skip this groups if the users are not group users of the groups
if (!group.isUser(user) || !otherGroup.isUser(otherUser)) {
continue;
}
if (group.equals(otherGroup)) {
return true;
}
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
String otherShowInRoster = otherGroup.getProperties().get("sharedRoster.showInRoster");
// Return true if both groups are public groups (i.e. anybody can see them)
if ("everybody".equals(showInRoster) && "everybody".equals(otherShowInRoster)) {
return true;
}
else if ("onlyGroup".equals(showInRoster) && "onlyGroup".equals(otherShowInRoster)) {
String groupNames = group.getProperties().get("sharedRoster.groupList");
String otherGroupNames = otherGroup.getProperties().get("sharedRoster.groupList");
// Return true if each group may see the other group
if (groupNames != null && otherGroupNames != null) {
if (groupNames.contains(otherGroup.getName()) &&
otherGroupNames.contains(group.getName())) {
return true;
}
// Check if each shared group can be seen by a group where each user belongs
Collection<Group> groupList = parseGroups(groupNames);
Collection<Group> otherGroupList = parseGroups(otherGroupNames);
for (Group groupName : groupList) {
if (groupName.isUser(otherUser)) {
for (Group otherGroupName : otherGroupList) {
if (otherGroupName.isUser(user)) {
return true;
}
}
}
}
}
}
else if ("everybody".equals(showInRoster) && "onlyGroup".equals(otherShowInRoster)) {
// Return true if one group is public and the other group allowed the public
// group to see him
String otherGroupNames = otherGroup.getProperties().get("sharedRoster.groupList");
if (otherGroupNames != null && otherGroupNames.contains(group.getName())) {
return true;
}
}
else if ("onlyGroup".equals(showInRoster) && "everybody".equals(otherShowInRoster)) {
// Return true if one group is public and the other group allowed the public
// group to see him
String groupNames = group.getProperties().get("sharedRoster.groupList");
// Return true if each group may see the other group
if (groupNames != null && groupNames.contains(otherGroup.getName())) {
return true;
}
}
}
}
return false;
}
@Override
public void start() throws IllegalStateException {
super.start();
// Add this module as a user event listener so we can update
// rosters when users are created or deleted
UserEventDispatcher.addListener(this);
// Add the new instance as a listener of group events
GroupEventDispatcher.addListener(this);
}
@Override
public void stop() {
super.stop();
// Remove this module as a user event listener
UserEventDispatcher.removeListener(this);
// Remove this module as a listener of group events
GroupEventDispatcher.removeListener(this);
}
public static RosterItemProvider getRosterItemProvider() {
return XMPPServer.getInstance().getRosterManager().provider;
}
private void initProvider() {
JiveGlobals.migrateProperty("provider.roster.className");
String className = JiveGlobals.getProperty("provider.roster.className",
"org.jivesoftware.openfire.roster.DefaultRosterItemProvider");
if (provider == null || !className.equals(provider.getClass().getName())) {
try {
Class c = ClassUtils.forName(className);
provider = (RosterItemProvider) c.newInstance();
}
catch (Exception e) {
Log.error("Error loading roster provider: " + className, e);
provider = new DefaultRosterItemProvider();
}
}
}
}
/*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.openfire.roster;
import org.jivesoftware.openfire.RoutingTable;
import org.jivesoftware.openfire.SharedGroupException;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.openfire.event.GroupEventListener;
import org.jivesoftware.openfire.event.UserEventDispatcher;
import org.jivesoftware.openfire.event.UserEventListener;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
import org.xmpp.packet.Presence;
import java.util.*;
/**
* A simple service that allows components to retrieve a roster based solely on the ID
* of the owner. Users have convenience methods for obtaining a roster associated with
* the owner. However there are many components that need to retrieve the roster
* based solely on the generic ID owner key. This interface defines a service that can
* do that. This allows classes that generically manage resource for resource owners
* (such as presence updates) to generically offer their services without knowing or
* caring if the roster owner is a user, chatbot, etc.
*
* @author Iain Shigeoka
*/
public class RosterManager extends BasicModule implements GroupEventListener, UserEventListener {
private static final Logger Log = LoggerFactory.getLogger(RosterManager.class);
private Cache<String, Roster> rosterCache = null;
private XMPPServer server;
private RoutingTable routingTable;
private RosterItemProvider provider;
/**
* Returns true if the roster service is enabled. When disabled it is not possible to
* retrieve users rosters or broadcast presence packets to roster contacts.
*
* @return true if the roster service is enabled.
*/
public static boolean isRosterServiceEnabled() {
return JiveGlobals.getBooleanProperty("xmpp.client.roster.active", true);
}
/**
* Returns true if the roster versioning is enabled.
*
* @return true if the roster versioning is enabled.
*/
public static boolean isRosterVersioningEnabled() {
return JiveGlobals.getBooleanProperty("xmpp.client.roster.versioning.active", true);
}
public RosterManager() {
super("Roster Manager");
rosterCache = CacheFactory.createCache("Roster");
initProvider();
PropertyEventDispatcher.addListener(new PropertyEventListener() {
@Override
public void propertySet(String property, Map params) {
if (property.equals("provider.roster.className")) {
initProvider();
}
}
@Override
public void propertyDeleted(String property, Map params) {}
@Override
public void xmlPropertySet(String property, Map params) {}
@Override
public void xmlPropertyDeleted(String property, Map params) {}
});
}
/**
* Returns the roster for the given username.
*
* @param username the username to search for.
* @return the roster associated with the ID.
* @throws org.jivesoftware.openfire.user.UserNotFoundException if the ID does not correspond
* to a known entity on the server.
*/
public Roster getRoster(String username) throws UserNotFoundException {
Roster roster = rosterCache.get(username);
if (roster == null) {
// Synchronize using a unique key so that other threads loading the User
// and not the Roster cannot produce a deadlock
synchronized ((username + " ro").intern()) {
roster = rosterCache.get(username);
if (roster == null) {
// Not in cache so load a new one:
roster = new Roster(username);
rosterCache.put(username, roster);
}
}
}
return roster;
}
/**
* Removes the entire roster of a given user. This is necessary when a user
* account is being deleted from the server.
*
* @param user the user.
*/
public void deleteRoster(JID user) {
if (!server.isLocal(user)) {
// Ignore request if user is not a local user
return;
}
try {
String username = user.getNode();
// Get the roster of the deleted user
Roster roster = getRoster(username);
// Remove each roster item from the user's roster
for (RosterItem item : roster.getRosterItems()) {
try {
roster.deleteRosterItem(item.getJid(), false);
}
catch (SharedGroupException e) {
// Do nothing. We shouldn't have this exception since we disabled the checkings
Log.warn( "Unexpected exception while deleting roster of user '{}' .", user, e );
}
}
// Remove the cached roster from memory
rosterCache.remove(username);
// Get the rosters that have a reference to the deleted user
Iterator<String> usernames = provider.getUsernames(user.toBareJID());
while (usernames.hasNext()) {
username = usernames.next();
try {
// Get the roster that has a reference to the deleted user
roster = getRoster(username);
// Remove the deleted user reference from this roster
roster.deleteRosterItem(user, false);
}
catch (SharedGroupException e) {
// Do nothing. We shouldn't have this exception since we disabled the checkings
Log.warn( "Unexpected exception while deleting roster of user '{}' .", user, e );
}
catch (UserNotFoundException e) {
// Deleted user had user that no longer exists on their roster. Ignore and move on.
}
}
}
catch (UnsupportedOperationException | UserNotFoundException e) {
// Do nothing
}
}
/**
* Returns a collection with all the groups that the user may include in his roster. The
* following criteria will be used to select the groups: 1) Groups that are configured so that
* everybody can include in his roster, 2) Groups that are configured so that its users may
* include the group in their rosters and the user is a group user of the group and 3) User
* belongs to a Group that may see a Group that whose members may include the Group in their
* rosters.
*
* @param username the username of the user to return his shared groups.
* @return a collection with all the groups that the user may include in his roster.
*/
public Collection<Group> getSharedGroups(String username) {
Collection<Group> answer = new HashSet<>();
Collection<Group> groups = GroupManager.getInstance().getSharedGroups(username);
for (Group group : groups) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("onlyGroup".equals(showInRoster)) {
if (group.isUser(username)) {
// The user belongs to the group so add the group to the answer
answer.add(group);
}
else {
// Check if the user belongs to a group that may see this group
Collection<Group> groupList = parseGroups(group.getProperties().get("sharedRoster.groupList"));
for (Group groupInList : groupList) {
if (groupInList.isUser(username)) {
answer.add(group);
}
}
}
}
else if ("everybody".equals(showInRoster)) {
// Anyone can see this group so add the group to the answer
answer.add(group);
}
}
return answer;
}
/**
* Returns the list of shared groups whose visibility is public.
*
* @return the list of shared groups whose visibility is public.
*/
public Collection<Group> getPublicSharedGroups() {
return GroupManager.getInstance().getPublicSharedGroups();
}
/**
* Returns a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*
* @param groupNames a comma delimited string with group names.
* @return a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*/
private Collection<Group> parseGroups(String groupNames) {
Collection<Group> answer = new HashSet<>();
for (String groupName : parseGroupNames(groupNames)) {
try {
answer.add(GroupManager.getInstance().getGroup(groupName));
}
catch (GroupNotFoundException e) {
// Do nothing. Silently ignore the invalid reference to the group
}
}
return answer;
}
/**
* Returns a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*
* @param groupNames a comma delimited string with group names.
* @return a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*/
private static Collection<String> parseGroupNames(String groupNames) {
Collection<String> answer = new HashSet<>();
if (groupNames != null) {
StringTokenizer tokenizer = new StringTokenizer(groupNames, ",");
while (tokenizer.hasMoreTokens()) {
answer.add(tokenizer.nextToken());
}
}
return answer;
}
@Override
public void groupCreated(Group group, Map params) {
//Do nothing
}
@Override
public void groupDeleting(Group group, Map params) {
// Get group members
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Get users whose roster will be updated
Collection<JID> affectedUsers = getAffectedUsers(group);
// Iterate on group members and update rosters of affected users
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
}
@Override
public void groupModified(Group group, Map params) {
// Do nothing if no group property has been modified
if ("propertyDeleted".equals(params.get("type"))) {
return;
}
String keyChanged = (String) params.get("propertyKey");
String originalValue = (String) params.get("originalValue");
if ("sharedRoster.showInRoster".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.showInRoster");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Get the users of the group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Get the users whose roster will be affected
Collection<JID> affectedUsers = getAffectedUsers(group, originalValue,
group.getProperties().get("sharedRoster.groupList"));
// Remove the group members from the affected rosters
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
// Simulate that the group users has been added to the group. This will cause to push
// roster items to the "affected" users for the group users
for (JID user : users) {
groupUserAdded(group, user);
}
}
else if ("sharedRoster.groupList".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.groupList");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Get the users of the group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Get the users whose roster will be affected
Collection<JID> affectedUsers = getAffectedUsers(group,
group.getProperties().get("sharedRoster.showInRoster"), originalValue);
// Remove the group members from the affected rosters
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
// Simulate that the group users has been added to the group. This will cause to push
// roster items to the "affected" users for the group users
for (JID user : users) {
groupUserAdded(group, user);
}
}
else if ("sharedRoster.displayName".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.displayName");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Do nothing if the group is not being shown in users' rosters
if (!isSharedGroup(group)) {
return;
}
// Get all the affected users
Collection<JID> users = getAffectedUsers(group);
// Iterate on all the affected users and update their rosters
for (JID updatedUser : users) {
// Get the roster to update.
Roster roster;
if (server.isLocal(updatedUser)) {
try {
roster = getRoster(updatedUser.getNode());
// Update the roster with the new group display name
roster.shareGroupRenamed(users);
} catch (UserNotFoundException e) {
Log.debug( "Unexpected exception while applying group modification for user '{}' .", updatedUser.getNode(), e );
}
}
}
}
}
@Override
public void initialize(XMPPServer server) {
super.initialize(server);
this.server = server;
this.routingTable = server.getRoutingTable();
RosterEventDispatcher.addListener(new RosterEventListener() {
@Override
public void rosterLoaded(Roster roster) {
// Do nothing
}
@Override
public boolean addingContact(Roster roster, RosterItem item, boolean persistent) {
// Do nothing
return true;
}
@Override
public void contactAdded(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
@Override
public void contactUpdated(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
@Override
public void contactDeleted(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
});
}
/**
* Returns true if the specified Group may be included in a user roster. The decision is made
* based on the group properties that are configurable through the Admin Console.
*
* @param group the group to check if it may be considered a shared group.
* @return true if the specified Group may be included in a user roster.
*/
public static boolean isSharedGroup(Group group) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("onlyGroup".equals(showInRoster) || "everybody".equals(showInRoster)) {
return true;
}
return false;
}
/**
* Returns true if the specified Group may be seen by all users in the system. The decision
* is made based on the group properties that are configurable through the Admin Console.
*
* @param group the group to check if it may be seen by all users in the system.
* @return true if the specified Group may be seen by all users in the system.
*/
public static boolean isPublicSharedGroup(Group group) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("everybody".equals(showInRoster)) {
return true;
}
return false;
}
@Override
public void memberAdded(Group group, Map params) {
JID addedUser = new JID((String) params.get("member"));
// Do nothing if the user was an admin that became a member
if (group.getAdmins().contains(addedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserAdded(visibleGroup, users, addedUser);
}
}
else {
groupUserAdded(group, addedUser);
}
}
@Override
public void memberRemoved(Group group, Map params) {
String member = (String) params.get("member");
if (member == null) {
return;
}
JID deletedUser = new JID(member);
// Do nothing if the user is still an admin
if (group.getAdmins().contains(deletedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserDeleted(visibleGroup, users, deletedUser);
}
}
else {
groupUserDeleted(group, deletedUser);
}
}
@Override
public void adminAdded(Group group, Map params) {
JID addedUser = new JID((String) params.get("admin"));
// Do nothing if the user was a member that became an admin
if (group.getMembers().contains(addedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserAdded(visibleGroup, users, addedUser);
}
}
else {
groupUserAdded(group, addedUser);
}
}
@Override
public void adminRemoved(Group group, Map params) {
JID deletedUser = new JID((String) params.get("admin"));
// Do nothing if the user is still a member
if (group.getMembers().contains(deletedUser)) {
return;
}
// Do nothing if the group is not being shown in group members' rosters
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserDeleted(visibleGroup, users, deletedUser);
}
}
else {
groupUserDeleted(group, deletedUser);
}
}
/**
* A new user has been created so members of public shared groups need to have
* their rosters updated. Members of public shared groups need to have a roster
* item with subscription FROM for the new user since the new user can see them.
*
* @param newUser the newly created user.
* @param params event parameters.
*/
@Override
public void userCreated(User newUser, Map<String,Object> params) {
JID newUserJID = server.createJID(newUser.getUsername(), null);
// Shared public groups that are public should have a presence subscription
// of type FROM for the new user
for (Group group : getPublicSharedGroups()) {
// Get group members of public group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Update the roster of each group member to include a subscription of type FROM
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Get the roster. If the user does not exist then continue with the next user
try {
roster = getRoster(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
}
// Update roster
roster.addSharedUser(group, newUserJID);
if (!server.isLocal(userToUpdate)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(newUserJID, userToUpdate, true);
}
}
}
}
@Override
public void userDeleting(User user, Map<String,Object> params) {
// Shared public groups that have a presence subscription of type FROM
// for the deleted user should no longer have a reference to the deleted user
JID userJID = server.createJID(user.getUsername(), null);
// Shared public groups that are public should have a presence subscription
// of type FROM for the new user
for (Group group : getPublicSharedGroups()) {
// Get group members of public group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Update the roster of each group member to include a subscription of type FROM
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Get the roster. If the user does not exist then continue with the next user
try {
roster = getRoster(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
}
// Update roster
roster.deleteSharedUser(group, userJID);
if (!server.isLocal(userToUpdate)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userJID, userToUpdate, false);
}
}
}
deleteRoster(userJID);
}
@Override
public void userModified(User user, Map<String,Object> params) {
if ("nameModified".equals(params.get("type"))) {
for (Group group : getSharedGroups(user.getUsername())) {
ArrayList<JID> groupUsers = new ArrayList<>();
groupUsers.addAll(group.getAdmins());
groupUsers.addAll(group.getMembers());
for (JID groupUser : groupUsers) {
rosterCache.remove(groupUser.getNode());
}
}
}
}
/**
* Notification that a Group user has been added. Update the group users' roster accordingly.
*
* @param group the group where the user was added.
* @param addedUser the username of the user that has been added to the group.
*/
private void groupUserAdded(Group group, JID addedUser) {
groupUserAdded(group, getAffectedUsers(group), addedUser);
}
/**
* Notification that a Group user has been added. Update the group users' roster accordingly.
*
* @param group the group where the user was added.
* @param users the users to update their rosters
* @param addedUser the username of the user that has been added to the group.
*/
private void groupUserAdded(Group group, Collection<JID> users, JID addedUser) {
// Get the roster of the added user.
Roster addedUserRoster = null;
if (server.isLocal(addedUser)) {
try {
addedUserRoster = getRoster(addedUser.getNode());
} catch (UserNotFoundException e) {
Log.warn( "Unexpected exception while adding user '{}' to group '{}'.", addedUser, group, e );
}
}
// Iterate on all the affected users and update their rosters
for (JID userToUpdate : users) {
if (!addedUser.equals(userToUpdate)) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Get the roster. If the user does not exist then continue with the next user
try {
roster = getRoster(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
}
// Update roster
roster.addSharedUser(group, addedUser);
// Check if the roster is still not in memory
if (addedUserRoster == null && server.isLocal(addedUser)) {
try {
addedUserRoster =
getRoster(addedUser.getNode());
} catch (UserNotFoundException e) {
Log.warn( "Unexpected exception while adding user '{}' to group '{}'.", addedUser, group, e );
}
}
// Update the roster of the newly added group user.
if (addedUserRoster != null) {
Collection<Group> groups = GroupManager.getInstance().getGroups(userToUpdate);
addedUserRoster.addSharedUser(userToUpdate, groups, group);
}
if (!server.isLocal(addedUser)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userToUpdate, addedUser, true);
}
if (!server.isLocal(userToUpdate)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(addedUser, userToUpdate, true);
}
}
}
}
/**
* Notification that a Group user has been deleted. Update the group users' roster accordingly.
*
* @param group the group from where the user was deleted.
* @param deletedUser the username of the user that has been deleted from the group.
*/
private void groupUserDeleted(Group group, JID deletedUser) {
groupUserDeleted(group, getAffectedUsers(group), deletedUser);
}
/**
* Notification that a Group user has been deleted. Update the group users' roster accordingly.
*
* @param group the group from where the user was deleted.
* @param users the users to update their rosters
* @param deletedUser the username of the user that has been deleted from the group.
*/
private void groupUserDeleted(Group group, Collection<JID> users, JID deletedUser) {
// Get the roster of the deleted user.
Roster deletedUserRoster = null;
if (server.isLocal(deletedUser)) {
try {
deletedUserRoster = getRoster(deletedUser.getNode());
} catch (UserNotFoundException e) {
Log.warn( "Unexpected exception while deleting user '{}' from group '{}'.", deletedUser, group, e );
}
}
// Iterate on all the affected users and update their rosters
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Get the roster. If the user does not exist then continue with the next user
try {
roster = getRoster(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
}
// Update roster
roster.deleteSharedUser(group, deletedUser);
// Update the roster of the newly deleted group user.
if (deletedUserRoster != null) {
deletedUserRoster.deleteSharedUser(userToUpdate, group);
}
if (!server.isLocal(deletedUser)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userToUpdate, deletedUser, false);
}
if (!server.isLocal(userToUpdate)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(deletedUser, userToUpdate, false);
}
}
}
private void sendSubscribeRequest(JID sender, JID recipient, boolean isSubscribe) {
Presence presence = new Presence();
presence.setFrom(sender);
presence.setTo(recipient);
if (isSubscribe) {
presence.setType(Presence.Type.subscribe);
}
else {
presence.setType(Presence.Type.unsubscribe);
}
routingTable.routePacket(recipient, presence, false);
}
private Collection<Group> getVisibleGroups(Group groupToCheck) {
return GroupManager.getInstance().getVisibleGroups(groupToCheck);
}
/**
* Returns true if a given group is visible to a given user. That means, if the user can
* see the group in his roster.
*
* @param group the group to check if the user can see.
* @param user the JID of the user to check if he may see the group.
* @return true if a given group is visible to a given user.
*/
public boolean isGroupVisible(Group group, JID user) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("everybody".equals(showInRoster)) {
return true;
}
else if ("onlyGroup".equals(showInRoster)) {
if (group.isUser(user)) {
return true;
}
// Check if the user belongs to a group that may see this group
Collection<Group> groupList = parseGroups(group.getProperties().get(
"sharedRoster.groupList"));
for (Group groupInList : groupList) {
if (groupInList.isUser(user)) {
return true;
}
}
}
return false;
}
/**
* Returns all the users that are related to a shared group. This is the logic that we are
* using: 1) If the group visiblity is configured as "Everybody" then all users in the system or
* all logged users in the system will be returned (configurable thorugh the "filterOffline"
* flag), 2) if the group visiblity is configured as "onlyGroup" then all the group users will
* be included in the answer and 3) if the group visiblity is configured as "onlyGroup" and
* the group allows other groups to include the group in the groups users' roster then all
* the users of the allowed groups will be included in the answer.
*/
private Collection<JID> getAffectedUsers(Group group) {
return getAffectedUsers(group, group.getProperties().get("sharedRoster.showInRoster"),
group.getProperties().get("sharedRoster.groupList"));
}
/**
* This method is similar to {@link #getAffectedUsers(Group)} except that it receives
* some group properties. The group properties are passed as parameters since the called of this
* method may want to obtain the related users of the group based in some properties values.
*
* This is useful when the group is being edited and some properties has changed and we need to
* obtain the related users of the group based on the previous group state.
*/
private Collection<JID> getAffectedUsers(Group group, String showInRoster, String groupNames) {
// Answer an empty collection if the group is not being shown in users' rosters
if (!"onlyGroup".equals(showInRoster) && !"everybody".equals(showInRoster)) {
return new ArrayList<>();
}
// Add the users of the group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// Check if anyone can see this shared group
if ("everybody".equals(showInRoster)) {
// Add all users in the system
for (String username : UserManager.getInstance().getUsernames()) {
users.add(server.createJID(username, null, true));
}
// Add all logged users. We don't need to add all users in the system since only the
// logged ones will be affected.
//users.addAll(SessionManager.getInstance().getSessionUsers());
}
else {
// Add the users that may see the group
Collection<Group> groupList = parseGroups(groupNames);
for (Group groupInList : groupList) {
users.addAll(groupInList.getMembers());
users.addAll(groupInList.getAdmins());
}
}
return users;
}
Collection<JID> getSharedUsersForRoster(Group group, Roster roster) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
String groupNames = group.getProperties().get("sharedRoster.groupList");
// Answer an empty collection if the group is not being shown in users' rosters
if (!"onlyGroup".equals(showInRoster) && !"everybody".equals(showInRoster)) {
return new ArrayList<>();
}
// Add the users of the group
Collection<JID> users = new HashSet<>(group.getMembers());
users.addAll(group.getAdmins());
// If the user of the roster belongs to the shared group then we should return
// users that need to be in the roster with subscription "from"
if (group.isUser(roster.getUsername())) {
// Check if anyone can see this shared group
if ("everybody".equals(showInRoster)) {
// Add all users in the system
for (String username : UserManager.getInstance().getUsernames()) {
users.add(server.createJID(username, null, true));
}
}
else {
// Add the users that may see the group
Collection<Group> groupList = parseGroups(groupNames);
for (Group groupInList : groupList) {
users.addAll(groupInList.getMembers());
users.addAll(groupInList.getAdmins());
}
}
}
return users;
}
/**
* Returns true if a group in the first collection may mutually see a group of the
* second collection. More precisely, return true if both collections contain a public
* group (i.e. anybody can see the group) or if both collection have a group that may see
* each other and the users are members of those groups or if one group is public and the
* other group allowed the public group to see it.
*
* @param user the name of the user associated to the first collection of groups. This is always a local user.
* @param groups a collection of groups to check against the other collection of groups.
* @param otherUser the JID of the user associated to the second collection of groups.
* @param otherGroups the other collection of groups to check against the first collection.
* @return true if a group in the first collection may mutually see a group of the
* second collection.
*/
boolean hasMutualVisibility(String user, Collection<Group> groups, JID otherUser,
Collection<Group> otherGroups) {
for (Group group : groups) {
for (Group otherGroup : otherGroups) {
// Skip this groups if the users are not group users of the groups
if (!group.isUser(user) || !otherGroup.isUser(otherUser)) {
continue;
}
if (group.equals(otherGroup)) {
return true;
}
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
String otherShowInRoster = otherGroup.getProperties().get("sharedRoster.showInRoster");
// Return true if both groups are public groups (i.e. anybody can see them)
if ("everybody".equals(showInRoster) && "everybody".equals(otherShowInRoster)) {
return true;
}
else if ("onlyGroup".equals(showInRoster) && "onlyGroup".equals(otherShowInRoster)) {
String groupNames = group.getProperties().get("sharedRoster.groupList");
String otherGroupNames = otherGroup.getProperties().get("sharedRoster.groupList");
// Return true if each group may see the other group
if (groupNames != null && otherGroupNames != null) {
if (groupNames.contains(otherGroup.getName()) &&
otherGroupNames.contains(group.getName())) {
return true;
}
// Check if each shared group can be seen by a group where each user belongs
Collection<Group> groupList = parseGroups(groupNames);
Collection<Group> otherGroupList = parseGroups(otherGroupNames);
for (Group groupName : groupList) {
if (groupName.isUser(otherUser)) {
for (Group otherGroupName : otherGroupList) {
if (otherGroupName.isUser(user)) {
return true;
}
}
}
}
}
}
else if ("everybody".equals(showInRoster) && "onlyGroup".equals(otherShowInRoster)) {
// Return true if one group is public and the other group allowed the public
// group to see him
String otherGroupNames = otherGroup.getProperties().get("sharedRoster.groupList");
if (otherGroupNames != null && otherGroupNames.contains(group.getName())) {
return true;
}
}
else if ("onlyGroup".equals(showInRoster) && "everybody".equals(otherShowInRoster)) {
// Return true if one group is public and the other group allowed the public
// group to see him
String groupNames = group.getProperties().get("sharedRoster.groupList");
// Return true if each group may see the other group
if (groupNames != null && groupNames.contains(otherGroup.getName())) {
return true;
}
}
}
}
return false;
}
@Override
public void start() throws IllegalStateException {
super.start();
// Add this module as a user event listener so we can update
// rosters when users are created or deleted
UserEventDispatcher.addListener(this);
// Add the new instance as a listener of group events
GroupEventDispatcher.addListener(this);
}
@Override
public void stop() {
super.stop();
// Remove this module as a user event listener
UserEventDispatcher.removeListener(this);
// Remove this module as a listener of group events
GroupEventDispatcher.removeListener(this);
}
public static RosterItemProvider getRosterItemProvider() {
return XMPPServer.getInstance().getRosterManager().provider;
}
private void initProvider() {
JiveGlobals.migrateProperty("provider.roster.className");
String className = JiveGlobals.getProperty("provider.roster.className",
"org.jivesoftware.openfire.roster.DefaultRosterItemProvider");
if (provider == null || !className.equals(provider.getClass().getName())) {
try {
Class c = ClassUtils.forName(className);
provider = (RosterItemProvider) c.newInstance();
}
catch (Exception e) {
Log.error("Error loading roster provider: " + className, e);
provider = new DefaultRosterItemProvider();
}
}
}
}
......@@ -29,6 +29,7 @@ import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.privacy.PrivacyList;
import org.jivesoftware.openfire.privacy.PrivacyListManager;
import org.jivesoftware.openfire.roster.RosterManager;
import org.jivesoftware.openfire.spi.ConnectionConfiguration;
import org.jivesoftware.openfire.streammanagement.StreamManager;
import org.jivesoftware.openfire.user.PresenceEventDispatcher;
......@@ -877,6 +878,12 @@ public class LocalClientSession extends LocalSession implements ClientSession {
"<compression xmlns=\"http://jabber.org/features/compress\"><method>zlib</method></compression>");
}
// If a server supports roster versioning,
// then it MUST advertise the following stream feature during stream negotiation.
if (RosterManager.isRosterVersioningEnabled()) {
sb.append("<ver xmlns=\"urn:xmpp:features:rosterver\"/>");
}
if (getAuthToken() == null) {
// Advertise that the server supports Non-SASL Authentication
if ( XMPPServer.getInstance().getIQRouter().supports( "jabber:iq:auth" ) ) {
......
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