1 /*
2  * Copyright (C) 2004-2008 Jive Software. All rights reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.jivesoftware.openfire.spi;
18 
19 import java.sql.Connection;
20 import java.sql.PreparedStatement;
21 import java.sql.ResultSet;
22 import java.sql.SQLException;
23 import java.sql.Types;
24 import java.util.*;
25 import java.util.concurrent.locks.Lock;
26 
27 import org.dom4j.Document;
28 import org.dom4j.DocumentException;
29 import org.dom4j.DocumentHelper;
30 import org.jivesoftware.database.DbConnectionManager;
31 import org.jivesoftware.openfire.*;
32 import org.jivesoftware.openfire.auth.UnauthorizedException;
33 import org.jivesoftware.openfire.component.InternalComponentManager;
34 import org.jivesoftware.openfire.container.BasicModule;
35 import org.jivesoftware.openfire.event.UserEventDispatcher;
36 import org.jivesoftware.openfire.event.UserEventListener;
37 import org.jivesoftware.openfire.handler.PresenceUpdateHandler;
38 import org.jivesoftware.openfire.privacy.PrivacyList;
39 import org.jivesoftware.openfire.privacy.PrivacyListManager;
40 import org.jivesoftware.openfire.roster.Roster;
41 import org.jivesoftware.openfire.roster.RosterItem;
42 import org.jivesoftware.openfire.roster.RosterManager;
43 import org.jivesoftware.openfire.session.ClientSession;
44 import org.jivesoftware.openfire.user.User;
45 import org.jivesoftware.openfire.user.UserManager;
46 import org.jivesoftware.openfire.user.UserNotFoundException;
47 import org.jivesoftware.util.LocaleUtils;
48 import org.jivesoftware.util.StringUtils;
49 import org.jivesoftware.util.cache.Cache;
50 import org.jivesoftware.util.cache.CacheFactory;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53 import org.xmpp.packet.JID;
54 import org.xmpp.packet.PacketError;
55 import org.xmpp.packet.Presence;
56 
57 /**
58  * Simple in memory implementation of the PresenceManager interface.
59  *
60  * @author Iain Shigeoka
61  */
62 public class PresenceManagerImpl extends BasicModule implements PresenceManager, UserEventListener, XMPPServerListener {
63 
64     private static final Logger Log = LoggerFactory.getLogger(PresenceManagerImpl.class);
65 
66     private static final String LOAD_OFFLINE_PRESENCE =
67             "SELECT offlinePresence, offlineDate FROM ofPresence WHERE username=?";
68     private static final String INSERT_OFFLINE_PRESENCE =
69             "INSERT INTO ofPresence(username, offlinePresence, offlineDate) VALUES(?,?,?)";
70     private static final String DELETE_OFFLINE_PRESENCE =
71             "DELETE FROM ofPresence WHERE username=?";
72 
73     private static final String NULL_STRING = "NULL";
74     private static final long NULL_LONG = -1L;
75 
76     private RoutingTable routingTable;
77     private SessionManager sessionManager;
78     private UserManager userManager;
79     private RosterManager rosterManager;
80     private XMPPServer server;
81     private PacketDeliverer deliverer;
82     private PresenceUpdateHandler presenceUpdateHandler;
83 
84     private InternalComponentManager componentManager;
85 
86     private Cache<String, Long> lastActivityCache;
87     private Cache<String, String> offlinePresenceCache;
88 
PresenceManagerImpl()89     public PresenceManagerImpl() {
90         super("Presence manager");
91     }
92 
93     @Override
isAvailable(User user)94     public boolean isAvailable(User user) {
95         return sessionManager.getActiveSessionCount(user.getUsername()) > 0;
96     }
97 
98     @Override
getPresence(User user)99     public Presence getPresence(User user) {
100         if (user == null) {
101             return null;
102         }
103         Presence presence = null;
104 
105         for (ClientSession session : sessionManager.getSessions(user.getUsername())) {
106             if (presence == null) {
107                 presence = session.getPresence();
108             }
109             else {
110                 // Get the ordinals of the presences to compare. If no ordinal is available then
111                 // assume a value of -1
112                 int o1 = presence.getShow() != null ? presence.getShow().ordinal() : -1;
113                 int o2 = session.getPresence().getShow() != null ?
114                         session.getPresence().getShow().ordinal() : -1;
115                 // Compare the presences' show ordinals
116                 if (o1 > o2) {
117                     presence = session.getPresence();
118                 }
119             }
120         }
121         return presence;
122     }
123 
124     @Override
getPresences(String username)125     public Collection<Presence> getPresences(String username) {
126         if (username == null) {
127             return null;
128         }
129         List<Presence> presences = new ArrayList<>();
130 
131         for (ClientSession session : sessionManager.getSessions(username)) {
132             presences.add(session.getPresence());
133         }
134         return Collections.unmodifiableCollection(presences);
135     }
136 
137     @Override
getLastPresenceStatus(User user)138     public String getLastPresenceStatus(User user) {
139         String username = user.getUsername();
140         String presenceStatus = null;
141         String presenceXML = offlinePresenceCache.get(username);
142         if (presenceXML == null) {
143             loadOfflinePresence(username);
144         }
145         presenceXML = offlinePresenceCache.get(username);
146         if (presenceXML != null) {
147             // If the cached answer is no data, return null.
148             if (presenceXML.equals(NULL_STRING)) {
149                 return null;
150             }
151             // Otherwise, parse out the status from the XML.
152             try {
153                 // Parse the element
154                 Document element = DocumentHelper.parseText(presenceXML);
155                 presenceStatus = element.getRootElement().elementTextTrim("status");
156             }
157             catch (DocumentException e) {
158                 Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
159             }
160         }
161         return presenceStatus;
162     }
163 
164     @Override
getLastActivity(User user)165     public long getLastActivity(User user) {
166         String username = user.getUsername();
167         long lastActivity = NULL_LONG;
168         Long offlineDate = lastActivityCache.get(username);
169         if (offlineDate == null) {
170             loadOfflinePresence(username);
171         }
172         offlineDate = lastActivityCache.get(username);
173         if (offlineDate != null) {
174             // If the cached answer is no data, return -1.
175             if (offlineDate == NULL_LONG) {
176                 return NULL_LONG;
177             }
178             else {
179                 try {
180                     lastActivity = (System.currentTimeMillis() - offlineDate);
181                 }
182                 catch (NumberFormatException e) {
183                     Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
184                 }
185             }
186         }
187         return lastActivity;
188     }
189 
190     @Override
userAvailable(Presence presence)191     public void userAvailable(Presence presence) {
192         // Delete the last unavailable presence of this user since the user is now
193         // available. Only perform this operation if this is an available presence sent to
194         // THE SERVER and the presence belongs to a local user.
195         if (presence.getTo() == null && userManager.isRegisteredUser(presence.getFrom(), false)) {
196             String username = presence.getFrom().getNode();
197 
198             // Optimization: only delete the unavailable presence information if this
199             // is the first session created on the server.
200             if (sessionManager.getSessionCount(username) > 1) {
201                 return;
202             }
203 
204             deleteOfflinePresenceFromDB(username);
205 
206             // Remove data from cache.
207             offlinePresenceCache.remove(username);
208             lastActivityCache.remove(username);
209         }
210     }
211 
deleteOfflinePresenceFromDB(String username)212     private void deleteOfflinePresenceFromDB(String username) {
213         Connection con = null;
214         PreparedStatement pstmt = null;
215         try {
216             con = DbConnectionManager.getConnection();
217             pstmt = con.prepareStatement(DELETE_OFFLINE_PRESENCE);
218             pstmt.setString(1, username);
219             pstmt.execute();
220         }
221         catch (SQLException sqle) {
222             Log.error(sqle.getMessage(), sqle);
223         }
224         finally {
225             DbConnectionManager.closeConnection(pstmt, con);
226         }
227     }
228 
229     @Override
userUnavailable(Presence presence)230     public void userUnavailable(Presence presence) {
231         // Only save the last presence status and keep track of the time when the user went
232         // offline if this is an unavailable presence sent to THE SERVER and the presence belongs
233         // to a local user.
234         if (presence.getTo() == null && userManager.isRegisteredUser(presence.getFrom(), false)) {
235             String username = presence.getFrom().getNode();
236 
237             // If the user has any remaining sessions, don't record the offline info.
238             if (sessionManager.getActiveSessionCount(username) > 0) {
239                 return;
240             }
241 
242             String offlinePresence = null;
243             // Save the last unavailable presence of this user if the presence contains any
244             // child element such as <status>.
245             if (!presence.getElement().elements().isEmpty()) {
246                 offlinePresence = presence.toXML();
247             }
248             // Keep track of the time when the user went offline
249             java.util.Date offlinePresenceDate = new java.util.Date();
250 
251             boolean addedToCache;
252             if (offlinePresence == null) {
253                 addedToCache = !NULL_STRING.equals(offlinePresenceCache.put(username, NULL_STRING));
254             }
255             else {
256                 addedToCache = !offlinePresence.equals(offlinePresenceCache.put(username, offlinePresence));
257             }
258             if (!addedToCache) {
259                 return;
260             }
261             Log.debug( "Recording 'last activity' for user '{}'.", username);
262             lastActivityCache.put(username, offlinePresenceDate.getTime());
263 
264             writeToDatabase(username, offlinePresence, offlinePresenceDate);
265         }
266     }
267 
writeToDatabase(String username, String offlinePresence, Date offlinePresenceDate)268     private void writeToDatabase(String username, String offlinePresence, Date offlinePresenceDate) {
269         // delete existing offline presence (if any)
270         deleteOfflinePresenceFromDB(username);
271 
272         // Insert data into the database.
273         Connection con = null;
274         PreparedStatement pstmt = null;
275         try {
276             con = DbConnectionManager.getConnection();
277             pstmt = con.prepareStatement(INSERT_OFFLINE_PRESENCE);
278             pstmt.setString(1, username);
279             if (offlinePresence != null) {
280                 DbConnectionManager.setLargeTextField(pstmt, 2, offlinePresence);
281             } else {
282                 pstmt.setNull(2, Types.VARCHAR);
283             }
284             pstmt.setString(3, StringUtils.dateToMillis(offlinePresenceDate));
285             pstmt.execute();
286         } catch (SQLException sqle) {
287             Log.error("Error storing offline presence of user: " + username, sqle);
288         } finally {
289             DbConnectionManager.closeConnection(pstmt, con);
290         }
291     }
292 
293     @Override
handleProbe(Presence packet)294     public void handleProbe(Presence packet) throws UnauthorizedException {
295         String username = packet.getTo().getNode();
296         try {
297             Roster roster = rosterManager.getRoster(username);
298             RosterItem item = roster.getRosterItem(packet.getFrom());
299             if (item.getSubStatus() == RosterItem.SUB_FROM
300                     || item.getSubStatus() == RosterItem.SUB_BOTH) {
301                 probePresence(packet.getFrom(),  packet.getTo());
302             }
303             else {
304                 PacketError.Condition error = PacketError.Condition.not_authorized;
305                 if ((item.getSubStatus() == RosterItem.SUB_NONE &&
306                         item.getRecvStatus() != RosterItem.RECV_SUBSCRIBE) ||
307                         (item.getSubStatus() == RosterItem.SUB_TO &&
308                         item.getRecvStatus() != RosterItem.RECV_SUBSCRIBE)) {
309                     error = PacketError.Condition.forbidden;
310                 }
311                 Presence presenceToSend = new Presence();
312                 presenceToSend.setError(error);
313                 presenceToSend.setTo(packet.getFrom());
314                 presenceToSend.setFrom(packet.getTo());
315                 deliverer.deliver(presenceToSend);
316             }
317         }
318         catch (UserNotFoundException e) {
319             Presence presenceToSend = new Presence();
320             presenceToSend.setError(PacketError.Condition.forbidden);
321             presenceToSend.setTo(packet.getFrom());
322             presenceToSend.setFrom(packet.getTo());
323             deliverer.deliver(presenceToSend);
324         }
325     }
326 
327     @Override
canProbePresence(JID prober, String probee)328     public boolean canProbePresence(JID prober, String probee) throws UserNotFoundException {
329         if (probee.equals(prober.getNode()) && XMPPServer.getInstance().isLocal(prober)) {
330             return true;
331         }
332         RosterItem item = rosterManager.getRoster(probee).getRosterItem(prober);
333         return item.getSubStatus() == RosterItem.SUB_FROM
334                 || item.getSubStatus() == RosterItem.SUB_BOTH;
335     }
336 
337     @Override
probePresence(JID prober, JID probee)338     public void probePresence(JID prober, JID probee) {
339         try {
340             if (server.isLocal(probee)) {
341                 // Local probers should receive presences of probee in all connected resources
342                 Collection<JID> proberFullJIDs = new ArrayList<>();
343                 if (prober.getResource() == null && server.isLocal(prober)) {
344                     for (ClientSession session : sessionManager.getSessions(prober.getNode())) {
345                         proberFullJIDs.add(session.getAddress());
346                     }
347                 }
348                 else {
349                     proberFullJIDs.add(prober);
350                 }
351                 // If the probee is a local user then don't send a probe to the contact's server.
352                 // But instead just send the contact's presence to the prober
353                 Collection<ClientSession> sessions = sessionManager.getSessions(probee.getNode());
354                 if (sessions.isEmpty()) {
355                     // If the probee is not online then try to retrieve his last unavailable
356                     // presence which may contain particular information and send it to the
357                     // prober
358                     String presenceXML = offlinePresenceCache.get(probee.getNode());
359                     if (presenceXML == null) {
360                         loadOfflinePresence(probee.getNode());
361                     }
362                     presenceXML = offlinePresenceCache.get(probee.getNode());
363                     if (presenceXML != null && !NULL_STRING.equals(presenceXML)) {
364                         try {
365                             // Parse the element
366                             Document element = DocumentHelper.parseText(presenceXML);
367                             // Create the presence from the parsed element
368                             Presence presencePacket = new Presence(element.getRootElement());
369                             presencePacket.setFrom(probee.toBareJID());
370                             // Check if default privacy list of the probee blocks the
371                             // outgoing presence
372                             PrivacyList list = PrivacyListManager.getInstance()
373                                     .getDefaultPrivacyList(probee.getNode());
374                             // Send presence to all prober's resources
375                             for (JID receipient : proberFullJIDs) {
376                                 presencePacket.setTo(receipient);
377                                 if (list == null || !list.shouldBlockPacket(presencePacket)) {
378                                     // Send the presence to the prober
379                                     deliverer.deliver(presencePacket);
380                                 }
381                             }
382                         }
383                         catch (Exception e) {
384                             Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
385                         }
386                     }
387                 }
388                 else {
389                     // The contact is online so send to the prober all the resources where the
390                     // probee is connected
391                     for (ClientSession session : sessions) {
392                         // Create presence to send from probee to prober
393                         Presence presencePacket = session.getPresence().createCopy();
394                         presencePacket.setFrom(session.getAddress());
395                         // Check if a privacy list of the probee blocks the outgoing presence
396                         PrivacyList list = session.getActiveList();
397                         list = list == null ? session.getDefaultList() : list;
398                         // Send presence to all prober's resources
399                         for (JID receipient : proberFullJIDs) {
400                             presencePacket.setTo(receipient);
401                             if (list != null) {
402                                 if (list.shouldBlockPacket(presencePacket)) {
403                                     // Default list blocked outgoing presence so skip this session
404                                     continue;
405                                 }
406                             }
407                             try {
408                                 deliverer.deliver(presencePacket);
409                             }
410                             catch (Exception e) {
411                                 Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
412                             }
413                         }
414                     }
415                 }
416             }
417             else {
418                 if (routingTable.hasComponentRoute(probee)) {
419                     // If the probee belongs to a component then ask the component to process the
420                     // probe presence
421                     Presence presence = new Presence();
422                     presence.setType(Presence.Type.probe);
423                     presence.setFrom(prober);
424                     presence.setTo(probee);
425                     routingTable.routePacket(probee, presence, true);
426                 }
427                 else {
428                     // Check if the probee may be hosted by this server
429                     /*String serverDomain = server.getServerInfo().getName();
430                     if (!probee.getDomain().contains(serverDomain)) {*/
431                     if (server.isRemote(probee)) {
432                         // Send the probe presence to the remote server
433                         Presence probePresence = new Presence();
434                         probePresence.setType(Presence.Type.probe);
435                         probePresence.setFrom(prober);
436                         probePresence.setTo(probee.toBareJID());
437                         // Send the probe presence
438                         deliverer.deliver(probePresence);
439                     }
440                     else {
441                         // The probee may be related to a component that has not yet been connected so
442                         // we will keep a registry of this presence probe. The component will answer
443                         // this presence probe when he becomes online
444                         componentManager.addPresenceRequest(prober, probee);
445                     }
446                 }
447             }
448         }
449         catch (Exception e) {
450             Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
451         }
452     }
453 
454     @Override
sendUnavailableFromSessions(JID recipientJID, JID userJID)455     public void sendUnavailableFromSessions(JID recipientJID, JID userJID) {
456         if (userManager.isRegisteredUser(userJID, false)) {
457             for (ClientSession session : sessionManager.getSessions(userJID.getNode())) {
458                 // Do not send an unavailable presence if the user sent a direct available presence
459                 if (presenceUpdateHandler.hasDirectPresence(session.getAddress(), recipientJID)) {
460                     continue;
461                 }
462                 Presence presencePacket = new Presence();
463                 presencePacket.setType(Presence.Type.unavailable);
464                 presencePacket.setFrom(session.getAddress());
465                 // Ensure that unavailable presence is sent to all receipient's resources
466                 Collection<JID> recipientFullJIDs = new ArrayList<>();
467                 if (server.isLocal(recipientJID)) {
468                     for (ClientSession targetSession : sessionManager
469                             .getSessions(recipientJID.getNode())) {
470                         recipientFullJIDs.add(targetSession.getAddress());
471                     }
472                 }
473                 else {
474                     recipientFullJIDs.add(recipientJID);
475                 }
476                 for (JID jid : recipientFullJIDs) {
477                     presencePacket.setTo(jid);
478                     try {
479                         deliverer.deliver(presencePacket);
480                     }
481                     catch (Exception e) {
482                         Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
483                     }
484                 }
485             }
486         }
487     }
488 
489     @Override
userCreated(User user, Map<String, Object> params)490     public void userCreated(User user, Map<String, Object> params) {
491         // Do nothing
492     }
493 
494     @Override
userDeleting(User user, Map<String, Object> params)495     public void userDeleting(User user, Map<String, Object> params) {
496         // Delete user information
497         deleteOfflinePresenceFromDB(user.getUsername());
498         offlinePresenceCache.remove(user.getUsername());
499         lastActivityCache.remove(user.getUsername());
500     }
501 
502     @Override
userModified(User user, Map<String, Object> params)503     public void userModified(User user, Map<String, Object> params) {
504         // Do nothing
505     }
506 
507     // #####################################################################
508     // Module management
509     // #####################################################################
510 
511     @Override
initialize(XMPPServer server)512     public void initialize(XMPPServer server) {
513         super.initialize(server);
514         this.server = server;
515 
516         offlinePresenceCache = CacheFactory.createCache("Offline Presence Cache");
517         lastActivityCache = CacheFactory.createCache("Last Activity Cache");
518 
519         deliverer = server.getPacketDeliverer();
520         sessionManager = server.getSessionManager();
521         userManager = server.getUserManager();
522         presenceUpdateHandler = server.getPresenceUpdateHandler();
523         rosterManager = server.getRosterManager();
524         routingTable = server.getRoutingTable();
525     }
526 
527     @Override
start()528     public void start() throws IllegalStateException {
529         super.start();
530         // Use component manager for Presence Updates.
531         componentManager = InternalComponentManager.getInstance();
532         // Listen for user deletion events
533         UserEventDispatcher.addListener(this);
534 
535     }
536 
537     @Override
stop()538     public void stop() {
539         // Clear the caches when stopping the module.
540         offlinePresenceCache.clear();
541         lastActivityCache.clear();
542         // Stop listening for user deletion events
543         UserEventDispatcher.removeListener(this);
544     }
545 
546     /**
547      * Loads offline presence data for the user into cache.
548      *
549      * @param username the username.
550      */
loadOfflinePresence(String username)551     private void loadOfflinePresence(String username) {
552         Connection con = null;
553         PreparedStatement pstmt = null;
554         ResultSet rs = null;
555         Lock lock = offlinePresenceCache.getLock(username);
556         lock.lock();
557         try {
558             if (!offlinePresenceCache.containsKey(username) || !lastActivityCache.containsKey(username)) {
559                 con = DbConnectionManager.getConnection();
560                 pstmt = con.prepareStatement(LOAD_OFFLINE_PRESENCE);
561                 pstmt.setString(1, username);
562                 rs = pstmt.executeQuery();
563                 if (rs.next()) {
564                     String offlinePresence = DbConnectionManager.getLargeTextField(rs, 1);
565                     if (rs.wasNull()) {
566                         offlinePresence = NULL_STRING;
567                     }
568                     long offlineDate = Long.parseLong(rs.getString(2).trim());
569                     offlinePresenceCache.put(username, offlinePresence);
570                     lastActivityCache.put(username, offlineDate);
571                 }
572                 else {
573                     offlinePresenceCache.put(username, NULL_STRING);
574                     lastActivityCache.put(username, NULL_LONG);
575                 }
576             }
577         }
578         catch (SQLException sqle) {
579             Log.error(sqle.getMessage(), sqle);
580         }
581         finally {
582             DbConnectionManager.closeConnection(rs, pstmt, con);
583             lock.unlock();
584         }
585     }
586 
587     @Override
serverStarted()588     public void serverStarted() {
589     }
590 
591     @Override
serverStopping()592     public void serverStopping() {
593         for (ClientSession session : XMPPServer.getInstance().getSessionManager().getSessions()) {
594             if (!session.isAnonymousUser()) {
595                 try {
596                     writeToDatabase(session.getUsername(), null, new Date());
597                 } catch (UserNotFoundException e) {
598                     Log.error(e.getMessage(), e);
599                 }
600             }
601         }
602     }
603 }
604