package com.reucon.openfire.plugin.archive.xep0313; import java.text.ParseException; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; import org.dom4j.*; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.disco.ServerFeaturesProvider; import org.jivesoftware.openfire.forward.Forwarded; import org.jivesoftware.openfire.handler.IQHandler; import org.jivesoftware.openfire.muc.MUCRole; import org.jivesoftware.openfire.muc.MUCRoom; import org.jivesoftware.openfire.muc.MultiUserChatService; import org.jivesoftware.openfire.session.Session; import org.jivesoftware.util.XMPPDateTimeFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.forms.DataForm; import org.xmpp.forms.FormField; import org.xmpp.packet.*; import com.reucon.openfire.plugin.archive.model.ArchivedMessage; import com.reucon.openfire.plugin.archive.xep.AbstractIQHandler; import com.reucon.openfire.plugin.archive.xep0059.XmppResultSet; /** * XEP-0313 IQ Query Handler */ abstract class IQQueryHandler extends AbstractIQHandler implements ServerFeaturesProvider { private static final Logger Log = LoggerFactory.getLogger(IQHandler.class); protected final String NAMESPACE; private final XMPPDateTimeFormat xmppDateTimeFormat = new XMPPDateTimeFormat(); IQQueryHandler(final String moduleName, final String namespace) { super(moduleName, "query", namespace); NAMESPACE = namespace; } public IQ handleIQ(IQ packet) throws UnauthorizedException { Session session = sessionManager.getSession(packet.getFrom()); // If no session was found then answer with an error (if possible) if (session == null) { Log.error("Error during resource binding. Session not found in " + sessionManager.getPreAuthenticatedKeys() + " for key " + packet.getFrom()); return buildErrorResponse(packet); } if(packet.getType().equals(IQ.Type.get)) { return buildSupportedFieldsResult(packet, session); } // Default to user's own archive 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)"); if (XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService(archiveJid) == null) { Log.debug("No chat service for this domain"); return buildErrorResponse(packet); } else { muc = true; Log.debug("MUC"); } } JID requestor = packet.getFrom().asBareJID(); Log.debug("Requestor is {} for muc=={}", requestor, muc); // Auth checking. if(muc) { MultiUserChatService service = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatService(archiveJid); MUCRoom room = service.getChatRoom(archiveJid.getNode()); if (room == null) { return buildErrorResponse(packet); } boolean pass = false; if (service.isSysadmin(requestor)) { pass = true; } MUCRole.Affiliation aff = room.getAffiliation(requestor); if (aff != MUCRole.Affiliation.outcast) { if (aff == MUCRole.Affiliation.owner || aff == MUCRole.Affiliation.admin) { pass = true; } else if (room.isMembersOnly()) { if (aff == MUCRole.Affiliation.member) { pass = true; } } else { pass = true; } } if (!pass) { return buildForbiddenResponse(packet); } } else if(!archiveJid.equals(requestor)) { // Not user's own // ... disallow unless admin. if (!XMPPServer.getInstance().getAdmins().contains(requestor)) { return buildForbiddenResponse(packet); } } sendMidQuery(packet, session); final QueryRequest queryRequest = new QueryRequest(packet.getChildElement(), archiveJid); Collection<ArchivedMessage> archivedMessages = retrieveMessages(queryRequest); for(ArchivedMessage archivedMessage : archivedMessages) { sendMessageResult(session, queryRequest, archivedMessage); } sendEndQuery(packet, session, queryRequest); 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 * @param packet IQ stanza received * @return IQ stanza to be sent. */ private IQ buildErrorResponse(IQ packet) { IQ reply = IQ.createResultIQ(packet); reply.setChildElement(packet.getChildElement().createCopy()); reply.setError(PacketError.Condition.internal_server_error); return reply; } /** * Create error response due to forbidden request * @param packet Received request * @return */ private IQ buildForbiddenResponse(IQ packet) { IQ reply = IQ.createResultIQ(packet); reply.setChildElement(packet.getChildElement().createCopy()); reply.setError(PacketError.Condition.forbidden); return reply; } /** * Retrieve messages matching query request from server archive * @param queryRequest * @return */ private Collection<ArchivedMessage> retrieveMessages(QueryRequest queryRequest) { String withField = null; String startField = null; String endField = null; DataForm dataForm = queryRequest.getDataForm(); if(dataForm != null) { if(dataForm.getField("with") != null) { withField = dataForm.getField("with").getFirstValue(); } if(dataForm.getField("start") != null) { startField = dataForm.getField("start").getFirstValue(); } if(dataForm.getField("end") != null) { endField = dataForm.getField("end").getFirstValue(); } } Date startDate = null; Date endDate = null; try { if(startField != null) { startDate = xmppDateTimeFormat.parseString(startField); } if(endField != null) { endDate = xmppDateTimeFormat.parseString(endField); } } catch (ParseException e) { Log.error("Error parsing query date filters.", e); } return getPersistenceManager(queryRequest.getArchive()).findMessages( startDate, endDate, queryRequest.getArchive().toBareJID(), withField, queryRequest.getResultSet()); } /** * 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); 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"); } } session.process(finalMessage); } /** * Send archived message to requesting client * @param session Client session that send message to * @param queryRequest Query request made by client * @param archivedMessage Message to send to client * @return */ private void sendMessageResult(Session session, QueryRequest queryRequest, ArchivedMessage archivedMessage) { String stanzaText = archivedMessage.getStanza(); if(stanzaText == null || stanzaText.equals("")) { // 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(); messagePacket.setTo(session.getAddress()); Forwarded fwd; Document stanza; try { stanza = DocumentHelper.parseText(stanzaText); fwd = new Forwarded(stanza.getRootElement(), archivedMessage.getTime(), null); } catch (DocumentException 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 return; } if (fwd == null) return; // Shouldn't be possible. messagePacket.addExtension(new Result(fwd, NAMESPACE, queryRequest.getQueryid(), archivedMessage.getId().toString())); session.process(messagePacket); } /** * Declare DataForm fields supported by the MAM implementation on this server * @param packet Incoming query (form field request) packet * @param session Session with client */ private IQ buildSupportedFieldsResult(IQ packet, Session session) { IQ result = IQ.createResultIQ(packet); Element query = result.setChildElement("query", NAMESPACE); DataForm form = new DataForm(DataForm.Type.form); form.addField("FORM_TYPE", null, FormField.Type.hidden); form.getField("FORM_TYPE").addValue(NAMESPACE); form.addField("with", null, FormField.Type.jid_single); form.addField("start", null, FormField.Type.text_single); form.addField("end", null, FormField.Type.text_single); query.add(form.getElement()); return result; } @Override public Iterator<String> getFeatures() { 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"); } } } }