/*
* Tigase Jabber/XMPP Server
* Copyright (C) 2004-2007 "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 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. Look for COPYING file in the top folder.
* If not, see http://www.gnu.org/licenses/.
*
* $Rev: 1256 $
* Last modified by $Author: kobit $
* $Date: 2008-12-02 00:15:24 +0000 (Tue, 02 Dec 2008) $
*/
package tigase.xmpp.impl;
import java.util.Set;
import java.util.HashSet;
import java.util.Queue;
import java.util.EnumSet;
import java.util.Map;
import java.util.logging.Logger;
import java.util.logging.Level;
import tigase.xml.Element;
import tigase.xmpp.Authorization;
import tigase.xmpp.XMPPResourceConnection;
import tigase.xmpp.StanzaType;
import tigase.xmpp.NotAuthorizedException;
import tigase.xmpp.XMPPException;
import tigase.server.Packet;
import tigase.db.NonAuthUserRepository;
import tigase.db.TigaseDBException;
import tigase.util.JIDUtils;
import tigase.xmpp.impl.roster.RosterAbstract;
import tigase.xmpp.impl.roster.RosterFactory;
import static tigase.xmpp.impl.roster.RosterAbstract.SubscriptionType;
import static tigase.xmpp.impl.roster.RosterAbstract.PresenceType;
import static tigase.xmpp.impl.roster.RosterAbstract.TO_SUBSCRIBED;
import static tigase.xmpp.impl.roster.RosterAbstract.FROM_SUBSCRIBED;
/**
* Describe class Presence here.
*
*
* Created: Wed Feb 22 07:30:03 2006
*
* @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a>
* @version $Rev: 1256 $
*/
public abstract class Presence {
/**
* Constant <code>PRESENCE_KEY</code> is a key in temporary session data
* where the last presence sent by the userto server is stored,
* either initial presence or off-line presence before disconnecting.
*/
public static final String PRESENCE_KEY = "user-presence";
/**
* <code>DIRECT_PRESENCE</code> is a key in temporary session data for
* the collection of JIDs where direct presence was sent.
* To all these addresses unavailable presence must be sent when user
* disconnects.
*/
public static final String DIRECT_PRESENCE = "direct-presences";
public static final String PRESENCE_ELEMENT_NAME = "presence";
/**
* Private logger for class instancess.
*/
private static Logger log = Logger.getLogger("tigase.xmpp.impl.Presence");
protected static final String XMLNS = "jabber:client";
private static final String[] ELEMENTS = {PRESENCE_ELEMENT_NAME};
private static final String[] XMLNSS = {XMLNS};
private static RosterAbstract roster_util =
RosterFactory.getRosterImplementation(true);
/**
* <code>stopped</code> method is called when user disconnects or logs-out.
*
* @param session a <code>XMPPResourceConnection</code> value
*/
@SuppressWarnings({"unchecked"})
public static 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) {
Element pres = (Element)session.getSessionData(PRESENCE_KEY);
// According to the spec and logic actually offline status should
// not be broadcasted if initial presence was not sent by the client.
if (pres != null && (pres.getAttribute("type") == null
|| !pres.getAttribute("type").equals("unavailable"))) {
try {
sendPresenceBroadcast(StanzaType.unavailable, session,
FROM_SUBSCRIBED, results, null, settings);
updateOfflineChange(session, results);
} catch (NotAuthorizedException e) {
// Do nothing, it may happen quite often when the user disconnects before
// it authenticates
} catch (TigaseDBException e) {
log.warning("Error accessing database for offline message: " + e);
} // end of try-catch
}
if (session.isAnonymous()) {
Set<String> direct_presences =
(Set<String>)session.getSessionData(DIRECT_PRESENCE);
if (direct_presences != null) {
try {
for (String buddy: direct_presences) {
String peer = JIDUtils.getNodeID(buddy);
Packet roster_update =
new Packet(JabberIqRoster.createRosterPacket("set",
session.nextStanzaId(), peer, peer, session.getUserId(), null,
null, "remove", JabberIqRoster.ANON));
results.offer(roster_update);
} // end of for (String buddy: buddies)
} catch (NotAuthorizedException e) {
log.finest("Anonymous user has logged out already: "
+ session.getConnectionId());
}
} // end of if (direct_presence != null)
}
}
}
/**
* <code>sendPresenceBroadcast</code> method broadcasts given presence
* to all budies from roster and to all users to which direct presence
* was sent.
*
* @param t a <code>StanzaType</code> value
* @param session a <code>XMPPResourceConnection</code> value
* @param subscrs
* @param results
* @param pres an <code>Element</code> value
* @param settings
* @exception NotAuthorizedException if an error occurs
* @throws TigaseDBException
*/
@SuppressWarnings({"unchecked"})
protected static void sendPresenceBroadcast(final StanzaType t,
final XMPPResourceConnection session,
final EnumSet<SubscriptionType> subscrs,
final Queue<Packet> results, final Element pres,
final Map<String, Object> settings)
throws NotAuthorizedException, TigaseDBException {
String[] buddies = roster_util.getBuddies(session, subscrs);
buddies = DynamicRoster.addBuddies(session, settings, buddies);
if (buddies != null) {
for (String buddy: buddies) {
sendPresence(t, buddy, session.getJID(), results, pres);
} // end of for (String buddy: buddies)
} // end of if (buddies == null)
Set<String> direct_presences =
(Set<String>)session.getSessionData(DIRECT_PRESENCE);
if (direct_presences != null && t != null && t == StanzaType.unavailable) {
for (String buddy: direct_presences) {
log.finest("Updating direct presence for: " + buddy);
sendPresence(t, buddy, session.getJID(), results, pres);
} // end of for (String buddy: buddies)
} // end of if (direct_presence != null)
}
protected static void resendPendingInRequests(final XMPPResourceConnection session,
final Queue<Packet> results)
throws NotAuthorizedException, TigaseDBException {
String[] buddies = roster_util.getBuddies(session, RosterAbstract.PENDING_IN);
if (buddies != null) {
for (String buddy: buddies) {
Element presence = new Element(PRESENCE_ELEMENT_NAME);
presence.setAttribute("type", StanzaType.subscribe.toString());
presence.setAttribute("from", buddy);
presence.setXMLNS(XMLNS);
updatePresenceChange(presence, session, results);
}
}
}
/**
* <code>updateOfflineChange</code> method broadcast off-line presence
* to all other user active resources.
*
* @param session a <code>XMPPResourceConnection</code> value
* @param results
* @exception NotAuthorizedException if an error occurs
*/
protected static void updateOfflineChange(final XMPPResourceConnection session,
final Queue<Packet> results)
throws NotAuthorizedException {
for (XMPPResourceConnection conn: session.getActiveSessions()) {
log.finer("Update presence change to: " + conn.getJID());
if (conn != session && conn.getResource() != null
&& !conn.getResource().equals(session.getResource())) {
// Send to old resource presence about new resource
Element pres_update = new Element(PRESENCE_ELEMENT_NAME);
pres_update.setAttribute("from", session.getJID());
pres_update.setAttribute("to", conn.getJID());
pres_update.setAttribute("type", StanzaType.unavailable.toString());
pres_update.setXMLNS(XMLNS);
Packet pack_update = new Packet(pres_update);
pack_update.setTo(conn.getConnectionId());
results.offer(pack_update);
} else {
log.finer("Skipping presence update to: " + conn.getJID());
} // end of else
} // end of for (XMPPResourceConnection conn: sessions)
}
/**
* <code>updateUserResources</code> method is used to broadcast to all
* <strong>other</strong> resources presence stanza from one user resource.
* So if new resource connects this method updates presence information about
* new resource to old resources and about old resources to new resource.
*
* @param presence an <code>Element</code> presence received from other users,
* we have to change 'to' attribute to full resource JID.
* @param session a <code>XMPPResourceConnection</code> value keeping
* connection session object.
* @exception NotAuthorizedException if an error occurs
*/
protected static void updateUserResources(final Element presence,
final XMPPResourceConnection session, final Queue<Packet> results)
throws NotAuthorizedException {
for (XMPPResourceConnection conn: session.getActiveSessions()) {
log.finer("Update presence change to: " + conn.getJID());
if (conn != session && conn.isResourceSet()) {
// Send to new resource presence about old resource
Element pres_update = presence.clone();
pres_update.setAttribute("from", session.getJID());
pres_update.setAttribute("to", conn.getJID());
Packet pack_update = new Packet(pres_update);
pack_update.setTo(conn.getConnectionId());
results.offer(pack_update);
Element presence_el = (Element)conn.getSessionData(PRESENCE_KEY);
if (presence_el != null) {
pres_update = presence_el.clone();
pres_update.setAttribute("to", session.getJID());
pres_update.setAttribute("from", conn.getJID());
pack_update = new Packet(pres_update);
pack_update.setTo(session.getConnectionId());
results.offer(pack_update);
}
} else {
log.finer("Skipping presence update to: " + conn.getJID());
} // end of else
} // end of for (XMPPResourceConnection conn: sessions)
}
/**
* <code>updatePresenceChange</code> method is used to broadcast
* to all active resources presence stanza received from other users, like
* incoming avaiability presence, subscription presence and so on...
* Initial presences are however sent only to those resources which
* already have sent initial presence.
*
* @param presence an <code>Element</code> presence received from other users,
* we have to change 'to' attribute to full resource JID.
* @param session a <code>XMPPResourceConnection</code> value keeping
* connection session object.
* @param results
* @exception NotAuthorizedException if an error occurs
*/
protected static void updatePresenceChange(final Element presence,
final XMPPResourceConnection session, final Queue<Packet> results)
throws NotAuthorizedException {
boolean initial_p = (presence.getAttribute("type") == null ||
"available".equals(presence.getAttribute("type")) ||
"unavailable".equals(presence.getAttribute("type")));
for (XMPPResourceConnection conn: session.getActiveSessions()) {
if (conn.getSessionData(PRESENCE_KEY) != null || !initial_p) {
// Update presence change only for online resources that is
// resources which already sent initial presence.
log.finer("Update presence change to: " + conn.getJID());
// Send to old resource presence about new resource
Element pres_update = presence.clone();
pres_update.setAttribute("to", conn.getJID());
Packet pack_update = new Packet(pres_update);
pack_update.setTo(conn.getConnectionId());
results.offer(pack_update);
} else {
// Ignore....
log.finest("Skipping update presence change for a resource which hasn't sent initial presence yet.");
}
} // end of for (XMPPResourceConnection conn: sessions)
}
protected static void forwardPresence(final Queue<Packet> results,
final Packet packet, final String from) {
Element result = packet.getElement().clone();
// Not needed anymore. Packet filter does it for all stanzas.
// According to spec we must set proper FROM attribute
// Yes, but packet filter put full JID and we need a subscription
// presence without resource here.
result.setAttribute("from", from);
log.finest("\n\nFORWARD presence: " + result.toString());
results.offer(new Packet(result));
}
protected static void sendPresence(final StanzaType t, final String to,
final String from, final Queue<Packet> results, final Element pres) {
Element presence = null;
if (pres == null) {
presence = new Element(PRESENCE_ELEMENT_NAME);
if (t != null) {
presence.setAttribute("type", t.toString());
} // end of if (t != null)
else {
presence.setAttribute("type", StanzaType.unavailable.toString());
} // end of if (t != null) else
} // end of if (pres == null)
else {
presence = pres.clone();
} // end of if (pres == null) else
presence.setAttribute("to", to);
presence.setAttribute("from", from);
presence.setXMLNS(XMLNS);
Packet packet = new Packet(presence);
log.finest("Sending presence info: " + packet.getStringData());
results.offer(packet);
}
@SuppressWarnings({"unchecked"})
protected static void addDirectPresenceJID(String jid,
XMPPResourceConnection session ) {
Set<String> direct_presences =
(Set<String>)session.getSessionData(DIRECT_PRESENCE);
if (direct_presences == null) {
direct_presences = new HashSet<String>();
session.putSessionData(DIRECT_PRESENCE, direct_presences);
} // end of if (direct_presences == null)
direct_presences.add(jid);
log.finest("Added direct presence jid: " + jid);
}
@SuppressWarnings({"unchecked"})
protected static void removeDirectPresenceJID(String jid,
XMPPResourceConnection session ) {
Set<String> direct_presences =
(Set<String>)session.getSessionData(DIRECT_PRESENCE);
if (direct_presences != null) {
direct_presences.remove(jid);
} // end of if (direct_presences == null)
log.finest("Added direct presence jid: " + jid);
}
@SuppressWarnings({"unchecked", "fallthrough"})
public static void process(final Packet packet,
final XMPPResourceConnection session,
final NonAuthUserRepository repo, final Queue<Packet> results,
final Map<String, Object> settings)
throws XMPPException {
// Synchronization to avoid conflict with login/logout events
// processed in the SessionManager asynchronously
synchronized (session) {
try {
final String jid = session.getJID();
PresenceType pres_type = roster_util.getPresenceType(session, packet);
if (pres_type == null) {
log.warning("Invalid presence found: " + packet.toString());
return;
} // end of if (type == null)
StanzaType type = packet.getType();
if (type == null) {
type = StanzaType.available;
} // end of if (type == null)
// Not needed anymore. Packet filter does it for all stanzas.
// // For all messages coming from the owner of this account set
// // proper 'from' attribute
// if (packet.getFrom().equals(session.getConnectionId())) {
// packet.getElement().setAttribute("from", session.getJID());
// } // end of if (packet.getFrom().equals(session.getConnectionId()))
if (log.isLoggable(Level.FINEST)) {
log.finest(pres_type + " presence found: " + packet.toString());
}
// All 'in' subscription presences must have a valid from address
switch (pres_type) {
case in_unsubscribe:
case in_subscribe:
case in_unsubscribed:
case in_subscribed:
if (packet.getElemFrom() == null) {
log.fine("'in' subscription presence without valid 'from' address, dropping packet: "
+ packet.toString());
return;
}
if (JIDUtils.getNodeID(packet.getElemFrom()).equals(session.getUserId())) {
log.fine("'in' subscription to myself, not allowed, returning error for packet: " + packet.toString());
results.offer(Authorization.NOT_ALLOWED.getResponseMessage(packet,
"You can not subscribe to yourself.", false));
return;
}
break;
default:
break;
}
boolean subscr_changed = false;
switch (pres_type) {
case out_initial:
// Is it direct presence to some entity on the network?
if (packet.getElemTo() != null) {
// Yes this is it, send direct presence
if (session.isAnonymous()) {
log.finest("Anonymous session: " + session.getUserId());
String peer = JIDUtils.getNodeID(packet.getElemTo());
String nick = packet.getElemCData("/presence/nick");
if (nick == null) {
nick = session.getUserName();
}
Packet rost_update =
new Packet(JabberIqRoster.createRosterPacket("set",
session.nextStanzaId(), peer, peer, session.getUserId(),
nick, "Anonymous peers", null,
JabberIqRoster.ANON));
results.offer(rost_update);
log.finest("Sending roster update: " + rost_update.toString());
}
Element result = packet.getElement().clone();
results.offer(new Packet(result));
// If this is unavailable presence, remove jid from Set
// otherwise add it to the Set
if (packet.getType() != null &&
packet.getType() == StanzaType.unavailable) {
removeDirectPresenceJID(packet.getElemTo(), session);
} else {
addDirectPresenceJID(packet.getElemTo(), session);
}
} else {
boolean first = false;
if (session.getSessionData(PRESENCE_KEY) == null) {
first = true;
}
// Store user presence for later time...
// To send response to presence probes for example.
session.putSessionData(PRESENCE_KEY, packet.getElement());
// Parse resource priority:
String priority = packet.getElemCData("/presence/priority");
if (priority != null) {
int pr = 1;
try {
pr = Integer.decode(priority);
} catch (NumberFormatException e) {
log.finer("Incorrect priority value: " + priority
+ ", setting 1 as default.");
pr = 1;
}
session.setPriority(pr);
}
// Special actions on the first availability presence
if (first && type == StanzaType.available) {
// Send presence probes to 'to' or 'both' contacts
sendPresenceBroadcast(StanzaType.probe, session, TO_SUBSCRIBED,
results, null, settings);
// Resend pending in subscription requests
resendPendingInRequests(session, results);
} // end of if (type == StanzaType.available)
// Broadcast initial presence to 'from' or 'both' contacts
sendPresenceBroadcast(type, session, FROM_SUBSCRIBED,
results, packet.getElement(), settings);
// Broadcast initial presence to other available user resources
// Element presence = packet.getElement().clone();
// Already done above, don't need to set it again here
// presence.setAttribute("from", session.getJID());
updateUserResources(packet.getElement(), session, results);
}
break;
case out_subscribe:
case out_unsubscribe:
if (pres_type == PresenceType.out_subscribe) {
SubscriptionType current_subscription =
roster_util.getBuddySubscription(session, packet.getElemTo());
if (current_subscription == null) {
roster_util.addBuddy(session, packet.getElemTo(), null, null);
} // end of if (current_subscription == null)
}
subscr_changed = roster_util.updateBuddySubscription(session, pres_type,
packet.getElemTo());
if (subscr_changed) {
roster_util.updateBuddyChange(session, results,
roster_util.getBuddyItem(session, packet.getElemTo()));
} // end of if (subscr_changed)
// According to RFC-3921 I must forward all these kind presence
// requests, it allows to resynchronize
// subscriptions in case of synchronization loss
forwardPresence(results, packet, session.getUserId());
break;
case out_subscribed:
case out_unsubscribed:
forwardPresence(results, packet, session.getUserId());
subscr_changed = roster_util.updateBuddySubscription(session, pres_type,
packet.getElemTo());
if (subscr_changed) {
roster_util.updateBuddyChange(session, results,
roster_util.getBuddyItem(session, packet.getElemTo()));
if (pres_type == PresenceType.out_subscribed) {
Element presence = (Element)session.getSessionData(PRESENCE_KEY);
if (presence != null) {
sendPresence(null, packet.getElemTo(), session.getJID(),
results, presence);
} else {
sendPresence(StanzaType.available, packet.getElemTo(),
session.getJID(), results, null);
}
} else {
sendPresence(StanzaType.unavailable, packet.getElemTo(),
session.getJID(), results, null);
}
} // end of if (subscr_changed)
break;
case in_initial:
if (packet.getElemFrom() == null) {
// That really happened already. It looks like a bug in tigase
// let's try to catch it here....
log.warning("Initial presence without from attribute set: "
+ packet.toString());
return;
}
// If other users are in 'to' or 'both' contacts, broadcast
// their preseces to all active resources
if (roster_util.isSubscribedTo(session, packet.getElemFrom())
|| (DynamicRoster.getBuddyItem(session, settings,
packet.getElemFrom()) != null)) {
updatePresenceChange(packet.getElement(), session, results);
} else {
// The code below looks like a bug to me.
// If the buddy is nt subscribed I should ignore all presences
// states from him. Commenting this out for now....
// Well, it is not a bug and it is intentional.
// All presences received from MUC come from not subscribed buddies
// therefore it seems presences from unknown buddy should be passed out
Element elem = packet.getElement().clone();
Packet result = new Packet(elem);
result.setTo(session.getConnectionId());
result.setFrom(packet.getTo());
results.offer(result);
}
break;
case in_subscribe:
// If the buddy is already subscribed then auto-reply with sybscribed
// presence stanza.
if (roster_util.isSubscribedFrom(session, packet.getElemFrom())) {
sendPresence(StanzaType.subscribed, packet.getElemFrom(),
session.getJID(), results, null);
} else {
SubscriptionType curr_sub =
roster_util.getBuddySubscription(session, packet.getElemFrom());
if (curr_sub == null) {
curr_sub = SubscriptionType.none;
roster_util.addBuddy(session, packet.getElemFrom(), null, null);
} // end of if (curr_sub == null)
roster_util.updateBuddySubscription(session, pres_type,
packet.getElemFrom());
updatePresenceChange(packet.getElement(), session, results);
} // end of else
break;
case in_unsubscribe:
subscr_changed = roster_util.updateBuddySubscription(session, pres_type,
packet.getElemFrom());
if (subscr_changed) {
// No longer needed according to RFC-3921-bis5
//sendPresence(StanzaType.unsubscribed, packet.getElemFrom(),
// session.getJID(), results, null);
//updatePresenceChange(packet.getElement(), session, results);
roster_util.updateBuddyChange(session, results,
roster_util.getBuddyItem(session, packet.getElemFrom()));
}
break;
case in_subscribed: {
SubscriptionType curr_sub =
roster_util.getBuddySubscription(session, packet.getElemFrom());
if (curr_sub == null) {
curr_sub = SubscriptionType.none;
roster_util.addBuddy(session, packet.getElemFrom(), null, null);
} // end of if (curr_sub == null)
subscr_changed = roster_util.updateBuddySubscription(session, pres_type,
packet.getElemFrom());
if (subscr_changed) {
//updatePresenceChange(packet.getElement(), session, results);
roster_util.updateBuddyChange(session, results,
roster_util.getBuddyItem(session, packet.getElemFrom()));
}
}
break;
case in_unsubscribed: {
SubscriptionType curr_sub =
roster_util.getBuddySubscription(session, packet.getElemFrom());
if (curr_sub != null) {
subscr_changed = roster_util.updateBuddySubscription(session, pres_type,
packet.getElemFrom());
if (subscr_changed) {
// No longer needed according to RFC-3921-bis5
//updatePresenceChange(packet.getElement(), session, results);
roster_util.updateBuddyChange(session, results,
roster_util.getBuddyItem(session, packet.getElemFrom()));
}
}
}
break;
case in_probe:
SubscriptionType buddy_subscr = null;
if (DynamicRoster.getBuddyItem(session, settings,
packet.getElemFrom()) != null) {
buddy_subscr = SubscriptionType.both;
} else {
buddy_subscr =
roster_util.getBuddySubscription(session, packet.getElemFrom());
}
if (buddy_subscr == null) {
buddy_subscr = SubscriptionType.none;
} // end of if (buddy_subscr == null)
switch (buddy_subscr) {
case none:
case none_pending_out:
case to:
results.offer(Authorization.FORBIDDEN.getResponseMessage(packet,
"Presence information is forbidden.", false));
break;
case none_pending_in:
case none_pending_out_in:
case to_pending_in:
results.offer(Authorization.NOT_AUTHORIZED.getResponseMessage(packet,
"You are not authorized to get presence information.", false));
break;
default:
break;
} // end of switch (buddy_subscr)
if (roster_util.isSubscribedFrom(buddy_subscr)) {
for (XMPPResourceConnection conn: session.getActiveSessions()) {
Element pres = (Element)conn.getSessionData(PRESENCE_KEY);
sendPresence(null, packet.getElemFrom(), conn.getJID(),
results, pres);
}
} // end of if (roster_util.isSubscribedFrom(session, packet.getElemFrom()))
break;
case error: {
// This is message to 'this' client probably
// Only error responses to DIRECT presence should be sent back
// to the client, all other should be ignored for now.
// Later on the Tigase should remember who responded with
// an error and don't send presence updates to this entity
Set<String> direct_presences =
(Set<String>) session.getSessionData(DIRECT_PRESENCE);
if (direct_presences != null &&
direct_presences.contains(packet.getElemFrom())) {
Element elem = packet.getElement().clone();
Packet result = new Packet(elem);
result.setTo(session.getConnectionId());
result.setFrom(packet.getTo());
results.offer(result);
} else {
// Ignore for now....
}
}
break;
default:
results.offer(Authorization.BAD_REQUEST.getResponseMessage(packet,
"Request type is incorrect", false));
break;
} // end of switch (type)
} // end of try
catch (NotAuthorizedException e) {
log.warning(
"Can not access user Roster, user session is not authorized yet: " +
packet.getStringData());
log.log(Level.FINEST, "presence problem...", e);
// results.offer(Authorization.NOT_AUTHORIZED.getResponseMessage(packet,
// "You must authorize session first.", true));
} catch (TigaseDBException e) {
log.warning("Error accessing database for presence data: " + e);
} // end of try-catch
}
}
} // Presence