/** * * Copyright 2019 Paul Schaub * * 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.smackx.message_fastening.element; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.MessageBuilder; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.XmlElement; import org.jivesoftware.smack.packet.XmlEnvironment; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smackx.message_fastening.MessageFasteningManager; import org.jivesoftware.smackx.sid.element.OriginIdElement; /** * Message Fastening container element. */ public final class FasteningElement implements ExtensionElement { public static final String ELEMENT = "apply-to"; public static final String NAMESPACE = MessageFasteningManager.NAMESPACE; public static final String ATTR_ID = "id"; public static final String ATTR_CLEAR = "clear"; public static final String ATTR_SHELL = "shell"; private final OriginIdElement referencedStanzasOriginId; private final List externalPayloads = new ArrayList<>(); private final List wrappedPayloads = new ArrayList<>(); private final boolean clear; private final boolean shell; private FasteningElement(OriginIdElement originId, List wrappedPayloads, List externalPayloads, boolean clear, boolean shell) { this.referencedStanzasOriginId = Objects.requireNonNull(originId, "Fastening element MUST contain an origin-id."); this.wrappedPayloads.addAll(wrappedPayloads); this.externalPayloads.addAll(externalPayloads); this.clear = clear; this.shell = shell; } /** * Return the {@link OriginIdElement origin-id} of the {@link Stanza} that the message fastenings are to be * applied to. * * @return origin id of the referenced stanza */ public OriginIdElement getReferencedStanzasOriginId() { return referencedStanzasOriginId; } /** * Return all wrapped payloads of this element. * * @see XEP-0422: §3.1. Wrapped Payloads * * @return wrapped payloads. */ public List getWrappedPayloads() { return Collections.unmodifiableList(wrappedPayloads); } /** * Return all external payloads of this element. * * @see XEP-0422: §3.2. External Payloads * * @return external payloads. */ public List getExternalPayloads() { return Collections.unmodifiableList(externalPayloads); } /** * Does this element remove a previously sent {@link FasteningElement}? * * @see * XEP-0422: Message Fastening §3.4 Removing fastenings * * @return true if the clear attribute is set. */ public boolean isRemovingElement() { return clear; } /** * Is this a shell element? * Shell elements are otherwise empty elements that indicate that an encrypted payload of a message * encrypted using XEP-420: Stanza Content Encryption contains a sensitive {@link FasteningElement}. * * @see * XEP-0422: Message Fastening §3.5 Interaction with stanza encryption * * @return true if this is a shell element. */ public boolean isShellElement() { return shell; } /** * Return true if the provided {@link Message} contains a {@link FasteningElement}. * * @param message message * @return true if the stanza has an {@link FasteningElement}. */ public static boolean hasFasteningElement(Message message) { return message.hasExtension(ELEMENT, MessageFasteningManager.NAMESPACE); } /** * Return true if the provided {@link MessageBuilder} contains a {@link FasteningElement}. * * @param builder message builder * @return true if the stanza has an {@link FasteningElement}. */ public static boolean hasFasteningElement(MessageBuilder builder) { return builder.hasExtension(FasteningElement.class); } @Override public String getNamespace() { return MessageFasteningManager.NAMESPACE; } @Override public String getElementName() { return ELEMENT; } @Override public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { XmlStringBuilder xml = new XmlStringBuilder(this) .attribute(ATTR_ID, referencedStanzasOriginId.getId()) .optBooleanAttribute(ATTR_CLEAR, isRemovingElement()) .optBooleanAttribute(ATTR_SHELL, isShellElement()) .rightAngleBracket(); addPayloads(xml); return xml.closeElement(this); } private void addPayloads(XmlStringBuilder xml) { for (ExternalElement external : externalPayloads) { xml.append(external); } for (XmlElement wrapped : wrappedPayloads) { xml.append(wrapped); } } public static FasteningElement createShellElementForSensitiveElement(FasteningElement sensitiveElement) { return createShellElementForSensitiveElement(sensitiveElement.getReferencedStanzasOriginId()); } public static FasteningElement createShellElementForSensitiveElement(String originIdOfSensitiveElement) { return createShellElementForSensitiveElement(new OriginIdElement(originIdOfSensitiveElement)); } public static FasteningElement createShellElementForSensitiveElement(OriginIdElement originIdOfSensitiveElement) { return FasteningElement.builder() .setOriginId(originIdOfSensitiveElement) .setShell() .build(); } /** * Add this element to the provided message builder. * Note: The stanza MUST NOT contain more than one apply-to elements at the same time. * * @see XEP-0422 §4: Business Rules * * @param messageBuilder message builder */ public void applyTo(MessageBuilder messageBuilder) { if (FasteningElement.hasFasteningElement(messageBuilder)) { throw new IllegalArgumentException("Stanza cannot contain more than one apply-to elements."); } else { messageBuilder.addExtension(this); } } public static Builder builder() { return new Builder(); } public static class Builder { private OriginIdElement originId; private final List wrappedPayloads = new ArrayList<>(); private final List externalPayloads = new ArrayList<>(); private boolean isClear = false; private boolean isShell = false; /** * Set the origin-id of the referenced message. * * @param originIdString origin id as String * @return builder instance */ public Builder setOriginId(String originIdString) { return setOriginId(new OriginIdElement(originIdString)); } /** * Set the {@link OriginIdElement} of the referenced message. * * @param originId origin-id as element * @return builder instance */ public Builder setOriginId(OriginIdElement originId) { this.originId = originId; return this; } /** * Add a wrapped payload. * * @param wrappedPayload wrapped payload * @return builder instance */ public Builder addWrappedPayload(XmlElement wrappedPayload) { return addWrappedPayloads(Collections.singletonList(wrappedPayload)); } /** * Add multiple wrapped payloads at once. * * @param wrappedPayloads list of wrapped payloads * @return builder instance */ public Builder addWrappedPayloads(List wrappedPayloads) { this.wrappedPayloads.addAll(wrappedPayloads); return this; } /** * Add an external payload. * * @param externalPayload external payload * @return builder instance */ public Builder addExternalPayload(ExternalElement externalPayload) { return addExternalPayloads(Collections.singletonList(externalPayload)); } /** * Add multiple external payloads at once. * * @param externalPayloads external payloads * @return builder instance */ public Builder addExternalPayloads(List externalPayloads) { this.externalPayloads.addAll(externalPayloads); return this; } /** * Declare this {@link FasteningElement} to remove previous fastenings. * Semantically the wrapped payloads of this element declares all wrapped payloads from the referenced * fastening element that share qualified names as removed. * * @see * XEP-0422: Message Fastening §3.4 Removing fastenings * * @return builder instance */ public Builder setClear() { isClear = true; return this; } /** * Declare this {@link FasteningElement} to be a shell element. * Shell elements are used as hints that a Stanza Content Encryption payload contains another sensitive * {@link FasteningElement}. The outer "shell" {@link FasteningElement} is used to do fastening collation. * * @see XEP-0422: Message Fastening §3.5 Interaction with stanza encryption * @see XEP-0420: Stanza Content Encryption * * @return builder instance */ public Builder setShell() { isShell = true; return this; } /** * Build the element. * @return built element. */ public FasteningElement build() { validateThatIfIsShellThenOtherwiseEmpty(); return new FasteningElement(originId, wrappedPayloads, externalPayloads, isClear, isShell); } private void validateThatIfIsShellThenOtherwiseEmpty() { if (!isShell) { return; } if (isClear || !wrappedPayloads.isEmpty() || !externalPayloads.isEmpty()) { throw new IllegalArgumentException("A fastening that is a shell element must be otherwise empty " + "and cannot have a 'clear' attribute."); } } } }