/* * Copyright 2016 Anno van Vliet * * 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. */ package org.jivesoftware.openfire.plugin; import java.io.InputStream; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.Namespace; import org.dom4j.QName; import org.dom4j.io.DOMReader; import org.jivesoftware.openfire.OfflineMessage; import org.jivesoftware.openfire.OfflineMessageStore; import org.jivesoftware.openfire.PrivateStorage; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.auth.AuthFactory; import org.jivesoftware.openfire.roster.RosterItem; import org.jivesoftware.openfire.roster.RosterItem.AskType; import org.jivesoftware.openfire.roster.RosterItem.RecvType; import org.jivesoftware.openfire.roster.RosterItem.SubType; import org.jivesoftware.openfire.roster.RosterItemProvider; import org.jivesoftware.openfire.roster.RosterManager; import org.jivesoftware.openfire.user.User; import org.jivesoftware.openfire.user.UserAlreadyExistsException; import org.jivesoftware.openfire.user.UserManager; import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.openfire.vcard.VCardManager; import org.jivesoftware.util.XMPPDateTimeFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.packet.JID; import gnu.inet.encoding.Stringprep; import gnu.inet.encoding.StringprepException; /** * A In and Exporter which conforms to XEP-0227. * * @author Anno van Vliet * */ public class Xep227Exporter implements InExporter { /** * constants defining field and attribute names */ private static final String V_CARD_NAME = "vCard"; private static final String ASK_SUBSCRIBE_ENUM = "subscribe"; private static final String STAMP_NAME = "stamp"; private static final String DELAY_ELEMENT_NAME = "delay"; private static final String FROM_NAME = "from"; private static final String MESSAGE_ELEMENT_NAME = "message"; private static final String OFFLINE_MESSAGES_ELEMENT_NAME = "offline-messages"; private static final String GROUP_ELEMENT_NAME = "group"; private static final String SUBSCRIPTION_NAME = "subscription"; private static final String ASK_NAME = "ask"; private static final String ITEM_ELEMENT_NAME = "item"; private static final String QUERY_ELEMENT_NAME = "query"; private static final String PASSWORD_NAME = "password"; private static final String NAME_NAME = "name"; private static final String USER_ELEMENT_NAME = "user"; private static final String JID_NAME = "jid"; private static final String HOST_ELEMENT_NAME = "host"; private static final String SERVER_DATA_ELEMENT_NAME = "server-data"; /** * the relevant namespaces */ private static final String JABBER_CLIENT_NS = "jabber:client"; private static final String VCARD_TEMP_NS = "vcard-temp"; private static final String JABBER_IQ_ROSTER_NS = "jabber:iq:roster"; private static final String URN_XMPP_PIE_0_NS = "urn:xmpp:pie:0"; private static final Namespace JABBER_MSG_NS = new Namespace("", JABBER_CLIENT_NS); private static final Logger Log = LoggerFactory.getLogger(Xep227Exporter.class); private final String serverName; private final OfflineMessageStore offlineMessagesStore; private final VCardManager vCardManager; //private final PrivateStorage privateStorage; private final UserManager userManager; private final RosterItemProvider rosterItemProvider; private final DateFormat dateformater = new SimpleDateFormat(XMPPDateTimeFormat.XMPP_DATETIME_FORMAT, Locale.US); /** */ public Xep227Exporter() { offlineMessagesStore = XMPPServer.getInstance() .getOfflineMessageStore(); serverName = XMPPServer.getInstance().getServerInfo().getXMPPDomain(); //TODO not yet implemented //privateStorage = XMPPServer.getInstance().getPrivateStorage(); vCardManager = VCardManager.getInstance(); userManager = UserManager.getInstance(); rosterItemProvider = RosterManager.getRosterItemProvider(); } /** * Constructor used for testing purposes. * * @param serverName * @param offlineMessagesStore * @param vCardManager * @param privateStorage * @param userManager * @param rosterItemProvider */ public Xep227Exporter(String serverName, OfflineMessageStore offlineMessagesStore, VCardManager vCardManager, PrivateStorage privateStorage, UserManager userManager, RosterItemProvider rosterItemProvider) { super(); this.serverName = serverName; this.offlineMessagesStore = offlineMessagesStore; this.vCardManager = vCardManager; //this.privateStorage = privateStorage; this.userManager = userManager; this.rosterItemProvider = rosterItemProvider; } /* (non-Javadoc) * @see org.jivesoftware.openfire.plugin.Exporter#exportUsers(org.jivesoftware.openfire.user.UserManager) */ @Override public Document exportUsers() { Log.debug("exportUsers"); Document document = DocumentHelper.createDocument(); Element root = document.addElement(SERVER_DATA_ELEMENT_NAME, URN_XMPP_PIE_0_NS); Element host = root.addElement(HOST_ELEMENT_NAME); host.addAttribute(JID_NAME, serverName); Collection<User> users = userManager.getUsers(); for (User user : users) { String userName = user.getUsername(); Element userElement = host.addElement(USER_ELEMENT_NAME); exportUser(userElement, user); exportOfflineMessages(serverName, userElement, userName); exportRoster(userElement, user); exportVCard(userElement, userName); exportPrivateStorage(userName, userElement); } return document; } /** * Adding heading of an user and its parameters * * @param userElement * DOM element * @param user * User object */ private void exportUser(Element userElement, User user) { String userName = user.getUsername(); userElement.addAttribute(NAME_NAME, userName); try { String pw = AuthFactory.getPassword(userName); userElement.addAttribute(PASSWORD_NAME, pw ); } catch (UserNotFoundException e) { Log.info("User " + userName + " not found, setting their password to their username"); userElement.addAttribute(PASSWORD_NAME, userName); } catch (UnsupportedOperationException e) { Log.info("Unable to retrieve " + userName + " password, setting their password to their username"); userElement.addAttribute(PASSWORD_NAME, userName); } } /** * Add roster and its groups to a DOM element * * @param userElement * DOM element * @param user * User */ private void exportRoster(Element userElement, User user) { Element rosterElement = userElement.addElement(QUERY_ELEMENT_NAME, JABBER_IQ_ROSTER_NS); Collection<RosterItem> roster = user.getRoster().getRosterItems(); for (RosterItem ri : roster) { Element itemElement = rosterElement.addElement(ITEM_ELEMENT_NAME); itemElement.addAttribute(JID_NAME, ri.getJid().toBareJID()); itemElement.addAttribute(NAME_NAME, ri.getNickname()); itemElement.addAttribute(SUBSCRIPTION_NAME, ri.getSubStatus() .getName()); if ( ri.getAskStatus() == AskType.SUBSCRIBE ) { itemElement.addAttribute(ASK_NAME, ASK_SUBSCRIBE_ENUM); } /** * Adding groups */ Element groupElement = itemElement.addElement(GROUP_ELEMENT_NAME); List<String> groups = ri.getGroups(); for (String group : groups) { groupElement.addText(group); } } } /** * Adding offline messages, if there are some. * * @param hostname * host name * @param userElement * DOM element * @param userName * user name */ @SuppressWarnings("unchecked") private void exportOfflineMessages(String hostname, Element userElement, String userName) { Collection<OfflineMessage> offlineMessages = offlineMessagesStore .getMessages(userName, false); if (!offlineMessages.isEmpty()) { Element offlineElement = userElement.addElement(OFFLINE_MESSAGES_ELEMENT_NAME); for (OfflineMessage offMessage : offlineMessages) { Element messageElement = offlineElement.addElement(new QName(MESSAGE_ELEMENT_NAME, JABBER_MSG_NS)); for ( Object att : offMessage.getElement().attributes() ) { Attribute attribute = (Attribute) att; messageElement.addAttribute(attribute.getQName(),attribute.getValue()); } for (Iterator<Element> iterator = offMessage.getElement().elementIterator(); iterator .hasNext();) { Element element = iterator.next(); messageElement.add(element.createCopy(new QName(element.getName(), (element.getNamespace() == Namespace.NO_NAMESPACE ? JABBER_MSG_NS : element.getNamespace())))); } /** * Adding delay element */ Element delayElement = messageElement.addElement("delay", "urn:xmpp:delay"); delayElement.addAttribute(FROM_NAME, hostname); delayElement.addAttribute("stamp", XMPPDateTimeFormat.format(offMessage.getCreationDate())); delayElement.addText("Offline Storage"); } } } /** * Adding vcard element * * @param userElement * DOM element * @param userName * user name */ @SuppressWarnings("unchecked") private void exportVCard(Element userElement, String userName) { Element vCard = vCardManager.getVCard(userName); if (vCard != null) { Element vCardElement = userElement .addElement(V_CARD_NAME, VCARD_TEMP_NS); for (Iterator<Element> iterator = vCard.elementIterator(); iterator .hasNext();) { Element element = iterator.next(); vCardElement.add(element.createCopy()); } } } /** * Add all the private stored information (XEP-0049) * <b>Note: this method is not supported in the available openfire releases. * * </b> * @param userName User name * @param userElement User element */ private void exportPrivateStorage(String userName, Element userElement) { // Element result = privateStorage.getAll(userName); // if (result.elements().size() > 0) { // userElement.add(result.createCopy()); // } } /* (non-Javadoc) * @see org.jivesoftware.openfire.plugin.InExporter#validate() */ @Override public boolean validate(InputStream file) { Log.debug("validate"); org.w3c.dom.Document doc = new UserSchemaValidator(file, "pie.xsd", "jabber-iq-roster.xsd", "jabber-iq-private.xsd","xml.xsd","stanzaerror.xsd","jabber-client.xsd").validateAndParse(); return ( doc != null ); } /* (non-Javadoc) * @see org.jivesoftware.openfire.plugin.InExporter#importUsers(java.io.InputStream, java.lang.String, boolean) */ @Override public List<String> importUsers(InputStream inputStream, String previousDomain, boolean isUserProviderReadOnly) { Log.debug("importUsers"); DOMReader xmlReader = new DOMReader(); Document doc = xmlReader.read(new UserSchemaValidator(inputStream).validateAndParse()); return importUsers(doc, previousDomain, isUserProviderReadOnly); } /* (non-Javadoc) * @see org.jivesoftware.openfire.plugin.InExporter#importUsers(org.dom4j.Document, java.lang.String, boolean) */ @SuppressWarnings("unchecked") private List<String> importUsers(Document document, String previousDomain, boolean isUserProviderReadOnly) { List<String> invalidUsers = new ArrayList<String>(); Element hosts = document.getRootElement(); Iterator<Element> hostsIter = hosts.elementIterator(HOST_ELEMENT_NAME); while (hostsIter.hasNext()) { Element host = hostsIter.next(); Iterator<Element> usersIter = host.elementIterator(USER_ELEMENT_NAME); while (usersIter.hasNext()) { Element user = usersIter.next(); importUser(user,previousDomain,isUserProviderReadOnly,invalidUsers); } } return invalidUsers; } /** * @param user * @param previousDomain * @param isUserProviderReadOnly * @param invalidUsers */ @SuppressWarnings("unchecked") private void importUser(Element user, String previousDomain, boolean isUserProviderReadOnly, List<String> invalidUsers) { Log.debug("importUser"); List<RosterItem> rosterItems = new ArrayList<RosterItem>(); List<OfflineMessage> offlineMessages = new ArrayList<OfflineMessage>(); Element vCardElement = null; String userName = user.attributeValue(NAME_NAME); String password = user.attributeValue(PASSWORD_NAME); Iterator<Element> userElements = user.elementIterator(); while (userElements.hasNext()) { Element userElement = userElements.next(); String nameElement = userElement.getName(); if (OFFLINE_MESSAGES_ELEMENT_NAME.equals(nameElement)) { importOffLineMessages(userElement, offlineMessages); } else if (QUERY_ELEMENT_NAME.equals(nameElement) && JABBER_IQ_ROSTER_NS.equals(userElement.getNamespaceURI())) { importUserRoster(userElement, rosterItems, previousDomain); } else if (V_CARD_NAME.equals(nameElement) && VCARD_TEMP_NS.equals(userElement.getNamespaceURI())) { vCardElement = userElement; } } if ( userName != null ) { try { userName = Stringprep.nodeprep(userName); if (!isUserProviderReadOnly && (password != null) ) { userManager.createUser(userName, password, userName, null); } if ( !isUserProviderReadOnly && vCardElement != null ) { try { vCardManager.setVCard(userName, vCardElement); } catch (Exception e) { Log.warn("Error updating VCard:" + userName + ":" + e.getMessage()); Log.debug("", e); } } // Check to see user exists before adding their roster, this is for // read-only user providers. userManager.getUser(userName); for (RosterItem ri : rosterItems) { rosterItemProvider.createItem(userName, ri); } for (OfflineMessage offlineMessage : offlineMessages) { offlineMessagesStore.addMessage(offlineMessage); } } catch (StringprepException se) { Log.info("Invalid username " + userName); invalidUsers.add(userName); } catch (UserAlreadyExistsException e) { Log.info("User already exists " + userName); invalidUsers.add(userName); } catch (UserNotFoundException e) { Log.info("User not found " + userName); invalidUsers.add(userName); } catch (Exception e) { Log.warn("Error updating User:" + userName + ":" + e.getLocalizedMessage()); invalidUsers.add(userName); } } } /** * @param userElement * @param rosterItems * @param previousDomain */ @SuppressWarnings("unchecked") private void importUserRoster(Element userElement, List<RosterItem> rosterItems, String previousDomain) { Log.debug("importUserRoster"); Iterator<Element> rosterIter = userElement.elementIterator(ITEM_ELEMENT_NAME); while (rosterIter.hasNext()) { Element rosterElement = rosterIter.next(); String jid = rosterElement.attributeValue(JID_NAME); String nickname = rosterElement.attributeValue(NAME_NAME); String substatus = rosterElement.attributeValue(SUBSCRIPTION_NAME); String askstatus = rosterElement.attributeValue(ASK_NAME); List<String> groups = new ArrayList<String>(); Iterator<Element> groupIter = rosterElement.elementIterator(GROUP_ELEMENT_NAME); while (groupIter.hasNext()) { Element group = groupIter.next(); String groupName = group.getText(); if (groupName != null && groupName.trim().length() > 0) { groups.add(groupName); } } // used for migration if (previousDomain != null && jid != null ) { jid = jid.replace(previousDomain, serverName); } try { rosterItems.add(new RosterItem(new JID(jid), ( substatus != null ? SubType.valueOf(substatus.toUpperCase()) : SubType.BOTH ), ( ASK_SUBSCRIBE_ENUM.equals(askstatus) ? AskType.SUBSCRIBE : AskType.NONE), RecvType.NONE, nickname, groups)); } catch (Exception e) { Log.warn("Adding User Roster failed:" + e.getLocalizedMessage()); Log.debug("", e); } } } /** * @param userElement * @param offlineMessages */ @SuppressWarnings("unchecked") private void importOffLineMessages(Element userElement, List<OfflineMessage> offlineMessages) { Log.debug("importOffLineMessages"); // TODO Auto-generated method stub Iterator<Element> messageIter = userElement.elementIterator(MESSAGE_ELEMENT_NAME); while (messageIter.hasNext()) { Element msgElement = messageIter.next(); String creationDateStr = null; if (msgElement.element(DELAY_ELEMENT_NAME) != null) { creationDateStr = msgElement.element(DELAY_ELEMENT_NAME).attributeValue(STAMP_NAME); } Date creationDate = null; try { if (creationDateStr != null) { creationDate = dateformater.parse(creationDateStr); } } catch (ParseException e) { Log.warn("Date not parsable:" + e.getLocalizedMessage()); } offlineMessages.add(new OfflineMessage(creationDate, msgElement)); } } }