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}