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