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