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