001/** 002 * 003 * Copyright 2003-2007 Jive Software. 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 */ 017 018package org.jivesoftware.smackx.iqregister; 019 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.Map; 023import java.util.Set; 024import java.util.WeakHashMap; 025 026import org.jivesoftware.smack.Manager; 027import org.jivesoftware.smack.SmackException; 028import org.jivesoftware.smack.SmackException.NoResponseException; 029import org.jivesoftware.smack.SmackException.NotConnectedException; 030import org.jivesoftware.smack.StanzaCollector; 031import org.jivesoftware.smack.XMPPConnection; 032import org.jivesoftware.smack.XMPPException; 033import org.jivesoftware.smack.XMPPException.XMPPErrorException; 034import org.jivesoftware.smack.filter.StanzaIdFilter; 035import org.jivesoftware.smack.packet.ExtensionElement; 036import org.jivesoftware.smack.packet.IQ; 037import org.jivesoftware.smack.util.StringUtils; 038 039import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 040import org.jivesoftware.smackx.iqregister.packet.Registration; 041 042import org.jxmpp.jid.parts.Localpart; 043 044/** 045 * Allows creation and management of accounts on an XMPP server. 046 * 047 * @author Matt Tucker 048 */ 049public final class AccountManager extends Manager { 050 051 private static final Map<XMPPConnection, AccountManager> INSTANCES = new WeakHashMap<>(); 052 053 /** 054 * Returns the AccountManager instance associated with a given XMPPConnection. 055 * 056 * @param connection the connection used to look for the proper ServiceDiscoveryManager. 057 * @return the AccountManager associated with a given XMPPConnection. 058 */ 059 public static synchronized AccountManager getInstance(XMPPConnection connection) { 060 AccountManager accountManager = INSTANCES.get(connection); 061 if (accountManager == null) { 062 accountManager = new AccountManager(connection); 063 INSTANCES.put(connection, accountManager); 064 } 065 return accountManager; 066 } 067 068 private static boolean allowSensitiveOperationOverInsecureConnectionDefault = false; 069 070 /** 071 * The default value used by new account managers for <code>allowSensitiveOperationOverInsecureConnection</code>. 072 * 073 * @param allow TODO javadoc me please 074 * @see #sensitiveOperationOverInsecureConnection(boolean) 075 * @since 4.1 076 */ 077 public static void sensitiveOperationOverInsecureConnectionDefault(boolean allow) { 078 AccountManager.allowSensitiveOperationOverInsecureConnectionDefault = allow; 079 } 080 081 private boolean allowSensitiveOperationOverInsecureConnection = allowSensitiveOperationOverInsecureConnectionDefault; 082 083 /** 084 * Set to <code>true</code> to allow sensitive operation over insecure connection. 085 * <p> 086 * Set to true to allow sensitive operations like account creation or password changes over an insecure (e.g. 087 * unencrypted) connections. 088 * </p> 089 * 090 * @param allow TODO javadoc me please 091 * @since 4.1 092 */ 093 public void sensitiveOperationOverInsecureConnection(boolean allow) { 094 this.allowSensitiveOperationOverInsecureConnection = allow; 095 } 096 097 private Registration info = null; 098 099 /** 100 * Flag that indicates whether the server supports In-Band Registration. 101 * In-Band Registration may be advertised as a stream feature. If no stream feature 102 * was advertised from the server then try sending an IQ stanza to discover if In-Band 103 * Registration is available. 104 */ 105 private boolean accountCreationSupported = false; 106 107 /** 108 * Creates a new AccountManager instance. 109 * 110 * @param connection a connection to an XMPP server. 111 */ 112 private AccountManager(XMPPConnection connection) { 113 super(connection); 114 } 115 116 /** 117 * Sets whether the server supports In-Band Registration. In-Band Registration may be 118 * advertised as a stream feature. If no stream feature was advertised from the server 119 * then try sending an IQ stanza to discover if In-Band Registration is available. 120 * 121 * @param accountCreationSupported true if the server supports In-Band Registration. 122 */ 123 // TODO: Remove this method and the accountCreationSupported boolean. 124 void setSupportsAccountCreation(boolean accountCreationSupported) { 125 this.accountCreationSupported = accountCreationSupported; 126 } 127 128 /** 129 * Returns true if the server supports creating new accounts. Many servers require 130 * that you not be currently authenticated when creating new accounts, so the safest 131 * behavior is to only create new accounts before having logged in to a server. 132 * 133 * @return true if the server support creating new accounts. 134 * @throws XMPPErrorException if there was an XMPP error returned. 135 * @throws NoResponseException if there was no response from the remote entity. 136 * @throws NotConnectedException if the XMPP connection is not connected. 137 * @throws InterruptedException if the calling thread was interrupted. 138 */ 139 public boolean supportsAccountCreation() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 140 // TODO: Replace this body with isSupported() and possible deprecate this method. 141 142 // Check if we already know that the server supports creating new accounts 143 if (accountCreationSupported) { 144 return true; 145 } 146 // No information is known yet (e.g. no stream feature was received from the server 147 // indicating that it supports creating new accounts) so send an IQ packet as a way 148 // to discover if this feature is supported 149 if (info == null) { 150 getRegistrationInfo(); 151 accountCreationSupported = info.getType() != IQ.Type.error; 152 } 153 return accountCreationSupported; 154 } 155 156 /** 157 * Returns an unmodifiable collection of the names of the required account attributes. 158 * All attributes must be set when creating new accounts. The standard set of possible 159 * attributes are as follows: <ul> 160 * <li>name -- the user's name. 161 * <li>first -- the user's first name. 162 * <li>last -- the user's last name. 163 * <li>email -- the user's email address. 164 * <li>city -- the user's city. 165 * <li>state -- the user's state. 166 * <li>zip -- the user's ZIP code. 167 * <li>phone -- the user's phone number. 168 * <li>url -- the user's website. 169 * <li>date -- the date the registration took place. 170 * <li>misc -- other miscellaneous information to associate with the account. 171 * <li>text -- textual information to associate with the account. 172 * <li>remove -- empty flag to remove account. 173 * </ul><p> 174 * 175 * Typically, servers require no attributes when creating new accounts, or just 176 * the user's email address. 177 * 178 * @return the required account attributes. 179 * @throws XMPPErrorException if there was an XMPP error returned. 180 * @throws NoResponseException if there was no response from the remote entity. 181 * @throws NotConnectedException if the XMPP connection is not connected. 182 * @throws InterruptedException if the calling thread was interrupted. 183 */ 184 public Set<String> getAccountAttributes() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 185 if (info == null) { 186 getRegistrationInfo(); 187 } 188 Map<String, String> attributes = info.getAttributes(); 189 if (attributes != null) { 190 return Collections.unmodifiableSet(attributes.keySet()); 191 } else { 192 return Collections.emptySet(); 193 } 194 } 195 196 /** 197 * Returns the value of a given account attribute or <code>null</code> if the account 198 * attribute wasn't found. 199 * 200 * @param name the name of the account attribute to return its value. 201 * @return the value of the account attribute or <code>null</code> if an account 202 * attribute wasn't found for the requested name. 203 * @throws XMPPErrorException if there was an XMPP error returned. 204 * @throws NoResponseException if there was no response from the remote entity. 205 * @throws NotConnectedException if the XMPP connection is not connected. 206 * @throws InterruptedException if the calling thread was interrupted. 207 */ 208 public String getAccountAttribute(String name) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 209 if (info == null) { 210 getRegistrationInfo(); 211 } 212 return info.getAttributes().get(name); 213 } 214 215 /** 216 * Returns the instructions for creating a new account, or <code>null</code> if there 217 * are no instructions. If present, instructions should be displayed to the end-user 218 * that will complete the registration process. 219 * 220 * @return the account creation instructions, or <code>null</code> if there are none. 221 * @throws XMPPErrorException if there was an XMPP error returned. 222 * @throws NoResponseException if there was no response from the remote entity. 223 * @throws NotConnectedException if the XMPP connection is not connected. 224 * @throws InterruptedException if the calling thread was interrupted. 225 */ 226 public String getAccountInstructions() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 227 if (info == null) { 228 getRegistrationInfo(); 229 } 230 return info.getInstructions(); 231 } 232 233 /** 234 * Creates a new account using the specified username and password. The server may 235 * require a number of extra account attributes such as an email address and phone 236 * number. In that case, Smack will attempt to automatically set all required 237 * attributes with blank values, which may or may not be accepted by the server. 238 * Therefore, it's recommended to check the required account attributes and to let 239 * the end-user populate them with real values instead. 240 * 241 * @param username the username. 242 * @param password the password. 243 * @throws XMPPErrorException if there was an XMPP error returned. 244 * @throws NoResponseException if there was no response from the remote entity. 245 * @throws NotConnectedException if the XMPP connection is not connected. 246 * @throws InterruptedException if the calling thread was interrupted. 247 */ 248 public void createAccount(Localpart username, String password) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 249 // Create a map for all the required attributes, but give them blank values. 250 Map<String, String> attributes = new HashMap<>(); 251 for (String attributeName : getAccountAttributes()) { 252 attributes.put(attributeName, ""); 253 } 254 createAccount(username, password, attributes); 255 } 256 257 /** 258 * Creates a new account using the specified username, password and account attributes. 259 * The attributes Map must contain only String name/value pairs and must also have values 260 * for all required attributes. 261 * 262 * @param username the username. 263 * @param password the password. 264 * @param attributes the account attributes. 265 * @throws XMPPErrorException if an error occurs creating the account. 266 * @throws NoResponseException if there was no response from the server. 267 * @throws NotConnectedException if the XMPP connection is not connected. 268 * @throws InterruptedException if the calling thread was interrupted. 269 * @see #getAccountAttributes() 270 */ 271 public void createAccount(Localpart username, String password, Map<String, String> attributes) 272 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 273 if (!connection().isSecureConnection() && !allowSensitiveOperationOverInsecureConnection) { 274 throw new IllegalStateException("Creating account over insecure connection"); 275 } 276 if (username == null) { 277 throw new IllegalArgumentException("Username must not be null"); 278 } 279 if (StringUtils.isNullOrEmpty(password)) { 280 throw new IllegalArgumentException("Password must not be null"); 281 } 282 283 attributes.put("username", username.toString()); 284 attributes.put("password", password); 285 Registration reg = new Registration(attributes); 286 reg.setType(IQ.Type.set); 287 reg.setTo(connection().getXMPPServiceDomain()); 288 createStanzaCollectorAndSend(reg).nextResultOrThrow(); 289 } 290 291 /** 292 * Changes the password of the currently logged-in account. This operation can only 293 * be performed after a successful login operation has been completed. Not all servers 294 * support changing passwords; an XMPPException will be thrown when that is the case. 295 * 296 * @param newPassword new password. 297 * 298 * @throws IllegalStateException if not currently logged-in to the server. 299 * @throws XMPPErrorException if an error occurs when changing the password. 300 * @throws NoResponseException if there was no response from the server. 301 * @throws NotConnectedException if the XMPP connection is not connected. 302 * @throws InterruptedException if the calling thread was interrupted. 303 */ 304 public void changePassword(String newPassword) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 305 if (!connection().isSecureConnection() && !allowSensitiveOperationOverInsecureConnection) { 306 throw new IllegalStateException("Changing password over insecure connection."); 307 } 308 Map<String, String> map = new HashMap<>(); 309 map.put("username", connection().getUser().getLocalpart().toString()); 310 map.put("password", newPassword); 311 Registration reg = new Registration(map); 312 reg.setType(IQ.Type.set); 313 reg.setTo(connection().getXMPPServiceDomain()); 314 createStanzaCollectorAndSend(reg).nextResultOrThrow(); 315 } 316 317 /** 318 * Deletes the currently logged-in account from the server. This operation can only 319 * be performed after a successful login operation has been completed. Not all servers 320 * support deleting accounts; an XMPPException will be thrown when that is the case. 321 * 322 * @throws IllegalStateException if not currently logged-in to the server. 323 * @throws XMPPErrorException if an error occurs when deleting the account. 324 * @throws NoResponseException if there was no response from the server. 325 * @throws NotConnectedException if the XMPP connection is not connected. 326 * @throws InterruptedException if the calling thread was interrupted. 327 */ 328 public void deleteAccount() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 329 Map<String, String> attributes = new HashMap<>(); 330 // To delete an account, we add a single attribute, "remove", that is blank. 331 attributes.put("remove", ""); 332 Registration reg = new Registration(attributes); 333 reg.setType(IQ.Type.set); 334 reg.setTo(connection().getXMPPServiceDomain()); 335 createStanzaCollectorAndSend(reg).nextResultOrThrow(); 336 } 337 338 public boolean isSupported() 339 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 340 XMPPConnection connection = connection(); 341 342 ExtensionElement extensionElement = connection.getFeature(Registration.Feature.class); 343 if (extensionElement != null) { 344 return true; 345 } 346 347 // Fallback to disco#info only if this connection is authenticated, as otherwise we won't have an full JID and 348 // won't be able to do IQs. 349 if (connection.isAuthenticated()) { 350 return ServiceDiscoveryManager.getInstanceFor(connection).serverSupportsFeature(Registration.NAMESPACE); 351 } 352 353 return false; 354 } 355 356 /** 357 * Gets the account registration info from the server. 358 * @throws XMPPErrorException if there was an XMPP error returned. 359 * @throws NoResponseException if there was no response from the remote entity. 360 * @throws NotConnectedException if the XMPP connection is not connected. 361 * @throws InterruptedException if the calling thread was interrupted. 362 * 363 * @throws XMPPException if an error occurs. 364 * @throws SmackException if there was no response from the server. 365 */ 366 private synchronized void getRegistrationInfo() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 367 Registration reg = new Registration(); 368 reg.setTo(connection().getXMPPServiceDomain()); 369 info = createStanzaCollectorAndSend(reg).nextResultOrThrow(); 370 } 371 372 private StanzaCollector createStanzaCollectorAndSend(IQ req) throws NotConnectedException, InterruptedException { 373 return connection().createStanzaCollectorAndSend(new StanzaIdFilter(req.getStanzaId()), req); 374 } 375}