001/**
002 *
003 * Copyright 2019 Paul Schaub
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.message_fastening.element;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.List;
022
023import org.jivesoftware.smack.packet.ExtensionElement;
024import org.jivesoftware.smack.packet.Message;
025import org.jivesoftware.smack.packet.MessageBuilder;
026import org.jivesoftware.smack.packet.Stanza;
027import org.jivesoftware.smack.packet.XmlEnvironment;
028import org.jivesoftware.smack.util.Objects;
029import org.jivesoftware.smack.util.XmlStringBuilder;
030import org.jivesoftware.smackx.message_fastening.MessageFasteningManager;
031import org.jivesoftware.smackx.sid.element.OriginIdElement;
032
033/**
034 * Message Fastening container element.
035 */
036public final class FasteningElement implements ExtensionElement {
037
038    public static final String ELEMENT = "apply-to";
039    public static final String NAMESPACE = MessageFasteningManager.NAMESPACE;
040    public static final String ATTR_ID = "id";
041    public static final String ATTR_CLEAR = "clear";
042    public static final String ATTR_SHELL = "shell";
043
044    private final OriginIdElement referencedStanzasOriginId;
045    private final List<ExternalElement> externalPayloads = new ArrayList<>();
046    private final List<ExtensionElement> wrappedPayloads = new ArrayList<>();
047    private final boolean clear;
048    private final boolean shell;
049
050    private FasteningElement(OriginIdElement originId,
051                             List<ExtensionElement> wrappedPayloads,
052                             List<ExternalElement> externalPayloads,
053                             boolean clear,
054                             boolean shell) {
055        this.referencedStanzasOriginId = Objects.requireNonNull(originId, "Fastening element MUST contain an origin-id.");
056        this.wrappedPayloads.addAll(wrappedPayloads);
057        this.externalPayloads.addAll(externalPayloads);
058        this.clear = clear;
059        this.shell = shell;
060    }
061
062    /**
063     * Return the {@link OriginIdElement origin-id} of the {@link Stanza} that the message fastenings are to be
064     * applied to.
065     *
066     * @return origin id of the referenced stanza
067     */
068    public OriginIdElement getReferencedStanzasOriginId() {
069        return referencedStanzasOriginId;
070    }
071
072    /**
073     * Return all wrapped payloads of this element.
074     *
075     * @see <a href="https://xmpp.org/extensions/xep-0422.html#wrapped-payloads">XEP-0422: §3.1. Wrapped Payloads</a>
076     *
077     * @return wrapped payloads.
078     */
079    public List<ExtensionElement> getWrappedPayloads() {
080        return Collections.unmodifiableList(wrappedPayloads);
081    }
082
083    /**
084     * Return all external payloads of this element.
085     *
086     * @see <a href="https://xmpp.org/extensions/xep-0422.html#external-payloads">XEP-0422: §3.2. External Payloads</a>
087     *
088     * @return external payloads.
089     */
090    public List<ExternalElement> getExternalPayloads() {
091        return Collections.unmodifiableList(externalPayloads);
092    }
093
094    /**
095     * Does this element remove a previously sent {@link FasteningElement}?
096     *
097     * @see <a href="https://xmpp.org/extensions/xep-0422.html#remove">
098     *     XEP-0422: Message Fastening §3.4 Removing fastenings</a>
099     *
100     * @return true if the clear attribute is set.
101     */
102    public boolean isRemovingElement() {
103        return clear;
104    }
105
106    /**
107     * Is this a shell element?
108     * Shell elements are otherwise empty elements that indicate that an encrypted payload of a message
109     * encrypted using XEP-420: Stanza Content Encryption contains a sensitive {@link FasteningElement}.
110     *
111     * @see <a href="https://xmpp.org/extensions/xep-0422.html#encryption">
112     *     XEP-0422: Message Fastening §3.5 Interaction with stanza encryption</a>
113     *
114     * @return true if this is a shell element.
115     */
116    public boolean isShellElement() {
117        return shell;
118    }
119
120    /**
121     * Return true if the provided {@link Message} contains a {@link FasteningElement}.
122     *
123     * @param message message
124     * @return true if the stanza has an {@link FasteningElement}.
125     */
126    public static boolean hasFasteningElement(Message message) {
127        return message.hasExtension(ELEMENT, MessageFasteningManager.NAMESPACE);
128    }
129
130    /**
131     * Return true if the provided {@link MessageBuilder} contains a {@link FasteningElement}.
132     *
133     * @param builder message builder
134     * @return true if the stanza has an {@link FasteningElement}.
135     */
136    public static boolean hasFasteningElement(MessageBuilder builder) {
137        return builder.hasExtension(FasteningElement.class);
138    }
139
140    @Override
141    public String getNamespace() {
142        return MessageFasteningManager.NAMESPACE;
143    }
144
145    @Override
146    public String getElementName() {
147        return ELEMENT;
148    }
149
150    @Override
151    public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
152        XmlStringBuilder xml = new XmlStringBuilder(this)
153                .attribute(ATTR_ID, referencedStanzasOriginId.getId())
154                .optBooleanAttribute(ATTR_CLEAR, isRemovingElement())
155                .optBooleanAttribute(ATTR_SHELL, isShellElement())
156                .rightAngleBracket();
157        addPayloads(xml);
158        return xml.closeElement(this);
159    }
160
161    private void addPayloads(XmlStringBuilder xml) {
162        for (ExternalElement external : externalPayloads) {
163            xml.append(external);
164        }
165        for (ExtensionElement wrapped : wrappedPayloads) {
166            xml.append(wrapped);
167        }
168    }
169
170    public static FasteningElement createShellElementForSensitiveElement(FasteningElement sensitiveElement) {
171        return createShellElementForSensitiveElement(sensitiveElement.getReferencedStanzasOriginId());
172    }
173
174    public static FasteningElement createShellElementForSensitiveElement(String originIdOfSensitiveElement) {
175        return createShellElementForSensitiveElement(new OriginIdElement(originIdOfSensitiveElement));
176    }
177
178    public static FasteningElement createShellElementForSensitiveElement(OriginIdElement originIdOfSensitiveElement) {
179        return FasteningElement.builder()
180                .setOriginId(originIdOfSensitiveElement)
181                .setShell()
182                .build();
183    }
184
185    /**
186     * Add this element to the provided message builder.
187     * Note: The stanza MUST NOT contain more than one apply-to elements at the same time.
188     *
189     * @see <a href="https://xmpp.org/extensions/xep-0422.html#rules">XEP-0422 §4: Business Rules</a>
190     *
191     * @param messageBuilder message builder
192     */
193    public void applyTo(MessageBuilder messageBuilder) {
194        if (FasteningElement.hasFasteningElement(messageBuilder)) {
195            throw new IllegalArgumentException("Stanza cannot contain more than one apply-to elements.");
196        } else {
197            messageBuilder.addExtension(this);
198        }
199    }
200
201    public static Builder builder() {
202        return new Builder();
203    }
204
205    public static class Builder {
206        private OriginIdElement originId;
207        private final List<ExtensionElement> wrappedPayloads = new ArrayList<>();
208        private final List<ExternalElement> externalPayloads = new ArrayList<>();
209        private boolean isClear = false;
210        private boolean isShell = false;
211
212        /**
213         * Set the origin-id of the referenced message.
214         *
215         * @param originIdString origin id as String
216         * @return builder instance
217         */
218        public Builder setOriginId(String originIdString) {
219            return setOriginId(new OriginIdElement(originIdString));
220        }
221
222        /**
223         * Set the {@link OriginIdElement} of the referenced message.
224         *
225         * @param originId origin-id as element
226         * @return builder instance
227         */
228        public Builder setOriginId(OriginIdElement originId) {
229            this.originId = originId;
230            return this;
231        }
232
233        /**
234         * Add a wrapped payload.
235         *
236         * @param wrappedPayload wrapped payload
237         * @return builder instance
238         */
239        public Builder addWrappedPayload(ExtensionElement wrappedPayload) {
240            return addWrappedPayloads(Collections.singletonList(wrappedPayload));
241        }
242
243        /**
244         * Add multiple wrapped payloads at once.
245         *
246         * @param wrappedPayloads list of wrapped payloads
247         * @return builder instance
248         */
249        public Builder addWrappedPayloads(List<ExtensionElement> wrappedPayloads) {
250            this.wrappedPayloads.addAll(wrappedPayloads);
251            return this;
252        }
253
254        /**
255         * Add an external payload.
256         *
257         * @param externalPayload external payload
258         * @return builder instance
259         */
260        public Builder addExternalPayload(ExternalElement externalPayload) {
261            return addExternalPayloads(Collections.singletonList(externalPayload));
262        }
263
264        /**
265         * Add multiple external payloads at once.
266         *
267         * @param externalPayloads external payloads
268         * @return builder instance
269         */
270        public Builder addExternalPayloads(List<ExternalElement> externalPayloads) {
271            this.externalPayloads.addAll(externalPayloads);
272            return this;
273        }
274
275        /**
276         * Declare this {@link FasteningElement} to remove previous fastenings.
277         * Semantically the wrapped payloads of this element declares all wrapped payloads from the referenced
278         * fastening element that share qualified names as removed.
279         *
280         * @see <a href="https://xmpp.org/extensions/xep-0422.html#remove">
281         *     XEP-0422: Message Fastening §3.4 Removing fastenings</a>
282         *
283         * @return builder instance
284         */
285        public Builder setClear() {
286            isClear = true;
287            return this;
288        }
289
290        /**
291         * Declare this {@link FasteningElement} to be a shell element.
292         * Shell elements are used as hints that a Stanza Content Encryption payload contains another sensitive
293         * {@link FasteningElement}. The outer "shell" {@link FasteningElement} is used to do fastening collation.
294         *
295         * @see <a href="https://xmpp.org/extensions/xep-0422.html#encryption">XEP-0422: Message Fastening §3.5 Interaction with stanza encryption</a>
296         * @see <a href="https://xmpp.org/extensions/xep-0420.html">XEP-0420: Stanza Content Encryption</a>
297         *
298         * @return builder instance
299         */
300        public Builder setShell() {
301            isShell = true;
302            return this;
303        }
304
305        /**
306         * Build the element.
307         * @return built element.
308         */
309        public FasteningElement build() {
310            validateThatIfIsShellThenOtherwiseEmpty();
311            return new FasteningElement(originId, wrappedPayloads, externalPayloads, isClear, isShell);
312        }
313
314        private void validateThatIfIsShellThenOtherwiseEmpty() {
315            if (!isShell) {
316                return;
317            }
318
319            if (isClear || !wrappedPayloads.isEmpty() || !externalPayloads.isEmpty()) {
320                throw new IllegalArgumentException("A fastening that is a shell element must be otherwise empty " +
321                        "and cannot have a 'clear' attribute.");
322            }
323        }
324    }
325}