/*
* Tigase Jabber/XMPP Server
* Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. Look for COPYING file in the top folder.
* If not, see http://www.gnu.org/licenses/.
*
* $Rev$
* Last modified by $Author$
* $Date$
*/
package tigase.xmpp.impl;
//~--- non-JDK imports --------------------------------------------------------
import tigase.db.NonAuthUserRepository;
import tigase.db.TigaseDBException;
import tigase.server.Packet;
import tigase.server.Priority;
import tigase.util.TigaseStringprepException;
import tigase.xml.Element;
import tigase.xmpp.Authorization;
import tigase.xmpp.JID;
import tigase.xmpp.NoConnectionIdException;
import tigase.xmpp.NotAuthorizedException;
import tigase.xmpp.PacketErrorTypeException;
import tigase.xmpp.StanzaType;
import tigase.xmpp.XMPPException;
import tigase.xmpp.XMPPProcessor;
import tigase.xmpp.XMPPProcessorIfc;
import tigase.xmpp.XMPPResourceConnection;
import tigase.xmpp.XMPPStopListenerIfc;
import tigase.xmpp.impl.roster.RosterAbstract;
import tigase.xmpp.impl.roster.RosterFactory;
import static tigase.xmpp.impl.roster.Roster.SubscriptionType;
//~--- JDK imports ------------------------------------------------------------
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;
//~--- classes ----------------------------------------------------------------
/**
* Class <code>JabberIqRoster</code> implements part of <em>RFC-3921</em> -
* <em>XMPP Instant Messaging</em> specification describing roster management.
* 7. Roster Management
*
*
* Created: Tue Feb 21 17:42:53 2006
*
* @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a>
* @version $Rev$
*/
public class JabberIqRoster extends XMPPProcessor implements XMPPProcessorIfc,
XMPPStopListenerIfc {
/**
* Private logger for class instance.
*/
private static final Logger log = Logger.getLogger(JabberIqRoster.class.getName());
private static final String[] ELEMENTS = { "query", "query" };
private static final String[] XMLNSS = { RosterAbstract.XMLNS,
RosterAbstract.XMLNS_DYNAMIC };
private static final String ID = RosterAbstract.XMLNS;
/** Field description */
public static final String ANON = "anon";
// ~--- fields ---------------------------------------------------------------
protected RosterAbstract roster_util = getRosterUtil();
// ~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param item
*
* @return
*/
public static String[] getItemGroups(Element item) {
List<Element> elgr = item.getChildren();
if ((elgr != null) && (elgr.size() > 0)) {
ArrayList<String> groups = new ArrayList<String>(1);
for (Element grp : elgr) {
if (grp.getName() == RosterAbstract.GROUP) {
groups.add(grp.getCData());
}
}
if (groups.size() > 0) {
return groups.toArray(new String[groups.size()]);
}
}
return null;
}
// ~--- methods --------------------------------------------------------------
protected static void dynamicGetRequest(Packet packet, XMPPResourceConnection session,
Queue<Packet> results, Map<String, Object> settings) throws NotAuthorizedException {
Element request = packet.getElement();
Element item = request.findChild("/iq/query/item");
if (item != null) {
Element new_item = DynamicRoster.getItemExtraData(session, settings, item);
if (new_item == null) {
new_item = item;
}
results.offer(packet.okResult(new_item, 1));
} else {
try {
results.offer(Authorization.BAD_REQUEST.getResponseMessage(packet,
"Missing 'item' element, request can not be processed.", true));
} catch (PacketErrorTypeException ex) {
log.log(Level.SEVERE, "Received error packet? not possible.", ex);
}
}
}
protected static void dynamicSetRequest(Packet packet, XMPPResourceConnection session,
Queue<Packet> results, Map<String, Object> settings) {
Element request = packet.getElement();
List<Element> items = request.getChildren("/iq/query");
if ((items != null) && (items.size() > 0)) {
for (Element item : items) {
DynamicRoster.setItemExtraData(session, settings, item);
}
results.offer(packet.okResult((String) null, 0));
} else {
try {
results.offer(Authorization.BAD_REQUEST.getResponseMessage(packet,
"Missing 'item' element, request can not be processed.", true));
} catch (PacketErrorTypeException ex) {
log.log(Level.SEVERE, "Received error packet? not possible.", ex);
}
}
}
/**
* Method description
*
*
* @return
*/
@Override
public int concurrentQueuesNo() {
return Runtime.getRuntime().availableProcessors() * 2;
}
/**
* Method description
*
*
* @return
*/
@Override
public String id() {
return ID;
}
/**
* Method description
*
*
* @param packet
* @param session
* @param repo
* @param results
* @param settings
*
* @throws XMPPException
*/
@Override
public void process(Packet packet, XMPPResourceConnection session,
NonAuthUserRepository repo, Queue<Packet> results, Map<String, Object> settings)
throws XMPPException {
if (session == null) {
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "Session is null, ignoring packet: {0}", packet);
}
return;
} // end of if (session == null)
if (!session.isAuthorized()) {
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "Session is not authorized, ignoring packet: {0}", packet);
}
return;
}
// The roster request can be between the user and the server or between the
// user and some other entity like transport
JID connectionId = session.getConnectionId();
if (connectionId.equals(packet.getPacketFrom())) {
// Packet from the user, let's check where it should go
if ((packet.getStanzaTo() != null)
&& !session.isLocalDomain(packet.getStanzaTo().toString(), false)
&& !session.isUserId(packet.getStanzaTo().getBareJID())) {
results.offer(packet.copyElementOnly());
return;
}
} else {
// Packet probably to the user, let's check where it came from
if (session.isUserId(packet.getStanzaTo().getBareJID())) {
Packet result = packet.copyElementOnly();
result.setPacketTo(session.getConnectionId(packet.getStanzaTo()));
result.setPacketFrom(packet.getTo());
results.offer(result);
return;
} else {
// Hm, I do not know what to do here, should not happen
}
}
try {
if ((packet.getStanzaFrom() != null)
&& !session.isUserId(packet.getStanzaFrom().getBareJID())) {
// RFC says: ignore such request
log.log(Level.WARNING, "Roster request ''from'' attribute doesn't match "
+ "session: {0}, request: {1}", new Object[] { session, packet });
return;
} // end of if (packet.getElemFrom() != null
// &&
// !session.getUserId().equals(JIDUtils.getNodeID(packet.getElemFrom())))
StanzaType type = packet.getType();
String xmlns = packet.getElement().getXMLNS("/iq/query");
if (xmlns == RosterAbstract.XMLNS) {
switch (type) {
case get:
processGetRequest(packet, session, results, settings);
break;
case set:
processSetRequest(packet, session, results, settings);
break;
case result:
// Ignore
break;
default:
results.offer(Authorization.BAD_REQUEST.getResponseMessage(packet,
"Request type is incorrect", false));
break;
} // end of switch (type)
} else {
if (xmlns == RosterAbstract.XMLNS_DYNAMIC) {
switch (type) {
case get:
dynamicGetRequest(packet, session, results, settings);
break;
case set:
dynamicSetRequest(packet, session, results, settings);
break;
case result:
// Ignore
break;
default:
results.offer(Authorization.BAD_REQUEST.getResponseMessage(packet,
"Request type is incorrect", false));
break;
} // end of switch (type)
} else {
// Hm, don't know what to do, unexpected name space, let's record it
log.log(Level.WARNING, "Unknown XMLNS for the roster plugin: {0}", packet);
}
}
} catch (NotAuthorizedException e) {
log.log(Level.WARNING,
"Received roster request but user session is not authorized yet: {0}", packet);
results.offer(Authorization.NOT_AUTHORIZED.getResponseMessage(packet,
"You must authorize session first.", true));
} catch (TigaseDBException e) {
log.log(Level.WARNING, "Database problem, please contact admin:", e);
results.offer(Authorization.INTERNAL_SERVER_ERROR.getResponseMessage(packet,
"Database access problem, please contact administrator.", true));
} // end of try-catch
}
/**
* <code>stopped</code> method is called when user disconnects or logs-out.
*
* @param session
* a <code>XMPPResourceConnection</code> value
* @param results
* @param settings
*/
@Override
public void stopped(final XMPPResourceConnection session, final Queue<Packet> results,
final Map<String, Object> settings) {
// // Synchronization to avoid conflict with login/logout events
// // processed in the SessionManager asynchronously
// synchronized (session) {
// try {
// if (session.isAnonymous() && session.getAnonymousPeers() != null) {
// log.finest("Anonymous session: " + session.getUserId());
// String[] anon_peers = session.getAnonymousPeers();
// for (String peer: anon_peers) {
// Element iq = new Element("iq",
// new String[] {"type", "id", "to", "from"},
// new String[] {"set", session.getUserName(), peer, peer});
// Element query = new Element("query");
// query.setXMLNS(XMLNS);
// iq.addChild(query);
// Element item = new Element("item",
// new String[] {"jid", "subscription", "type"},
// new String[] {session.getUserId(), "remove", ANON});
// query.addChild(item);
// Packet rost_update = new Packet(iq);
// results.offer(rost_update);
// log.finest("Sending roster update: " + rost_update.toString());
// }
// }
// } catch (NotAuthorizedException e) {
// log.warning("Can not proceed with anonymous logout, session not authorized yet..."
// + session.getConnectionId());
// }
// }
}
/**
* Method description
*
*
* @param session
*
* @return
*/
@Override
public Element[] supDiscoFeatures(final XMPPResourceConnection session) {
return RosterAbstract.DISCO_FEATURES;
}
/**
* Method description
*
*
* @return
*/
@Override
public String[] supElements() {
return ELEMENTS;
}
/**
* Method description
*
*
* @return
*/
@Override
public String[] supNamespaces() {
return XMLNSS;
}
/**
* Method description
*
*
* @param session
*
* @return
*/
@Override
public Element[] supStreamFeatures(final XMPPResourceConnection session) {
return RosterAbstract.FEATURES;
}
// ~--- get methods ----------------------------------------------------------
protected RosterAbstract getRosterUtil() {
return RosterFactory.getRosterImplementation(true);
}
// ~--- methods --------------------------------------------------------------
protected void updateHash(XMPPResourceConnection session, Map<String, Object> settings)
throws NotAuthorizedException, TigaseDBException {
// Retrieve standard roster items.
List<Element> ritems = roster_util.getRosterItems(session);
// Recalculate the roster hash again with dynamic roster content
StringBuilder roster_str = new StringBuilder(5000);
// Retrieve all Dynamic roster elements from the roster repository
List<Element> its = DynamicRoster.getRosterItems(session, settings);
// There is always a chance that the same elements exist in a dynamic roster
// and the standard user roster. Moreover, the items in the standard roster
// may have a different presence subscription set.
// Here we make sure they are both in sync, that is for each entry which
// exists in both rosters we enforce 'both' subscription type for element in
// standard roster and remove it from the dynamic roster list.
if ((its != null) && (its.size() > 0)) {
for (Iterator<Element> it = its.iterator(); it.hasNext();) {
Element element = it.next();
try {
JID jid = JID.jidInstance(element.getAttribute("jid"));
if (roster_util.containsBuddy(session, jid)) {
roster_util.setBuddySubscription(session, SubscriptionType.both, jid);
String[] itemGroups = getItemGroups(element);
if (itemGroups != null) {
roster_util.addBuddyGroup(session, jid, itemGroups);
}
it.remove();
}
} catch (TigaseStringprepException ex) {
log.log(Level.INFO,
"JID from dynamic roster is incorrect, stringprep failed for: {0}",
element.getAttribute("jid"));
it.remove();
}
}
// This may seem to be redundant as this call has already been made
// but the roster could have been changed during above dynamic roster
// merge
ritems = roster_util.getRosterItems(session);
for (Element ritem : its) {
roster_str.append(ritem.toString());
}
}
for (Element ritem : ritems) {
roster_str.append(ritem.toString());
}
roster_util.updateRosterHash(roster_str.toString(), session);
}
protected void processGetRequest(Packet packet, XMPPResourceConnection session,
Queue<Packet> results, Map<String, Object> settings) throws NotAuthorizedException,
TigaseDBException {
// Retrieve all Dynamic roster elements from the roster repository
List<Element> its = DynamicRoster.getRosterItems(session, settings);
// If the dynamic roster exists, we have to always recalculate hash, as the
// part of the roster could have changed outside of the Tigase server.
if (its != null && its.size() > 0) {
updateHash(session, settings);
}
// Check roster version hash.
String incomingHash =
packet.getElement().getAttribute("/iq/query", RosterAbstract.VER_ATT);
String storedHash = "";
// If client provided hash and the server calculated hash are the same
// return the success result and abort further roster processing.
// No need to send the whole roster to the client.
if (incomingHash != null) {
storedHash = roster_util.getBuddiesHash(session);
if (storedHash == null || storedHash.isEmpty()) {
updateHash(session, settings);
storedHash = roster_util.getBuddiesHash(session);
}
if (incomingHash.equals(storedHash)) {
results.offer(packet.okResult((String) null, 0));
return;
}
}
// Retrieve standard roster items.
List<Element> ritems = roster_util.getRosterItems(session);
// Send the user's standard roster first
if ((ritems != null) && (ritems.size() > 0)) {
Element query = new Element("query");
query.setXMLNS(RosterAbstract.XMLNS);
if (incomingHash != null) {
query.setAttribute(RosterAbstract.VER_ATT, storedHash);
}
query.addChildren(ritems);
results.offer(packet.okResult(query, 0));
} else {
results.offer(packet.okResult((String) null, 1));
}
// Push the dynamic roster items now
try {
if ((its != null) && (its.size() > 0)) {
ArrayDeque<Element> items = new ArrayDeque<Element>(its);
while (items.size() > 0) {
Element iq =
new Element("iq", new String[] { "type", "id", "to" }, new String[] {
"set", session.nextStanzaId(), session.getJID().toString() });
iq.setXMLNS(CLIENT_XMLNS);
Element query = new Element("query");
query.setXMLNS(RosterAbstract.XMLNS);
iq.addChild(query);
query.addChild(items.poll());
while ((query.getChildren().size() < 20) && (items.size() > 0)) {
query.addChild(items.poll());
}
Packet rost_res = Packet.packetInstance(iq, null, session.getJID());
rost_res.setPacketTo(session.getConnectionId());
rost_res.setPacketFrom(packet.getTo());
results.offer(rost_res);
}
}
} catch (NoConnectionIdException ex) {
log.log(Level.WARNING,
"Problem with roster request, no connection ID for session: {0}, request: {1}",
new Object[] { session, packet });
}
}
protected void processSetRequest(Packet packet, XMPPResourceConnection session,
Queue<Packet> results, final Map<String, Object> settings) throws XMPPException,
NotAuthorizedException, TigaseDBException {
// Element request = packet.getElement();
List<Element> items = packet.getElemChildren("/iq/query");
if (items != null) {
try {
// RFC-3921 draft bis-03 forbids multiple items in one request
// This however seems to make no much sense and actually was
// requested by many users to allow for multiple items
for (Element item : items) {
JID buddy = JID.jidInstance(item.getAttribute("jid"));
if (DynamicRoster.getBuddyItem(session, settings, buddy) != null) {
// Let's return an error. Dynamic roster cannot be modified via
// XMPP.
results
.offer(Authorization.FEATURE_NOT_IMPLEMENTED
.getResponseMessage(
packet,
"You cannot modify this contact. It is controlled by an external service.",
true));
return;
}
if (session.isUserId(buddy.getBareJID())) {
results.offer(Authorization.NOT_ALLOWED.getResponseMessage(packet,
"User can't add himself to the roster, RFC says NO.", true));
return;
}
String subscription = item.getAttribute("subscription");
if ((subscription != null) && subscription.equals("remove")) {
SubscriptionType sub = roster_util.getBuddySubscription(session, buddy);
if (sub == null) {
sub = SubscriptionType.none;
}
String type = item.getAttribute("type");
if ((sub != SubscriptionType.none) && ((type == null) || !type.equals(ANON))) {
// Unavailable presence should be sent first, otherwise it will be
// blocked by the server after the subscription is canceled
Element pres = new Element("presence");
pres.setXMLNS(CLIENT_XMLNS);
pres.setAttribute("to", buddy.toString());
pres.setAttribute("from", session.getJID().toString());
pres.setAttribute("type", "unavailable");
Packet pres_packet = Packet.packetInstance(pres, session.getJID(), buddy);
// We have to set a higher priority for this particular
// unavailable packet
// to make sure it is delivered before subscription cancellation
pres_packet.setPriority(Priority.HIGH);
results.offer(pres_packet);
pres = new Element("presence");
pres.setXMLNS(CLIENT_XMLNS);
pres.setAttribute("to", buddy.toString());
pres.setAttribute("from", session.getBareJID().toString());
pres.setAttribute("type", "unsubscribe");
results.offer(Packet.packetInstance(pres, session.getJID()
.copyWithoutResource(), buddy));
pres = new Element("presence");
pres.setXMLNS(CLIENT_XMLNS);
pres.setAttribute("to", buddy.toString());
pres.setAttribute("from", session.getBareJID().toString());
pres.setAttribute("type", "unsubscribed");
results.offer(Packet.packetInstance(pres, session.getJID()
.copyWithoutResource(), buddy));
} // is in the roster while he isn't. In such a case just ensure the
// client that the buddy has been removed for sure
Element it = new Element("item");
it.setAttribute("jid", buddy.toString());
it.setAttribute("subscription", "remove");
roster_util.removeBuddy(session, buddy);
roster_util.updateBuddyChange(session, results, it);
} else {
String name = item.getAttribute("name");
// if (name == null) {
// name = buddy;
// } // end of if (name == null)
List<Element> groups = item.getChildren();
String[] gr = null;
if ((groups != null) && (groups.size() > 0)) {
gr = new String[groups.size()];
int cnt = 0;
for (Element group : groups) {
gr[cnt++] = ((group.getCData() == null) ? "" : group.getCData());
} // end of for (ElementData group : groups)
// end of for (ElementData group : groups)
}
roster_util.addBuddy(session, buddy, name, gr, null);
String type = item.getAttribute("type");
if ((type != null) && type.equals(ANON)) {
roster_util.setBuddySubscription(session, SubscriptionType.both, buddy);
Element pres =
(Element) session.getSessionData(XMPPResourceConnection.PRESENCE_KEY);
if (pres == null) {
pres = new Element("presence");
pres.setXMLNS(CLIENT_XMLNS);
} else {
pres = pres.clone();
}
pres.setAttribute("to", buddy.toString());
pres.setAttribute("from", session.getJID().toString());
results.offer(Packet.packetInstance(pres, session.getJID(), buddy));
}
Element new_buddy = roster_util.getBuddyItem(session, buddy);
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "1. New Buddy: {0}", new_buddy.toString());
}
if (roster_util.getBuddySubscription(session, buddy) == null) {
roster_util.setBuddySubscription(session, SubscriptionType.none, buddy);
} // end of if (getBuddySubscription(session, buddy) == null)
new_buddy = roster_util.getBuddyItem(session, buddy);
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "2. New Buddy: {0}", new_buddy.toString());
}
roster_util.updateBuddyChange(session, results, new_buddy);
} // end of else
// end of else
}
results.offer(packet.okResult((String) null, 0));
} catch (TigaseStringprepException ex) {
results.offer(Authorization.BAD_REQUEST.getResponseMessage(packet,
"Buddy JID is incorrct, stringprep failed.", true));
}
} else {
log.log(Level.WARNING, "No items found in roster set request: {0}", packet);
results.offer(Authorization.BAD_REQUEST.getResponseMessage(packet,
"No items found in the roster set request", true));
}
}
} // JabberIqRoster
// ~ Formatted in Sun Code Convention
// ~ Formatted by Jindent --- http://www.jindent.com