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