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