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