001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.text.MessageFormat;
007
008import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
009import org.openstreetmap.josm.data.osm.User;
010import org.openstreetmap.josm.data.osm.UserInfo;
011import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
012import org.openstreetmap.josm.io.NetworkManager;
013import org.openstreetmap.josm.io.OnlineResource;
014import org.openstreetmap.josm.io.OsmApi;
015import org.openstreetmap.josm.io.OsmServerUserInfoReader;
016import org.openstreetmap.josm.io.OsmTransferException;
017import org.openstreetmap.josm.io.auth.CredentialsManager;
018import org.openstreetmap.josm.spi.preferences.Config;
019import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
020import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
021import org.openstreetmap.josm.spi.preferences.StringSetting;
022import org.openstreetmap.josm.tools.CheckParameterUtil;
023import org.openstreetmap.josm.tools.JosmRuntimeException;
024import org.openstreetmap.josm.tools.Logging;
025
026/**
027 * UserIdentityManager is a global object which keeps track of what JOSM knows about
028 * the identity of the current user.
029 *
030 * JOSM can be operated anonymously provided the current user never invokes an operation
031 * on the OSM server which required authentication. In this case JOSM neither knows
032 * the user name of the OSM account of the current user nor its unique id. Perhaps the
033 * user doesn't have one.
034 *
035 * If the current user supplies a user name and a password in the JOSM preferences JOSM
036 * can partially identify the user.
037 *
038 * The current user is fully identified if JOSM knows both the user name and the unique
039 * id of the users OSM account. The latter is retrieved from the OSM server with a
040 * <code>GET /api/0.6/user/details</code> request, submitted with the user name and password
041 * of the current user.
042 *
043 * The global UserIdentityManager listens to {@link PreferenceChangeEvent}s and keeps track
044 * of what the current JOSM instance knows about the current user. Other subsystems can
045 * let the global UserIdentityManager know in case they fully identify the current user, see
046 * {@link #setFullyIdentified}.
047 *
048 * The information kept by the UserIdentityManager can be used to
049 * <ul>
050 *   <li>safely query changesets owned by the current user based on its user id, not on its user name</li>
051 *   <li>safely search for objects last touched by the current user based on its user id, not on its user name</li>
052 * </ul>
053 * @since 12743 (renamed from {@code org.openstreetmap.josm.gui.JosmUserIdentityManager})
054 * @since 2689 (creation)
055 */
056public final class UserIdentityManager implements PreferenceChangedListener {
057
058    private static UserIdentityManager instance;
059
060    /**
061     * Replies the unique instance of the JOSM user identity manager
062     *
063     * @return the unique instance of the JOSM user identity manager
064     */
065    public static synchronized UserIdentityManager getInstance() {
066        if (instance == null) {
067            instance = new UserIdentityManager();
068            if (OsmApi.isUsingOAuth() && OAuthAccessTokenHolder.getInstance().containsAccessToken() &&
069                    !NetworkManager.isOffline(OnlineResource.OSM_API)) {
070                try {
071                    instance.initFromOAuth();
072                } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
073                    Logging.error(e);
074                    // Fall back to preferences if OAuth identification fails for any reason
075                    instance.initFromPreferences();
076                }
077            } else {
078                instance.initFromPreferences();
079            }
080            Config.getPref().addPreferenceChangeListener(instance);
081        }
082        return instance;
083    }
084
085    private String userName;
086    private UserInfo userInfo;
087    private boolean accessTokenKeyChanged;
088    private boolean accessTokenSecretChanged;
089
090    private UserIdentityManager() {
091    }
092
093    /**
094     * Remembers the fact that the current JOSM user is anonymous.
095     */
096    public void setAnonymous() {
097        userName = null;
098        userInfo = null;
099    }
100
101    /**
102     * Remembers the fact that the current JOSM user is partially identified
103     * by the user name of its OSM account.
104     *
105     * @param userName the user name. Must not be null. Must not be empty (whitespace only).
106     * @throws IllegalArgumentException if userName is null
107     * @throws IllegalArgumentException if userName is empty
108     */
109    public void setPartiallyIdentified(String userName) {
110        CheckParameterUtil.ensureParameterNotNull(userName, "userName");
111        String trimmedUserName = userName.trim();
112        if (trimmedUserName.isEmpty())
113            throw new IllegalArgumentException(
114                    MessageFormat.format("Expected non-empty value for parameter ''{0}'', got ''{1}''", "userName", userName));
115        this.userName = trimmedUserName;
116        userInfo = null;
117    }
118
119    /**
120     * Remembers the fact that the current JOSM user is fully identified with a
121     * verified pair of user name and user id.
122     *
123     * @param userName the user name. Must not be null. Must not be empty.
124     * @param userInfo additional information about the user, retrieved from the OSM server and including the user id
125     * @throws IllegalArgumentException if userName is null
126     * @throws IllegalArgumentException if userName is empty
127     * @throws IllegalArgumentException if userInfo is null
128     */
129    public void setFullyIdentified(String userName, UserInfo userInfo) {
130        CheckParameterUtil.ensureParameterNotNull(userName, "userName");
131        String trimmedUserName = userName.trim();
132        if (trimmedUserName.isEmpty())
133            throw new IllegalArgumentException(tr("Expected non-empty value for parameter ''{0}'', got ''{1}''", "userName", userName));
134        CheckParameterUtil.ensureParameterNotNull(userInfo, "userInfo");
135        this.userName = trimmedUserName;
136        this.userInfo = userInfo;
137    }
138
139    /**
140     * Replies true if the current JOSM user is anonymous.
141     *
142     * @return {@code true} if the current user is anonymous.
143     */
144    public boolean isAnonymous() {
145        return userName == null && userInfo == null;
146    }
147
148    /**
149     * Replies true if the current JOSM user is partially identified.
150     *
151     * @return true if the current JOSM user is partially identified.
152     */
153    public boolean isPartiallyIdentified() {
154        return userName != null && userInfo == null;
155    }
156
157    /**
158     * Replies true if the current JOSM user is fully identified.
159     *
160     * @return true if the current JOSM user is fully identified.
161     */
162    public boolean isFullyIdentified() {
163        return userName != null && userInfo != null;
164    }
165
166    /**
167     * Replies the user name of the current JOSM user. null, if {@link #isAnonymous()} is true.
168     *
169     * @return the user name of the current JOSM user
170     */
171    public String getUserName() {
172        return userName;
173    }
174
175    /**
176     * Replies the user id of the current JOSM user. 0, if {@link #isAnonymous()} or
177     * {@link #isPartiallyIdentified()} is true.
178     *
179     * @return the user id of the current JOSM user
180     */
181    public int getUserId() {
182        if (userInfo == null) return 0;
183        return userInfo.getId();
184    }
185
186    /**
187     * Replies verified additional information about the current user if the user is
188     * {@link #isFullyIdentified()}.
189     *
190     * @return verified additional information about the current user
191     */
192    public UserInfo getUserInfo() {
193        return userInfo;
194    }
195
196    /**
197     * Returns the identity as a {@link User} object
198     *
199     * @return the identity as user, or {@link User#getAnonymous()} if {@link #isAnonymous()}
200     */
201    public User asUser() {
202        return isAnonymous() ? User.getAnonymous() : User.createOsmUser(userInfo != null ? userInfo.getId() : 0, userName);
203    }
204
205    /**
206     * Initializes the user identity manager from Basic Authentication values in the {@link org.openstreetmap.josm.data.Preferences}
207     * This method should be called if {@code osm-server.auth-method} is set to {@code basic}.
208     * @see #initFromOAuth
209     */
210    public void initFromPreferences() {
211        String userName = CredentialsManager.getInstance().getUsername();
212        if (isAnonymous()) {
213            if (userName != null && !userName.trim().isEmpty()) {
214                setPartiallyIdentified(userName);
215            }
216        } else {
217            if (userName != null && !userName.equals(this.userName)) {
218                setPartiallyIdentified(userName);
219            }
220            // else: same name in the preferences as JOSM already knows about.
221            // keep the state, be it partially or fully identified
222        }
223    }
224
225    /**
226     * Initializes the user identity manager from OAuth request of user details.
227     * This method should be called if {@code osm-server.auth-method} is set to {@code oauth}.
228     * @see #initFromPreferences
229     * @since 5434
230     */
231    public void initFromOAuth() {
232        try {
233            UserInfo info = new OsmServerUserInfoReader().fetchUserInfo(NullProgressMonitor.INSTANCE);
234            setFullyIdentified(info.getDisplayName(), info);
235        } catch (IllegalArgumentException | OsmTransferException e) {
236            Logging.error(e);
237        }
238    }
239
240    /**
241     * Replies true if the user with name <code>username</code> is the current user
242     *
243     * @param userName the user name
244     * @return true if the user with name <code>username</code> is the current user
245     */
246    public boolean isCurrentUser(String userName) {
247        return this.userName != null && this.userName.equals(userName);
248    }
249
250    /**
251     * Replies true if the current user is {@link #isFullyIdentified() fully identified} and the {@link #getUserId() user ids} match,
252     * or if the current user is not {@link #isFullyIdentified() fully identified} and the {@link #getUserName() user names} match.
253     *
254     * @param user the user to test
255     * @return true if given user is the current user
256     */
257    public boolean isCurrentUser(User user) {
258        if (user == null) {
259            return false;
260        } else if (isFullyIdentified()) {
261            return getUserId() == user.getId();
262        } else {
263            return isCurrentUser(user.getName());
264        }
265    }
266
267    /* ------------------------------------------------------------------- */
268    /* interface PreferenceChangeListener                                  */
269    /* ------------------------------------------------------------------- */
270    @Override
271    public void preferenceChanged(PreferenceChangeEvent evt) {
272        switch (evt.getKey()) {
273        case "osm-server.username":
274            String newUserName = null;
275            if (evt.getNewValue() instanceof StringSetting) {
276                newUserName = ((StringSetting) evt.getNewValue()).getValue();
277            }
278            if (newUserName == null || newUserName.trim().isEmpty()) {
279                setAnonymous();
280            } else {
281                if (!newUserName.equals(userName)) {
282                    setPartiallyIdentified(newUserName);
283                }
284            }
285            return;
286        case "osm-server.url":
287            String newUrl = null;
288            if (evt.getNewValue() instanceof StringSetting) {
289                newUrl = ((StringSetting) evt.getNewValue()).getValue();
290            }
291            if (newUrl == null || newUrl.trim().isEmpty()) {
292                setAnonymous();
293            } else if (isFullyIdentified()) {
294                setPartiallyIdentified(getUserName());
295            }
296            break;
297        case "oauth.access-token.key":
298            accessTokenKeyChanged = true;
299            break;
300        case "oauth.access-token.secret":
301            accessTokenSecretChanged = true;
302            break;
303        default: // Do nothing
304        }
305
306        if (accessTokenKeyChanged && accessTokenSecretChanged) {
307            accessTokenKeyChanged = false;
308            accessTokenSecretChanged = false;
309            if (OsmApi.isUsingOAuth()) {
310                getInstance().initFromOAuth();
311            }
312        }
313    }
314}