1 /*
2  * Copyright (C) 2004-2008 Jive Software, 2021 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;
18 
19 import org.dom4j.DocumentHelper;
20 import org.dom4j.Element;
21 import org.dom4j.QName;
22 import org.jivesoftware.database.JiveID;
23 import org.jivesoftware.database.SequenceManager;
24 import org.jivesoftware.openfire.XMPPServer;
25 import org.jivesoftware.openfire.auth.UnauthorizedException;
26 import org.jivesoftware.openfire.event.GroupEventListener;
27 import org.jivesoftware.openfire.group.*;
28 import org.jivesoftware.openfire.muc.spi.*;
29 import org.jivesoftware.openfire.user.UserAlreadyExistsException;
30 import org.jivesoftware.openfire.user.UserNotFoundException;
31 import org.jivesoftware.util.*;
32 import org.jivesoftware.util.cache.CacheSizes;
33 import org.jivesoftware.util.cache.Cacheable;
34 import org.jivesoftware.util.cache.CannotCalculateSizeException;
35 import org.jivesoftware.util.cache.ExternalizableUtil;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38 import org.xmpp.packet.*;
39 import org.xmpp.resultsetmanagement.Result;
40 
41 import javax.annotation.Nonnull;
42 import javax.annotation.Nullable;
43 import java.io.Externalizable;
44 import java.io.IOException;
45 import java.io.ObjectInput;
46 import java.io.ObjectOutput;
47 import java.time.Duration;
48 import java.time.temporal.ChronoUnit;
49 import java.util.*;
50 import java.util.concurrent.*;
51 import java.util.stream.Collectors;
52 
53 /**
54  * A chat room on the chat server manages its users, and enforces it's own security rules.
55  *
56  * A MUCRoom could represent a persistent room which means that its configuration will be maintained in sync with its
57  * representation in the database, or it represents a non-persistent room. These rooms have no representation in the
58  * database.
59  *
60  * @author Gaston Dombiak
61  */
62 @JiveID(JiveConstants.MUC_ROOM)
63 public class MUCRoom implements GroupEventListener, Externalizable, Result, Cacheable {
64 
65     private static final Logger Log = LoggerFactory.getLogger(MUCRoom.class);
66 
67     public static final SystemProperty<Boolean> JOIN_PRESENCE_ENABLE = SystemProperty.Builder.ofType(Boolean.class)
68         .setKey("xmpp.muc.join.presence")
69         .setDynamic(true)
70         .setDefaultValue(true)
71         .build();
72 
73     private static final SystemProperty<Duration> SELF_PRESENCE_TIMEOUT = SystemProperty.Builder.ofType(Duration.class)
74         .setKey("xmpp.muc.join.self-presence-timeout")
75         .setDynamic(true)
76         .setDefaultValue(Duration.ofSeconds( 2 ))
77         .setChronoUnit(ChronoUnit.MILLIS)
78         .build();
79 
80     public static SystemProperty<Boolean> ALLOWPM_BLOCKALL = SystemProperty.Builder.ofType( Boolean.class )
81         .setKey("xmpp.muc.allowpm.blockall")
82         .setDefaultValue(false)
83         .setDynamic(true)
84         .build();
85 
86     /**
87      * The service hosting the room.
88      */
89     private MultiUserChatService mucService;
90 
91     /**
92      * All occupants that are associated with this room.
93      */
94     public ArrayList<MUCRole> occupants = new ArrayList<>();
95 
96     /**
97      * The name of the room.
98      */
99     private String name;
100 
101     /**
102      * The role of the room itself.
103      */
104     private MUCRole role;
105 
106     /**
107      * The start time of the chat.
108      */
109     long startTime;
110 
111     /**
112      * The end time of the chat.
113      */
114     long endTime;
115 
116     /**
117      * After a room has been destroyed it may remain in memory but it won't be possible to use it.
118      * When a room is destroyed it is immediately removed from the MultiUserChatService but it's
119      * possible that while the room was being destroyed it was being used by another thread so we
120      * need to protect the room under these rare circumstances.
121      */
122     public boolean isDestroyed = false;
123 
124     /**
125      * ChatRoomHistory object.
126      */
127     private MUCRoomHistory roomHistory;
128 
129     /**
130      * Time when the room was locked. A value of zero means that the room is unlocked.
131      */
132     private long lockedTime;
133 
134     /**
135      * List of chatroom's owner. The list contains only bare jid.
136      */
137     GroupAwareList<JID> owners = new ConcurrentGroupList<>();
138 
139     /**
140      * List of chatroom's admin. The list contains only bare jid.
141      */
142     GroupAwareList<JID> admins = new ConcurrentGroupList<>();
143 
144     /**
145      * List of chatroom's members. The list contains only bare jid, mapped to a nickname.
146      */
147     GroupAwareMap<JID, String> members = new ConcurrentGroupMap<>();
148 
149     /**
150      * List of chatroom's outcast. The list contains only bare jid of not allowed users.
151      */
152     private GroupAwareList<JID> outcasts = new ConcurrentGroupList<>();
153 
154     /**
155      * The natural language name of the room.
156      */
157     private String naturalLanguageName;
158 
159     /**
160      * Description of the room. The owner can change the description using the room configuration
161      * form.
162      */
163     private String description;
164 
165     /**
166      * Indicates if occupants are allowed to change the subject of the room.
167      */
168     private boolean canOccupantsChangeSubject;
169 
170     /**
171      * Maximum number of occupants that could be present in the room. If the limit's been reached
172      * and a user tries to join, a not-allowed error will be returned.
173      */
174     private int maxUsers;
175 
176     /**
177      * List of roles of which presence will be broadcast to the rest of the occupants. This
178      * feature is useful for implementing "invisible" occupants.
179      */
180     private List<MUCRole.Role> rolesToBroadcastPresence = new ArrayList<>();
181 
182     /**
183      * A public room means that the room is searchable and visible. This means that the room can be
184      * located using service discovery requests.
185      */
186     private boolean publicRoom;
187 
188     /**
189      * Persistent rooms are saved to the database to make sure that rooms configurations can be
190      * restored in case the server goes down.
191      */
192     private boolean persistent;
193 
194     /**
195      * Moderated rooms enable only participants to speak. Users that join the room and aren't
196      * participants can't speak (they are just visitors).
197      */
198     private boolean moderated;
199 
200     /**
201      * A room is considered members-only if an invitation is required in order to enter the room.
202      * Any user that is not a member of the room won't be able to join the room unless the user
203      * decides to register with the room (thus becoming a member).
204      */
205     private boolean membersOnly;
206 
207     /**
208      * Some rooms may restrict the occupants that are able to send invitations. Sending an
209      * invitation in a members-only room adds the invitee to the members list.
210      */
211     private boolean canOccupantsInvite;
212 
213     /**
214      * The password that every occupant should provide in order to enter the room.
215      */
216     private String password = null;
217 
218     /**
219      * Every presence packet can include the JID of every occupant unless the owner deactives this
220      * configuration.
221      */
222     private boolean canAnyoneDiscoverJID;
223 
224     /**
225      * The minimal role of persons that are allowed to send private messages in the room.
226      */
227     private String canSendPrivateMessage;
228 
229     /**
230      * Enables the logging of the conversation. The conversation in the room will be saved to the
231      * database.
232      */
233     private boolean logEnabled;
234 
235     /**
236      * Enables the logging of the conversation. The conversation in the room will be saved to the
237      * database.
238      */
239     private boolean loginRestrictedToNickname;
240 
241     /**
242      * Enables the logging of the conversation. The conversation in the room will be saved to the
243      * database.
244      */
245     private boolean canChangeNickname;
246 
247     /**
248      * Enables the logging of the conversation. The conversation in the room will be saved to the
249      * database.
250      */
251     private boolean registrationEnabled;
252 
253     /**
254      * Enables the FMUC functionality.
255      */
256     private boolean fmucEnabled;
257 
258     /**
259      * The address of the MUC room (typically on a remote XMPP domain) to which this room should initiate
260      * FMUC federation. In this federation, the local node takes the role of the 'joining' node, while the remote node
261      * takes the role of the 'joined' node.
262      *
263      * When this room is not expected to initiate federation (note that it can still accept inbound federation attempts)
264      * then this is null.
265      *
266      * Although a room can accept multiple inbound joins (where it acts as a 'parent' node), it can initiate only one
267      * outbound join at a time (where it acts as a 'child' node).
268      */
269     private JID fmucOutboundNode;
270 
271     /**
272      * The 'mode' that describes the FMUC configuration is captured in the supplied object, which is
273      * either master-master or master-slave.
274      *
275      * This should be null only when no outbound federation should be attempted (when {@link #fmucEnabled} is false).
276      */
277     private FMUCMode fmucOutboundMode;
278 
279     /**
280      * A set of addresses of MUC rooms (typically on a remote XMPP domain) that defines the list of rooms that is
281      * permitted to to federate with the local room.
282      *
283      * A null value is to be interpreted as allowing all rooms to be permitted.
284      *
285      * An empty set of addresses is to be interpreted as disallowing all rooms to be permitted.
286      */
287     private Set<JID> fmucInboundNodes;
288 
289     /**
290      * Internal component that handles IQ packets sent by the room owners.
291      */
292     private IQOwnerHandler iqOwnerHandler;
293 
294     /**
295      * Internal component that handles IQ packets sent by moderators, admins and owners.
296      */
297     private IQAdminHandler iqAdminHandler;
298 
299     /**
300      * Internal component that handles FMUC stanzas.
301      */
302     private FMUCHandler fmucHandler;
303 
304     /**
305      * The last known subject of the room. This information is used to respond disco requests. The
306      * MUCRoomHistory class holds the history of the room together with the last message that set
307      * the room's subject.
308      */
309     private String subject = "";
310 
311     /**
312      * The ID of the room. If the room is temporary and does not log its conversation then the value
313      * will always be -1. Otherwise a value will be obtained from the database.
314      */
315     private long roomID = -1;
316 
317     /**
318      * The date when the room was created.
319      */
320     private Date creationDate;
321 
322     /**
323      * The last date when the room's configuration was modified.
324      */
325     private Date modificationDate;
326 
327     /**
328      * The date when the last occupant left the room. A null value means that there are occupants
329      * in the room at the moment.
330      */
331     private Date emptyDate;
332 
333     /**
334      * Indicates if the room is present in the database.
335      */
336     private boolean savedToDB = false;
337 
338     /**
339      * Do not use this constructor. It was added to implement the Externalizable
340      * interface required to work inside of a cluster.
341      */
MUCRoom()342     public MUCRoom() {
343     }
344 
345     /**
346      * Create a new chat room.
347      *
348      * @param chatservice the service hosting the room.
349      * @param roomname the name of the room.
350      */
MUCRoom(@onnull MultiUserChatService chatservice, @Nonnull String roomname)351     public MUCRoom(@Nonnull MultiUserChatService chatservice, @Nonnull String roomname) {
352         this.mucService = chatservice;
353         this.name = roomname;
354         this.naturalLanguageName = roomname;
355         this.description = roomname;
356         this.startTime = System.currentTimeMillis();
357         this.creationDate = new Date(startTime);
358         this.modificationDate = new Date(startTime);
359         this.emptyDate = new Date(startTime);
360         this.canOccupantsChangeSubject = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.canOccupantsChangeSubject", false);
361         this.maxUsers = MUCPersistenceManager.getIntProperty(mucService.getServiceName(), "room.maxUsers", 30);
362         this.publicRoom = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.publicRoom", true);
363         this.persistent = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.persistent", false);
364         this.moderated = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.moderated", false);
365         this.membersOnly = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.membersOnly", false);
366         this.canOccupantsInvite = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.canOccupantsInvite", false);
367         this.canAnyoneDiscoverJID = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.canAnyoneDiscoverJID", true);
368         this.logEnabled = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.logEnabled", true);
369         this.loginRestrictedToNickname = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.loginRestrictedToNickname", false);
370         this.canChangeNickname = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.canChangeNickname", true);
371         this.registrationEnabled = MUCPersistenceManager.getBooleanProperty(mucService.getServiceName(), "room.registrationEnabled", true);
372         // TODO Allow to set the history strategy from the configuration form?
373         roomHistory = new MUCRoomHistory(this, new HistoryStrategy(mucService.getHistoryStrategy()));
374         this.iqOwnerHandler = new IQOwnerHandler(this);
375         this.iqAdminHandler = new IQAdminHandler(this);
376         this.fmucHandler = new FMUCHandler(this);
377         // No one can join the room except the room's owner
378         this.lockedTime = startTime;
379         // Set the default roles for which presence is broadcast
380         rolesToBroadcastPresence.add(MUCRole.Role.moderator);
381         rolesToBroadcastPresence.add(MUCRole.Role.participant);
382         rolesToBroadcastPresence.add(MUCRole.Role.visitor);
383         role = MUCRole.createRoomRole(this);
384     }
385 
386     /**
387      * Get the name of this room.
388      *
389      * @return The name for this room
390      */
getName()391     public String getName() {
392         return name;
393     }
394 
395     /**
396      * Get the full JID of this room.
397      *
398      * @return the JID for this room.
399      */
getJID()400     public JID getJID() {
401         return new JID(getName(), getMUCService().getServiceDomain(), null);
402     }
403 
404     /**
405      * Get the multi user chat service the room is attached to.
406      *
407      * @return the MultiUserChatService instance that the room is attached to.
408      */
getMUCService()409     public MultiUserChatService getMUCService() {
410         return mucService;
411     }
412 
413     /**
414      * Sets the multi user chat service the room is attached to.
415      *
416      * @param service The MultiUserChatService that the room is attached to (cannot be {@code null}).
417      */
setMUCService( MultiUserChatService service)418     public void setMUCService( MultiUserChatService service) {
419         this.mucService = service;
420     }
421 
422     /**
423      * Obtain a unique numerical id for this room. Useful for storing rooms in databases. If the
424      * room is persistent or is logging the conversation then the returned ID won't be -1.
425      *
426      * @return The unique id for this room or -1 if the room is temporary and is not logging the
427      * conversation.
428      */
getID()429     public long getID() {
430         if (isPersistent() || isLogEnabled()) {
431             if (roomID == -1) {
432                 roomID = SequenceManager.nextID(JiveConstants.MUC_ROOM);
433             }
434         }
435         return roomID;
436     }
437 
438     /**
439      * Sets a new room ID if the room has just been saved to the database or sets the saved ID of
440      * the room in the database while loading the room.
441      *
442      * @param roomID the saved ID of the room in the DB or a new one if the room is being saved to the DB.
443      */
setID(long roomID)444     public void setID(long roomID) {
445         this.roomID = roomID;
446     }
447 
448     /**
449      * Returns the date when the room was created.
450      *
451      * @return the date when the room was created.
452      */
getCreationDate()453     public Date getCreationDate() {
454         return creationDate;
455     }
456 
457     /**
458      * Sets the date when the room was created.
459      *
460      * @param creationDate the date when the room was created (cannot be {@code null}).
461      */
setCreationDate(Date creationDate)462     public void setCreationDate(Date creationDate) {
463         this.creationDate = creationDate;
464     }
465 
466     /**
467      * Returns the last date when the room's configuration was modified. If the room's configuration
468      * was never modified then the creation date will be returned.
469      *
470      * @return the last date when the room's configuration was modified.
471      */
getModificationDate()472     public Date getModificationDate() {
473         return modificationDate;
474     }
475 
476     /**
477      * Sets the last date when the room's configuration was modified. If the room's configuration
478      * was never modified then the initial value will be the same as the creation date.
479      *
480      * @param modificationDate the last date when the room's configuration was modified (cannot be {@code null}).
481      */
setModificationDate(Date modificationDate)482     public void setModificationDate(Date modificationDate) {
483         this.modificationDate = modificationDate;
484     }
485 
486     /**
487      * Sets the date when the last occupant left the room. A null value means that there are
488      * occupants in the room at the moment.
489      *
490      * @param emptyDate the date when the last occupant left the room or null if there are occupants in the room (can be {@code null}).
491      */
setEmptyDate(Date emptyDate)492     public void setEmptyDate(Date emptyDate) {
493         // Do nothing if old value is same as new value
494         if (this.emptyDate == emptyDate) {
495             return;
496         }
497         this.emptyDate = emptyDate;
498         MUCPersistenceManager.updateRoomEmptyDate(this);
499     }
500 
501     /**
502      * Returns the date when the last occupant left the room. A null value means that there are
503      * occupants in the room at the moment.
504      *
505      * @return the date when the last occupant left the room or null if there are occupants in the
506      *         room at the moment.
507      */
getEmptyDate()508     public Date getEmptyDate() {
509         return this.emptyDate;
510     }
511 
512     /**
513      * Obtain the role of the chat server (mainly for addressing messages and presence).
514      *
515      * @return The role for the chat room itself
516      */
getRole()517     public MUCRole getRole() {
518         return role;
519     }
520 
521     /**
522      * Obtain the first role of a given user by nickname.
523      *
524      * @param nickname The nickname of the user you'd like to obtain (cannot be {@code null})
525      * @return The user's role in the room
526      * @throws UserNotFoundException If there is no user with the given nickname
527      * @deprecated Prefer {@link #getOccupantsByNickname(String)} instead (a user may be connected more than once)
528      */
529     @Deprecated
getOccupant(String nickname)530     public MUCRole getOccupant(String nickname) throws UserNotFoundException {
531         if (nickname == null) {
532             throw new UserNotFoundException();
533         }
534         List<MUCRole> roles = getOccupantsByNickname(nickname);
535         if (roles != null && roles.size() > 0) {
536             return roles.get(0);
537         }
538         throw new UserNotFoundException();
539     }
540 
541     /**
542      * Obtain the roles of a given user by nickname. A user can be connected to a room more than once.
543      *
544      * @param nickname The nickname of the user you'd like to obtain (cannot be {@code null})
545      * @return The user's role in the room
546      * @throws UserNotFoundException If there is no user with the given nickname
547      */
getOccupantsByNickname(String nickname)548     public List<MUCRole> getOccupantsByNickname(String nickname) throws UserNotFoundException {
549         if (nickname == null) {
550             throw new UserNotFoundException();
551         }
552 
553         final List<MUCRole> roles = occupants.stream()
554             .filter(mucRole -> mucRole.getNickname().equalsIgnoreCase(nickname))
555             .collect(Collectors.toList());
556 
557         if (roles.isEmpty()) {
558             throw new UserNotFoundException("Unable to find occupant with nickname '" + nickname + "' in room '" + name + "'");
559         }
560         return roles;
561     }
562 
563     /**
564      * Obtain the roles of a given user in the room by his bare JID. A user can have several roles,
565      * one for each client resource from which the user has joined the room.
566      *
567      * @param jid The bare jid of the user you'd like to obtain  (cannot be {@code null}).
568      * @return The user's roles in the room
569      * @throws UserNotFoundException If there is no user with the given nickname
570      */
getOccupantsByBareJID(JID jid)571     public List<MUCRole> getOccupantsByBareJID(JID jid) throws UserNotFoundException
572     {
573         final List<MUCRole> roles = occupants.stream()
574             .filter(mucRole -> mucRole.getUserAddress().asBareJID().equals(jid))
575             .collect(Collectors.toList());
576 
577         if (roles.isEmpty()) {
578             throw new UserNotFoundException();
579         }
580 
581         return Collections.unmodifiableList(roles);
582     }
583 
584     /**
585      * Returns the role of a given user in the room by his full JID or {@code null}
586      * if no role was found for the specified user.
587      *
588      * @param jid The full jid of the user you'd like to obtain  (cannot be {@code null}).
589      * @return The user's role in the room or null if not found.
590      */
getOccupantByFullJID(JID jid)591     public MUCRole getOccupantByFullJID(JID jid)
592     {
593         final List<MUCRole> roles = occupants.stream()
594             .filter(mucRole -> mucRole.getUserAddress().equals(jid))
595             .collect(Collectors.toList());
596 
597         switch (roles.size()) {
598             case 0: return null;
599             default:
600                 Log.warn("Room '{}' has more than one role with full JID '{}'!", getJID(), jid);
601                 // Intended fall-through: return the first one.
602             case 1: return roles.iterator().next();
603         }
604     }
605 
606     /**
607      * Obtain the roles of all users in the chatroom.
608      *
609      * @return a collection with all users in the chatroom
610      */
getOccupants()611     public Collection<MUCRole> getOccupants() {
612         return Collections.unmodifiableCollection(occupants);
613     }
614 
615     /**
616      * Returns the number of occupants in the chatroom at the moment.
617      *
618      * @return int the number of occupants in the chatroom at the moment.
619      */
getOccupantsCount()620     public int getOccupantsCount() {
621         return occupants.size();
622     }
623 
624     /**
625      * Determine if a given nickname is taken.
626      *
627      * @param nickname The nickname of the user you'd like to obtain  (cannot be {@code null}).
628      * @return True if a nickname is taken
629      */
hasOccupant(String nickname)630     public boolean hasOccupant(String nickname)
631     {
632         return occupants.stream()
633             .anyMatch(mucRole -> mucRole.getNickname().equalsIgnoreCase(nickname));
634     }
635 
hasOccupant(JID jid)636     public boolean hasOccupant(JID jid)
637     {
638         return occupants.stream()
639             .anyMatch(mucRole -> mucRole.getUserAddress().equals(jid) || mucRole.getUserAddress().asBareJID().equals(jid));
640     }
641 
642     /**
643      * Returns the reserved room nickname for the bare JID or null if none.
644      *
645      * @param jid The bare jid of the user of which you'd like to obtain his reserved nickname (cannot be {@code null}).
646      * @return the reserved room nickname for the bare JID or null if none.
647      */
getReservedNickname(JID jid)648     public String getReservedNickname(JID jid) {
649         final JID bareJID = jid.asBareJID();
650         String answer = members.get(bareJID);
651         if (answer == null || answer.trim().length() == 0) {
652             return null;
653         }
654         return answer;
655     }
656 
657     /**
658      * Returns the bare JID of the member for which a nickname is reserved. Returns null if no member registered the
659      * nickname.
660      *
661      * @param nickname The nickname for which to lookup a member. Cannot be {@code null}.
662      * @return the bare JID of the member that has registered this nickname, or null if none.
663      */
getMemberForReservedNickname(String nickname)664     public JID getMemberForReservedNickname(String nickname) {
665         for (final Map.Entry<JID, String> entry : members.entrySet()) {
666             if (entry.getValue().equalsIgnoreCase(nickname)) {
667                 return entry.getKey();
668             }
669         }
670         return null;
671     }
672 
673     /**
674      * Returns the affiliation state of the user in the room. Possible affiliations are
675      * MUCRole.OWNER, MUCRole.ADMINISTRATOR, MUCRole.MEMBER, MUCRole.OUTCAST and MUCRole.NONE.<p>
676      *
677      * Note: Prerequisite - A lock must already be obtained before sending this message.
678      *
679      * @param jid The bare jid of the user of which you'd like to obtain his affiliation (cannot be {@code null}).
680      * @return the affiliation state of the user in the room.
681      */
getAffiliation(@onnull JID jid)682     public MUCRole.Affiliation getAffiliation(@Nonnull JID jid) {
683         final JID bareJID = jid.asBareJID();
684 
685         if (owners.includes(bareJID)) {
686             return MUCRole.Affiliation.owner;
687         }
688         else if (admins.includes(bareJID)) {
689             return MUCRole.Affiliation.admin;
690         }
691         // explicit outcast status has higher precedence than member status
692         else if (outcasts.includes(bareJID)) {
693             return MUCRole.Affiliation.outcast;
694         }
695         else if (members.includesKey(bareJID)) {
696             return MUCRole.Affiliation.member;
697         }
698         return MUCRole.Affiliation.none;
699     }
700 
getRole(@onnull JID jid)701     public MUCRole.Role getRole(@Nonnull JID jid)
702     {
703         final JID bareJID = jid.asBareJID();
704 
705         if (owners.includes(bareJID)) {
706             return MUCRole.Role.moderator;
707         }
708         else if (admins.includes(bareJID)) {
709             return MUCRole.Role.moderator;
710         }
711         // explicit outcast status has higher precedence than member status
712         else if (outcasts.contains(bareJID)) {
713             return null; // Outcasts have no role, as they're not allowed in the room.
714         }
715         else if (members.includesKey(bareJID)) {
716             // The user is a member. Set the role and affiliation accordingly.
717             return MUCRole.Role.participant;
718         }
719         else {
720             return isModerated() ? MUCRole.Role.visitor : MUCRole.Role.participant;
721         }
722     }
723 
724     /**
725      * Joins the room using the given nickname.
726      *
727      * @param nickname       The nickname the user wants to use in the chatroom  (cannot be {@code null}).
728      * @param password       The password provided by the user to enter the chatroom or null if none.
729      * @param historyRequest The amount of history that the user request or null meaning default.
730      * @param realAddress    The 'real' (non-room) JID of the user that is joining (cannot be {@code null}).
731      * @param presence       The presence sent by the user to join the room (cannot be {@code null}).
732      * @return The role created for the user.
733      * @throws UnauthorizedException         If the user doesn't have permission to join the room.
734      * @throws UserAlreadyExistsException    If the nickname is already taken.
735      * @throws RoomLockedException           If the user is trying to join a locked room.
736      * @throws ForbiddenException            If the user is an outcast.
737      * @throws RegistrationRequiredException If the user is not a member of a members-only room.
738      * @throws ConflictException             If another user attempts to join the room with a
739      *                                       nickname reserved by the first user.
740      * @throws ServiceUnavailableException   If the user cannot join the room since the max number
741      *                                       of users has been reached.
742      * @throws NotAcceptableException       If the registered user is trying to join with a
743      *                                      nickname different than the reserved nickname.
744      */
joinRoom( @onnull String nickname, @Nullable String password, @Nullable HistoryRequest historyRequest, @Nonnull JID realAddress, @Nonnull Presence presence )745     public MUCRole joinRoom( @Nonnull String nickname,
746                              @Nullable String password,
747                              @Nullable HistoryRequest historyRequest,
748                              @Nonnull JID realAddress,
749                              @Nonnull Presence presence )
750         throws UnauthorizedException, UserAlreadyExistsException, RoomLockedException, ForbiddenException,
751         RegistrationRequiredException, ConflictException, ServiceUnavailableException, NotAcceptableException
752     {
753         Log.debug( "User '{}' attempts to join room '{}' using nickname '{}'.", realAddress, this.getJID(), nickname );
754         MUCRole joinRole;
755         boolean clientOnlyJoin; // A "client only join" here is one where the client is already joined, but has re-joined.
756 
757         synchronized (this) {
758             // Determine the corresponding role based on the user's affiliation
759             final JID bareJID = realAddress.asBareJID();
760             MUCRole.Role role = getRole( bareJID );
761             MUCRole.Affiliation affiliation = getAffiliation( bareJID );
762             if (affiliation != MUCRole.Affiliation.owner && mucService.isSysadmin(bareJID)) {
763                 // The user is a system administrator of the MUC service. Treat him as an owner although he won't appear in the list of owners
764                 Log.debug( "User '{}' is a sysadmin. Treat as owner.", realAddress);
765                 role = MUCRole.Role.moderator;
766                 affiliation = MUCRole.Affiliation.owner;
767             }
768             Log.debug( "User '{}' role and affiliation in room '{} are determined to be: {}, {}", realAddress, this.getJID(), role, affiliation );
769 
770             // Verify that the attempt meets all preconditions for joining the room.
771             checkJoinRoomPreconditions( realAddress, nickname, affiliation, password, presence );
772 
773             // Is this client already joined with this nickname?
774             clientOnlyJoin = alreadyJoinedWithThisNick( realAddress, nickname );
775 
776             // TODO up to this point, room state has not been modified, even though a write-lock has been acquired. Can we optimize concurrency by locking with only a read-lock up until here?
777             if (!clientOnlyJoin)
778             {
779                 Log.debug( "Adding user '{}' as an occupant of room '{}' using nickname '{}'.", realAddress, this.getJID(), nickname );
780 
781                 // Create a new role for this user in this room.
782                 joinRole = new MUCRole(this, nickname, role, affiliation, realAddress, presence);
783 
784                 // See if we need to join a federated room. Note that this can be blocking!
785                 final Future<?> join = fmucHandler.join(joinRole);
786                 try
787                 {
788                     // FIXME make this properly asynchronous, instead of blocking the thread!
789                     join.get(5, TimeUnit.MINUTES);
790                 }
791                 catch ( InterruptedException | ExecutionException | TimeoutException e )
792                 {
793                     Log.error( "An exception occurred while processing FMUC join for user '{}' in room '{}'", joinRole.getUserAddress(), this.getJID(), e);
794                 }
795 
796                 addOccupantRole(joinRole);
797 
798             } else {
799                 // Grab the existing one.
800                 Log.debug( "Skip adding user '{}' as an occupant of room '{}' using nickname '{}', as it already is. Updating occupancy with its latest presence information.", realAddress, this.getJID(), nickname );
801                 joinRole = getOccupantByFullJID(realAddress);
802                 joinRole.setPresence( presence ); // OF-1581: Use latest presence information.
803             }
804         }
805 
806         // Exchange initial presence information between occupants of the room.
807         sendInitialPresencesToNewOccupant( joinRole );
808 
809         // OF-2042: XEP dictates an order of events. Wait for the presence exchange to finish, before progressing.
810         final CompletableFuture<Void> future = sendInitialPresenceToExistingOccupants(joinRole);
811         try {
812             final Duration timeout = SELF_PRESENCE_TIMEOUT.getValue();
813             future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
814         } catch ( InterruptedException e ) {
815             Log.debug( "Presence broadcast has been interrupted before it completed. Will continue to process the join of occupant '{}' to room '{}' as if it has.", joinRole.getUserAddress(), this.getJID(), e);
816         } catch ( TimeoutException e ) {
817             Log.warn( "Presence broadcast has not yet been completed within the allocated period. Will continue to process the join of occupant '{}' to room '{}' as if it has.", joinRole.getUserAddress(), this.getJID(), e);
818         } catch ( ExecutionException e ) {
819             Log.warn( "Presence broadcast caused an exception. Will continue to process the join of occupant '{}' to room '{}' as if it has.", joinRole.getUserAddress(), this.getJID(), e);
820         }
821 
822         // If the room has just been created send the "room locked until configuration is confirmed" message.
823         // It is assumed that the room is new based on the fact that it's locked and that it was locked when it was created.
824         final boolean isRoomNew = isLocked() && creationDate.getTime() == lockedTime;
825         if (!isRoomNew && isLocked()) {
826             // TODO Verify if it's right that this check occurs only _after_ a join was deemed 'successful' and initial presences have been exchanged.
827             // http://xmpp.org/extensions/xep-0045.html#enter-locked
828             Log.debug( "User '{}' attempts to join room '{}' that is locked (pending configuration confirmation). Sending an error.", realAddress, this.getJID() );
829             final Presence presenceItemNotFound = new Presence(Presence.Type.error);
830             presenceItemNotFound.setError(PacketError.Condition.item_not_found);
831             presenceItemNotFound.setFrom(role.getRoleAddress());
832 
833             // Not needed to create a defensive copy of the stanza. It's not used anywhere else.
834             joinRole.send(presenceItemNotFound);
835         }
836 
837         sendRoomHistoryAfterJoin( realAddress, joinRole, historyRequest );
838         sendRoomSubjectAfterJoin( realAddress, joinRole );
839 
840         if (!clientOnlyJoin) {
841             // Update the date when the last occupant left the room
842             setEmptyDate(null);
843         }
844         return joinRole;
845     }
846 
847     /**
848      * Sends the room history to a user that just joined the room.
849      */
sendRoomHistoryAfterJoin(@onnull final JID realAddress, @Nonnull MUCRole joinRole, @Nullable HistoryRequest historyRequest )850     private void sendRoomHistoryAfterJoin(@Nonnull final JID realAddress, @Nonnull MUCRole joinRole, @Nullable HistoryRequest historyRequest )
851     {
852         if (historyRequest == null) {
853             Log.trace( "Sending default room history to user '{}' that joined room '{}'.", realAddress, this.getJID() );
854             final Iterator<Message> history = roomHistory.getMessageHistory();
855             while (history.hasNext()) {
856                 // OF-2163: Prevent modifying the original history stanza (that can be retrieved by others later) by making a defensive copy.
857                 //          This prevents the stanzas in the room history to have a 'to' address for the last user that it was sent to.
858                 final Message message = history.next().createCopy();
859                 joinRole.send(message);
860             }
861         } else {
862             Log.trace( "Sending user-requested room history to user '{}' that joined room '{}'.", realAddress, this.getJID() );
863             historyRequest.sendHistory(joinRole, roomHistory);
864         }
865     }
866 
867     /**
868      * Sends the room subject to a user that just joined the room.
869      */
sendRoomSubjectAfterJoin(@onnull final JID realAddress, @Nonnull MUCRole joinRole )870     private void sendRoomSubjectAfterJoin(@Nonnull final JID realAddress, @Nonnull MUCRole joinRole )
871     {
872         Log.trace( "Sending room subject to user '{}' that joined room '{}'.", realAddress, this.getJID() );
873 
874         Message roomSubject = roomHistory.getChangedSubject();
875         if (roomSubject != null) {
876             // OF-2163: Prevent modifying the original subject stanza (that can be retrieved by others later) by making a defensive copy.
877             //          This prevents the stanza kept in memory to have the 'to' address for the last user that it was sent to.
878             roomSubject.createCopy();
879         } else {
880             // 7.2.15 If there is no subject set, the room MUST return an empty <subject/> element.
881             roomSubject = new Message();
882             roomSubject.setFrom( this.getJID() );
883             roomSubject.setType( Message.Type.groupchat );
884             roomSubject.setID( UUID.randomUUID().toString() );
885             roomSubject.getElement().addElement( "subject" );
886         }
887         joinRole.send(roomSubject);
888     }
889 
alreadyJoinedWithThisNick(@onnull final JID realJID, @Nonnull final String nickname)890     public boolean alreadyJoinedWithThisNick(@Nonnull final JID realJID, @Nonnull final String nickname)
891     {
892         return occupants.stream()
893             .anyMatch(mucRole -> mucRole.getUserAddress().equals(realJID) && mucRole.getNickname().equalsIgnoreCase(nickname));
894     }
895 
896     /**
897      * Checks all preconditions for joining a room. If one of them fails, an Exception is thrown.
898      */
checkJoinRoomPreconditions( @onnull final JID realAddress, @Nonnull final String nickname, @Nonnull final MUCRole.Affiliation affiliation, @Nullable final String password, @Nonnull final Presence presence)899     private void checkJoinRoomPreconditions(
900         @Nonnull final JID realAddress,
901         @Nonnull final String nickname,
902         @Nonnull final MUCRole.Affiliation affiliation,
903         @Nullable final String password,
904         @Nonnull final Presence presence)
905         throws ServiceUnavailableException, RoomLockedException, UserAlreadyExistsException, UnauthorizedException, ConflictException, NotAcceptableException, ForbiddenException, RegistrationRequiredException
906     {
907         Log.debug( "Checking all preconditions for user '{}' to join room '{}'.", realAddress, this.getJID() );
908 
909         checkJoinRoomPreconditionDelegate( realAddress );
910 
911         // If the room has a limit of max user then check if the limit has been reached
912         checkJoinRoomPreconditionMaxOccupants( realAddress );
913 
914         // If the room is locked and this user is not an owner raise a RoomLocked exception
915         checkJoinRoomPreconditionLocked( realAddress );
916 
917         // Check if the nickname is already used in the room
918         checkJoinRoomPreconditionNicknameInUse( realAddress, nickname );
919 
920         // If the room is password protected and the provided password is incorrect raise a
921         // Unauthorized exception - unless the JID that is joining is a system admin.
922         checkJoinRoomPreconditionPasswordProtection( realAddress, password );
923 
924         // If another user attempts to join the room with a nickname reserved by the first user
925         // raise a ConflictException
926         checkJoinRoomPreconditionNicknameReserved( realAddress, nickname );
927 
928         checkJoinRoomPreconditionRestrictedToNickname( realAddress, nickname );
929 
930         // Check if the user can join the room.
931         checkJoinRoomPreconditionIsOutcast( realAddress, affiliation );
932 
933         // If the room is members-only and the user is not a member. Raise a "Registration Required" exception.
934         checkJoinRoomPreconditionMemberOnly( realAddress, affiliation );
935 
936         Log.debug( "All preconditions for user '{}' to join room '{}' have been met. User can join the room.", realAddress, this.getJID() );
937     }
938 
checkJoinRoomPreconditionDelegate( @onnull final JID realAddress )939     private void checkJoinRoomPreconditionDelegate( @Nonnull final JID realAddress ) throws UnauthorizedException
940     {
941         boolean canJoin = true;
942         if (mucService.getMUCDelegate() != null) {
943             if (!mucService.getMUCDelegate().joiningRoom(this, realAddress)) {
944                 // Delegate said no, reject join.
945                 canJoin = false;
946             }
947         }
948         Log.trace( "{} Room join precondition 'delegate': User '{}' {} join room '{}'.", canJoin ? "PASS" : "FAIL", realAddress, canJoin ? "can" : "cannot", this.getJID() );
949         if (!canJoin) {
950             throw new UnauthorizedException();
951         }
952     }
953 
954     /**
955      * Checks if the room has a limit of max user, then check if the limit has been reached
956      *
957      * @param realAddress The address of the user that attempts to join.
958      * @throws ServiceUnavailableException when joining is prevented by virtue of the room having reached maximum capacity.
959      */
checkJoinRoomPreconditionMaxOccupants( @onnull final JID realAddress )960     private void checkJoinRoomPreconditionMaxOccupants( @Nonnull final JID realAddress ) throws ServiceUnavailableException
961     {
962         final boolean canJoin = canJoinRoom(realAddress);
963         Log.trace( "{} Room join precondition 'max occupants': User '{}' {} join room '{}'.", canJoin ? "PASS" : "FAIL", realAddress, canJoin ? "can" : "cannot", this.getJID() );
964         if (!canJoinRoom(realAddress)) {
965             throw new ServiceUnavailableException( "This room has reached its maximum number of occupants." );
966         }
967     }
968 
969     /**
970      * Checks if the room is locked and this user is not an owner
971      *
972      * @param realAddress The address of the user that attempts to join.
973      * @throws RoomLockedException when joining is prevented by virtue of the room being locked.
974      */
checkJoinRoomPreconditionLocked( @onnull final JID realAddress )975     private void checkJoinRoomPreconditionLocked( @Nonnull final JID realAddress ) throws RoomLockedException
976     {
977         boolean canJoin = true;
978         final JID bareJID = realAddress.asBareJID();
979         boolean isOwner = owners.includes(bareJID);
980         if (isLocked()) {
981             if (!isOwner) {
982                 canJoin = false;
983             }
984         }
985         Log.trace( "{} Room join precondition 'room locked': User '{}' {} join room '{}'.", canJoin ? "PASS" : "FAIL", realAddress, canJoin ? "can" : "cannot", this.getJID() );
986         if (!canJoin) {
987             throw new RoomLockedException( "This room is locked (and you are not an owner)." );
988         }
989     }
990 
991     /**
992      * Checks if the nickname that the user attempts to use is already used by someone else in the room.
993      *
994      * @param realAddress The address of the user that attempts to join.
995      * @param nickname The nickname that the user is attempting to use
996      * @throws UserAlreadyExistsException when joining is prevented by virtue of someone else in the room using the nickname.
997      */
checkJoinRoomPreconditionNicknameInUse(@onnull final JID realAddress, @Nonnull String nickname )998     private void checkJoinRoomPreconditionNicknameInUse(@Nonnull final JID realAddress, @Nonnull String nickname ) throws UserAlreadyExistsException
999     {
1000         final JID bareJID = realAddress.asBareJID();
1001         final boolean canJoin = occupants == null || occupants.stream().noneMatch(mucRole -> !mucRole.getUserAddress().asBareJID().equals(bareJID) && mucRole.getNickname().equalsIgnoreCase(nickname));
1002         Log.trace( "{} Room join precondition 'nickname in use': User '{}' {} join room '{}'.", canJoin ? "PASS" : "FAIL", realAddress, canJoin ? "can" : "cannot", this.getJID() );
1003         if (!canJoin) {
1004             throw new UserAlreadyExistsException( "Someone else in the room uses the nickname that you want to use." );
1005         }
1006     }
1007 
1008     /**
1009      * Checks if the user provided the correct password, if applicable.
1010      *
1011      * @param realAddress The address of the user that attempts to join.
1012      * @throws UnauthorizedException when joining is prevented by virtue of password protection.
1013      */
checkJoinRoomPreconditionPasswordProtection(@onnull final JID realAddress, @Nullable String providedPassword )1014     private void checkJoinRoomPreconditionPasswordProtection(@Nonnull final JID realAddress, @Nullable String providedPassword ) throws UnauthorizedException
1015     {
1016         boolean canJoin = true;
1017         final JID bareJID = realAddress.asBareJID();
1018         if (isPasswordProtected()) {
1019             final boolean isCorrectPassword = (providedPassword != null && providedPassword.equals(getPassword()));
1020             final boolean isSysadmin = mucService.isSysadmin(bareJID);
1021             final boolean requirePassword = !isSysadmin || mucService.isPasswordRequiredForSysadminsToJoinRoom();
1022             if (!isCorrectPassword && requirePassword ) {
1023                 canJoin = false;
1024             }
1025         }
1026         Log.trace( "{} Room join precondition 'password protection': User '{}' {} join room '{}'.", canJoin ? "PASS" : "FAIL", realAddress, canJoin ? "can" : "cannot", this.getJID() );
1027         if (!canJoin) {
1028             throw new UnauthorizedException( "You did not supply the correct password needed to join this room." );
1029         }
1030     }
1031 
1032     /**
1033      * Checks if the nickname that the user attempts to use has been reserved by a(nother) member of the room.
1034      *
1035      * @param realAddress The address of the user that attempts to join.
1036      * @param nickname The nickname that the user is attempting to use
1037      * @throws ConflictException when joining is prevented by virtue of someone else in the room having reserved the nickname.
1038      */
checkJoinRoomPreconditionNicknameReserved(@onnull final JID realAddress, @Nonnull final String nickname )1039     private void checkJoinRoomPreconditionNicknameReserved(@Nonnull final JID realAddress, @Nonnull final String nickname ) throws ConflictException
1040     {
1041         final JID bareJID = realAddress.asBareJID();
1042         final JID bareMemberJid = getMemberForReservedNickname(nickname);
1043         final boolean canJoin = bareMemberJid == null || bareMemberJid.equals(bareJID);
1044         Log.trace( "{} Room join precondition 'nickname reserved': User '{}' {} join room '{}'.", canJoin ? "PASS" : "FAIL", realAddress, canJoin ? "can" : "cannot", this.getJID() );
1045         if (!canJoin) {
1046             throw new ConflictException( "Someone else in the room has reserved the nickname that you want to use." );
1047         }
1048     }
1049 
1050     /**
1051      * Checks, when joins are restricted to reserved nicknames, if the nickname that the user attempts to use is the
1052      * nickname that has been reserved by that room.
1053      *
1054      * @param realAddress The address of the user that attempts to join.
1055      * @param nickname The nickname that the user is attempting to use
1056      * @throws NotAcceptableException when joining is prevented by virtue of using an incorrect nickname.
1057      */
checkJoinRoomPreconditionRestrictedToNickname(@onnull final JID realAddress, @Nonnull final String nickname )1058     private void checkJoinRoomPreconditionRestrictedToNickname(@Nonnull final JID realAddress, @Nonnull final String nickname ) throws NotAcceptableException
1059     {
1060         boolean canJoin = true;
1061         String reservedNickname = null;
1062         final JID bareJID = realAddress.asBareJID();
1063         if (isLoginRestrictedToNickname()) {
1064             reservedNickname = members.get(bareJID);
1065             if (reservedNickname != null && !nickname.toLowerCase().equals(reservedNickname)) {
1066                 canJoin = false;
1067             }
1068         }
1069 
1070         Log.trace( "{} Room join precondition 'restricted to nickname': User '{}' {} join room {}. Reserved nickname: '{}'.", canJoin ? "PASS" : "FAIL", realAddress, canJoin ? "can" : "cannot", this.getJID(), reservedNickname );
1071         if (!canJoin) {
1072             throw new NotAcceptableException( "This room is configured to restrict joins to reserved nicknames. The nickname that you supplied was not the nickname that you reserved for this room, which is: " + reservedNickname );
1073         }
1074     }
1075 
1076     /**
1077      * Checks if the person that attempts to join has been banned from the room.
1078      *
1079      * @param realAddress The address of the user that attempts to join.
1080      * @throws ForbiddenException when joining is prevented by virtue of the user being banned.
1081      */
checkJoinRoomPreconditionIsOutcast(@onnull final JID realAddress, @Nonnull final MUCRole.Affiliation affiliation )1082     private void checkJoinRoomPreconditionIsOutcast(@Nonnull final JID realAddress, @Nonnull final MUCRole.Affiliation affiliation ) throws ForbiddenException
1083     {
1084         boolean canJoin = affiliation != MUCRole.Affiliation.outcast;
1085 
1086         Log.trace( "{} Room join precondition 'is outcast': User '{}' {} join room '{}'.", canJoin ? "PASS" : "FAIL", realAddress, canJoin ? "can" : "cannot", this.getJID() );
1087         if (!canJoin) {
1088             throw new ForbiddenException( "You have been banned (marked as 'outcast') from this room." );
1089         }
1090     }
1091 
1092     /**
1093      * Checks if the person that attempts to join is a member of a member-only room.
1094      *
1095      * @param realAddress The address of the user that attempts to join.
1096      * @throws RegistrationRequiredException when joining is prevented by virtue of the user joining a member-only room without being a member.
1097      */
checkJoinRoomPreconditionMemberOnly(@onnull final JID realAddress, @Nonnull final MUCRole.Affiliation affiliation )1098     private void checkJoinRoomPreconditionMemberOnly(@Nonnull final JID realAddress, @Nonnull final MUCRole.Affiliation affiliation ) throws RegistrationRequiredException
1099     {
1100         boolean canJoin = !isMembersOnly() || Arrays.asList( MUCRole.Affiliation.admin, MUCRole.Affiliation.owner, MUCRole.Affiliation.member ).contains( affiliation );
1101 
1102         Log.trace( "{} Room join precondition 'member-only': User '{}' {} join room '{}'.", canJoin ? "PASS" : "FAIL", realAddress, canJoin ? "can" : "cannot", this.getJID() );
1103         if (!canJoin) {
1104             throw new RegistrationRequiredException( "This room is member-only, but you are not a member." );
1105         }
1106     }
1107 
1108     /**
1109      * Can a user join this room
1110      *
1111      * @param realAddress The address of the user that attempts to join.
1112      * @return indication if the user can join
1113      */
canJoinRoom(@onnull final JID realAddress)1114     private boolean canJoinRoom(@Nonnull final JID realAddress){
1115         boolean isOwner = owners.includes(realAddress.asBareJID());
1116         boolean isAdmin = admins.includes(realAddress.asBareJID());
1117         return (!isDestroyed && (!hasOccupancyLimit() || isAdmin || isOwner || (getOccupantsCount() < getMaxUsers())));
1118     }
1119 
1120     /**
1121      * Does this room have an occupancy limit?
1122      *
1123      * @return boolean
1124      */
hasOccupancyLimit()1125     private boolean hasOccupancyLimit(){
1126         return getMaxUsers() != 0;
1127     }
1128 
1129     /**
1130      * Sends presence of existing occupants to new occupant.
1131      *
1132      * @param joinRole the role of the new occupant in the room.
1133      */
sendInitialPresencesToNewOccupant(MUCRole joinRole)1134     void sendInitialPresencesToNewOccupant(MUCRole joinRole) {
1135         if (!JOIN_PRESENCE_ENABLE.getValue()) {
1136             Log.debug( "Skip exchanging presence between existing occupants of room '{}' and new occupant '{}' as it is disabled by configuration.", this.getJID(), joinRole.getUserAddress() );
1137             return;
1138         }
1139 
1140         Log.trace( "Send presence of existing occupants of room '{}' to new occupant '{}'.", this.getJID(), joinRole.getUserAddress() );
1141         for ( final MUCRole occupant : getOccupants() ) {
1142             if (occupant == joinRole) {
1143                 continue;
1144             }
1145 
1146             // Skip to the next occupant if we cannot send presence of this occupant
1147             if (!canBroadcastPresence(occupant.getRole())) {
1148                 continue;
1149             }
1150 
1151             final Presence occupantPresence = occupant.getPresence().createCopy(); // defensive copy to prevent modifying the original.
1152             if (!canAnyoneDiscoverJID() && MUCRole.Role.moderator != joinRole.getRole()) {
1153                 // Don't include the occupant's JID if the room is semi-anon and the new occupant is not a moderator
1154                 final Element frag = occupantPresence.getChildElement("x", "http://jabber.org/protocol/muc#user");
1155                 frag.element("item").addAttribute("jid", null);
1156             }
1157             joinRole.send(occupantPresence);
1158         }
1159     }
1160 
1161     /**
1162      * Adds the role of the occupant from all the internal occupants collections.
1163      *
1164      * @param role the role to add.
1165      */
addOccupantRole(@onnull final MUCRole role)1166     public void addOccupantRole(@Nonnull final MUCRole role)
1167     {
1168         if (occupants.contains(role)) {
1169             // Ignore a data consistency problem. This indicates that a bug exists somewhere, so log it verbosely.
1170             Log.warn("Not re-adding an occupant {} that already exists in room {}!", role, this.getJID(), new IllegalStateException("Duplicate occupant: " + role));
1171             return;
1172         }
1173 
1174         Log.trace( "Add occupant to room {}: {}", this.getJID(), role );
1175         occupants.add(role);
1176 
1177         // Fire event that occupant joined the room.
1178         MUCEventDispatcher.occupantJoined(role.getRoleAddress().asBareJID(), role.getUserAddress(), role.getNickname());
1179     }
1180 
1181     /**
1182      * Sends presence of a leaving occupant to applicable occupants of the room that is being left.
1183      *
1184      * @param leaveRole the role of the occupant that is leaving.
1185      */
sendLeavePresenceToExistingOccupants(MUCRole leaveRole)1186     public CompletableFuture<Void> sendLeavePresenceToExistingOccupants(MUCRole leaveRole) {
1187         // Send the presence of this new occupant to existing occupants
1188         Log.trace( "Send presence of leaving occupant '{}' to existing occupants of room '{}'.", leaveRole.getUserAddress(), this.getJID() );
1189         try {
1190             final Presence originalPresence = leaveRole.getPresence();
1191             final Presence presence = originalPresence.createCopy(); // Defensive copy to not modify the original.
1192             presence.setType(Presence.Type.unavailable);
1193             presence.setStatus(null);
1194             // Change (or add) presence information about roles and affiliations
1195             Element childElement = presence.getChildElement("x", "http://jabber.org/protocol/muc#user");
1196             if (childElement == null) {
1197                 childElement = presence.addChildElement("x", "http://jabber.org/protocol/muc#user");
1198             }
1199             Element item = childElement.element("item");
1200             if (item == null) {
1201                 item = childElement.addElement("item");
1202             }
1203             item.addAttribute("role", "none");
1204 
1205             // Check to see if the user's original role is one we should broadcast a leave packet for,
1206             // or if the leaving role is using multi-session nick (in which case _only_ the leaving client should be informed).
1207             if(!canBroadcastPresence(leaveRole.getRole()) || getOccupantsByNickname(leaveRole.getNickname()).size() > 1){
1208                 // Inform the leaving user that he/she has left the room
1209                 leaveRole.send(createSelfPresenceCopy(presence, false));
1210                 return CompletableFuture.completedFuture(null);
1211             }
1212             else {
1213                 // Inform all room occupants that the user has left the room
1214                 if (JOIN_PRESENCE_ENABLE.getValue()) {
1215                     return broadcastPresence(presence, false, leaveRole);
1216                 }
1217             }
1218         }
1219         catch (Exception e) {
1220             Log.error( "An exception occurred while sending leave presence of occupant '{}' to the other occupants of room: '{}'.", leaveRole.getUserAddress(), this.getJID(), e);
1221         }
1222         return CompletableFuture.completedFuture(null);
1223     }
1224 
1225     /**
1226      * Remove a member from the chat room.
1227      *
1228      * @param leaveRole The role that the user that left the room has prior to the user leaving.
1229      */
leaveRoom(@onnull final MUCRole leaveRole)1230     public void leaveRoom(@Nonnull final MUCRole leaveRole) {
1231         sendLeavePresenceToExistingOccupants(leaveRole)
1232             // DO NOT use 'thenRunAsync', as that will cause issues with clustering (it uses an executor that overrides the contextClassLoader, causing ClassNotFound exceptions in ClusterExternalizableUtil).
1233             .thenRun( () -> {
1234                 // Remove occupant from room and destroy room if empty and not persistent
1235                 removeOccupantRole(leaveRole);
1236 
1237                 // TODO Implement this: If the room owner becomes unavailable for any reason before
1238                 // submitting the form (e.g., a lost connection), the service will receive a presence
1239                 // stanza of type "unavailable" from the owner to the room@service/nick or room@service
1240                 // (or both). The service MUST then destroy the room, sending a presence stanza of type
1241                 // "unavailable" from the room to the owner including a <destroy/> element and reason
1242                 // (if provided) as defined under the "Destroying a Room" use case.
1243 
1244                 // Remove the room from the service only if there are no more occupants and the room is
1245                 // not persistent
1246                 if (getOccupants().isEmpty()) {
1247                     if (!isPersistent()) {
1248                         endTime = System.currentTimeMillis();
1249                         destroyRoom(null, "Removal of empty, non-persistent room.");
1250                     }
1251                     // Update the date when the last occupant left the room
1252                     setEmptyDate(new Date());
1253                 }
1254             });
1255     }
1256 
1257     /**
1258      * Sends presence of new occupant to existing occupants.
1259      *
1260      * @param joinRole the role of the new occupant in the room.
1261      */
sendInitialPresenceToExistingOccupants( MUCRole joinRole)1262     public CompletableFuture<Void> sendInitialPresenceToExistingOccupants( MUCRole joinRole) {
1263         // Send the presence of this new occupant to existing occupants
1264         Log.trace( "Send presence of new occupant '{}' to existing occupants of room '{}'.", joinRole.getUserAddress(), this.getJID() );
1265         try {
1266             final Presence joinPresence = joinRole.getPresence().createCopy();
1267             return broadcastPresence(joinPresence, true, joinRole);
1268         } catch (Exception e) {
1269             Log.error( "An exception occurred while sending initial presence of new occupant '{}' to the existing occupants of room: '{}'", joinRole.getUserAddress(), this.getJID(), e);
1270         }
1271         return CompletableFuture.completedFuture(null);
1272     }
1273 
1274     /**
1275      * Removes the role of the occupant from all the internal occupants collections. The role will
1276      * also be removed from the user's roles.
1277      *
1278      * @param leaveRole the role to remove.
1279      */
removeOccupantRole(@onnull final MUCRole leaveRole)1280     public void removeOccupantRole(@Nonnull final MUCRole leaveRole) {
1281         Log.trace( "Remove occupant from room {}: {}", this.getJID(), leaveRole );
1282         occupants.remove(leaveRole);
1283         MUCEventDispatcher.occupantLeft(leaveRole.getRoleAddress(), leaveRole.getUserAddress(), leaveRole.getNickname());
1284     }
1285 
1286     /**
1287      * Destroys the room. Each occupant will be removed and will receive a presence stanza of type
1288      * "unavailable" whose "from" attribute will be the occupant's nickname that the user knows he
1289      * or she has been removed from the room.
1290      *
1291      * @param alternateJID an optional alternate JID. Commonly used to provide a replacement room. (can be {@code null})
1292      * @param reason an optional reason why the room was destroyed (can be {@code null}).
1293      */
destroyRoom(JID alternateJID, String reason)1294     public void destroyRoom(JID alternateJID, String reason) {
1295         Collection<MUCRole> removedRoles = new CopyOnWriteArrayList<>();
1296 
1297         fmucHandler.stop();
1298 
1299         // Remove each occupant from a copy of the list of occupants (to prevent ConcurrentModificationException).
1300         for (MUCRole leaveRole : getOccupants()) {
1301             if (leaveRole != null) {
1302                 // Add the removed occupant to the list of removed occupants. We are keeping a
1303                 // list of removed occupants to process later outside of the lock.
1304                 removedRoles.add(leaveRole);
1305             }
1306         }
1307         endTime = System.currentTimeMillis();
1308         // Set that the room has been destroyed
1309         isDestroyed = true;
1310         // Removes the room from the list of rooms hosted in the service
1311         mucService.removeChatRoom(name);
1312 
1313         // Send an unavailable presence to each removed occupant
1314         for (MUCRole removedRole : removedRoles) {
1315             try {
1316                 // Send a presence stanza of type "unavailable" to the occupant
1317                 final Presence presence = createPresence(Presence.Type.unavailable);
1318                 presence.setFrom(removedRole.getRoleAddress());
1319 
1320                 // A fragment containing the x-extension for room destruction.
1321                 final Element fragment = presence.addChildElement("x","http://jabber.org/protocol/muc#user");
1322                 final Element item = fragment.addElement("item");
1323                 item.addAttribute("affiliation", "none");
1324                 item.addAttribute("role", "none");
1325                 if (alternateJID != null) {
1326                     fragment.addElement("destroy").addAttribute("jid", alternateJID.toString());
1327                 }
1328                 if (reason != null && reason.length() > 0) {
1329                     Element destroy = fragment.element("destroy");
1330                     if (destroy == null) {
1331                         destroy = fragment.addElement("destroy");
1332                     }
1333                     destroy.addElement("reason").setText(reason);
1334                 }
1335 
1336                 // Not needed to create a defensive copy of the stanza. It's not used anywhere else.
1337                 removedRole.send(presence);
1338                 removeOccupantRole(removedRole);
1339             }
1340             catch (Exception e) {
1341                 Log.error(e.getMessage(), e);
1342             }
1343         }
1344 
1345         // Remove the room from the DB if the room was persistent
1346         MUCPersistenceManager.deleteFromDB(this);
1347         // Fire event that the room has been destroyed
1348         MUCEventDispatcher.roomDestroyed(getRole().getRoleAddress());
1349     }
1350 
1351     /**
1352      * Create a new presence in this room for the given role.
1353      *
1354      * @param presenceType Type of presence to create (cannot be {@code null}).
1355      * @return The new presence
1356      * @throws UnauthorizedException If the user doesn't have permission to leave the room
1357      */
createPresence(Presence.Type presenceType)1358     public Presence createPresence(Presence.Type presenceType) {
1359         Presence presence = new Presence();
1360         presence.setType(presenceType);
1361         presence.setFrom(role.getRoleAddress());
1362         return presence;
1363     }
1364 
1365     /**
1366      * Broadcast a given message to all members of this chat room. The sender is always set to
1367      * be the chatroom.
1368      *
1369      * @param msg The message to broadcast (cannot be {@code null})
1370      */
serverBroadcast(String msg)1371     public void serverBroadcast(String msg) {
1372         Message message = new Message();
1373         message.setType(Message.Type.groupchat);
1374         message.setBody(msg);
1375         message.setFrom(role.getRoleAddress());
1376         broadcast(message, role);
1377     }
1378 
1379     /**
1380      * Sends a message to the all the occupants. In a moderated room, this privilege is restricted
1381      * to occupants with a role of participant or higher. In an unmoderated room, any occupant can
1382      * send a message to all other occupants.
1383      *
1384      * @param message The message to send (cannot be {@code null}).
1385      * @param senderRole the role of the user that is trying to send a public message (cannot be {@code null}).
1386      * @throws ForbiddenException If the user is not allowed to send a public message (i.e. does not
1387      *             have voice in the room).
1388      */
sendPublicMessage(Message message, MUCRole senderRole)1389     public void sendPublicMessage(Message message, MUCRole senderRole) throws ForbiddenException {
1390         // Check that if the room is moderated then the sender of the message has to have voice
1391         if (isModerated() && senderRole.getRole().compareTo(MUCRole.Role.participant) > 0) {
1392             throw new ForbiddenException();
1393         }
1394         // Send the message to all occupants
1395         message.setFrom(senderRole.getRoleAddress());
1396         if (canAnyoneDiscoverJID) {
1397             addRealJidToMessage(message, senderRole);
1398         }
1399         send(message, senderRole);
1400         // Fire event that message was received by the room
1401         MUCEventDispatcher.messageReceived(getRole().getRoleAddress(), senderRole.getUserAddress(),
1402             senderRole.getNickname(), message);
1403     }
1404 
1405     /**
1406      * Sends a private packet to a selected occupant. The packet can be a Message for private
1407      * conversation between room occupants or IQ packets when an occupant wants to send IQ packets
1408      * to other room occupants.
1409      *
1410      * If the system property xmpp.muc.allowpm.blockall is set to true, this will block all private packets
1411      * However, if this property is false (by default) then it will allow non-message private packets.
1412      *
1413      * @param packet The packet to send.
1414      * @param senderRole the role of the user that is trying to send a public message.
1415      * @throws NotFoundException If the user is sending a packet to a room JID that does not exist.
1416      * @throws ForbiddenException If a user of this role is not permitted to send private messages in this room.
1417      */
sendPrivatePacket(Packet packet, MUCRole senderRole)1418     public void sendPrivatePacket(Packet packet, MUCRole senderRole) throws NotFoundException, ForbiddenException {
1419 
1420         if (packet instanceof Message || ALLOWPM_BLOCKALL.getValue()){
1421             //If the packet is a message, check that the user has permissions to send
1422             switch (senderRole.getRole()) { // intended fall-through
1423                 case none:
1424                     throw new ForbiddenException();
1425                 default:
1426                 case visitor:
1427                     if (canSendPrivateMessage().equals( "participants" )) throw new ForbiddenException();
1428                 case participant:
1429                     if (canSendPrivateMessage().equals( "moderators" )) throw new ForbiddenException();
1430                 case moderator:
1431                     if (canSendPrivateMessage().equals( "none" )) throw new ForbiddenException();
1432             }
1433         }
1434 
1435         String resource = packet.getTo().getResource();
1436 
1437         List<MUCRole> occupants;
1438         try {
1439             occupants = getOccupantsByNickname(resource.toLowerCase());
1440         } catch (UserNotFoundException e) {
1441             throw new NotFoundException();
1442         }
1443 
1444         // OF-2163: Prevent modifying the original stanza (that can be used by unrelated code, after this method returns) by making a defensive copy.
1445         final Packet stanza = packet.createCopy();
1446         if (canAnyoneDiscoverJID && stanza instanceof Message) {
1447             addRealJidToMessage((Message)stanza, senderRole);
1448         }
1449         stanza.setFrom(senderRole.getRoleAddress());
1450 
1451         // Sending the stanza will modify it. Make sure that the event listeners that are triggered after sending
1452         // the stanza don't get the 'real' address from the recipient.
1453         final Packet immutable = stanza.createCopy();
1454 
1455         // Forward it to each occupant.
1456         for (final MUCRole occupant : occupants) {
1457             occupant.send(stanza); // Use the stanza copy to send data. The 'to' address of this object will be changed by sending it.
1458             if (stanza instanceof Message) {
1459                 // Use an unmodified copy of the stanza (with the original 'to' address) when invoking event listeners (OF-2163)
1460                 MUCEventDispatcher.privateMessageRecieved(occupant.getUserAddress(), senderRole.getUserAddress(), (Message) immutable);
1461             }
1462         }
1463     }
1464 
1465     /**
1466      * Sends a packet to the occupants of the room.
1467      *
1468      * The second argument defines the sender/originator of the stanza. Typically, this is the same entity that's also
1469      * the 'subject' of the stanza (eg: someone that changed its presence or nickname). It is important to realize that
1470      * this needs to be the case. When, for example, an occupant is made a moderator, the 'sender' typically is the
1471      * entity that granted the role to another entity. It is also possible for the sender to be a reflection of the room
1472      * itself. This scenario typically occurs when the sender can't be identified as an occupant of the room, such as,
1473      * for example, changes applied through the Openfire admin console.
1474      *
1475      * @param packet The packet to send
1476      * @param sender Representation of the entity that sent the stanza.
1477      */
send(@onnull Packet packet, @Nonnull MUCRole sender)1478     public void send(@Nonnull Packet packet, @Nonnull MUCRole sender) {
1479         if (packet instanceof Message) {
1480             broadcast((Message)packet, sender);
1481         }
1482         else if (packet instanceof Presence) {
1483             broadcastPresence((Presence)packet, false, sender);
1484         }
1485         else if (packet instanceof IQ) {
1486             IQ reply = IQ.createResultIQ((IQ) packet);
1487             reply.setChildElement(((IQ) packet).getChildElement());
1488             reply.setError(PacketError.Condition.bad_request);
1489             XMPPServer.getInstance().getPacketRouter().route(reply);
1490         }
1491     }
1492 
1493     /**
1494      * Broadcasts the specified presence to all room occupants. If the presence belongs to a
1495      * user whose role cannot be broadcast then the presence will only be sent to the presence's
1496      * user. On the other hand, the JID of the user that sent the presence won't be included if the
1497      * room is semi-anon and the target occupant is not a moderator.
1498      *
1499      * @param presence the presence to broadcast.
1500      * @param isJoinPresence If the presence is sent in the context of joining the room.
1501      * @param sender The role of the entity that initiated the presence broadcast
1502      */
broadcastPresence( Presence presence, boolean isJoinPresence, @Nonnull MUCRole sender)1503     private CompletableFuture<Void> broadcastPresence( Presence presence, boolean isJoinPresence, @Nonnull MUCRole sender) {
1504         if (presence == null) {
1505             return CompletableFuture.completedFuture(null);
1506         }
1507 
1508         if (!presence.getFrom().asBareJID().equals(this.getJID())) {
1509             // At this point, the 'from' address of the to-be broadcasted stanza can be expected to be the role-address
1510             // of the subject, or more broadly: it's bare JID representation should match that of the room. If that's not
1511             // the case then there's a bug in Openfire. Catch this here, as otherwise, privacy-sensitive data is leaked.
1512             // See: OF-2152
1513             throw new IllegalArgumentException("Broadcasted presence stanza's 'from' JID " + presence.getFrom() + " does not match room JID: " + this.getJID());
1514         }
1515 
1516         // Create a defensive copy, to prevent modifications to leak back to the invoker.
1517         final Presence stanza = presence.createCopy();
1518 
1519         // Some clients send a presence update to the room, rather than to their own nickname.
1520         if ( JiveGlobals.getBooleanProperty("xmpp.muc.presence.overwrite-to-room", true) && stanza.getTo() != null && stanza.getTo().getResource() == null && sender.getRoleAddress() != null) {
1521             stanza.setTo( sender.getRoleAddress() );
1522         }
1523 
1524         if (!canBroadcastPresence(sender.getRole())) {
1525             // Just send the presence to the sender of the presence
1526             final Presence selfPresence = createSelfPresenceCopy(stanza, isJoinPresence);
1527             sender.send(selfPresence);
1528             return CompletableFuture.completedFuture(null);
1529         }
1530 
1531         // If FMUC is active, propagate the presence through FMUC first. Note that when a master-slave mode is active,
1532         // we need to wait for an echo back, before the message can be broadcasted locally. The 'propagate' method will
1533         // return a CompletableFuture object that is completed as soon as processing can continue.
1534         return fmucHandler.propagate(stanza, sender)
1535             // DO NOT use 'thenRunAsync', as that will cause issues with clustering (it uses an executor that overrides the contextClassLoader, causing ClassNotFound exceptions in ClusterExternalizableUtil.
1536             .thenRun(() -> broadcast(stanza, isJoinPresence));
1537     }
1538 
1539     /**
1540      * Broadcasts the presence stanza as captured by the argument to all occupants that are local to the local domain
1541      * (in other words, it excludes occupants that are connected via FMUC).
1542      *
1543      * @param presence The presence stanza
1544      * @param isJoinPresence If the presence is sent in the context of joining the room.
1545      */
broadcast(final @Nonnull Presence presence, final boolean isJoinPresence)1546     public void broadcast(final @Nonnull Presence presence, final boolean isJoinPresence)
1547     {
1548         Log.debug("Broadcasting presence update in room {} for occupant {}", this.getName(), presence.getFrom() );
1549 
1550         if (!presence.getFrom().asBareJID().equals(this.getJID())) {
1551             // At this point, the 'from' address of the to-be broadcasted stanza can be expected to be the role-address
1552             // of the subject, or more broadly: it's bare JID representation should match that of the room. If that's not
1553             // the case then there's a bug in Openfire. Catch this here, as otherwise, privacy-sensitive data is leaked.
1554             // See: OF-2152
1555             throw new IllegalArgumentException("Broadcasted presence stanza's 'from' JID " + presence.getFrom() + " does not match room JID: " + this.getJID());
1556         }
1557 
1558         // Three distinct flavors of the presence stanzas can be sent:
1559         // 1. The original stanza (that includes the real JID of the user), usable when the room is not semi-anon or when the occupant is a moderator.
1560         // 2. One that does not include the real JID of the user (if the room is semi-anon and the occupant isn't a moderator)
1561         // 3. One that is reflected to the joining user (this stanza has additional status codes, signalling 'self-presence')
1562         final Presence nonAnonPresence = presence.createCopy(); // create a copy, as the 'to' will be overwritten when dispatched!
1563         final Presence anonPresence = createAnonCopy(presence);
1564         final Presence selfPresence = createSelfPresenceCopy(presence, isJoinPresence);
1565 
1566         for (final MUCRole occupant : getOccupants())
1567         {
1568             try
1569             {
1570                 Log.trace("Broadcasting presence update in room {} for occupant {} to occupant {}", this.getName(), presence.getFrom(), occupant );
1571 
1572                 // Do not send broadcast presence to occupants hosted in other FMUC nodes.
1573                 if (occupant.isRemoteFmuc()) {
1574                     Log.trace( "Not sending presence update of '{}' to {}: This occupant is on another FMUC node.", presence.getFrom(), occupant.getUserAddress() );
1575                     continue;
1576                 }
1577 
1578                 // Determine what stanza flavor to send to this occupant.
1579                 final Presence toSend;
1580                 if (occupant.getPresence().getFrom().equals(presence.getTo())) {
1581                     // This occupant is the subject of the stanza. Send the 'self-presence' stanza.
1582                     Log.trace( "Sending self-presence of '{}' to {}", presence.getFrom(), occupant.getUserAddress() );
1583                     toSend = selfPresence;
1584                 } else if ( !canAnyoneDiscoverJID && MUCRole.Role.moderator != occupant.getRole() ) {
1585                     Log.trace( "Sending anonymized presence of '{}' to {}: The room is semi-anon, and this occupant is not a moderator.", presence.getFrom(), occupant.getUserAddress() );
1586                     toSend = anonPresence;
1587                 } else {
1588                     Log.trace( "Sending presence of '{}' to {}", presence.getFrom(), occupant.getUserAddress() );
1589                     toSend = nonAnonPresence;
1590                 }
1591 
1592                 // Send stanza to this occupant.
1593                 occupant.send(toSend);
1594             }
1595             catch ( Exception e )
1596             {
1597                 Log.warn( "An unexpected exception prevented a presence update from {} to be broadcasted to {}.", presence.getFrom(), occupant.getUserAddress(), e );
1598             }
1599         }
1600     }
1601 
1602     /**
1603      * Creates a copy of the presence stanza encapsulated in the argument, that is suitable to be sent to entities that
1604      * have no permission to view the real JID of the sender.
1605      *
1606      * @param presence The object encapsulating the presence to be broadcast.
1607      * @return A copy of the stanza to be broadcast.
1608      */
1609     @Nonnull
createAnonCopy(@onnull Presence presence)1610     private Presence createAnonCopy(@Nonnull Presence presence)
1611     {
1612         final Presence result = presence.createCopy();
1613         final Element frag = result.getChildElement("x", "http://jabber.org/protocol/muc#user");
1614         frag.element("item").addAttribute("jid", null);
1615         return result;
1616     }
1617 
1618     /**
1619      * Creates a copy of the presence stanza encapsulated in the argument, that is suitable to be sent back to the
1620      * entity that is the subject of the presence update (a 'self-presence'). The returned stanza contains several
1621      * flags that indicate to the receiving client that the information relates to their user.
1622      *
1623      * @param presence The object encapsulating the presence to be broadcast.
1624      * @param isJoinPresence If the presence is sent in the context of joining the room.
1625      * @return A copy of the stanza to be broadcast.
1626      */
1627     @Nonnull
createSelfPresenceCopy(@onnull final Presence presence, final boolean isJoinPresence)1628     private Presence createSelfPresenceCopy(@Nonnull final Presence presence, final boolean isJoinPresence)
1629     {
1630         final Presence result = presence.createCopy();
1631         Element fragSelfPresence = result.getChildElement("x", "http://jabber.org/protocol/muc#user");
1632         fragSelfPresence.addElement("status").addAttribute("code", "110");
1633 
1634         // Only in the context of entering the room status code 100, 201 and 210 should be sent.
1635         // http://xmpp.org/registrar/mucstatus.html
1636         if ( isJoinPresence )
1637         {
1638             boolean isRoomNew = isLocked() && creationDate.getTime() == lockedTime;
1639             if ( canAnyoneDiscoverJID() )
1640             {
1641                 // XEP-0045: Example 26.
1642                 // If the user is entering a room that is non-anonymous (i.e., which informs all occupants of each occupant's full JID as shown above),
1643                 // the service MUST warn the user by including a status code of "100" in the initial presence that the room sends to the new occupant
1644                 fragSelfPresence.addElement("status").addAttribute("code", "100");
1645             }
1646 
1647             if ( isLogEnabled() )
1648             {
1649                 // XEP-0045 section 7.2.12: Room Logging:
1650                 // If the user is entering a room in which the discussions are logged (...), the service (..) MUST also
1651                 // warn the user that the discussions are logged. This is done by including a status code of "170" in
1652                 // the initial presence that the room sends to the new occupant
1653                 fragSelfPresence.addElement("status").addAttribute("code", "170");
1654             }
1655 
1656             if ( isRoomNew )
1657             {
1658                 fragSelfPresence.addElement("status").addAttribute("code", "201");
1659             }
1660         }
1661 
1662         return result;
1663     }
1664 
broadcast(@onnull final Message message, @Nonnull final MUCRole sender)1665     private void broadcast(@Nonnull final Message message, @Nonnull final MUCRole sender)
1666     {
1667         if (!message.getFrom().asBareJID().equals(this.getJID())) {
1668             // At this point, the 'from' address of the to-be broadcasted stanza can be expected to be the role-address
1669             // of the subject, or more broadly: it's bare JID representation should match that of the room. If that's not
1670             // the case then there's a bug in Openfire. Catch this here, as otherwise, privacy-sensitive data is leaked.
1671             // See: OF-2152
1672             throw new IllegalArgumentException("Broadcasted message stanza's 'from' JID " + message.getFrom() + " does not match room JID: " + this.getJID());
1673         }
1674 
1675         // If FMUC is active, propagate the message through FMUC first. Note that when a master-slave mode is active,
1676         // we need to wait for an echo back, before the message can be broadcasted locally. The 'propagate' method will
1677         // return a CompletableFuture object that is completed as soon as processing can continue.
1678         fmucHandler.propagate( message, sender )
1679             // DO NOT use 'thenRunAsync', as that will cause issues with clustering (it uses an executor that overrides the contextClassLoader, causing ClassNotFound exceptions in ClusterExternalizableUtil.
1680             .thenRun( () -> {
1681                     // Broadcast message to occupants connected to this domain.
1682                     broadcast(message);
1683                 }
1684             );
1685     }
1686 
1687     /**
1688      * Broadcasts the message stanza as captured by the argument to all occupants that are local to the domain (in other
1689      * words, it excludes occupants that connected via FMUC).
1690      *
1691      * This method also ensures that the broadcasted message is logged to persistent storage, if that feature is enabled
1692      * for this room
1693      *
1694      * @param message The message stanza
1695      */
broadcast(@onnull final Message message)1696     public void broadcast(@Nonnull final Message message)
1697     {
1698         Log.debug("Broadcasting message in room {} for occupant {}", this.getName(), message.getFrom() );
1699 
1700         if (!message.getFrom().asBareJID().equals(this.getJID())) {
1701             // At this point, the 'from' address of the to-be broadcasted stanza can be expected to be the role-address
1702             // of the sender, or more broadly: it's bare JID representation should match that of the room. If that's not
1703             // the case then there's a bug in Openfire. Catch this here, as otherwise, privacy-sensitive data is leaked.
1704             // See: OF-2152
1705             throw new IllegalArgumentException("Broadcasted message stanza's 'from' JID " + message.getFrom() + " does not match room JID: " + this.getJID());
1706         }
1707 
1708         // Add message to the room history
1709         roomHistory.addMessage(message);
1710         // Send message to occupants connected to this JVM
1711 
1712         // Create a defensive copy of the message that will be broadcast, as the broadcast will modify it ('to' addresses
1713         // will be changed), and it's undesirable to see these modifications in post-processing (OF-2163).
1714         final Message mutatingCopy = message.createCopy();
1715         final Collection<MUCRole> occupants = getOccupants();
1716         for (final MUCRole occupant : occupants) {
1717             try
1718             {
1719                 // Do not send broadcast messages to deaf occupants or occupants hosted in other FMUC nodes.
1720                 if ( !occupant.isVoiceOnly() && !occupant.isRemoteFmuc() )
1721                 {
1722                     occupant.send( mutatingCopy );
1723                 }
1724             }
1725             catch ( Exception e )
1726             {
1727                 Log.warn( "An unexpected exception prevented a message from {} to be broadcast to {}.", message.getFrom(), occupant.getUserAddress(), e );
1728             }
1729         }
1730         if (isLogEnabled()) {
1731             JID senderAddress = getRole().getRoleAddress(); // default to the room being the sender of the message.
1732 
1733             // convert the MUC nickname/role JID back into a real user JID
1734             if (message.getFrom() != null && message.getFrom().getResource() != null) {
1735                 try {
1736                     // get the first MUCRole for the sender
1737                     senderAddress = getOccupantsByNickname(message.getFrom().getResource()).get(0).getUserAddress();
1738                 } catch (UserNotFoundException e) {
1739                     // The room itself is sending the message
1740                     senderAddress = getRole().getRoleAddress();
1741                 }
1742             }
1743             // Log the conversation
1744             mucService.logConversation(this, message, senderAddress);
1745         }
1746         mucService.messageBroadcastedTo(occupants.size());
1747     }
1748 
1749     /**
1750      * Based on XEP-0045, section 7.2.13:
1751      * If the room is non-anonymous, the service MAY include an
1752      * Extended Stanza Addressing (XEP-0033) [16] element that notes the original
1753      * full JID of the sender by means of the "ofrom" address type
1754      */
addRealJidToMessage(Message message, MUCRole role)1755     public void addRealJidToMessage(Message message, MUCRole role) {
1756         Element addresses = DocumentHelper.createElement(QName.get("addresses", "http://jabber.org/protocol/address"));
1757         Element address = addresses.addElement("address");
1758         address.addAttribute("type", "ofrom");
1759         address.addAttribute("jid", role.getUserAddress().toBareJID());
1760         message.addExtension(new PacketExtension(addresses));
1761     }
1762 
1763     /**
1764      * Returns the total length of the chat session.
1765      *
1766      * @return length of chat session in milliseconds.
1767      */
getChatLength()1768     public long getChatLength() {
1769         return endTime - startTime;
1770     }
1771 
1772     /**
1773      * Updates all the presences of the given user with the new affiliation and role information. Do
1774      * nothing if the given jid is not present in the room. If the user has joined the room from
1775      * several client resources, all his/her occupants' presences will be updated.
1776      *
1777      * @param jid the bare jid of the user to update his/her role.
1778      * @param newAffiliation the new affiliation for the JID.
1779      * @param newRole the new role for the JID.
1780      * @return the list of updated presences of all the client resources that the client used to
1781      *         join the room.
1782      * @throws NotAllowedException If trying to change the moderator role to an owner or an admin or
1783      *         if trying to ban an owner or an administrator.
1784      */
changeOccupantAffiliation(MUCRole senderRole, JID jid, MUCRole.Affiliation newAffiliation, MUCRole.Role newRole)1785     private List<Presence> changeOccupantAffiliation(MUCRole senderRole, JID jid, MUCRole.Affiliation newAffiliation, MUCRole.Role newRole)
1786         throws NotAllowedException {
1787         List<Presence> presences = new ArrayList<>();
1788         // Get all the roles (i.e. occupants) of this user based on his/her bare JID
1789         JID bareJID = jid.asBareJID();
1790         List<MUCRole> roles = null;
1791         try {
1792             roles = getOccupantsByBareJID(bareJID);
1793         } catch (UserNotFoundException e) {
1794             return presences;
1795         }
1796         // Collect all the updated presences of these roles
1797         for (MUCRole role : roles) {
1798 // TODO
1799 //            if (!isPrivilegedToChangeAffiliationAndRole(senderRole.getAffiliation(), senderRole.getRole(), role.getAffiliation(), role.getRole(), newAffiliation, newRole)) {
1800 //                throw new NotAllowedException();
1801 //            }
1802             // Update the presence with the new affiliation and role
1803             role.setAffiliation(newAffiliation);
1804             role.setRole(newRole);
1805 
1806             // Prepare a new presence to be sent to all the room occupants
1807             presences.add(role.getPresence().createCopy());
1808         }
1809         // Answer all the updated presences
1810         return presences;
1811     }
1812 
1813     /**
1814      * Updates the presence of the given user with the new role information. Do nothing if the given
1815      * jid is not present in the room.
1816      *
1817      * @param jid the full jid of the user to update his/her role.
1818      * @param newRole the new role for the JID.
1819      * @return the updated presence of the user or null if none.
1820      * @throws NotAllowedException If trying to change the moderator role to an owner or an admin.
1821      */
changeOccupantRole(JID jid, MUCRole.Role newRole)1822     private Presence changeOccupantRole(JID jid, MUCRole.Role newRole) throws NotAllowedException {
1823         // Try looking the role in the bare JID list
1824         MUCRole role = getOccupantByFullJID(jid);
1825 // TODO
1826 //            if (!isPrivilegedToChangeAffiliationAndRole(senderRole.getAffiliation(), senderRole.getRole(), role.getAffiliation(), role.getRole(), newAffiliation, newRole)) {
1827 //                throw new NotAllowedException();
1828 //            }
1829         if (role != null) {
1830             // Update the presence with the new role
1831             role.setRole(newRole);
1832             // Prepare a new presence to be sent to all the room occupants
1833             return role.getPresence().createCopy();
1834         }
1835         return null;
1836     }
1837 
isPrivilegedToChangeAffiliationAndRole(MUCRole.Affiliation actorAffiliation, MUCRole.Role actorRole, MUCRole.Affiliation occupantAffiliation, MUCRole.Role occupantRole, MUCRole.Affiliation newAffiliation, MUCRole.Role newRole)1838     public static boolean isPrivilegedToChangeAffiliationAndRole(MUCRole.Affiliation actorAffiliation, MUCRole.Role actorRole, MUCRole.Affiliation occupantAffiliation, MUCRole.Role occupantRole, MUCRole.Affiliation newAffiliation, MUCRole.Role newRole) {
1839         switch (actorAffiliation) {
1840             case owner:
1841                 // An owner has all privileges
1842                 return true;
1843             case admin:
1844                 // If affiliation has not changed
1845                 if (occupantAffiliation == newAffiliation) {
1846                     // Only check, if the admin wants to modify an owner (e.g. revoke an owner's moderator role).
1847                     return occupantAffiliation != MUCRole.Affiliation.owner;
1848                 } else {
1849                     // An admin is not allowed to modify the admin or owner list.
1850                     return occupantAffiliation != MUCRole.Affiliation.owner && newAffiliation != MUCRole.Affiliation.admin && newAffiliation != MUCRole.Affiliation.owner;
1851                 }
1852             default:
1853                 // Every other affiliation (member, none, outcast) is not allowed to change anything, except he's a moderator and he doesn't want to change affiliations.
1854                 if (actorRole == MUCRole.Role.moderator && occupantAffiliation == newAffiliation) {
1855                     // A moderator SHOULD NOT be allowed to revoke moderation privileges from someone with a higher affiliation than themselves
1856                     // (i.e., an unaffiliated moderator SHOULD NOT be allowed to revoke moderation privileges from an admin or an owner, and an admin SHOULD NOT be allowed to revoke moderation privileges from an owner).
1857                     if (occupantRole == MUCRole.Role.moderator && newRole != MUCRole.Role.moderator) {
1858                         return occupantAffiliation != MUCRole.Affiliation.owner && occupantAffiliation != MUCRole.Affiliation.admin;
1859                     }
1860                 }
1861                 return false;
1862         }
1863     }
1864 
1865     /**
1866      * Adds a new user to the list of owners. The user is the actual creator of the room. Only the
1867      * MultiUserChatServer should use this method. Regular owners list maintenance MUST be done
1868      * through {@link #addOwner(JID jid,MUCRole)}.
1869      *
1870      * @param bareJID The bare JID of the user to add as owner (cannot be {@code null}).
1871      */
addFirstOwner(JID bareJID)1872     public void addFirstOwner(JID bareJID) {
1873         owners.add( bareJID.asBareJID() );
1874     }
1875 
1876     /**
1877      * Adds a new user to the list of owners.
1878      *
1879      * @param jid The JID of the user to add as owner (cannot be {@code null}).
1880      * @param sendRole the role of the user that is trying to modify the owners list (cannot be {@code null}).
1881      * @return the list of updated presences of all the client resources that the client used to
1882      *         join the room.
1883      * @throws ForbiddenException If the user is not allowed to modify the owner list.
1884      */
addOwner(JID jid, MUCRole sendRole)1885     public List<Presence> addOwner(JID jid, MUCRole sendRole) throws ForbiddenException {
1886 
1887         final JID bareJID = jid.asBareJID();
1888 
1889         synchronized (this) {
1890             MUCRole.Affiliation oldAffiliation = MUCRole.Affiliation.none;
1891             if (MUCRole.Affiliation.owner != sendRole.getAffiliation()) {
1892                 throw new ForbiddenException();
1893             }
1894             // Check if user is already an owner (explicitly)
1895             if (owners.contains(bareJID)) {
1896                 // Do nothing
1897                 return Collections.emptyList();
1898             }
1899             owners.add(bareJID);
1900             // Remove the user from other affiliation lists
1901             if (removeAdmin(bareJID)) {
1902                 oldAffiliation = MUCRole.Affiliation.admin;
1903             }
1904             else if (removeMember(bareJID)) {
1905                 oldAffiliation = MUCRole.Affiliation.member;
1906             }
1907             else if (removeOutcast(bareJID)) {
1908                 oldAffiliation = MUCRole.Affiliation.outcast;
1909             }
1910             // Update the DB if the room is persistent
1911             MUCPersistenceManager.saveAffiliationToDB(
1912                 this,
1913                 bareJID,
1914                 null,
1915                 MUCRole.Affiliation.owner,
1916                 oldAffiliation);
1917         }
1918         // apply the affiliation change, assigning a new affiliation
1919         // based on the group(s) of the affected user(s)
1920         return applyAffiliationChange(getRole(), bareJID, null);
1921     }
1922 
removeOwner(JID jid)1923     private boolean removeOwner(JID jid) {
1924         return owners.remove(jid.asBareJID());
1925     }
1926 
1927     /**
1928      * Adds a new user to the list of admins.
1929      *
1930      * @param jid The JID of the user to add as admin (cannot be {@code null}).
1931      * @param sendRole The role of the user that is trying to modify the admins list (cannot be {@code null}).
1932      * @return the list of updated presences of all the client resources that the client used to
1933      *         join the room.
1934      * @throws ForbiddenException If the user is not allowed to modify the admin list.
1935      * @throws ConflictException If the room was going to lose all its owners.
1936      */
addAdmin(JID jid, MUCRole sendRole)1937     public List<Presence> addAdmin(JID jid, MUCRole sendRole) throws ForbiddenException,
1938         ConflictException {
1939         final JID bareJID = jid.asBareJID();
1940         synchronized (this) {
1941             MUCRole.Affiliation oldAffiliation = MUCRole.Affiliation.none;
1942             if (MUCRole.Affiliation.owner != sendRole.getAffiliation()) {
1943                 throw new ForbiddenException();
1944             }
1945             // Check that the room always has an owner
1946             if (owners.contains(bareJID) && owners.size() == 1) {
1947                 throw new ConflictException();
1948             }
1949             // Check if user is already an admin
1950             if (admins.contains(bareJID)) {
1951                 // Do nothing
1952                 return Collections.emptyList();
1953             }
1954             admins.add(bareJID);
1955             // Remove the user from other affiliation lists
1956             if (removeOwner(bareJID)) {
1957                 oldAffiliation = MUCRole.Affiliation.owner;
1958             }
1959             else if (removeMember(bareJID)) {
1960                 oldAffiliation = MUCRole.Affiliation.member;
1961             }
1962             else if (removeOutcast(bareJID)) {
1963                 oldAffiliation = MUCRole.Affiliation.outcast;
1964             }
1965             // Update the DB if the room is persistent
1966             MUCPersistenceManager.saveAffiliationToDB(
1967                 this,
1968                 bareJID,
1969                 null,
1970                 MUCRole.Affiliation.admin,
1971                 oldAffiliation);
1972         }
1973         // apply the affiliation change, assigning a new affiliation
1974         // based on the group(s) of the affected user(s)
1975         return applyAffiliationChange(getRole(), bareJID, null);
1976     }
1977 
removeAdmin(JID bareJID)1978     private boolean removeAdmin(JID bareJID) {
1979         return admins.remove( bareJID.asBareJID() );
1980     }
1981 
1982     /**
1983      * Adds a new user to the list of members.
1984      *
1985      * @param jid The JID of the user to add as a member (cannot be {@code null}).
1986      * @param nickname The reserved nickname of the member for the room or null if none.
1987      * @param sendRole the role of the user that is trying to modify the members list (cannot be {@code null}).
1988      * @return the list of updated presences of all the client resources that the client used to
1989      *         join the room.
1990      * @throws ForbiddenException If the user is not allowed to modify the members list.
1991      * @throws ConflictException If the desired room nickname is already reserved for the room or if
1992      *             the room was going to lose all its owners.
1993      */
addMember(JID jid, String nickname, MUCRole sendRole)1994     public List<Presence> addMember(JID jid, String nickname, MUCRole sendRole)
1995         throws ForbiddenException, ConflictException {
1996         final JID bareJID = jid.asBareJID();
1997         synchronized (this) {
1998             MUCRole.Affiliation oldAffiliation = (members.containsKey(bareJID) ?
1999                 MUCRole.Affiliation.member : MUCRole.Affiliation.none);
2000             if (isMembersOnly()) {
2001                 if (!canOccupantsInvite()) {
2002                     if (MUCRole.Affiliation.admin != sendRole.getAffiliation()
2003                         && MUCRole.Affiliation.owner != sendRole.getAffiliation()) {
2004                         throw new ForbiddenException();
2005                     }
2006                 }
2007             }
2008             else {
2009                 if (MUCRole.Affiliation.admin != sendRole.getAffiliation()
2010                     && MUCRole.Affiliation.owner != sendRole.getAffiliation()) {
2011                     throw new ForbiddenException();
2012                 }
2013             }
2014             // Check if the desired nickname is already reserved for another member
2015             if (nickname != null && nickname.trim().length() > 0 && members.containsValue(nickname.toLowerCase())) {
2016                 if (!nickname.equals(members.get(bareJID))) {
2017                     throw new ConflictException();
2018                 }
2019             } else if (isLoginRestrictedToNickname() && (nickname == null || nickname.trim().length() == 0)) {
2020                 throw new ConflictException();
2021             }
2022             // Check that the room always has an owner
2023             if (owners.contains(bareJID) && owners.size() == 1) {
2024                 throw new ConflictException();
2025             }
2026             // Check if user is already an member
2027             if (members.containsKey(bareJID)) {
2028                 // Do nothing
2029                 return Collections.emptyList();
2030             }
2031             // Associate the reserved nickname with the bareJID. If nickname is null then associate an
2032             // empty string
2033             members.put(bareJID, (nickname == null ? "" : nickname.toLowerCase()));
2034             // Remove the user from other affiliation lists
2035             if (removeOwner(bareJID)) {
2036                 oldAffiliation = MUCRole.Affiliation.owner;
2037             }
2038             else if (removeAdmin(bareJID)) {
2039                 oldAffiliation = MUCRole.Affiliation.admin;
2040             }
2041             else if (removeOutcast(bareJID)) {
2042                 oldAffiliation = MUCRole.Affiliation.outcast;
2043             }
2044             // Update the DB if the room is persistent
2045             MUCPersistenceManager.saveAffiliationToDB(
2046                 this,
2047                 bareJID,
2048                 nickname,
2049                 MUCRole.Affiliation.member,
2050                 oldAffiliation);
2051         }
2052 
2053         // apply the affiliation change, assigning a new affiliation
2054         // based on the group(s) of the affected user(s)
2055         return applyAffiliationChange(getRole(), bareJID, null);
2056     }
2057 
removeMember(JID jid)2058     private boolean removeMember(JID jid) {
2059         return members.remove(jid.asBareJID()) != null;
2060     }
2061 
2062     /**
2063      * Adds a new user to the list of outcast users.
2064      *
2065      * @param jid The JID of the user to add as an outcast (cannot be {@code null}).
2066      * @param reason an optional reason why the user was banned (can be {@code null}).
2067      * @param senderRole The role of the user that initiated the ban (cannot be {@code null}).
2068      * @return the list of updated presences of all the client resources that the client used to
2069      *         join the room.
2070      * @throws NotAllowedException Thrown if trying to ban an owner or an administrator.
2071      * @throws ForbiddenException If the user is not allowed to modify the outcast list.
2072      * @throws ConflictException If the room was going to lose all its owners.
2073      */
addOutcast(JID jid, String reason, MUCRole senderRole)2074     public List<Presence> addOutcast(JID jid, String reason, MUCRole senderRole)
2075         throws NotAllowedException, ForbiddenException, ConflictException {
2076         final JID bareJID = jid.asBareJID();
2077 
2078         synchronized (this) {
2079             MUCRole.Affiliation oldAffiliation = MUCRole.Affiliation.none;
2080             if (MUCRole.Affiliation.admin != senderRole.getAffiliation()
2081                 && MUCRole.Affiliation.owner != senderRole.getAffiliation()) {
2082                 throw new ForbiddenException();
2083             }
2084             // Check that the room always has an owner
2085             if (owners.contains(bareJID) && owners.size() == 1) {
2086                 throw new ConflictException();
2087             }
2088             // Check if user is already an outcast
2089             if (outcasts.contains(bareJID)) {
2090                 // Do nothing
2091                 return Collections.emptyList();
2092             }
2093 
2094             // Update the affiliation lists
2095             outcasts.add(bareJID);
2096             // Remove the user from other affiliation lists
2097             if (removeOwner(bareJID)) {
2098                 oldAffiliation = MUCRole.Affiliation.owner;
2099             }
2100             else if (removeAdmin(bareJID)) {
2101                 oldAffiliation = MUCRole.Affiliation.admin;
2102             }
2103             else if (removeMember(bareJID)) {
2104                 oldAffiliation = MUCRole.Affiliation.member;
2105             }
2106             // Update the DB if the room is persistent
2107             MUCPersistenceManager.saveAffiliationToDB(
2108                 this,
2109                 bareJID,
2110                 null,
2111                 MUCRole.Affiliation.outcast,
2112                 oldAffiliation);
2113         }
2114         // apply the affiliation change, assigning a new affiliation
2115         // based on the group(s) of the affected user(s)
2116         return applyAffiliationChange(senderRole, bareJID, reason);
2117     }
2118 
removeOutcast(JID bareJID)2119     private boolean removeOutcast(JID bareJID) {
2120         return outcasts.remove( bareJID.asBareJID() );
2121     }
2122 
2123     /**
2124      * Removes the user from all the other affiliation list thus giving the user a NONE affiliation.
2125      *
2126      * @param jid The JID of the user to keep with a NONE affiliation (cannot be {@code null}).
2127      * @param senderRole The role of the user that set the affiliation to none (cannot be {@code null}).
2128      * @return the list of updated presences of all the client resources that the client used to
2129      *         join the room or null if none was updated.
2130      * @throws ForbiddenException If the user is not allowed to modify the none list.
2131      * @throws ConflictException If the room was going to lose all its owners.
2132      */
addNone(JID jid, MUCRole senderRole)2133     public List<Presence> addNone(JID jid, MUCRole senderRole) throws ForbiddenException, ConflictException {
2134 
2135         final JID bareJID = jid.asBareJID();
2136         MUCRole.Affiliation oldAffiliation = MUCRole.Affiliation.none;
2137         boolean jidWasAffiliated = false;
2138         synchronized (this) {
2139             if (MUCRole.Affiliation.admin != senderRole.getAffiliation()
2140                 && MUCRole.Affiliation.owner != senderRole.getAffiliation()) {
2141                 throw new ForbiddenException();
2142             }
2143             // Check that the room always has an owner
2144             if (owners.contains(bareJID) && owners.size() == 1) {
2145                 throw new ConflictException();
2146             }
2147             // Remove the jid from ALL the affiliation lists
2148             if (removeOwner(bareJID)) {
2149                 oldAffiliation = MUCRole.Affiliation.owner;
2150                 jidWasAffiliated = true;
2151             }
2152             else if (removeAdmin(bareJID)) {
2153                 oldAffiliation = MUCRole.Affiliation.admin;
2154                 jidWasAffiliated = true;
2155             }
2156             else if (removeMember(bareJID)) {
2157                 oldAffiliation = MUCRole.Affiliation.member;
2158                 jidWasAffiliated = true;
2159             }
2160             else if (removeOutcast(bareJID)) {
2161                 oldAffiliation = MUCRole.Affiliation.outcast;
2162             }
2163             // Remove the affiliation of this user from the DB if the room is persistent
2164             MUCPersistenceManager.removeAffiliationFromDB(this, bareJID, oldAffiliation);
2165         }
2166         if (jidWasAffiliated) {
2167             // apply the affiliation change, assigning a new affiliation
2168             // based on the group(s) of the affected user(s)
2169             return applyAffiliationChange(senderRole, bareJID, null);
2170         } else {
2171             // no presence updates needed
2172             return Collections.emptyList();
2173         }
2174     }
2175 
2176     /**
2177      * Evaluate the given JID to determine what the appropriate affiliation should be
2178      * after a change has been made. Each affected user will be granted the highest
2179      * affiliation they now possess, either explicitly or implicitly via membership
2180      * in one or more groups. If the JID is a user, the effective affiliation is
2181      * applied to each presence corresponding to that user. If the given JID is a group,
2182      * each user in the group is evaluated to determine what their new affiliations will
2183      * be. The returned presence updates will be broadcast to the occupants of the room.
2184      *
2185      * @param senderRole Typically the room itself, or an owner/admin
2186      * @param affiliationJID The JID for the user or group that has been changed
2187      * @param reason An optional reason to explain why a user was kicked from the room
2188      * @return List of presence updates to be delivered to the room's occupants
2189      */
applyAffiliationChange(MUCRole senderRole, final JID affiliationJID, String reason)2190     private List<Presence> applyAffiliationChange(MUCRole senderRole, final JID affiliationJID, String reason) {
2191 
2192         // Update the presence(s) for the new affiliation and inform all occupants
2193         List<JID> affectedOccupants = new ArrayList<>();
2194 
2195         // first, determine which actual (user) JIDs are affected by the affiliation change
2196         if (GroupJID.isGroup(affiliationJID)) {
2197             try {
2198                 Group group = GroupManager.getInstance().getGroup(affiliationJID);
2199                 // check each occupant to see if they are in the group that was changed
2200                 // if so, calculate a new affiliation (if any) for the occupant(s)
2201                 for (JID groupMember : group.getAll()) {
2202                     if (hasOccupant(groupMember)) {
2203                         affectedOccupants.add(groupMember);
2204                     }
2205                 }
2206             } catch (GroupNotFoundException gnfe) {
2207                 Log.error("Error updating group presences for " + affiliationJID , gnfe);
2208             }
2209         } else {
2210             if (hasOccupant(affiliationJID)) {
2211                 affectedOccupants.add(affiliationJID);
2212             }
2213         }
2214 
2215         // now update each of the affected occupants with a new role/affiliation
2216         MUCRole.Role newRole;
2217         MUCRole.Affiliation newAffiliation;
2218         List<Presence> updatedPresences = new ArrayList<>();
2219         // new role/affiliation may be granted via group membership
2220         for (JID occupantJID : affectedOccupants) {
2221             Log.info("Applying affiliation change for " + occupantJID);
2222             boolean kickMember = false, isOutcast = false;
2223             if (owners.includes(occupantJID)) {
2224                 newRole = MUCRole.Role.moderator;
2225                 newAffiliation = MUCRole.Affiliation.owner;
2226             }
2227             else if (admins.includes(occupantJID)) {
2228                 newRole = MUCRole.Role.moderator;
2229                 newAffiliation = MUCRole.Affiliation.admin;
2230             }
2231             // outcast trumps member when an affiliation is changed
2232             else if (outcasts.includes(occupantJID)) {
2233                 newAffiliation = MUCRole.Affiliation.outcast;
2234                 newRole = MUCRole.Role.none;
2235                 kickMember = true;
2236                 isOutcast = true;
2237             }
2238             else if (members.includesKey(occupantJID)) {
2239                 newRole = MUCRole.Role.participant;
2240                 newAffiliation = MUCRole.Affiliation.member;
2241             }
2242             else if (isMembersOnly()) {
2243                 newRole = MUCRole.Role.none;
2244                 newAffiliation = MUCRole.Affiliation.none;
2245                 kickMember = true;
2246             }
2247             else {
2248                 newRole = isModerated() ? MUCRole.Role.visitor : MUCRole.Role.participant;
2249                 newAffiliation = MUCRole.Affiliation.none;
2250             }
2251             Log.info("New affiliation: " + newAffiliation);
2252             try {
2253                 List<Presence> thisOccupant = changeOccupantAffiliation(senderRole, occupantJID, newAffiliation, newRole);
2254                 if (kickMember) {
2255                     // If the room is members-only, remove the user from the room including a status
2256                     // code of 321 to indicate that the user was removed because of an affiliation change
2257                     // a status code of 301 indicates the user was removed as an outcast
2258                     for (Presence presence : thisOccupant) {
2259                         presence.setType(Presence.Type.unavailable);
2260                         presence.setStatus(null);
2261                         Element x = presence.getChildElement("x", "http://jabber.org/protocol/muc#user");
2262                         if (reason != null && reason.trim().length() > 0) {
2263                             x.element("item").addElement("reason").setText(reason);
2264                         }
2265                         x.addElement("status").addAttribute("code", isOutcast ? "301" : "321");
2266 
2267                         // This removes the kicked occupant from the room. The presenceUpdates returned by this method
2268                         // (that will be broadcast to all occupants by the caller) won't reach it.
2269                         kickPresence(presence, senderRole.getUserAddress(), senderRole.getNickname());
2270                     }
2271                 }
2272                 updatedPresences.addAll(thisOccupant);
2273             } catch (NotAllowedException e) {
2274                 Log.error("Error updating presences for " + occupantJID, e);
2275             }
2276         }
2277         return updatedPresences;
2278     }
2279 
2280     /**
2281      * Returns true if the room is locked. The lock will persist for a defined period of time. If
2282      * the room owner does not configure the room within the timeout period, the room owner is
2283      * assumed to have accepted the default configuration.
2284      *
2285      * @return true if the room is locked.
2286      */
isLocked()2287     public boolean isLocked() {
2288         return lockedTime > 0;
2289     }
2290 
2291     /**
2292      * Returns true if the room is locked and it was locked by a room owner after the room was
2293      * initially configured.
2294      *
2295      * @return true if the room is locked and it was locked by a room owner after the room was
2296      *         initially configured.
2297      */
isManuallyLocked()2298     public boolean isManuallyLocked() {
2299         return lockedTime > 0 && creationDate.getTime() != lockedTime;
2300     }
2301 
2302     /**
2303      * An event callback fired whenever an occupant updated his presence in the chatroom.
2304      *
2305      * Handles occupants updating their presence in the chatroom. Assumes the user updates their presence whenever their
2306      * availability in the room changes. This method should not be called to handle other presence related updates, such
2307      * as nickname changes.
2308      *
2309      * @param occupantRole occupant that changed his presence in the room (cannot be {@code null}).
2310      * @param newPresence presence sent by the occupant (cannot be {@code null}).
2311      */
presenceUpdated(final MUCRole occupantRole, final Presence newPresence)2312     public void presenceUpdated(final MUCRole occupantRole, final Presence newPresence) {
2313         final String occupantNickName = occupantRole.getNickname();
2314 
2315         try {
2316             List<MUCRole> occupants = getOccupantsByNickname(occupantNickName);
2317             for (MUCRole occupant : occupants) {
2318                 occupant.setPresence(newPresence.createCopy());
2319             }
2320         } catch (UserNotFoundException e) {
2321             Log.debug("Failed to update presence of room occupant. Occupant nickname: {}", occupantNickName, e);
2322         }
2323 
2324         // Get the new, updated presence for the occupant in the room. The presence reflects the occupant's updated
2325         // availability and their existing association.
2326         final Presence updatedPresence = occupantRole.getPresence().createCopy();
2327 
2328         // Broadcast updated presence of occupant.
2329         broadcastPresence(updatedPresence, false, occupantRole);
2330     }
2331 
2332     /**
2333      * An event callback fired whenever an occupant changes his nickname within the chatroom.
2334      *
2335      * @param occupantRole occupant that changed his nickname in the room (cannot be {@code null}).
2336      * @param newPresence presence sent by the occupant with the new nickname (cannot be {@code null}).
2337      * @param oldNick old nickname within the room (cannot be {@code null}).
2338      * @param newNick new nickname within the room (cannot be {@code null}).
2339      */
nicknameChanged(MUCRole occupantRole, Presence newPresence, String oldNick, String newNick)2340     public void nicknameChanged(MUCRole occupantRole, Presence newPresence, String oldNick, String newNick)
2341     {
2342         List<MUCRole> occupants;
2343         try {
2344             occupants = getOccupantsByNickname(oldNick);
2345         } catch (UserNotFoundException e) {
2346             Log.debug("Unable to process nickname change from old '{}' to new '{}' for occupant '{}' as no occupant with the old nickname is found.", oldNick, newNick, occupantRole, e);
2347             return;
2348         }
2349 
2350         for (MUCRole occupant : occupants) {
2351             // Update the role with the new info
2352             occupant.setPresence(newPresence);
2353             occupant.changeNickname(newNick);
2354 
2355             // Fire event that user changed his nickname
2356             MUCEventDispatcher.nicknameChanged(getRole().getRoleAddress(), occupant.getUserAddress(), oldNick, newNick);
2357         }
2358 
2359         // Broadcast new presence of occupant
2360         broadcastPresence(occupantRole.getPresence().createCopy(), false, occupantRole);
2361     }
2362 
2363     /**
2364      * Changes the room's subject if the occupant has enough permissions. The occupant must be
2365      * a moderator or the room must be configured so that anyone can change its subject. Otherwise
2366      * a forbidden exception will be thrown.<p>
2367      *
2368      * The new subject will be added to the history of the room.
2369      *
2370      * @param packet the sent packet to change the room's subject (cannot be {@code null}).
2371      * @param role the role of the user that is trying to change the subject (cannot be {@code null}).
2372      * @throws ForbiddenException If the user is not allowed to change the subject.
2373      */
changeSubject(Message packet, MUCRole role)2374     public void changeSubject(Message packet, MUCRole role) throws ForbiddenException {
2375         if ((canOccupantsChangeSubject() && role.getRole().compareTo(MUCRole.Role.visitor) < 0) ||
2376             MUCRole.Role.moderator == role.getRole()) {
2377             // Set the new subject to the room
2378             subject = packet.getSubject();
2379             MUCPersistenceManager.updateRoomSubject(this);
2380             // Notify all the occupants that the subject has changed
2381             packet.setFrom(role.getRoleAddress());
2382             send(packet, role);
2383 
2384             // Fire event signifying that the room's subject has changed.
2385             MUCEventDispatcher.roomSubjectChanged(getJID(), role.getUserAddress(), subject);
2386         }
2387         else {
2388             throw new ForbiddenException();
2389         }
2390     }
2391 
2392     /**
2393      * Returns the last subject that some occupant set to the room.
2394      *
2395      * @return the last subject that some occupant set to the room.
2396      */
getSubject()2397     public String getSubject() {
2398         return subject;
2399     }
2400 
2401     /**
2402      * Sets the last subject that some occupant set to the room. This message will only be used
2403      * when loading a room from the database.
2404      *
2405      * @param subject the last known subject of the room (cannot be {@code null}).
2406      */
setSubject(String subject)2407     public void setSubject(String subject) {
2408         this.subject = subject;
2409     }
2410 
2411     /**
2412      * Sends an invitation to a user. The invitation will be sent as if the room is inviting the
2413      * user. The invitation will include the original occupant the sent the invitation together with
2414      * the reason for the invitation if any. Since the invitee could be offline at the moment we
2415      * need the originating session so that the offline strategy could potentially bounce the
2416      * message with the invitation.
2417      *
2418      * @param to the JID of the user that is being invited.
2419      * @param reason the reason of the invitation or null if none.
2420      * @param senderRole the role of the occupant that sent the invitation.
2421      * @param extensions the list of extensions sent with the original message invitation or null
2422      *        if none.
2423      * @throws ForbiddenException If the user is not allowed to send the invitation.
2424      * @throws CannotBeInvitedException (Optionally) If the user being invited does not have access to the room
2425      */
sendInvitation(JID to, String reason, MUCRole senderRole, List<Element> extensions)2426     public void sendInvitation(JID to, String reason, MUCRole senderRole, List<Element> extensions)
2427         throws ForbiddenException, CannotBeInvitedException {
2428         if (!isMembersOnly() || canOccupantsInvite()
2429             || MUCRole.Affiliation.admin == senderRole.getAffiliation()
2430             || MUCRole.Affiliation.owner == senderRole.getAffiliation()) {
2431             // If the room is not members-only OR if the room is members-only and anyone can send
2432             // invitations or the sender is an admin or an owner, then send the invitation
2433             Message message = new Message();
2434             message.setFrom(role.getRoleAddress());
2435             message.setTo(to);
2436 
2437             if (mucService.getMUCDelegate() != null) {
2438                 switch(mucService.getMUCDelegate().sendingInvitation(this, to, senderRole.getUserAddress(), reason)) {
2439                     case HANDLED_BY_DELEGATE:
2440                         //if the delegate is taking care of it, there's nothing for us to do
2441                         return;
2442                     case HANDLED_BY_OPENFIRE:
2443                         //continue as normal if we're asked to handle it
2444                         break;
2445                     case REJECTED:
2446                         //we can't invite that person
2447                         throw new CannotBeInvitedException();
2448                 }
2449             }
2450 
2451             // Add a list of extensions sent with the original message invitation (if any)
2452             if (extensions != null) {
2453                 for(Element element : extensions) {
2454                     element.setParent(null);
2455                     message.getElement().add(element);
2456                 }
2457             }
2458             Element frag = message.addChildElement("x", "http://jabber.org/protocol/muc#user");
2459             // ChatUser will be null if the room itself (ie. via admin console) made the request
2460             if (senderRole.getUserAddress() != null) {
2461                 frag.addElement("invite").addAttribute("from", senderRole.getUserAddress().toBareJID());
2462             }
2463             if (reason != null && reason.length() > 0) {
2464                 Element invite = frag.element("invite");
2465                 if (invite == null) {
2466                     invite = frag.addElement("invite");
2467                 }
2468                 invite.addElement("reason").setText(reason);
2469             }
2470             if (isPasswordProtected()) {
2471                 frag.addElement("password").setText(getPassword());
2472             }
2473 
2474             // Include the jabber:x:conference information for backward compatibility
2475             frag = message.addChildElement("x", "jabber:x:conference");
2476             frag.addAttribute("jid", role.getRoleAddress().toBareJID());
2477 
2478             // Send the message with the invitation
2479             XMPPServer.getInstance().getPacketRouter().route(message);
2480         }
2481         else {
2482             throw new ForbiddenException();
2483         }
2484     }
2485 
2486     /**
2487      * Sends the rejection to the inviter. The rejection will be sent as if the room is rejecting
2488      * the invitation is named of the invitee. The rejection will include the address of the invitee
2489      * together with the reason for the rejection if any. Since the inviter could be offline at the
2490      * moment we need the originating session so that the offline strategy could potentially bounce
2491      * the message with the rejection.
2492      *
2493      * @param to the JID of the user that is originated the invitation.
2494      * @param reason the reason for the rejection or null if none.
2495      * @param sender the JID of the invitee that is rejecting the invitation.
2496      */
sendInvitationRejection(JID to, String reason, JID sender)2497     public void sendInvitationRejection(JID to, String reason, JID sender) {
2498         if (mucService.getMUCDelegate() != null) {
2499             switch(mucService.getMUCDelegate().sendingInvitationRejection(this, to, sender, reason)) {
2500                 case HANDLED_BY_DELEGATE:
2501                     //if the delegate is taking care of it, there's nothing for us to do
2502                     return;
2503                 case HANDLED_BY_OPENFIRE:
2504                     //continue as normal if we're asked to handle it
2505                     break;
2506             }
2507         }
2508 
2509         Message message = new Message();
2510         message.setFrom(role.getRoleAddress());
2511         message.setTo(to);
2512         Element frag = message.addChildElement("x", "http://jabber.org/protocol/muc#user");
2513         frag.addElement("decline").addAttribute("from", sender.toBareJID());
2514         if (reason != null && reason.length() > 0) {
2515             frag.element("decline").addElement("reason").setText(reason);
2516         }
2517 
2518         // Send the message with the invitation
2519         XMPPServer.getInstance().getPacketRouter().route(message);
2520     }
2521 
getIQOwnerHandler()2522     public IQOwnerHandler getIQOwnerHandler() {
2523         return iqOwnerHandler;
2524     }
2525 
getIQAdminHandler()2526     public IQAdminHandler getIQAdminHandler() {
2527         return iqAdminHandler;
2528     }
2529 
getFmucHandler()2530     public FMUCHandler getFmucHandler() {
2531         return fmucHandler;
2532     }
2533 
2534     /**
2535      * Returns the history of the room which includes chat transcripts.
2536      *
2537      * @return the history of the room which includes chat transcripts.
2538      */
getRoomHistory()2539     public MUCRoomHistory getRoomHistory() {
2540         return roomHistory;
2541     }
2542 
2543     /**
2544      * Returns a collection with the current list of owners. The collection contains the bareJID of
2545      * the users with owner affiliation.
2546      *
2547      * @return a collection with the current list of owners.
2548      */
getOwners()2549     public Collection<JID> getOwners() {
2550         return Collections.unmodifiableList(owners);
2551     }
2552 
2553     /**
2554      * Returns a collection with the current list of admins. The collection contains the bareJID of
2555      * the users with admin affiliation.
2556      *
2557      * @return a collection with the current list of admins.
2558      */
getAdmins()2559     public Collection<JID> getAdmins() {
2560         return Collections.unmodifiableList(admins);
2561     }
2562 
2563     /**
2564      * Returns a collection with the current list of room members. The collection contains the
2565      * bareJID of the users with member affiliation. If the room is not members-only then the list
2566      * will contain the users that registered with the room and therefore they may have reserved a
2567      * nickname.
2568      *
2569      * @return a collection with the current list of members.
2570      */
getMembers()2571     public Collection<JID> getMembers() {
2572         return Collections.unmodifiableMap(members).keySet();
2573     }
2574 
2575     /**
2576      * Returns a collection with the current list of outcast users. An outcast user is not allowed
2577      * to join the room again. The collection contains the bareJID of the users with outcast
2578      * affiliation.
2579      *
2580      * @return a collection with the current list of outcast users.
2581      */
getOutcasts()2582     public Collection<JID> getOutcasts() {
2583         return Collections.unmodifiableList(outcasts);
2584     }
2585 
2586     /**
2587      * Returns a collection with the current list of room moderators. The collection contains the
2588      * MUCRole of the occupants with moderator role.
2589      *
2590      * @return a collection with the current list of moderators.
2591      */
getModerators()2592     public Collection<MUCRole> getModerators() {
2593         final List<MUCRole> roles = occupants.stream()
2594             .filter(mucRole -> mucRole.getRole() == MUCRole.Role.moderator)
2595             .collect(Collectors.toList());
2596         return Collections.unmodifiableList(roles);
2597     }
2598 
2599     /**
2600      * Returns a collection with the current list of room participants. The collection contains the
2601      * MUCRole of the occupants with participant role.
2602      *
2603      * @return a collection with the current list of moderators.
2604      */
getParticipants()2605     public Collection<MUCRole> getParticipants() {
2606         if (occupants == null) {
2607             return Collections.emptyList();
2608         }
2609         final List<MUCRole> roles = occupants.stream().filter(mucRole -> mucRole.getRole() == MUCRole.Role.participant).collect(Collectors.toList());
2610         return Collections.unmodifiableList(roles);
2611     }
2612 
2613     /**
2614      * Changes the role of the user within the room to moderator. A moderator is allowed to kick
2615      * occupants as well as granting/revoking voice from occupants.
2616      *
2617      * @param jid The full JID of the occupant to give moderator privileges (cannot be {@code null}).
2618      * @param senderRole The role of the user that is granting moderator privileges to an occupant (cannot be {@code null}).
2619      * @return the updated presence of the occupant or {@code null} if the JID does not belong to
2620      *         an existing occupant.
2621      * @throws ForbiddenException If the user is not allowed to grant moderator privileges.
2622      */
addModerator(JID jid, MUCRole senderRole)2623     public Presence addModerator(JID jid, MUCRole senderRole) throws ForbiddenException {
2624         if (MUCRole.Affiliation.admin != senderRole.getAffiliation()
2625             && MUCRole.Affiliation.owner != senderRole.getAffiliation()) {
2626             throw new ForbiddenException();
2627         }
2628         // Update the presence with the new role and inform all occupants
2629         try {
2630             return changeOccupantRole(jid, MUCRole.Role.moderator);
2631         }
2632         catch (NotAllowedException e) {
2633             // We should never receive this exception....in theory
2634             return null;
2635         }
2636     }
2637 
2638     /**
2639      * Changes the role of the user within the room to participant. A participant is allowed to send
2640      * messages to the room (i.e. has voice) and may change the room's subject.
2641      *
2642      * @param jid The full JID of the occupant to give participant privileges (cannot be {@code null}).
2643      * @param reason The reason why participant privileges were gave to the user or {@code null}
2644      *        if none.
2645      * @param senderRole The role of the user that is granting participant privileges to an occupant (cannot be {@code null}).
2646      * @return the updated presence of the occupant or {@code null} if the JID does not belong to
2647      *         an existing occupant.
2648      * @throws NotAllowedException If trying to change the moderator role to an owner or an admin.
2649      * @throws ForbiddenException If the user is not allowed to grant participant privileges.
2650      */
addParticipant(JID jid, String reason, MUCRole senderRole)2651     public Presence addParticipant(JID jid, String reason, MUCRole senderRole)
2652         throws NotAllowedException, ForbiddenException {
2653         if (MUCRole.Role.moderator != senderRole.getRole()) {
2654             throw new ForbiddenException();
2655         }
2656         // Update the presence with the new role and inform all occupants
2657         Presence updatedPresence = changeOccupantRole(jid, MUCRole.Role.participant);
2658         if (updatedPresence != null) {
2659             Element frag = updatedPresence.getChildElement(
2660                 "x", "http://jabber.org/protocol/muc#user");
2661             // Add the reason why the user was granted voice
2662             if (reason != null && reason.trim().length() > 0) {
2663                 frag.element("item").addElement("reason").setText(reason);
2664             }
2665         }
2666         return updatedPresence;
2667     }
2668 
2669     /**
2670      * Changes the role of the user within the room to visitor. A visitor can receive messages but
2671      * is not allowed to send messages to the room (i.e. does not has voice) and may invite others
2672      * to the room.
2673      *
2674      * @param jid the full JID of the occupant to change to visitor (cannot be {@code null}).
2675      * @param senderRole the role of the user that is changing the role to visitor (cannot be {@code null}).
2676      * @return the updated presence of the occupant or {@code null} if the JID does not belong to
2677      *         an existing occupant.
2678      * @throws NotAllowedException if trying to change the moderator role to an owner or an admin.
2679      * @throws ForbiddenException if the user is not a moderator.
2680      */
addVisitor(JID jid, MUCRole senderRole)2681     public Presence addVisitor(JID jid, MUCRole senderRole) throws NotAllowedException,
2682         ForbiddenException {
2683         if (MUCRole.Role.moderator != senderRole.getRole()) {
2684             throw new ForbiddenException();
2685         }
2686         return changeOccupantRole(jid, MUCRole.Role.visitor);
2687     }
2688 
2689     /**
2690      * Kicks a user from the room. If the user was in the room, the returned updated presence will
2691      * be sent to the remaining occupants.
2692      *
2693      * @param jid       The full JID of the kicked user  (cannot be {@code null}).
2694      * @param actorJID      The JID of the actor that initiated the kick (cannot be {@code null}).
2695      * @param actorNickname The actor nickname.
2696      * @param reason        An optional reason why the user was kicked (can be {@code null}).
2697      * @return the updated presence of the kicked user or null if the user was not in the room.
2698      * @throws NotAllowedException Thrown if trying to ban an owner or an administrator.
2699      */
kickOccupant(JID jid, JID actorJID, String actorNickname, String reason)2700     public Presence kickOccupant(JID jid, JID actorJID, String actorNickname, String reason)
2701         throws NotAllowedException {
2702         // Update the presence with the new role and inform all occupants
2703         Presence updatedPresence = changeOccupantRole(jid, MUCRole.Role.none);
2704 
2705         // Determine the role of the actor that initiates the kick.
2706         MUCRole sender;
2707         if ( actorJID == null ) {
2708             sender = getRole(); // originates from the room itself (eg: through admin console changes).
2709         } else {
2710             sender = getOccupantByFullJID(actorJID);
2711             if ( sender == null ) {
2712                 sender = getRole();
2713             }
2714         }
2715 
2716         if (updatedPresence != null) {
2717             Element frag = updatedPresence.getChildElement(
2718                 "x", "http://jabber.org/protocol/muc#user");
2719 
2720             // Add the status code 307 that indicates that the user was kicked
2721             frag.addElement("status").addAttribute("code", "307");
2722             // Add the reason why the user was kicked
2723             if (reason != null && reason.trim().length() > 0) {
2724                 frag.element("item").addElement("reason").setText(reason);
2725             }
2726 
2727             // Effectively kick the occupant from the room
2728             kickPresence(updatedPresence, actorJID, actorNickname);
2729 
2730             //Inform the other occupants that user has been kicked
2731             broadcastPresence(updatedPresence, false, sender);
2732         }
2733         // TODO should this return presence/should callers distribute this stanza (alternatively: should this stanza be broadcast in this method)?
2734         return updatedPresence;
2735     }
2736 
2737     /**
2738      * Kicks the occupant from the room. This means that the occupant will receive an unavailable
2739      * presence with the actor that initiated the kick (if any). The occupant will also be removed
2740      * from the occupants lists.
2741      *
2742      * Note that the remaining occupants of the room are not informed by this implementation. It is the responsibility
2743      * of the caller to ensure that this occurs.
2744      *
2745      * @param kickPresence the presence of the occupant to kick from the room.
2746      * @param actorJID The JID of the actor that initiated the kick or {@code null} if the info
2747      * @param nick The actor nickname.
2748      * was not provided.
2749      */
kickPresence(Presence kickPresence, JID actorJID, String nick)2750     private void kickPresence(Presence kickPresence, JID actorJID, String nick) {
2751         // Get the role(s) to kick
2752         final List<MUCRole> kickedRoles;
2753         try {
2754             kickedRoles = getOccupantsByNickname(kickPresence.getFrom().getResource());
2755             for (MUCRole kickedRole : kickedRoles) {
2756                 // Add the actor's JID that kicked this user from the room
2757                 if (actorJID!=null && actorJID.toString().length() > 0) {
2758                     Element frag = kickPresence.getChildElement(
2759                         "x", "http://jabber.org/protocol/muc#user");
2760                     Element actor = frag.element("item").addElement("actor");
2761                     actor.addAttribute("jid", actorJID.toBareJID());
2762                     if (nick!=null) {
2763                         actor.addAttribute("nick", nick);
2764                     }
2765                 }
2766 
2767                 // Send a defensive copy (to not leak a change to the 'to' address - this is possibly overprotective here,
2768                 // but we're erring on the side of caution) of the unavailable presence to the banned user.
2769                 final Presence kickSelfPresence = kickPresence.createCopy();
2770                 Element fragKickSelfPresence = kickSelfPresence.getChildElement("x", "http://jabber.org/protocol/muc#user");
2771                 fragKickSelfPresence.addElement("status").addAttribute("code", "110");
2772                 kickedRole.send(kickSelfPresence);
2773 
2774                 // Remove the occupant from the room's occupants lists
2775                 removeOccupantRole(kickedRole);
2776             }
2777         } catch (UserNotFoundException e) {
2778             Log.debug("Unable to kick '{}' from room '{}' as there's no occupant with that nickname.", kickPresence.getFrom().getResource(), getJID(), e);
2779         }
2780     }
2781 
2782     /**
2783      * Returns true if every presence packet will include the JID of every occupant. This
2784      * configuration can be modified by the owner while editing the room's configuration.
2785      *
2786      * @return true if every presence packet will include the JID of every occupant.
2787      */
canAnyoneDiscoverJID()2788     public boolean canAnyoneDiscoverJID() {
2789         return canAnyoneDiscoverJID;
2790     }
2791 
2792     /**
2793      * Sets if every presence packet will include the JID of every occupant. This
2794      * configuration can be modified by the owner while editing the room's configuration.
2795      *
2796      * @param canAnyoneDiscoverJID boolean that specifies if every presence packet will include the
2797      *        JID of every occupant.
2798      */
setCanAnyoneDiscoverJID(boolean canAnyoneDiscoverJID)2799     public void setCanAnyoneDiscoverJID(boolean canAnyoneDiscoverJID) {
2800         this.canAnyoneDiscoverJID = canAnyoneDiscoverJID;
2801     }
2802 
2803     /**
2804      * Returns the minimal role of persons that are allowed to send private messages in the room. The returned value is
2805      * any one of: "anyone", "moderators", "participants", "none".
2806      *
2807      * @return The minimal role of persons that are allowed to send private messages in the room (never null).
2808      */
canSendPrivateMessage()2809     public String canSendPrivateMessage() {
2810         return canSendPrivateMessage == null ? "anyone" : canSendPrivateMessage;
2811     }
2812 
2813     /**
2814      * Sets the minimal role of persons that are allowed to send private messages in the room. The provided value is
2815      * any one of: "anyone", "moderators", "participants", "none". If another value is set, "anyone" is used instead.
2816      *
2817      * @param role The minimal role of persons that are allowed to send private messages in the room (never null).
2818      */
setCanSendPrivateMessage(String role)2819     public void setCanSendPrivateMessage(String role) {
2820         if ( role == null ) {
2821             role = "(null)";
2822         }
2823 
2824         switch( role.toLowerCase() ) {
2825             case "none":
2826             case "moderators":
2827             case "participants":
2828             case "anyone":
2829                 this.canSendPrivateMessage = role.toLowerCase();
2830                 break;
2831             default:
2832                 Log.warn( "Illegal value for muc#roomconfig_allowpm: '{}'. Defaulting to 'anyone'", role.toLowerCase() );
2833                 this.canSendPrivateMessage = "anyone";
2834         }
2835     }
2836 
2837     /**
2838      * Returns true if participants are allowed to change the room's subject.
2839      *
2840      * @return true if participants are allowed to change the room's subject.
2841      */
canOccupantsChangeSubject()2842     public boolean canOccupantsChangeSubject() {
2843         return canOccupantsChangeSubject;
2844     }
2845 
2846     /**
2847      * Sets if participants are allowed to change the room's subject.
2848      *
2849      * @param canOccupantsChangeSubject boolean that specifies if participants are allowed to
2850      *        change the room's subject.
2851      */
setCanOccupantsChangeSubject(boolean canOccupantsChangeSubject)2852     public void setCanOccupantsChangeSubject(boolean canOccupantsChangeSubject) {
2853         this.canOccupantsChangeSubject = canOccupantsChangeSubject;
2854     }
2855 
2856     /**
2857      * Returns true if occupants can invite other users to the room. If the room does not require an
2858      * invitation to enter (i.e. is not members-only) then any occupant can send invitations. On
2859      * the other hand, if the room is members-only and occupants cannot send invitation then only
2860      * the room owners and admins are allowed to send invitations.
2861      *
2862      * @return true if occupants can invite other users to the room.
2863      */
canOccupantsInvite()2864     public boolean canOccupantsInvite() {
2865         return canOccupantsInvite;
2866     }
2867 
2868     /**
2869      * Sets if occupants can invite other users to the room. If the room does not require an
2870      * invitation to enter (i.e. is not members-only) then any occupant can send invitations. On
2871      * the other hand, if the room is members-only and occupants cannot send invitation then only
2872      * the room owners and admins are allowed to send invitations.
2873      *
2874      * @param canOccupantsInvite boolean that specified in any occupant can invite other users to
2875      *        the room.
2876      */
setCanOccupantsInvite(boolean canOccupantsInvite)2877     public void setCanOccupantsInvite(boolean canOccupantsInvite) {
2878         this.canOccupantsInvite = canOccupantsInvite;
2879     }
2880 
2881     /**
2882      * Returns the natural language name of the room. This name can only be modified by room owners.
2883      * It's mainly used for users while discovering rooms hosted by the Multi-User Chat service.
2884      *
2885      * @return the natural language name of the room.
2886      */
getNaturalLanguageName()2887     public String getNaturalLanguageName() {
2888         return naturalLanguageName;
2889     }
2890 
2891     /**
2892      * Sets the natural language name of the room. This name can only be modified by room owners.
2893      * It's mainly used for users while discovering rooms hosted by the Multi-User Chat service.
2894      *
2895      * @param naturalLanguageName the natural language name of the room.
2896      */
setNaturalLanguageName(String naturalLanguageName)2897     public void setNaturalLanguageName(String naturalLanguageName) {
2898         this.naturalLanguageName = naturalLanguageName;
2899     }
2900 
2901     /**
2902      * Returns a description set by the room's owners about the room. This information will be used
2903      * when discovering extended information about the room.
2904      *
2905      * @return a description set by the room's owners about the room.
2906      */
getDescription()2907     public String getDescription() {
2908         return description;
2909     }
2910 
2911     /**
2912      * Sets a description set by the room's owners about the room. This information will be used
2913      * when discovering extended information about the room.
2914      *
2915      * @param description a description set by the room's owners about the room.
2916      */
setDescription(String description)2917     public void setDescription(String description) {
2918         this.description = description;
2919     }
2920 
2921     /**
2922      * Returns true if the room requires an invitation to enter. That is if the room is
2923      * members-only.
2924      *
2925      * @return true if the room requires an invitation to enter.
2926      */
isMembersOnly()2927     public boolean isMembersOnly() {
2928         return membersOnly;
2929     }
2930 
2931     /**
2932      * Sets if the room requires an invitation to enter. That is if the room is members-only.
2933      *
2934      * @param membersOnly if true then the room is members-only.
2935      * @return the list of updated presences of all the occupants that aren't members of the room if
2936      *         the room is now members-only.
2937      */
setMembersOnly(boolean membersOnly)2938     public List<Presence> setMembersOnly(boolean membersOnly) {
2939         List<Presence> presences = new CopyOnWriteArrayList<>();
2940         if (membersOnly && !this.membersOnly) {
2941             // If the room was not members-only and now it is, kick occupants that aren't member
2942             // of the room
2943             for (MUCRole occupant : getOccupants()) {
2944                 if (occupant.getAffiliation().compareTo(MUCRole.Affiliation.member) > 0) {
2945                     try {
2946                         presences.add(kickOccupant(occupant.getRoleAddress(), null, null,
2947                             LocaleUtils.getLocalizedString("muc.roomIsNowMembersOnly")));
2948                     }
2949                     catch (NotAllowedException e) {
2950                         Log.error(e.getMessage(), e);
2951                     }
2952                 }
2953             }
2954         }
2955         this.membersOnly = membersOnly;
2956         return presences;
2957     }
2958 
2959     /**
2960      * Returns true if the room's conversation is being logged. If logging is activated the room
2961      * conversation will be saved to the database every couple of minutes. The saving frequency is
2962      * the same for all the rooms and can be configured by changing the property
2963      * "xmpp.muc.tasks.log.timeout" of MultiUserChatServerImpl.
2964      *
2965      * @return true if the room's conversation is being logged.
2966      */
isLogEnabled()2967     public boolean isLogEnabled() {
2968         return logEnabled;
2969     }
2970 
2971     /**
2972      * Sets if the room's conversation is being logged. If logging is activated the room
2973      * conversation will be saved to the database every couple of minutes. The saving frequency is
2974      * the same for all the rooms and can be configured by changing the property
2975      * "xmpp.muc.tasks.log.timeout" of MultiUserChatServerImpl.
2976      *
2977      * @param logEnabled boolean that specified if the room's conversation must be logged.
2978      */
setLogEnabled(boolean logEnabled)2979     public void setLogEnabled(boolean logEnabled) {
2980         this.logEnabled = logEnabled;
2981     }
2982 
2983     /**
2984      * Sets if registered users can only join the room using their registered nickname. A
2985      * not_acceptable error will be returned if the user tries to join the room with a nickname
2986      * different than the reserved nickname.
2987      *
2988      * @param restricted if registered users can only join the room using their registered nickname.
2989      */
setLoginRestrictedToNickname(boolean restricted)2990     public void setLoginRestrictedToNickname(boolean restricted) {
2991         this.loginRestrictedToNickname = restricted;
2992     }
2993 
2994     /**
2995      * Returns true if registered users can only join the room using their registered nickname. By
2996      * default, registered users can join the room using any nickname. A not_acceptable error
2997      * will be returned if the user tries to join the room with a nickname different than the
2998      * reserved nickname.
2999      *
3000      * @return true if registered users can only join the room using their registered nickname.
3001      */
isLoginRestrictedToNickname()3002     public boolean isLoginRestrictedToNickname() {
3003         return loginRestrictedToNickname;
3004     }
3005 
3006     /**
3007      * Sets if room occupants are allowed to change their nicknames in the room. By default,
3008      * occupants are allowed to change their nicknames. A not_acceptable error will be returned if
3009      * an occupant tries to change his nickname and this feature is not enabled.<p>
3010      *
3011      * Notice that this feature is not supported by the MUC spec so answering a not_acceptable
3012      * error may break some cliens.
3013      *
3014      * @param canChange if room occupants are allowed to change their nicknames in the room.
3015      */
setChangeNickname(boolean canChange)3016     public void setChangeNickname(boolean canChange) {
3017         this.canChangeNickname = canChange;
3018     }
3019 
3020     /**
3021      * Returns true if room occupants are allowed to change their nicknames in the room. By
3022      * default, occupants are allowed to change their nicknames. A not_acceptable error will be
3023      * returned if an occupant tries to change his nickname and this feature is not enabled.<p>
3024      *
3025      * Notice that this feature is not supported by the MUC spec so answering a not_acceptable
3026      * error may break some cliens.
3027      *
3028      * @return true if room occupants are allowed to change their nicknames in the room.
3029      */
canChangeNickname()3030     public boolean canChangeNickname() {
3031         return canChangeNickname;
3032     }
3033 
3034     /**
3035      * Sets if users are allowed to register with the room. By default, room registration
3036      * is enabled. A not_allowed error will be returned if a user tries to register with the room
3037      * and this feature is disabled.
3038      *
3039      * @param registrationEnabled if users are allowed to register with the room.
3040      */
setRegistrationEnabled(boolean registrationEnabled)3041     public void setRegistrationEnabled(boolean registrationEnabled) {
3042         this.registrationEnabled = registrationEnabled;
3043     }
3044 
3045     /**
3046      * Returns true if users are allowed to register with the room. By default, room registration
3047      * is enabled. A not_allowed error will be returned if a user tries to register with the room
3048      * and this feature is disabled.
3049      *
3050      * @return true if users are allowed to register with the room.
3051      */
isRegistrationEnabled()3052     public boolean isRegistrationEnabled() {
3053         return registrationEnabled;
3054     }
3055 
3056     /**
3057      * Sets if this room accepts FMUC joins. By default, FMUC functionality is not enabled.
3058      * When joining nodes are attempting a join, a rejection will be returned when this feature is disabled.
3059      */
setFmucEnabled(boolean fmucEnabled)3060     public void setFmucEnabled(boolean fmucEnabled) {
3061         this.fmucEnabled = fmucEnabled;
3062     }
3063 
3064     /**
3065      * Returns true if this room accepts FMUC joins. By default, FMUC functionality is not enabled.
3066      * When joining nodes are attempting a join, a rejection will be returned when this feature is disabled.
3067      */
isFmucEnabled()3068     public boolean isFmucEnabled() {
3069         return fmucEnabled;
3070     }
3071 
3072     /**
3073      * Sets the address of the MUC room (typically on a remote XMPP domain) to which this room should initiate
3074      * FMUC federation. In this federation, the local node takes the role of the 'joining' node, while the remote node
3075      * takes the role of the 'joined' node.
3076      *
3077      * When this room is not expected to initiate federation (note that it can still accept inbound federation attempts)
3078      * then this method returns null.
3079      *
3080      * Although a room can accept multiple inbound joins (where it acts as a 'parent' node), it can initiate only one
3081      * outbound join at a time (where it acts as a 'child' node).
3082      *
3083      * @param fmucOutboundNode Address of peer for to-be-initiated outbound FMUC federation, possibly null.
3084      */
setFmucOutboundNode( JID fmucOutboundNode )3085     public void setFmucOutboundNode( JID fmucOutboundNode ) {
3086         this.fmucOutboundNode = fmucOutboundNode;
3087     }
3088 
3089     /**
3090      * Returns the address of the MUC room (typically on a remote XMPP domain) to which this room should initiate
3091      * FMUC federation. In this federation, the local node takes the role of the 'joining' node, while the remote node
3092      * takes the role of the 'joined' node.
3093      *
3094      * When this room is not expected to initiate federation (note that it can still accept inbound federation attempts)
3095      * then this method returns null.
3096      *
3097      * Although a room can accept multiple inbound joins (where it acts as a 'parent' node), it can initiate only one
3098      * outbound join at a time (where it acts as a 'child' node).
3099      *
3100      * @return Address of peer for to-be-initiated outbound FMUC federation, possibly null.
3101      */
getFmucOutboundNode()3102     public JID getFmucOutboundNode() {
3103         return fmucOutboundNode;
3104     }
3105 
3106     /**
3107      * Sets the 'mode' that describes the FMUC configuration is captured in the supplied object, which is
3108      * either master-master or master-slave.
3109      *
3110      * @param fmucOutboundMode FMUC mode applied to outbound FMUC federation attempts.
3111      */
setFmucOutboundMode( FMUCMode fmucOutboundMode )3112     public void setFmucOutboundMode( FMUCMode fmucOutboundMode ) {
3113         this.fmucOutboundMode = fmucOutboundMode;
3114     }
3115 
3116     /**
3117      * Returns the 'mode' that describes the FMUC configuration is captured in the supplied object, which is
3118      * either master-master or master-slave.
3119      *
3120      * This method should return null only when no outbound federation should be attempted.
3121      *
3122      * @return FMUC mode applied to outbound FMUC federation attempts.
3123      */
getFmucOutboundMode()3124     public FMUCMode getFmucOutboundMode() {
3125         return fmucOutboundMode;
3126     }
3127 
3128     /**
3129      * A set of addresses of MUC rooms (typically on a remote XMPP domain) that defines the list of rooms that is
3130      * permitted to to federate with the local room.
3131      *
3132      * A null value is to be interpreted as allowing all rooms to be permitted.
3133      *
3134      * An empty set of addresses is to be interpreted as disallowing all rooms to be permitted.
3135      *
3136      * @param fmucInboundNodes A list of rooms allowed to join, possibly empty, possibly null
3137      */
setFmucInboundNodes( Set<JID> fmucInboundNodes )3138     public void setFmucInboundNodes( Set<JID> fmucInboundNodes ) {
3139         this.fmucInboundNodes = fmucInboundNodes;
3140     }
3141 
3142     /**
3143      * A set of addresses of MUC rooms (typically on a remote XMPP domain) that defines the list of rooms that is
3144      * permitted to to federate with the local room.
3145      *
3146      * A null value is to be interpreted as allowing all rooms to be permitted.
3147      *
3148      * An empty set of addresses is to be interpreted as disallowing all rooms to be permitted.
3149      *
3150      * @return A list of rooms allowed to join, possibly empty, possibly null
3151      */
getFmucInboundNodes()3152     public Set<JID> getFmucInboundNodes() {
3153         return fmucInboundNodes;
3154     }
3155 
3156     /**
3157      * Returns the maximum number of occupants that can be simultaneously in the room. If the number
3158      * is zero then there is no limit.
3159      *
3160      * @return the maximum number of occupants that can be simultaneously in the room. Zero means
3161      *         unlimited number of occupants.
3162      */
getMaxUsers()3163     public int getMaxUsers() {
3164         return maxUsers;
3165     }
3166 
3167     /**
3168      * Sets the maximum number of occupants that can be simultaneously in the room. If the number
3169      * is zero then there is no limit.
3170      *
3171      * @param maxUsers the maximum number of occupants that can be simultaneously in the room. Zero
3172      *        means unlimited number of occupants.
3173      */
setMaxUsers(int maxUsers)3174     public void setMaxUsers(int maxUsers) {
3175         this.maxUsers = maxUsers;
3176     }
3177 
3178     /**
3179      * Returns if the room in which only those with "voice" may send messages to all occupants.
3180      *
3181      * @return if the room in which only those with "voice" may send messages to all occupants.
3182      */
isModerated()3183     public boolean isModerated() {
3184         return moderated;
3185     }
3186 
3187     /**
3188      * Sets if the room in which only those with "voice" may send messages to all occupants.
3189      *
3190      * @param moderated if the room in which only those with "voice" may send messages to all
3191      *        occupants.
3192      */
setModerated(boolean moderated)3193     public void setModerated(boolean moderated) {
3194         this.moderated = moderated;
3195     }
3196 
3197     /**
3198      * Returns the password that the user must provide to enter the room.
3199      *
3200      * @return the password that the user must provide to enter the room.
3201      */
getPassword()3202     public String getPassword() {
3203         return password;
3204     }
3205 
3206     /**
3207      * Sets the password that the user must provide to enter the room.
3208      *
3209      * @param password the password that the user must provide to enter the room.
3210      */
setPassword(String password)3211     public void setPassword(String password) {
3212         this.password = password;
3213     }
3214 
3215     /**
3216      * Returns true if a user cannot enter without first providing the correct password.
3217      *
3218      * @return true if a user cannot enter without first providing the correct password.
3219      */
isPasswordProtected()3220     public boolean isPasswordProtected() {
3221         return password != null && password.trim().length() > 0;
3222     }
3223 
3224     /**
3225      * Returns true if the room is not destroyed if the last occupant exits. Persistent rooms are
3226      * saved to the database to make their configurations persistent together with the affiliation
3227      * of the users.
3228      *
3229      * @return true if the room is not destroyed if the last occupant exits.
3230      */
isPersistent()3231     public boolean isPersistent() {
3232         return persistent;
3233     }
3234 
3235     /**
3236      * Returns true if the room has already been made persistent. If the room is temporary the
3237      * answer will always be false.
3238      *
3239      * @return true if the room has already been made persistent.
3240      */
wasSavedToDB()3241     public boolean wasSavedToDB() {
3242         return isPersistent() && savedToDB;
3243     }
3244 
3245     /**
3246      * Sets if the room has already been made persistent.
3247      *
3248      * @param saved boolean that indicates if the room was saved to the database.
3249      */
setSavedToDB(boolean saved)3250     public void setSavedToDB(boolean saved) {
3251         this.savedToDB = saved;
3252     }
3253 
3254     /**
3255      * Sets if the room is not destroyed if the last occupant exits. Persistent rooms are
3256      * saved to the database to make their configurations persistent together with the affiliation
3257      * of the users.
3258      *
3259      * @param persistent if the room is not destroyed if the last occupant exits.
3260      */
setPersistent(boolean persistent)3261     public void setPersistent(boolean persistent) {
3262         this.persistent = persistent;
3263     }
3264 
3265     /**
3266      * Returns true if the room is searchable and visible through service discovery.
3267      *
3268      * @return true if the room is searchable and visible through service discovery.
3269      */
isPublicRoom()3270     public boolean isPublicRoom() {
3271         return !isDestroyed && publicRoom;
3272     }
3273 
3274     /**
3275      * Sets if the room is searchable and visible through service discovery.
3276      *
3277      * @param publicRoom if the room is searchable and visible through service discovery.
3278      */
setPublicRoom(boolean publicRoom)3279     public void setPublicRoom(boolean publicRoom) {
3280         this.publicRoom = publicRoom;
3281     }
3282 
3283     /**
3284      * Returns the list of roles of which presence will be broadcast to the rest of the occupants.
3285      * This feature is useful for implementing "invisible" occupants.
3286      *
3287      * @return the list of roles of which presence will be broadcast to the rest of the occupants.
3288      */
3289     @Nonnull
getRolesToBroadcastPresence()3290     public List<MUCRole.Role> getRolesToBroadcastPresence() {
3291         return Collections.unmodifiableList(rolesToBroadcastPresence);
3292     }
3293 
3294     /**
3295      * Sets the list of roles of which presence will be broadcast to the rest of the occupants.
3296      * This feature is useful for implementing "invisible" occupants.
3297      *
3298      * @param rolesToBroadcastPresence the list of roles of which presence will be broadcast to
3299      * the rest of the occupants.
3300      */
setRolesToBroadcastPresence(@onnull final List<MUCRole.Role> rolesToBroadcastPresence)3301     public void setRolesToBroadcastPresence(@Nonnull final List<MUCRole.Role> rolesToBroadcastPresence) {
3302         // TODO If the list changes while there are occupants in the room we must send available or
3303         // unavailable presences of the affected occupants to the rest of the occupants
3304         this.rolesToBroadcastPresence = rolesToBroadcastPresence;
3305     }
3306 
3307     /**
3308      * Returns true if the presences of the requested role will be broadcast.
3309      *
3310      * @param roleToBroadcast the role to check if its presences will be broadcast.
3311      * @return true if the presences of the requested role will be broadcast.
3312      */
canBroadcastPresence(@onnull final MUCRole.Role roleToBroadcast)3313     public boolean canBroadcastPresence(@Nonnull final MUCRole.Role roleToBroadcast) {
3314         return MUCRole.Role.none.equals(roleToBroadcast) || rolesToBroadcastPresence.contains(roleToBroadcast);
3315     }
3316 
3317     /**
3318      * Locks the room so that users cannot join the room. Only the owner of the room can lock/unlock
3319      * the room.
3320      *
3321      * @param senderRole the role of the occupant that locked the room.
3322      * @throws ForbiddenException If the user is not an owner of the room.
3323      */
lock(MUCRole senderRole)3324     public void lock(MUCRole senderRole) throws ForbiddenException {
3325         if (MUCRole.Affiliation.owner != senderRole.getAffiliation()) {
3326             throw new ForbiddenException();
3327         }
3328         if (isLocked()) {
3329             // Do nothing if the room was already locked
3330             return;
3331         }
3332         setLocked(true);
3333     }
3334 
3335     /**
3336      * Unlocks the room so that users can join the room. The room is locked when created and only
3337      * the owner of the room can unlock it by sending the configuration form to the Multi-User Chat
3338      * service.
3339      *
3340      * @param senderRole the role of the occupant that unlocked the room.
3341      * @throws ForbiddenException If the user is not an owner of the room.
3342      */
unlock(MUCRole senderRole)3343     public void unlock(MUCRole senderRole) throws ForbiddenException {
3344         if (MUCRole.Affiliation.owner != senderRole.getAffiliation()) {
3345             throw new ForbiddenException();
3346         }
3347         if (!isLocked()) {
3348             // Do nothing if the room was already unlocked
3349             return;
3350         }
3351         setLocked(false);
3352     }
3353 
setLocked(boolean locked)3354     private void setLocked(boolean locked) {
3355         if (locked) {
3356             this.lockedTime = System.currentTimeMillis();
3357         }
3358         else {
3359             this.lockedTime = 0;
3360         }
3361         MUCPersistenceManager.updateRoomLock(this);
3362     }
3363 
3364     /**
3365      * Sets the date when the room was locked. Initially when the room is created it is locked so
3366      * the locked date is the creation date of the room. Afterwards, the room may be manually
3367      * locked and unlocked so the locked date may be in these cases different than the creation
3368      * date. A Date with time 0 means that the the room is unlocked.
3369      *
3370      * @param lockedTime the date when the room was locked.
3371      */
setLockedDate(Date lockedTime)3372     public void setLockedDate(Date lockedTime) {
3373         this.lockedTime = lockedTime.getTime();
3374     }
3375 
3376     /**
3377      * Returns the date when the room was locked. Initially when the room is created it is locked so
3378      * the locked date is the creation date of the room. Afterwards, the room may be manually
3379      * locked and unlocked so the locked date may be in these cases different than the creation
3380      * date. When the room is unlocked a Date with time 0 is returned.
3381      *
3382      * @return the date when the room was locked.
3383      */
getLockedDate()3384     public Date getLockedDate() {
3385         return new Date(lockedTime);
3386     }
3387 
3388     /**
3389      * Adds a list of users to the list of admins.
3390      *
3391      * @param newAdmins the list of bare JIDs of the users to add to the list of existing admins (cannot be {@code null}).
3392      * @param senderRole the role of the user that is trying to modify the admins list (cannot be {@code null}).
3393      * @return the list of updated presences of all the clients resources that the clients used to
3394      *         join the room.
3395      * @throws ForbiddenException If the user is not allowed to modify the admin list.
3396      * @throws ConflictException If the room was going to lose all its owners.
3397      */
addAdmins(List<JID> newAdmins, MUCRole senderRole)3398     public List<Presence> addAdmins(List<JID> newAdmins, MUCRole senderRole)
3399         throws ForbiddenException, ConflictException {
3400         List<Presence> answer = new ArrayList<>(newAdmins.size());
3401         for (JID newAdmin : newAdmins) {
3402             final JID bareJID = newAdmin.asBareJID();
3403             if (!admins.contains(bareJID)) {
3404                 answer.addAll(addAdmin(bareJID, senderRole));
3405             }
3406         }
3407         return answer;
3408     }
3409 
3410     /**
3411      * Adds a list of users to the list of owners.
3412      *
3413      * @param newOwners the list of bare JIDs of the users to add to the list of existing owners (cannot be {@code null}).
3414      * @param senderRole the role of the user that is trying to modify the owners list (cannot be {@code null}).
3415      * @return the list of updated presences of all the clients resources that the clients used to
3416      *         join the room.
3417      * @throws ForbiddenException If the user is not allowed to modify the owner list.
3418      */
addOwners(List<JID> newOwners, MUCRole senderRole)3419     public List<Presence> addOwners(List<JID> newOwners, MUCRole senderRole)
3420         throws ForbiddenException {
3421         List<Presence> answer = new ArrayList<>(newOwners.size());
3422         for (JID newOwner : newOwners) {
3423             final JID bareJID = newOwner.asBareJID();
3424             if (!owners.contains(newOwner)) {
3425                 answer.addAll(addOwner(bareJID, senderRole));
3426             }
3427         }
3428         return answer;
3429     }
3430 
3431     /**
3432      * Saves the room configuration to the DB. After the room has been saved to the DB it will
3433      * become persistent.
3434      */
saveToDB()3435     public void saveToDB() {
3436         // Make the room persistent
3437         MUCPersistenceManager.saveToDB(this);
3438         if (!savedToDB) {
3439             // Set that the room is now in the DB
3440             savedToDB = true;
3441             // Save the existing room owners to the DB
3442             for (JID owner : owners) {
3443                 MUCPersistenceManager.saveAffiliationToDB(
3444                     this,
3445                     owner,
3446                     null,
3447                     MUCRole.Affiliation.owner,
3448                     MUCRole.Affiliation.none);
3449             }
3450             // Save the existing room admins to the DB
3451             for (JID admin : admins) {
3452                 MUCPersistenceManager.saveAffiliationToDB(
3453                     this,
3454                     admin,
3455                     null,
3456                     MUCRole.Affiliation.admin,
3457                     MUCRole.Affiliation.none);
3458             }
3459             // Save the existing room members to the DB
3460             for (JID bareJID : members.keySet()) {
3461                 MUCPersistenceManager.saveAffiliationToDB(this, bareJID, members.get(bareJID),
3462                     MUCRole.Affiliation.member, MUCRole.Affiliation.none);
3463             }
3464             // Save the existing room outcasts to the DB
3465             for (JID outcast : outcasts) {
3466                 MUCPersistenceManager.saveAffiliationToDB(
3467                     this,
3468                     outcast,
3469                     null,
3470                     MUCRole.Affiliation.outcast,
3471                     MUCRole.Affiliation.none);
3472             }
3473         }
3474     }
3475 
3476     @Override
getCachedSize()3477     public int getCachedSize() throws CannotCalculateSizeException {
3478         // Approximate the size of the object in bytes by calculating the size of each field.
3479         int size = 0;
3480         size += CacheSizes.sizeOfObject();      // overhead of object
3481         size += CacheSizes.sizeOfCollection(occupants);
3482         size += CacheSizes.sizeOfString(name);
3483         size += CacheSizes.sizeOfAnything(role);
3484         size += CacheSizes.sizeOfLong();        // startTime
3485         size += CacheSizes.sizeOfLong();        // endTime
3486         size += CacheSizes.sizeOfBoolean();     // isDestroyed
3487         size += CacheSizes.sizeOfAnything(roomHistory);
3488         size += CacheSizes.sizeOfLong();        // lockedTime
3489         size += CacheSizes.sizeOfCollection(owners);
3490         size += CacheSizes.sizeOfCollection(admins);
3491         size += CacheSizes.sizeOfMap(members);
3492         size += CacheSizes.sizeOfCollection(outcasts);
3493         size += CacheSizes.sizeOfString(naturalLanguageName);
3494         size += CacheSizes.sizeOfString(description);
3495         size += CacheSizes.sizeOfBoolean();     // canOccupantsChangeSubject
3496         size += CacheSizes.sizeOfInt();         // maxUsers
3497         size += CacheSizes.sizeOfCollection(rolesToBroadcastPresence);
3498         size += CacheSizes.sizeOfBoolean();     // publicRoom
3499         size += CacheSizes.sizeOfBoolean();     // persistent
3500         size += CacheSizes.sizeOfBoolean();     // moderated
3501         size += CacheSizes.sizeOfBoolean();     // membersOnly
3502         size += CacheSizes.sizeOfBoolean();     // canOccupantsInvite
3503         size += CacheSizes.sizeOfString(password);
3504         size += CacheSizes.sizeOfBoolean();     // canAnyoneDiscoverJID
3505         size += CacheSizes.sizeOfString(canSendPrivateMessage);
3506         size += CacheSizes.sizeOfBoolean();     // logEnabled
3507         size += CacheSizes.sizeOfBoolean();     // loginRestrictedToNickname
3508         size += CacheSizes.sizeOfBoolean();     // canChangeNickname
3509         size += CacheSizes.sizeOfBoolean();     // registrationEnabled
3510         size += CacheSizes.sizeOfBoolean();     // fmucEnabled
3511         size += CacheSizes.sizeOfAnything(fmucOutboundNode);
3512         size += CacheSizes.sizeOfObject();      // fmucOutboundMode enum reference
3513         size += CacheSizes.sizeOfCollection(fmucInboundNodes);
3514         size += 1024; // Handwavy size of IQOwnerHandler (which holds sizeable data forms)
3515         size += CacheSizes.sizeOfObject() + CacheSizes.sizeOfObject() + CacheSizes.sizeOfBoolean(); // iqAdminHandler
3516         if (fmucHandler != null) {
3517             size += 2048; // Guestimate of fmucHandler
3518         }
3519         size += CacheSizes.sizeOfString(subject);
3520         size += CacheSizes.sizeOfLong();        // roomID
3521         size += CacheSizes.sizeOfDate();        // creationDate
3522         size += CacheSizes.sizeOfDate();        // modificationDate
3523         size += CacheSizes.sizeOfDate();        // emptyDate
3524         size += CacheSizes.sizeOfBoolean();     // savedToDB
3525         return size;
3526     }
3527 
3528     @Override
writeExternal(ObjectOutput out)3529     public void writeExternal(ObjectOutput out) throws IOException {
3530         ExternalizableUtil.getInstance().writeSafeUTF(out, name);
3531         ExternalizableUtil.getInstance().writeExternalizableCollection(out, occupants);
3532         ExternalizableUtil.getInstance().writeLong(out, startTime);
3533         ExternalizableUtil.getInstance().writeLong(out, endTime);
3534         ExternalizableUtil.getInstance().writeLong(out, lockedTime);
3535         ExternalizableUtil.getInstance().writeSerializableCollection(out, owners);
3536         ExternalizableUtil.getInstance().writeSerializableCollection(out, admins);
3537         ExternalizableUtil.getInstance().writeSerializableMap(out, members);
3538         ExternalizableUtil.getInstance().writeSerializableCollection(out, outcasts);
3539         ExternalizableUtil.getInstance().writeSafeUTF(out, naturalLanguageName);
3540         ExternalizableUtil.getInstance().writeSafeUTF(out, description);
3541         ExternalizableUtil.getInstance().writeBoolean(out, canOccupantsChangeSubject);
3542         ExternalizableUtil.getInstance().writeInt(out, maxUsers);
3543         ExternalizableUtil.getInstance().writeStringList(out, rolesToBroadcastPresence.stream().map(Enum::name).collect(Collectors.toList())); // This uses stringlist for compatibility with Openfire 4.6.0. Can be replaced the next major release.
3544         ExternalizableUtil.getInstance().writeBoolean(out, publicRoom);
3545         ExternalizableUtil.getInstance().writeBoolean(out, persistent);
3546         ExternalizableUtil.getInstance().writeBoolean(out, moderated);
3547         ExternalizableUtil.getInstance().writeBoolean(out, membersOnly);
3548         ExternalizableUtil.getInstance().writeBoolean(out, canOccupantsInvite);
3549         ExternalizableUtil.getInstance().writeSafeUTF(out, password);
3550         ExternalizableUtil.getInstance().writeBoolean(out, canAnyoneDiscoverJID);
3551         ExternalizableUtil.getInstance().writeSafeUTF(out, canSendPrivateMessage);
3552         ExternalizableUtil.getInstance().writeBoolean(out, logEnabled);
3553         ExternalizableUtil.getInstance().writeBoolean(out, loginRestrictedToNickname);
3554         ExternalizableUtil.getInstance().writeBoolean(out, canChangeNickname);
3555         ExternalizableUtil.getInstance().writeBoolean(out, registrationEnabled);
3556         ExternalizableUtil.getInstance().writeBoolean(out, fmucEnabled);
3557         ExternalizableUtil.getInstance().writeBoolean(out, fmucOutboundNode != null);
3558         if (fmucOutboundNode != null) {
3559             ExternalizableUtil.getInstance().writeSerializable(out, fmucOutboundNode);
3560         }
3561         ExternalizableUtil.getInstance().writeBoolean(out, fmucOutboundMode != null);
3562         if (fmucOutboundMode != null) {
3563             ExternalizableUtil.getInstance().writeInt(out, fmucOutboundMode.ordinal());
3564         }
3565         ExternalizableUtil.getInstance().writeBoolean(out, fmucInboundNodes != null);
3566         if (fmucInboundNodes != null) {
3567             ExternalizableUtil.getInstance().writeSerializableCollection(out, fmucInboundNodes);
3568         }
3569         ExternalizableUtil.getInstance().writeSafeUTF(out, subject);
3570         ExternalizableUtil.getInstance().writeLong(out, roomID);
3571         ExternalizableUtil.getInstance().writeLong(out, creationDate.getTime());
3572         ExternalizableUtil.getInstance().writeBoolean(out, modificationDate != null);
3573         if (modificationDate != null) {
3574             ExternalizableUtil.getInstance().writeLong(out, modificationDate.getTime());
3575         }
3576         ExternalizableUtil.getInstance().writeBoolean(out, emptyDate != null);
3577         if (emptyDate != null) {
3578             ExternalizableUtil.getInstance().writeLong(out, emptyDate.getTime());
3579         }
3580         ExternalizableUtil.getInstance().writeBoolean(out, savedToDB);
3581         ExternalizableUtil.getInstance().writeSafeUTF(out, mucService.getServiceName());
3582         ExternalizableUtil.getInstance().writeSerializable(out, roomHistory);
3583         ExternalizableUtil.getInstance().writeSerializable(out, role);
3584     }
3585 
3586     @Override
readExternal(ObjectInput in)3587     public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
3588         name = ExternalizableUtil.getInstance().readSafeUTF(in);
3589         ExternalizableUtil.getInstance().readExternalizableCollection(in, occupants, getClass().getClassLoader());
3590         startTime = ExternalizableUtil.getInstance().readLong(in);
3591         endTime = ExternalizableUtil.getInstance().readLong(in);
3592         lockedTime = ExternalizableUtil.getInstance().readLong(in);
3593         ExternalizableUtil.getInstance().readSerializableCollection(in, owners, getClass().getClassLoader());
3594         ExternalizableUtil.getInstance().readSerializableCollection(in, admins, getClass().getClassLoader());
3595         ExternalizableUtil.getInstance().readSerializableMap(in, members, getClass().getClassLoader());
3596         ExternalizableUtil.getInstance().readSerializableCollection(in, outcasts, getClass().getClassLoader());
3597         naturalLanguageName = ExternalizableUtil.getInstance().readSafeUTF(in);
3598         description = ExternalizableUtil.getInstance().readSafeUTF(in);
3599         canOccupantsChangeSubject = ExternalizableUtil.getInstance().readBoolean(in);
3600         maxUsers = ExternalizableUtil.getInstance().readInt(in);
3601         rolesToBroadcastPresence.addAll(ExternalizableUtil.getInstance().readStringList(in).stream().map(MUCRole.Role::valueOf).collect(Collectors.toSet())); // This uses stringlist for compatibility with Openfire 4.6.0. Can be replaced the next major release.
3602         publicRoom = ExternalizableUtil.getInstance().readBoolean(in);
3603         persistent = ExternalizableUtil.getInstance().readBoolean(in);
3604         moderated = ExternalizableUtil.getInstance().readBoolean(in);
3605         membersOnly = ExternalizableUtil.getInstance().readBoolean(in);
3606         canOccupantsInvite = ExternalizableUtil.getInstance().readBoolean(in);
3607         password = ExternalizableUtil.getInstance().readSafeUTF(in);
3608         canAnyoneDiscoverJID = ExternalizableUtil.getInstance().readBoolean(in);
3609         canSendPrivateMessage = ExternalizableUtil.getInstance().readSafeUTF(in);
3610         logEnabled = ExternalizableUtil.getInstance().readBoolean(in);
3611         loginRestrictedToNickname = ExternalizableUtil.getInstance().readBoolean(in);
3612         canChangeNickname = ExternalizableUtil.getInstance().readBoolean(in);
3613         registrationEnabled = ExternalizableUtil.getInstance().readBoolean(in);
3614         fmucEnabled = ExternalizableUtil.getInstance().readBoolean(in);
3615         if (ExternalizableUtil.getInstance().readBoolean(in)) {
3616             fmucOutboundNode = (JID) ExternalizableUtil.getInstance().readSerializable(in);
3617         } else {
3618             fmucOutboundNode = null;
3619         }
3620         if (ExternalizableUtil.getInstance().readBoolean(in)) {
3621             final int i = ExternalizableUtil.getInstance().readInt(in);
3622             fmucOutboundMode = FMUCMode.values()[i];
3623         } else {
3624             fmucOutboundMode = null;
3625         }
3626         if (ExternalizableUtil.getInstance().readBoolean(in)) {
3627             fmucInboundNodes = new HashSet<>();
3628             ExternalizableUtil.getInstance().readSerializableCollection(in, fmucInboundNodes, getClass().getClassLoader());
3629         } else {
3630             fmucInboundNodes = null;
3631         }
3632         subject = ExternalizableUtil.getInstance().readSafeUTF(in);
3633         roomID = ExternalizableUtil.getInstance().readLong(in);
3634         creationDate = new Date(ExternalizableUtil.getInstance().readLong(in));
3635         if (ExternalizableUtil.getInstance().readBoolean(in)) {
3636             modificationDate = new Date(ExternalizableUtil.getInstance().readLong(in));
3637         }
3638         if (ExternalizableUtil.getInstance().readBoolean(in)) {
3639             emptyDate = new Date(ExternalizableUtil.getInstance().readLong(in));
3640         }
3641         savedToDB = ExternalizableUtil.getInstance().readBoolean(in);
3642         String subdomain = ExternalizableUtil.getInstance().readSafeUTF(in);
3643         mucService = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService(subdomain);
3644         if (mucService == null) throw new IllegalArgumentException("MUC service not found for subdomain: " + subdomain);
3645         roomHistory = new MUCRoomHistory(this, new HistoryStrategy(mucService.getHistoryStrategy()));
3646 
3647         this.iqOwnerHandler = new IQOwnerHandler(this);
3648         this.iqAdminHandler = new IQAdminHandler(this);
3649         this.fmucHandler = new FMUCHandler(this);
3650 
3651         roomHistory = (MUCRoomHistory) ExternalizableUtil.getInstance().readSerializable(in);
3652         role = (MUCRole) ExternalizableUtil.getInstance().readSerializable(in);
3653     }
3654 
updateConfiguration(MUCRoom otherRoom)3655     public void updateConfiguration(MUCRoom otherRoom) {
3656         startTime = otherRoom.startTime;
3657         lockedTime = otherRoom.lockedTime;
3658         owners = otherRoom.owners;
3659         admins = otherRoom.admins;
3660         members = otherRoom.members;
3661         outcasts = otherRoom.outcasts;
3662         naturalLanguageName = otherRoom.naturalLanguageName;
3663         description = otherRoom.description;
3664         canOccupantsChangeSubject = otherRoom.canOccupantsChangeSubject;
3665         maxUsers = otherRoom.maxUsers;
3666         rolesToBroadcastPresence = otherRoom.rolesToBroadcastPresence;
3667         publicRoom = otherRoom.publicRoom;
3668         persistent = otherRoom.persistent;
3669         moderated = otherRoom.moderated;
3670         membersOnly = otherRoom.membersOnly;
3671         canOccupantsInvite = otherRoom.canOccupantsInvite;
3672         password = otherRoom.password;
3673         canAnyoneDiscoverJID = otherRoom.canAnyoneDiscoverJID;
3674         logEnabled = otherRoom.logEnabled;
3675         loginRestrictedToNickname = otherRoom.loginRestrictedToNickname;
3676         canChangeNickname = otherRoom.canChangeNickname;
3677         registrationEnabled = otherRoom.registrationEnabled;
3678         fmucHandler = otherRoom.fmucHandler;
3679         subject = otherRoom.subject;
3680         roomID = otherRoom.roomID;
3681         creationDate = otherRoom.creationDate;
3682         modificationDate = otherRoom.modificationDate;
3683         emptyDate = otherRoom.emptyDate;
3684         savedToDB = otherRoom.savedToDB;
3685         mucService = otherRoom.mucService;
3686     }
3687 
3688     @Override
toString()3689     public String toString() {
3690         return "MUCRoom{" +
3691             "roomID=" + roomID +
3692             ", name='" + name + '\'' +
3693             ", occupants=" + occupants.size() +
3694             ", mucService=" + mucService +
3695             ", savedToDB=" + savedToDB +
3696             '}';
3697     }
3698 
3699     /*
3700      * (non-Javadoc)
3701      * @see org.jivesoftware.util.resultsetmanager.Result#getUID()
3702      */
3703     @Override
getUID()3704     public String getUID()
3705     {
3706         // name is unique for each one particular MUC service.
3707         return name;
3708     }
3709 
3710     @Override
hashCode()3711     public int hashCode() {
3712         final int prime = 31;
3713         int result = 1;
3714         result = prime * result
3715             + ((creationDate == null) ? 0 : creationDate.hashCode());
3716         result = prime * result
3717             + ((description == null) ? 0 : description.hashCode());
3718         result = prime * result + ((name == null) ? 0 : name.hashCode());
3719         result = prime * result
3720             + ((password == null) ? 0 : password.hashCode());
3721         result = prime * result + (int) (roomID ^ (roomID >>> 32));
3722         return result;
3723     }
3724 
3725     @Override
equals(Object obj)3726     public boolean equals(Object obj) {
3727         if (this == obj)
3728             return true;
3729         if (obj == null)
3730             return false;
3731         if (getClass() != obj.getClass())
3732             return false;
3733         MUCRoom other = (MUCRoom) obj;
3734         if (creationDate == null) {
3735             if (other.creationDate != null)
3736                 return false;
3737         } else if (!creationDate.equals(other.creationDate))
3738             return false;
3739         if (description == null) {
3740             if (other.description != null)
3741                 return false;
3742         } else if (!description.equals(other.description))
3743             return false;
3744         if (name == null) {
3745             if (other.name != null)
3746                 return false;
3747         } else if (!name.equals(other.name))
3748             return false;
3749         if (password == null) {
3750             if (other.password != null)
3751                 return false;
3752         } else if (!password.equals(other.password))
3753             return false;
3754         return roomID==other.roomID;
3755     }
3756 
3757     // overrides for important Group events
3758 
3759     @Override
groupDeleting(Group group, Map params)3760     public void groupDeleting(Group group, Map params) {
3761         // remove the group from this room's affiliations
3762         GroupJID groupJID = group.getJID();
3763         try {
3764             addNone(groupJID, getRole());
3765         } catch (Exception ex) {
3766             Log.error("Failed to remove deleted group from affiliation lists: " + groupJID, ex);
3767         }
3768     }
3769 
3770     @Override
groupModified(Group group, Map params)3771     public void groupModified(Group group, Map params) {
3772         // check the affiliation lists for the old group jid, replace with a new group jid
3773         if ("nameModified".equals(params.get("type"))) {
3774             GroupJID originalJID = (GroupJID) params.get("originalJID");
3775             GroupJID newJID = group.getJID();
3776             try {
3777                 if (owners.contains(originalJID)) {
3778                     addOwner(newJID, getRole());
3779                 } else if (admins.contains(originalJID)) {
3780                     addAdmin(newJID, getRole());
3781                 } else if (outcasts.contains(originalJID)) {
3782                     addOutcast(newJID, null, getRole());
3783                 } else if (members.containsKey(originalJID)) {
3784                     addMember(newJID, null, getRole());
3785                 }
3786                 addNone(originalJID, getRole());
3787             } catch (Exception ex) {
3788                 Log.error("Failed to update group affiliation for " + newJID, ex);
3789             }
3790         }
3791     }
3792 
3793     @Override
memberAdded(Group group, Map params)3794     public void memberAdded(Group group, Map params) {
3795         applyAffiliationChangeAndSendPresence(new JID((String)params.get("member")));
3796     }
3797 
3798     @Override
memberRemoved(Group group, Map params)3799     public void memberRemoved(Group group, Map params) {
3800         applyAffiliationChangeAndSendPresence(new JID((String)params.get("member")));
3801     }
3802 
3803     @Override
adminAdded(Group group, Map params)3804     public void adminAdded(Group group, Map params) {
3805         applyAffiliationChangeAndSendPresence(new JID((String)params.get("admin")));
3806     }
3807 
3808     @Override
adminRemoved(Group group, Map params)3809     public void adminRemoved(Group group, Map params) {
3810         applyAffiliationChangeAndSendPresence(new JID((String)params.get("admin")));
3811     }
3812 
applyAffiliationChangeAndSendPresence(JID groupMember)3813     private void applyAffiliationChangeAndSendPresence(JID groupMember) {
3814         List<Presence> presences = applyAffiliationChange(getRole(), groupMember, null);
3815         for (Presence presence : presences) {
3816             send(presence, this.getRole());
3817         }
3818     }
3819 
3820     @Override
groupCreated(Group group, Map params)3821     public void groupCreated(Group group, Map params) {
3822         // ignore
3823     }
3824 }
3825