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