Commit bac0519e authored by Dave Cridland's avatar Dave Cridland Committed by GitHub

Merge pull request #670 from surevine/mam-update

OF-1214 Update MAM (XEP-0313) to support :0 and :1 versions
parents f363143c 6335c1c0
...@@ -2,21 +2,42 @@ package org.jivesoftware.openfire.forward; ...@@ -2,21 +2,42 @@ package org.jivesoftware.openfire.forward;
import org.dom4j.Element; import org.dom4j.Element;
import org.dom4j.QName; import org.dom4j.QName;
import org.jivesoftware.util.XMPPDateTimeFormat;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message; import org.xmpp.packet.Message;
import org.xmpp.packet.PacketExtension; import org.xmpp.packet.PacketExtension;
import java.util.Date;
/** /**
* @author Christian Schudt * @author Christian Schudt
*/ */
public class Forwarded extends PacketExtension { public class Forwarded extends PacketExtension {
public Forwarded(Element copy, Date delay, JID delayFrom) {
super("forwarded", "urn:xmpp:forward:0");
populate(copy, delay, delayFrom);
}
public Forwarded(Message message, Date delay, JID delayFrom) {
super("forwarded", "urn:xmpp:forward:0");
Message copy = message.createCopy();
populate(copy.getElement(), delay, delayFrom);
}
public Forwarded(Element copy) {
super("forwarded", "urn:xmpp:forward:0");
populate(copy, null, null);
}
public Forwarded(Message message) { public Forwarded(Message message) {
super("forwarded", "urn:xmpp:forward:0"); super("forwarded", "urn:xmpp:forward:0");
Message copy = message.createCopy(); Message copy = message.createCopy();
populate(copy.getElement(), null, null);
}
private void populate(Element copy, Date delay, JID delayFrom) {
copy.getElement().setQName(QName.get("message", "jabber:client")); copy.setQName(QName.get("message", "jabber:client"));
for (Object element : copy.getElement().elements()) { for (Object element : copy.elements()) {
if (element instanceof Element) { if (element instanceof Element) {
Element el = (Element) element; Element el = (Element) element;
// Only set the "jabber:client" namespace if the namespace is empty (otherwise the resulting xml would look like <body xmlns=""/>) // Only set the "jabber:client" namespace if the namespace is empty (otherwise the resulting xml would look like <body xmlns=""/>)
...@@ -25,6 +46,14 @@ public class Forwarded extends PacketExtension { ...@@ -25,6 +46,14 @@ public class Forwarded extends PacketExtension {
} }
} }
} }
element.add(copy.getElement()); if (delay != null) {
Element delayInfo = element.addElement("delay", "urn:xmpp:delay");
delayInfo.addAttribute("stamp", XMPPDateTimeFormat.format(delay));
if (delayFrom != null) {
// Set the Full JID as the "from" attribute
delayInfo.addAttribute("from", delayFrom.toString());
}
}
element.add(copy);
} }
} }
...@@ -129,11 +129,13 @@ public class JdbcPersistenceManager implements PersistenceManager { ...@@ -129,11 +129,13 @@ public class JdbcPersistenceManager implements PersistenceManager {
+ "ofMessageArchive.toJID, " + "ofMessageArchive.sentDate, " + "ofMessageArchive.stanza, " + "ofMessageArchive.toJID, " + "ofMessageArchive.sentDate, " + "ofMessageArchive.stanza, "
+ "ofMessageArchive.messageID, " + "ofConParticipant.bareJID " + "ofMessageArchive.messageID, " + "ofConParticipant.bareJID "
+ "FROM ofMessageArchive " + "FROM ofMessageArchive "
+ "INNER JOIN ofConParticipant ON ofMessageArchive.conversationID = ofConParticipant.conversationID "; + "INNER JOIN ofConParticipant ON ofMessageArchive.conversationID = ofConParticipant.conversationID "
+ "WHERE ofMessageArchive.stanza != NULL OR ofMessageArchive.body != NULL";
public static final String COUNT_MESSAGES = "SELECT COUNT(DISTINCT ofMessageArchive.messageID) " public static final String COUNT_MESSAGES = "SELECT COUNT(DISTINCT ofMessageArchive.messageID) "
+ "FROM ofMessageArchive " + "FROM ofMessageArchive "
+ "INNER JOIN ofConParticipant ON ofMessageArchive.conversationID = ofConParticipant.conversationID "; + "INNER JOIN ofConParticipant ON ofMessageArchive.conversationID = ofConParticipant.conversationID "
+ "WHERE ofMessageArchive.stanza != NULL OR ofMessageArchive.body != NULL";
public boolean createMessage(ArchivedMessage message) { public boolean createMessage(ArchivedMessage message) {
/* read only */ /* read only */
......
package com.reucon.openfire.plugin.archive.xep0313; package com.reucon.openfire.plugin.archive.xep0313;
import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.TimeZone;
import org.dom4j.*; import org.dom4j.*;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider; import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.jivesoftware.openfire.forward.Forwarded;
import org.jivesoftware.openfire.handler.IQHandler; import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.openfire.session.LocalClientSession; import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.XMPPDateTimeFormat; import org.jivesoftware.util.XMPPDateTimeFormat;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.xmpp.forms.DataForm; import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField; import org.xmpp.forms.FormField;
import org.xmpp.packet.IQ; import org.xmpp.packet.*;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.PacketError;
import com.reucon.openfire.plugin.archive.model.ArchivedMessage; import com.reucon.openfire.plugin.archive.model.ArchivedMessage;
import com.reucon.openfire.plugin.archive.xep.AbstractIQHandler; import com.reucon.openfire.plugin.archive.xep.AbstractIQHandler;
...@@ -31,22 +27,22 @@ import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet; ...@@ -31,22 +27,22 @@ import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet;
/** /**
* XEP-0313 IQ Query Handler * XEP-0313 IQ Query Handler
*/ */
public class IQQueryHandler extends AbstractIQHandler implements abstract class IQQueryHandler extends AbstractIQHandler implements
ServerFeaturesProvider { ServerFeaturesProvider {
private static final Logger Log = LoggerFactory.getLogger(IQHandler.class); private static final Logger Log = LoggerFactory.getLogger(IQHandler.class);
private static final String NAMESPACE = "urn:xmpp:mam:0"; protected final String NAMESPACE;
private static final String MODULE_NAME = "Message Archive Management Query Handler";
XMPPDateTimeFormat xmppDateTimeFormat = new XMPPDateTimeFormat(); private final XMPPDateTimeFormat xmppDateTimeFormat = new XMPPDateTimeFormat();
protected IQQueryHandler() { IQQueryHandler(final String moduleName, final String namespace) {
super(MODULE_NAME, "query", NAMESPACE); super(moduleName, "query", namespace);
NAMESPACE = namespace;
} }
public IQ handleIQ(IQ packet) throws UnauthorizedException { public IQ handleIQ(IQ packet) throws UnauthorizedException {
LocalClientSession session = (LocalClientSession) sessionManager.getSession(packet.getFrom()); Session session = sessionManager.getSession(packet.getFrom());
// If no session was found then answer with an error (if possible) // If no session was found then answer with an error (if possible)
if (session == null) { if (session == null) {
...@@ -58,21 +54,36 @@ public class IQQueryHandler extends AbstractIQHandler implements ...@@ -58,21 +54,36 @@ public class IQQueryHandler extends AbstractIQHandler implements
} }
if(packet.getType().equals(IQ.Type.get)) { if(packet.getType().equals(IQ.Type.get)) {
sendSupportedFieldsResult(packet, session); return buildSupportedFieldsResult(packet, session);
return null;
} }
// Default to user's own archive // Default to user's own archive
JID archiveJid = packet.getFrom(); JID archiveJid = packet.getTo();
if (archiveJid == null) {
archiveJid = packet.getFrom().asBareJID();
}
Log.debug("Archive requested is {}", archiveJid);
// Now decide the type.
boolean muc = false;
if (!XMPPServer.getInstance().isLocal(archiveJid)) {
Log.debug("Archive is not local (user)");
return buildErrorResponse(packet);
}
if(packet.getElement().attribute("to") != null) { JID requestor = packet.getFrom().asBareJID();
archiveJid = new JID(packet.getElement().attribute("to").getStringValue()); Log.debug("Requestor is {} for muc=={}", requestor, muc);
// Only allow queries to users own archives
if(!archiveJid.toBareJID().equals(packet.getFrom().toBareJID())) { // Auth checking.
if(!archiveJid.equals(requestor)) { // Not user's own
// ... disallow unless admin.
if (!XMPPServer.getInstance().getAdmins().contains(requestor)) {
return buildForbiddenResponse(packet); return buildForbiddenResponse(packet);
} }
} }
sendMidQuery(packet, session);
final QueryRequest queryRequest = new QueryRequest(packet.getChildElement(), archiveJid); final QueryRequest queryRequest = new QueryRequest(packet.getChildElement(), archiveJid);
Collection<ArchivedMessage> archivedMessages = retrieveMessages(queryRequest); Collection<ArchivedMessage> archivedMessages = retrieveMessages(queryRequest);
...@@ -80,17 +91,21 @@ public class IQQueryHandler extends AbstractIQHandler implements ...@@ -80,17 +91,21 @@ public class IQQueryHandler extends AbstractIQHandler implements
sendMessageResult(session, queryRequest, archivedMessage); sendMessageResult(session, queryRequest, archivedMessage);
} }
sendFinalMessage(session, queryRequest); sendEndQuery(packet, session, queryRequest);
sendAcknowledgementResult(packet, session);
return null; return null;
} }
protected void sendMidQuery(IQ packet, Session session) {
// Default: Do nothing.
}
protected abstract void sendEndQuery(IQ packet, Session session, QueryRequest queryRequest);
/** /**
* Create error response to send to client * Create error response to send to client
* @param packet * @param packet IQ stanza received
* @return * @return IQ stanza to be sent.
*/ */
private IQ buildErrorResponse(IQ packet) { private IQ buildErrorResponse(IQ packet) {
IQ reply = IQ.createResultIQ(packet); IQ reply = IQ.createResultIQ(packet);
...@@ -160,7 +175,7 @@ public class IQQueryHandler extends AbstractIQHandler implements ...@@ -160,7 +175,7 @@ public class IQQueryHandler extends AbstractIQHandler implements
* @param packet Received query packet * @param packet Received query packet
* @param session Client session to respond to * @param session Client session to respond to
*/ */
private void sendAcknowledgementResult(IQ packet, LocalClientSession session) { private void sendAcknowledgementResult(IQ packet, Session session) {
IQ result = IQ.createResultIQ(packet); IQ result = IQ.createResultIQ(packet);
session.process(result); session.process(result);
} }
...@@ -170,7 +185,7 @@ public class IQQueryHandler extends AbstractIQHandler implements ...@@ -170,7 +185,7 @@ public class IQQueryHandler extends AbstractIQHandler implements
* @param session Client session to respond to * @param session Client session to respond to
* @param queryRequest Received query request * @param queryRequest Received query request
*/ */
private void sendFinalMessage(LocalClientSession session, private void sendFinalMessage(Session session,
final QueryRequest queryRequest) { final QueryRequest queryRequest) {
Message finalMessage = new Message(); Message finalMessage = new Message();
...@@ -199,44 +214,37 @@ public class IQQueryHandler extends AbstractIQHandler implements ...@@ -199,44 +214,37 @@ public class IQQueryHandler extends AbstractIQHandler implements
* @param archivedMessage Message to send to client * @param archivedMessage Message to send to client
* @return * @return
*/ */
private void sendMessageResult(LocalClientSession session, private void sendMessageResult(Session session,
QueryRequest queryRequest, ArchivedMessage archivedMessage) { QueryRequest queryRequest, ArchivedMessage archivedMessage) {
if(archivedMessage.getStanza() == null) { String stanzaText = archivedMessage.getStanza();
// Don't send legacy archived messages (that have no stanza) if(stanzaText == null || stanzaText.equals("")) {
return; // Try creating a fake one from the body.
if (archivedMessage.getBody() != null && !archivedMessage.getBody().equals("")) {
stanzaText = String.format("<message from=\"{}\" to=\"{}\" type=\"chat\"><body>{}</body>", archivedMessage.getWithJid(), archivedMessage.getWithJid(), archivedMessage.getBody());
} else {
// Don't send legacy archived messages (that have no stanza)
return;
}
} }
Message messagePacket = new Message(); Message messagePacket = new Message();
messagePacket.setTo(session.getAddress()); messagePacket.setTo(session.getAddress());
Forwarded fwd;
Element result = messagePacket.addChildElement("result", NAMESPACE);
result.addAttribute("id", archivedMessage.getId().toString());
if(queryRequest.getQueryid() != null) {
result.addAttribute("queryid", queryRequest.getQueryid());
}
Element forwarded = result.addElement("forwarded", "urn:xmpp:forward:0");
Element delay = forwarded.addElement("delay", "urn:xmpp:delay");
delay.addAttribute("stamp", XMPPDateTimeFormat.format(archivedMessage.getTime()));
Document stanza; Document stanza;
try { try {
stanza = DocumentHelper.parseText(archivedMessage.getStanza()); stanza = DocumentHelper.parseText(stanzaText);
if ( stanza.getRootElement().getNamespaceURI() == null || stanza.getRootElement().getNamespaceURI().isEmpty() ) fwd = new Forwarded(stanza.getRootElement(), archivedMessage.getTime(), null);
{
// OF-1132: If no 'xmlns' is set for the stanza then as per XML namespacing rules it would inherit the
// 'urn:xmpp:forward:0' namespace, which is wrong (see XEP-0297).
stanza.getRootElement().setQName( QName.get( stanza.getRootElement().getName(), "jabber:client") );
}
forwarded.add(stanza.getRootElement());
} catch (DocumentException e) { } catch (DocumentException e) {
Log.error("Failed to parse message stanza.", e); Log.error("Failed to parse message stanza.", e);
// If we can't parse stanza then we have no message to send to client, abort // If we can't parse stanza then we have no message to send to client, abort
return; return;
} }
if (fwd == null) return; // Shouldn't be possible.
messagePacket.addExtension(new Result(fwd, NAMESPACE, queryRequest.getQueryid(), archivedMessage.getId().toString()));
session.process(messagePacket); session.process(messagePacket);
} }
...@@ -245,7 +253,7 @@ public class IQQueryHandler extends AbstractIQHandler implements ...@@ -245,7 +253,7 @@ public class IQQueryHandler extends AbstractIQHandler implements
* @param packet Incoming query (form field request) packet * @param packet Incoming query (form field request) packet
* @param session Session with client * @param session Session with client
*/ */
private void sendSupportedFieldsResult(IQ packet, LocalClientSession session) { private IQ buildSupportedFieldsResult(IQ packet, Session session) {
IQ result = IQ.createResultIQ(packet); IQ result = IQ.createResultIQ(packet);
...@@ -260,7 +268,7 @@ public class IQQueryHandler extends AbstractIQHandler implements ...@@ -260,7 +268,7 @@ public class IQQueryHandler extends AbstractIQHandler implements
query.add(form.getElement()); query.add(form.getElement());
session.process(result); return result;
} }
@Override @Override
...@@ -268,4 +276,18 @@ public class IQQueryHandler extends AbstractIQHandler implements ...@@ -268,4 +276,18 @@ public class IQQueryHandler extends AbstractIQHandler implements
return Collections.singleton(NAMESPACE).iterator(); return Collections.singleton(NAMESPACE).iterator();
} }
void completeFinElement(QueryRequest queryRequest, Element fin) {
if(queryRequest.getQueryid() != null) {
fin.addAttribute("queryid", queryRequest.getQueryid());
}
XmppResultSet resultSet = queryRequest.getResultSet();
if (resultSet != null) {
fin.add(resultSet.createResultElement());
if(resultSet.isComplete()) {
fin.addAttribute("complete", "true");
}
}
}
} }
package com.reucon.openfire.plugin.archive.xep0313;
import org.dom4j.*;
import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Message;
/**
* XEP-0313 IQ Query Handler
*/
class IQQueryHandler0 extends IQQueryHandler {
private static final Logger Log = LoggerFactory.getLogger(IQHandler.class);
private static final String MODULE_NAME = "Message Archive Management Query Handler v0";
IQQueryHandler0() {
super(MODULE_NAME, "urn:xmpp:mam:0");
}
@Override
protected void sendMidQuery(IQ packet, Session session) {
sendAcknowledgementResult(packet, session);
}
@Override
protected void sendEndQuery(IQ packet, Session session, QueryRequest queryRequest) {
sendFinalMessage(session, queryRequest);
}
/**
* Send result packet to client acknowledging query.
* @param packet Received query packet
* @param session Client session to respond to
*/
private void sendAcknowledgementResult(IQ packet, Session session) {
IQ result = IQ.createResultIQ(packet);
session.process(result);
}
/**
* Send final message back to client following query.
* @param session Client session to respond to
* @param queryRequest Received query request
*/
private void sendFinalMessage(Session session,
final QueryRequest queryRequest) {
Message finalMessage = new Message();
finalMessage.setTo(session.getAddress());
Element fin = finalMessage.addChildElement("fin", NAMESPACE);
completeFinElement(queryRequest, fin);
session.process(finalMessage);
}
}
package com.reucon.openfire.plugin.archive.xep0313;
import org.dom4j.*;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
/**
* XEP-0313 IQ Query Handler
*/
class IQQueryHandler1 extends IQQueryHandler {
private static final Logger Log = LoggerFactory.getLogger(IQQueryHandler1.class);
private static final String MODULE_NAME = "Message Archive Management Query Handler v1";
IQQueryHandler1() {
super(MODULE_NAME, "urn:xmpp:mam:1");
}
@Override
protected void sendEndQuery(IQ packet, Session session, QueryRequest queryRequest) {
sendAcknowledgementResult(packet, session, queryRequest);
}
/**
* Send result packet to client acknowledging query.
* @param packet Received query packet
* @param session Client session to respond to
*/
private void sendAcknowledgementResult(IQ packet, Session session, QueryRequest queryRequest) {
IQ result = IQ.createResultIQ(packet);
Element fin = result.setChildElement("fin", NAMESPACE);
completeFinElement(queryRequest, fin);
session.process(result);
}
}
package com.reucon.openfire.plugin.archive.xep0313;
import org.jivesoftware.openfire.forward.Forwarded;
import org.xmpp.packet.PacketExtension;
import java.util.Date;
/**
* Created by dwd on 26/07/16.
*/
public final class Result extends PacketExtension {
public Result(Forwarded forwarded, String xmlns, String queryId, String id) {
super("result", xmlns);
element.add(forwarded.getElement());
}
}
...@@ -18,8 +18,8 @@ public class Xep0313Support extends AbstractXepSupport { ...@@ -18,8 +18,8 @@ public class Xep0313Support extends AbstractXepSupport {
public Xep0313Support(XMPPServer server) { public Xep0313Support(XMPPServer server) {
super(server, NAMESPACE,NAMESPACE, "XEP-0313 IQ Dispatcher"); super(server, NAMESPACE,NAMESPACE, "XEP-0313 IQ Dispatcher");
this.iqHandlers = new ArrayList<IQHandler>(); this.iqHandlers = new ArrayList<>();
iqHandlers.add(new IQQueryHandler()); iqHandlers.add(new IQQueryHandler0());
} }
} }
package com.reucon.openfire.plugin.archive.xep0313;
import com.reucon.openfire.plugin.archive.xep.AbstractXepSupport;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.handler.IQHandler;
import java.util.ArrayList;
/**
* Encapsulates support for <a
* href="http://www.xmpp.org/extensions/xep-0313.html">XEP-0313</a>.
*/
public class Xep0313Support1 extends AbstractXepSupport {
private static final String NAMESPACE = "urn:xmpp:mam:1";
public Xep0313Support1(XMPPServer server) {
super(server, NAMESPACE,NAMESPACE, "XEP-0313 IQ Dispatcher");
this.iqHandlers = new ArrayList<>();
iqHandlers.add(new IQQueryHandler1());
}
}
...@@ -22,6 +22,7 @@ package org.jivesoftware.openfire.plugin; ...@@ -22,6 +22,7 @@ package org.jivesoftware.openfire.plugin;
import java.io.File; import java.io.File;
import java.io.FileFilter; import java.io.FileFilter;
import com.reucon.openfire.plugin.archive.xep0313.Xep0313Support1;
import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.archive.ArchiveIndexer; import org.jivesoftware.openfire.archive.ArchiveIndexer;
import org.jivesoftware.openfire.archive.ArchiveInterceptor; import org.jivesoftware.openfire.archive.ArchiveInterceptor;
...@@ -73,6 +74,7 @@ public class MonitoringPlugin implements Plugin { ...@@ -73,6 +74,7 @@ public class MonitoringPlugin implements Plugin {
private IndexManager indexManager; private IndexManager indexManager;
private Xep0136Support xep0136Support; private Xep0136Support xep0136Support;
private Xep0313Support xep0313Support; private Xep0313Support xep0313Support;
private Xep0313Support1 xep0313Support1;
public MonitoringPlugin() { public MonitoringPlugin() {
instance = this; instance = this;
...@@ -162,6 +164,9 @@ public class MonitoringPlugin implements Plugin { ...@@ -162,6 +164,9 @@ public class MonitoringPlugin implements Plugin {
xep0313Support = new Xep0313Support(XMPPServer.getInstance()); xep0313Support = new Xep0313Support(XMPPServer.getInstance());
xep0313Support.start(); xep0313Support.start();
xep0313Support1 = new Xep0313Support1(XMPPServer.getInstance());
xep0313Support1.start();
// Check if we Enterprise is installed and stop loading this plugin if // Check if we Enterprise is installed and stop loading this plugin if
// found // found
File pluginDir = new File(JiveGlobals.getHomeDirectory(), "plugins"); File pluginDir = new File(JiveGlobals.getHomeDirectory(), "plugins");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment