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