1 /*
2  * Copyright (C) 2004-2008 Jive Software, 2021-2022 Ignite Realtime Foundation. 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.muc.spi;
18 
19 import org.dom4j.DocumentHelper;
20 import org.dom4j.Element;
21 import org.dom4j.QName;
22 import org.jivesoftware.openfire.PacketException;
23 import org.jivesoftware.openfire.RoutingTable;
24 import org.jivesoftware.openfire.XMPPServer;
25 import org.jivesoftware.openfire.XMPPServerListener;
26 import org.jivesoftware.openfire.archive.Archiver;
27 import org.jivesoftware.openfire.auth.UnauthorizedException;
28 import org.jivesoftware.openfire.cluster.ClusterEventListener;
29 import org.jivesoftware.openfire.cluster.ClusterManager;
30 import org.jivesoftware.openfire.cluster.NodeID;
31 import org.jivesoftware.openfire.disco.DiscoInfoProvider;
32 import org.jivesoftware.openfire.disco.DiscoItem;
33 import org.jivesoftware.openfire.disco.DiscoItemsProvider;
34 import org.jivesoftware.openfire.disco.DiscoServerItem;
35 import org.jivesoftware.openfire.disco.IQDiscoInfoHandler;
36 import org.jivesoftware.openfire.disco.ServerItemsProvider;
37 import org.jivesoftware.openfire.group.ConcurrentGroupList;
38 import org.jivesoftware.openfire.group.GroupAwareList;
39 import org.jivesoftware.openfire.group.GroupJID;
40 import org.jivesoftware.openfire.handler.IQHandler;
41 import org.jivesoftware.openfire.handler.IQPingHandler;
42 import org.jivesoftware.openfire.muc.CannotBeInvitedException;
43 import org.jivesoftware.openfire.muc.ConflictException;
44 import org.jivesoftware.openfire.muc.ForbiddenException;
45 import org.jivesoftware.openfire.muc.HistoryRequest;
46 import org.jivesoftware.openfire.muc.HistoryStrategy;
47 import org.jivesoftware.openfire.muc.MUCEventDelegate;
48 import org.jivesoftware.openfire.muc.MUCEventDispatcher;
49 import org.jivesoftware.openfire.muc.MUCRole;
50 import org.jivesoftware.openfire.muc.MUCRoom;
51 import org.jivesoftware.openfire.muc.MultiUserChatService;
52 import org.jivesoftware.openfire.muc.NotAcceptableException;
53 import org.jivesoftware.openfire.muc.NotAllowedException;
54 import org.jivesoftware.openfire.muc.RegistrationRequiredException;
55 import org.jivesoftware.openfire.muc.RoomLockedException;
56 import org.jivesoftware.openfire.muc.ServiceUnavailableException;
57 import org.jivesoftware.openfire.muc.cluster.SyncLocalOccupantsAndSendJoinPresenceTask;
58 import org.jivesoftware.openfire.stanzaid.StanzaIDUtil;
59 import org.jivesoftware.openfire.user.UserAlreadyExistsException;
60 import org.jivesoftware.openfire.user.UserManager;
61 import org.jivesoftware.openfire.user.UserNotFoundException;
62 import org.jivesoftware.util.AutoCloseableReentrantLock;
63 import org.jivesoftware.util.JiveGlobals;
64 import org.jivesoftware.util.JiveProperties;
65 import org.jivesoftware.util.LocaleUtils;
66 import org.jivesoftware.util.NotFoundException;
67 import org.jivesoftware.util.TaskEngine;
68 import org.jivesoftware.util.XMPPDateTimeFormat;
69 import org.jivesoftware.util.cache.Cache;
70 import org.jivesoftware.util.cache.CacheFactory;
71 import org.slf4j.Logger;
72 import org.slf4j.LoggerFactory;
73 import org.xmpp.component.Component;
74 import org.xmpp.component.ComponentManager;
75 import org.xmpp.forms.DataForm;
76 import org.xmpp.forms.DataForm.Type;
77 import org.xmpp.forms.FormField;
78 import org.xmpp.packet.IQ;
79 import org.xmpp.packet.JID;
80 import org.xmpp.packet.Message;
81 import org.xmpp.packet.Packet;
82 import org.xmpp.packet.PacketError;
83 import org.xmpp.packet.Presence;
84 import org.xmpp.resultsetmanagement.ResultSet;
85 
86 import javax.annotation.Nonnull;
87 import javax.annotation.Nullable;
88 import java.time.Duration;
89 import java.time.Instant;
90 import java.util.*;
91 import java.util.concurrent.ExecutorService;
92 import java.util.concurrent.Executors;
93 import java.util.concurrent.TimeUnit;
94 import java.util.concurrent.atomic.AtomicInteger;
95 import java.util.concurrent.atomic.AtomicLong;
96 import java.util.concurrent.locks.Lock;
97 import java.util.stream.Collectors;
98 import java.util.stream.Stream;
99 
100 /**
101  * Implements the chat server as a cached memory resident chat server. The server is also
102  * responsible for responding Multi-User Chat disco requests as well as removing inactive users from
103  * the rooms after a period of time and to maintain a log of the conversation in the rooms that
104  * require to log their conversations. The conversations log is saved to the database using a
105  * separate process.
106  * <p>
107  * Temporary rooms are held in memory as long as they have occupants. They will be destroyed after
108  * the last occupant left the room. On the other hand, persistent rooms are always present in memory
109  * even after the last occupant left the room. In order to keep memory clean of persistent rooms that
110  * have been forgotten or abandoned this class includes a clean up process. The clean up process
111  * will remove from memory rooms that haven't had occupants for a while. Moreover, forgotten or
112  * abandoned rooms won't be loaded into memory when the Multi-User Chat service starts up.</p>
113  *
114  * @author Gaston Dombiak
115  */
116 public class MultiUserChatServiceImpl implements Component, MultiUserChatService,
117     ServerItemsProvider, DiscoInfoProvider, DiscoItemsProvider, XMPPServerListener, ClusterEventListener
118 {
119     private static final Logger Log = LoggerFactory.getLogger(MultiUserChatServiceImpl.class);
120 
121     /**
122      * The time to elapse between clearing of idle chat users.
123      */
124     private Duration userIdleTaskInterval;
125 
126     /**
127      * The period that a user must be idle before he/she gets kicked from all the rooms. Null to disable the feature.
128      */
129     private Duration userIdleKick = null;
130 
131     /**
132      * The period that a user must be idle before he/she gets pinged from the rooms that they're in, to determine if
133      * they're a 'ghost'. Null to disable the feature.
134      */
135     private Duration userIdlePing = null;
136 
137     /**
138      * Task that kicks and pings idle users from the rooms.
139      */
140     private UserTimeoutTask userTimeoutTask;
141 
142     /**
143      * The maximum amount of logs to be written to the database in one iteration.
144      */
145     private int logMaxConversationBatchSize;
146 
147     /**
148      * The maximum time between database writes of log batches.
149      */
150     private Duration logMaxBatchInterval;
151 
152     /**
153      * Logs are written to the database almost instantly, but are batched together
154      * when a new log entry becomes available within the amount of time defined
155      * in this field - unless the total amount of time since the last write
156      * is larger then #maxbatchinterval.
157      */
158     private Duration logBatchGracePeriod;
159 
160     /**
161      * the chat service's hostname (subdomain)
162      */
163     private final String chatServiceName;
164     /**
165      * the chat service's description
166      */
167     private String chatDescription;
168 
169     /**
170      * Responsible for maintaining the in-memory collection of MUCRooms for this service.
171      */
172     private final LocalMUCRoomManager localMUCRoomManager;
173 
174     /**
175      * Responsible for maintaining the in-memory collection of MUCUsers for this service.
176      */
177     private final OccupantManager occupantManager;
178 
179     private final HistoryStrategy historyStrategy;
180 
181     private RoutingTable routingTable = null;
182 
183     /**
184      * The handler of packets with namespace jabber:iq:register for the server.
185      */
186     private IQMUCRegisterHandler registerHandler = null;
187 
188     /**
189      * The handler of search requests ('jabber:iq:search' namespace).
190      */
191     private IQMUCSearchHandler searchHandler = null;
192 
193     /**
194      * The handler of search requests ('https://xmlns.zombofant.net/muclumbus/search/1.0' namespace).
195      */
196     private IQMuclumbusSearchHandler muclumbusSearchHandler = null;
197 
198     /**
199      * The handler of VCard requests.
200      */
201     private IQMUCvCardHandler mucVCardHandler = null;
202 
203     /**
204      * Plugin (etc) provided IQ Handlers for MUC:
205      */
206     private Map<String,IQHandler> iqHandlers = null;
207 
208     /**
209      * The total time all agents took to chat *
210      */
211     private long totalChatTime;
212 
213     /**
214      * Flag that indicates if the service should provide information about locked rooms when
215      * handling service discovery requests.
216      * Note: Setting this flag in false is not compliant with the spec. A user may try to join a
217      * locked room thinking that the room doesn't exist because the user didn't discover it before.
218      */
219     private boolean allowToDiscoverLockedRooms = true;
220 
221     /**
222      * Flag that indicates if the service should provide information about non-public members-only
223      * rooms when handling service discovery requests.
224      */
225     private boolean allowToDiscoverMembersOnlyRooms = false;
226 
227     /**
228      * Returns the permission policy for creating rooms. A true value means that not anyone can
229      * create a room. Users are allowed to create rooms only when
230      * <code>isAllRegisteredUsersAllowedToCreate</code> or <code>getUsersAllowedToCreate</code>
231      * (or both) allow them to.
232      */
233     private boolean roomCreationRestricted = false;
234 
235     /**
236      * Determines if all registered users (as opposed to anonymous users, and users from other
237      * XMPP domains) are allowed to create rooms.
238      */
239     private boolean allRegisteredUsersAllowedToCreate = false;
240 
241     /**
242      * Bare jids of users that are allowed to create MUC rooms. Might also include group jids.
243      */
244     private GroupAwareList<JID> allowedToCreate = new ConcurrentGroupList<>();
245 
246     /**
247      * Bare jids of users that are system administrators of the MUC service. A sysadmin has the same
248      * permissions as a room owner. Might also contain group jids.
249      */
250     private GroupAwareList<JID> sysadmins = new ConcurrentGroupList<>();
251 
252     /**
253      * Queue that holds the messages to log for the rooms that need to log their conversations.
254      */
255     private volatile Archiver<ConversationLogEntry> archiver;
256 
257     /**
258      * Max number of hours that a persistent room may be empty before the service removes the
259      * room from memory. Unloaded rooms will exist in the database and may be loaded by a user
260      * request. Default time limit is: 30 days.
261      */
262     private long emptyLimit = 30 * 24;
263 
264     /**
265      * The time to elapse between each rooms cleanup. Default frequency is 60 minutes.
266      */
267     private static final long CLEANUP_FREQUENCY = 60;
268 
269     /**
270      * Total number of received messages in all rooms since the last reset. The counter
271      * is reset each time the Statistic makes a sampling.
272      */
273     private final AtomicInteger inMessages = new AtomicInteger(0);
274     /**
275      * Total number of broadcasted messages in all rooms since the last reset. The counter
276      * is reset each time the Statistic makes a sampling.
277      */
278     private final AtomicLong outMessages = new AtomicLong(0);
279 
280     /**
281      * Flag that indicates if MUC service is enabled.
282      */
283     private boolean serviceEnabled = true;
284 
285     /**
286      * Flag that indicates if MUC service is hidden from services views.
287      */
288     private boolean isHidden;
289 
290     /**
291      * Delegate responds to events for the MUC service.
292      */
293     private MUCEventDelegate mucEventDelegate;
294 
295     /**
296      * Additional features to be added to the disco response for the service.
297      */
298     private final List<String> extraDiscoFeatures = new ArrayList<>();
299 
300     /**
301      * Additional identities to be added to the disco response for the service.
302      */
303     private final List<Element> extraDiscoIdentities = new ArrayList<>();
304 
305     /**
306      * Briefly holds stanza IDs and recipients of IQ ping requests, sent to determine if an occupant is still present in
307      * the room ('ghost user' detection). Maps a stanza ID to the JID that has been pinged. Note that the stanza ID
308      * value should be unique, to not overwrite others.
309      *
310      * The eviction time of entries is short, so that we do not need to bother with manual cache evictions.
311      *
312      * The same cache can be used for all MUC services as there is no service or room-specific data in the cache.
313      */
314     private static final Cache<String, JID> PINGS_SENT = CacheFactory.createCache("MUC Service Pings Sent");
315 
316     /**
317      * Create a new group chat server.
318      *
319      * @param subdomain
320      *            Subdomain portion of the conference services (for example,
321      *            conference for conference.example.org)
322      * @param description
323      *            Short description of service for disco and such. If
324      *            {@code null} or empty, a default value will be used.
325      * @param isHidden
326      *            True if this service should be hidden from services views.
327      * @throws IllegalArgumentException
328      *             if the provided subdomain is an invalid, according to the JID
329      *             domain definition.
330      */
MultiUserChatServiceImpl(final String subdomain, final String description, final Boolean isHidden)331     public MultiUserChatServiceImpl(final String subdomain, final String description, final Boolean isHidden) {
332         // Check subdomain and throw an IllegalArgumentException if its invalid
333         new JID(null,subdomain + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain(), null);
334 
335         this.chatServiceName = subdomain;
336         if (description != null && description.trim().length() > 0) {
337             this.chatDescription = description;
338         }
339         else {
340             this.chatDescription = LocaleUtils.getLocalizedString("muc.service-name");
341         }
342         this.isHidden = isHidden;
343         historyStrategy = new HistoryStrategy(null);
344 
345         localMUCRoomManager = new LocalMUCRoomManager(this);
346         occupantManager = new OccupantManager(this);
347     }
348 
349     @Override
350     @Nonnull
getOccupantManager()351     public OccupantManager getOccupantManager() {
352         return occupantManager;
353     }
354 
355     @Override
addIQHandler(final IQHandler iqHandler)356     public void addIQHandler(final IQHandler iqHandler) {
357         if (this.iqHandlers == null) {
358             this.iqHandlers = new HashMap<>();
359         }
360         this.iqHandlers.put(iqHandler.getInfo().getNamespace(), iqHandler);
361     }
362 
363     @Override
removeIQHandler(final IQHandler iqHandler)364     public void removeIQHandler(final IQHandler iqHandler) {
365         if (this.iqHandlers != null) {
366             if (iqHandler == this.iqHandlers.get(iqHandler.getInfo().getNamespace())) {
367                 this.iqHandlers.remove(iqHandler.getInfo().getNamespace());
368             }
369         }
370     }
371 
372     @Override
getDescription()373     public String getDescription() {
374         return chatDescription;
375     }
376 
setDescription(final String desc)377     public void setDescription(final String desc) {
378         this.chatDescription = desc;
379     }
380 
381     @Override
processPacket(final Packet packet)382     public void processPacket(final Packet packet) {
383 
384         Log.trace( "Routing stanza: {}", packet.toXML() );
385         if (!isServiceEnabled()) {
386             Log.trace( "Service is disabled. Ignoring stanza." );
387             return;
388         }
389         // The MUC service will receive all the packets whose domain matches the domain of the MUC
390         // service. This means that, for instance, a disco request should be responded by the
391         // service itself instead of relying on the server to handle the request.
392         try {
393             // Check for IQ Ping responses
394             if (isPendingPingResponse(packet)) {
395                 Log.debug("Ping response received from occupant '{}', addressed to: '{}'", packet.getFrom(), packet.getTo());
396                 return;
397             }
398             // Check for 'ghost' users (OF-910 / OF-2209 / OF-2369)
399             if (isDeliveryRelatedErrorResponse(packet)) {
400                 Log.info("Received a stanza that contained a delivery-related error response from {}. This is indicative of a 'ghost' user. Removing this user from all chat rooms.", packet.getFrom());
401                 removeChatUser(packet.getFrom());
402                 return;
403             }
404             // Check if the packet is a disco request or a packet with namespace iq:register
405             if (packet instanceof IQ) {
406                 if (process((IQ)packet)) {
407                     Log.trace( "Done processing IQ stanza." );
408                     return;
409                 }
410             }
411 
412             if ( packet.getTo().getNode() == null )
413             {
414                 Log.trace( "Stanza was addressed at the service itself, which by now should have been handled." );
415                 if ( packet instanceof IQ && ((IQ) packet).isRequest() )
416                 {
417                     final IQ reply = IQ.createResultIQ( (IQ) packet );
418                     reply.setChildElement( ((IQ) packet).getChildElement().createCopy() );
419                     reply.setError( PacketError.Condition.feature_not_implemented );
420                     XMPPServer.getInstance().getPacketRouter().route( reply );
421                 }
422                 Log.debug( "Ignoring stanza addressed at conference service: {}", packet.toXML() );
423             }
424             else
425             {
426                 Log.trace( "The stanza is a normal packet that should possibly be sent to the room." );
427                 final JID recipient = packet.getTo();
428                 final String roomName = recipient != null ? recipient.getNode() : null;
429                 final JID userJid = packet.getFrom();
430                 occupantManager.registerActivity(userJid);
431                 Log.trace( "Stanza recipient: {}, room name: {}, sender: {}", recipient, roomName, userJid );
432                 try (final AutoCloseableReentrantLock.AutoCloseableLock ignored = new AutoCloseableReentrantLock(MultiUserChatServiceImpl.class, userJid.toString()).lock()) {
433                     if ( !packet.getElement().elements(FMUCHandler.FMUC).isEmpty() ) {
434                         Log.trace( "Stanza is a FMUC stanza." );
435                         if (roomName == null) {
436                             Log.warn("Unable to process FMUC stanza, as it does not address a room: {}", packet.toXML());
437                         } else {
438                             final Lock lock = getChatRoomLock(roomName);
439                             lock.lock();
440                             try {
441                                 final MUCRoom chatRoom = getChatRoom(roomName);
442                                 if (chatRoom != null) {
443                                     chatRoom.getFmucHandler().process(packet);
444                                     // Ensure that other cluster nodes see the changes applied by the method above.
445                                     syncChatRoom(chatRoom);
446                                 } else {
447                                     Log.warn("Unable to process FMUC stanza, as room it's addressed to does not exist: {}", roomName);
448                                     // FIXME need to send error back in case of IQ request, and FMUC join. Might want to send error back in other cases too.
449                                 }
450                             } finally {
451                                 lock.unlock();
452                             }
453                         }
454                     } else {
455                         Log.trace( "Stanza is a regular MUC stanza." );
456                         processRegularStanza(packet);
457                     }
458                 }
459             }
460         }
461         catch (final Exception e) {
462             Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
463         }
464     }
465 
466     /**
467      * Returns true if the IQ packet was processed. This method should only process disco packets
468      * as well as jabber:iq:register packets sent to the MUC service.
469      *
470      * @param iq the IQ packet to process.
471      * @return true if the IQ packet was processed.
472      */
process(final IQ iq)473     private boolean process(final IQ iq) {
474         final Element childElement = iq.getChildElement();
475         String namespace = null;
476         // Ignore IQs of type ERROR
477         if (IQ.Type.error == iq.getType()) {
478             return false;
479         }
480         if (iq.getTo().getResource() != null) {
481             // Ignore IQ packets sent to room occupants
482             return false;
483         }
484         if (childElement != null) {
485             namespace = childElement.getNamespaceURI();
486         }
487         if ("jabber:iq:register".equals(namespace)) {
488             final IQ reply = registerHandler.handleIQ(iq);
489             XMPPServer.getInstance().getPacketRouter().route(reply);
490         }
491         else if ("jabber:iq:search".equals(namespace)) {
492             final IQ reply = searchHandler.handleIQ(iq);
493             XMPPServer.getInstance().getPacketRouter().route(reply);
494         }
495         else if (IQMuclumbusSearchHandler.NAMESPACE.equals(namespace)) {
496             final IQ reply = muclumbusSearchHandler.handleIQ(iq);
497             XMPPServer.getInstance().getPacketRouter().route(reply);
498         }
499         else if (IQMUCvCardHandler.NAMESPACE.equals(namespace)) {
500             final IQ reply = mucVCardHandler.handleIQ(iq);
501             XMPPServer.getInstance().getPacketRouter().route(reply);
502         }
503         else if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
504             // TODO MUC should have an IQDiscoInfoHandler of its own when MUC becomes
505             // a component
506             final IQ reply = XMPPServer.getInstance().getIQDiscoInfoHandler().handleIQ(iq);
507             XMPPServer.getInstance().getPacketRouter().route(reply);
508         }
509         else if ("http://jabber.org/protocol/disco#items".equals(namespace)) {
510             // TODO MUC should have an IQDiscoItemsHandler of its own when MUC becomes
511             // a component
512             final IQ reply = XMPPServer.getInstance().getIQDiscoItemsHandler().handleIQ(iq);
513             XMPPServer.getInstance().getPacketRouter().route(reply);
514         }
515         else if ("urn:xmpp:ping".equals(namespace)) {
516             XMPPServer.getInstance().getPacketRouter().route( IQ.createResultIQ(iq) );
517         }
518         else if (this.iqHandlers != null) {
519             final IQHandler h = this.iqHandlers.get(namespace);
520             if (h != null) {
521                 try {
522                     final IQ reply = h.handleIQ(iq);
523                     if (reply != null) {
524                         XMPPServer.getInstance().getPacketRouter().route(reply);
525                     }
526                 } catch (final UnauthorizedException e) {
527                     final IQ reply = IQ.createResultIQ(iq);
528                     reply.setType(IQ.Type.error);
529                     reply.setError(PacketError.Condition.service_unavailable);
530                     XMPPServer.getInstance().getPacketRouter().route(reply);
531                 }
532                 return true;
533             }
534             return false;
535         } else {
536             return false;
537         }
538         return true;
539     }
540 
541     /**
542      * Generate and send an error packet to indicate that something went wrong.
543      *
544      * @param packet  the packet to be responded to with an error.
545      * @param error   the reason why the operation failed.
546      * @param message an optional human-readable error message.
547      */
sendErrorPacket( Packet packet, PacketError.Condition error, String message )548     private void sendErrorPacket( Packet packet, PacketError.Condition error, String message )
549     {
550         if (packet.getError() != null) {
551             Log.debug("Avoid generating an error in response to a stanza that itself has an error (to avoid the chance of entering an endless back-and-forth of exchanging errors). Suppress sending an {} error (with message '{}') in response to: {}", error, message, packet);
552             return;
553         }
554         if ( packet instanceof IQ )
555         {
556             IQ reply = IQ.createResultIQ((IQ) packet);
557             reply.setChildElement(((IQ) packet).getChildElement().createCopy());
558             reply.setError(error);
559             if ( message != null )
560             {
561                 reply.getError().setText(message);
562             }
563             XMPPServer.getInstance().getPacketRouter().route(reply);
564         }
565         else
566         {
567             Packet reply = packet.createCopy();
568             reply.setError(error);
569             if ( message != null )
570             {
571                 reply.getError().setText(message);
572             }
573             reply.setFrom(packet.getTo());
574             reply.setTo(packet.getFrom());
575             XMPPServer.getInstance().getPacketRouter().route(reply);
576         }
577     }
578 
579     /**
580      * This method does all stanz routing in the chat server for 'regular' MUC stanzas. Packet routing is actually very
581      * simple:
582      *
583      * <ul>
584      *   <li>Discover the room the user is talking to</li>
585      *   <li>If the room is not registered and this is a presence "available" packet, try to join the room</li>
586      *   <li>If the room is registered, and presence "unavailable" leave the room</li>
587      *   <li>Otherwise, rewrite the sender address and send to the room.</li>
588      * </ul>
589      *
590      * @param packet The stanza to route
591      */
processRegularStanza( Packet packet )592     public void processRegularStanza( Packet packet ) throws UnauthorizedException, PacketException
593     {
594         // Name of the room that the stanza is addressed to.
595         final String roomName = packet.getTo().getNode();
596 
597         if ( roomName == null )
598         {
599             // Packets to the groupchat service (as opposed to a specific room on the service). This should not occur
600             // (should be handled by MultiUserChatServiceImpl instead).
601             Log.warn(LocaleUtils.getLocalizedString("muc.error.not-supported") + " " + packet.toString());
602             if ( packet instanceof IQ && ((IQ) packet).isRequest() )
603             {
604                 sendErrorPacket(packet, PacketError.Condition.feature_not_implemented, "Unable to process stanza.");
605             }
606             return;
607         }
608 
609         StanzaIDUtil.ensureUniqueAndStableStanzaID(packet, packet.getTo().asBareJID());
610 
611         final Lock lock = getChatRoomLock(roomName);
612         lock.lock();
613         try {
614             // Get the room, if one exists.
615             @Nullable MUCRoom room = getChatRoom(roomName);
616 
617             // Determine if this user has a pre-existing role in the addressed room.
618             final MUCRole preExistingRole;
619             if (room == null) {
620                 preExistingRole = null;
621             } else {
622                 preExistingRole = room.getOccupantByFullJID(packet.getFrom());
623             }
624             Log.debug("Preexisting role for user {} in room {} (that currently {} exist): {}", packet.getFrom(), roomName, room == null ? "does not" : "does", preExistingRole == null ? "(none)" : preExistingRole);
625 
626             if ( packet instanceof IQ )
627             {
628                 process((IQ) packet, room, preExistingRole);
629             }
630             else if ( packet instanceof Message )
631             {
632                 process((Message) packet, room, preExistingRole);
633             }
634             else if ( packet instanceof Presence )
635             {
636                 // Return value is non-null while argument is, in case this is a request to create a new room.
637                 room = process((Presence) packet, roomName, room, preExistingRole);
638 
639             }
640 
641             // Ensure that other cluster nodes see any changes that might have been applied.
642             if (room != null) {
643                 syncChatRoom(room);
644             }
645         } finally {
646             lock.unlock();
647         }
648     }
649 
650     /**
651      * Processes a Message stanza.
652      *
653      * @param packet          The stanza to route
654      * @param room            The room that the stanza was addressed to.
655      * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any.
656      */
process( @onnull final Message packet, @Nullable final MUCRoom room, @Nullable final MUCRole preExistingRole )657     private void process(
658         @Nonnull final Message packet,
659         @Nullable final MUCRoom room,
660         @Nullable final MUCRole preExistingRole )
661     {
662         if (Message.Type.error == packet.getType()) {
663             Log.trace("Ignoring messages of type 'error' sent by '{}' to MUC room '{}'", packet.getFrom(), packet.getTo());
664             return;
665         }
666 
667         if (room == null) {
668             Log.debug("Rejecting message stanza sent by '{}' to room '{}': Room does not exist.", packet.getFrom(), packet.getTo());
669             sendErrorPacket(packet, PacketError.Condition.recipient_unavailable, "The room that the message was addressed to is not available.");
670             return;
671         }
672 
673         if ( preExistingRole == null )
674         {
675             processNonOccupantMessage(packet, room);
676         }
677         else
678         {
679             processOccupantMessage(packet, room, preExistingRole);
680         }
681     }
682 
683     /**
684      * Processes a Message stanza that was sent by a user that's not in the room.
685      *
686      * Only declined invitations (to join a room) are acceptable messages from users that are not in the room. Other
687      * messages are responded to with an error.
688      *
689      * @param packet   The stanza to process
690      * @param room     The room that the stanza was addressed to.
691      */
processNonOccupantMessage( @onnull final Message packet, @Nonnull final MUCRoom room )692     private void processNonOccupantMessage(
693         @Nonnull final Message packet,
694         @Nonnull final MUCRoom room )
695     {
696         boolean declinedInvitation = false;
697         Element userInfo = null;
698         if ( Message.Type.normal == packet.getType() )
699         {
700             // An user that is not an occupant could be declining an invitation
701             userInfo = packet.getChildElement("x", "http://jabber.org/protocol/muc#user");
702             if ( userInfo != null && userInfo.element("decline") != null )
703             {
704                 // A user has declined an invitation to a room
705                 // WARNING: Potential fraud if someone fakes the "from" of the
706                 // message with the JID of a member and sends a "decline"
707                 declinedInvitation = true;
708             }
709         }
710 
711         if ( declinedInvitation )
712         {
713             Log.debug("Processing room invitation declination sent by '{}' to room '{}'.", packet.getFrom(), room.getName());
714             final Element info = userInfo.element("decline");
715             room.sendInvitationRejection(
716                 new JID(info.attributeValue("to")),
717                 info.elementTextTrim("reason"),
718                 packet.getFrom());
719         }
720         else
721         {
722             Log.debug("Rejecting message stanza sent by '{}' to room '{}': Sender is not an occupant of the room: {}", packet.getFrom(), room.getName(), packet.toXML());
723             sendErrorPacket(packet, PacketError.Condition.not_acceptable, "You are not in the room.");
724         }
725     }
726 
727     /**
728      * Processes a Message stanza that was sent by a user that's in the room.
729      *
730      * @param packet          The stanza to process
731      * @param room            The room that the stanza was addressed to.
732      * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any.
733      */
processOccupantMessage( @onnull final Message packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )734     private void processOccupantMessage(
735         @Nonnull final Message packet,
736         @Nonnull final MUCRoom room,
737         @Nonnull final MUCRole preExistingRole )
738     {
739         // Check and reject conflicting packets with conflicting roles In other words, another user already has this nickname
740         if ( !preExistingRole.getUserAddress().equals(packet.getFrom()) )
741         {
742             Log.debug("Rejecting conflicting stanza with conflicting roles: {}", packet.toXML());
743             sendErrorPacket(packet, PacketError.Condition.conflict, "Another user uses this nickname.");
744             return;
745         }
746 
747         if (room.getRoomHistory().isSubjectChangeRequest(packet))
748         {
749             processChangeSubjectMessage(packet, room, preExistingRole);
750             return;
751         }
752 
753         // An occupant is trying to send a private message, send public message, invite someone to the room or reject an invitation.
754         final Message.Type type = packet.getType();
755         String nickname = packet.getTo().getResource();
756         if ( nickname == null || nickname.trim().length() == 0 )
757         {
758             nickname = null;
759         }
760 
761         // Public message (not addressed to a specific occupant)
762         if ( nickname == null && Message.Type.groupchat == type )
763         {
764             processPublicMessage(packet, room, preExistingRole);
765             return;
766         }
767 
768         // Private message (addressed to a specific occupant)
769         if ( nickname != null && (Message.Type.chat == type || Message.Type.normal == type) )
770         {
771             processPrivateMessage(packet, room, preExistingRole);
772             return;
773         }
774 
775         if ( nickname == null && Message.Type.normal == type )
776         {
777             // An occupant could be sending an invitation or declining an invitation
778             final Element userInfo = packet.getChildElement("x", "http://jabber.org/protocol/muc#user");
779 
780             if ( userInfo != null && userInfo.element("invite") != null )
781             {
782                 // An occupant is sending invitations
783                 processSendingInvitationMessage(packet, room, preExistingRole);
784                 return;
785             }
786 
787             if ( userInfo != null && userInfo.element("decline") != null )
788             {
789                 // An occupant has declined an invitation
790                 processDecliningInvitationMessage(packet, room);
791                 return;
792             }
793         }
794 
795         Log.debug("Unable to process message: {}", packet.toXML());
796         sendErrorPacket(packet, PacketError.Condition.bad_request, "Unable to process message.");
797     }
798 
799     /**
800      * Process a 'change subject' message sent by an occupant of the room.
801      *
802      * @param packet          The stanza to process
803      * @param room            The room that the stanza was addressed to.
804      * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any.
805      */
processChangeSubjectMessage( @onnull final Message packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )806     private void processChangeSubjectMessage(
807         @Nonnull final Message packet,
808         @Nonnull final MUCRoom room,
809         @Nonnull final MUCRole preExistingRole )
810     {
811         Log.trace("Processing subject change request from occupant '{}' to room '{}'.", packet.getFrom(), room.getName());
812         try
813         {
814             room.changeSubject(packet, preExistingRole);
815         }
816         catch ( ForbiddenException e )
817         {
818             Log.debug("Rejecting subject change request from occupant '{}' to room '{}'.", packet.getFrom(), room.getName(), e);
819             sendErrorPacket(packet, PacketError.Condition.forbidden, "You are not allowed to change the subject of this room.");
820         }
821     }
822 
823     /**
824      * Process a public message sent by an occupant of the room.
825      *
826      * @param packet          The stanza to process
827      * @param room            The room that the stanza was addressed to.
828      * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any.
829      */
processPublicMessage( @onnull final Message packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )830     private void processPublicMessage(
831         @Nonnull final Message packet,
832         @Nonnull final MUCRoom room,
833         @Nonnull final MUCRole preExistingRole )
834     {
835         Log.trace("Processing public message from occupant '{}' to room '{}'.", packet.getFrom(), room.getName());
836         try
837         {
838             room.sendPublicMessage(packet, preExistingRole);
839         }
840         catch ( ForbiddenException e )
841         {
842             Log.debug("Rejecting public message from occupant '{}' to room '{}'. User is not allowed to send message (might not have voice).", packet.getFrom(), room.getName(), e);
843             sendErrorPacket(packet, PacketError.Condition.forbidden, "You are not allowed to send a public message to the room (you might require 'voice').");
844         }
845     }
846 
847     /**
848      * Process a private message sent by an occupant of the room.
849      *
850      * @param packet          The stanza to process
851      * @param room            The room that the stanza was addressed to.
852      * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any.
853      */
processPrivateMessage( @onnull final Message packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )854     private void processPrivateMessage(
855         @Nonnull final Message packet,
856         @Nonnull final MUCRoom room,
857         @Nonnull final MUCRole preExistingRole )
858     {
859         Log.trace("Processing private message from occupant '{}' to room '{}'.", packet.getFrom(), room.getName());
860         try
861         {
862             room.sendPrivatePacket(packet, preExistingRole);
863         }
864         catch ( ForbiddenException e )
865         {
866             Log.debug("Rejecting private message from occupant '{}' to room '{}'. User has a role that disallows sending private messages in this room.", packet.getFrom(), room.getName(), e);
867             sendErrorPacket(packet, PacketError.Condition.forbidden, "You are not allowed to send a private messages in the room.");
868         }
869         catch ( NotFoundException e )
870         {
871             Log.debug("Rejecting private message from occupant '{}' to room '{}'. User addressing a non-existent recipient.", packet.getFrom(), room.getName(), e);
872             sendErrorPacket(packet, PacketError.Condition.recipient_unavailable, "The intended recipient of your private message is not available.");
873         }
874     }
875 
876     /**
877      * Process a room-invitation message sent by an occupant of the room.
878      *
879      * @param packet          The stanza to process
880      * @param room            The room that the stanza was addressed to.
881      * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any.
882      */
processSendingInvitationMessage( @onnull final Message packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )883     private void processSendingInvitationMessage(
884         @Nonnull final Message packet,
885         @Nonnull final MUCRoom room,
886         @Nonnull final MUCRole preExistingRole )
887     {
888         Log.trace("Processing an invitation message from occupant '{}' to room '{}'.", packet.getFrom(), room.getName());
889         try
890         {
891             final Element userInfo = packet.getChildElement("x", "http://jabber.org/protocol/muc#user");
892 
893             // Try to keep the list of extensions sent together with the message invitation. These extensions will be sent to the invitees.
894             final List<Element> extensions = new ArrayList<>(packet.getElement().elements());
895             extensions.remove(userInfo);
896 
897             // Send invitations to invitees
898             final Iterator<Element> it = userInfo.elementIterator("invite");
899             while ( it.hasNext() )
900             {
901                 Element info = it.next();
902                 JID jid = new JID(info.attributeValue("to"));
903 
904                 // Add the user as a member of the room if the room is members only
905                 if (room.isMembersOnly())
906                 {
907                     room.addMember(jid, null, preExistingRole);
908                 }
909 
910                 // Send the invitation to the invitee
911                 room.sendInvitation(jid, info.elementTextTrim("reason"), preExistingRole, extensions);
912             }
913         }
914         catch ( ForbiddenException e )
915         {
916             Log.debug("Rejecting invitation message from occupant '{}' in room '{}': Invitations are not allowed, or occupant is not allowed to modify the member list.", packet.getFrom(), room.getName(), e);
917             sendErrorPacket(packet, PacketError.Condition.forbidden, "This room disallows invitations to be sent, or you're not allowed to modify the member list of this room.");
918         }
919         catch ( ConflictException e )
920         {
921             Log.debug("Rejecting invitation message from occupant '{}' in room '{}'.", packet.getFrom(), room.getName(), e);
922             sendErrorPacket(packet, PacketError.Condition.conflict, "An unexpected exception occurred."); // TODO Is this code reachable?
923         }
924         catch ( CannotBeInvitedException e )
925         {
926             Log.debug("Rejecting invitation message from occupant '{}' in room '{}': The user being invited does not have access to the room.", packet.getFrom(), room.getName(), e);
927             sendErrorPacket(packet, PacketError.Condition.not_acceptable, "The user being invited does not have access to the room.");
928         }
929     }
930 
931     /**
932      * Process a declination of a room-invitation message sent by an occupant of the room.
933      *
934      * @param packet          The stanza to process
935      * @param room            The room that the stanza was addressed to.
936      */
processDecliningInvitationMessage( @onnull final Message packet, @Nonnull final MUCRoom room)937     private void processDecliningInvitationMessage(
938         @Nonnull final Message packet,
939         @Nonnull final MUCRoom room)
940     {
941         Log.trace("Processing an invite declination message from '{}' to room '{}'.", packet.getFrom(), room.getName());
942         final Element info = packet.getChildElement("x", "http://jabber.org/protocol/muc#user").element("decline");
943         room.sendInvitationRejection(new JID(info.attributeValue("to")),
944             info.elementTextTrim("reason"), packet.getFrom());
945     }
946 
947     /**
948      * Processes an IQ stanza.
949      *
950      * @param packet          The stanza to route
951      * @param room            The room that the stanza was addressed to.
952      * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any.
953      */
process( @onnull final IQ packet, @Nullable final MUCRoom room, @Nullable final MUCRole preExistingRole )954     private void process(
955         @Nonnull final IQ packet,
956         @Nullable final MUCRoom room,
957         @Nullable final MUCRole preExistingRole )
958     {
959         // Packets to a specific node/group/room
960         if ( preExistingRole == null || room == null)
961         {
962             Log.debug("Ignoring stanza received from a non-occupant of a room (room might not even exist): {}", packet.toXML());
963             if ( packet.isRequest() )
964             {
965                 // If a non-occupant sends a disco to an address of the form <room@service/nick>, a MUC service MUST
966                 // return a <bad-request/> error. http://xmpp.org/extensions/xep-0045.html#disco-occupant
967                 sendErrorPacket(packet, PacketError.Condition.bad_request, "You are not an occupant of this room.");
968             }
969             return;
970         }
971 
972         if ( packet.isResponse() )
973         {
974             // Only process IQ result packet if it's a private packet sent to another room occupant
975             if ( packet.getTo().getResource() != null )
976             {
977                 try
978                 {
979                     // User is sending an IQ result packet to another room occupant
980                     room.sendPrivatePacket(packet, preExistingRole);
981                 }
982                 catch ( NotFoundException | ForbiddenException e )
983                 {
984                     // Do nothing. No error will be sent to the sender of the IQ result packet
985                     Log.debug("Silently ignoring an IQ response sent to the room as a private message that caused an exception while being processed: {}", packet.toXML(), e);
986                 }
987             }
988             else
989             {
990                 Log.trace("Silently ignoring an IQ response sent to the room, but not as a private message: {}", packet.toXML());
991             }
992         }
993         else
994         {
995             // Check and reject conflicting packets with conflicting roles In other words, another user already has this nickname
996             if ( !preExistingRole.getUserAddress().equals(packet.getFrom()) )
997             {
998                 Log.debug("Rejecting conflicting stanza with conflicting roles: {}", packet.toXML());
999                 sendErrorPacket(packet, PacketError.Condition.conflict, "Another user uses this nickname.");
1000                 return;
1001             }
1002 
1003             try
1004             {
1005                 // TODO Analyze if it is correct for these first two blocks to be processed without evaluating if they're addressed to the room or if they're a PM.
1006                 Element query = packet.getElement().element("query");
1007                 if ( query != null && "http://jabber.org/protocol/muc#owner".equals(query.getNamespaceURI()) )
1008                 {
1009                     room.getIQOwnerHandler().handleIQ(packet, preExistingRole);
1010                 }
1011                 else if ( query != null && "http://jabber.org/protocol/muc#admin".equals(query.getNamespaceURI()) )
1012                 {
1013                     room.getIQAdminHandler().handleIQ(packet, preExistingRole);
1014                 }
1015                 else
1016                 {
1017                     final String toNickname = packet.getTo().getResource();
1018                     if ( toNickname != null )
1019                     {
1020                         // User is sending to a room occupant.
1021                         final boolean selfPingEnabled = JiveGlobals.getBooleanProperty("xmpp.muc.self-ping.enabled", true);
1022                         if ( selfPingEnabled && toNickname.equals(preExistingRole.getNickname()) && packet.isRequest()
1023                             && packet.getElement().element(QName.get(IQPingHandler.ELEMENT_NAME, IQPingHandler.NAMESPACE)) != null )
1024                         {
1025                             Log.trace("User '{}' is sending an IQ 'ping' to itself. See XEP-0410: MUC Self-Ping (Schrödinger's Chat).", packet.getFrom());
1026                             XMPPServer.getInstance().getPacketRouter().route(IQ.createResultIQ(packet));
1027                         }
1028                         else
1029                         {
1030                             Log.trace("User '{}' is sending an IQ stanza to another room occupant (as a PM) with nickname: '{}'.", packet.getFrom(), toNickname);
1031                             room.sendPrivatePacket(packet, preExistingRole);
1032                         }
1033                     }
1034                     else
1035                     {
1036                         Log.debug("An IQ request was addressed to the MUC room '{}' which cannot answer it: {}", room.getName(), packet.toXML());
1037                         sendErrorPacket(packet, PacketError.Condition.bad_request, "IQ request cannot be processed by the MUC room itself.");
1038                     }
1039                 }
1040             }
1041             catch ( NotAcceptableException e )
1042             {
1043                 Log.debug("Unable to process IQ stanza: room requires a password, but none was supplied.", e);
1044                 sendErrorPacket(packet, PacketError.Condition.not_acceptable, "Room requires a password, but none was supplied.");
1045             }
1046             catch ( ForbiddenException e )
1047             {
1048                 Log.debug("Unable to process IQ stanza: sender don't have authorization to perform the request.", e);
1049                 sendErrorPacket(packet, PacketError.Condition.forbidden, "You don't have authorization to perform this request.");
1050             }
1051             catch ( NotFoundException e )
1052             {
1053                 Log.debug("Unable to process IQ stanza: the intended recipient is not available.", e);
1054                 sendErrorPacket(packet, PacketError.Condition.recipient_unavailable, "The intended recipient is not available.");
1055             }
1056             catch ( ConflictException e )
1057             {
1058                 Log.debug("Unable to process IQ stanza: processing this request would leave the room in an invalid state (eg: without owners).", e);
1059                 sendErrorPacket(packet, PacketError.Condition.conflict, "Processing this request would leave the room in an invalid state (eg: without owners).");
1060             }
1061             catch ( NotAllowedException e )
1062             {
1063                 Log.debug("Unable to process IQ stanza: an owner or administrator cannot be banned from the room.", e);
1064                 sendErrorPacket(packet, PacketError.Condition.not_allowed, "An owner or administrator cannot be banned from the room.");
1065             }
1066             catch ( CannotBeInvitedException e )
1067             {
1068                 Log.debug("Unable to process IQ stanza: user being invited as a result of being added to a members-only room still does not have permission.", e);
1069                 sendErrorPacket(packet, PacketError.Condition.not_acceptable, "User being invited as a result of being added to a members-only room still does not have permission.");
1070             }
1071             catch ( Exception e )
1072             {
1073                 Log.error("An unexpected exception occurred while processing IQ stanza: {}", packet.toXML(), e);
1074                 sendErrorPacket(packet, PacketError.Condition.internal_server_error, "An unexpected exception occurred while processing your request.");
1075             }
1076         }
1077     }
1078 
1079     /**
1080      * Process a Presence stanza.
1081      *
1082      * This method might be invoked for a room that does not yet exist (when the presence is a room-creation request).
1083      * This is why this method, unlike the process methods for Message and IQ stanza takes a <em>room name</em> argument
1084      * and returns the room that processed to request.
1085      *
1086      * @param packet          The stanza to process.
1087      * @param roomName        The name of the room that the stanza was addressed to.
1088      * @param room            The room that the stanza was addressed to, if it exists.
1089      * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any.
1090      * @return the room that handled the request
1091      */
1092     @Nullable
process( @onnull final Presence packet, @Nonnull final String roomName, @Nullable final MUCRoom room, @Nullable MUCRole preExistingRole )1093     private MUCRoom process(
1094         @Nonnull final Presence packet,
1095         @Nonnull final String roomName,
1096         @Nullable final MUCRoom room,
1097         @Nullable MUCRole preExistingRole )
1098     {
1099         final Element mucInfo = packet.getChildElement("x", "http://jabber.org/protocol/muc"); // only sent in initial presence
1100         final String nickname = packet.getTo().getResource() == null
1101             || packet.getTo().getResource().trim().isEmpty() ? null
1102             : packet.getTo().getResource().trim();
1103 
1104         if ( preExistingRole == null && Presence.Type.unavailable == packet.getType() ) {
1105 
1106             // This is for clustering scenarios where one node could already have cleaned up the clustered cache,
1107             // but the local node still needs to process the 'unavailable' presence of the leaving occupant.
1108             final MUCRoom localRoom = localMUCRoomManager.getLocalRooms().get(roomName);
1109             if (localRoom != null) {
1110                 preExistingRole = localRoom.getOccupantByFullJID(packet.getFrom());
1111             }
1112 
1113             if (preExistingRole == null) {
1114                 Log.debug("Silently ignoring user '{}' leaving a room that it has no role in '{}' (was the room just destroyed)?", packet.getFrom(), roomName);
1115                 return null;
1116             } else {
1117                 Log.debug("NOT silently ignoring user {} leaving a room. Sending 'unavailable' presence for room {} because the occupant was still present in the local room cache", packet.getFrom(), roomName);
1118             }
1119         }
1120         if ( preExistingRole == null || (mucInfo != null && preExistingRole.getNickname().equalsIgnoreCase(nickname) ) )
1121         {
1122             // If we're not already in a room (role == null), we either are joining it or it's not properly addressed and we drop it silently.
1123             // Alternative is that mucInfo is not null, in which case the client thinks it isn't in the room, so we should join anyway.
1124             return processRoomJoinRequest(packet, roomName, room, nickname);
1125         }
1126         else
1127         {
1128             // Check and reject conflicting packets with conflicting roles
1129             // In other words, another user already has this nickname
1130             if ( !preExistingRole.getUserAddress().equals(packet.getFrom()) )
1131             {
1132                 Log.debug("Rejecting conflicting stanza with conflicting roles: {}", packet.toXML());
1133                 sendErrorPacket(packet, PacketError.Condition.conflict, "Another user uses this nickname.");
1134                 return room;
1135             }
1136 
1137             if (room == null) {
1138                 if (Presence.Type.unavailable == packet.getType()) {
1139                     Log.debug("Silently ignoring user '{}' leaving a non-existing room '{}' (was the room just destroyed)?", packet.getFrom(), roomName);
1140                 } else {
1141                     Log.warn("Unable to process presence update from user '{}' to a non-existing room: {}", packet.getFrom(), roomName);
1142                 }
1143                 return null;
1144             }
1145             try
1146             {
1147                 if ( nickname != null && !preExistingRole.getNickname().equalsIgnoreCase(nickname) && Presence.Type.unavailable != packet.getType() )
1148                 {
1149                     // Occupant has changed his nickname. Send two presences to each room occupant.
1150                     processNickNameChange(packet, room, preExistingRole, nickname);
1151                 }
1152                 else
1153                 {
1154                     processPresenceUpdate(packet, room, preExistingRole);
1155                 }
1156             }
1157             catch ( Exception e )
1158             {
1159                 Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
1160             }
1161             return room;
1162         }
1163     }
1164 
1165     /**
1166      * Process a request to join a room.
1167      *
1168      * This method might be invoked for a room that does not yet exist (when the presence is a room-creation request).
1169      *
1170      * @param packet   The stanza representing the nickname-change request.
1171      * @param roomName The name of the room that the stanza was addressed to.
1172      * @param room     The room that the stanza was addressed to, if it exists.
1173      * @param nickname The requested nickname.
1174      * @return the room that handled the request
1175      */
processRoomJoinRequest( @onnull final Presence packet, @Nonnull final String roomName, @Nullable MUCRoom room, @Nullable String nickname )1176     private MUCRoom processRoomJoinRequest(
1177         @Nonnull final Presence packet,
1178         @Nonnull final String roomName,
1179         @Nullable MUCRoom room,
1180         @Nullable String nickname )
1181     {
1182         Log.trace("Processing join request from '{}' for room '{}'", packet.getFrom(), roomName);
1183 
1184         if ( nickname == null )
1185         {
1186             Log.debug("Request from '{}' to join room '{}' rejected: request did not specify a nickname", packet.getFrom(), roomName);
1187 
1188             // A resource is required in order to join a room http://xmpp.org/extensions/xep-0045.html#enter
1189             // If the user does not specify a room nickname (note the bare JID on the 'from' address in the following example), the service MUST return a <jid-malformed/> error
1190             if ( packet.getType() != Presence.Type.error )
1191             {
1192                 sendErrorPacket(packet, PacketError.Condition.jid_malformed, "A nickname (resource-part) is required in order to join a room.");
1193             }
1194             return null;
1195         }
1196 
1197         if ( !packet.isAvailable() )
1198         {
1199             Log.debug("Request from '{}' to join room '{}' rejected: request unexpectedly provided a presence stanza of type '{}'. Expected none.", packet.getFrom(), roomName, packet.getType());
1200             if ( packet.getType() != Presence.Type.error )
1201             {
1202                 sendErrorPacket(packet, PacketError.Condition.unexpected_request, "Unexpected stanza type: " + packet.getType());
1203             }
1204             return null;
1205         }
1206 
1207         if (room == null) {
1208             try {
1209                 // Create the room
1210                 room = getChatRoom(roomName, packet.getFrom());
1211             } catch (NotAllowedException e) {
1212                 Log.debug("Request from '{}' to join room '{}' rejected: user does not have permission to create a new room.", packet.getFrom(), roomName, e);
1213                 sendErrorPacket(packet, PacketError.Condition.not_allowed, "You do not have permission to create a new room.");
1214                 return null;
1215             }
1216         }
1217 
1218         try
1219         {
1220             // User must support MUC in order to create a room
1221             HistoryRequest historyRequest = null;
1222             String password = null;
1223 
1224             // Check for password & requested history if client supports MUC
1225             final Element mucInfo = packet.getChildElement("x", "http://jabber.org/protocol/muc");
1226             if ( mucInfo != null )
1227             {
1228                 password = mucInfo.elementTextTrim("password");
1229                 if ( mucInfo.element("history") != null )
1230                 {
1231                     historyRequest = new HistoryRequest(mucInfo);
1232                 }
1233             }
1234 
1235             // The user joins the room
1236             final MUCRole role = room.joinRoom(nickname,
1237                 password,
1238                 historyRequest,
1239                 packet.getFrom(),
1240                 packet.createCopy());
1241 
1242             // If the client that created the room is non-MUC compliant then
1243             // unlock the room thus creating an "instant" room
1244             if ( mucInfo == null && room.isLocked() && !room.isManuallyLocked() )
1245             {
1246                 room.unlock(role);
1247             }
1248         }
1249         catch ( UnauthorizedException e )
1250         {
1251             Log.debug("Request from '{}' to join room '{}' rejected: user not authorized to create or join the room.", packet.getFrom(), roomName, e);
1252             sendErrorPacket(packet, PacketError.Condition.not_authorized, "You're not authorized to create or join the room.");
1253         }
1254         catch ( ServiceUnavailableException e )
1255         {
1256             Log.debug("Request from '{}' to join room '{}' rejected: the maximum number of users of the room has been reached.", packet.getFrom(), roomName, e);
1257             sendErrorPacket(packet, PacketError.Condition.service_unavailable, "The maximum number of users of the room has been reached.");
1258         }
1259         catch ( UserAlreadyExistsException | ConflictException e )
1260         {
1261             Log.debug("Request from '{}' to join room '{}' rejected: the requested nickname '{}' is being used by someone else in the room.", packet.getFrom(), roomName, nickname, e);
1262             sendErrorPacket(packet, PacketError.Condition.conflict, "The nickname that is being used is used by someone else.");
1263         }
1264         catch ( RoomLockedException e )
1265         {
1266             // If a user attempts to enter a room while it is "locked" (i.e., before the room creator provides an initial configuration and therefore before the room officially exists), the service MUST refuse entry and return an <item-not-found/> error to the user
1267             Log.debug("Request from '{}' to join room '{}' rejected: room is locked.", packet.getFrom(), roomName, e);
1268             sendErrorPacket(packet, PacketError.Condition.item_not_found, "This room is locked (it might not have been configured yet).");
1269         }
1270         catch ( ForbiddenException e )
1271         {
1272             Log.debug("Request from '{}' to join room '{}' rejected: user not authorized join the room.", packet.getFrom(), roomName, e);
1273             sendErrorPacket(packet, PacketError.Condition.forbidden, "You're not allowed to join this room.");
1274         }
1275         catch ( RegistrationRequiredException e )
1276         {
1277             Log.debug("Request from '{}' to join room '{}' rejected: room is member-only, user is not a member.", packet.getFrom(), roomName, e);
1278             sendErrorPacket(packet, PacketError.Condition.registration_required, "This is a member-only room. Membership is required.");
1279         }
1280         catch ( NotAcceptableException e )
1281         {
1282             Log.debug("Request from '{}' to join room '{}' rejected: user attempts to use nickname '{}' which is different from the reserved nickname.", packet.getFrom(), roomName, nickname, e);
1283             sendErrorPacket(packet, PacketError.Condition.not_acceptable, "You're trying to join with a nickname different than the reserved nickname.");
1284         }
1285         return room;
1286     }
1287 
1288     /**
1289      * Process a presence status update for a user.
1290      *
1291      * @param packet          The stanza to process
1292      * @param room            The room that the stanza was addressed to.
1293      * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza.
1294      */
processPresenceUpdate( @onnull final Presence packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )1295     private void processPresenceUpdate(
1296         @Nonnull final Presence packet,
1297         @Nonnull final MUCRoom room,
1298         @Nonnull final MUCRole preExistingRole )
1299     {
1300         if ( Presence.Type.unavailable == packet.getType() )
1301         {
1302             Log.trace("Occupant '{}' of room '{}' is leaving.", preExistingRole.getUserAddress(), room.getName());
1303             // TODO Consider that different nodes can be creating and processing this presence at the same time (when remote node went down)
1304             preExistingRole.setPresence(packet);
1305             room.leaveRoom(preExistingRole);
1306         }
1307         else
1308         {
1309             Log.trace("Occupant '{}' of room '{}' changed its availability status.", preExistingRole.getUserAddress(), room.getName());
1310             room.presenceUpdated(preExistingRole, packet);
1311         }
1312     }
1313 
1314     /**
1315      * Process a request to change a nickname.
1316      *
1317      * @param packet          The stanza representing the nickname-change request.
1318      * @param room            The room that the stanza was addressed to.
1319      * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza.
1320      * @param nickname        The requested nickname.
1321      */
processNickNameChange( @onnull final Presence packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole, @Nonnull String nickname )1322     private void processNickNameChange(
1323         @Nonnull final Presence packet,
1324         @Nonnull final MUCRoom room,
1325         @Nonnull final MUCRole preExistingRole,
1326         @Nonnull String nickname )
1327         throws UserNotFoundException
1328     {
1329         Log.trace("Occupant '{}' of room '{}' tries to change its nickname to '{}'.", preExistingRole.getUserAddress(), room.getName(), nickname);
1330 
1331         if ( room.getOccupantsByBareJID(packet.getFrom().asBareJID()).isEmpty() )
1332         {
1333             Log.trace("Nickname change request denied: requestor '{}' is not an occupant of the room.", packet.getFrom().asBareJID());
1334             sendErrorPacket(packet, PacketError.Condition.not_acceptable, "You are not an occupant of this chatroom.");
1335             return;
1336         }
1337 
1338         if ( !room.canChangeNickname() )
1339         {
1340             Log.trace("Nickname change request denied: Room configuration does not allow nickname changes.");
1341             sendErrorPacket(packet, PacketError.Condition.not_acceptable, "Chatroom does not allow nickname changes.");
1342             return;
1343         }
1344 
1345         List<MUCRole> existingOccupants;
1346         try {
1347             existingOccupants = room.getOccupantsByNickname(nickname);
1348         } catch (UserNotFoundException e) {
1349             existingOccupants = Collections.emptyList();
1350         }
1351 
1352         if ( !existingOccupants.isEmpty() && !existingOccupants.stream().allMatch(r->r.getUserAddress().asBareJID().equals(preExistingRole.getUserAddress().asBareJID())) )
1353         {
1354             Log.trace("Nickname change request denied: the requested nickname '{}' is used by another occupant of the room.", nickname);
1355             sendErrorPacket(packet, PacketError.Condition.conflict, "This nickname is taken.");
1356             return;
1357         }
1358 
1359         final JID memberBareJID = room.getMemberForReservedNickname(nickname);
1360         if (memberBareJID != null && !memberBareJID.equals(preExistingRole.getUserAddress().asBareJID()))
1361         {
1362             Log.trace("Nickname change request denied: the requested nickname '{}' is reserved by a member of the room.", nickname);
1363             sendErrorPacket(packet, PacketError.Condition.conflict, "This nickname is taken.");
1364             return;
1365         }
1366 
1367         // Send "unavailable" presence for the old nickname
1368         final Presence presence = preExistingRole.getPresence().createCopy();
1369         // Switch the presence to OFFLINE
1370         presence.setType(Presence.Type.unavailable);
1371         presence.setStatus(null);
1372         // Add the new nickname and status 303 as properties
1373         final Element frag = presence.getChildElement("x", "http://jabber.org/protocol/muc#user");
1374         frag.element("item").addAttribute("nick", nickname);
1375         frag.addElement("status").addAttribute("code", "303");
1376         room.send(presence, preExistingRole);
1377 
1378         // Send availability presence for the new nickname
1379         final String oldNick = preExistingRole.getNickname();
1380         room.nicknameChanged(preExistingRole, packet, oldNick, nickname);
1381     }
1382 
1383     /**
1384      * Determines if a stanza is a client-generated response to an IQ Ping request sent by this server.
1385      *
1386      * A 'true' result of this method indicates that the client sending the IQ response is currently reachable.
1387      *
1388      * @param stanza The stanza to check
1389      * @return true if the stanza is a response to an IQ Ping request sent by the server, otherwise false.
1390      */
isPendingPingResponse(@onnull final Packet stanza)1391     public boolean isPendingPingResponse(@Nonnull final Packet stanza) {
1392         if (!(stanza instanceof IQ)) {
1393             return false;
1394         }
1395         final IQ iq = (IQ) stanza;
1396         if (iq.isRequest()) {
1397             return false;
1398         }
1399 
1400         // Check if this is an error to a ghost-detection ping that we've sent out. Note that clients that are
1401         // connected but do not support XEP-0199 should send back a 'service-unavailable' error per the XEP, but
1402         // some clients are known to send 'feature-not-available'. Treat these as indications that the client is
1403         // still connected.
1404         final Collection<PacketError.Condition> pingErrorsIndicatingClientConnectivity = Arrays.asList(
1405             PacketError.Condition.service_unavailable,
1406             PacketError.Condition.feature_not_implemented
1407         );
1408 
1409         final JID jid = PINGS_SENT.get(iq.getID());
1410         final boolean result = jid != null && iq.getFrom().equals(jid)
1411             && (iq.getError() == null || pingErrorsIndicatingClientConnectivity.contains(iq.getError().getCondition()));
1412 
1413         if (result) {
1414             // If this is _not_ a valid response, 'isDeliveryRelatedErrorResponse' might want to process this. Otherwise, remove.
1415             PINGS_SENT.remove(iq.getID());
1416         }
1417         return result;
1418     }
1419 
1420     /**
1421      * Determines if a stanza is sent by (on behalf of) an entity that MUC room believes to be an occupant
1422      * when they've left: a 'ghost' occupant
1423      *
1424      * For message and presence stanzas, the delivery errors as defined in section 18.1.2 of XEP-0045 are used.
1425      *
1426      * For IQ stanzas, these errors may be due to lack of client support rather than a vanished occupant. Therefore,
1427      * IQ stanzas will only return 'true' when they are an error response to an IQ Ping request sent by this server.
1428      *
1429      * @param stanza The stanza to check
1430      * @return true if the stanza is a delivery related error response (from a 'ghost user'), otherwise false.
1431      * @see <a href="https://xmpp.org/extensions/xep-0045.html#impl-service-ghosts">XEP-0045, section 'ghost users'</a>
1432      */
isDeliveryRelatedErrorResponse(@onnull final Packet stanza)1433     public boolean isDeliveryRelatedErrorResponse(@Nonnull final Packet stanza)
1434     {
1435         if (stanza instanceof IQ) {
1436             final IQ iq = ((IQ)stanza);
1437             if (iq.getType() != IQ.Type.error) {
1438                 return false;
1439             }
1440 
1441             // Check if this is an error to a ghost-detection ping that we've sent out. Note that clients that are
1442             // connected but do not support XEP-0199 should send back a 'service-unavailable' error per the XEP, but
1443             // some clients are known to send 'feature-not-available'. Treat these as indications that the client is
1444             // still connected.
1445             final Collection<PacketError.Condition> pingErrorsIndicatingClientConnectivity = Arrays.asList(
1446                 PacketError.Condition.service_unavailable,
1447                 PacketError.Condition.feature_not_implemented
1448             );
1449 
1450             if (stanza.getError() != null && pingErrorsIndicatingClientConnectivity.contains(stanza.getError().getCondition())) {
1451                 return false;
1452             }
1453 
1454             final JID jid = PINGS_SENT.get(iq.getID());
1455             return jid != null && iq.getFrom().equals(jid) && stanza.getError() != null;
1456         }
1457 
1458         // The remainder of this implementation applies to Message and Presence stanzas.
1459 
1460         // Conditions as defined in XEP-0045, section 'ghost users'.
1461         final Collection<PacketError.Condition> deliveryRelatedErrorConditions = Arrays.asList(
1462             PacketError.Condition.gone,
1463             PacketError.Condition.item_not_found,
1464             PacketError.Condition.recipient_unavailable,
1465             PacketError.Condition.redirect,
1466             PacketError.Condition.remote_server_not_found,
1467             PacketError.Condition.remote_server_timeout
1468         );
1469 
1470         final PacketError error = stanza.getError();
1471         return error != null && deliveryRelatedErrorConditions.contains(error.getCondition());
1472     }
1473 
1474     @Override
initialize(final JID jid, final ComponentManager componentManager)1475     public void initialize(final JID jid, final ComponentManager componentManager) {
1476         initialize(XMPPServer.getInstance());
1477     }
1478 
1479     @Override
shutdown()1480     public void shutdown() {
1481         enableService( false, false );
1482         ClusterManager.removeListener(this);
1483         MUCEventDispatcher.removeListener(occupantManager);
1484     }
1485 
1486     @Override
getServiceDomain()1487     public String getServiceDomain() {
1488         return chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain();
1489     }
1490 
getAddress()1491     public JID getAddress() {
1492         return new JID(null, getServiceDomain(), null, true);
1493     }
1494 
1495     @Override
serverStarted()1496     public void serverStarted()
1497     {}
1498 
1499     @Override
serverStopping()1500     public void serverStopping()
1501     {
1502         // When this is executed, we can be certain that all server modules have not yet shut down. This allows us to
1503         // inform all users.
1504         shutdown();
1505     }
1506 
1507     /**
1508      * Operates on users that have been inactive for a while. Depending on the configuration of Openfire, these uses
1509      * could either be kicked, or be pinged (to determine if they're 'ghost users').
1510      */
1511     private class UserTimeoutTask extends TimerTask {
1512         @Override
run()1513         public void run() {
1514             checkForTimedOutUsers();
1515         }
1516     }
1517 
1518     /**
1519      * Informs all users local to this cluster node that he or she is being removed from the room because the MUC
1520      * service is being shut down.
1521      *
1522      * The implementation is optimized to run as fast as possible (to prevent prolonging the shutdown).
1523      */
broadcastShutdown()1524     private void broadcastShutdown()
1525     {
1526         Log.debug( "Notifying all local users about the imminent destruction of chat service '{}'", chatServiceName );
1527 
1528         final Set<OccupantManager.Occupant> localOccupants = occupantManager.getLocalOccupants();
1529 
1530         if (localOccupants.isEmpty()) {
1531             return;
1532         }
1533 
1534         // A thread pool is used to broadcast concurrently, as well as to limit the execution time of this service.
1535         final ExecutorService service = Executors.newFixedThreadPool( Math.min( localOccupants.size(), 10 ) );
1536 
1537         // Queue all tasks in the executor service.
1538         for ( final OccupantManager.Occupant localOccupant : localOccupants )
1539         {
1540             service.submit(() -> {
1541                 try
1542                 {
1543                     // Obtaining the room without acquiring a lock. Usage of the room is read-only (the implementation below
1544                     // should not modify the room state in a way that the cluster cares about), and more importantly, speed
1545                     // is of importance (waiting for every room's lock to be acquired would slow down the shutdown process).
1546                     // Lastly, this service is shutting down (likely because the server is shutting down). The trade-off
1547                     // between speed and access of room state while not holding a lock seems worth while here.
1548                     final MUCRoom room = getChatRoom(localOccupant.getRoomName());
1549                     if (room == null) {
1550                         // Mismatch between MUCUser#getRooms() and MUCRoom#localMUCRoomManager ?
1551                         Log.warn("User '{}' appears to have had a role in room '{}' of service '{}' that does not seem to exist.", localOccupant.getRealJID(), localOccupant.getRoomName(), chatServiceName);
1552                         return;
1553                     }
1554                     final MUCRole role = room.getOccupantByFullJID(localOccupant.getRealJID());
1555                     if (role == null) {
1556                         // Mismatch between MUCUser#getRooms() and MUCRoom#occupants ?
1557                         Log.warn("User '{}' appears to have had a role in room '{}' of service '{}' but that role does not seem to exist.", localOccupant.getRealJID(), localOccupant.getRoomName(), chatServiceName);
1558                         return;
1559                     }
1560 
1561                     // Send a presence stanza of type "unavailable" to the occupant
1562                     final Presence presence = room.createPresence( Presence.Type.unavailable );
1563                     presence.setFrom( role.getRoleAddress() );
1564 
1565                     // A fragment containing the x-extension.
1566                     final Element fragment = presence.addChildElement( "x", "http://jabber.org/protocol/muc#user" );
1567                     final Element item = fragment.addElement( "item" );
1568                     item.addAttribute( "affiliation", "none" );
1569                     item.addAttribute( "role", "none" );
1570                     fragment.addElement( "status" ).addAttribute( "code", "332" );
1571 
1572                     // Make sure that the presence change for each user is only sent to that user (and not broadcast in the room)!
1573                     // Not needed to create a defensive copy of the stanza. It's not used anywhere else.
1574                     role.send( presence );
1575 
1576                     // Let all other cluster nodes know!
1577                     room.removeOccupantRole(role);
1578                 }
1579                 catch ( final Exception e )
1580                 {
1581                     Log.debug( "Unable to inform {} about the imminent destruction of chat service '{}'", localOccupant.realJID, chatServiceName, e );
1582                 }
1583             });
1584         }
1585 
1586         // Try to shutdown - wait - force shutdown.
1587         service.shutdown();
1588         try
1589         {
1590             if (service.awaitTermination( JiveGlobals.getIntProperty( "xmpp.muc.await-termination-millis", 500 ), TimeUnit.MILLISECONDS )) {
1591                 Log.debug("Successfully notified all local users about the imminent destruction of chat service '{}'", chatServiceName);
1592             } else {
1593                 Log.debug("Unable to notify all local users about the imminent destruction of chat service '{}' (timeout)", chatServiceName);
1594             }
1595         }
1596         catch ( final InterruptedException e )
1597         {
1598             Log.debug( "Interrupted while waiting for all users to be notified of shutdown of chat service '{}'. Shutting down immediately.", chatServiceName );
1599         }
1600         service.shutdownNow();
1601     }
1602 
1603     /**
1604      * Iterates over the local occupants of MUC rooms (users connected to the local cluster node), to determine if an
1605      * action needs to be taken based on their (lack of) activity. Depending on the configuration of Openfire, inactive
1606      * users (users that are connected, but have not typed anything) are kicked from the room, and/or are explicitly
1607      * asked for a proof of life (connectivity), removing them if this proof is not given.
1608      */
checkForTimedOutUsers()1609     private void checkForTimedOutUsers()
1610     {
1611         for (final OccupantManager.Occupant occupant : occupantManager.getLocalOccupants())
1612         {
1613             try
1614             {
1615                 if (userIdleKick != null && occupant.getLastActive().isBefore(Instant.now().minus(userIdleKick)))
1616                 {
1617                     // Kick users if 'user_idle' feature is enabled and the user has been idle for too long.
1618                     tryRemoveOccupantFromRoom(occupant, JiveGlobals.getProperty("admin.mucRoom.timeoutKickReason", "User was inactive for longer than the allowed maximum duration of " + userIdleKick.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase()) + "." );
1619                 }
1620                 else if (userIdlePing != null)
1621                 {
1622                     // Check if the occupant has been inactive for to long.
1623                     final Instant lastActive = occupant.getLastActive();
1624                     final boolean isInactive = lastActive.isBefore(Instant.now().minus(userIdlePing));
1625 
1626                     // Ensure that we didn't quite recently send a ping already.
1627                     final Instant lastPing = occupant.getLastPingRequest();
1628                     final boolean isRecentlyPinged = lastPing != null && lastPing.isAfter(Instant.now().minus(userIdlePing));
1629 
1630                     if (isInactive && !isRecentlyPinged) {
1631                         // Ping the user if it hasn't been kicked already, the feature is enabled, and the user has been idle for too long.
1632                         pingOccupant(occupant);
1633                     }
1634                 }
1635             }
1636             catch (final Throwable e) {
1637                 Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
1638             }
1639         }
1640     }
1641 
1642     /**
1643      * Removes an occupant from a room.
1644      *
1645      * When the system is 'busy', the occupant will not be removed to prevent threads blocking. In such cases, a message
1646      * is added to the log files, but the occupant remains in the room.
1647      *
1648      * @param occupant The occupant to be removed
1649      * @param reason A human-readable reason for the removal (to be shared with the occupant as well as other occupants of the room).
1650      */
tryRemoveOccupantFromRoom(@onnull final OccupantManager.Occupant occupant, @Nonnull final String reason)1651     private void tryRemoveOccupantFromRoom(@Nonnull final OccupantManager.Occupant occupant, @Nonnull final String reason)
1652     {
1653         final Lock lock = getChatRoomLock(occupant.getRoomName());
1654         if (!lock.tryLock()) { // Don't block on locked rooms, as we're processing many of them. We'll get them in the next round.
1655             Log.info("Skip removing as a cluster-wide mutex for the room could not immediately be obtained: {}, should have been removed, because: {}", occupant, reason);
1656             return;
1657         }
1658         try {
1659             final MUCRoom room = getChatRoom(occupant.getRoomName());
1660             if (room == null) {
1661                 // Room was recently removed? Mismatch between MUCUser#getRooms() and MUCRoom#localMUCRoomManager?
1662                 Log.info("Skip removing {} as the room no longer exists.", occupant);
1663                 return;
1664             }
1665 
1666             if (!room.hasOccupant(occupant.getRealJID())) {
1667                 // Occupant no longer in room? Mismatch between MUCUser#getRooms() and MUCRoom#localMUCRoomManager?
1668                 Log.debug("Skip removing {} as this occupant no longer is in the room.", occupant);
1669                 return;
1670             }
1671 
1672             // Kick the user from the room that he/she had previously joined.
1673             Log.debug("Removing/kicking {}: {}", occupant, reason);
1674             room.kickOccupant(occupant.getRealJID(), null, null, reason);
1675 
1676             // Ensure that other cluster nodes see any changes that might have been applied.
1677             syncChatRoom(room);
1678         } catch (final NotAllowedException e) {
1679             // Do nothing since we cannot kick owners or admins
1680             Log.debug("Skip removing {}, because it's not allowed (this user likely is an owner of admin of the room).", occupant, e);
1681         } finally {
1682             lock.unlock();
1683         }
1684     }
1685 
1686     /**
1687      * Sends an IQ Ping request to an occupant, and schedules a check that determines if a response to that request
1688      * was received (removing the occupant if it is not).
1689      *
1690      * @param occupant The occupant to ping.
1691      */
pingOccupant(@onnull final OccupantManager.Occupant occupant)1692     private void pingOccupant(@Nonnull final OccupantManager.Occupant occupant)
1693     {
1694         Log.debug("Pinging {} as the occupant is exceeding the idle time limit.", occupant);
1695         final IQ pingRequest = new IQ( IQ.Type.get );
1696         pingRequest.setChildElement( IQPingHandler.ELEMENT_NAME, IQPingHandler.NAMESPACE );
1697         pingRequest.setFrom( occupant.getRoomName() + "@" + getServiceDomain() );
1698         pingRequest.setTo( occupant.getRealJID() );
1699         pingRequest.setID( UUID.randomUUID().toString() ); // Generate unique ID, to prevent duplicate cache entries.
1700         XMPPServer.getInstance().getPacketRouter().route(pingRequest);
1701         PINGS_SENT.put(pingRequest.getID(), pingRequest.getTo());
1702 
1703         // Schedule a check to see if the ping was answered, kicking the occupant if it wasn't.
1704         // The check should be done _before_ the next ping would be sent (to prevent a backlog of ping requests forming)
1705         long timeoutMs = userIdlePing.dividedBy(4).toMillis();
1706         final CheckPingResponseTask task = new CheckPingResponseTask(occupant, pingRequest.getID());
1707         occupant.setPendingPingTask(task);
1708         TaskEngine.getInstance().schedule(task, timeoutMs);
1709     }
1710 
1711     /**
1712      * A task that verifies if a response to a certain IQ Ping stanza (identified by stanza ID) was received, kicking
1713      * an occupant of a MUC room if it is not, and if there hasn't been any other activity from the occupant since.
1714      */
1715     private class CheckPingResponseTask extends TimerTask {
1716         final OccupantManager.Occupant occupant;
1717         final String stanzaID;
1718         final Instant pingRequestSent = Instant.now();
1719 
CheckPingResponseTask(@onnull final OccupantManager.Occupant occupant, @Nonnull final String stanzaID)1720         public CheckPingResponseTask(@Nonnull final OccupantManager.Occupant occupant, @Nonnull final String stanzaID) {
1721             this.occupant = occupant;
1722             this.stanzaID = stanzaID;
1723         }
1724 
1725         @Override
run()1726         public void run()
1727         {
1728             occupant.setPendingPingTask(null);
1729 
1730             Log.trace("Checking if {} has responded to a ping request that we sent earlier (with stanza ID '{}').", occupant, stanzaID);
1731             if (!occupant.getRealJID().equals(PINGS_SENT.remove(stanzaID)))  // A null-check should be enough. Check against recipient for extra safety.
1732             {
1733                 Log.trace("The ping request that we sent earlier to {} seems to have been answered. No need to remove this occupant.", occupant);
1734                 return;
1735             }
1736 
1737             final Instant lastActivity = occupantManager.lastActivityOnLocalNode(occupant.getRealJID());
1738             if (lastActivity == null) {
1739                 // If the occupant is in the room, it likely reconnected to another cluster node. Have that node worry about ghost detection.
1740                 Log.debug("{} that has been sent a ping request earlier is no longer connected to the local cluster node. No need to remove this occupant.", occupant);
1741                 return;
1742             }
1743 
1744             if (lastActivity.isAfter(pingRequestSent)) {
1745                 Log.debug("{} has not responded to a ping request that we sent earlier, but has had other activity. No need to remove this occupant.", occupant);
1746                 return;
1747             }
1748 
1749             Log.debug("{} has not responded to a ping request that we sent earlier and didn't have other activity. Occupant should be kicked from the room.", occupant);
1750             tryRemoveOccupantFromRoom(occupant, JiveGlobals.getProperty("admin.mucRoom.noPingResponseKickReason", "User seems to be unreachable (didn't respond to a ping request).") );
1751         }
1752     }
1753 
1754     /**
1755      * Stores Conversations in the database.
1756      */
1757     private static class ConversationLogEntryArchiver extends Archiver<ConversationLogEntry>
1758     {
ConversationLogEntryArchiver( String id, int maxWorkQueueSize, Duration maxPurgeInterval, Duration gracePeriod )1759         ConversationLogEntryArchiver( String id, int maxWorkQueueSize, Duration maxPurgeInterval, Duration gracePeriod )
1760         {
1761             super( id, maxWorkQueueSize, maxPurgeInterval, gracePeriod );
1762         }
1763 
1764         @Override
store( List<ConversationLogEntry> batch )1765         protected void store( List<ConversationLogEntry> batch )
1766         {
1767             if ( batch.isEmpty() )
1768             {
1769                 return;
1770             }
1771 
1772             MUCPersistenceManager.saveConversationLogBatch( batch );
1773         }
1774     }
1775 
1776     /**
1777      * Removes from memory rooms that have been without activity for a period of time. A room is
1778      * considered without activity when no occupants are present in the room for a while.
1779      */
1780     private class CleanupTask extends TimerTask {
1781         @Override
run()1782         public void run() {
1783             if (ClusterManager.isClusteringStarted() && !ClusterManager.isSeniorClusterMember()) {
1784                 // Do nothing if we are in a cluster and this JVM is not the senior cluster member
1785                 return;
1786             }
1787             try {
1788                 Date cleanUpDate = getCleanupDate();
1789                 if (cleanUpDate!=null)
1790                 {
1791                     totalChatTime += localMUCRoomManager.unloadInactiveRooms(cleanUpDate).toMillis();
1792                 }
1793             }
1794             catch (final Throwable e) {
1795                 Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
1796             }
1797         }
1798     }
1799 
1800     /**
1801      * Checks if a particular JID is allowed to create rooms.
1802      *
1803      * @param jid The jid for which to check (cannot be null).
1804      * @return true if the JID is allowed to create a room, otherwise false.
1805      */
isAllowedToCreate(final JID jid)1806     private boolean isAllowedToCreate(final JID jid) {
1807         // If room creation is not restricted, everyone is allowed to create a room.
1808         if (!isRoomCreationRestricted()) {
1809             return true;
1810         }
1811 
1812         final JID bareJID = jid.asBareJID();
1813 
1814         // System administrators are always allowed to create rooms.
1815         if (sysadmins.includes(bareJID)) {
1816             return true;
1817         }
1818 
1819         // If the JID of the user has explicitly been given permission, room creation is allowed.
1820         if (allowedToCreate.includes(bareJID)) {
1821             return true;
1822         }
1823 
1824         // Verify the policy that allows all local, registered users to create rooms.
1825         return allRegisteredUsersAllowedToCreate && UserManager.getInstance().isRegisteredUser(bareJID, false);
1826     }
1827 
1828     @Override
getChatRoomLock(@onnull final String roomName)1829     @Nonnull public Lock getChatRoomLock(@Nonnull final String roomName) {
1830         return localMUCRoomManager.getLock(roomName);
1831     }
1832 
1833     @Override
syncChatRoom(@onnull final MUCRoom room)1834     public void syncChatRoom(@Nonnull final MUCRoom room) {
1835         localMUCRoomManager.sync(room);
1836     }
1837 
1838     @Override
1839     @Nonnull
getChatRoom(@onnull final String roomName, @Nonnull final JID userjid)1840     public MUCRoom getChatRoom(@Nonnull final String roomName, @Nonnull final JID userjid) throws NotAllowedException {
1841         MUCRoom room;
1842         boolean loaded = false;
1843         boolean created = false;
1844         final Lock lock = localMUCRoomManager.getLock(roomName);
1845         lock.lock();
1846         try {
1847             room = localMUCRoomManager.get(roomName);
1848             if (room == null) {
1849                 room = new MUCRoom(this, roomName);
1850                 // If the room is persistent load the configuration values from the DB
1851                 try {
1852                     // Try to load the room's configuration from the database (if the room is
1853                     // persistent but was added to the DB after the server was started up or the
1854                     // room may be an old room that was not present in memory)
1855                     MUCPersistenceManager.loadFromDB(room);
1856                     loaded = true;
1857                 }
1858                 catch (final IllegalArgumentException e) {
1859                     // Check if room needs to be recreated in case it failed to be created previously
1860                     // (or was deleted somehow and is expected to exist by a delegate).
1861                     if (mucEventDelegate != null && mucEventDelegate.shouldRecreate(roomName, userjid)) {
1862                         if (mucEventDelegate.loadConfig(room)) {
1863                             loaded = true;
1864                             if (room.isPersistent()) {
1865                                 MUCPersistenceManager.saveToDB(room);
1866                             }
1867                         }
1868                         else {
1869                             // Room does not exist and delegate does not recognize it and does
1870                             // not allow room creation
1871                             throw new NotAllowedException();
1872 
1873                         }
1874                     }
1875                     else {
1876                         // The room does not exist so check for creation permissions
1877                         if (!isAllowedToCreate(userjid)) {
1878                             throw new NotAllowedException();
1879                         }
1880                         room.addFirstOwner(userjid);
1881                         created = true;
1882                     }
1883                 }
1884                 localMUCRoomManager.add(room);
1885             }
1886         } finally {
1887             lock.unlock();
1888         }
1889         if (created) {
1890             // Fire event that a new room has been created
1891             MUCEventDispatcher.roomCreated(room.getRole().getRoleAddress());
1892         }
1893         if (loaded || created) {
1894             // Initiate FMUC, when enabled.
1895             room.getFmucHandler().applyConfigurationChanges();
1896         }
1897         return room;
1898     }
1899 
1900     @Override
getChatRoom(@onnull final String roomName)1901     public MUCRoom getChatRoom(@Nonnull final String roomName) {
1902         boolean loaded = false;
1903         MUCRoom room = localMUCRoomManager.get(roomName);
1904         if (room == null) {
1905             // Check if the room exists in the database and was not present in memory
1906             final Lock lock = localMUCRoomManager.getLock(roomName);
1907             lock.lock();
1908             try {
1909                 room = localMUCRoomManager.get(roomName);
1910                 if (room == null) {
1911                     room = new MUCRoom(this, roomName);
1912                     // If the room is persistent load the configuration values from the DB
1913                     try {
1914                         // Try to load the room's configuration from the database (if the room is
1915                         // persistent but was added to the DB after the server was started up or the
1916                         // room may be an old room that was not present in memory)
1917                         MUCPersistenceManager.loadFromDB(room);
1918                         loaded = true;
1919                         localMUCRoomManager.add(room);
1920                     }
1921                     catch (final IllegalArgumentException e) {
1922                         // The room does not exist so do nothing
1923                         room = null;
1924                         loaded = false;
1925                     }
1926                 }
1927             } finally {
1928                 lock.unlock();
1929             }
1930         }
1931         if (loaded) {
1932             // Initiate FMUC, when enabled.
1933             room.getFmucHandler().applyConfigurationChanges();
1934         }
1935         return room;
1936     }
1937 
1938     @Override
1939     @Deprecated
getChatRooms()1940     public List<MUCRoom> getChatRooms() {
1941         return new ArrayList<>(localMUCRoomManager.getAll());
1942     }
1943 
1944     @Override
getActiveChatRooms()1945     public List<MUCRoom> getActiveChatRooms() {
1946         return new ArrayList<>(localMUCRoomManager.getAll());
1947     }
1948 
1949     /**
1950      *  Combine names of all rooms in the database (to catch any rooms that aren't currently in memory) with all
1951      *  names of rooms currently in memory (to include rooms that are non-persistent / never saved in the database).
1952      *  Duplicates will be removed by virtue of using a Set.
1953      *
1954      * @return Names of all rooms that are known to this service.
1955      */
1956     @Override
getAllRoomNames()1957     public Set<String> getAllRoomNames() {
1958         final Set<String> result = new HashSet<>();
1959         result.addAll( MUCPersistenceManager.loadRoomNamesFromDB(this) );
1960         result.addAll( localMUCRoomManager.getAll().stream().map(MUCRoom::getName).collect(Collectors.toSet()) );
1961 
1962         return result;
1963     }
1964 
1965     // This method operates on MUC rooms without acquiring a cluster lock for them. As the usage is read-only, and the
1966     // method would have to lock _every_ room, the cost of acquiring all locks seem to outweigh the benefit.
1967     @Override
getAllRoomSearchInfo()1968     public Collection<MUCRoomSearchInfo> getAllRoomSearchInfo() {
1969         // Base the result for all rooms that are in memory, then complement with rooms in the database that haven't
1970         // been added yet (to catch all non-active rooms);
1971         return getActiveAndInactiveRooms().stream().map(MUCRoomSearchInfo::new).collect(Collectors.toList());
1972     }
1973 
1974     /**
1975      * Returns all rooms serviced by this service. This includes rooms designated as non-active, which are loaded from
1976      * the database if necessary. This method can also be used solely for that 'side' effect of ensuring that all rooms
1977      * are loaded.
1978      *
1979      * @return All rooms serviced by this service.
1980      */
getActiveAndInactiveRooms()1981     public List<MUCRoom> getActiveAndInactiveRooms() {
1982         final List<MUCRoom> result = getActiveChatRooms();
1983 
1984         if (JiveGlobals.getBooleanProperty("xmpp.muc.search.skip-unloaded-rooms", false)) {
1985             return result;
1986         }
1987 
1988         final Set<String> loadedNames = result.stream().map(MUCRoom::getName).collect(Collectors.toSet());
1989         final Collection<String> dbNames = MUCPersistenceManager.loadRoomNamesFromDB(this);
1990         dbNames.removeAll(loadedNames); // what remains needs to be loaded from the database;
1991 
1992         for (final String name : dbNames) {
1993             // TODO improve scalability instead of loading every room that wasn't loaded before.
1994             final MUCRoom chatRoom = this.getChatRoom(name);
1995             if (chatRoom != null) {
1996                 result.add(chatRoom);
1997             }
1998         }
1999         return result;
2000     }
2001 
2002     @Override
hasChatRoom(final String roomName)2003     public boolean hasChatRoom(final String roomName) {
2004         return getChatRoom(roomName) != null;
2005     }
2006 
2007     @Override
removeChatRoom(final String roomName)2008     public void removeChatRoom(final String roomName) {
2009         final Lock lock = localMUCRoomManager.getLock(roomName);
2010         lock.lock();
2011         try {
2012             final MUCRoom room = localMUCRoomManager.remove(roomName);
2013             if (room != null) {
2014                 Log.info("removing chat room:" + roomName + "|" + room.getClass().getName());
2015                 totalChatTime += room.getChatLength();
2016             } else {
2017                 Log.info("No chatroom {} during removal.", roomName);
2018             }
2019         } finally {
2020             lock.unlock();
2021         }
2022     }
2023 
2024     @Override
getServiceName()2025     public String getServiceName() {
2026         return chatServiceName;
2027     }
2028 
2029     @Override
getName()2030     public String getName() {
2031         return getServiceName();
2032     }
2033 
2034     @Override
getHistoryStrategy()2035     public HistoryStrategy getHistoryStrategy() {
2036         return historyStrategy;
2037     }
2038 
2039     /**
2040      * Removes a user from all chat rooms.
2041      *
2042      * @param userAddress The user's normal jid, not the chat nickname jid.
2043      */
removeChatUser(final JID userAddress)2044     private void removeChatUser(final JID userAddress)
2045     {
2046         final Set<String> roomNames = occupantManager.roomNamesForAddress(userAddress);
2047 
2048         for (final String roomName : roomNames)
2049         {
2050             final Lock lock = getChatRoomLock(roomName);
2051             lock.lock();
2052             try {
2053                 final MUCRoom room = getChatRoom(roomName);
2054                 if (room == null) {
2055                     // Mismatch between MUCUser#getRooms() and MUCRoom#localMUCRoomManager ?
2056                     Log.warn("User '{}' appears to have had a role in room '{}' of service '{}' that does not seem to exist.", userAddress, roomName, chatServiceName);
2057                     continue;
2058                 }
2059                 final MUCRole role = room.getOccupantByFullJID(userAddress);
2060                 if (role == null) {
2061                     // Mismatch between MUCUser#getRooms() and MUCRoom#occupants ?
2062                     Log.warn("User '{}' appears to have had a role in room '{}' of service '{}' but that role does not seem to exist.", userAddress, roomName, chatServiceName);
2063                     continue;
2064                 }
2065                 try {
2066                     room.leaveRoom(role);
2067                     // Ensure that all cluster nodes see the change to the room
2068                     syncChatRoom(room);
2069                 } catch (final Exception e) {
2070                     Log.error(e.getMessage(), e);
2071                 }
2072             } finally {
2073                 lock.unlock();
2074             }
2075         }
2076     }
2077 
2078     @Override
getMUCRoles(final JID user)2079     public Collection<MUCRole> getMUCRoles(final JID user) {
2080         final List<MUCRole> userRoles = new ArrayList<>();
2081         for (final MUCRoom room : localMUCRoomManager.getAll()) {
2082             final MUCRole role = room.getOccupantByFullJID(user);
2083             if (role != null) {
2084                 userRoles.add(role);
2085             }
2086         }
2087         return userRoles;
2088     }
2089 
2090     /**
2091      * Returns the limit date after which rooms without activity will be removed from memory.
2092      *
2093      * @return the limit date after which rooms without activity will be removed from memory.
2094      */
getCleanupDate()2095     private Date getCleanupDate() {
2096         if (emptyLimit!=-1)
2097             return new Date(System.currentTimeMillis() - (emptyLimit * 3600000));
2098         else
2099             return null;
2100     }
2101 
2102     @Override
setIdleUserTaskInterval(final @Nonnull Duration duration)2103     public void setIdleUserTaskInterval(final @Nonnull Duration duration) {
2104         // Set the new property value
2105         MUCPersistenceManager.setProperty(chatServiceName, "tasks.user.timeout", Long.toString(userIdleTaskInterval.toMillis()));
2106 
2107         rescheduleUserTimeoutTask();
2108     }
2109 
rescheduleUserTimeoutTask()2110     private void rescheduleUserTimeoutTask()
2111     {
2112         // Use the 25% of the smallest of 'userIdleKick' or 'userIdlePing', or 5 minutes if both are unset.
2113         Duration recalculated = Stream.of(userIdleKick, userIdlePing)
2114             .filter(Objects::nonNull)
2115             .filter(duration -> duration.compareTo(Duration.ofSeconds(30)) > 0) // Not faster than every so often.
2116             .sorted()
2117             .findFirst()
2118             .orElse(Duration.ofMinutes(5*4))
2119             .dividedBy(4);
2120 
2121         // But if the property is set, use that.
2122         String value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.user.timeout");
2123         if (value != null) {
2124             try {
2125                 recalculated = Duration.ofMillis(Long.parseLong(value));
2126             }
2127             catch (final NumberFormatException e) {
2128                 Log.error("Wrong number format of property tasks.user.timeout for service "+chatServiceName, e);
2129             }
2130         }
2131 
2132         if (Objects.equals(recalculated, this.userIdleTaskInterval)) {
2133             return;
2134         }
2135         Log.info("Rescheduling user idle task, recurring every {}", recalculated);
2136         this.userIdleTaskInterval = recalculated;
2137 
2138         synchronized (this) {
2139             // Cancel the existing task because the timeout has changed
2140             if (userTimeoutTask != null) {
2141                 TaskEngine.getInstance().cancelScheduledTask(userTimeoutTask);
2142             }
2143 
2144             // Create a new task and schedule it with the new timeout
2145             userTimeoutTask = new UserTimeoutTask();
2146             TaskEngine.getInstance().schedule(userTimeoutTask, userIdleTaskInterval.toMillis(), userIdleTaskInterval.toMillis());
2147         }
2148     }
2149 
2150     @Override
2151     @Nonnull
getIdleUserTaskInterval()2152     public Duration getIdleUserTaskInterval() {
2153         return this.userIdleTaskInterval;
2154     }
2155 
2156     @Override
setIdleUserKickThreshold(final @Nullable Duration duration)2157     public void setIdleUserKickThreshold(final @Nullable Duration duration)
2158     {
2159         if (Objects.equals(duration, this.userIdleKick)) {
2160             return;
2161         }
2162 
2163         this.userIdleKick = duration;
2164 
2165         // Set the new property value
2166         MUCPersistenceManager.setProperty(chatServiceName, "tasks.user.idle", userIdleKick == null ? "-1" : Long.toString(userIdleKick.toMillis()));
2167 
2168         rescheduleUserTimeoutTask();
2169     }
2170 
2171     @Override
getIdleUserKickThreshold()2172     public Duration getIdleUserKickThreshold()
2173     {
2174         return userIdleKick;
2175     }
2176 
2177     @Override
setIdleUserPingThreshold(final @Nullable Duration duration)2178     public void setIdleUserPingThreshold(final @Nullable Duration duration)
2179     {
2180         if (Objects.equals(duration, this.userIdlePing)) {
2181             return;
2182         }
2183 
2184         this.userIdlePing = duration;
2185 
2186         if (userIdlePing != null && PINGS_SENT.getMaxLifetime() < userIdlePing.dividedBy(4).toMillis()) {
2187             Log.warn("The cache that tracks Ping requests ({}) has a maximum entry lifetime ({}) that is shorter than the time that we wait for responses ({}). This will result in (some) occupants not being removed when they fail to respond to Pings. An Openfire admin should adjust the configuration.", PINGS_SENT.getName(), Duration.ofMillis(PINGS_SENT.getMaxLifetime()), userIdlePing.dividedBy(4));
2188         }
2189 
2190         // Set the new property value
2191         MUCPersistenceManager.setProperty(chatServiceName, "tasks.user.ping", userIdlePing == null ? "-1" : Long.toString(userIdlePing.toMillis()));
2192 
2193         rescheduleUserTimeoutTask();
2194     }
2195 
2196     @Override
2197     @Nullable
getIdleUserPingThreshold()2198     public Duration getIdleUserPingThreshold() {
2199         return userIdlePing;
2200     }
2201 
2202     @Override
getUsersAllowedToCreate()2203     public Collection<JID> getUsersAllowedToCreate() {
2204         return Collections.unmodifiableCollection(allowedToCreate);
2205     }
2206 
2207     @Override
getSysadmins()2208     public Collection<JID> getSysadmins() {
2209         return Collections.unmodifiableCollection(sysadmins);
2210     }
2211 
2212     @Override
isSysadmin(final JID bareJID)2213     public boolean isSysadmin(final JID bareJID) {
2214         return sysadmins.includes(bareJID);
2215     }
2216 
2217     @Override
addSysadmins(final Collection<JID> userJIDs)2218     public void addSysadmins(final Collection<JID> userJIDs) {
2219         for (final JID userJID : userJIDs) {
2220             addSysadmin(userJID);
2221         }
2222     }
2223 
2224     @Override
addSysadmin(final JID userJID)2225     public void addSysadmin(final JID userJID) {
2226         final JID bareJID = userJID.asBareJID();
2227 
2228         if (!sysadmins.contains(userJID)) {
2229             sysadmins.add(bareJID);
2230         }
2231 
2232         // CopyOnWriteArray does not allow sorting, so do sorting in temp list.
2233         final ArrayList<JID> tempList = new ArrayList<>(sysadmins);
2234         Collections.sort(tempList);
2235         sysadmins = new ConcurrentGroupList<>(tempList);
2236 
2237         // Update the config.
2238         final String[] jids = new String[sysadmins.size()];
2239         for (int i = 0; i < jids.length; i++) {
2240             jids[i] = sysadmins.get(i).toBareJID();
2241         }
2242         MUCPersistenceManager.setProperty(chatServiceName, "sysadmin.jid", fromArray(jids));
2243     }
2244 
2245     @Override
removeSysadmin(final JID userJID)2246     public void removeSysadmin(final JID userJID) {
2247         final JID bareJID = userJID.asBareJID();
2248 
2249         sysadmins.remove(bareJID);
2250 
2251         // Update the config.
2252         final String[] jids = new String[sysadmins.size()];
2253         for (int i = 0; i < jids.length; i++) {
2254             jids[i] = sysadmins.get(i).toBareJID();
2255         }
2256         MUCPersistenceManager.setProperty(chatServiceName, "sysadmin.jid", fromArray(jids));
2257     }
2258 
2259     /**
2260      * Returns the flag that indicates if the service should provide information about non-public
2261      * members-only rooms when handling service discovery requests.
2262      *
2263      * @return true if the service should provide information about non-public members-only rooms.
2264      */
isAllowToDiscoverMembersOnlyRooms()2265     public boolean isAllowToDiscoverMembersOnlyRooms() {
2266         return allowToDiscoverMembersOnlyRooms;
2267     }
2268 
2269     /**
2270      * Sets the flag that indicates if the service should provide information about non-public
2271      * members-only rooms when handling service discovery requests.
2272      *
2273      * @param allowToDiscoverMembersOnlyRooms
2274      *         if the service should provide information about
2275      *         non-public members-only rooms.
2276      */
setAllowToDiscoverMembersOnlyRooms(final boolean allowToDiscoverMembersOnlyRooms)2277     public void setAllowToDiscoverMembersOnlyRooms(final boolean allowToDiscoverMembersOnlyRooms) {
2278         this.allowToDiscoverMembersOnlyRooms = allowToDiscoverMembersOnlyRooms;
2279         MUCPersistenceManager.setProperty(chatServiceName, "discover.membersOnly",
2280             Boolean.toString(allowToDiscoverMembersOnlyRooms));
2281     }
2282 
2283     /**
2284      * Returns the flag that indicates if the service should provide information about locked rooms
2285      * when handling service discovery requests.
2286      *
2287      * @return true if the service should provide information about locked rooms.
2288      */
isAllowToDiscoverLockedRooms()2289     public boolean isAllowToDiscoverLockedRooms() {
2290         return allowToDiscoverLockedRooms;
2291     }
2292 
2293     /**
2294      * Sets the flag that indicates if the service should provide information about locked rooms
2295      * when handling service discovery requests.
2296      * Note: Setting this flag in false is not compliant with the spec. A user may try to join a
2297      * locked room thinking that the room doesn't exist because the user didn't discover it before.
2298      *
2299      * @param allowToDiscoverLockedRooms if the service should provide information about locked
2300      *        rooms.
2301      */
setAllowToDiscoverLockedRooms(final boolean allowToDiscoverLockedRooms)2302     public void setAllowToDiscoverLockedRooms(final boolean allowToDiscoverLockedRooms) {
2303         this.allowToDiscoverLockedRooms = allowToDiscoverLockedRooms;
2304         MUCPersistenceManager.setProperty(chatServiceName, "discover.locked",
2305             Boolean.toString(allowToDiscoverLockedRooms));
2306     }
2307 
2308     @Override
isRoomCreationRestricted()2309     public boolean isRoomCreationRestricted() {
2310         return roomCreationRestricted;
2311     }
2312 
2313     @Override
setRoomCreationRestricted(final boolean roomCreationRestricted)2314     public void setRoomCreationRestricted(final boolean roomCreationRestricted) {
2315         this.roomCreationRestricted = roomCreationRestricted;
2316         MUCPersistenceManager.setProperty(chatServiceName, "create.anyone", Boolean.toString(roomCreationRestricted));
2317     }
2318 
2319     @Override
isAllRegisteredUsersAllowedToCreate()2320     public boolean isAllRegisteredUsersAllowedToCreate() {
2321         return allRegisteredUsersAllowedToCreate;
2322     }
2323 
2324     @Override
setAllRegisteredUsersAllowedToCreate( final boolean allow )2325     public void setAllRegisteredUsersAllowedToCreate( final boolean allow ) {
2326         this.allRegisteredUsersAllowedToCreate = allow;
2327         MUCPersistenceManager.setProperty(chatServiceName, "create.all-registered", Boolean.toString(allow));
2328     }
2329 
2330     @Override
addUsersAllowedToCreate(final Collection<JID> userJIDs)2331     public void addUsersAllowedToCreate(final Collection<JID> userJIDs) {
2332         boolean listChanged = false;
2333 
2334         for(final JID userJID: userJIDs) {
2335             // Update the list of allowed JIDs to create MUC rooms. Since we are updating the instance
2336             // variable there is no need to restart the service
2337             if (!allowedToCreate.contains(userJID)) {
2338                 allowedToCreate.add(userJID);
2339                 listChanged = true;
2340             }
2341         }
2342 
2343         // if nothing was added, there's nothing to update
2344         if(listChanged) {
2345             // CopyOnWriteArray does not allow sorting, so do sorting in temp list.
2346             final List<JID> tempList = new ArrayList<>(allowedToCreate);
2347             Collections.sort(tempList);
2348             allowedToCreate = new ConcurrentGroupList<>(tempList);
2349             // Update the config.
2350             MUCPersistenceManager.setProperty(chatServiceName, "create.jid", fromCollection(allowedToCreate));
2351         }
2352     }
2353 
2354     @Override
addUserAllowedToCreate(final JID userJID)2355     public void addUserAllowedToCreate(final JID userJID) {
2356         final List<JID> asList = new ArrayList<>();
2357         asList.add(userJID);
2358         addUsersAllowedToCreate(asList);
2359     }
2360 
2361     @Override
removeUsersAllowedToCreate(final Collection<JID> userJIDs)2362     public void removeUsersAllowedToCreate(final Collection<JID> userJIDs) {
2363         boolean listChanged = false;
2364 
2365         for(final JID userJID: userJIDs) {
2366             // Update the list of allowed JIDs to create MUC rooms. Since we are updating the instance
2367             // variable there is no need to restart the service
2368             listChanged |= allowedToCreate.remove(userJID);
2369         }
2370 
2371         // if none of the JIDs were on the list, there's nothing to update
2372         if(listChanged) {
2373             MUCPersistenceManager.setProperty(chatServiceName, "create.jid", fromCollection(allowedToCreate));
2374         }
2375     }
2376 
2377     @Override
removeUserAllowedToCreate(final JID userJID)2378     public void removeUserAllowedToCreate(final JID userJID) {
2379         removeUsersAllowedToCreate(Collections.singleton(userJID));
2380     }
2381 
initialize(final XMPPServer server)2382     public void initialize(final XMPPServer server) {
2383         initializeSettings();
2384 
2385         routingTable = server.getRoutingTable();
2386         // Configure the handler of iq:register packets
2387         registerHandler = new IQMUCRegisterHandler(this);
2388         // Configure the handlers of search requests
2389         searchHandler = new IQMUCSearchHandler(this);
2390         muclumbusSearchHandler = new IQMuclumbusSearchHandler(this);
2391         mucVCardHandler = new IQMUCvCardHandler(this);
2392         MUCEventDispatcher.addListener(occupantManager);
2393 
2394         // Ensure that cluster events are handled in RoutingTableImpl first (due to higher listener sequence here)
2395         ClusterManager.addListener(this, 0);
2396     }
2397 
initializeSettings()2398     public void initializeSettings() {
2399         serviceEnabled = JiveProperties.getInstance().getBooleanProperty("xmpp.muc.enabled", true);
2400         serviceEnabled = MUCPersistenceManager.getBooleanProperty(chatServiceName, "enabled", serviceEnabled);
2401         // Trigger the strategy to load itself from the context
2402         historyStrategy.setContext(chatServiceName, "history");
2403         // Load the list of JIDs that are sysadmins of the MUC service
2404         String property = MUCPersistenceManager.getProperty(chatServiceName, "sysadmin.jid");
2405 
2406         sysadmins.clear();
2407         if (property != null && property.trim().length() > 0) {
2408             final String[] jids = property.split(",");
2409             for (final String jid : jids) {
2410                 if (jid == null || jid.trim().length() == 0) {
2411                     continue;
2412                 }
2413                 try {
2414                     // could be a group jid
2415                     sysadmins.add(GroupJID.fromString(jid.trim().toLowerCase()).asBareJID());
2416                 } catch (final IllegalArgumentException e) {
2417                     Log.warn("The 'sysadmin.jid' property contains a value that is not a valid JID. It is ignored. Offending value: '" + jid + "'.", e);
2418                 }
2419             }
2420         }
2421         allowToDiscoverLockedRooms =
2422             MUCPersistenceManager.getBooleanProperty(chatServiceName, "discover.locked", true);
2423         allowToDiscoverMembersOnlyRooms =
2424             MUCPersistenceManager.getBooleanProperty(chatServiceName, "discover.membersOnly", true);
2425         roomCreationRestricted =
2426             MUCPersistenceManager.getBooleanProperty(chatServiceName, "create.anyone", false);
2427         allRegisteredUsersAllowedToCreate =
2428             MUCPersistenceManager.getBooleanProperty(chatServiceName, "create.all-registered", false );
2429         // Load the list of JIDs that are allowed to create a MUC room
2430         property = MUCPersistenceManager.getProperty(chatServiceName, "create.jid");
2431         allowedToCreate.clear();
2432         if (property != null && property.trim().length() > 0) {
2433             final String[] jids = property.split(",");
2434             for (final String jid : jids) {
2435                 if (jid == null || jid.trim().length() == 0) {
2436                     continue;
2437                 }
2438                 try {
2439                     // could be a group jid
2440                     allowedToCreate.add(GroupJID.fromString(jid.trim().toLowerCase()).asBareJID());
2441                 } catch (final IllegalArgumentException e) {
2442                     Log.warn("The 'create.jid' property contains a value that is not a valid JID. It is ignored. Offending value: '" + jid + "'.", e);
2443                 }
2444             }
2445         }
2446         String value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.user.idle");
2447         userIdleKick = null;
2448         if (value != null) {
2449             try {
2450                 final long millis = Long.parseLong(value);
2451                 if ( millis < 0 ) {
2452                     userIdleKick = null; // feature is disabled.
2453                 } else {
2454                     userIdleKick = Duration.ofMillis(millis);
2455                 }
2456             }
2457             catch (final NumberFormatException e) {
2458                 Log.error("Wrong number format of property tasks.user.idle for service "+chatServiceName, e);
2459             }
2460         }
2461         value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.user.ping");
2462         userIdlePing = Duration.ofMinutes(60);
2463         if (value != null) {
2464             try {
2465                 final long millis = Long.parseLong(value);
2466                 if ( millis < 0 ) {
2467                     userIdlePing = null; // feature is disabled.
2468                 } else {
2469                     userIdlePing = Duration.ofMillis(millis);
2470                 }
2471             }
2472             catch (final NumberFormatException e) {
2473                 Log.error("Wrong number format of property tasks.user.ping for service "+chatServiceName, e);
2474             }
2475         }
2476         if (userIdlePing != null && PINGS_SENT.getMaxLifetime() < userIdlePing.dividedBy(4).toMillis()) {
2477             Log.warn("The cache that tracks Ping requests ({}) has a maximum entry lifetime ({}) that is shorter than the time that we wait for responses ({}). This will result in (some) occupants not being removed when they fail to respond to Pings. An Openfire admin should adjust the configuration.", PINGS_SENT.getName(), Duration.ofMillis(PINGS_SENT.getMaxLifetime()), userIdlePing.dividedBy(4));
2478         }
2479 
2480         value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.log.maxbatchsize");
2481         logMaxConversationBatchSize = 500;
2482         if (value != null) {
2483             try {
2484                 logMaxConversationBatchSize = Integer.parseInt(value);
2485             }
2486             catch (final NumberFormatException e) {
2487                 Log.error("Wrong number format of property tasks.log.maxbatchsize for service "+chatServiceName, e);
2488             }
2489         }
2490         value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.log.maxbatchinterval");
2491         logMaxBatchInterval = Duration.ofSeconds( 1 );
2492         if (value != null) {
2493             try {
2494                 logMaxBatchInterval = Duration.ofMillis( Long.parseLong(value) );
2495             }
2496             catch (final NumberFormatException e) {
2497                 Log.error("Wrong number format of property tasks.log.maxbatchinterval for service "+chatServiceName, e);
2498             }
2499         }
2500         value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.log.batchgrace");
2501         logBatchGracePeriod = Duration.ofMillis( 50 );
2502         if (value != null) {
2503             try {
2504                 logBatchGracePeriod = Duration.ofMillis( Long.parseLong(value) );
2505             }
2506             catch (final NumberFormatException e) {
2507                 Log.error("Wrong number format of property tasks.log.batchgrace for service "+chatServiceName, e);
2508             }
2509         }
2510         value = MUCPersistenceManager.getProperty(chatServiceName, "unload.empty_days");
2511         emptyLimit = 30 * 24;
2512         if (value != null) {
2513             try {
2514                 if (Integer.parseInt(value)>0)
2515                     emptyLimit = Integer.parseInt(value) * (long)24;
2516                 else
2517                     emptyLimit = -1;
2518             }
2519             catch (final NumberFormatException e) {
2520                 Log.error("Wrong number format of property unload.empty_days for service "+chatServiceName, e);
2521             }
2522         }
2523         rescheduleUserTimeoutTask();
2524     }
2525 
2526     /**
2527      * Property accessor temporarily retained for backward compatibility. The interface prescribes use of
2528      * {@link #setLogMaxConversationBatchSize(int)} - so please use that instead.
2529      * @param size the number of messages to save to the database on each run of the logging process.
2530      * @deprecated Use {@link #setLogMaxConversationBatchSize(int)} instead.
2531      */
2532     @Override
2533     @Deprecated
setLogConversationBatchSize(int size)2534     public void setLogConversationBatchSize(int size)
2535     {
2536         setLogMaxConversationBatchSize(size);
2537     }
2538 
2539     /**
2540      * Property accessor temporarily retained for backward compatibility. The interface prescribes use of
2541      * {@link #getLogMaxConversationBatchSize()} - so please use that instead.
2542      * @return the number of messages to save to the database on each run of the logging process.
2543      * @deprecated Use {@link #getLogMaxConversationBatchSize()} instead.
2544      */
2545     @Override
2546     @Deprecated
getLogConversationBatchSize()2547     public int getLogConversationBatchSize()
2548     {
2549         return getLogMaxConversationBatchSize();
2550     }
2551 
2552     /**
2553      * Sets the maximum number of messages to save to the database on each run of the archiving process.
2554      * Even though the saving of queued conversations takes place in another thread it is not
2555      * recommended specifying a big number.
2556      *
2557      * @param size the maximum number of messages to save to the database on each run of the archiving process.
2558      */
2559     @Override
setLogMaxConversationBatchSize(int size)2560     public void setLogMaxConversationBatchSize(int size) {
2561         if ( this.logMaxConversationBatchSize == size ) {
2562             return;
2563         }
2564         this.logMaxConversationBatchSize = size;
2565 
2566         if (archiver != null) {
2567             archiver.setMaxWorkQueueSize(size);
2568         }
2569         MUCPersistenceManager.setProperty( chatServiceName, "tasks.log.maxbatchsize", Integer.toString( size));
2570     }
2571 
2572     /**
2573      * Returns the maximum number of messages to save to the database on each run of the archiving process.
2574      * @return the maximum number of messages to save to the database on each run of the archiving process.
2575      */
2576     @Override
getLogMaxConversationBatchSize()2577     public int getLogMaxConversationBatchSize() {
2578         return logMaxConversationBatchSize;
2579     }
2580 
2581     /**
2582      * Sets the maximum time allowed to elapse between writing archive batches to the database.
2583      * @param interval the maximum time allowed to elapse between writing archive batches to the database.
2584      */
2585     @Override
setLogMaxBatchInterval( Duration interval )2586     public void setLogMaxBatchInterval( Duration interval )
2587     {
2588         if ( this.logMaxBatchInterval.equals( interval ) ) {
2589             return;
2590         }
2591         this.logMaxBatchInterval = interval;
2592 
2593         if (archiver != null) {
2594             archiver.setMaxPurgeInterval(interval);
2595         }
2596         MUCPersistenceManager.setProperty(chatServiceName, "tasks.log.maxbatchinterval", Long.toString( interval.toMillis() ) );
2597     }
2598 
2599     /**
2600      * Returns the maximum time allowed to elapse between writing archive entries to the database.
2601      * @return the maximum time allowed to elapse between writing archive entries to the database.
2602      */
2603     @Override
getLogMaxBatchInterval()2604     public Duration getLogMaxBatchInterval()
2605     {
2606         return logMaxBatchInterval;
2607     }
2608 
2609     /**
2610      * Sets the maximum time to wait for a next incoming entry before writing the batch to the database.
2611      * @param interval the maximum time to wait for a next incoming entry before writing the batch to the database.
2612      */
2613     @Override
setLogBatchGracePeriod( Duration interval )2614     public void setLogBatchGracePeriod( Duration interval )
2615     {
2616         if ( this.logBatchGracePeriod.equals( interval ) ) {
2617             return;
2618         }
2619 
2620         this.logBatchGracePeriod = interval;
2621         if (archiver != null) {
2622             archiver.setGracePeriod(interval);
2623         }
2624         MUCPersistenceManager.setProperty(chatServiceName, "tasks.log.batchgrace", Long.toString( interval.toMillis() ) );
2625     }
2626 
2627     /**
2628      * Returns the maximum time to wait for a next incoming entry before writing the batch to the database.
2629      * @return the maximum time to wait for a next incoming entry before writing the batch to the database.
2630      */
2631     @Override
getLogBatchGracePeriod()2632     public Duration getLogBatchGracePeriod()
2633     {
2634         return logBatchGracePeriod;
2635     }
2636 
2637     /**
2638      * Accessor uses the "double-check idiom" for proper lazy instantiation.
2639      * @return An Archiver instance, never null.
2640      */
2641     @Override
getArchiver()2642     public Archiver<ConversationLogEntry> getArchiver() {
2643         Archiver<ConversationLogEntry> result = this.archiver;
2644         if (result == null) {
2645             synchronized (this) {
2646                 result = this.archiver;
2647                 if (result == null) {
2648                     result = new ConversationLogEntryArchiver("MUC Service " + this.getAddress().toString(), logMaxConversationBatchSize, logMaxBatchInterval, logBatchGracePeriod);
2649                     XMPPServer.getInstance().getArchiveManager().add(result);
2650                     this.archiver = result;
2651                 }
2652             }
2653         }
2654 
2655         return result;
2656     }
2657 
2658     @Override
start()2659     public void start() {
2660         XMPPServer.getInstance().addServerListener( this );
2661 
2662         // Remove unused rooms from memory
2663         long cleanupFreq = JiveGlobals.getLongProperty("xmpp.muc.cleanupFrequency.inMinutes", CLEANUP_FREQUENCY) * 60 * 1000;
2664         TaskEngine.getInstance().schedule(new CleanupTask(), cleanupFreq, cleanupFreq);
2665 
2666         // Set us up to answer disco item requests
2667         XMPPServer.getInstance().getIQDiscoItemsHandler().addServerItemsProvider(this);
2668         XMPPServer.getInstance().getIQDiscoInfoHandler().setServerNodeInfoProvider(this.getServiceDomain(), this);
2669 
2670         Log.info(LocaleUtils.getLocalizedString("startup.starting.muc", Collections.singletonList(getServiceDomain())));
2671 
2672         final int preloadDays = MUCPersistenceManager.getIntProperty(chatServiceName, "preload.days", 30);
2673         if (preloadDays > 0) {
2674             // Load all the persistent rooms to memory
2675             final Instant cutoff = Instant.now().minus(Duration.ofDays(preloadDays));
2676             for (final MUCRoom room : MUCPersistenceManager.loadRoomsFromDB(this, Date.from(cutoff))) {
2677                 localMUCRoomManager.add(room);
2678 
2679                 // Start FMUC, if desired.
2680                 room.getFmucHandler().applyConfigurationChanges();
2681             }
2682         }
2683     }
2684 
stop()2685     private void stop() {
2686         XMPPServer.getInstance().getIQDiscoItemsHandler().removeServerItemsProvider(this);
2687         XMPPServer.getInstance().getIQDiscoInfoHandler().removeServerNodeInfoProvider(this.getServiceDomain());
2688         // Remove the route to this service
2689         routingTable.removeComponentRoute(getAddress());
2690         broadcastShutdown();
2691         XMPPServer.getInstance().removeServerListener( this );
2692         if (archiver != null) {
2693             XMPPServer.getInstance().getArchiveManager().remove(archiver);
2694         }
2695     }
2696 
2697     @Override
enableService(final boolean enabled, final boolean persistent)2698     public void enableService(final boolean enabled, final boolean persistent) {
2699         if (isServiceEnabled() == enabled) {
2700             // Do nothing if the service status has not changed
2701             return;
2702         }
2703         if (!enabled) {
2704             // Stop the service/module
2705             stop();
2706         }
2707         if (persistent) {
2708             MUCPersistenceManager.setProperty(chatServiceName, "enabled", Boolean.toString(enabled));
2709         }
2710         serviceEnabled = enabled;
2711         if (enabled) {
2712             // Start the service/module
2713             start();
2714         }
2715     }
2716 
2717     @Override
isServiceEnabled()2718     public boolean isServiceEnabled() {
2719         return serviceEnabled;
2720     }
2721 
2722     @Override
getTotalChatTime()2723     public long getTotalChatTime() {
2724         return totalChatTime;
2725     }
2726 
2727     /**
2728      * Returns the number of existing rooms in the server (i.e. persistent or not,
2729      * in memory or not).
2730      *
2731      * @return the number of existing rooms in the server.
2732      */
2733     @Override
getNumberChatRooms()2734     public int getNumberChatRooms() {
2735         int persisted = MUCPersistenceManager.countRooms(this);
2736         final long nonPersisted = localMUCRoomManager.getAll().stream().filter(room -> !room.isPersistent()).count();
2737         return persisted + (int) nonPersisted;
2738     }
2739 
2740     /**
2741      * Returns the total number of occupants in all rooms.
2742      *
2743      * @return the total number of occupants.
2744      */
2745     @Override
getNumberConnectedUsers()2746     public int getNumberConnectedUsers() {
2747         return occupantManager.numberOfUniqueUsers();
2748     }
2749 
2750     /**
2751      * Retuns the total number of users that have joined in all rooms in the server.
2752      *
2753      * @return the number of existing rooms in the server.
2754      */
2755     @Override
getNumberRoomOccupants()2756     public int getNumberRoomOccupants() {
2757         int total = 0;
2758         for (final MUCRoom room : localMUCRoomManager.getAll()) {
2759             total = total + room.getOccupantsCount();
2760         }
2761         return total;
2762     }
2763 
2764     /**
2765      * Returns the total number of incoming messages since last reset.
2766      *
2767      * @param resetAfter True if you want the counter to be reset after results returned.
2768      * @return the number of incoming messages through the service.
2769      */
2770     @Override
getIncomingMessageCount(final boolean resetAfter)2771     public long getIncomingMessageCount(final boolean resetAfter) {
2772         if (resetAfter) {
2773             return inMessages.getAndSet(0);
2774         }
2775         else {
2776             return inMessages.get();
2777         }
2778     }
2779 
2780     /**
2781      * Returns the total number of outgoing messages since last reset.
2782      *
2783      * @param resetAfter True if you want the counter to be reset after results returned.
2784      * @return the number of outgoing messages through the service.
2785      */
2786     @Override
getOutgoingMessageCount(final boolean resetAfter)2787     public long getOutgoingMessageCount(final boolean resetAfter) {
2788         if (resetAfter) {
2789             return outMessages.getAndSet(0);
2790         }
2791         else {
2792             return outMessages.get();
2793         }
2794     }
2795 
2796     @Override
logConversation(final MUCRoom room, final Message message, final JID sender)2797     public void logConversation(final MUCRoom room, final Message message, final JID sender) {
2798         // Only log messages that have a subject or body. Otherwise ignore it.
2799         if (message.getSubject() != null || message.getBody() != null) {
2800             getArchiver().archive( new ConversationLogEntry( new Date(), room, message, sender) );
2801         }
2802     }
2803 
2804     @Override
messageBroadcastedTo(final int numOccupants)2805     public void messageBroadcastedTo(final int numOccupants) {
2806         // Increment counter of received messages that where broadcasted by one
2807         inMessages.incrementAndGet();
2808         // Increment counter of outgoing messages with the number of room occupants
2809         // that received the message
2810         outMessages.addAndGet(numOccupants);
2811     }
2812 
2813     @Override
getItems()2814     public Iterator<DiscoServerItem> getItems() {
2815         // Check if the service is disabled. Info is not available when
2816         // disabled.
2817         if (!isServiceEnabled())
2818         {
2819             return null;
2820         }
2821 
2822         final ArrayList<DiscoServerItem> items = new ArrayList<>();
2823         final DiscoServerItem item = new DiscoServerItem(new JID(
2824             getServiceDomain()), getDescription(), null, null, this, this);
2825         items.add(item);
2826         return items.iterator();
2827     }
2828 
2829     @Override
getIdentities(final String name, final String node, final JID senderJID)2830     public Iterator<Element> getIdentities(final String name, final String node, final JID senderJID) {
2831         final ArrayList<Element> identities = new ArrayList<>();
2832         if (name == null && node == null) {
2833             // Answer the identity of the MUC service
2834             final Element identity = DocumentHelper.createElement("identity");
2835             identity.addAttribute("category", "conference");
2836             identity.addAttribute("name", getDescription());
2837             identity.addAttribute("type", "text");
2838             identities.add(identity);
2839 
2840             // TODO: Should internationalize Public Chatroom Search, and make it configurable.
2841             final Element searchId = DocumentHelper.createElement("identity");
2842             searchId.addAttribute("category", "directory");
2843             searchId.addAttribute("name", "Public Chatroom Search");
2844             searchId.addAttribute("type", "chatroom");
2845             identities.add(searchId);
2846 
2847             if (!extraDiscoIdentities.isEmpty()) {
2848                 identities.addAll(extraDiscoIdentities);
2849             }
2850         }
2851         else if (name != null && node == null) {
2852             // Answer the identity of a given room
2853             final MUCRoom room = getChatRoom(name);
2854             if (room != null) {
2855                 final Element identity = DocumentHelper.createElement("identity");
2856                 identity.addAttribute("category", "conference");
2857                 identity.addAttribute("name", room.getNaturalLanguageName());
2858                 identity.addAttribute("type", "text");
2859 
2860                 identities.add(identity);
2861             }
2862         }
2863         else if (name != null && "x-roomuser-item".equals(node)) {
2864             // Answer reserved nickname for the sender of the disco request in the requested room
2865             final MUCRoom room = getChatRoom(name);
2866             if (room != null) {
2867                 final String reservedNick = room.getReservedNickname(senderJID);
2868                 if (reservedNick != null) {
2869                     final Element identity = DocumentHelper.createElement("identity");
2870                     identity.addAttribute("category", "conference");
2871                     identity.addAttribute("name", reservedNick);
2872                     identity.addAttribute("type", "text");
2873 
2874                     identities.add(identity);
2875                 }
2876             }
2877         }
2878         return identities.iterator();
2879     }
2880 
2881     @Override
getFeatures(final String name, final String node, final JID senderJID)2882     public Iterator<String> getFeatures(final String name, final String node, final JID senderJID) {
2883         final ArrayList<String> features = new ArrayList<>();
2884         if (name == null && node == null) {
2885             // Answer the features of the MUC service
2886             features.add("http://jabber.org/protocol/muc");
2887             features.add("http://jabber.org/protocol/disco#info");
2888             features.add("http://jabber.org/protocol/disco#items");
2889             if ( IQMuclumbusSearchHandler.PROPERTY_ENABLED.getValue() ) {
2890                 features.add( "jabber:iq:search" );
2891             }
2892             features.add(IQMuclumbusSearchHandler.NAMESPACE);
2893             features.add(ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT);
2894             if (!extraDiscoFeatures.isEmpty()) {
2895                 features.addAll(extraDiscoFeatures);
2896             }
2897         }
2898         else if (name != null && node == null) {
2899             // Answer the features of a given room
2900             final MUCRoom room = getChatRoom(name);
2901             if (room != null) {
2902                 features.add("http://jabber.org/protocol/muc");
2903                 if (room.isPublicRoom()) {
2904                     features.add("muc_public");
2905                 } else {
2906                     features.add("muc_hidden");
2907                 }
2908                 if (room.isMembersOnly()) {
2909                     features.add("muc_membersonly");
2910                 }
2911                 else {
2912                     features.add("muc_open");
2913                 }
2914                 if (room.isModerated()) {
2915                     features.add("muc_moderated");
2916                 }
2917                 else {
2918                     features.add("muc_unmoderated");
2919                 }
2920                 if (room.canAnyoneDiscoverJID()) {
2921                     features.add("muc_nonanonymous");
2922                 }
2923                 else {
2924                     features.add("muc_semianonymous");
2925                 }
2926                 if (room.isPasswordProtected()) {
2927                     features.add("muc_passwordprotected");
2928                 }
2929                 else {
2930                     features.add("muc_unsecured");
2931                 }
2932                 if (room.isPersistent()) {
2933                     features.add("muc_persistent");
2934                 }
2935                 else {
2936                     features.add("muc_temporary");
2937                 }
2938                 if (!extraDiscoFeatures.isEmpty()) {
2939                     features.addAll(extraDiscoFeatures);
2940                 }
2941                 if ( JiveGlobals.getBooleanProperty( "xmpp.muc.self-ping.enabled", true ) ) {
2942                     features.add( "http://jabber.org/protocol/muc#self-ping-optimization" );
2943                 }
2944                 if ( IQMUCvCardHandler.PROPERTY_ENABLED.getValue() ) {
2945                     features.add( IQMUCvCardHandler.NAMESPACE );
2946                 }
2947                 features.add( "urn:xmpp:sid:0" );
2948             }
2949         }
2950         return features.iterator();
2951     }
2952 
2953     @Override
getExtendedInfo(final String name, final String node, final JID senderJID)2954     public DataForm getExtendedInfo(final String name, final String node, final JID senderJID) {
2955         return IQDiscoInfoHandler.getFirstDataForm(this.getExtendedInfos(name, node, senderJID));
2956     }
2957 
2958     @Override
getExtendedInfos(String name, String node, JID senderJID)2959     public Set<DataForm> getExtendedInfos(String name, String node, JID senderJID) {
2960         if (name != null && node == null) {
2961             // Answer the extended info of a given room
2962             final MUCRoom room = getChatRoom(name);
2963             if (room != null) {
2964                 final DataForm dataForm = new DataForm(Type.result);
2965 
2966                 final FormField fieldType = dataForm.addField();
2967                 fieldType.setVariable("FORM_TYPE");
2968                 fieldType.setType(FormField.Type.hidden);
2969                 fieldType.addValue("http://jabber.org/protocol/muc#roominfo");
2970 
2971                 final FormField fieldDescr = dataForm.addField();
2972                 fieldDescr.setVariable("muc#roominfo_description");
2973                 fieldDescr.setType(FormField.Type.text_single);
2974                 fieldDescr.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.desc"));
2975                 fieldDescr.addValue(room.getDescription());
2976 
2977                 final FormField fieldSubj = dataForm.addField();
2978                 fieldSubj.setVariable("muc#roominfo_subject");
2979                 fieldSubj.setType(FormField.Type.text_single);
2980                 fieldSubj.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.subject"));
2981                 fieldSubj.addValue(room.getSubject());
2982 
2983                 final FormField fieldOcc = dataForm.addField();
2984                 fieldOcc.setVariable("muc#roominfo_occupants");
2985                 fieldOcc.setType(FormField.Type.text_single);
2986                 fieldOcc.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.occupants"));
2987                 fieldOcc.addValue(Integer.toString(room.getOccupantsCount()));
2988 
2989                 /*field = new XFormFieldImpl("muc#roominfo_lang");
2990                 field.setType(FormField.Type.text_single);
2991                 field.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.language"));
2992                 field.addValue(room.getLanguage());
2993                 dataForm.addField(field);*/
2994 
2995                 final FormField fieldDate = dataForm.addField();
2996                 fieldDate.setVariable("x-muc#roominfo_creationdate");
2997                 fieldDate.setType(FormField.Type.text_single);
2998                 fieldDate.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.creationdate"));
2999                 fieldDate.addValue(XMPPDateTimeFormat.format(room.getCreationDate()));
3000                 final Set<DataForm> dataForms = new HashSet<>();
3001                 dataForms.add(dataForm);
3002                 return dataForms;
3003             }
3004         }
3005         return new HashSet<>();
3006     }
3007 
3008     /**
3009      * Adds an extra Disco feature to the list of features returned for the conference service.
3010      * @param feature Feature to add.
3011      */
3012     @Override
addExtraFeature(final String feature)3013     public void addExtraFeature(final String feature) {
3014         extraDiscoFeatures.add(feature);
3015     }
3016 
3017     /**
3018      * Removes an extra Disco feature from the list of features returned for the conference service.
3019      * @param feature Feature to remove.
3020      */
3021     @Override
removeExtraFeature(final String feature)3022     public void removeExtraFeature(final String feature) {
3023         extraDiscoFeatures.remove(feature);
3024     }
3025 
3026     /**
3027      * Adds an extra Disco identity to the list of identities returned for the conference service.
3028      * @param category Category for identity.  e.g. conference
3029      * @param name Descriptive name for identity.  e.g. Public Chatrooms
3030      * @param type Type for identity.  e.g. text
3031      */
3032     @Override
addExtraIdentity(final String category, final String name, final String type)3033     public void addExtraIdentity(final String category, final String name, final String type) {
3034         final Element identity = DocumentHelper.createElement("identity");
3035         identity.addAttribute("category", category);
3036         identity.addAttribute("name", name);
3037         identity.addAttribute("type", type);
3038         extraDiscoIdentities.add(identity);
3039     }
3040 
3041     /**
3042      * Removes an extra Disco identity from the list of identities returned for the conference service.
3043      * @param name Name of identity to remove.
3044      */
3045     @Override
removeExtraIdentity(final String name)3046     public void removeExtraIdentity(final String name) {
3047         final Iterator<Element> iter = extraDiscoIdentities.iterator();
3048         while (iter.hasNext()) {
3049             final Element elem = iter.next();
3050             if (name.equals(elem.attribute("name").getStringValue())) {
3051                 iter.remove();
3052                 break;
3053             }
3054         }
3055     }
3056 
3057     /**
3058      * Sets the MUC event delegate handler for this service.
3059      * @param delegate Handler for MUC events.
3060      */
3061     @Override
setMUCDelegate(final MUCEventDelegate delegate)3062     public void setMUCDelegate(final MUCEventDelegate delegate) {
3063         mucEventDelegate = delegate;
3064     }
3065 
3066     /**
3067      * Gets the MUC event delegate handler for this service.
3068      * @return Handler for MUC events (delegate)
3069      */
3070     @Override
getMUCDelegate()3071     public MUCEventDelegate getMUCDelegate() {
3072         return mucEventDelegate;
3073     }
3074 
3075     @Override
hasInfo(final String name, final String node, final JID senderJID)3076     public boolean hasInfo(final String name, final String node, final JID senderJID) {
3077         // Check if the service is disabled. Info is not available when disabled.
3078         if (!isServiceEnabled()) {
3079             return false;
3080         }
3081         if (name == null && node == null) {
3082             // We always have info about the MUC service
3083             return true;
3084         }
3085         else if (name != null && node == null) {
3086             // We only have info if the room exists
3087             return hasChatRoom(name);
3088         }
3089         else if (name != null && "x-roomuser-item".equals(node)) {
3090             // We always have info about reserved names as long as the room exists
3091             return hasChatRoom(name);
3092         }
3093         return false;
3094     }
3095 
3096     @Override
getItems(final String name, final String node, final JID senderJID)3097     public Iterator<DiscoItem> getItems(final String name, final String node, final JID senderJID) {
3098         // Check if the service is disabled. Info is not available when disabled.
3099         if (!isServiceEnabled()) {
3100             return null;
3101         }
3102         final Set<DiscoItem> answer = new HashSet<>();
3103         if (name == null && node == null)
3104         {
3105             // Before returning the items, ensure that all rooms are properly loaded in memory
3106             getActiveAndInactiveRooms();
3107 
3108             // Answer all the public rooms as items
3109             for (final MUCRoom room : localMUCRoomManager.getAll())
3110             {
3111                 if (canDiscoverRoom(room, senderJID))
3112                 {
3113                     answer.add(new DiscoItem(room.getRole().getRoleAddress(),
3114                         room.getNaturalLanguageName(), null, null));
3115                 }
3116             }
3117         }
3118         else if (name != null && node == null) {
3119             // Answer the room occupants as items if that info is publicly available
3120             final MUCRoom room = getChatRoom(name);
3121             if (room != null && canDiscoverRoom(room, senderJID)) {
3122                 for (final org.jivesoftware.openfire.muc.MUCRole role : room.getOccupants()) {
3123                     // TODO Should we filter occupants that are invisible (presence is not broadcasted)?
3124                     answer.add(new DiscoItem(role.getRoleAddress(), null, null, null));
3125                 }
3126             }
3127         }
3128         return answer.iterator();
3129     }
3130 
3131     @Override
canDiscoverRoom(final MUCRoom room, final JID entity)3132     public boolean canDiscoverRoom(final MUCRoom room, final JID entity) {
3133         // Check if locked rooms may be discovered
3134         if (!allowToDiscoverLockedRooms && room.isLocked()) {
3135             return false;
3136         }
3137         if (!room.isPublicRoom()) {
3138             if (!allowToDiscoverMembersOnlyRooms && room.isMembersOnly()) {
3139                 return false;
3140             }
3141             final MUCRole.Affiliation affiliation = room.getAffiliation(entity.asBareJID());
3142             return affiliation == MUCRole.Affiliation.owner
3143                 || affiliation == MUCRole.Affiliation.admin
3144                 || affiliation == MUCRole.Affiliation.member;
3145         }
3146         return true;
3147     }
3148 
3149     /**
3150      * Converts an array to a comma-delimited String.
3151      *
3152      * @param array the array.
3153      * @return a comma delimited String of the array values.
3154      */
fromArray(final String [] array)3155     private static String fromArray(final String [] array) {
3156         final StringBuilder buf = new StringBuilder();
3157         for (int i=0; i<array.length; i++) {
3158             buf.append(array[i]);
3159             if (i != array.length-1) {
3160                 buf.append(',');
3161             }
3162         }
3163         return buf.toString();
3164     }
3165 
3166     /**
3167      * Converts a collection to a comma-delimited String.
3168      *
3169      * @param coll the collection.
3170      * @return a comma delimited String of the array values.
3171      */
fromCollection(final Collection<JID> coll)3172     private static String fromCollection(final Collection<JID> coll) {
3173         final StringBuilder buf = new StringBuilder();
3174         for (final JID elem: coll) {
3175             buf.append(elem.toBareJID()).append(',');
3176         }
3177         final int endPos = buf.length() > 1 ? buf.length() - 1 : 0;
3178         return buf.substring(0, endPos);
3179     }
3180 
setHidden(boolean isHidden)3181     public void setHidden(boolean isHidden) {
3182         this.isHidden = isHidden;
3183     }
3184 
3185     @Override
isHidden()3186     public boolean isHidden() {
3187         return isHidden;
3188     }
3189 
3190 
3191     @Override
joinedCluster()3192     public void joinedCluster() {
3193         final String fullServiceName = chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain();
3194         Log.debug("Cluster event: service {} joined a cluster - going to restore {} rooms", fullServiceName, localMUCRoomManager.size());
3195 
3196         // The local node joined a cluster.
3197 
3198         // Upon joining a cluster, clustered caches are reset to their clustered equivalent (by the swap from the local
3199         // cache implementation to the clustered cache implementation that's done in the implementation of
3200         // org.jivesoftware.util.cache.CacheFactory.joinedCluster). This means that they now hold data that's
3201         // available on all other cluster nodes. Data that's available on the local node needs to be added again.
3202         final Set<OccupantManager.Occupant> occupantsToSync = localMUCRoomManager.restoreCacheContentAfterJoin(occupantManager);
3203 
3204         Log.debug("Occupants to sync: {}", occupantsToSync);
3205 
3206         // Let the other nodes know about our local occupants, as they need this information when it might leave the
3207         // cluster (OF-2224) and should tell its local users that new occupants have joined (OF-2233).
3208         if (!occupantsToSync.isEmpty()) {
3209             CacheFactory.doClusterTask(new SyncLocalOccupantsAndSendJoinPresenceTask(this.chatServiceName, occupantsToSync));
3210         }
3211         // TODO does this work properly when the rooms are not known on the other nodes?
3212     }
3213 
3214     @Override
joinedCluster(byte[] nodeID)3215     public void joinedCluster(byte[] nodeID) {
3216 
3217         final String fullServiceName = chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain();
3218         Log.debug("Cluster event: service {} got notified that node {} joined a cluster", fullServiceName, new String(nodeID));
3219 
3220         // Another node joined a cluster that we're already part of.
3221 
3222         // Let the new node know about our local occupants, as it needs this information when it might leave the cluster
3223         // again (OF-2224). The data is also needed for the joined node to be able to tell its local users that other
3224         // occupants have now joined a MUC room that they're in (OF-2233). To generate these stanzas, the receiving node
3225         // will probably need additional information. It can obtain this from the clustered cache. Even though we can't
3226         // be sure if that node has fully added _its own_ data to that cache (we don't know if restoreCacheContent()
3227         // has finished execution), we can be sure that the cache already holds data as provided by _us_.
3228         final Set<OccupantManager.Occupant> localOccupants = occupantManager.getLocalOccupants();
3229         CacheFactory.doClusterTask( new SyncLocalOccupantsAndSendJoinPresenceTask(this.chatServiceName, localOccupants), nodeID );
3230     }
3231 
3232     @Override
leftCluster()3233     public void leftCluster() {
3234         final String fullServiceName = chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain();
3235         Log.debug("Cluster event: service {} left a cluster - going to restore {} rooms", fullServiceName, localMUCRoomManager.size());
3236 
3237         // The local cluster node left the cluster.
3238         if (XMPPServer.getInstance().isShuttingDown()) {
3239             // Do not put effort in restoring the correct state if we're shutting down anyway.
3240             return;
3241         }
3242 
3243         // Get all room occupants that lived on all other nodes, as from the perspective of this node, those nodes are
3244         // now no longer part of the cluster.
3245         final Set<OccupantManager.Occupant> occupantsOnRemovedNodes = occupantManager.leftCluster();
3246 
3247         // Upon leaving a cluster, clustered caches are reset to their local equivalent (by the swap from the clustered
3248         // cache implementation to the default cache implementation that's done in the implementation of
3249         // org.jivesoftware.util.cache.CacheFactory.leftCluster). This means that they now hold no data (as a new cache
3250         // has been created). Data that's available on the local node needs to be added again.
3251         localMUCRoomManager.restoreCacheContentAfterLeave(occupantsOnRemovedNodes);
3252 
3253         // Send presence 'leave' for all of these users to the users that remain in the chatroom (on this node)
3254         makeOccupantsOnDisconnectedClusterNodesLeave(occupantsOnRemovedNodes, null);
3255     }
3256 
3257     @Override
leftCluster(byte[] nodeID)3258     public void leftCluster(byte[] nodeID)
3259     {
3260         // Another node left the cluster.
3261         //
3262         // If the cluster node leaves in an orderly fashion, it might have broadcast
3263         // the necessary events itself. This cannot be depended on, as the cluster node
3264         // might have disconnected unexpectedly (as a result of a crash or network issue).
3265         //
3266         // All chatroom occupants that were connected to the now disconnected node are no longer 'in the room'. The
3267         // remaining occupants should receive 'occupant left' stanzas to reflect this.
3268 
3269         final String fullServiceName = chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain();
3270         Log.debug("Cluster event: service {} got notified that node {} left a cluster", fullServiceName, new String(nodeID));
3271 
3272 
3273         // We can't be certain if the room cache is fully intact at this point, as described in OF-2297. It is not always
3274         // feasible to maintain enough 'local data' to be able to recover from this. Here, we check the cache for
3275         // consistency, and remove occupants from rooms (citing 'technical difficulties') where we find such inconsistency.
3276         final Set<String> lostRoomNames = localMUCRoomManager.detectAndRemoveLostRooms();
3277         for (final String lostRoomName : lostRoomNames) {
3278             try {
3279                 Log.info("Room '{}' was lost from the data structure that's shared in the cluster (the cache). This room is now considered 'gone' for this cluster node. Occupants will be informed.", lostRoomName);
3280                 final Set<OccupantManager.Occupant> occupants = occupantManager.occupantsForRoomByNode(lostRoomName, XMPPServer.getInstance().getNodeID());
3281                 final JID roomJID = new JID(lostRoomName, fullServiceName, null);
3282                 for (final OccupantManager.Occupant occupant : occupants) {
3283                     try {
3284                         // Send a presence stanza of type "unavailable" to the occupant
3285                         final Presence presence = new Presence(Presence.Type.unavailable);
3286                         presence.setTo(occupant.getRealJID());
3287                         assert lostRoomName.equals(occupant.getRoomName());
3288                         presence.setFrom(new JID(lostRoomName, fullServiceName, occupant.getNickname()));
3289 
3290                         // A fragment containing the x-extension.
3291                         final Element fragment = presence.addChildElement("x", "http://jabber.org/protocol/muc#user");
3292                         final Element item = fragment.addElement("item");
3293                         item.addAttribute("affiliation", "none");
3294                         item.addAttribute("role", "none");
3295                         fragment.addElement("status").addAttribute("code", "110"); // Inform user that presence refers to itself.
3296                         fragment.addElement("status").addAttribute("code", "333"); // Inform users that a user was removed because of an error reply.
3297 
3298                         Log.debug("Informing local occupant '{}' on local cluster node that it is being removed from room '{}' because of a (cluster) error.", occupant.getRealJID(), lostRoomName);
3299                         XMPPServer.getInstance().getPacketRouter().route(presence);
3300                         XMPPServer.getInstance().getPresenceUpdateHandler().removeDirectPresence(occupant.getRealJID(), roomJID);
3301                     } catch (Exception e) {
3302                         Log.warn("Unable to inform local occupant '{}' on local cluster node that it is being removed from room '{}' because of a (cluster) error.", occupant.getRealJID(), lostRoomName, e);
3303                     }
3304                 }
3305                 // Clean up the locally maintained bookkeeping.
3306                 occupantManager.roomDestroyed(roomJID);
3307                 removeChatRoom(lostRoomName);
3308             } catch (Exception e) {
3309                 Log.warn("Unable to inform occupants on local cluster node that they are being removed from room '{}' because of a (cluster) error.", lostRoomName, e);
3310             }
3311         }
3312 
3313         // Now that occupants have been properly ousted from the lost rooms, we can make an effort to restore the rooms
3314         // from the database. Calling getActiveAndInactiveRooms() will do just that.
3315         getActiveAndInactiveRooms();
3316 
3317         // From this point onwards, the remainder of what's in the cache can be considered 'consistent' (as we've dealt with the inconsistencies).
3318         // We now need to inform these occupants that occupants of the same room, that exist on other cluster nodes (which are now unreachable)
3319         // are no longer in the room.
3320 
3321 
3322         // Get all room occupants that lived on the node that disconnected
3323         final Set<OccupantManager.Occupant> occupantsOnRemovedNode = occupantManager.leftCluster(NodeID.getInstance(nodeID));
3324 
3325         // The content of the room cache can still hold (some) of these occupants. They should be removed from there!
3326         // The state of the rooms in the clustered cache should be modified to remove all but our local occupants.
3327         for (Iterator<OccupantManager.Occupant> i = occupantsOnRemovedNode.iterator(); i.hasNext(); ) {
3328             OccupantManager.Occupant occupant = i.next();
3329             final Lock lock = this.getChatRoomLock(occupant.getRoomName());
3330             lock.lock();
3331             try {
3332                 final MUCRoom room = this.getChatRoom(occupant.getRoomName());
3333                 if (room == null) {
3334                     continue;
3335                 }
3336                 final MUCRole occupantRole = room.getOccupantByFullJID(occupant.getRealJID());
3337 
3338                 // When leaving a cluster, the routing table will also detect that some clients are no
3339                 // longer available. And send presence unavailable stanzas accordingly. Because of that,
3340                 // the occupant may already have been removed from the room. This may however not happen
3341                 // at all if federated users are in play. Hence the null check here: if the occupant is
3342                 // no longer in the room, we don't need to remove it.
3343                 if (occupantRole != null) {
3344                     room.removeOccupantRole(occupantRole);
3345                     this.syncChatRoom(room);
3346                     Log.debug("Removed occupant role {} from room {}.", occupantRole, room.getJID());
3347                 } else {
3348                     Log.debug("Occupant '{}' already removed, so we don't need to send 'leave' presence in room {}", occupant, room.getJID());
3349                     i.remove();
3350                 }
3351             } finally {
3352                 lock.unlock();
3353             }
3354         }
3355 
3356         // Send presence 'leave' for all of these users to the users that remain in the chatroom (on this node)
3357         makeOccupantsOnDisconnectedClusterNodesLeave(occupantsOnRemovedNode, NodeID.getInstance(nodeID));
3358     }
3359 
3360     @Override
markedAsSeniorClusterMember()3361     public void markedAsSeniorClusterMember() {
3362         final String fullServiceName = chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain();
3363         Log.debug("Cluster event: service {} got notified that it is now the senior cluster member", fullServiceName);
3364         // Do nothing
3365         // TODO: Check if all occupants are still reachable
3366     }
3367 
3368     @Override
getLocalMUCRoomManager()3369     public LocalMUCRoomManager getLocalMUCRoomManager() {
3370         return localMUCRoomManager;
3371     }
3372 
3373     /**
3374      * Used by other nodes telling us about their occupants.
3375      * @param task
3376      */
process(@onnull final SyncLocalOccupantsAndSendJoinPresenceTask task)3377     public void process(@Nonnull final SyncLocalOccupantsAndSendJoinPresenceTask task) {
3378         occupantManager.process(task);
3379         makeOccupantsOnConnectedClusterNodeJoin(task.getOccupants(), task.getOriginator());
3380     }
3381 
3382     /**
3383      * Sends presence 'join' stanzas on behalf of chatroom occupants that are now connected to the cluster (by virtue
3384      * of a cluster node joining the cluster) to other room occupants are local to this cluster node.
3385      *
3386      * Note that this method does not change state (in either the clustered cache, or local equivalents): it only sends stanzas.
3387      *
3388      * @param occupantsOnConnectedNodes The occupants for which to send stanzas.
3389      * @param remoteNodeID The cluster node ID of the server that joined the cluster.
3390      */
makeOccupantsOnConnectedClusterNodeJoin(@ullable final Set<OccupantManager.Occupant> occupantsOnConnectedNodes, @Nonnull NodeID remoteNodeID)3391     private void makeOccupantsOnConnectedClusterNodeJoin(@Nullable final Set<OccupantManager.Occupant> occupantsOnConnectedNodes, @Nonnull NodeID remoteNodeID)
3392     {
3393         Log.debug("Going to send 'join' presence stanzas on the local cluster node for {} occupant(s) of cluster node {} that is now part of our cluster.", occupantsOnConnectedNodes == null || occupantsOnConnectedNodes.isEmpty() ? 0 : occupantsOnConnectedNodes.size(), remoteNodeID);
3394         if (occupantsOnConnectedNodes == null || occupantsOnConnectedNodes.isEmpty()) {
3395             return;
3396         }
3397 
3398         if (!MUCRoom.JOIN_PRESENCE_ENABLE.getValue()) {
3399             Log.trace("Skipping, as presences are disabled by configuration.");
3400             return;
3401         }
3402 
3403         // Find all occupants that were not already on any other node in the cluster. This intends to prevent sending 'join'
3404         // presences for occupants that are in the same room, using the same nickname, but using a client that is
3405         // connected to a cluster node that already was in the cluster.
3406         final Set<OccupantManager.Occupant> toAdd = occupantsOnConnectedNodes.stream()
3407             .filter(occupant -> !occupantManager.exists(occupant, remoteNodeID))
3408             .collect(Collectors.toSet());
3409         Log.trace("After accounting for occupants already in the room by virtue of having another connected device using the same nickname, {} presence stanza(s) are to be sent.", toAdd.size());
3410 
3411         // For each, broadcast a 'join' presence in the room(s).
3412         for (final OccupantManager.Occupant occupant : toAdd)
3413         {
3414             Log.trace("Preparing to send 'join' stanza for occupant '{}' (using nickname '{}') of room '{}'", occupant.getRealJID(), occupant.getNickname(), occupant.getRoomName());
3415 
3416             final MUCRoom chatRoom = getChatRoom(occupant.roomName);
3417             if (chatRoom == null) {
3418                 Log.info("User {} seems to be an occupant (using nickname '{}') of a non-existent room named '{}' on newly connected cluster node(s).", occupant.realJID, occupant.nickname, occupant.roomName);
3419                 continue;
3420             }
3421 
3422             // At this stage, the occupant information must already be available through the clustered cache.
3423             final MUCRole occupantRole = chatRoom.getOccupantByFullJID(occupant.getRealJID());
3424             if (occupantRole == null) {
3425                 Log.warn("A remote cluster node ({}) tells us that user {} is supposed to be an occupant (using nickname '{}') of a room named '{}' but the data in the cluster cache does not indicate that this is true.", remoteNodeID, occupant.realJID, occupant.nickname, occupant.roomName);
3426                 continue;
3427             }
3428 
3429             if (!chatRoom.canBroadcastPresence(occupantRole.getRole())) {
3430                 Log.trace("Skipping join stanza, as room is configured to not broadcast presence for role {}", occupantRole.getRole());
3431                 continue;
3432             }
3433 
3434             // To prevent each (remaining) cluster node from broadcasting the same presence to all occupants of all cluster nodes,
3435             // this broadcasts only to occupants on the local node.
3436             final Set<OccupantManager.Occupant> recipients = occupantManager.occupantsForRoomByNode(occupant.roomName, XMPPServer.getInstance().getNodeID());
3437             for (OccupantManager.Occupant recipient : recipients) {
3438                 Log.trace("Preparing stanza for recipient {} (nickname: {})", recipient.getRealJID(), recipient.getNickname());
3439                 try {
3440                     // Note that we cannot use org.jivesoftware.openfire.muc.MUCRoom.broadcastPresence() as this would attempt to
3441                     // broadcast to the user that is 'joining' to all users. We only want to broadcast locally here.
3442 
3443                     // We _need_ to go through the MUCRole for sending this stanza, as that has some additional logic (eg: FMUC).
3444                     final MUCRole recipientRole = chatRoom.getOccupantByFullJID(recipient.getRealJID());
3445                     if (recipientRole != null) {
3446                         recipientRole.send(occupantRole.getPresence());
3447                     } else {
3448                         Log.warn("Unable to find MUCRole for recipient '{}' in room {} while broadcasting 'join' presence for occupants on joining cluster node {}.", recipient.getRealJID(), chatRoom.getJID(), remoteNodeID);
3449                         XMPPServer.getInstance().getPacketRouter().route(occupantRole.getPresence()); // This is a partial fix that will _probably_ work if FMUC is not used. Better than nothing? (although an argument for failing-fast can be made).
3450                     }
3451                 } catch (Exception e) {
3452                     Log.warn("A problem occurred while notifying local occupant that user '{}' joined room '{}' as a result of a cluster node {} joining the cluster.", occupant.nickname, occupant.roomName, remoteNodeID, e);
3453                 }
3454             }
3455         }
3456         Log.debug("Finished sending 'join' presence stanzas on the local cluster node.");
3457     }
3458 
3459     /**
3460      * Sends presence 'leave' stanzas on behalf of chatroom occupants that are no longer connected to the cluster to
3461      * room occupants that are local to this cluster node.
3462      *
3463      * Note that this method does not change state (in either the clustered cache, or local equivalents): it only sends stanzas.
3464      *
3465      * @param occupantsOnRemovedNodes The occupants for which to send stanzas
3466      */
makeOccupantsOnDisconnectedClusterNodesLeave(@ullable final Set<OccupantManager.Occupant> occupantsOnRemovedNodes, NodeID nodeID)3467     private void makeOccupantsOnDisconnectedClusterNodesLeave(@Nullable final Set<OccupantManager.Occupant> occupantsOnRemovedNodes, NodeID nodeID)
3468     {
3469         Log.debug("Going to send 'leave' presence stanzas on the local cluster node for {} occupant(s) of one or more cluster nodes that are no longer part of our cluster.", occupantsOnRemovedNodes == null || occupantsOnRemovedNodes.isEmpty() ? 0 : occupantsOnRemovedNodes.size());
3470         if (occupantsOnRemovedNodes == null || occupantsOnRemovedNodes.isEmpty()) {
3471             return;
3472         }
3473 
3474         if (!MUCRoom.JOIN_PRESENCE_ENABLE.getValue()) {
3475             Log.trace("Skipping, as presences are disabled by configuration.");
3476             return;
3477         }
3478 
3479         // Find all occupants that are now no longer on any node in the cluster. This intends to prevent sending 'leave'
3480         // presences for occupants that are in the same room, using the same nickname, but using a client that is
3481         // connected to a cluster node that is still in the cluster.
3482         final Set<OccupantManager.Occupant> toRemove = occupantsOnRemovedNodes.stream()
3483             .filter(occupant -> !occupantManager.exists(occupant))
3484             .collect(Collectors.toSet());
3485         Log.trace("After accounting for occupants still in the room by virtue of having another connected device using the same nickname, {} presence stanza(s) are to be sent.", toRemove.size());
3486 
3487         // For each, broadcast a 'leave' presence in the room(s).
3488         for(final OccupantManager.Occupant occupant : toRemove) {
3489             Log.trace("Preparing to send 'leave' stanza for occupant '{}' (using nickname '{}') of room '{}'", occupant.getRealJID(), occupant.getNickname(), occupant.getRoomName());
3490             final MUCRoom chatRoom = getChatRoom(occupant.getRoomName());
3491             if (chatRoom == null) {
3492                 Log.info("User {} seems to be an occupant (using nickname '{}') of a non-existent room named '{}' on disconnected cluster node(s).", occupant.getRealJID(), occupant.getNickname(), occupant.getRoomName());
3493                 continue;
3494             }
3495 
3496             // To prevent each (remaining) cluster node from broadcasting the same presence to all occupants of all remaining nodes,
3497             // this broadcasts only to occupants on the local node.
3498             final Set<OccupantManager.Occupant> recipients;
3499             if (nodeID == null) {
3500                 recipients = occupantManager.occupantsForRoomByNode(occupant.getRoomName(), XMPPServer.getInstance().getNodeID());
3501                 Log.trace("Intended recipients, count: {} (occupants of the same room, on local node): {}", recipients.size(), recipients.stream().map(OccupantManager.Occupant::getRealJID).map(JID::toString).collect(Collectors.joining( ", " )));
3502             } else {
3503                 recipients = occupantManager.occupantsForRoomExceptForNode(occupant.getRoomName(), nodeID);
3504                 Log.trace("Intended recipients, count: {} (occupants of the same room, on all remaining cluster nodes): {}", recipients.size(), recipients.stream().map(OccupantManager.Occupant::getRealJID).map(JID::toString).collect(Collectors.joining( ", " )));
3505             }
3506             for (OccupantManager.Occupant recipient : recipients) {
3507                 try {
3508                     Log.trace("Preparing stanza for recipient {} (nickname: {})", recipient.getRealJID(), recipient.getNickname());
3509                     // Note that we cannot use chatRoom.sendLeavePresenceToExistingOccupants(leaveRole) as this would attempt to
3510                     // broadcast to the user that is leaving. That user is clearly unreachable in this instance (as it lives on
3511                     // a now disconnected cluster node.
3512                     final Presence presence = new Presence(Presence.Type.unavailable);
3513                     presence.setTo(recipient.getRealJID());
3514                     presence.setFrom(new JID(chatRoom.getJID().getNode(), chatRoom.getJID().getDomain(), occupant.nickname));
3515                     final Element childElement = presence.addChildElement("x", "http://jabber.org/protocol/muc#user");
3516                     final Element item = childElement.addElement("item");
3517                     item.addAttribute("role", "none");
3518                     if (chatRoom.canAnyoneDiscoverJID() || chatRoom.getModerators().stream().anyMatch(m->m.getUserAddress().asBareJID().equals(recipient.getRealJID().asBareJID()))) {
3519                         // Send non-anonymous - add JID.
3520                         item.addAttribute("jid", occupant.getRealJID().toString());
3521                     }
3522                     childElement.addElement( "status" ).addAttribute( "code", "333" );
3523 
3524                     // We _need_ to go through the MUCRole for sending this stanza, as that has some additional logic (eg: FMUC).
3525                     final MUCRole recipientRole = chatRoom.getOccupantByFullJID(recipient.getRealJID());
3526                     Log.debug("Stanza now being sent: {}", presence.toXML());
3527                     if (recipientRole != null) {
3528                         recipientRole.send(presence);
3529                     } else {
3530                         Log.warn("Unable to find MUCRole for recipient '{}' in room {} while broadcasting 'leave' presence for occupants on disconnected cluster node(s).", recipient.getRealJID(), chatRoom.getJID());
3531                         XMPPServer.getInstance().getPacketRouter().route(presence); // This is a partial fix that will _probably_ work if FMUC is not used. Better than nothing? (although an argument for failing-fast can be made).
3532                     }
3533                 } catch (Exception e) {
3534                     Log.warn("A problem occurred while notifying local occupant that user '{}' left room '{}' as a result of a cluster disconnect.", occupant.nickname, occupant.roomName, e);
3535                 }
3536             }
3537         }
3538         Log.debug("Finished sending 'leave' presence stanzas on the local cluster node.");
3539     }
3540 }
3541