1 /*******************************************************************************
2  * Copyright (c) 2000, 2011 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.team.internal.ccvs.core.connection;
15 
16 import java.io.IOException;
17 import java.util.*;
18 
19 import org.eclipse.core.resources.ResourcesPlugin;
20 import org.eclipse.core.runtime.*;
21 import org.eclipse.core.runtime.Status;
22 import org.eclipse.core.runtime.jobs.ILock;
23 import org.eclipse.core.runtime.jobs.Job;
24 import org.eclipse.core.runtime.preferences.DefaultScope;
25 import org.eclipse.equinox.security.storage.*;
26 import org.eclipse.osgi.util.NLS;
27 import org.eclipse.team.core.TeamException;
28 import org.eclipse.team.internal.ccvs.core.*;
29 import org.eclipse.team.internal.ccvs.core.client.*;
30 import org.eclipse.team.internal.ccvs.core.resources.*;
31 import org.eclipse.team.internal.ccvs.core.util.KnownRepositories;
32 import org.osgi.service.prefs.BackingStoreException;
33 import org.osgi.service.prefs.Preferences;
34 
35 /**
36  * This class manages a CVS repository location.
37  *
38  * It provides the mapping between connection method name and the
39  * plugged in ICunnectionMethod.
40  *
41  * It parses location strings into instances.
42  *
43  * It provides a method to open a connection to the server along
44  * with a method to validate that connections can be made.
45  *
46  * It manages its user info using the plugged in IUserAuthenticator
47  * (unless a username and password are provided as part of the creation
48  * string, in which case, no authenticator is used).
49  *
50  * Instances must be disposed of when no longer needed in order to
51  * notify the authenticator so cached properties can be cleared
52  */
53 public class CVSRepositoryLocation extends PlatformObject implements ICVSRepositoryLocation, IUserInfo {
54 	/**
55 	 * Top secure preferences node to cache CVS information
56 	 */
57 	static final private String cvsNameSegment = "/CVS/"; //$NON-NLS-1$
58 
59 	/**
60 	 * Keys determining connection information for a given server
61 	 */
62 	static final private String PASSWORD_KEY = "password"; //$NON-NLS-1$
63 	static final private String USERNAME_KEY = "login"; //$NON-NLS-1$
64 
65 	/**
66 	 * The name of the preferences node in the CVS preferences that contains
67 	 * the known repositories as its children.
68 	 */
69 	public static final String PREF_REPOSITORIES_NODE = "repositories"; //$NON-NLS-1$
70 
71 	/*
72 	 * The name of the node in the default scope that has the default settings
73 	 * for a repository.
74 	 */
75 	private static final String DEFAULT_REPOSITORY_SETTINGS_NODE = "default_repository_settings"; //$NON-NLS-1$
76 
77 	// Preference keys used to persist the state of the location
78 	public static final String PREF_LOCATION = "location"; //$NON-NLS-1$
79 	public static final String PREF_SERVER_ENCODING = "encoding"; //$NON-NLS-1$
80 
81 	// server platform constants
82 	public static final int UNDETERMINED_PLATFORM = 0;
83 	public static final int CVS_SERVER = 1;
84 	public static final int CVSNT_SERVER = 2;
85 	public static final int UNSUPPORTED_SERVER = 3;
86 	public static final int UNKNOWN_SERVER = 4;
87 
88 	// static variables for extension points
89 	private static IUserAuthenticator authenticator;
90 	private static IConnectionMethod[] pluggedInConnectionMethods = null;
91 
92 	// Locks for ensuring that authentication to a host is serialized
93 	// so that invalid passwords do not result in account lockout
94 	private static Map<String, ILock> hostLocks = new HashMap<>();
95 
96 	private IConnectionMethod method;
97 	private String user;
98 	private String password;
99 	private String host;
100 	private int port;
101 	private String root;
102 	private boolean userFixed;
103 	private boolean passwordFixed;
104 	private boolean allowCaching;
105 
106 	private int serverPlatform = UNDETERMINED_PLATFORM;
107 
108 	public static final char COLON = ':';
109 	public static final char SEMICOLON = ';';
110 	public static final char HOST_SEPARATOR = '@';
111 	public static final char PORT_SEPARATOR = '#';
112 	public static final boolean STANDALONE_MODE = (System.getProperty("eclipse.cvs.standalone")==null) ? //$NON-NLS-1$
113 		false	:(Boolean.valueOf(System.getProperty("eclipse.cvs.standalone")).booleanValue()); //$NON-NLS-1$
114 
115 	// command to start remote cvs in server mode
116 	private static final String INVOKE_SVR_CMD = "server"; //$NON-NLS-1$
117 
118 	// fields needed for caching the password
119 	public static final String INFO_PASSWORD = "org.eclipse.team.cvs.core.password";//$NON-NLS-1$
120 	public static final String INFO_USERNAME = "org.eclipse.team.cvs.core.username";//$NON-NLS-1$
121 	public static final String AUTH_SCHEME = "";//$NON-NLS-1$
122 
123 	/*
124 	 * Fields used to create the EXT command invocation
125 	 */
126 	public static final String USER_VARIABLE = "{user}"; //$NON-NLS-1$
127 	public static final String PASSWORD_VARIABLE = "{password}"; //$NON-NLS-1$
128 	public static final String HOST_VARIABLE = "{host}"; //$NON-NLS-1$
129 	public static final String PORT_VARIABLE = "{port}"; //$NON-NLS-1$
130 
131 	/*
132 	 * Field that indicates which connection method is to be used for
133 	 * locations that use the EXT connection method.
134 	 */
135 	private static String extProxy;
136 
137 	/*
138 	 * Field that indicates that the last connection attempt made to
139 	 * this repository location failed due to an authentication failure.
140 	 * When this is set, subsequent attempts should prompt before attempting to connect
141 	 */
142 	private boolean previousAuthenticationFailed = false;
143 
144 	/**
145 	 * Return the preferences node whose child nodes are the know repositories
146 	 * @return a preferences node
147 	 */
getParentPreferences()148 	public static Preferences getParentPreferences() {
149 		return CVSProviderPlugin.getPlugin().getInstancePreferences().node(PREF_REPOSITORIES_NODE);
150 	}
151 
152 	/**
153 	 * Return a preferences node that contains suitable defaults for a
154 	 * repository location.
155 	 * @return  a preferences node
156 	 */
getDefaultPreferences()157 	public static Preferences getDefaultPreferences() {
158 		Preferences defaults = DefaultScope.INSTANCE.getNode(CVSProviderPlugin.ID).node(DEFAULT_REPOSITORY_SETTINGS_NODE);
159 		defaults.put(PREF_SERVER_ENCODING, getDefaultEncoding());
160 		return defaults;
161 	}
162 
getDefaultEncoding()163 	private static String getDefaultEncoding() {
164 		return System.getProperty("file.encoding", "UTF-8"); //$NON-NLS-1$ //$NON-NLS-2$
165 	}
166 
167 	/**
168 	 * Set the proxy connection method that is to be used when a
169 	 * repository location has the ext connection method. This is
170 	 * usefull with the extssh connection method as it can be used to
171 	 * keep the sandbox compatible with the command line client.
172 	 * @param string
173 	 */
setExtConnectionMethodProxy(String string)174 	public static void setExtConnectionMethodProxy(String string) {
175 		extProxy = string;
176 	}
177 
178 	/**
179 	 * Create a repository location instance from the given properties.
180 	 * The supported properties are:
181 	 *
182 	 *   connection The connection method to be used
183 	 *   user The username for the connection (optional)
184 	 *   password The password used for the connection (optional)
185 	 *   host The host where the repository resides
186 	 *   port The port to connect to (optional)
187 	 *   root The server directory where the repository is located
188 	 *   encoding The file system encoding of the server
189 	 */
fromProperties(Properties configuration)190 	public static CVSRepositoryLocation fromProperties(Properties configuration) throws CVSException {
191 		// We build a string to allow validation of the components that are provided to us
192 		String connection = configuration.getProperty("connection");//$NON-NLS-1$
193 		if (connection == null)
194 			connection = "pserver";//$NON-NLS-1$
195 		IConnectionMethod method = getPluggedInConnectionMethod(connection);
196 		if (method == null)
197 			throw new CVSException(new Status(IStatus.ERROR, CVSProviderPlugin.ID, TeamException.UNABLE, NLS.bind(CVSMessages.CVSRepositoryLocation_methods, (new Object[] {getPluggedInConnectionMethodNames()})), null));//
198 		String user = configuration.getProperty("user");//$NON-NLS-1$
199 		if (user.length() == 0)
200 			user = null;
201 		String password = configuration.getProperty("password");//$NON-NLS-1$
202 		if (user == null)
203 			password = null;
204 		String host = configuration.getProperty("host");//$NON-NLS-1$
205 		if (host == null)
206 			throw new CVSException(new Status(IStatus.ERROR, CVSProviderPlugin.ID, TeamException.UNABLE, CVSMessages.CVSRepositoryLocation_hostRequired, null));//
207 		String portString = configuration.getProperty("port");//$NON-NLS-1$
208 		int port;
209 		if (portString == null)
210 			port = ICVSRepositoryLocation.USE_DEFAULT_PORT;
211 		else
212 			port = Integer.parseInt(portString);
213 		String root = configuration.getProperty("root");//$NON-NLS-1$
214 		if (root == null)
215 			throw new CVSException(new Status(IStatus.ERROR, CVSProviderPlugin.ID, TeamException.UNABLE, CVSMessages.CVSRepositoryLocation_rootRequired, null));//
216 
217 		String encoding = configuration.getProperty("encoding"); //$NON-NLS-1$
218 
219 		return new CVSRepositoryLocation(method, user, password, host, port, root, encoding, user != null, false);
220 	}
221 
222 	/**
223 	 * Parse a location string and return a CVSRepositoryLocation.
224 	 *
225 	 * On failure, the status of the exception will be a MultiStatus
226 	 * that includes the original parsing error and a general status
227 	 * displaying the passed location and proper form. This form is
228 	 * better for logging, etc.
229 	 */
fromString(String location)230 	public static CVSRepositoryLocation fromString(String location) throws CVSException {
231 		try {
232 			return fromString(location, false);
233 		} catch (CVSException e) {
234 			// Parsing failed. Include a status that
235 			// shows the passed location and the proper form
236 			MultiStatus error = new MultiStatus(CVSProviderPlugin.ID, IStatus.ERROR, NLS.bind(CVSMessages.CVSRepositoryLocation_invalidFormat, (new Object[] {location})), null);//
237 			error.merge(new CVSStatus(IStatus.ERROR, CVSMessages.CVSRepositoryLocation_locationForm));//
238 			error.merge(e.getStatus());
239 			throw new CVSException(error);
240 		}
241 	}
242 
243 	/**
244 	 * Parse a location string and return a CVSRepositoryLocation.
245 	 *
246 	 * The valid format (from the cederqvist) is:
247 	 *
248 	 * :method:[[user][:password]@]hostname[:[port]]/path/to/repository
249 	 *
250 	 * However, this does not work with CVS on NT so we use the format
251 	 *
252 	 * :method:[user[:password]@]hostname[#port]:/path/to/repository
253 	 *
254 	 * Some differences to note:
255 	 *    The : after the host/port is not optional because of NT naming including device
256 	 *    e.g. :pserver:username:password@hostname#port:D:\cvsroot
257 	 *
258 	 * Also parse alternative format from WinCVS, which stores connection
259 	 * parameters such as username and hostname in method options:
260 	 *
261 	 * :method[;option=arg...]:other_connection_data
262 	 *
263 	 * e.g. :pserver;username=anonymous;hostname=localhost:/path/to/repository
264 	 *
265 	 * If validateOnly is true, this method will always throw an exception.
266 	 * The status of the exception indicates success or failure. The status
267 	 * of the exception contains a specific message suitable for displaying
268 	 * to a user who has knowledge of the provided location string.
269 	 * @see CVSRepositoryLocation#fromString(String)
270 	 */
fromString(String location, boolean validateOnly)271 	public static CVSRepositoryLocation fromString(String location, boolean validateOnly) throws CVSException {
272 		String errorMessage = null;
273 		try {
274 			// Get the connection method
275 			errorMessage = CVSMessages.CVSRepositoryLocation_parsingMethod;
276 			int start = location.indexOf(COLON);
277 			String methodName;
278 			int end;
279 			// For parsing alternative location format
280 			int optionStart = location.indexOf(SEMICOLON);
281 			HashMap<String, String> hmOptions = new HashMap<>();
282 
283 			if (start == 0) {
284 				end = location.indexOf(COLON, start + 1);
285 
286 				// Check for alternative location syntax
287 				if (optionStart != -1) {
288 					// errorMessage = CVSMessages.CVSRepositoryLocation_parsingMethodOptions;
289 					methodName = location.substring(start + 1, optionStart);
290 					// Save options in hash table
291 					StringTokenizer stOpt = new StringTokenizer(
292 						location.substring(optionStart+1, end),
293 								"=;" //$NON-NLS-1$
294 					);
295 					while (stOpt.hasMoreTokens()) {
296 						hmOptions.put(stOpt.nextToken(), stOpt.nextToken());
297 					}
298 					start = end + 1;
299 				} else {
300 					methodName = location.substring(start + 1, end);
301 					start = end + 1;
302 				}
303 			} else {
304 				// this could be an alternate format for ext: username:password@host:path
305 				methodName = "ext"; //$NON-NLS-1$
306 				start = 0;
307 			}
308 
309 			IConnectionMethod method = getPluggedInConnectionMethod(methodName);
310 			if (method == null)
311 				throw new CVSException(new CVSStatus(IStatus.ERROR, NLS.bind(CVSMessages.CVSRepositoryLocation_methods, (new Object[] {getPluggedInConnectionMethodNames()}))));//
312 
313 			// Get the user name and password (if provided)
314 			errorMessage = CVSMessages.CVSRepositoryLocation_parsingUser;
315 			//Since there is a @ sign in the user name so use lastIndexOf to get to the host separator @
316 			end = location.lastIndexOf(HOST_SEPARATOR, location.length());
317 			String user = null;
318 			String password = null;
319 			// if end is -1 then there is no host separator meaning that the username is not present
320 			// or set in options of alternative-style location string
321 			if (end != -1) {
322 				// Get the optional user and password
323 				user = location.substring(start, end);
324 				// Separate the user and password (if there is a password)
325 				start = user.indexOf(COLON);
326 				if (start != -1) {
327 					errorMessage = CVSMessages.CVSRepositoryLocation_parsingPassword;
328 					password = user.substring(start+1);
329 					user = user.substring(0, start);
330 				}
331 				// Set start to point after the host separator
332 				start = end + 1;
333 			} else if (optionStart != -1) {
334 				// alternative location string data
335 				// errorMessage = CVSMessages.CVSRepositoryLocation_parsingOptionsUsername;
336 				if (hmOptions.containsKey("username")) user = hmOptions.get("username").toString(); //$NON-NLS-1$ //$NON-NLS-2$
337 				// errorMessage = CVSMessages.CVSRepositoryLocation_parsingOptionsPassword;
338 				if (hmOptions.containsKey("password")) password = hmOptions.get("password").toString(); //$NON-NLS-1$ //$NON-NLS-2$
339 			}
340 
341 			// Get the host (and port)
342 			errorMessage = CVSMessages.CVSRepositoryLocation_parsingHost;
343 			end= location.indexOf(COLON, start);
344 			int hostEnd = end;
345 			if (end == -1) {
346 				// The last colon is optional so look for the slash that starts the path
347 				end = location.indexOf('/', start);
348 				hostEnd = end;
349 				// Decrement the end since the slash is part of the path
350 				if (end != -1) end--;
351 			}
352 			String host = (hmOptions.containsKey("hostname")) ? hmOptions.get("hostname").toString() : location.substring(start, hostEnd); //$NON-NLS-1$ //$NON-NLS-2$
353 			int port = USE_DEFAULT_PORT;
354 			boolean havePort = false;
355 			if (hmOptions.containsKey("port")) { //$NON-NLS-1$
356 				port = Integer.parseInt(hmOptions.get("port").toString()); //$NON-NLS-1$
357 				havePort = true;
358 			}
359 			// Separate the port and host if there is a port
360 			start = host.indexOf(PORT_SEPARATOR);
361 			if (start != -1) {
362 				try {
363 					// Initially, we used a # between the host and port
364 					errorMessage = CVSMessages.CVSRepositoryLocation_parsingPort;
365 					port = Integer.parseInt(host.substring(start+1));
366 					host = host.substring(0, start);
367 					havePort = true;
368 				} catch (NumberFormatException e) {
369 					// Ignore this as the #1234 port could be part of a proxy host string
370 				}
371 			}
372 			if (!havePort) {
373 				// In the correct CVS format, the port follows the COLON
374 				errorMessage = CVSMessages.CVSRepositoryLocation_parsingPort;
375 				int index = end;
376 				char c = location.charAt(++index);
377 				String portString = new String();
378 				while (Character.isDigit(c)) {
379 					portString += c;
380 					c = location.charAt(++index);
381 				}
382 				if (portString.length() > 0) {
383 					end = index - 1;
384 					port = Integer.parseInt(portString);
385 				}
386 			}
387 
388 			// Get the repository path (translating backslashes to slashes)
389 			errorMessage = CVSMessages.CVSRepositoryLocation_parsingRoot;
390 			start = end + 1;
391 			String root = location.substring(start);
392 
393 			if (validateOnly)
394 				throw new CVSException(new CVSStatus(IStatus.OK, CVSMessages.ok));//
395 			return new CVSRepositoryLocation(method, user, password, host, port, root, null /* encoding */, (user != null), (password != null));
396 		} catch (IndexOutOfBoundsException e) {
397 			// We'll get here if anything funny happened while extracting substrings
398 			IStatus status = new CVSStatus(IStatus.ERROR, errorMessage);
399 			throw new CVSException(status);
400 		} catch (NumberFormatException e) {
401 			IStatus status = new CVSStatus(IStatus.ERROR, errorMessage);
402 			// We'll get here if we couldn't parse a number
403 			throw new CVSException(status);
404 		}
405 	}
406 
407 	/**
408 	 * Get the plugged-in user authenticator if there is one.
409 	 * @return the plugged-in user authenticator or <code>null</code>
410 	 */
getAuthenticator()411 	public static IUserAuthenticator getAuthenticator() {
412 		if (authenticator == null) {
413 			authenticator = getPluggedInAuthenticator();
414 		}
415 		return authenticator;
416 	}
417 
418 	/**
419 	 * Return the sorted array of plugged-in connection methods.
420 	 * @return the sorted array of plugged-in connection methods
421 	 */
getPluggedInConnectionMethods()422 	public static IConnectionMethod[] getPluggedInConnectionMethods() {
423 		if(pluggedInConnectionMethods==null) {
424 			List<Object> connectionMethods = new ArrayList<>();
425 
426 			if (STANDALONE_MODE) {
427 				connectionMethods.add(new PServerConnectionMethod());
428 			} else {
429 				IExtension[] extensions = Platform.getExtensionRegistry().getExtensionPoint(CVSProviderPlugin.ID, CVSProviderPlugin.PT_CONNECTIONMETHODS).getExtensions();
430 				for (IExtension extension : extensions) {
431 					IConfigurationElement[] configs = extension.getConfigurationElements();
432 					if (configs.length == 0) {
433 						CVSProviderPlugin.log(IStatus.ERROR, NLS.bind("Connection method {0} is missing required fields", new Object[] {extension.getUniqueIdentifier()}), null);//$NON-NLS-1$
434 						continue;
435 					}
436 					try {
437 						IConfigurationElement config = configs[0];
438 						connectionMethods.add(config.createExecutableExtension("run"));//$NON-NLS-1$
439 					} catch (CoreException ex) {
440 						CVSProviderPlugin.log(IStatus.ERROR, NLS.bind("Could not instantiate connection method for  {0}", new Object[] {extension.getUniqueIdentifier()}), ex);//$NON-NLS-1$
441 					}
442 				}
443 			}
444 			IConnectionMethod[] methods = connectionMethods.toArray(new IConnectionMethod[0]);
445 			Arrays.sort(methods, (cm1, cm2) -> cm1.getName().compareTo(cm2.getName()));
446 			pluggedInConnectionMethods = methods;
447 		}
448 		return pluggedInConnectionMethods;
449 	}
450 
451 	/*
452 	 * Return the connection method registered for the given name
453 	 * or <code>null</code> if none is registered with the given name.
454 	 */
getPluggedInConnectionMethod(String methodName)455 	private static IConnectionMethod getPluggedInConnectionMethod(String methodName) {
456 		Assert.isNotNull(methodName);
457 		IConnectionMethod[] methods = getPluggedInConnectionMethods();
458 		for (IConnectionMethod m : methods) {
459 			if (methodName.equals(m.getName())) {
460 				return m;
461 			}
462 		}
463 		return null;
464 	}
465 
466 	/*
467 	 * Return a string containing a list of all connection methods
468 	 * that is suitable for inclusion in an error message.
469 	 */
getPluggedInConnectionMethodNames()470 	private static String getPluggedInConnectionMethodNames() {
471 		IConnectionMethod[] methods = getPluggedInConnectionMethods();
472 		StringBuilder methodNames = new StringBuilder();
473 		for(int i=0; i<methods.length; i++) {
474 			String name = methods[i].getName();
475 			if (i>0)
476 				methodNames.append(", ");//$NON-NLS-1$
477 			methodNames.append(name);
478 		}
479 		return methodNames.toString();
480 	}
481 
482 	/*
483 	 * Get the pluged-in authenticator from the plugin manifest.
484 	 */
getPluggedInAuthenticator()485 	private static IUserAuthenticator getPluggedInAuthenticator() {
486 		IExtension[] extensions = Platform.getExtensionRegistry().getExtensionPoint(CVSProviderPlugin.ID, CVSProviderPlugin.PT_AUTHENTICATOR).getExtensions();
487 		if (extensions.length == 0)
488 			return null;
489 		IExtension extension = extensions[0];
490 		IConfigurationElement[] configs = extension.getConfigurationElements();
491 		if (configs.length == 0) {
492 			CVSProviderPlugin.log(IStatus.ERROR, NLS.bind("User autheticator {0} is missing required fields", (new Object[] {extension.getUniqueIdentifier()})), null);//$NON-NLS-1$
493 			return null;
494 		}
495 		try {
496 			IConfigurationElement config = configs[0];
497 			return (IUserAuthenticator) config.createExecutableExtension("run");//$NON-NLS-1$
498 		} catch (CoreException ex) {
499 			CVSProviderPlugin.log(IStatus.ERROR, NLS.bind("Unable to instantiate user authenticator {0}", (new Object[] {extension.getUniqueIdentifier()})), ex);//$NON-NLS-1$
500 			return null;
501 		}
502 	}
503 
504 	/*
505 	 * Create a CVSRepositoryLocation from its composite parts.
506 	 */
CVSRepositoryLocation(IConnectionMethod method, String user, String password, String host, int port, String root, String encoding, boolean userFixed, boolean passwordFixed)507 	private CVSRepositoryLocation(IConnectionMethod method, String user, String password, String host, int port, String root, String encoding, boolean userFixed, boolean passwordFixed) {
508 		this.method = method;
509 		this.user = user;
510 		this.password = password;
511 		this.host = host;
512 		this.port = port;
513 		this.root = root;
514 		// The username can be fixed only if one is provided
515 		if (userFixed && (user != null))
516 			this.userFixed = true;
517 		// The password can only be fixed if the username is and a password is provided
518 		if (userFixed && passwordFixed && (password != null))
519 			this.passwordFixed = true;
520 		if (encoding != null) {
521 			setEncoding(encoding);
522 		}
523 	}
524 
525 	/*
526 	 * Create the connection to the remote server.
527 	 * If anything fails, an exception will be thrown and must
528 	 * be handled by the caller.
529 	 */
createConnection(String password, IProgressMonitor monitor)530 	private Connection createConnection(String password, IProgressMonitor monitor) throws CVSException {
531 		IConnectionMethod methodToUse = method;
532 		if (method.getName().equals("ext") && extProxy != null && !extProxy.equals(method.getName())) { //$NON-NLS-1$
533 			methodToUse = getPluggedInConnectionMethod(extProxy);
534 		}
535 		Connection connection = new Connection(this, methodToUse.createConnection(this, password));
536 		connection.open(monitor);
537 		return connection;
538 	}
539 
540 	/*
541 	 * Dispose of the receiver by clearing any cached authorization information.
542 	 * This method should only be invoked when the corresponding adapter is shut
543 	 * down or a connection is being validated.
544 	 */
dispose()545 	public void dispose() {
546 		removeNode();
547 		try {
548 			if (hasPreferences()) {
549 				internalGetPreferences().removeNode();
550 				getParentPreferences().flush();
551 			}
552 		} catch (BackingStoreException e) {
553 			CVSProviderPlugin.log(IStatus.ERROR, NLS.bind(CVSMessages.CVSRepositoryLocation_73, new String[] { getLocation(true) }), e);
554 		}
555 	}
556 
557 	/*
558 	 * Clear and flush the keyring entry associated with the receiver
559 	 */
removeNode()560 	private void removeNode() {
561 		ISecurePreferences node = getCVSNode();
562 		if (node == null)
563 			return;
564 		try {
565 			node.clear();
566 			node.flush(); // save immediately
567 		} catch (IllegalStateException e) {
568 			CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
569 		} catch (IOException e) {
570 			CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
571 		}
572 	}
573 
574 	/*
575 	 * @see ICVSRepositoryLocation#getHost()
576 	 */
getHost()577 	public String getHost() {
578 		return host;
579 	}
580 
581 	/*
582 	 * @see IRepositoryLocation#getLocation()
583 	 *
584 	 * The username is included if it is fixed.
585 	 * The password is never included even if it is fixed.
586 	 * The port is included if it is not the default port.
587 	 */
getLocation()588 	public String getLocation() {
589 		return getLocation(false);
590 	}
591 
getLocation(boolean forDisplay)592 	public String getLocation(boolean forDisplay) {
593 		return COLON + method.getName() + COLON +
594 			(userFixed?(user +
595 				((passwordFixed && !forDisplay)?(COLON + password):"")//$NON-NLS-1$
596 					+ HOST_SEPARATOR):"") +//$NON-NLS-1$
597 			host + COLON +
598 			((port == USE_DEFAULT_PORT)?"":(Integer.valueOf(port).toString())) + //$NON-NLS-1$
599 			root;
600 	}
601 
602 	@Override
getMethod()603 	public IConnectionMethod getMethod() {
604 		return method;
605 	}
606 
607 	@Override
getPort()608 	public int getPort() {
609 		return port;
610 	}
611 
612 	@Override
getEncoding()613 	public String getEncoding() {
614 		if (hasPreferences()) {
615 			return internalGetPreferences().get(PREF_SERVER_ENCODING, getDefaultEncoding());
616 		} else {
617 			return getDefaultEncoding();
618 		}
619 	}
620 
621 	@Override
setEncoding(String encoding)622 	public void setEncoding(String encoding) {
623 		if (encoding == null || encoding == getDefaultEncoding()) {
624 			if (hasPreferences()) {
625 				internalGetPreferences().remove(PREF_SERVER_ENCODING);
626 			}
627 		} else {
628 			ensurePreferencesStored();
629 			internalGetPreferences().put(PREF_SERVER_ENCODING, encoding);
630 			flushPreferences();
631 		}
632 	}
633 
634 	@Override
members(CVSTag tag, boolean modules, IProgressMonitor progress)635 	public ICVSRemoteResource[] members(CVSTag tag, boolean modules, IProgressMonitor progress) throws CVSException {
636 		try {
637 			if (modules) {
638 				return RemoteModule.getRemoteModules(this, tag, progress);
639 			} else {
640 				RemoteFolder root = new RemoteFolder(null, this, ICVSRemoteFolder.REPOSITORY_ROOT_FOLDER_NAME, tag);
641 				ICVSRemoteResource[] resources = root.members(progress);
642 				// There is the off chance that there is a file in the root of the repository.
643 				// This is not supported by cvs so we need to make sure there are no files
644 				List<ICVSRemoteResource> folders = new ArrayList<>(resources.length);
645 				for (ICVSRemoteResource remoteResource : resources) {
646 					if (remoteResource.isContainer()) {
647 						folders.add(remoteResource);
648 					}
649 				}
650 				return folders.toArray(new ICVSRemoteResource[folders.size()]);
651 			}
652 		} catch (CVSException e){
653 			// keep current CVSException
654 			throw e;
655 		} catch(TeamException e1) {
656 			throw new CVSException(e1.getStatus());
657 		}
658 	}
659 
660 	@Override
getRemoteFolder(String remotePath, CVSTag tag)661 	public ICVSRemoteFolder getRemoteFolder(String remotePath, CVSTag tag) {
662 		return new RemoteFolder(null, this, remotePath, tag);
663 	}
664 
665 	@Override
getRemoteFile(String remotePath, CVSTag tag)666 	public ICVSRemoteFile getRemoteFile(String remotePath, CVSTag tag) {
667 		IPath path = new Path(null, remotePath);
668 		RemoteFolderTree remoteFolder = new RemoteFolderTree(null, this, path.removeLastSegments(1).toString(), tag);
669 		RemoteFile remoteFile = new RemoteFile(remoteFolder, Update.STATE_ADDED_LOCAL, path.lastSegment(), null, null, tag);
670 		remoteFolder.setChildren(new ICVSRemoteResource[] { remoteFile });
671 		return remoteFile;
672 	}
673 
674 	@Override
getRootDirectory()675 	public String getRootDirectory() {
676 		return root;
677 	}
678 
679 	/*
680 	 * @see ICVSRepositoryLocation#getTimeout()
681 	 *
682 	 * For the time being, the timeout value is a system wide value
683 	 * associated with the CVSPlugin singleton.
684 	 */
getTimeout()685 	public int getTimeout() {
686 		return CVSProviderPlugin.getPlugin().getTimeout();
687 	}
688 
689 	@Override
getUserInfo(boolean makeUsernameMutable)690 	public IUserInfo getUserInfo(boolean makeUsernameMutable) {
691 		return new UserInfo(getUsername(), password, makeUsernameMutable ? true : isUsernameMutable());
692 	}
693 
694 	@Override
getUsername()695 	public String getUsername() {
696 		// If the username is mutable, get it from the cache if it's there
697 		if (user == null && isUsernameMutable()) {
698 			retrieveUsername();
699 		}
700 		return user == null ? "" : user; //$NON-NLS-1$
701 	}
702 
703 	@Override
isUsernameMutable()704 	public boolean isUsernameMutable() {
705 		return !userFixed;
706 	}
707 
708 	/*
709 	 * Open a connection to the repository represented by the receiver.
710 	 * If the username or password are not fixed, openConnection will
711 	 * use the plugged-in authenticator to prompt for the username and/or
712 	 * password if one has not previously been provided or if the previously
713 	 * supplied username and password are invalid.
714 	 *
715 	 * This method is synchronized to ensure that authentication with the
716 	 * remote server is serialized. This is needed to avoid the situation where
717 	 * multiple failed authentications occur and result in the remote account
718 	 * being locked. The CVSProviderPlugin enforces that there is one instance
719 	 * of a CVSRepositoryLocation per remote location thus this method is called
720 	 * for any connection made to this remote location.
721 	 */
openConnection(IProgressMonitor monitor)722 	public Connection openConnection(IProgressMonitor monitor) throws CVSException {
723 		// Get the lock for the host to ensure that we are not connecting to the same host concurrently.
724 		Policy.checkCanceled(monitor);
725 		ILock hostLock;
726 		synchronized(hostLocks) {
727 			hostLock = hostLocks.get(getHost());
728 			if (hostLock == null) {
729 				hostLock = Job.getJobManager().newLock();
730 				hostLocks.put(getHost(), hostLock);
731 			}
732 		}
733 		try {
734 			boolean acquired = false;
735 			int count = 0;
736 			int timeout = CVSProviderPlugin.getPlugin().getTimeout();
737 			while (!acquired) {
738 				try {
739 					acquired = hostLock.acquire(1000);
740 				} catch (InterruptedException e) {
741 					// Ignore
742 				}
743 				if (timeout > 0 && count > timeout) {
744 					throw new CVSCommunicationException(NLS.bind(CVSMessages.CVSRepositoryLocation_72, getHost()));
745 				}
746 				count++;
747 				Policy.checkCanceled(monitor);
748 			}
749 			// Allow two ticks in case of a retry
750 			monitor.beginTask(NLS.bind(CVSMessages.CVSRepositoryLocation_openingConnection, new String[] { getHost() }), 2);
751 			ensureLocationCached();
752 			boolean cacheNeedsUpdate = false;
753 			// If the previous connection failed, prompt before attempting to connect
754 			if (previousAuthenticationFailed) {
755 				promptForUserInfo(null);
756 				// The authentication information has been change so update the cache
757 				cacheNeedsUpdate = true;
758 			}
759 			while (true) {
760 				try {
761 					// The following will throw an exception if authentication fails
762 					String password = this.password;
763 					if (password == null) {
764 						// If the instance has no password, obtain it from the cache
765 						password = retrievePassword();
766 					}
767 					if (user == null) {
768 						// This is possible if the cache was cleared somehow for a location with a mutable username
769 						throw new CVSAuthenticationException(CVSMessages.CVSRepositoryLocation_usernameRequired, CVSAuthenticationException.RETRY, this, null);
770 					}
771 					//if (password == null)
772 					//	password = "";//$NON-NLS-1$
773 					Connection connection = createConnection(password, monitor);
774 					if (cacheNeedsUpdate)
775 						updateCachedLocation();
776 					previousAuthenticationFailed = false;
777 					return connection;
778 				} catch (CVSAuthenticationException ex) {
779 					previousAuthenticationFailed = true;
780 					if (ex.getRetryStatus() == CVSAuthenticationException.RETRY) {
781 						String message = ex.getMessage();
782 						promptForUserInfo(message);
783 						// The authentication information has been change so update the cache
784 						cacheNeedsUpdate = true;
785 					} else {
786 						throw ex;
787 					}
788 				}
789 			}
790 		} finally {
791 			hostLock.release();
792 			monitor.done();
793 		}
794 	}
795 
796 	/*
797 	 * Prompt for the user authentication information (i.e. user name and password).
798 	 */
promptForUserInfo(String message)799 	private void promptForUserInfo(String message) throws CVSException {
800 		IUserAuthenticator authenticator = getAuthenticator();
801 		if (authenticator == null) {
802 			throw new CVSAuthenticationException(CVSMessages.CVSRepositoryLocation_noAuthenticator, CVSAuthenticationException.NO_RETRY,this);//
803 		}
804 		authenticator.promptForUserInfo(this, this, message);
805 	}
806 
807 	/*
808 	 * Ensure that this location is in the known repositories list
809 	 * and that the authentication information matches what is in the
810 	 * cache, if this instance is not the instance in the cache.
811 	 */
ensureLocationCached()812 	private void ensureLocationCached() {
813 		String location = getLocation();
814 		KnownRepositories repositories = KnownRepositories.getInstance();
815 		if (repositories.isKnownRepository(location)) {
816 			try {
817 				// The repository is already known.
818 				// Ensure that the authentication information of this
819 				// location matches that of the known location
820 				setAuthenticationInformation((CVSRepositoryLocation)repositories.getRepository(location));
821 			} catch (CVSException e) {
822 				// Log the exception and continue
823 				CVSProviderPlugin.log(e);
824 			}
825 		} else {
826 			// The repository is not known so record it so any authentication
827 			// information the user may provide is remembered
828 			repositories.addRepository(this, true /* broadcast */);
829 		}
830 	}
831 
832 	/*
833 	 * Set the authentication information of this instance such that it matches the
834 	 * provided instances.
835 	 */
setAuthenticationInformation(CVSRepositoryLocation other)836 	private void setAuthenticationInformation(CVSRepositoryLocation other) {
837 		if (other != this) {
838 			// The instances differ so copy from the other location to this one
839 			if (other.getUserInfoCached()) {
840 				// The user info is cached for the other instance
841 				// so null all the values in this instance so the
842 				// information is obtained from the cache
843 				this.allowCaching = true;
844 				if (!userFixed) this.user = null;
845 				if (!passwordFixed) this.password = null;
846 			} else {
847 				// The user info is not cached for the other instance so
848 				// copy the authentication information into this instance
849 				setAllowCaching(false); /* this will clear any cached values */
850 				// Only copy the username and password if they are not fixed.
851 				// (If they are fixed, they would be included in the location
852 				// identifier and therefore must already match)
853 				if (!other.userFixed)
854 					this.user = other.user;
855 				if (!other.passwordFixed)
856 					this.password = other.password;
857 			}
858 		}
859 	}
860 
861 	/*
862 	 * The connection was successfully made. Update the cached
863 	 * repository location if it is a different instance than
864 	 * this location.
865 	 */
updateCachedLocation()866 	private void updateCachedLocation() {
867 		try {
868 			CVSRepositoryLocation known = (CVSRepositoryLocation)KnownRepositories.getInstance().getRepository(getLocation());
869 			known.setAuthenticationInformation(this);
870 		} catch (CVSException e) {
871 			// Log the exception and continue
872 			CVSProviderPlugin.log(e);
873 		}
874 	}
875 
876 	/*
877 	 * Implementation of inherited toString()
878 	 */
toString()879 	public String toString() {
880 		return getLocation(true);
881 	}
882 
equals(Object o)883 	public boolean equals(Object o) {
884 		if (this == o) return true;
885 		if (!(o instanceof CVSRepositoryLocation)) return false;
886 		return getLocation().equals(((CVSRepositoryLocation)o).getLocation());
887 	}
hashCode()888 	public int hashCode() {
889 		return getLocation().hashCode();
890 	}
891 
892 	/*
893 	 * Set the username of the receiver if the username is mutable. Return the
894 	 * username from the keyring if available.
895 	 */
retrieveUsername()896 	private String retrieveUsername() {
897 		ISecurePreferences node = getCVSNode();
898 		if (node == null)
899 			return null;
900 		try {
901 			String username = node.get(USERNAME_KEY, null);
902 			if (username != null && isUsernameMutable())
903 				setUsername(username);
904 			return username;
905 		} catch (StorageException e) { // most likely invalid keyring password or corrupted data
906 			CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
907 		}
908 		return null;
909 	}
910 
911 	/*
912 	 * Return the cached password from the keyring.
913 	 * Also, set the username of the receiver if the username is mutable
914 	 */
retrievePassword()915 	private String retrievePassword() {
916 		ISecurePreferences node = getCVSNode();
917 		if (node == null)
918 			return null;
919 		try {
920 			retrieveUsername();
921 			return node.get(PASSWORD_KEY, null);
922 		} catch (StorageException e) { // most likely invalid keyring password or corrupted data
923 			CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
924 		}
925 		return null;
926 	}
927 	@Override
setPassword(String password)928 	public void setPassword(String password) {
929 		if (passwordFixed)
930 			throw new UnsupportedOperationException();
931 		// We set the password here but it will be cleared
932 		// if the user info is cached using updateCache()
933 		this.password = password;
934 		// The password has been changed, reset the flag, so we won't
935 		// prompt before attempting to connect
936 		previousAuthenticationFailed = false;
937 	}
938 
939 	@Override
setUsername(String user)940 	public void setUsername(String user) {
941 		if (userFixed)
942 			throw new UnsupportedOperationException();
943 		this.user = user;
944 	}
945 
setUserMuteable(boolean muteable)946 	public void setUserMuteable(boolean muteable) {
947 		userFixed = !muteable;
948 	}
949 
setAllowCaching(boolean value)950 	public void setAllowCaching(boolean value) {
951 		allowCaching = value;
952 		if (allowCaching) {
953 			updateCache();
954 		} else {
955 			if (password == null)
956 				password = retrievePassword();
957 			removeNode();
958 		}
959 	}
960 
updateCache()961 	public void updateCache() {
962 		// Nothing to cache if the password is fixed
963 		if (passwordFixed || ! allowCaching) return;
964 		// Nothing to cache if the password is null and the user is fixed
965 		if (password == null && userFixed) return;
966 		if (updateCache(user, password)) {
967 			// If the cache was updated, null the password field
968 			// so we will obtain the password from the cache when needed
969 			password = null;
970 		}
971 		ensurePreferencesStored();
972 	}
973 
974 	/*
975 	 * Cache the user info in the keyring. Return true if the operation
976 	 * succeeded and false otherwise. If an error occurs, it will be logged.
977 	 */
updateCache(String username, String password)978 	private boolean updateCache(String username, String password) {
979 		ISecurePreferences node = getCVSNode();
980 		if (node == null)
981 			return false;
982 		try {
983 			node.put(USERNAME_KEY, username, false);
984 			node.put(PASSWORD_KEY, password, true);
985 			node.flush();
986 		} catch (StorageException e) {
987 			CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
988 			return false;
989 		} catch (IOException e) {
990 			CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
991 			return false;
992 		}
993 		return true;
994 	}
995 
996 	/*
997 	 * Validate that the receiver contains valid information for
998 	 * making a connection. If the receiver contains valid
999 	 * information, the method returns. Otherwise, an exception
1000 	 * indicating the problem is throw.
1001 	 */
validateConnection(IProgressMonitor monitor)1002 	public void validateConnection(IProgressMonitor monitor) throws CVSException {
1003 		try {
1004 			monitor = Policy.monitorFor(monitor);
1005 			monitor.beginTask(null, 100);
1006 			ICVSFolder root = CVSWorkspaceRoot.getCVSFolderFor(ResourcesPlugin.getWorkspace().getRoot());
1007 			Session session = new Session(this, root, false /* output to console */);
1008 			session.open(Policy.subMonitorFor(monitor, 50), false /* read-only */);
1009 			try {
1010 				IStatus status = Command.VERSION.execute(session, this, Policy.subMonitorFor(monitor, 50));
1011 				// Log any non-ok status
1012 				if (! status.isOK()) {
1013 					CVSProviderPlugin.log(status);
1014 				}
1015 			} finally {
1016 				session.close();
1017 				monitor.done();
1018 			}
1019 		} catch (CVSException e) {
1020 			// If the validation failed, dispose of any cached info
1021 			dispose();
1022 			throw e;
1023 		}
1024 	}
1025 
1026 	/**
1027 	 * Return the server platform type. It will be one of the following:
1028 	 *		UNDETERMINED_PLATFORM: The platform has not been determined
1029 	 *		CVS_SERVER: The platform is regular CVS server
1030 	 *		CVSNT_SERVER: The platform in CVSNT
1031 	 * If UNDETERMINED_PLATFORM is returned, the platform can be determined
1032 	 * using the Command.VERSION command.
1033 	 */
getServerPlatform()1034 	public int getServerPlatform() {
1035 		return serverPlatform;
1036 	}
1037 
1038 	/**
1039 	 * This method is called from Command.VERSION to set the platform type.
1040 	 */
setServerPlaform(int serverType)1041 	public void setServerPlaform(int serverType) {
1042 		// Second, check the code of the status itself to see if it is NT
1043 		switch (serverType) {
1044 			case CVS_SERVER:
1045 			case CVSNT_SERVER:
1046 			case UNKNOWN_SERVER:
1047 			case UNSUPPORTED_SERVER:
1048 				serverPlatform = serverType;
1049 				break;
1050 			default:
1051 				// We had an error status with no info about the server.
1052 				// Mark it as undetermined.
1053 				serverPlatform = UNDETERMINED_PLATFORM;
1054 		}
1055 	}
1056 
1057 	@Override
flushUserInfo()1058 	public void flushUserInfo() {
1059 		removeNode();
1060 	}
1061 
1062 	/*
1063 	 * Return the command string that is to be used by the EXT connection method.
1064 	 */
getExtCommand(String password)1065 	String[] getExtCommand(String password) throws IOException {
1066 		// Get the user specified connection parameters
1067 		String CVS_RSH = CVSProviderPlugin.getPlugin().getCvsRshCommand();
1068 		String CVS_RSH_PARAMETERS = CVSProviderPlugin.getPlugin().getCvsRshParameters();
1069 		String CVS_SERVER = CVSProviderPlugin.getPlugin().getCvsServer();
1070 		if(CVS_RSH == null || CVS_SERVER == null) {
1071 			throw new IOException(CVSMessages.EXTServerConnection_varsNotSet);
1072 		}
1073 
1074 		// If there is only one token, assume it is the command and use the default parameters and order
1075 		if (CVS_RSH_PARAMETERS == null || CVS_RSH_PARAMETERS.length() == 0) {
1076 			if (port != USE_DEFAULT_PORT)
1077 				throw new IOException(CVSMessages.EXTServerConnection_invalidPort);
1078 			return new String[] {CVS_RSH, host, "-l", user, CVS_SERVER, INVOKE_SVR_CMD}; //$NON-NLS-1$
1079 		}
1080 
1081 		// Substitute any variables for their appropriate values
1082 		CVS_RSH_PARAMETERS = stringReplace(CVS_RSH_PARAMETERS, USER_VARIABLE, user);
1083 		CVS_RSH_PARAMETERS = stringReplace(CVS_RSH_PARAMETERS, PASSWORD_VARIABLE, password);
1084 		CVS_RSH_PARAMETERS = stringReplace(CVS_RSH_PARAMETERS, HOST_VARIABLE, host);
1085 		CVS_RSH_PARAMETERS = stringReplace(CVS_RSH_PARAMETERS, PORT_VARIABLE, Integer.valueOf(port).toString());
1086 
1087 		// Build the command list to be sent to the OS.
1088 		List<String> commands = new ArrayList<>();
1089 		commands.add(CVS_RSH);
1090 		StringTokenizer tokenizer = new StringTokenizer(CVS_RSH_PARAMETERS);
1091 		while (tokenizer.hasMoreTokens()) {
1092 			String next = tokenizer.nextToken();
1093 			commands.add(next);
1094 		}
1095 		commands.add(CVS_SERVER);
1096 		commands.add(INVOKE_SVR_CMD);
1097 		return commands.toArray(new String[commands.size()]);
1098 	}
1099 
1100 	/*
1101 	 * Replace all occurrences of oldString with newString
1102 	 */
stringReplace(String string, String oldString, String newString)1103 	private String stringReplace(String string, String oldString, String newString) {
1104 		int index = string.toLowerCase().indexOf(oldString);
1105 		if (index == -1) return string;
1106 		return stringReplace(
1107 			string.substring(0, index) + newString + string.substring(index + oldString.length()),
1108 			oldString, newString);
1109 	}
1110 
1111 	/**
1112 	 * Return the server message with the prefix removed.
1113 	 * Server aborted messages typically start with
1114 	 *    "cvs server: ..."
1115 	 *    "cvs [server aborted]: ..."
1116 	 *    "cvs rtag: ..."
1117 	 */
getServerMessageWithoutPrefix(String errorLine, String prefix)1118 	public String getServerMessageWithoutPrefix(String errorLine, String prefix) {
1119 		String message = errorLine;
1120 		int firstSpace = message.indexOf(' ');
1121 		if(firstSpace != -1) {
1122 			// remove the program name and the space
1123 			message = message.substring(firstSpace + 1);
1124 			// Quick fix to handle changes in server message format (see Bug 45138)
1125 			if (prefix.startsWith("[")) { //$NON-NLS-1$
1126 				// This is the server aborted message
1127 				// Remove the pattern "[command_name aborted]: "
1128 				int closingBracket = message.indexOf("]: "); //$NON-NLS-1$
1129 				if (closingBracket == -1) return null;
1130 				// get what is inside the brackets
1131 				String realPrefix = message.substring(1, closingBracket);
1132 				// check that there is two words and the second word is "aborted"
1133 				int space = realPrefix.indexOf(' ');
1134 				if (space == -1) return null;
1135 				if (realPrefix.indexOf(' ', space +1) != -1) return null;
1136 				if (!realPrefix.substring(space +1).equals("aborted")) return null; //$NON-NLS-1$
1137 				// It's a match, return the rest of the line
1138 				message = message.substring(closingBracket + 2);
1139 				if (message.charAt(0) == ' ') {
1140 					message = message.substring(1);
1141 				}
1142 				return message;
1143 			} else {
1144 				// This is the server command message
1145 				// Remove the pattern "command_name: "
1146 				int colon = message.indexOf(": "); //$NON-NLS-1$
1147 				if (colon == -1) return null;
1148 				// get what is before the colon
1149 				String realPrefix = message.substring(0, colon);
1150 				// ensure that it is a single word
1151 				if (realPrefix.indexOf(' ') != -1) return null;
1152 				message = message.substring(colon + 1);
1153 				if (message.charAt(0) == ' ') {
1154 					message = message.substring(1);
1155 				}
1156 				return message;
1157 			}
1158 		}
1159 		// This is not a server message with the desired prefix
1160 		return null;
1161 	}
1162 
1163 	@Override
getUserAuthenticator()1164 	public IUserAuthenticator getUserAuthenticator() {
1165 		return getAuthenticator();
1166 	}
1167 
1168 	@Override
setUserAuthenticator(IUserAuthenticator authenticator)1169 	public void setUserAuthenticator(IUserAuthenticator authenticator) {
1170 		CVSRepositoryLocation.authenticator = authenticator;
1171 	}
1172 
1173 	/*
1174 	 * Return the preferences node for this repository
1175 	 */
getPreferences()1176 	public Preferences getPreferences() {
1177 		if (!hasPreferences()) {
1178 			ensurePreferencesStored();
1179 		}
1180 		return internalGetPreferences();
1181 	}
1182 
internalGetPreferences()1183 	private Preferences internalGetPreferences() {
1184 		return getParentPreferences().node(getPreferenceName());
1185 	}
1186 
hasPreferences()1187 	private boolean hasPreferences() {
1188 		try {
1189 			return getParentPreferences().nodeExists(getPreferenceName());
1190 		} catch (BackingStoreException e) {
1191 			CVSProviderPlugin.log(IStatus.ERROR, NLS.bind(CVSMessages.CVSRepositoryLocation_74, new String[] { getLocation(true) }), e);
1192 			return false;
1193 		}
1194 	}
1195 
1196 	/**
1197 	 * Return a unique name that identifies this location but
1198 	 * does not contain any slashes (/). Also, do not use ':'.
1199 	 * Although a valid path character, the initial core implementation
1200 	 * didn't handle it well.
1201 	 */
getPreferenceName()1202 	private String getPreferenceName() {
1203 		return getLocation().replace('/', '%').replace(':', '%');
1204 	}
1205 
storePreferences()1206 	public void storePreferences() {
1207 		Preferences prefs = internalGetPreferences();
1208 		// Must store at least one preference in the node
1209 		prefs.put(PREF_LOCATION, getLocation());
1210 		flushPreferences();
1211 	}
1212 
flushPreferences()1213 	private void flushPreferences() {
1214 		try {
1215 			internalGetPreferences().flush();
1216 		} catch (BackingStoreException e) {
1217 			CVSProviderPlugin.log(IStatus.ERROR, NLS.bind(CVSMessages.CVSRepositoryLocation_75, new String[] { getLocation(true) }), e);
1218 		}
1219 	}
1220 
ensurePreferencesStored()1221 	private void ensurePreferencesStored() {
1222 		if (!hasPreferences()) {
1223 			storePreferences();
1224 		}
1225 	}
1226 
1227 	@Override
getUserInfoCached()1228 	public boolean getUserInfoCached() {
1229 		ISecurePreferences node = getCVSNode();
1230 		if (node == null)
1231 			return false;
1232 		try {
1233 			String password = node.get(PASSWORD_KEY, null);
1234 			return (password != null);
1235 		} catch (StorageException e) { // most likely invalid keyring password or corrupted data
1236 			CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
1237 		}
1238 		return false;
1239 	}
1240 
1241 	/**
1242 	 * At this time information is saved in a simplistic flat form. In future, this
1243 	 * can be modified into a hierarchy of storing information in "connections"
1244 	 * where "connection" would combine "server" and "account" information (allowing
1245 	 * user to have the same password for different connections on the server).
1246 	 *
1247 	 * Hopefully, we'll get some simplified notion of "account" from Higgins into Equinox
1248 	 * and then we'll be able to re-use it.
1249 	 *
1250 	 * For now, the structure is rather simple:
1251 	 * node: "CVS" 										"/CVS/"
1252 	 * 		node: account_name							name combines all attributes
1253 	 * 			| value: login
1254 	 * 			| value: password
1255 	 */
getCVSNode()1256 	private ISecurePreferences getCVSNode() {
1257 		ISecurePreferences preferences = SecurePreferencesFactory.getDefault();
1258 		if (preferences == null)
1259 			return null;
1260 		String accountName = EncodingUtils.encodeSlashes(getLocation(true));
1261 		String path = cvsNameSegment + accountName;
1262 		try {
1263 			return preferences.node(path);
1264 		} catch (IllegalArgumentException e) {
1265 			return null; // invalid path
1266 		}
1267 	}
1268 }
1269