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}