diff --git a/i18n/src/main/resources/openfire_i18n.properties b/i18n/src/main/resources/openfire_i18n.properties index dd96dab6a6..782d19d530 100644 --- a/i18n/src/main/resources/openfire_i18n.properties +++ b/i18n/src/main/resources/openfire_i18n.properties @@ -225,6 +225,8 @@ tab.tab-groupchat.descr=Click to manage group chat settings sidebar.muc-room-affiliations.descr=Click to edit room permissions (affiliations) for users and groups sidebar.muc-room-federation=Federation sidebar.muc-room-federation.descr=Click to edit the room's FMUC settings + sidebar.muc-room-clear-chat=Clear Chat History + sidebar.muc-room-clear-chat.descr=Click to clear chat history sidebar.muc-room-delete=Delete Room sidebar.muc-room-delete.descr=Click to delete the room sidebar.muc-room-retirees=Retired Rooms @@ -829,6 +831,15 @@ muc.room.affiliations.room_admin=Room Admins muc.room.affiliations.room_member=Room Members muc.room.affiliations.room_outcast=Room Outcasts + +# Muc clear room chat history + +muc.room.clear_chat.title=Clear Chat History +muc.room.clear_chat.info=Are you sure you want to clear the current chat history for the room +muc.room.clear_chat.detail=from the system? +muc.room.clear_chat.clear_command=Clear Chat History +muc.room.clear_chat.retraction_fallback_msg=A request to retract a previous message to clear chat history was received, but your client does not support this. + # Muc room delete Page muc.room.delete.title=Destroy Room @@ -926,6 +937,7 @@ muc.room.summary.persistent=Persistent muc.room.summary.users=Users muc.room.summary.edit=Edit muc.room.summary.destroy=Destroy +muc.room.summary.cleared_chat=Cleared chat history muc.room.summary.no_room_in_group=No rooms in the Group Chat service. muc.room.summary.alt_persistent=Room is persistent muc.room.summary.alt_temporary=Room is temporary @@ -1361,6 +1373,7 @@ system_property.adminConsole.perUsernameAttemptResetInterval=Time frame before A system_property.xmpp.muc.muclumbus.v1-0.enabled=Determine is the multi-user chat "muclumbus" (v1.0) search feature is enabled. system_property.xmpp.muc.join.presence=Setting the presence send of participants joining in MUC rooms. system_property.xmpp.muc.join.self-presence-timeout=Maximum duration to wait for presence to be broadcast while joining a MUC room. +system_property.xmpp.muc.bulkretraction=Enable or disable the bulk retraction of messages in MUC rooms. system_property.ldap.authorizeField=Name of attribute in user's LDAP object used by the LDAP authorization policy. system_property.ldap.pagedResultsSize=The maximum number of records to retrieve from LDAP in a single page. \ The default value of -1 means rely on the paging of the LDAP server itself. \ diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCEventDispatcher.java b/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCEventDispatcher.java index 7753c99b61..ec2ea1c8fd 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCEventDispatcher.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCEventDispatcher.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2008 Jive Software, 2017-2021 Ignite Realtime Foundation. All rights reserved. + * Copyright (C) 2005-2008 Jive Software, 2017-2024 Ignite Realtime Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -122,6 +122,16 @@ public static void roomDestroyed(JID roomJID) { } } + public static void roomClearChatHistory(JID roomJID) { + for (MUCEventListener listener : listeners) { + try { + listener.roomClearChatHistory(roomJID); + } catch (Exception e) { + Log.warn("An exception occurred while dispatching a 'roomClearChatHistory' event for room {}!", roomJID, e); + } + } + } + public static void roomSubjectChanged(JID roomJID, JID user, String newSubject) { for (MUCEventListener listener : listeners) { try { diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCEventListener.java b/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCEventListener.java index c64acaac33..21e8e3d180 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCEventListener.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2008 Jive Software, 2017-2021 Ignite Realtime Foundation. All rights reserved. + * Copyright (C) 2005-2008 Jive Software, 2017-2024 Ignite Realtime Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,13 @@ public interface MUCEventListener { */ void roomDestroyed(JID roomJID); + /** + * Event triggered when a clear chat history command was issued. + * + * @param roomJID JID of the room to clear chat history. + */ + void roomClearChatHistory(JID roomJID); + /** * Event triggered when a new occupant joins a room. * diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCRoom.java b/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCRoom.java index 43537efcc5..fbe71799e8 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCRoom.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCRoom.java @@ -65,7 +65,6 @@ */ @JiveID(JiveConstants.MUC_ROOM) public class MUCRoom implements GroupEventListener, UserEventListener, Externalizable, Result, Cacheable { - private static final Logger Log = LoggerFactory.getLogger(MUCRoom.class); public static final SystemProperty JOIN_PRESENCE_ENABLE = SystemProperty.Builder.ofType(Boolean.class) @@ -87,6 +86,12 @@ public class MUCRoom implements GroupEventListener, UserEventListener, Externali .setDynamic(true) .build(); + public static final SystemProperty BULK_MSG_RETRACTION_ENABLED = SystemProperty.Builder.ofType(Boolean.class) + .setKey("xmpp.muc.bulkretraction") + .setDynamic(true) + .setDefaultValue(false) + .build(); + /** * The service hosting the room. */ @@ -1314,6 +1319,55 @@ public void removeOccupant(@Nonnull final MUCOccupant occupant) { MUCEventDispatcher.occupantLeft(occupant.getOccupantJID(), occupant.getUserAddress(), occupant.getNickname()); } + /** + * Asynchronously remove the chat history of a room from run-time memory and database storage. + * If bulk message retraction is enabled, it sends message retraction stanzas to all occupants. + */ + public CompletableFuture clearChatHistory() { + return CompletableFuture.runAsync(() -> { + if(BULK_MSG_RETRACTION_ENABLED.getValue()) { + ListIterator reverseMessageHistory = roomHistory.getReverseMessageHistory(); + while (reverseMessageHistory.hasPrevious()) { + Message originalMsg = reverseMessageHistory.previous(); + Message retractionMsg = new Message(); + + // Sets a unique ID for the retraction message by prefixing the original message ID with "retract-" + retractionMsg.setID("retract-" + originalMsg.getID()); + // Sets the sender of the retraction message to be the chat room itself + retractionMsg.setFrom(selfOccupantData.getOccupantJID()); + // Sets the recipient of the retraction message to be the chat room itself to send to all occupants + retractionMsg.setTo(new JID(getName(), getMUCService().getServiceDomain(), null).toBareJID()); + retractionMsg.setType(Message.Type.groupchat); + // An XML element is added to the message to indicate that it is a retraction, with an attribute specifying + // the ID of the message being retracted + retractionMsg.addChildElement("retract", "urn:xmpp:message-retract:1").addAttribute( + "id", + new JID(getName(), getMUCService().getServiceDomain(), null).toBareJID() + ); + // A fallback element is added to provide a fallback message for clients that do not support message retraction + retractionMsg.addChildElement("fallback", "urn:xmpp:fallback:0").addAttribute( + "for", + "urn:xmpp:message-retract:1" + ); + retractionMsg.setBody(LocaleUtils.getLocalizedString("muc.room.clear_chat.retraction_fallback_msg")); + // Finally, a hint is added to the message to indicate that it should be stored by the client. + // This ensures that the retraction event is recorded and can be referenced later. + retractionMsg.addChildElement("store", "urn:xmpp:hints"); + + // Broadcast the retraction message but don't store it in the history + broadcast(retractionMsg, false); + } + } + + // Clear the history of the room from the DB if the room was persistent + MUCPersistenceManager.clearRoomChatFromDB(this); + // Remove the history of the room from memory (preventing it to pop up in a new room by the same name). + roomHistory.purge(); + // Fire event to clear chat room history + MUCEventDispatcher.roomClearChatHistory(getJID()); + }); + } + /** * Destroys the room. Each occupant will be removed and will receive a presence stanza of type * "unavailable" whose "from" attribute will be the occupant's nickname that the user knows he @@ -1390,8 +1444,12 @@ public void destroyRoom(JID alternateJID, String password, String reason) { MUCPersistenceManager.deleteFromDB(this, alternateJID, reason); // Remove the history of the room from memory (preventing it to pop up in a new room by the same name). roomHistory.purge(); + // If we are not preserving room history on deletion, fire event to clear chat room history + if(!preserveHistOnRoomDeletion) { + MUCEventDispatcher.roomClearChatHistory(getJID()); + } // Fire event that the room has been destroyed - MUCEventDispatcher.roomDestroyed(getSelfRepresentation().getOccupantJID()); + MUCEventDispatcher.roomDestroyed(getJID()); } /** @@ -1762,6 +1820,11 @@ private void broadcast(@Nonnull final Message message, @Nonnull final MUCOccupan * @param message The message stanza */ public void broadcast(@Nonnull final Message message) + { + broadcast(message, true); + } + + private void broadcast(@Nonnull final Message message, final boolean storeMsgInRoomHistory) { Log.debug("Broadcasting message in room {} for occupant {}", this.getName(), message.getFrom() ); @@ -1774,7 +1837,7 @@ public void broadcast(@Nonnull final Message message) } // Add message to the room history - roomHistory.addMessage(message); + if (storeMsgInRoomHistory) roomHistory.addMessage(message); // Send message to occupants connected to this JVM // Create a defensive copy of the message that will be broadcast, as the broadcast will modify it ('to' addresses @@ -1795,7 +1858,7 @@ public void broadcast(@Nonnull final Message message) Log.warn("An unexpected exception prevented a message from {} to be broadcast to {}.", message.getFrom(), occupant.getUserAddress(), e); } } - if (isLogEnabled()) { + if (isLogEnabled() && storeMsgInRoomHistory) { JID senderAddress = getSelfRepresentation().getOccupantJID(); // default to the room being the sender of the message. // convert the MUC nickname/role JID back into a real user JID diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/muc/spi/MUCPersistenceManager.java b/xmppserver/src/main/java/org/jivesoftware/openfire/muc/spi/MUCPersistenceManager.java index 0160d93612..febd5f3953 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/muc/spi/MUCPersistenceManager.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/muc/spi/MUCPersistenceManager.java @@ -150,6 +150,9 @@ public class MUCPersistenceManager { "INSERT INTO ofMucConversationLog (roomID,messageID,sender,nickname,logTime,subject,body,stanza) VALUES (?,?,?,?,?,?,?,?)"; private static final String DELETE_ROOM_HISTORY = "DELETE FROM ofMucConversationLog WHERE roomID=?"; + // Clear the chat history for a room but don't clear the messages that set the room's subject + private static final String CLEAR_ROOM_CHAT_HISTORY = + "DELETE FROM ofMucConversationLog WHERE roomID=? AND subject IS NULL"; /* Map of subdomains to their associated properties */ private static ConcurrentHashMap propertyMaps = new ConcurrentHashMap<>(); @@ -607,6 +610,36 @@ public static void deleteFromDB(MUCRoom room, JID alternateJID, String reason) { } } + /** + * Clears the chat history for a room + * + * @param room the room to cleaer chat history from + */ + public static void clearRoomChatFromDB(MUCRoom room) { + Log.debug("Attempting to clear the chat history of room '{}' from the database.", room.getName()); + + if (!room.isPersistent() || !room.wasSavedToDB()) { + return; + } + Connection con = null; + PreparedStatement pstmt = null; + boolean abortTransaction = false; + try { + con = DbConnectionManager.getTransactionConnection(); + pstmt = con.prepareStatement(CLEAR_ROOM_CHAT_HISTORY); + pstmt.setLong(1, room.getID()); + pstmt.executeUpdate(); + } + catch (SQLException sqle) { + Log.error("A database error occurred while trying to delete room: {}", room.getName(), sqle); + abortTransaction = true; + } + finally { + DbConnectionManager.closeStatement(pstmt); + DbConnectionManager.closeTransactionConnection(con, abortTransaction); + } + } + /** * Loads the name of all the rooms that are in the database. * diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/muc/spi/OccupantManager.java b/xmppserver/src/main/java/org/jivesoftware/openfire/muc/spi/OccupantManager.java index 3232dc529b..971f0fb49b 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/muc/spi/OccupantManager.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/muc/spi/OccupantManager.java @@ -960,6 +960,11 @@ public void roomDestroyed(@Nonnull final JID roomJID) } } + @Override + public void roomClearChatHistory(JID roomJID) { + // Not used. + } + @Override public void messageReceived(JID roomJID, JID user, String nickname, Message message) { // Not used. diff --git a/xmppserver/src/main/resources/admin-sidebar.xml b/xmppserver/src/main/resources/admin-sidebar.xml index 3f08d15c28..ab0684d77e 100644 --- a/xmppserver/src/main/resources/admin-sidebar.xml +++ b/xmppserver/src/main/resources/admin-sidebar.xml @@ -505,6 +505,11 @@ url="muc-room-federation.jsp" description="${sidebar.muc-room-federation.descr}"/> + + + +<%-- + - + - Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + - + - Licensed under the Apache License, Version 2.0 (the "License"); + - you may not use this file except in compliance with the License. + - You may obtain a copy of the License at + - + - http://www.apache.org/licenses/LICENSE-2.0 + - + - Unless required by applicable law or agreed to in writing, software + - distributed under the License is distributed on an "AS IS" BASIS, + - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + - See the License for the specific language governing permissions and + - limitations under the License. + +--%> + +<%@ page import="org.jivesoftware.util.*, + org.jivesoftware.openfire.muc.MUCRoom, + java.net.URLEncoder" + errorPage="error.jsp" +%> +<%@ page import="org.xmpp.packet.JID" %> + +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> +<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> +<%@ taglib uri="admin" prefix="admin" %> + +<% webManager.init(request, response, session, application, out ); %> + +<% // Get parameters // + boolean cancel = request.getParameter("cancel") != null; + boolean clear = request.getParameter("clear") != null; + Cookie csrfCookie = CookieUtils.getCookie(request, "csrf"); + String csrfParam = ParamUtils.getParameter(request, "csrf"); + + if (clear) { + if (csrfCookie == null || csrfParam == null || !csrfCookie.getValue().equals(csrfParam)) { + clear = false; + } + } + csrfParam = StringUtils.randomString(15); + CookieUtils.setCookie(request, response, "csrf", csrfParam, -1); + pageContext.setAttribute("csrf", csrfParam); + + JID roomJID = new JID(ParamUtils.getParameter(request,"roomJID")); + String roomName = roomJID.getNode(); + + // Handle a cancel + if (cancel) { + response.sendRedirect("muc-room-edit-form.jsp?roomJID="+URLEncoder.encode(roomJID.toBareJID(), "UTF-8")); + return; + } + + // Load the room object + MUCRoom room = webManager.getMultiUserChatManager().getMultiUserChatService(roomJID).getChatRoom(roomName); + + // Handle a room clear: + if (clear) { + // Clear the room + if (room != null) { + // finalWebManager is a final variable that references webManager + // allowing webManager to be used inside the lambda expression without causing a compilation error + final WebManager finalWebManager = webManager; + room.clearChatHistory().thenRun(() -> { + finalWebManager.logEvent("Cleared the chat history of MUC room ", roomJID.toString()); + }); + } + // Done, so redirect to the room edit form + response.sendRedirect("muc-room-edit-form.jsp?roomJID="+URLEncoder.encode(roomJID.toBareJID(), "UTF-8")+"&clearchatsuccess=true"); + return; + } + + pageContext.setAttribute("roomBareJid", roomJID.toBareJID()); +%> + + + + <fmt:message key="muc.room.clear_chat.title"/> + + + + +

+ + + +

+ +
+ + + +
+ + "> + "> +
+ + diff --git a/xmppserver/src/main/webapp/muc-room-edit-form.jsp b/xmppserver/src/main/webapp/muc-room-edit-form.jsp index fb2de49852..3e7df9ddba 100644 --- a/xmppserver/src/main/webapp/muc-room-edit-form.jsp +++ b/xmppserver/src/main/webapp/muc-room-edit-form.jsp @@ -33,7 +33,6 @@ %> <%@ page import="org.jivesoftware.openfire.muc.NotAllowedException"%> <%@ page import="org.jivesoftware.openfire.muc.spi.MUCPersistenceManager" %> -<%@ page import="org.jivesoftware.openfire.muc.MUCOccupant" %> <%@ page import="org.jivesoftware.openfire.muc.Role" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> @@ -50,6 +49,7 @@ boolean save = ParamUtils.getBooleanParameter(request,"save"); boolean success = ParamUtils.getBooleanParameter(request,"success"); boolean addsuccess = ParamUtils.getBooleanParameter(request,"addsuccess"); + boolean clearchatsuccess = ParamUtils.getBooleanParameter(request,"clearchatsuccess"); String roomName = ParamUtils.getParameter(request,"roomName"); String mucName = ParamUtils.getParameter(request,"mucName"); String roomJIDStr = ParamUtils.getParameter(request,"roomJID"); @@ -360,6 +360,7 @@ pageContext.setAttribute("create", create); pageContext.setAttribute("success", success); pageContext.setAttribute("addsuccess", addsuccess); + pageContext.setAttribute("clearchatsuccess", clearchatsuccess); pageContext.setAttribute("room", room); pageContext.setAttribute("roomJID", roomJID); pageContext.setAttribute("roomJIDBare", roomJID != null ? roomJID.toBareJID() : null); @@ -447,6 +448,11 @@ + + + + +