package hudson.scm; import hudson.FilePath; import hudson.FilePath.FileCallable; import hudson.Launcher; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Hudson; import hudson.model.TaskListener; import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; import hudson.util.FormFieldValidator; import hudson.util.IOException2; import hudson.util.MultipartFormDataParser; import hudson.util.Scrambler; import org.apache.commons.fileupload.FileItem; import org.apache.commons.io.FileUtils; import org.apache.tools.ant.Project; import org.apache.tools.ant.taskdefs.Chmod; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; import org.tmatesoft.svn.core.auth.ISVNAuthenticationProvider; import org.tmatesoft.svn.core.auth.SVNAuthentication; import org.tmatesoft.svn.core.auth.SVNPasswordAuthentication; import org.tmatesoft.svn.core.auth.SVNSSHAuthentication; import org.tmatesoft.svn.core.auth.SVNSSLAuthentication; import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory; import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory; import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl; import org.tmatesoft.svn.core.internal.wc.DefaultSVNAuthenticationManager; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNInfo; import org.tmatesoft.svn.core.wc.SVNRevision; import org.tmatesoft.svn.core.wc.SVNUpdateClient; import org.tmatesoft.svn.core.wc.SVNWCClient; import org.tmatesoft.svn.core.wc.SVNWCUtil; import javax.servlet.ServletException; import javax.xml.transform.stream.StreamResult; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.io.Serializable; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; /** * Subversion SCM. * * @author Kohsuke Kawaguchi */ public class SubversionSCM extends SCM implements Serializable { /** * the locations field is used to store all configured SVN locations (with * their local and remote part). Direct access to this filed should be * avoided and the getLocations() method should be used instead. This is * needed to make importing of old hudson-configurations possible as * getLocations() will check if the modules field has been set and import * the data. * * @since 1.91 */ private ModuleLocation[] locations = new ModuleLocation[0]; private boolean useUpdate; private String username; private final SubversionRepositoryBrowser browser; // No longer in use but left for serialization compatibility. @Deprecated private String modules; SubversionSCM(String[] remoteLocations, String[] localLocations, boolean useUpdate, String username, SubversionRepositoryBrowser browser) { List modules = new ArrayList(); if (remoteLocations != null && localLocations != null) { int entries = Math.min(remoteLocations.length, localLocations.length); for (int i = 0; i < entries; i++) { // the remote (repository) location String remoteLoc = nullify(remoteLocations[i]); if (remoteLoc != null) {// null if skipped remoteLoc = Util.removeTrailingSlash(remoteLoc.trim()); modules.add(new ModuleLocation(remoteLoc, nullify(localLocations[i]))); } } } locations = modules.toArray(new ModuleLocation[modules.size()]); this.useUpdate = useUpdate; this.username = nullify(username); this.browser = browser; } /** * @deprecated * as of 1.91. Use {@link #getLocations()} instead. */ public String getModules() { return null; } /** * list of all configured svn locations * * @since 1.91 */ public ModuleLocation[] getLocations() { // check if we've got a old location if (modules != null) { // import the old configuration List oldLocations = new ArrayList(); StringTokenizer tokens = new StringTokenizer(modules); while (tokens.hasMoreTokens()) { // the remote (repository location) // the normalized name is always without the trailing '/' String remoteLoc = Util.removeTrailingSlash(tokens.nextToken()); oldLocations.add(new ModuleLocation(remoteLoc, null)); } locations = oldLocations.toArray(new ModuleLocation[oldLocations.size()]); modules = null; } return locations; } public boolean isUseUpdate() { return useUpdate; } public String getUsername() { return username; } public SubversionRepositoryBrowser getBrowser() { return browser; } /** * Called after checkout/update has finished to compute the changelog. */ private boolean calcChangeLog(AbstractBuild build, File changelogFile, BuildListener listener, List externals) throws IOException, InterruptedException { if(build.getPreviousBuild()==null) { // nothing to compare against return createEmptyChangeLog(changelogFile, listener, "log"); } if(!new SubversionChangeLogBuilder(build,listener,this).run(externals,new StreamResult(changelogFile))) createEmptyChangeLog(changelogFile, listener, "log"); return true; } /*package*/ static Map parseRevisionFile(AbstractBuild build) throws IOException { Map revisions = new HashMap(); // module -> revision {// read the revision file of the last build File file = getRevisionFile(build); if(!file.exists()) // nothing to compare against return revisions; BufferedReader br = new BufferedReader(new FileReader(file)); String line; while((line=br.readLine())!=null) { int index = line.lastIndexOf('/'); if(index<0) { continue; // invalid line? } try { revisions.put(line.substring(0,index), Long.parseLong(line.substring(index+1))); } catch (NumberFormatException e) { // perhaps a corrupted line. ignore } } } return revisions; } /** * Parses the file that stores the locations in the workspace where modules loaded by svn:external * is placed. */ /*package*/ static List parseExternalsFile(AbstractProject project) throws IOException { List ext = new ArrayList(); // workspace-relative path {// read the revision file of the last build File file = getExternalsFile(project); if(!file.exists()) // nothing to compare against return ext; BufferedReader br = new BufferedReader(new FileReader(file)); try { String line; while((line=br.readLine())!=null) { ext.add(line); } } finally { br.close(); } } return ext; } public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspace, final BuildListener listener, File changelogFile) throws IOException, InterruptedException { List externals = checkout(workspace,listener); if(externals==null) return false; // write out the revision file PrintWriter w = new PrintWriter(new FileOutputStream(getRevisionFile(build))); try { Map revMap = buildRevisionMap(workspace,listener,externals); for (Entry e : revMap.entrySet()) { w.println( e.getKey() +'/'+ e.getValue().revision ); } } finally { w.close(); } // write out the externals info w = new PrintWriter(new FileOutputStream(getExternalsFile(build.getProject()))); try { for (String p : externals) { w.println( p ); } } finally { w.close(); } return calcChangeLog(build, changelogFile, listener, externals); } /** * Performs the checkout or update, depending on the configuration and workspace state. * * @return null * if the operation failed. Otherwise the set of local workspace paths * (relative to the workspace root) that has loaded due to svn:external. */ private List checkout(FilePath workspace, final TaskListener listener) throws IOException, InterruptedException { final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); // true to "svn update", false to "svn checkout". final boolean update = useUpdate && isUpdatable(workspace, listener); return workspace.act(new FileCallable>() { public List invoke(File ws, VirtualChannel channel) throws IOException { SVNUpdateClient svnuc = createSvnClientManager(authProvider).getUpdateClient(); List externals = new ArrayList(); // store discovered externals to here if(update) { for (ModuleLocation l : getLocations()) { try { listener.getLogger().println("Updating "+ l.remote); svnuc.setEventHandler(new SubversionUpdateEventHandler(listener, externals, l.local)); svnuc.doUpdate(new File(ws, l.local), SVNRevision.HEAD, true); } catch (SVNException e) { e.printStackTrace(listener.error("Failed to update "+l.remote)); return null; } } } else { Util.deleteContentsRecursive(ws); for (ModuleLocation l : getLocations()) { try { SVNURL url = SVNURL.parseURIEncoded(l.remote); listener.getLogger().println("Checking out "+url); svnuc.setEventHandler(new SubversionUpdateEventHandler(listener, externals, l.local)); svnuc.doCheckout(url, new File(ws, l.local), SVNRevision.HEAD, SVNRevision.HEAD, true); } catch (SVNException e) { e.printStackTrace(listener.error("Failed to check out "+l.remote)); return null; } } } return externals; } }); } /** * Creates {@link SVNClientManager}. * *

* This method must be executed on the slave where svn operations are performed. * * @param authProvider * The value obtained from {@link DescriptorImpl#createAuthenticationProvider()}. * If the operation runs on slaves, * (and properly remoted, if the svn operations run on slaves.) */ /*package*/ static SVNClientManager createSvnClientManager(ISVNAuthenticationProvider authProvider) { ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(); sam.setAuthenticationProvider(authProvider); return SVNClientManager.newInstance(SVNWCUtil.createDefaultOptions(true),sam); } public static final class SvnInfo implements Serializable { /** * Decoded repository URL. */ final String url; final long revision; public SvnInfo(String url, long revision) { this.url = url; this.revision = revision; } public SvnInfo(SVNInfo info) { this( info.getURL().toDecodedString(), info.getCommittedRevision().getNumber() ); } public SVNURL getSVNURL() throws SVNException { return SVNURL.parseURIDecoded(url); } private static final long serialVersionUID = 1L; } /** * Gets the SVN metadata for the given local workspace. * * @param workspace * The target to run "svn info". */ private SVNInfo parseSvnInfo(File workspace, ISVNAuthenticationProvider authProvider) throws SVNException { SVNWCClient svnWc = createSvnClientManager(authProvider).getWCClient(); return svnWc.doInfo(workspace,SVNRevision.WORKING); } /** * Gets the SVN metadata for the remote repository. * * @param remoteUrl * The target to run "svn info". */ private SVNInfo parseSvnInfo(SVNURL remoteUrl, ISVNAuthenticationProvider authProvider) throws SVNException { SVNWCClient svnWc = createSvnClientManager(authProvider).getWCClient(); return svnWc.doInfo(remoteUrl, SVNRevision.HEAD, SVNRevision.HEAD); } /** * Checks .svn files in the workspace and finds out revisions of the modules * that the workspace has. * * @return * null if the parsing somehow fails. Otherwise a map from the repository URL to revisions. */ private Map buildRevisionMap(FilePath workspace, final TaskListener listener, final List externals) throws IOException, InterruptedException { final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); return workspace.act(new FileCallable>() { public Map invoke(File ws, VirtualChannel channel) throws IOException { Map revisions = new HashMap(); SVNWCClient svnWc = createSvnClientManager(authProvider).getWCClient(); // invoke the "svn info" for( ModuleLocation module : getLocations() ) { try { SvnInfo info = new SvnInfo(svnWc.doInfo(new File(ws,module.local),SVNRevision.WORKING)); revisions.put(info.url,info); } catch (SVNException e) { e.printStackTrace(listener.error("Failed to parse svn info for "+module)); } } for(String local : externals){ try { SvnInfo info = new SvnInfo(svnWc.doInfo(new File(ws, local),SVNRevision.WORKING)); revisions.put(info.url,info); } catch (SVNException e) { e.printStackTrace(listener.error("Failed to parse svn info for external "+local)); } } return revisions; } }); } /** * Gets the file that stores the revision. */ private static File getRevisionFile(AbstractBuild build) { return new File(build.getRootDir(),"revision.txt"); } /** * Gets the file that stores the externals. */ private static File getExternalsFile(AbstractProject project) { return new File(project.getRootDir(),"svnexternals.txt"); } /** * Returns true if we can use "svn update" instead of "svn checkout" */ private boolean isUpdatable(FilePath workspace, final TaskListener listener) throws IOException, InterruptedException { final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); return workspace.act(new FileCallable() { public Boolean invoke(File ws, VirtualChannel channel) throws IOException { for (ModuleLocation l : getLocations()) { String url = l.remote; String moduleName = l.local; File module = new File(ws,moduleName).getCanonicalFile(); // canonicalize to remove ".." and ".". See #474 if(!module.exists()) { listener.getLogger().println("Checking out a fresh workspace because "+module+" doesn't exist"); return false; } try { SvnInfo svnInfo = new SvnInfo(parseSvnInfo(module,authProvider)); if(!svnInfo.url.equals(url)) { listener.getLogger().println("Checking out a fresh workspace because the workspace is not "+url); return false; } } catch (SVNException e) { listener.getLogger().println("Checking out a fresh workspace because Hudson failed to detect the current workspace "+module); e.printStackTrace(listener.error(e.getMessage())); return false; } } return true; } }); } public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException { // we need to load the externals info, if any List externals = parseExternalsFile(project); // current workspace revision Map wsRev = buildRevisionMap(workspace,listener,externals); ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); // check the corresponding remote revision for (SvnInfo localInfo : wsRev.values()) { try { SvnInfo remoteInfo = new SvnInfo(parseSvnInfo(localInfo.getSVNURL(),authProvider)); listener.getLogger().println("Revision:"+remoteInfo.revision); if(remoteInfo.revision > localInfo.revision) return true; // change found } catch (SVNException e) { e.printStackTrace(listener.error("Failed to check repository revision for "+localInfo.url)); } } return false; // no change } public ChangeLogParser createChangeLogParser() { return new SubversionChangeLogParser(); } public DescriptorImpl getDescriptor() { return DescriptorImpl.DESCRIPTOR; } public FilePath getModuleRoot(FilePath workspace) { if (getLocations().length > 0) return workspace.child(getLocations()[0].local); return workspace; } private static String getLastPathComponent(String s) { String[] tokens = s.split("/"); return tokens[tokens.length-1]; // return the last token } public static final class DescriptorImpl extends SCMDescriptor { public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); /** * SVN authentication realm to its associated credentials. */ private final Map credentials = new Hashtable(); /** * Stores {@link SVNAuthentication} for a single realm. * *

* {@link Credential} holds data in a persistence-friendly way, * and it's capable of creating {@link SVNAuthentication} object, * to be passed to SVNKit. */ private static abstract class Credential implements Serializable { /** * @param kind * One of the constants defined in {@link ISVNAuthenticationManager}, * indicating what subype of {@link SVNAuthentication} is expected. */ abstract SVNAuthentication createSVNAuthentication(String kind) throws SVNException; } /** * Username/password based authentication. */ private static final class PasswordCredential extends Credential { private final String userName; private final String password; // scrambled by base64 public PasswordCredential(String userName, String password) { this.userName = userName; this.password = Scrambler.scramble(password); } @Override SVNAuthentication createSVNAuthentication(String kind) { if(kind.equals(ISVNAuthenticationManager.SSH)) return new SVNSSHAuthentication(userName,Scrambler.descramble(password),-1,false); else return new SVNPasswordAuthentication(userName,Scrambler.descramble(password),false); } } /** * Publickey authentication for Subversion over SSH. */ private static final class SshPublicKeyCredential extends Credential { private final String userName; private final String passphrase; // scrambled by base64 private final String id; /** * @param keyFile * stores SSH private key. The file will be copied. */ public SshPublicKeyCredential(String userName, String passphrase, File keyFile) throws SVNException { this.userName = userName; this.passphrase = Scrambler.scramble(passphrase); Random r = new Random(); StringBuilder buf = new StringBuilder(); for(int i=0;i<16;i++) buf.append(Integer.toHexString(r.nextInt(16))); this.id = buf.toString(); try { FileUtils.copyFile(keyFile,getKeyFile()); } catch (IOException e) { throw new SVNException(SVNErrorMessage.create(SVNErrorCode.AUTHN_CREDS_UNAVAILABLE,"Unable to save private key"),e); } } /** * Gets the location where the private key will be permanently stored. */ private File getKeyFile() { File dir = new File(Hudson.getInstance().getRootDir(),"subversion-credentials"); if(dir.mkdirs()) { // make sure the directory exists. if we created it, try to set the permission to 600 // since this is sensitive information try { Chmod chmod = new Chmod(); chmod.setProject(new Project()); chmod.setFile(dir); chmod.setPerm("600"); chmod.execute(); } catch (Throwable e) { // if we failed to set the permission, that's fine. LOGGER.log(Level.WARNING, "Failed to set directory permission of "+dir,e); } } return new File(dir,id); } @Override SVNSSHAuthentication createSVNAuthentication(String kind) throws SVNException { if(kind.equals(ISVNAuthenticationManager.SSH)) { try { Channel channel = Channel.current(); String privateKey; if(channel!=null) { // remote privateKey = channel.call(new Callable() { public String call() throws IOException { return FileUtils.readFileToString(getKeyFile(),"iso-8859-1"); } }); } else { privateKey = FileUtils.readFileToString(getKeyFile(),"iso-8859-1"); } return new SVNSSHAuthentication(userName, privateKey.toCharArray(), Scrambler.descramble(passphrase),-1,false); } catch (IOException e) { throw new SVNException(SVNErrorMessage.create(SVNErrorCode.AUTHN_CREDS_UNAVAILABLE,"Unable to load private key"),e); } catch (InterruptedException e) { throw new SVNException(SVNErrorMessage.create(SVNErrorCode.AUTHN_CREDS_UNAVAILABLE,"Unable to load private key"),e); } } else return null; // unknown } } /** * SSL client certificate based authentication. */ private static final class SslClientCertificateCredential extends Credential { private final String password; // scrambled by base64 public SslClientCertificateCredential(File certificate, String password) { this.password = Scrambler.scramble(password); } @Override SVNAuthentication createSVNAuthentication(String kind) { if(kind.equals(ISVNAuthenticationManager.SSL)) return new SVNSSLAuthentication(null,Scrambler.descramble(password),false); else return null; // unexpected authentication type } } /** * Remoting interface that allows remote {@link ISVNAuthenticationProvider} * to read from local {@link DescriptorImpl#credentials}. */ private interface RemotableSVNAuthenticationProvider { Credential getCredential(String realm); } private final class RemotableSVNAuthenticationProviderImpl implements RemotableSVNAuthenticationProvider, Serializable { public Credential getCredential(String realm) { LOGGER.fine(String.format("getCredential(%s)=>%s",realm,credentials.get(realm))); return credentials.get(realm); } /** * When sent to the remote node, send a proxy. */ private Object writeReplace() { return Channel.current().export(RemotableSVNAuthenticationProvider.class, this); } } /** * See {@link DescriptorImpl#createAuthenticationProvider()}. */ private static final class SVNAuthenticationProviderImpl implements ISVNAuthenticationProvider, Serializable { private final RemotableSVNAuthenticationProvider source; public SVNAuthenticationProviderImpl(RemotableSVNAuthenticationProvider source) { this.source = source; } public SVNAuthentication requestClientAuthentication(String kind, SVNURL url, String realm, SVNErrorMessage errorMessage, SVNAuthentication previousAuth, boolean authMayBeStored) { Credential cred = source.getCredential(realm); LOGGER.fine(String.format("requestClientAuthentication(%s,%s,%s)=>%s",kind,url,realm,cred)); if(cred==null) return null; try { return cred.createSVNAuthentication(kind); } catch (SVNException e) { logger.log(Level.SEVERE, "Failed to authorize",e); throw new RuntimeException("Failed to authorize",e); } } public int acceptServerAuthentication(SVNURL url, String realm, Object certificate, boolean resultMayBeStored) { return ACCEPTED_TEMPORARY; } private static final long serialVersionUID = 1L; } private DescriptorImpl() { super(SubversionSCM.class,SubversionRepositoryBrowser.class); load(); } public String getDisplayName() { return "Subversion"; } public SCM newInstance(StaplerRequest req) throws FormException { return new SubversionSCM( req.getParameterValues("svn.location_remote"), req.getParameterValues("svn.location_local"), req.getParameter("svn_use_update") != null, req.getParameter("svn_username"), RepositoryBrowsers.createInstance(SubversionRepositoryBrowser.class, req, "svn.browser")); } /** * Creates {@link ISVNAuthenticationProvider} backed by {@link #credentials}. * This method must be invoked on the master, but the returned object is remotable. */ public ISVNAuthenticationProvider createAuthenticationProvider() { return new SVNAuthenticationProviderImpl(new RemotableSVNAuthenticationProviderImpl()); } /** * Submits the authentication info. * * This code is fairly ugly because of the way SVNKit handles credentials. */ public void doPostCredential(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { if(!Hudson.adminCheck(req,rsp)) return; MultipartFormDataParser parser = new MultipartFormDataParser(req); String url = parser.get("url"); String kind = parser.get("kind"); int idx = Arrays.asList("","password","publickey","certificate").indexOf(kind); final String username = parser.get("username"+idx); final String password = parser.get("password"+idx); // SVNKit wants a key in a file final File keyFile; FileItem item=null; if(kind.equals("password")) { keyFile = null; } else { item = parser.getFileItem(kind.equals("publickey")?"privateKey":"certificate"); keyFile = File.createTempFile("hudson","key"); if(item!=null) try { item.write(keyFile); } catch (Exception e) { throw new IOException2(e); } } try { // the way it works with SVNKit is that // 1) svnkit calls AuthenticationManager asking for a credential. // this is when we can see the 'realm', which identifies the user domain. // 2) DefaultSVNAuthenticationManager returns the username and password we set below // 3) if the authentication is successful, svnkit calls back acknowledgeAuthentication // (so we store the password info here) SVNRepository repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(url)); repository.setAuthenticationManager(new DefaultSVNAuthenticationManager(SVNWCUtil.getDefaultConfigurationDirectory(),true,username,password,keyFile,password) { Credential cred = null; @Override public SVNAuthentication getFirstAuthentication(String kind, String realm, SVNURL url) throws SVNException { if(kind.equals(ISVNAuthenticationManager.PASSWORD)) cred = new PasswordCredential(username,password); if(kind.equals(ISVNAuthenticationManager.SSH)) { if(keyFile==null) cred = new PasswordCredential(username,password); else cred = new SshPublicKeyCredential(username,password,keyFile); } if(kind.equals(ISVNAuthenticationManager.SSL)) cred = new SslClientCertificateCredential(keyFile,password); if(cred==null) return null; return cred.createSVNAuthentication(kind); } @Override public void acknowledgeAuthentication(boolean accepted, String kind, String realm, SVNErrorMessage errorMessage, SVNAuthentication authentication) throws SVNException { if(accepted) { assert cred!=null; credentials.put(realm,cred); save(); } super.acknowledgeAuthentication(accepted, kind, realm, errorMessage, authentication); } }); repository.testConnection(); rsp.sendRedirect("credentialOK"); } catch (SVNException e) { req.setAttribute("message",e.getErrorMessage()); rsp.forward(Hudson.getInstance(),"error",req); } finally { if(keyFile!=null) keyFile.delete(); if(item!=null) item.delete(); } } /** * validate the value for a remote (repository) location. */ public void doSvnRemoteLocationCheck(final StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { // this can be used to hit any accessible URL, so limit that to admins new FormFieldValidator(req, rsp, true) { protected void check() throws IOException, ServletException { // syntax check first String url = Util.nullify(request.getParameter("value")); if (url == null) { ok(); // not entered yet return; } // remove unneeded whitespaces url = url.trim(); if(!URL_PATTERN.matcher(url).matches()) { error("Invalid URL syntax. See " + "this " + "for information about valid URLs."); return; } // test the connection try { SVNRepository repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(url)); ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(); sam.setAuthenticationProvider(createAuthenticationProvider()); repository.setAuthenticationManager(sam); repository.testConnection(); ok(); } catch (SVNException e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); String message=""; message += "Unable to access "+url+" : "+Util.escape( e.getErrorMessage().getFullMessage()); message += " (show details)"; message += "

"; message += " (Maybe you need to enter credential?)"; message += "
"; logger.log(Level.INFO, "Failed to access subversion repository "+url,e); error(message); } } }.process(); } /** * validate the value for a local location (local checkout directory). */ public void doSvnLocalLocationCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { new FormFieldValidator(req, rsp, false) { protected void check() throws IOException, ServletException { String v = Util.nullify(request.getParameter("value")); if (v == null) { // local directory is optional so this is ok ok(); return; } v = v.trim(); // check if a absolute path has been supplied // (the last check with the regex will match windows drives) if (v.startsWith("/") || v.startsWith("\\") || v.startsWith("..") || v.matches("^[A-Za-z]:")) { error("absolute path is not allowed"); } // all tests passed so far ok(); } }.process(); } static { new Initializer(); } } static final Pattern URL_PATTERN = Pattern.compile("(https?|svn(\\+[a-z0-9]+)?|file)://.+"); private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(SubversionSCM.class.getName()); static { new Initializer(); } private static final class Initializer { static { DAVRepositoryFactory.setup(); // http, https SVNRepositoryFactoryImpl.setup(); // svn, svn+xxx FSRepositoryFactory.setup(); // file } } /** * small structure to store local and remote (repository) location * information of the repository. As a addition it holds the invalid field * to make failure messages when doing a checkout possible */ public static final class ModuleLocation implements Serializable { public final String remote; public final String local; public ModuleLocation(String remote, String local) { if(local==null) local = getLastPathComponent(remote); this.remote = remote.trim(); this.local = local.trim(); } private static final long serialVersionUID = 1L; } private static final Logger LOGGER = Logger.getLogger(SubversionSCM.class.getName()); }