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; 025import java.util.logging.Logger; 026 027import org.jivesoftware.smack.Manager; 028import org.jivesoftware.smack.PacketCollector; 029import org.jivesoftware.smack.SmackException; 030import org.jivesoftware.smack.XMPPConnection; 031import org.jivesoftware.smack.XMPPException; 032import org.jivesoftware.smack.SmackException.NoResponseException; 033import org.jivesoftware.smack.SmackException.NotConnectedException; 034import org.jivesoftware.smack.XMPPException.XMPPErrorException; 035import org.jivesoftware.smack.filter.StanzaIdFilter; 036import org.jivesoftware.smack.packet.IQ; 037import org.jivesoftware.smackx.iqregister.packet.Registration; 038import org.jxmpp.util.XmppStringUtils; 039 040/** 041 * Allows creation and management of accounts on an XMPP server. 042 * 043 * @author Matt Tucker 044 */ 045public class AccountManager extends Manager { 046 047 private static final Logger LOGGER = Logger.getLogger(AccountManager.class.getName()); 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 void setSupportsAccountCreation(boolean accountCreationSupported) { 122 this.accountCreationSupported = accountCreationSupported; 123 } 124 125 /** 126 * Returns true if the server supports creating new accounts. Many servers require 127 * that you not be currently authenticated when creating new accounts, so the safest 128 * behavior is to only create new accounts before having logged in to a server. 129 * 130 * @return true if the server support creating new accounts. 131 * @throws XMPPErrorException 132 * @throws NoResponseException 133 * @throws NotConnectedException 134 */ 135 public boolean supportsAccountCreation() throws NoResponseException, XMPPErrorException, NotConnectedException { 136 // Check if we already know that the server supports creating new accounts 137 if (accountCreationSupported) { 138 return true; 139 } 140 // No information is known yet (e.g. no stream feature was received from the server 141 // indicating that it supports creating new accounts) so send an IQ packet as a way 142 // to discover if this feature is supported 143 if (info == null) { 144 getRegistrationInfo(); 145 accountCreationSupported = info.getType() != IQ.Type.error; 146 } 147 return accountCreationSupported; 148 } 149 150 /** 151 * Returns an unmodifiable collection of the names of the required account attributes. 152 * All attributes must be set when creating new accounts. The standard set of possible 153 * attributes are as follows: <ul> 154 * <li>name -- the user's name. 155 * <li>first -- the user's first name. 156 * <li>last -- the user's last name. 157 * <li>email -- the user's email address. 158 * <li>city -- the user's city. 159 * <li>state -- the user's state. 160 * <li>zip -- the user's ZIP code. 161 * <li>phone -- the user's phone number. 162 * <li>url -- the user's website. 163 * <li>date -- the date the registration took place. 164 * <li>misc -- other miscellaneous information to associate with the account. 165 * <li>text -- textual information to associate with the account. 166 * <li>remove -- empty flag to remove account. 167 * </ul><p> 168 * 169 * Typically, servers require no attributes when creating new accounts, or just 170 * the user's email address. 171 * 172 * @return the required account attributes. 173 * @throws XMPPErrorException 174 * @throws NoResponseException 175 * @throws NotConnectedException 176 */ 177 public Set<String> getAccountAttributes() throws NoResponseException, XMPPErrorException, NotConnectedException { 178 if (info == null) { 179 getRegistrationInfo(); 180 } 181 Map<String, String> attributes = info.getAttributes(); 182 if (attributes != null) { 183 return Collections.unmodifiableSet(attributes.keySet()); 184 } else { 185 return Collections.emptySet(); 186 } 187 } 188 189 /** 190 * Returns the value of a given account attribute or <tt>null</tt> if the account 191 * attribute wasn't found. 192 * 193 * @param name the name of the account attribute to return its value. 194 * @return the value of the account attribute or <tt>null</tt> if an account 195 * attribute wasn't found for the requested name. 196 * @throws XMPPErrorException 197 * @throws NoResponseException 198 * @throws NotConnectedException 199 */ 200 public String getAccountAttribute(String name) throws NoResponseException, XMPPErrorException, NotConnectedException { 201 if (info == null) { 202 getRegistrationInfo(); 203 } 204 return info.getAttributes().get(name); 205 } 206 207 /** 208 * Returns the instructions for creating a new account, or <tt>null</tt> if there 209 * are no instructions. If present, instructions should be displayed to the end-user 210 * that will complete the registration process. 211 * 212 * @return the account creation instructions, or <tt>null</tt> if there are none. 213 * @throws XMPPErrorException 214 * @throws NoResponseException 215 * @throws NotConnectedException 216 */ 217 public String getAccountInstructions() throws NoResponseException, XMPPErrorException, NotConnectedException { 218 if (info == null) { 219 getRegistrationInfo(); 220 } 221 return info.getInstructions(); 222 } 223 224 /** 225 * Creates a new account using the specified username and password. The server may 226 * require a number of extra account attributes such as an email address and phone 227 * number. In that case, Smack will attempt to automatically set all required 228 * attributes with blank values, which may or may not be accepted by the server. 229 * Therefore, it's recommended to check the required account attributes and to let 230 * the end-user populate them with real values instead. 231 * 232 * @param username the username. 233 * @param password the password. 234 * @throws XMPPErrorException 235 * @throws NoResponseException 236 * @throws NotConnectedException 237 */ 238 public void createAccount(String username, String password) throws NoResponseException, XMPPErrorException, NotConnectedException { 239 // Create a map for all the required attributes, but give them blank values. 240 Map<String, String> attributes = new HashMap<String, String>(); 241 for (String attributeName : getAccountAttributes()) { 242 attributes.put(attributeName, ""); 243 } 244 createAccount(username, password, attributes); 245 } 246 247 /** 248 * Creates a new account using the specified username, password and account attributes. 249 * The attributes Map must contain only String name/value pairs and must also have values 250 * for all required attributes. 251 * 252 * @param username the username. 253 * @param password the password. 254 * @param attributes the account attributes. 255 * @throws XMPPErrorException if an error occurs creating the account. 256 * @throws NoResponseException if there was no response from the server. 257 * @throws NotConnectedException 258 * @see #getAccountAttributes() 259 */ 260 public void createAccount(String username, String password, Map<String, String> attributes) 261 throws NoResponseException, XMPPErrorException, NotConnectedException { 262 if (!connection().isSecureConnection() && !allowSensitiveOperationOverInsecureConnection) { 263 // TODO throw exception in newer Smack versions 264 LOGGER.warning("Creating account over insecure connection. " 265 + "This will throw an exception in future versions of Smack if AccountManager.sensitiveOperationOverInsecureConnection(true) is not set"); 266 } 267 attributes.put("username", username); 268 attributes.put("password", password); 269 Registration reg = new Registration(attributes); 270 reg.setType(IQ.Type.set); 271 reg.setTo(connection().getServiceName()); 272 createPacketCollectorAndSend(reg).nextResultOrThrow(); 273 } 274 275 /** 276 * Changes the password of the currently logged-in account. This operation can only 277 * be performed after a successful login operation has been completed. Not all servers 278 * support changing passwords; an XMPPException will be thrown when that is the case. 279 * 280 * @throws IllegalStateException if not currently logged-in to the server. 281 * @throws XMPPErrorException if an error occurs when changing the password. 282 * @throws NoResponseException if there was no response from the server. 283 * @throws NotConnectedException 284 */ 285 public void changePassword(String newPassword) throws NoResponseException, XMPPErrorException, NotConnectedException { 286 if (!connection().isSecureConnection() && !allowSensitiveOperationOverInsecureConnection) { 287 // TODO throw exception in newer Smack versions 288 LOGGER.warning("Changing password over insecure connection. " 289 + "This will throw an exception in future versions of Smack if AccountManager.sensitiveOperationOverInsecureConnection(true) is not set"); 290 } 291 Map<String, String> map = new HashMap<String, String>(); 292 map.put("username",XmppStringUtils.parseLocalpart(connection().getUser())); 293 map.put("password",newPassword); 294 Registration reg = new Registration(map); 295 reg.setType(IQ.Type.set); 296 reg.setTo(connection().getServiceName()); 297 createPacketCollectorAndSend(reg).nextResultOrThrow(); 298 } 299 300 /** 301 * Deletes the currently logged-in account from the server. This operation can only 302 * be performed after a successful login operation has been completed. Not all servers 303 * support deleting accounts; an XMPPException will be thrown when that is the case. 304 * 305 * @throws IllegalStateException if not currently logged-in to the server. 306 * @throws XMPPErrorException if an error occurs when deleting the account. 307 * @throws NoResponseException if there was no response from the server. 308 * @throws NotConnectedException 309 */ 310 public void deleteAccount() throws NoResponseException, XMPPErrorException, NotConnectedException { 311 Map<String, String> attributes = new HashMap<String, String>(); 312 // To delete an account, we add a single attribute, "remove", that is blank. 313 attributes.put("remove", ""); 314 Registration reg = new Registration(attributes); 315 reg.setType(IQ.Type.set); 316 reg.setTo(connection().getServiceName()); 317 createPacketCollectorAndSend(reg).nextResultOrThrow(); 318 } 319 320 /** 321 * Gets the account registration info from the server. 322 * @throws XMPPErrorException 323 * @throws NoResponseException 324 * @throws NotConnectedException 325 * 326 * @throws XMPPException if an error occurs. 327 * @throws SmackException if there was no response from the server. 328 */ 329 private synchronized void getRegistrationInfo() throws NoResponseException, XMPPErrorException, NotConnectedException { 330 Registration reg = new Registration(); 331 reg.setTo(connection().getServiceName()); 332 info = createPacketCollectorAndSend(reg).nextResultOrThrow(); 333 } 334 335 private PacketCollector createPacketCollectorAndSend(IQ req) throws NotConnectedException { 336 PacketCollector collector = connection().createPacketCollectorAndSend(new StanzaIdFilter(req.getStanzaId()), req); 337 return collector; 338 } 339}