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