/**
*
* Copyright 2003-2007 Jive Software, 2019 Florian Schmaus.
*
* 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.smack.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import org.jivesoftware.smack.compress.packet.Compress;
import org.jivesoftware.smack.packet.EmptyResultIQ;
import org.jivesoftware.smack.packet.ErrorIQ;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Session;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smack.packet.StartTls;
import org.jivesoftware.smack.packet.StreamError;
import org.jivesoftware.smack.packet.UnparsedIQ;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.parsing.SmackParsingException;
import org.jivesoftware.smack.parsing.StandardExtensionElementProvider;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smack.provider.IQProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
import org.jivesoftware.smack.xml.SmackXmlParser;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jxmpp.jid.Jid;
import org.jxmpp.stringprep.XmppStringprepException;
/**
* Utility class that helps to parse packets. Any parsing packets method that must be shared
* between many clients must be placed in this utility class.
*
* @author Gaston Dombiak
*/
public class PacketParserUtils {
private static final Logger LOGGER = Logger.getLogger(PacketParserUtils.class.getName());
// TODO: Rename argument name from 'stanza' to 'element'.
public static XmlPullParser getParserFor(String stanza) throws XmlPullParserException, IOException {
return getParserFor(new StringReader(stanza));
}
public static XmlPullParser getParserFor(InputStream inputStream) throws XmlPullParserException {
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
return SmackXmlParser.newXmlParser(inputStreamReader);
}
public static XmlPullParser getParserFor(Reader reader) throws XmlPullParserException, IOException {
XmlPullParser parser = SmackXmlParser.newXmlParser(reader);
// Wind the parser forward to the first start tag
XmlPullParser.Event event = parser.getEventType();
while (event != XmlPullParser.Event.START_ELEMENT) {
if (event == XmlPullParser.Event.END_DOCUMENT) {
throw new IllegalArgumentException("Document contains no start tag");
}
event = parser.next();
}
return parser;
}
@SuppressWarnings("unchecked")
public static S parseStanza(String stanza) throws XmlPullParserException, SmackParsingException, IOException {
return (S) parseStanza(getParserFor(stanza), null);
}
/**
* Tries to parse and return either a Message, IQ or Presence stanza.
*
* connection is optional and is used to return feature-not-implemented errors for unknown IQ stanzas.
*
* @param parser
* @param outerXmlEnvironment the outer XML environment (optional).
* @return a stanza which is either a Message, IQ or Presence.
* @throws XmlPullParserException
* @throws SmackParsingException
* @throws IOException
*/
public static Stanza parseStanza(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, SmackParsingException, IOException {
ParserUtils.assertAtStartTag(parser);
final String name = parser.getName();
switch (name) {
case Message.ELEMENT:
return parseMessage(parser, outerXmlEnvironment);
case IQ.IQ_ELEMENT:
return parseIQ(parser, outerXmlEnvironment);
case Presence.ELEMENT:
return parsePresence(parser, outerXmlEnvironment);
default:
throw new IllegalArgumentException("Can only parse message, iq or presence, not " + name);
}
}
public static Message parseMessage(XmlPullParser parser) throws XmlPullParserException, IOException, SmackParsingException {
return parseMessage(parser, null);
}
/**
* Parses a message packet.
*
* @param parser the XML parser, positioned at the start of a message packet.
* @param outerXmlEnvironment the outer XML environment (optional).
* @return a Message packet.
* @throws XmlPullParserException
* @throws IOException
* @throws SmackParsingException
*/
public static Message parseMessage(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
ParserUtils.assertAtStartTag(parser);
assert parser.getName().equals(Message.ELEMENT);
XmlEnvironment messageXmlEnvironment = XmlEnvironment.from(parser, outerXmlEnvironment);
final int initialDepth = parser.getDepth();
Message message = new Message();
message.setStanzaId(parser.getAttributeValue("", "id"));
message.setTo(ParserUtils.getJidAttribute(parser, "to"));
message.setFrom(ParserUtils.getJidAttribute(parser, "from"));
String typeString = parser.getAttributeValue("", "type");
if (typeString != null) {
message.setType(Message.Type.fromString(typeString));
}
String language = ParserUtils.getXmlLang(parser);
message.setLanguage(language);
// Parse sub-elements. We include extra logic to make sure the values
// are only read once. This is because it's possible for the names to appear
// in arbitrary sub-elements.
String thread = null;
outerloop: while (true) {
XmlPullParser.Event eventType = parser.next();
switch (eventType) {
case START_ELEMENT:
String elementName = parser.getName();
String namespace = parser.getNamespace();
switch (elementName) {
case "subject":
String xmlLangSubject = ParserUtils.getXmlLang(parser);
String subject = parseElementText(parser);
if (message.getSubject(xmlLangSubject) == null) {
message.addSubject(xmlLangSubject, subject);
}
break;
case "thread":
if (thread == null) {
thread = parser.nextText();
}
break;
case "error":
message.setError(parseError(parser, messageXmlEnvironment));
break;
default:
PacketParserUtils.addExtensionElement(message, parser, elementName, namespace, messageXmlEnvironment);
break;
}
break;
case END_ELEMENT:
if (parser.getDepth() == initialDepth) {
break outerloop;
}
break;
default: // fall out
}
}
message.setThread(thread);
// TODO check for duplicate body elements. This means we need to check for duplicate xml:lang pairs and for
// situations where we have a body element with an explicit xml lang set and once where the value is inherited
// and both values are equal.
return message;
}
/**
* Returns the textual content of an element as String. After this method returns the parser
* position will be END_ELEMENT, following the established pull parser calling convention.
*
* The parser must be positioned on a START_ELEMENT of an element which MUST NOT contain Mixed * Content (as defined in XML 3.2.2), or else an XmlPullParserException will be thrown. *
* This method is used for the parts where the XMPP specification requires elements that contain * only text or are the empty element. * * @param parser * @return the textual content of the element as String * @throws XmlPullParserException * @throws IOException */ public static String parseElementText(XmlPullParser parser) throws XmlPullParserException, IOException { assert parser.getEventType() == XmlPullParser.Event.START_ELEMENT; String res; // Advance to the text of the Element XmlPullParser.Event event = parser.next(); if (event != XmlPullParser.Event.TEXT_CHARACTERS) { if (event == XmlPullParser.Event.END_ELEMENT) { // Assume this is the end tag of the start tag at the // beginning of this method. Typical examples where this // happens are body elements containing the empty string, // ie. , which appears to be valid XMPP, or a // least it's not explicitly forbidden by RFC 6121 5.2.3 return ""; } else { throw new XmlPullParserException( "Non-empty element tag not followed by text, while Mixed Content (XML 3.2.2) is disallowed"); } } res = parser.getText(); event = parser.next(); if (event != XmlPullParser.Event.END_ELEMENT) { throw new XmlPullParserException( "Non-empty element tag contains child-elements, while Mixed Content (XML 3.2.2) is disallowed"); } return res; } /** * Returns the current element as string. ** The parser must be positioned on START_ELEMENT. *
* Note that only the outermost namespace attributes ("xmlns") will be returned, not nested ones. * * @param parser the XML pull parser * @return the element as string * @throws XmlPullParserException * @throws IOException */ public static CharSequence parseElement(XmlPullParser parser) throws XmlPullParserException, IOException { return parseElement(parser, false); } public static CharSequence parseElement(XmlPullParser parser, boolean fullNamespaces) throws XmlPullParserException, IOException { assert parser.getEventType() == XmlPullParser.Event.START_ELEMENT; return parseContentDepth(parser, parser.getDepth(), fullNamespaces); } public static CharSequence parseContentDepth(XmlPullParser parser, int depth) throws XmlPullParserException, IOException { return parseContentDepth(parser, depth, false); } /** * Returns the content from the current position of the parser up to the closing tag of the * given depth. Note that only the outermost namespace attributes ("xmlns") will be returned, * not nested ones, iffullNamespaces
is false. If it is true, then namespaces of
* parent elements will be added to child elements that don't define a different namespace.
*
* This method is able to parse the content with MX- and KXmlParser. KXmlParser does not support
* xml-roundtrip. i.e. return a String on getText() on START_ELEMENT and END_ELEMENT. We check for the
* XML_ROUNDTRIP feature. If it's not found we are required to work around this limitation, which
* results in only partial support for XML namespaces ("xmlns"): Only the outermost namespace of
* elements will be included in the resulting String, if fullNamespaces
is set to false.
*
* In particular Android's XmlPullParser does not support XML_ROUNDTRIP. *
* * @param parser * @param depth * @param fullNamespaces * @return the content of the current depth * @throws XmlPullParserException * @throws IOException */ public static CharSequence parseContentDepth(XmlPullParser parser, int depth, boolean fullNamespaces) throws XmlPullParserException, IOException { if (parser.supportsRoundtrip()) { return parseContentDepthWithRoundtrip(parser, depth, fullNamespaces); } else { return parseContentDepthWithoutRoundtrip(parser, depth, fullNamespaces); } } private static CharSequence parseContentDepthWithoutRoundtrip(XmlPullParser parser, int depth, boolean fullNamespaces) throws XmlPullParserException, IOException { XmlStringBuilder xml = new XmlStringBuilder(); XmlPullParser.Event event = parser.getEventType(); // XmlPullParser reports namespaces in nested elements even if *only* the outer ones defines // it. This 'flag' ensures that when a namespace is set for an element, it won't be set again // in a nested element. It's an ugly workaround that has the potential to break things. String namespaceElement = null; boolean startElementJustSeen = false; outerloop: while (true) { switch (event) { case START_ELEMENT: if (startElementJustSeen) { xml.rightAngleBracket(); } else { startElementJustSeen = true; } xml.halfOpenElement(parser.getName()); if (namespaceElement == null || fullNamespaces) { String namespace = parser.getNamespace(); if (StringUtils.isNotEmpty(namespace)) { xml.attribute("xmlns", namespace); namespaceElement = parser.getName(); } } for (int i = 0; i < parser.getAttributeCount(); i++) { xml.attribute(parser.getAttributeName(i), parser.getAttributeValue(i)); } break; case END_ELEMENT: if (startElementJustSeen) { xml.closeEmptyElement(); startElementJustSeen = false; } else { xml.closeElement(parser.getName()); } if (namespaceElement != null && namespaceElement.equals(parser.getName())) { // We are on the closing tag, which defined the namespace as starting tag, reset the 'flag' namespaceElement = null; } if (parser.getDepth() <= depth) { // Abort parsing, we are done break outerloop; } break; case TEXT_CHARACTERS: if (startElementJustSeen) { startElementJustSeen = false; xml.rightAngleBracket(); } xml.escape(parser.getText()); break; default: // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement. break; } event = parser.next(); } return xml; } @SuppressWarnings("UnusedVariable") private static CharSequence parseContentDepthWithRoundtrip(XmlPullParser parser, int depth, boolean fullNamespaces) throws XmlPullParserException, IOException { XmlStringBuilder sb = new XmlStringBuilder(); XmlPullParser.Event event = parser.getEventType(); boolean startElementJustSeen = false; outerloop: while (true) { switch (event) { case START_ELEMENT: if (startElementJustSeen) { sb.rightAngleBracket(); } startElementJustSeen = true; break; case END_ELEMENT: boolean isEmptyElement = false; if (startElementJustSeen) { isEmptyElement = true; startElementJustSeen = false; } if (!isEmptyElement) { String text = parser.getText(); sb.append(text); } if (parser.getDepth() <= depth) { break outerloop; } break; default: startElementJustSeen = false; CharSequence text = parser.getText(); if (event == XmlPullParser.Event.TEXT_CHARACTERS) { text = StringUtils.escapeForXml(text); } sb.append(text); break; } event = parser.next(); } return sb; } public static Presence parsePresence(XmlPullParser parser) throws XmlPullParserException, IOException, SmackParsingException { return parsePresence(parser, null); } /** * Parses a presence packet. * * @param parser the XML parser, positioned at the start of a presence packet. * @param outerXmlEnvironment the outer XML environment (optional). * @return a Presence packet. * @throws IOException * @throws XmlPullParserException * @throws SmackParsingException */ public static Presence parsePresence(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException { ParserUtils.assertAtStartTag(parser); final int initialDepth = parser.getDepth(); XmlEnvironment presenceXmlEnvironment = XmlEnvironment.from(parser, outerXmlEnvironment); Presence.Type type = Presence.Type.available; String typeString = parser.getAttributeValue("", "type"); if (typeString != null && !typeString.equals("")) { type = Presence.Type.fromString(typeString); } Presence presence = new Presence(type); presence.setTo(ParserUtils.getJidAttribute(parser, "to")); presence.setFrom(ParserUtils.getJidAttribute(parser, "from")); presence.setStanzaId(parser.getAttributeValue("", "id")); String language = ParserUtils.getXmlLang(parser); if (language != null && !"".equals(language.trim())) { presence.setLanguage(language); } // Parse sub-elements outerloop: while (true) { XmlPullParser.Event eventType = parser.next(); switch (eventType) { case START_ELEMENT: String elementName = parser.getName(); String namespace = parser.getNamespace(); switch (elementName) { case "status": presence.setStatus(parser.nextText()); break; case "priority": Byte priority = ParserUtils.getByteAttributeFromNextText(parser); presence.setPriority(priority); break; case "show": String modeText = parser.nextText(); if (StringUtils.isNotEmpty(modeText)) { presence.setMode(Presence.Mode.fromString(modeText)); } else { // Some implementations send presence stanzas with a // '