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}