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.StanzaCollector; 028import org.jivesoftware.smack.SmackException; 029import org.jivesoftware.smack.XMPPConnection; 030import org.jivesoftware.smack.XMPPException; 031import org.jivesoftware.smack.SmackException.NoResponseException; 032import org.jivesoftware.smack.SmackException.NotConnectedException; 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; 038import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 039import org.jivesoftware.smackx.iqregister.packet.Registration; 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<XMPPConnection, AccountManager>(); 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 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 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(/packet) 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(/packet) 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 133 * @throws NoResponseException 134 * @throws NotConnectedException 135 * @throws InterruptedException 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 178 * @throws NoResponseException 179 * @throws NotConnectedException 180 * @throws InterruptedException 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 <tt>null</tt> 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 <tt>null</tt> if an account 200 * attribute wasn't found for the requested name. 201 * @throws XMPPErrorException 202 * @throws NoResponseException 203 * @throws NotConnectedException 204 * @throws InterruptedException 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 <tt>null</tt> 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 <tt>null</tt> if there are none. 219 * @throws XMPPErrorException 220 * @throws NoResponseException 221 * @throws NotConnectedException 222 * @throws InterruptedException 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 242 * @throws NoResponseException 243 * @throws NotConnectedException 244 * @throws InterruptedException 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<String, String>(); 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 266 * @throws InterruptedException 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 * @throws IllegalStateException if not currently logged-in to the server. 295 * @throws XMPPErrorException if an error occurs when changing the password. 296 * @throws NoResponseException if there was no response from the server. 297 * @throws NotConnectedException 298 * @throws InterruptedException 299 */ 300 public void changePassword(String newPassword) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 301 if (!connection().isSecureConnection() && !allowSensitiveOperationOverInsecureConnection) { 302 throw new IllegalStateException("Changing password over insecure connection."); 303 } 304 Map<String, String> map = new HashMap<String, String>(); 305 map.put("username", connection().getUser().getLocalpart().toString()); 306 map.put("password",newPassword); 307 Registration reg = new Registration(map); 308 reg.setType(IQ.Type.set); 309 reg.setTo(connection().getXMPPServiceDomain()); 310 createStanzaCollectorAndSend(reg).nextResultOrThrow(); 311 } 312 313 /** 314 * Deletes the currently logged-in account from the server. This operation can only 315 * be performed after a successful login operation has been completed. Not all servers 316 * support deleting accounts; an XMPPException will be thrown when that is the case. 317 * 318 * @throws IllegalStateException if not currently logged-in to the server. 319 * @throws XMPPErrorException if an error occurs when deleting the account. 320 * @throws NoResponseException if there was no response from the server. 321 * @throws NotConnectedException 322 * @throws InterruptedException 323 */ 324 public void deleteAccount() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 325 Map<String, String> attributes = new HashMap<String, String>(); 326 // To delete an account, we add a single attribute, "remove", that is blank. 327 attributes.put("remove", ""); 328 Registration reg = new Registration(attributes); 329 reg.setType(IQ.Type.set); 330 reg.setTo(connection().getXMPPServiceDomain()); 331 createStanzaCollectorAndSend(reg).nextResultOrThrow(); 332 } 333 334 public boolean isSupported() 335 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 336 XMPPConnection connection = connection(); 337 338 ExtensionElement extensionElement = connection.getFeature(Registration.Feature.ELEMENT, 339 Registration.Feature.NAMESPACE); 340 if (extensionElement != null) { 341 return true; 342 } 343 344 // Fallback to disco#info only if this connection is authenticated, as otherwise we won't have an full JID and 345 // won't be able to do IQs. 346 if (connection.isAuthenticated()) { 347 return ServiceDiscoveryManager.getInstanceFor(connection).serverSupportsFeature(Registration.NAMESPACE); 348 } 349 350 return false; 351 } 352 353 /** 354 * Gets the account registration info from the server. 355 * @throws XMPPErrorException 356 * @throws NoResponseException 357 * @throws NotConnectedException 358 * @throws InterruptedException 359 * 360 * @throws XMPPException if an error occurs. 361 * @throws SmackException if there was no response from the server. 362 */ 363 private synchronized void getRegistrationInfo() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 364 Registration reg = new Registration(); 365 reg.setTo(connection().getXMPPServiceDomain()); 366 info = createStanzaCollectorAndSend(reg).nextResultOrThrow(); 367 } 368 369 private StanzaCollector createStanzaCollectorAndSend(IQ req) throws NotConnectedException, InterruptedException { 370 StanzaCollector collector = connection().createStanzaCollectorAndSend(new StanzaIdFilter(req.getStanzaId()), req); 371 return collector; 372 } 373}