1 /* 2 * Copyright (C) 2005-2008 Jive Software. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.jivesoftware.openfire.handler; 18 19 import org.dom4j.DocumentHelper; 20 import org.dom4j.Element; 21 import org.jivesoftware.openfire.IQHandlerInfo; 22 import org.jivesoftware.openfire.OfflineMessage; 23 import org.jivesoftware.openfire.OfflineMessageStore; 24 import org.jivesoftware.openfire.RoutingTable; 25 import org.jivesoftware.openfire.XMPPServer; 26 import org.jivesoftware.openfire.auth.UnauthorizedException; 27 import org.jivesoftware.openfire.disco.DiscoInfoProvider; 28 import org.jivesoftware.openfire.disco.DiscoItem; 29 import org.jivesoftware.openfire.disco.DiscoItemsProvider; 30 import org.jivesoftware.openfire.disco.IQDiscoInfoHandler; 31 import org.jivesoftware.openfire.disco.IQDiscoItemsHandler; 32 import org.jivesoftware.openfire.disco.ServerFeaturesProvider; 33 import org.jivesoftware.openfire.session.LocalClientSession; 34 import org.jivesoftware.openfire.user.UserManager; 35 import org.jivesoftware.util.XMPPDateTimeFormat; 36 import org.slf4j.Logger; 37 import org.slf4j.LoggerFactory; 38 import org.xmpp.forms.DataForm; 39 import org.xmpp.forms.FormField; 40 import org.xmpp.packet.IQ; 41 import org.xmpp.packet.JID; 42 import org.xmpp.packet.PacketError; 43 44 import java.text.ParseException; 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.Date; 48 import java.util.HashSet; 49 import java.util.Iterator; 50 import java.util.List; 51 import java.util.Set; 52 53 /** 54 * Implements JEP-0013: Flexible Offline Message Retrieval. Allows users to request number of 55 * messages, request message headers, retrieve specific messages, remove specific messages, 56 * retrieve all messages and remove all messages. 57 * 58 * @author Gaston Dombiak 59 */ 60 public class IQOfflineMessagesHandler extends IQHandler implements ServerFeaturesProvider, 61 DiscoInfoProvider, DiscoItemsProvider { 62 63 private static final Logger Log = LoggerFactory.getLogger(IQOfflineMessagesHandler.class); 64 65 private static final String NAMESPACE = "http://jabber.org/protocol/offline"; 66 67 final private XMPPDateTimeFormat xmppDateTime = new XMPPDateTimeFormat(); 68 private IQHandlerInfo info; 69 private IQDiscoInfoHandler infoHandler; 70 private IQDiscoItemsHandler itemsHandler; 71 72 private RoutingTable routingTable; 73 private UserManager userManager; 74 private OfflineMessageStore messageStore; 75 IQOfflineMessagesHandler()76 public IQOfflineMessagesHandler() { 77 super("Flexible Offline Message Retrieval Handler"); 78 info = new IQHandlerInfo("offline", NAMESPACE); 79 } 80 81 @Override handleIQ(IQ packet)82 public IQ handleIQ(IQ packet) throws UnauthorizedException { 83 IQ reply = IQ.createResultIQ(packet); 84 Element offlineRequest = packet.getChildElement(); 85 86 JID from = packet.getFrom(); 87 if (offlineRequest.element("purge") != null) { 88 // User requested to delete all offline messages 89 messageStore.deleteMessages(from.getNode()); 90 } 91 else if (offlineRequest.element("fetch") != null) { 92 // Mark that offline messages shouldn't be sent when the user becomes available 93 stopOfflineFlooding(from); 94 // User requested to receive all offline messages 95 for (OfflineMessage offlineMessage : messageStore.getMessages(from.getNode(), false)) { 96 sendOfflineMessage(from, offlineMessage); 97 } 98 } 99 else { 100 for (Iterator it = offlineRequest.elementIterator("item"); it.hasNext();) { 101 Element item = (Element) it.next(); 102 Date creationDate = null; 103 try { 104 creationDate = xmppDateTime.parseString(item.attributeValue("node")); 105 } catch (ParseException e) { 106 Log.error("Error parsing date", e); 107 } 108 if ("view".equals(item.attributeValue("action"))) { 109 // User requested to receive specific message 110 OfflineMessage offlineMsg = messageStore.getMessage(from.getNode(), creationDate); 111 if (offlineMsg != null) { 112 sendOfflineMessage(from, offlineMsg); 113 } else { 114 // If the requester is authorized but the node does not exist, the server MUST return a <item-not-found/> error. 115 reply.setError(PacketError.Condition.item_not_found); 116 } 117 } 118 else if ("remove".equals(item.attributeValue("action"))) { 119 // User requested to delete specific message 120 if (messageStore.getMessage(from.getNode(), creationDate) != null) { 121 messageStore.deleteMessage(from.getNode(), creationDate); 122 } else { 123 // If the requester is authorized but the node does not exist, the server MUST return a <item-not-found/> error. 124 reply.setError(PacketError.Condition.item_not_found); 125 } 126 } 127 } 128 } 129 return reply; 130 } 131 sendOfflineMessage(JID receipient, OfflineMessage offlineMessage)132 private void sendOfflineMessage(JID receipient, OfflineMessage offlineMessage) { 133 Element offlineInfo = offlineMessage.addChildElement("offline", NAMESPACE); 134 offlineInfo.addElement("item").addAttribute("node", 135 XMPPDateTimeFormat.format(offlineMessage.getCreationDate())); 136 routingTable.routePacket(receipient, offlineMessage, true); 137 } 138 139 @Override getInfo()140 public IQHandlerInfo getInfo() { 141 return info; 142 } 143 144 @Override getFeatures()145 public Iterator<String> getFeatures() { 146 return Collections.singleton(NAMESPACE).iterator(); 147 } 148 149 @Override getIdentities(String name, String node, JID senderJID)150 public Iterator<Element> getIdentities(String name, String node, JID senderJID) { 151 Element identity = DocumentHelper.createElement("identity"); 152 identity.addAttribute("category", "automation"); 153 identity.addAttribute("type", "message-list"); 154 return Collections.singleton(identity).iterator(); 155 } 156 157 @Override getFeatures(String name, String node, JID senderJID)158 public Iterator<String> getFeatures(String name, String node, JID senderJID) { 159 return Collections.singleton(NAMESPACE).iterator(); 160 } 161 162 @Override getExtendedInfo(String name, String node, JID senderJID)163 public DataForm getExtendedInfo(String name, String node, JID senderJID) { 164 return IQDiscoInfoHandler.getFirstDataForm(this.getExtendedInfos(name, node, senderJID)); 165 } 166 @Override getExtendedInfos(String name, String node, JID senderJID)167 public Set<DataForm> getExtendedInfos(String name, String node, JID senderJID) { 168 // Mark that offline messages shouldn't be sent when the user becomes available 169 stopOfflineFlooding(senderJID); 170 171 final DataForm dataForm = new DataForm(DataForm.Type.result); 172 173 final FormField field1 = dataForm.addField(); 174 field1.setVariable("FORM_TYPE"); 175 field1.setType(FormField.Type.hidden); 176 field1.addValue(NAMESPACE); 177 178 final FormField field2 = dataForm.addField(); 179 field2.setVariable("number_of_messages"); 180 field2.addValue(String.valueOf(messageStore.getCount(senderJID.getNode()))); 181 182 final Set<DataForm> dataForms = new HashSet<>(); 183 dataForms.add(dataForm); 184 return dataForms; 185 } 186 187 @Override hasInfo(String name, String node, JID senderJID)188 public boolean hasInfo(String name, String node, JID senderJID) { 189 return NAMESPACE.equals(node) && userManager.isRegisteredUser(senderJID, false); 190 } 191 192 @Override getItems(String name, String node, JID senderJID)193 public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) { 194 // Mark that offline messages shouldn't be sent when the user becomes available 195 stopOfflineFlooding(senderJID); 196 List<DiscoItem> answer = new ArrayList<>(); 197 for (OfflineMessage offlineMessage : messageStore.getMessages(senderJID.getNode(), false)) { 198 answer.add(new DiscoItem(senderJID.asBareJID(), offlineMessage.getFrom().toString(), 199 XMPPDateTimeFormat.format(offlineMessage.getCreationDate()), null)); 200 } 201 202 return answer.iterator(); 203 } 204 205 @Override initialize(XMPPServer server)206 public void initialize(XMPPServer server) { 207 super.initialize(server); 208 infoHandler = server.getIQDiscoInfoHandler(); 209 itemsHandler = server.getIQDiscoItemsHandler(); 210 messageStore = server.getOfflineMessageStore(); 211 userManager = server.getUserManager(); 212 routingTable = server.getRoutingTable(); 213 } 214 215 @Override start()216 public void start() throws IllegalStateException { 217 super.start(); 218 infoHandler.setServerNodeInfoProvider(NAMESPACE, this); 219 itemsHandler.setServerNodeInfoProvider(NAMESPACE, this); 220 } 221 222 @Override stop()223 public void stop() { 224 super.stop(); 225 infoHandler.removeServerNodeInfoProvider(NAMESPACE); 226 itemsHandler.removeServerNodeInfoProvider(NAMESPACE); 227 } 228 stopOfflineFlooding(JID senderJID)229 private void stopOfflineFlooding(JID senderJID) { 230 LocalClientSession session = (LocalClientSession) sessionManager.getSession(senderJID); 231 if (session != null) { 232 session.setOfflineFloodStopped(true); 233 } 234 } 235 } 236