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