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 (Exception 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 281 case "osm-server.url": 282 String newUrl = null; 283 if (evt.getNewValue() instanceof StringSetting) { 284 newUrl = ((StringSetting) evt.getNewValue()).getValue(); 285 } 286 if (newUrl == null || newUrl.trim().isEmpty()) { 287 setAnonymous(); 288 } else if (isFullyIdentified()) { 289 setPartiallyIdentified(getUserName()); 290 } 291 break; 292 293 case "oauth.access-token.key": 294 accessTokenKeyChanged = true; 295 break; 296 297 case "oauth.access-token.secret": 298 accessTokenSecretChanged = true; 299 break; 300 } 301 302 if (accessTokenKeyChanged && accessTokenSecretChanged) { 303 accessTokenKeyChanged = false; 304 accessTokenSecretChanged = false; 305 if (OsmApi.isUsingOAuth()) { 306 try { 307 getInstance().initFromOAuth(); 308 } catch (Exception e) { 309 Main.error(e); 310 } 311 } 312 } 313 } 314}