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}